File size: 7,320 Bytes
a8cba5c
 
 
ae887df
 
 
 
 
 
a8cba5c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
eb70c60
 
 
a8cba5c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ae887df
 
a8cba5c
e5f134b
 
 
 
 
 
49f29b0
a8cba5c
 
 
ae887df
 
 
a8cba5c
 
 
 
 
 
49f29b0
 
 
 
 
 
 
a8cba5c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ae887df
 
a8cba5c
 
 
 
 
 
ae887df
a8cba5c
 
ae887df
 
a8cba5c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
"""
ALOE image processor — standard ViT-style preprocessing with B-cos channel expansion.

B-cos models expect 6-channel input ``[x, 1-x]`` (on the **normalised** 3-channel
tensor ``x``), matching ``HFTransform`` in ``src/data/transforms/transforms.py`` when
``is_bcos=True``.  Using ``-x`` for the second half was incorrect for training parity
and broke native-Hub k-NN / linear-probe eval.  This processor wraps resize →
normalise → to-tensor and appends the second half, so the pipeline matches
ModelFactory + dataloader code paths.

The processor is serialisable, loadable with ``AutoImageProcessor.from_pretrained``,
and can be pushed to the HF Hub alongside the model.

Typical usage
-------------
::

    from transformers import AutoImageProcessor
    proc = AutoImageProcessor.from_pretrained("your-org/aloe-siglip2-base", trust_remote_code=True)
    inputs = proc(images=pil_image, return_tensors="pt")
    # inputs.pixel_values.shape == (1, 6, H, W)

    out = model(**inputs)

Building from a backbone processor (during export)
---------------------------------------------------
::

    >>> from src.models.hf.aloe import AloeImageProcessor
    >>> backbone_proc = AutoImageProcessor.from_pretrained("google/siglip2-base-patch16-224")
    >>> aloe_proc = AloeImageProcessor.from_backbone_processor(backbone_proc)
"""

from __future__ import annotations

from typing import Any, Dict, List, Optional, Union

import numpy as np
from transformers import ViTImageProcessor
from transformers.image_processing_utils import BatchFeature


_IMAGENET_MEAN = [0.485, 0.456, 0.406]
_IMAGENET_STD  = [0.229, 0.224, 0.225]


class AloeImageProcessor(ViTImageProcessor):
    """
    Image processor for all ALOE vision backbones.

    Extends :class:`~transformers.ViTImageProcessor` with an optional
    B-cos 6-channel expansion: the normalised 3-channel tensor ``x`` is expanded to
    ``torch.cat([x, 1.0 - x], dim=channel)``, same as ``HFTransform`` for B-cos training.

    Hub checkpoints produced by
    :func:`src.models.hf.aloe_models.export_native_model_to_hub` patch
    ``preprocessor_config.json`` with ``image_processor_type`` and
    ``auto_map["AutoImageProcessor"]`` so
    :class:`transformers.AutoImageProcessor` resolves the class when using
    ``trust_remote_code=True``.

    Parameters
    ----------
    expand_to_6ch : bool
        If ``True`` (default), concatenate ``[pixel_values, 1 - pixel_values]``
        along the channel dimension after preprocessing (on the normalised tensor).
        Set to ``False`` for standard 3-channel output (e.g. visualisation).
    All other parameters are forwarded to :class:`~transformers.ViTImageProcessor`
    (``do_resize``, ``size``, ``do_normalize``, ``image_mean``, ``image_std``, …).
    """

    model_input_names = ["pixel_values"]

    def to_dict(self) -> dict[str, Any]:
        """Serialise with explicit type and B-cos flag (see :meth:`~transformers.image_processing_utils.ImageProcessingMixin.to_dict`)."""
        out = super().to_dict()
        out["image_processor_type"] = self.__class__.__name__
        out["expand_to_6ch"] = self.expand_to_6ch
        return out

    def __init__(
        self,
        expand_to_6ch: bool = True,
        do_resize: bool = True,
        size: Optional[Dict[str, int]] = None,
        do_rescale: bool = True,
        rescale_factor: float = 1 / 255,
        do_normalize: bool = True,
        image_mean: Optional[List[float]] = None,
        image_std: Optional[List[float]] = None,
        do_convert_rgb: bool = True,
        **kwargs: Any,
    ) -> None:
        self.expand_to_6ch = expand_to_6ch
        super().__init__(
            do_resize=do_resize,
            size=size or {"height": 224, "width": 224},
            do_rescale=do_rescale,
            rescale_factor=rescale_factor,
            do_normalize=do_normalize,
            image_mean=image_mean if image_mean is not None else _IMAGENET_MEAN,
            image_std=image_std if image_std is not None else _IMAGENET_STD,
            do_convert_rgb=do_convert_rgb,
            **kwargs,
        )

    def preprocess(
        self,
        images: Union[Any, List[Any]],
        *,
        return_tensors: Optional[str] = None,
        **kwargs: Any,
    ) -> BatchFeature:
        """
        Preprocess one or more images.

        Delegates to :class:`~transformers.ViTImageProcessor` for all
        standard steps, then — if ``expand_to_6ch=True`` — appends ``(1 - pv)``
        (same convention as ``HFTransform`` with ``is_bcos=True``).
        """
        result = super().preprocess(images, return_tensors=return_tensors, **kwargs)

        if self.expand_to_6ch and "pixel_values" in result:
            pv = result["pixel_values"]
            if isinstance(pv, np.ndarray):
                result["pixel_values"] = np.concatenate([pv, 1.0 - pv], axis=1)
            else:
                import torch

                result["pixel_values"] = torch.cat([pv, 1.0 - pv], dim=1)

        return result

    # ------------------------------------------------------------------
    # Factory helpers
    # ------------------------------------------------------------------

    @classmethod
    def from_backbone_processor(
        cls,
        backbone_processor: Any,
        *,
        expand_to_6ch: bool = True,
    ) -> "AloeImageProcessor":
        """
        Build an :class:`AloeImageProcessor` by copying preprocessing
        parameters from an existing backbone processor (e.g. the one loaded
        by ``AutoImageProcessor.from_pretrained("google/siglip2-base-…")``).

        Parameters
        ----------
        backbone_processor :
            Any HF image processor with ``image_mean``, ``image_std``, and
            (optionally) a ``size`` or ``crop_size`` attribute.
        expand_to_6ch :
            Whether to append the 6-channel expansion step (default ``True``).
        """
        mean = getattr(backbone_processor, "image_mean", _IMAGENET_MEAN)
        std  = getattr(backbone_processor, "image_std",  _IMAGENET_STD)

        # Resolve image size from various naming conventions used across HF processors.
        size: Dict[str, int] = {"height": 224, "width": 224}
        for attr in ("size", "crop_size"):
            raw = getattr(backbone_processor, attr, None)
            if isinstance(raw, dict) and "height" in raw and "width" in raw:
                size = {"height": raw["height"], "width": raw["width"]}
                break
            if isinstance(raw, int):
                size = {"height": raw, "width": raw}
                break

        do_rescale = getattr(backbone_processor, "do_rescale", True)
        rescale_factor = getattr(backbone_processor, "rescale_factor", 1 / 255)
        do_normalize = getattr(backbone_processor, "do_normalize", True)
        do_resize = getattr(backbone_processor, "do_resize", True)
        do_convert_rgb = getattr(backbone_processor, "do_convert_rgb", True)

        return cls(
            expand_to_6ch=expand_to_6ch,
            do_resize=do_resize,
            size=size,
            do_rescale=do_rescale,
            rescale_factor=rescale_factor,
            do_normalize=do_normalize,
            image_mean=list(mean),
            image_std=list(std),
            do_convert_rgb=do_convert_rgb,
        )