BiliSakura commited on
Commit
eb1c9a0
·
verified ·
1 Parent(s): 82a45d7

Upload 12 files

Browse files
README.md ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # DOFA Transformers Models
2
+
3
+ Self-contained HuggingFace model checkpoints for [DOFA](https://arxiv.org/abs/2403.15356).
4
+
5
+ Each checkpoint subfolder ships remote code for model, processor, and pipeline loading with `trust_remote_code=True`.
6
+
7
+ Sentinel-2 9-band defaults (`default_wavelengths`, `default_image_mean`, `default_image_std`) are baked into `config.json` and `preprocessor_config.json`.
8
+
9
+ ## Available checkpoints
10
+
11
+ | Folder | Hidden size | Layers | Heads |
12
+ |--------|-------------|--------|-------|
13
+ | `dofa-base-patch16-224/` | 768 | 12 | 12 |
14
+ | `dofa-large-patch16-224/` | 1024 | 24 | 16 |
15
+
16
+ ## Usage
17
+
18
+ ```python
19
+ from transformers import pipeline
20
+
21
+ MODEL = "/path/to/DOFA-transformers/dofa-base-patch16-224"
22
+
23
+ pipe = pipeline(
24
+ task="dofa-feature-extraction",
25
+ model=MODEL,
26
+ trust_remote_code=True,
27
+ )
28
+
29
+ features = pipe(image_array, pool=True, return_tensors=True)
30
+ ```
31
+
32
+ Override Sentinel-2 defaults for other sensors:
33
+
34
+ ```python
35
+ features = pipe(
36
+ image_array,
37
+ wavelengths=[...],
38
+ image_mean=[...],
39
+ image_std=[...],
40
+ pool=True,
41
+ return_tensors=True,
42
+ )
43
+ ```
44
+
45
+ ## Test CLI
46
+
47
+ ```bash
48
+ conda activate rsgen
49
+ python test_dofa.py
50
+ python test_dofa.py --model dofa-large-patch16-224
51
+ python test_dofa.py --model dofa-base-patch16-224 --no-pool
52
+ ```
53
+
54
+ ## Dependencies
55
+
56
+ - `transformers`
57
+ - `timm`
58
+ - `torch`
59
+ - `opencv-python` (only when resizing inputs with more than 4 channels)
dofa-base-patch16-224/config.json ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "DOFAModel"
4
+ ],
5
+ "attention_probs_dropout_prob": 0.0,
6
+ "default_wavelengths": [
7
+ 0.665,
8
+ 0.56,
9
+ 0.49,
10
+ 0.705,
11
+ 0.74,
12
+ 0.783,
13
+ 0.842,
14
+ 1.61,
15
+ 2.19
16
+ ],
17
+ "dtype": "float32",
18
+ "global_pool": true,
19
+ "head_dropout": 0.0,
20
+ "hidden_act": "gelu",
21
+ "hidden_dropout_prob": 0.0,
22
+ "hidden_size": 768,
23
+ "id2label": {},
24
+ "image_size": 224,
25
+ "initializer_range": 0.02,
26
+ "intermediate_size": 3072,
27
+ "label2id": {},
28
+ "layer_norm_eps": 1e-06,
29
+ "mlp_ratio": 4.0,
30
+ "model_type": "dofa",
31
+ "num_attention_heads": 12,
32
+ "num_channels": 9,
33
+ "num_hidden_layers": 12,
34
+ "patch_size": 16,
35
+ "qkv_bias": true,
36
+ "transformers_version": "5.0.0",
37
+ "wv_planes": 128,
38
+ "auto_map": {
39
+ "AutoConfig": "modeling_dofa.DOFAConfig",
40
+ "AutoModel": "modeling_dofa.DOFAModel",
41
+ "AutoModelForImageClassification": "modeling_dofa.DOFAForImageClassification"
42
+ },
43
+ "custom_pipelines": {
44
+ "dofa-feature-extraction": {
45
+ "impl": "pipeline_dofa.DOFAImageFeatureExtractionPipeline",
46
+ "pt": [
47
+ "AutoModel"
48
+ ]
49
+ }
50
+ },
51
+ "default_image_mean": [
52
+ 114.11,
53
+ 114.82,
54
+ 126.64,
55
+ 84.34,
56
+ 97.85,
57
+ 103.94,
58
+ 101.44,
59
+ 72.33,
60
+ 56.67
61
+ ],
62
+ "default_image_std": [
63
+ 77.84,
64
+ 69.97,
65
+ 67.42,
66
+ 64.57,
67
+ 61.73,
68
+ 61.34,
69
+ 60.3,
70
+ 47.89,
71
+ 42.56
72
+ ]
73
+ }
dofa-base-patch16-224/image_processing_dofa.py ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 The DOFA Authors and The HuggingFace Inc. team.
2
+ """Image processor for DOFA."""
3
+
4
+ from typing import Optional, Union
5
+
6
+ import numpy as np
7
+
8
+ from transformers.image_processing_utils import BaseImageProcessor, BatchFeature, get_size_dict
9
+ from transformers.image_transforms import resize, to_channel_dimension_format
10
+ from transformers.image_utils import (
11
+ ChannelDimension,
12
+ ImageInput,
13
+ PILImageResampling,
14
+ infer_channel_dimension_format,
15
+ make_flat_list_of_images,
16
+ to_numpy_array,
17
+ valid_images,
18
+ validate_preprocess_arguments,
19
+ )
20
+ from transformers.utils import TensorType, filter_out_non_signature_kwargs, logging
21
+
22
+
23
+ logger = logging.get_logger(__name__)
24
+
25
+
26
+ def _resize_multispectral(image: np.ndarray, size: dict[str, int], input_data_format: ChannelDimension) -> np.ndarray:
27
+ target_height, target_width = size["height"], size["width"]
28
+
29
+ if input_data_format == ChannelDimension.FIRST:
30
+ image = np.transpose(image, (1, 2, 0))
31
+
32
+ height, width, _ = image.shape
33
+ if height == target_height and width == target_width:
34
+ resized = image
35
+ else:
36
+ try:
37
+ import cv2
38
+ except ImportError as exc:
39
+ raise ImportError(
40
+ "Multispectral resize requires OpenCV (`opencv-python`) when input has more than 4 channels."
41
+ ) from exc
42
+ resized = cv2.resize(image, (target_width, target_height), interpolation=cv2.INTER_LINEAR)
43
+
44
+ if input_data_format == ChannelDimension.FIRST:
45
+ return np.transpose(resized, (2, 0, 1))
46
+ return resized
47
+
48
+
49
+ class DOFAImageProcessor(BaseImageProcessor):
50
+ model_input_names = ["pixel_values", "wavelengths"]
51
+
52
+ def __init__(
53
+ self,
54
+ do_resize: bool = True,
55
+ size: Optional[dict[str, int]] = None,
56
+ resample: PILImageResampling = PILImageResampling.BILINEAR,
57
+ do_rescale: bool = True,
58
+ rescale_factor: float = 1 / 255.0,
59
+ do_normalize: bool = True,
60
+ image_mean: Optional[Union[float, list[float]]] = None,
61
+ image_std: Optional[Union[float, list[float]]] = None,
62
+ do_convert_rgb: bool = False,
63
+ default_wavelengths: Optional[list[float]] = None,
64
+ **kwargs,
65
+ ):
66
+ super().__init__(**kwargs)
67
+ size = size if size is not None else {"height": 224, "width": 224}
68
+ self.do_resize = do_resize
69
+ self.size = size
70
+ self.resample = resample
71
+ self.do_rescale = do_rescale
72
+ self.rescale_factor = rescale_factor
73
+ self.do_normalize = do_normalize
74
+ self.image_mean = image_mean
75
+ self.image_std = image_std
76
+ self.do_convert_rgb = do_convert_rgb
77
+ self.default_wavelengths = default_wavelengths
78
+
79
+ @filter_out_non_signature_kwargs()
80
+ def preprocess(
81
+ self,
82
+ images: ImageInput,
83
+ do_resize: Optional[bool] = None,
84
+ size: Optional[dict[str, int]] = None,
85
+ resample: Optional[PILImageResampling] = None,
86
+ do_rescale: Optional[bool] = None,
87
+ rescale_factor: Optional[float] = None,
88
+ do_normalize: Optional[bool] = None,
89
+ image_mean: Optional[Union[float, list[float]]] = None,
90
+ image_std: Optional[Union[float, list[float]]] = None,
91
+ wavelengths: Optional[list[float]] = None,
92
+ return_tensors: Optional[Union[str, TensorType]] = None,
93
+ data_format: Union[str, ChannelDimension] = ChannelDimension.FIRST,
94
+ input_data_format: Optional[Union[str, ChannelDimension]] = None,
95
+ do_convert_rgb: Optional[bool] = None,
96
+ ):
97
+ do_resize = do_resize if do_resize is not None else self.do_resize
98
+ size = size if size is not None else self.size
99
+ size = get_size_dict(size, default_to_square=True)
100
+ resample = resample if resample is not None else self.resample
101
+ do_rescale = do_rescale if do_rescale is not None else self.do_rescale
102
+ rescale_factor = rescale_factor if rescale_factor is not None else self.rescale_factor
103
+ do_normalize = do_normalize if do_normalize is not None else self.do_normalize
104
+ image_mean = image_mean if image_mean is not None else self.image_mean
105
+ image_std = image_std if image_std is not None else self.image_std
106
+ do_convert_rgb = do_convert_rgb if do_convert_rgb is not None else self.do_convert_rgb
107
+ wavelengths = wavelengths if wavelengths is not None else self.default_wavelengths
108
+
109
+ if wavelengths is None:
110
+ raise ValueError(
111
+ "DOFA requires `wavelengths` (one value per channel, in micrometers). "
112
+ "Pass them to the processor call or set `default_wavelengths` in the processor config."
113
+ )
114
+
115
+ if do_normalize and (image_mean is None or image_std is None):
116
+ raise ValueError(
117
+ "Normalization requires `image_mean` and `image_std` with one value per channel. "
118
+ "Provide modality-specific statistics for your input bands."
119
+ )
120
+
121
+ if do_normalize and (
122
+ len(image_mean) != len(wavelengths) or len(image_std) != len(wavelengths)
123
+ ):
124
+ raise ValueError(
125
+ f"`image_mean` and `image_std` must match the number of wavelengths/channels "
126
+ f"({len(wavelengths)}), got mean={len(image_mean)} std={len(image_std)}."
127
+ )
128
+
129
+ images = make_flat_list_of_images(images)
130
+ if not valid_images(images):
131
+ raise ValueError("Invalid image type. Must be PIL, numpy, or torch tensor.")
132
+
133
+ validate_preprocess_arguments(
134
+ do_rescale=do_rescale,
135
+ rescale_factor=rescale_factor,
136
+ do_normalize=do_normalize,
137
+ image_mean=image_mean,
138
+ image_std=image_std,
139
+ do_resize=do_resize,
140
+ size=size,
141
+ resample=resample,
142
+ )
143
+
144
+ processed_images = []
145
+ for image in images:
146
+ image = to_numpy_array(image)
147
+ if do_convert_rgb:
148
+ image = self._convert_image_to_rgb(image)
149
+
150
+ if input_data_format is None:
151
+ try:
152
+ input_data_format = infer_channel_dimension_format(image)
153
+ except ValueError:
154
+ input_data_format = ChannelDimension.LAST
155
+
156
+ num_channels = image.shape[-1] if input_data_format == ChannelDimension.LAST else image.shape[0]
157
+
158
+ if do_resize:
159
+ if num_channels > 4:
160
+ image = _resize_multispectral(image, size=size, input_data_format=input_data_format)
161
+ else:
162
+ image = resize(image, size=size, resample=resample, input_data_format=input_data_format)
163
+
164
+ if do_rescale:
165
+ image = image * rescale_factor
166
+
167
+ if do_normalize:
168
+ image = self.normalize(image=image, mean=image_mean, std=image_std, input_data_format=input_data_format)
169
+
170
+ image = to_channel_dimension_format(image, data_format, input_channel_dim=input_data_format)
171
+ processed_images.append(image)
172
+
173
+ data = {
174
+ "pixel_values": processed_images,
175
+ "wavelengths": np.asarray(wavelengths, dtype=np.float32),
176
+ }
177
+ return BatchFeature(data=data, tensor_type=return_tensors)
178
+
179
+
180
+ __all__ = ["DOFAImageProcessor"]
dofa-base-patch16-224/modeling_dofa.py ADDED
@@ -0,0 +1,368 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 The DOFA Authors and The HuggingFace Inc. team.
2
+ """Self-contained DOFA model, config, and dynamic patch embedding."""
3
+
4
+ from functools import partial
5
+ from typing import Optional, Union
6
+
7
+ import torch
8
+ import torch.nn as nn
9
+ import torch.nn.functional as F
10
+ import torch.nn.init as init
11
+ from timm.models.vision_transformer import Block
12
+
13
+ from transformers.configuration_utils import PretrainedConfig as PreTrainedConfig
14
+ from transformers.modeling_outputs import BaseModelOutputWithPooling, ImageClassifierOutput
15
+ from transformers.modeling_utils import PreTrainedModel
16
+ from transformers.processing_utils import Unpack
17
+ from transformers.utils import TransformersKwargs, logging
18
+
19
+
20
+ logger = logging.get_logger(__name__)
21
+
22
+
23
+ class DOFAConfig(PreTrainedConfig):
24
+ model_type = "dofa"
25
+
26
+ def __init__(
27
+ self,
28
+ hidden_size=768,
29
+ num_hidden_layers=12,
30
+ num_attention_heads=12,
31
+ intermediate_size=None,
32
+ hidden_act="gelu",
33
+ hidden_dropout_prob=0.0,
34
+ attention_probs_dropout_prob=0.0,
35
+ initializer_range=0.02,
36
+ layer_norm_eps=1e-6,
37
+ image_size=224,
38
+ patch_size=16,
39
+ num_channels=3,
40
+ qkv_bias=True,
41
+ wv_planes=128,
42
+ mlp_ratio=4.0,
43
+ global_pool=True,
44
+ default_wavelengths=None,
45
+ default_image_mean=None,
46
+ default_image_std=None,
47
+ head_dropout=0.0,
48
+ num_labels=0,
49
+ **kwargs,
50
+ ):
51
+ super().__init__(**kwargs)
52
+
53
+ self.hidden_size = hidden_size
54
+ self.num_hidden_layers = num_hidden_layers
55
+ self.num_attention_heads = num_attention_heads
56
+ self.hidden_act = hidden_act
57
+ self.hidden_dropout_prob = hidden_dropout_prob
58
+ self.attention_probs_dropout_prob = attention_probs_dropout_prob
59
+ self.initializer_range = initializer_range
60
+ self.layer_norm_eps = layer_norm_eps
61
+ self.image_size = image_size
62
+ self.patch_size = patch_size
63
+ self.num_channels = num_channels
64
+ self.qkv_bias = qkv_bias
65
+ self.wv_planes = wv_planes
66
+ self.mlp_ratio = mlp_ratio
67
+ self.global_pool = global_pool
68
+ self.default_wavelengths = default_wavelengths
69
+ self.default_image_mean = default_image_mean
70
+ self.default_image_std = default_image_std
71
+ self.head_dropout = head_dropout
72
+ self.num_labels = num_labels
73
+
74
+ if intermediate_size is None:
75
+ self.intermediate_size = int(hidden_size * mlp_ratio)
76
+ else:
77
+ self.intermediate_size = intermediate_size
78
+
79
+
80
+ def get_1d_sincos_pos_embed_from_grid_torch(embed_dim, pos):
81
+ assert embed_dim % 2 == 0
82
+ omega = torch.arange(embed_dim // 2, dtype=torch.float32, device=pos.device)
83
+ omega /= embed_dim / 2.0
84
+ omega = 1.0 / 10000**omega
85
+
86
+ pos = pos.reshape(-1)
87
+ out = torch.einsum("m,d->md", pos, omega)
88
+
89
+ emb_sin = torch.sin(out)
90
+ emb_cos = torch.cos(out)
91
+
92
+ return torch.cat([emb_sin, emb_cos], dim=1)
93
+
94
+
95
+ class TransformerWeightGenerator(nn.Module):
96
+ def __init__(self, input_dim, output_dim, embed_dim, num_heads=4, num_layers=1):
97
+ super().__init__()
98
+ encoder_layer = nn.TransformerEncoderLayer(
99
+ d_model=input_dim,
100
+ nhead=num_heads,
101
+ activation="gelu",
102
+ norm_first=False,
103
+ batch_first=False,
104
+ dropout=False,
105
+ )
106
+ self.transformer_encoder = nn.TransformerEncoder(
107
+ encoder_layer, num_layers=num_layers, enable_nested_tensor=False
108
+ )
109
+
110
+ self.fc_weight = nn.Linear(input_dim, output_dim)
111
+ self.fc_bias = nn.Linear(input_dim, embed_dim)
112
+ self.wt_num = 128
113
+ self.weight_tokens = nn.Parameter(torch.empty([self.wt_num, input_dim]))
114
+ self.bias_token = nn.Parameter(torch.empty([1, input_dim]))
115
+
116
+ torch.nn.init.normal_(self.weight_tokens, std=0.02)
117
+ torch.nn.init.normal_(self.bias_token, std=0.02)
118
+
119
+ def forward(self, x):
120
+ pos_wave = x
121
+ x = torch.cat([self.weight_tokens, pos_wave], dim=0)
122
+ x = torch.cat([x, self.bias_token], dim=0)
123
+ transformer_output = self.transformer_encoder(x)
124
+ weights = self.fc_weight(transformer_output[self.wt_num : -1] + pos_wave)
125
+ bias = self.fc_bias(transformer_output[-1])
126
+ return weights, bias
127
+
128
+
129
+ class FCResLayer(nn.Module):
130
+ def __init__(self, linear_size=128):
131
+ super().__init__()
132
+ self.nonlin1 = nn.ReLU(inplace=True)
133
+ self.nonlin2 = nn.ReLU(inplace=True)
134
+ self.w1 = nn.Linear(linear_size, linear_size)
135
+ self.w2 = nn.Linear(linear_size, linear_size)
136
+
137
+ def forward(self, x):
138
+ y = self.w1(x)
139
+ y = self.nonlin1(y)
140
+ y = self.w2(y)
141
+ y = self.nonlin2(y)
142
+ return x + y
143
+
144
+
145
+ class DOFADynamicPatchEmbed(nn.Module):
146
+ def __init__(self, wv_planes, inter_dim=128, kernel_size=16, embed_dim=768):
147
+ super().__init__()
148
+ self.kernel_size = kernel_size
149
+ self.wv_planes = wv_planes
150
+ self.embed_dim = embed_dim
151
+ self._num_kernel = self.kernel_size * self.kernel_size * self.embed_dim
152
+ self.inter_dim = inter_dim
153
+ self.patch_size = (kernel_size, kernel_size)
154
+
155
+ self.weight_generator = TransformerWeightGenerator(wv_planes, self._num_kernel, embed_dim)
156
+ self.scaler = 0.01
157
+ self.fclayer = FCResLayer(wv_planes)
158
+ self.weight_generator.apply(self._weight_init)
159
+ self.fclayer.apply(self._weight_init)
160
+
161
+ def _weight_init(self, module):
162
+ if isinstance(module, nn.Linear):
163
+ init.xavier_uniform_(module.weight)
164
+ module.bias.data.fill_(0.01)
165
+
166
+ def forward(self, pixel_values, wavelengths):
167
+ inplanes = wavelengths.size(0)
168
+ waves = get_1d_sincos_pos_embed_from_grid_torch(self.wv_planes, wavelengths * 1000)
169
+ waves = self.fclayer(waves)
170
+ weight, bias = self.weight_generator(waves)
171
+
172
+ dynamic_weight = weight.view(inplanes, self.kernel_size, self.kernel_size, self.embed_dim)
173
+ dynamic_weight = dynamic_weight.permute([3, 0, 1, 2])
174
+
175
+ if bias is not None:
176
+ bias = bias.view([self.embed_dim]) * self.scaler
177
+
178
+ weights = dynamic_weight * self.scaler
179
+ dynamic_out = F.conv2d(
180
+ pixel_values, weights, bias=bias, stride=self.kernel_size, padding=1, dilation=1
181
+ )
182
+ return dynamic_out.flatten(2).transpose(1, 2), waves
183
+
184
+
185
+ def _prepare_wavelengths(wavelengths, pixel_values, default_wavelengths=None):
186
+ if wavelengths is None:
187
+ if default_wavelengths is None:
188
+ raise ValueError(
189
+ "DOFA requires per-channel wavelengths. Pass `wavelengths` to the model or image processor, "
190
+ "or set `default_wavelengths` in the model config."
191
+ )
192
+ wavelengths = default_wavelengths
193
+
194
+ if not torch.is_tensor(wavelengths):
195
+ wavelengths = torch.tensor(wavelengths, dtype=torch.float32, device=pixel_values.device)
196
+ else:
197
+ wavelengths = wavelengths.to(device=pixel_values.device, dtype=torch.float32)
198
+
199
+ if wavelengths.dim() == 2 and wavelengths.shape[0] == 1:
200
+ wavelengths = wavelengths.squeeze(0)
201
+ elif wavelengths.dim() == 2 and wavelengths.shape[0] == pixel_values.shape[0]:
202
+ if not torch.allclose(wavelengths[0], wavelengths):
203
+ raise ValueError("DOFA currently expects identical wavelengths for all items in a batch.")
204
+ wavelengths = wavelengths[0]
205
+
206
+ if wavelengths.dim() != 1:
207
+ raise ValueError("`wavelengths` must be a 1D tensor/list with one value per input channel.")
208
+
209
+ if wavelengths.shape[0] != pixel_values.shape[1]:
210
+ raise ValueError(
211
+ f"Expected {pixel_values.shape[1]} wavelengths for {pixel_values.shape[1]} channels, "
212
+ f"but got {wavelengths.shape[0]}."
213
+ )
214
+
215
+ return wavelengths
216
+
217
+
218
+ class DOFAPreTrainedModel(PreTrainedModel):
219
+ config_class = DOFAConfig
220
+ base_model_prefix = "dofa"
221
+ main_input_name = "pixel_values"
222
+ input_modalities = ("image",)
223
+ supports_gradient_checkpointing = True
224
+ _no_split_modules = ["Block"]
225
+ _supports_sdpa = False
226
+
227
+ def _init_weights(self, module):
228
+ super()._init_weights(module)
229
+ if isinstance(module, DOFAModel):
230
+ if hasattr(module, "pos_embed"):
231
+ nn.init.trunc_normal_(module.pos_embed, std=self.config.initializer_range)
232
+ if hasattr(module, "cls_token"):
233
+ nn.init.trunc_normal_(module.cls_token, std=self.config.initializer_range)
234
+
235
+
236
+ class DOFAModel(DOFAPreTrainedModel):
237
+ def __init__(self, config: DOFAConfig, add_pooling_layer: bool = True):
238
+ super().__init__(config)
239
+ self.config = config
240
+
241
+ image_size = config.image_size if isinstance(config.image_size, int) else config.image_size[0]
242
+ self.patch_embed = DOFADynamicPatchEmbed(
243
+ wv_planes=config.wv_planes,
244
+ inter_dim=config.wv_planes,
245
+ kernel_size=config.patch_size,
246
+ embed_dim=config.hidden_size,
247
+ )
248
+ self.num_patches = (image_size // config.patch_size) ** 2
249
+ self.cls_token = nn.Parameter(torch.zeros(1, 1, config.hidden_size))
250
+ self.pos_embed = nn.Parameter(
251
+ torch.zeros(1, self.num_patches + 1, config.hidden_size), requires_grad=False
252
+ )
253
+
254
+ norm_layer = partial(nn.LayerNorm, eps=config.layer_norm_eps)
255
+ self.blocks = nn.ModuleList(
256
+ [
257
+ Block(
258
+ config.hidden_size,
259
+ config.num_attention_heads,
260
+ config.mlp_ratio,
261
+ qkv_bias=config.qkv_bias,
262
+ norm_layer=norm_layer,
263
+ )
264
+ for _ in range(config.num_hidden_layers)
265
+ ]
266
+ )
267
+
268
+ self.global_pool = config.global_pool
269
+ if self.global_pool:
270
+ self.fc_norm = norm_layer(config.hidden_size)
271
+ self.norm = None
272
+ else:
273
+ self.fc_norm = None
274
+ self.norm = norm_layer(config.hidden_size)
275
+
276
+ self.add_pooling_layer = add_pooling_layer
277
+ self.post_init()
278
+
279
+ def forward_features(self, pixel_values, wavelengths):
280
+ patch_tokens, _ = self.patch_embed(pixel_values, wavelengths)
281
+ patch_tokens = patch_tokens + self.pos_embed[:, 1:, :]
282
+
283
+ cls_token = self.cls_token + self.pos_embed[:, :1, :]
284
+ cls_tokens = cls_token.expand(pixel_values.shape[0], -1, -1)
285
+ hidden_states = torch.cat((cls_tokens, patch_tokens), dim=1)
286
+
287
+ for block in self.blocks:
288
+ hidden_states = block(hidden_states)
289
+
290
+ if self.global_pool:
291
+ pooled_output = self.fc_norm(hidden_states[:, 1:, :].mean(dim=1))
292
+ else:
293
+ pooled_output = self.norm(hidden_states)[:, 0]
294
+
295
+ return hidden_states, pooled_output
296
+
297
+ def forward(
298
+ self,
299
+ pixel_values: Optional[torch.Tensor] = None,
300
+ wavelengths: Optional[Union[torch.Tensor, list]] = None,
301
+ return_dict: Optional[bool] = None,
302
+ **kwargs: Unpack[TransformersKwargs],
303
+ ) -> BaseModelOutputWithPooling:
304
+ if pixel_values is None:
305
+ raise ValueError("You must specify `pixel_values`")
306
+
307
+ pixel_values = pixel_values.to(dtype=self.dtype)
308
+
309
+ if return_dict is None:
310
+ return_dict = self.config.use_return_dict
311
+
312
+ wavelengths = _prepare_wavelengths(wavelengths, pixel_values, self.config.default_wavelengths)
313
+ last_hidden_state, pooled_output = self.forward_features(pixel_values, wavelengths)
314
+
315
+ if not self.add_pooling_layer:
316
+ pooled_output = None
317
+
318
+ if not return_dict:
319
+ return (last_hidden_state, pooled_output)
320
+
321
+ return BaseModelOutputWithPooling(last_hidden_state=last_hidden_state, pooler_output=pooled_output)
322
+
323
+
324
+ class DOFAForImageClassification(DOFAPreTrainedModel):
325
+ def __init__(self, config: DOFAConfig):
326
+ super().__init__(config)
327
+ self.num_labels = config.num_labels
328
+ self.dofa = DOFAModel(config, add_pooling_layer=True)
329
+ self.head_drop = nn.Dropout(config.head_dropout)
330
+ self.classifier = (
331
+ nn.Linear(config.hidden_size, config.num_labels) if config.num_labels > 0 else nn.Identity()
332
+ )
333
+ self.post_init()
334
+
335
+ def forward(
336
+ self,
337
+ pixel_values: Optional[torch.Tensor] = None,
338
+ wavelengths: Optional[Union[torch.Tensor, list]] = None,
339
+ labels: Optional[torch.Tensor] = None,
340
+ return_dict: Optional[bool] = None,
341
+ **kwargs: Unpack[TransformersKwargs],
342
+ ) -> ImageClassifierOutput:
343
+ outputs = self.dofa(
344
+ pixel_values=pixel_values,
345
+ wavelengths=wavelengths,
346
+ return_dict=True,
347
+ **kwargs,
348
+ )
349
+ pooled_output = self.head_drop(outputs.pooler_output)
350
+ logits = self.classifier(pooled_output)
351
+
352
+ loss = None
353
+ if labels is not None:
354
+ loss = self.loss_function(labels, logits, self.config, **kwargs)
355
+
356
+ if not return_dict:
357
+ output = (logits,) + outputs[1:]
358
+ return ((loss,) + output) if loss is not None else output
359
+
360
+ return ImageClassifierOutput(
361
+ loss=loss,
362
+ logits=logits,
363
+ hidden_states=outputs.hidden_states,
364
+ attentions=outputs.attentions,
365
+ )
366
+
367
+
368
+ __all__ = ["DOFAConfig", "DOFAForImageClassification", "DOFAModel", "DOFAPreTrainedModel"]
dofa-base-patch16-224/pipeline_dofa.py ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 The DOFA Authors and The HuggingFace Inc. team.
2
+ """Custom DOFA feature extraction pipeline for trust_remote_code loading."""
3
+
4
+ from typing import Any, Union
5
+
6
+ from transformers.pipelines.base import GenericTensor, build_pipeline_init_args
7
+ from transformers.pipelines.image_feature_extraction import ImageFeatureExtractionPipeline
8
+ from transformers.utils import add_end_docstrings, is_vision_available
9
+
10
+
11
+ if is_vision_available():
12
+ from transformers.image_utils import load_image
13
+
14
+
15
+ @add_end_docstrings(
16
+ build_pipeline_init_args(has_image_processor=True),
17
+ """
18
+ wavelengths (`list[float]`, *optional*):
19
+ Per-channel wavelengths in micrometers. Defaults to Sentinel-2 bands from config.
20
+ image_mean (`list[float]`, *optional*):
21
+ Per-channel normalization mean values.
22
+ image_std (`list[float]`, *optional*):
23
+ Per-channel normalization standard deviation values.
24
+ pool (`bool`, *optional*, defaults to `False`):
25
+ Whether to return pooled features instead of sequence hidden states.
26
+ """,
27
+ )
28
+ class DOFAImageFeatureExtractionPipeline(ImageFeatureExtractionPipeline):
29
+ """
30
+ DOFA wavelength-conditioned multispectral feature extraction pipeline.
31
+
32
+ This pipeline can be loaded from [`pipeline`] using the task identifier `"dofa-feature-extraction"`
33
+ with `trust_remote_code=True`.
34
+
35
+ Example:
36
+
37
+ ```python
38
+ >>> from transformers import pipeline
39
+
40
+ >>> pipe = pipeline(
41
+ ... task="dofa-feature-extraction",
42
+ ... model="/path/to/dofa-base-patch16-224",
43
+ ... trust_remote_code=True,
44
+ ... )
45
+ >>> features = pipe(image_array, pool=True, return_tensors=True)
46
+ ```
47
+ """
48
+
49
+ def _sanitize_parameters(
50
+ self,
51
+ image_processor_kwargs=None,
52
+ return_tensors=None,
53
+ pool=None,
54
+ wavelengths=None,
55
+ image_mean=None,
56
+ image_std=None,
57
+ **kwargs,
58
+ ):
59
+ preprocess_params = {} if image_processor_kwargs is None else dict(image_processor_kwargs)
60
+
61
+ if wavelengths is not None:
62
+ preprocess_params["wavelengths"] = wavelengths
63
+ if image_mean is not None:
64
+ preprocess_params["image_mean"] = image_mean
65
+ if image_std is not None:
66
+ preprocess_params["image_std"] = image_std
67
+ if "timeout" in kwargs:
68
+ preprocess_params["timeout"] = kwargs["timeout"]
69
+
70
+ postprocess_params = {}
71
+ if pool is not None:
72
+ postprocess_params["pool"] = pool
73
+ if return_tensors is not None:
74
+ postprocess_params["return_tensors"] = return_tensors
75
+
76
+ return preprocess_params, {}, postprocess_params
77
+
78
+ def preprocess(self, image, timeout=None, **image_processor_kwargs) -> dict[str, GenericTensor]:
79
+ if not isinstance(image, (list, tuple)) and not hasattr(image, "shape"):
80
+ image = load_image(image, timeout=timeout)
81
+ model_inputs = self.image_processor(image, return_tensors="pt", **image_processor_kwargs)
82
+ model_inputs = model_inputs.to(self.dtype)
83
+ return model_inputs
84
+
85
+ def __call__(self, *args: Union[str, Any, list[Any]], **kwargs: Any) -> list[Any]:
86
+ """
87
+ Extract features from multispectral inputs.
88
+
89
+ Accepts numpy arrays (HWC), file paths, URLs, or PIL images in addition to the
90
+ standard image inputs supported by [`ImageFeatureExtractionPipeline`].
91
+ """
92
+ return super().__call__(*args, **kwargs)
93
+
94
+
95
+ __all__ = ["DOFAImageFeatureExtractionPipeline"]
dofa-base-patch16-224/preprocessor_config.json ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "image_processor_type": "DOFAImageProcessor",
3
+ "size": {
4
+ "height": 224,
5
+ "width": 224
6
+ },
7
+ "do_resize": true,
8
+ "do_rescale": true,
9
+ "do_normalize": true,
10
+ "do_convert_rgb": false,
11
+ "rescale_factor": 0.00392156862745098,
12
+ "auto_map": {
13
+ "AutoImageProcessor": "image_processing_dofa.DOFAImageProcessor"
14
+ },
15
+ "default_wavelengths": [
16
+ 0.665,
17
+ 0.56,
18
+ 0.49,
19
+ 0.705,
20
+ 0.74,
21
+ 0.783,
22
+ 0.842,
23
+ 1.61,
24
+ 2.19
25
+ ],
26
+ "image_mean": [
27
+ 114.11,
28
+ 114.82,
29
+ 126.64,
30
+ 84.34,
31
+ 97.85,
32
+ 103.94,
33
+ 101.44,
34
+ 72.33,
35
+ 56.67
36
+ ],
37
+ "image_std": [
38
+ 77.84,
39
+ 69.97,
40
+ 67.42,
41
+ 64.57,
42
+ 61.73,
43
+ 61.34,
44
+ 60.3,
45
+ 47.89,
46
+ 42.56
47
+ ]
48
+ }
dofa-large-patch16-224/config.json ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "DOFAModel"
4
+ ],
5
+ "attention_probs_dropout_prob": 0.0,
6
+ "default_wavelengths": [
7
+ 0.665,
8
+ 0.56,
9
+ 0.49,
10
+ 0.705,
11
+ 0.74,
12
+ 0.783,
13
+ 0.842,
14
+ 1.61,
15
+ 2.19
16
+ ],
17
+ "dtype": "float32",
18
+ "global_pool": true,
19
+ "head_dropout": 0.0,
20
+ "hidden_act": "gelu",
21
+ "hidden_dropout_prob": 0.0,
22
+ "hidden_size": 1024,
23
+ "id2label": {},
24
+ "image_size": 224,
25
+ "initializer_range": 0.02,
26
+ "intermediate_size": 4096,
27
+ "label2id": {},
28
+ "layer_norm_eps": 1e-06,
29
+ "mlp_ratio": 4.0,
30
+ "model_type": "dofa",
31
+ "num_attention_heads": 16,
32
+ "num_channels": 9,
33
+ "num_hidden_layers": 24,
34
+ "patch_size": 16,
35
+ "qkv_bias": true,
36
+ "transformers_version": "5.0.0",
37
+ "wv_planes": 128,
38
+ "auto_map": {
39
+ "AutoConfig": "modeling_dofa.DOFAConfig",
40
+ "AutoModel": "modeling_dofa.DOFAModel",
41
+ "AutoModelForImageClassification": "modeling_dofa.DOFAForImageClassification"
42
+ },
43
+ "custom_pipelines": {
44
+ "dofa-feature-extraction": {
45
+ "impl": "pipeline_dofa.DOFAImageFeatureExtractionPipeline",
46
+ "pt": [
47
+ "AutoModel"
48
+ ]
49
+ }
50
+ },
51
+ "default_image_mean": [
52
+ 114.11,
53
+ 114.82,
54
+ 126.64,
55
+ 84.34,
56
+ 97.85,
57
+ 103.94,
58
+ 101.44,
59
+ 72.33,
60
+ 56.67
61
+ ],
62
+ "default_image_std": [
63
+ 77.84,
64
+ 69.97,
65
+ 67.42,
66
+ 64.57,
67
+ 61.73,
68
+ 61.34,
69
+ 60.3,
70
+ 47.89,
71
+ 42.56
72
+ ]
73
+ }
dofa-large-patch16-224/image_processing_dofa.py ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 The DOFA Authors and The HuggingFace Inc. team.
2
+ """Image processor for DOFA."""
3
+
4
+ from typing import Optional, Union
5
+
6
+ import numpy as np
7
+
8
+ from transformers.image_processing_utils import BaseImageProcessor, BatchFeature, get_size_dict
9
+ from transformers.image_transforms import resize, to_channel_dimension_format
10
+ from transformers.image_utils import (
11
+ ChannelDimension,
12
+ ImageInput,
13
+ PILImageResampling,
14
+ infer_channel_dimension_format,
15
+ make_flat_list_of_images,
16
+ to_numpy_array,
17
+ valid_images,
18
+ validate_preprocess_arguments,
19
+ )
20
+ from transformers.utils import TensorType, filter_out_non_signature_kwargs, logging
21
+
22
+
23
+ logger = logging.get_logger(__name__)
24
+
25
+
26
+ def _resize_multispectral(image: np.ndarray, size: dict[str, int], input_data_format: ChannelDimension) -> np.ndarray:
27
+ target_height, target_width = size["height"], size["width"]
28
+
29
+ if input_data_format == ChannelDimension.FIRST:
30
+ image = np.transpose(image, (1, 2, 0))
31
+
32
+ height, width, _ = image.shape
33
+ if height == target_height and width == target_width:
34
+ resized = image
35
+ else:
36
+ try:
37
+ import cv2
38
+ except ImportError as exc:
39
+ raise ImportError(
40
+ "Multispectral resize requires OpenCV (`opencv-python`) when input has more than 4 channels."
41
+ ) from exc
42
+ resized = cv2.resize(image, (target_width, target_height), interpolation=cv2.INTER_LINEAR)
43
+
44
+ if input_data_format == ChannelDimension.FIRST:
45
+ return np.transpose(resized, (2, 0, 1))
46
+ return resized
47
+
48
+
49
+ class DOFAImageProcessor(BaseImageProcessor):
50
+ model_input_names = ["pixel_values", "wavelengths"]
51
+
52
+ def __init__(
53
+ self,
54
+ do_resize: bool = True,
55
+ size: Optional[dict[str, int]] = None,
56
+ resample: PILImageResampling = PILImageResampling.BILINEAR,
57
+ do_rescale: bool = True,
58
+ rescale_factor: float = 1 / 255.0,
59
+ do_normalize: bool = True,
60
+ image_mean: Optional[Union[float, list[float]]] = None,
61
+ image_std: Optional[Union[float, list[float]]] = None,
62
+ do_convert_rgb: bool = False,
63
+ default_wavelengths: Optional[list[float]] = None,
64
+ **kwargs,
65
+ ):
66
+ super().__init__(**kwargs)
67
+ size = size if size is not None else {"height": 224, "width": 224}
68
+ self.do_resize = do_resize
69
+ self.size = size
70
+ self.resample = resample
71
+ self.do_rescale = do_rescale
72
+ self.rescale_factor = rescale_factor
73
+ self.do_normalize = do_normalize
74
+ self.image_mean = image_mean
75
+ self.image_std = image_std
76
+ self.do_convert_rgb = do_convert_rgb
77
+ self.default_wavelengths = default_wavelengths
78
+
79
+ @filter_out_non_signature_kwargs()
80
+ def preprocess(
81
+ self,
82
+ images: ImageInput,
83
+ do_resize: Optional[bool] = None,
84
+ size: Optional[dict[str, int]] = None,
85
+ resample: Optional[PILImageResampling] = None,
86
+ do_rescale: Optional[bool] = None,
87
+ rescale_factor: Optional[float] = None,
88
+ do_normalize: Optional[bool] = None,
89
+ image_mean: Optional[Union[float, list[float]]] = None,
90
+ image_std: Optional[Union[float, list[float]]] = None,
91
+ wavelengths: Optional[list[float]] = None,
92
+ return_tensors: Optional[Union[str, TensorType]] = None,
93
+ data_format: Union[str, ChannelDimension] = ChannelDimension.FIRST,
94
+ input_data_format: Optional[Union[str, ChannelDimension]] = None,
95
+ do_convert_rgb: Optional[bool] = None,
96
+ ):
97
+ do_resize = do_resize if do_resize is not None else self.do_resize
98
+ size = size if size is not None else self.size
99
+ size = get_size_dict(size, default_to_square=True)
100
+ resample = resample if resample is not None else self.resample
101
+ do_rescale = do_rescale if do_rescale is not None else self.do_rescale
102
+ rescale_factor = rescale_factor if rescale_factor is not None else self.rescale_factor
103
+ do_normalize = do_normalize if do_normalize is not None else self.do_normalize
104
+ image_mean = image_mean if image_mean is not None else self.image_mean
105
+ image_std = image_std if image_std is not None else self.image_std
106
+ do_convert_rgb = do_convert_rgb if do_convert_rgb is not None else self.do_convert_rgb
107
+ wavelengths = wavelengths if wavelengths is not None else self.default_wavelengths
108
+
109
+ if wavelengths is None:
110
+ raise ValueError(
111
+ "DOFA requires `wavelengths` (one value per channel, in micrometers). "
112
+ "Pass them to the processor call or set `default_wavelengths` in the processor config."
113
+ )
114
+
115
+ if do_normalize and (image_mean is None or image_std is None):
116
+ raise ValueError(
117
+ "Normalization requires `image_mean` and `image_std` with one value per channel. "
118
+ "Provide modality-specific statistics for your input bands."
119
+ )
120
+
121
+ if do_normalize and (
122
+ len(image_mean) != len(wavelengths) or len(image_std) != len(wavelengths)
123
+ ):
124
+ raise ValueError(
125
+ f"`image_mean` and `image_std` must match the number of wavelengths/channels "
126
+ f"({len(wavelengths)}), got mean={len(image_mean)} std={len(image_std)}."
127
+ )
128
+
129
+ images = make_flat_list_of_images(images)
130
+ if not valid_images(images):
131
+ raise ValueError("Invalid image type. Must be PIL, numpy, or torch tensor.")
132
+
133
+ validate_preprocess_arguments(
134
+ do_rescale=do_rescale,
135
+ rescale_factor=rescale_factor,
136
+ do_normalize=do_normalize,
137
+ image_mean=image_mean,
138
+ image_std=image_std,
139
+ do_resize=do_resize,
140
+ size=size,
141
+ resample=resample,
142
+ )
143
+
144
+ processed_images = []
145
+ for image in images:
146
+ image = to_numpy_array(image)
147
+ if do_convert_rgb:
148
+ image = self._convert_image_to_rgb(image)
149
+
150
+ if input_data_format is None:
151
+ try:
152
+ input_data_format = infer_channel_dimension_format(image)
153
+ except ValueError:
154
+ input_data_format = ChannelDimension.LAST
155
+
156
+ num_channels = image.shape[-1] if input_data_format == ChannelDimension.LAST else image.shape[0]
157
+
158
+ if do_resize:
159
+ if num_channels > 4:
160
+ image = _resize_multispectral(image, size=size, input_data_format=input_data_format)
161
+ else:
162
+ image = resize(image, size=size, resample=resample, input_data_format=input_data_format)
163
+
164
+ if do_rescale:
165
+ image = image * rescale_factor
166
+
167
+ if do_normalize:
168
+ image = self.normalize(image=image, mean=image_mean, std=image_std, input_data_format=input_data_format)
169
+
170
+ image = to_channel_dimension_format(image, data_format, input_channel_dim=input_data_format)
171
+ processed_images.append(image)
172
+
173
+ data = {
174
+ "pixel_values": processed_images,
175
+ "wavelengths": np.asarray(wavelengths, dtype=np.float32),
176
+ }
177
+ return BatchFeature(data=data, tensor_type=return_tensors)
178
+
179
+
180
+ __all__ = ["DOFAImageProcessor"]
dofa-large-patch16-224/modeling_dofa.py ADDED
@@ -0,0 +1,368 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 The DOFA Authors and The HuggingFace Inc. team.
2
+ """Self-contained DOFA model, config, and dynamic patch embedding."""
3
+
4
+ from functools import partial
5
+ from typing import Optional, Union
6
+
7
+ import torch
8
+ import torch.nn as nn
9
+ import torch.nn.functional as F
10
+ import torch.nn.init as init
11
+ from timm.models.vision_transformer import Block
12
+
13
+ from transformers.configuration_utils import PretrainedConfig as PreTrainedConfig
14
+ from transformers.modeling_outputs import BaseModelOutputWithPooling, ImageClassifierOutput
15
+ from transformers.modeling_utils import PreTrainedModel
16
+ from transformers.processing_utils import Unpack
17
+ from transformers.utils import TransformersKwargs, logging
18
+
19
+
20
+ logger = logging.get_logger(__name__)
21
+
22
+
23
+ class DOFAConfig(PreTrainedConfig):
24
+ model_type = "dofa"
25
+
26
+ def __init__(
27
+ self,
28
+ hidden_size=768,
29
+ num_hidden_layers=12,
30
+ num_attention_heads=12,
31
+ intermediate_size=None,
32
+ hidden_act="gelu",
33
+ hidden_dropout_prob=0.0,
34
+ attention_probs_dropout_prob=0.0,
35
+ initializer_range=0.02,
36
+ layer_norm_eps=1e-6,
37
+ image_size=224,
38
+ patch_size=16,
39
+ num_channels=3,
40
+ qkv_bias=True,
41
+ wv_planes=128,
42
+ mlp_ratio=4.0,
43
+ global_pool=True,
44
+ default_wavelengths=None,
45
+ default_image_mean=None,
46
+ default_image_std=None,
47
+ head_dropout=0.0,
48
+ num_labels=0,
49
+ **kwargs,
50
+ ):
51
+ super().__init__(**kwargs)
52
+
53
+ self.hidden_size = hidden_size
54
+ self.num_hidden_layers = num_hidden_layers
55
+ self.num_attention_heads = num_attention_heads
56
+ self.hidden_act = hidden_act
57
+ self.hidden_dropout_prob = hidden_dropout_prob
58
+ self.attention_probs_dropout_prob = attention_probs_dropout_prob
59
+ self.initializer_range = initializer_range
60
+ self.layer_norm_eps = layer_norm_eps
61
+ self.image_size = image_size
62
+ self.patch_size = patch_size
63
+ self.num_channels = num_channels
64
+ self.qkv_bias = qkv_bias
65
+ self.wv_planes = wv_planes
66
+ self.mlp_ratio = mlp_ratio
67
+ self.global_pool = global_pool
68
+ self.default_wavelengths = default_wavelengths
69
+ self.default_image_mean = default_image_mean
70
+ self.default_image_std = default_image_std
71
+ self.head_dropout = head_dropout
72
+ self.num_labels = num_labels
73
+
74
+ if intermediate_size is None:
75
+ self.intermediate_size = int(hidden_size * mlp_ratio)
76
+ else:
77
+ self.intermediate_size = intermediate_size
78
+
79
+
80
+ def get_1d_sincos_pos_embed_from_grid_torch(embed_dim, pos):
81
+ assert embed_dim % 2 == 0
82
+ omega = torch.arange(embed_dim // 2, dtype=torch.float32, device=pos.device)
83
+ omega /= embed_dim / 2.0
84
+ omega = 1.0 / 10000**omega
85
+
86
+ pos = pos.reshape(-1)
87
+ out = torch.einsum("m,d->md", pos, omega)
88
+
89
+ emb_sin = torch.sin(out)
90
+ emb_cos = torch.cos(out)
91
+
92
+ return torch.cat([emb_sin, emb_cos], dim=1)
93
+
94
+
95
+ class TransformerWeightGenerator(nn.Module):
96
+ def __init__(self, input_dim, output_dim, embed_dim, num_heads=4, num_layers=1):
97
+ super().__init__()
98
+ encoder_layer = nn.TransformerEncoderLayer(
99
+ d_model=input_dim,
100
+ nhead=num_heads,
101
+ activation="gelu",
102
+ norm_first=False,
103
+ batch_first=False,
104
+ dropout=False,
105
+ )
106
+ self.transformer_encoder = nn.TransformerEncoder(
107
+ encoder_layer, num_layers=num_layers, enable_nested_tensor=False
108
+ )
109
+
110
+ self.fc_weight = nn.Linear(input_dim, output_dim)
111
+ self.fc_bias = nn.Linear(input_dim, embed_dim)
112
+ self.wt_num = 128
113
+ self.weight_tokens = nn.Parameter(torch.empty([self.wt_num, input_dim]))
114
+ self.bias_token = nn.Parameter(torch.empty([1, input_dim]))
115
+
116
+ torch.nn.init.normal_(self.weight_tokens, std=0.02)
117
+ torch.nn.init.normal_(self.bias_token, std=0.02)
118
+
119
+ def forward(self, x):
120
+ pos_wave = x
121
+ x = torch.cat([self.weight_tokens, pos_wave], dim=0)
122
+ x = torch.cat([x, self.bias_token], dim=0)
123
+ transformer_output = self.transformer_encoder(x)
124
+ weights = self.fc_weight(transformer_output[self.wt_num : -1] + pos_wave)
125
+ bias = self.fc_bias(transformer_output[-1])
126
+ return weights, bias
127
+
128
+
129
+ class FCResLayer(nn.Module):
130
+ def __init__(self, linear_size=128):
131
+ super().__init__()
132
+ self.nonlin1 = nn.ReLU(inplace=True)
133
+ self.nonlin2 = nn.ReLU(inplace=True)
134
+ self.w1 = nn.Linear(linear_size, linear_size)
135
+ self.w2 = nn.Linear(linear_size, linear_size)
136
+
137
+ def forward(self, x):
138
+ y = self.w1(x)
139
+ y = self.nonlin1(y)
140
+ y = self.w2(y)
141
+ y = self.nonlin2(y)
142
+ return x + y
143
+
144
+
145
+ class DOFADynamicPatchEmbed(nn.Module):
146
+ def __init__(self, wv_planes, inter_dim=128, kernel_size=16, embed_dim=768):
147
+ super().__init__()
148
+ self.kernel_size = kernel_size
149
+ self.wv_planes = wv_planes
150
+ self.embed_dim = embed_dim
151
+ self._num_kernel = self.kernel_size * self.kernel_size * self.embed_dim
152
+ self.inter_dim = inter_dim
153
+ self.patch_size = (kernel_size, kernel_size)
154
+
155
+ self.weight_generator = TransformerWeightGenerator(wv_planes, self._num_kernel, embed_dim)
156
+ self.scaler = 0.01
157
+ self.fclayer = FCResLayer(wv_planes)
158
+ self.weight_generator.apply(self._weight_init)
159
+ self.fclayer.apply(self._weight_init)
160
+
161
+ def _weight_init(self, module):
162
+ if isinstance(module, nn.Linear):
163
+ init.xavier_uniform_(module.weight)
164
+ module.bias.data.fill_(0.01)
165
+
166
+ def forward(self, pixel_values, wavelengths):
167
+ inplanes = wavelengths.size(0)
168
+ waves = get_1d_sincos_pos_embed_from_grid_torch(self.wv_planes, wavelengths * 1000)
169
+ waves = self.fclayer(waves)
170
+ weight, bias = self.weight_generator(waves)
171
+
172
+ dynamic_weight = weight.view(inplanes, self.kernel_size, self.kernel_size, self.embed_dim)
173
+ dynamic_weight = dynamic_weight.permute([3, 0, 1, 2])
174
+
175
+ if bias is not None:
176
+ bias = bias.view([self.embed_dim]) * self.scaler
177
+
178
+ weights = dynamic_weight * self.scaler
179
+ dynamic_out = F.conv2d(
180
+ pixel_values, weights, bias=bias, stride=self.kernel_size, padding=1, dilation=1
181
+ )
182
+ return dynamic_out.flatten(2).transpose(1, 2), waves
183
+
184
+
185
+ def _prepare_wavelengths(wavelengths, pixel_values, default_wavelengths=None):
186
+ if wavelengths is None:
187
+ if default_wavelengths is None:
188
+ raise ValueError(
189
+ "DOFA requires per-channel wavelengths. Pass `wavelengths` to the model or image processor, "
190
+ "or set `default_wavelengths` in the model config."
191
+ )
192
+ wavelengths = default_wavelengths
193
+
194
+ if not torch.is_tensor(wavelengths):
195
+ wavelengths = torch.tensor(wavelengths, dtype=torch.float32, device=pixel_values.device)
196
+ else:
197
+ wavelengths = wavelengths.to(device=pixel_values.device, dtype=torch.float32)
198
+
199
+ if wavelengths.dim() == 2 and wavelengths.shape[0] == 1:
200
+ wavelengths = wavelengths.squeeze(0)
201
+ elif wavelengths.dim() == 2 and wavelengths.shape[0] == pixel_values.shape[0]:
202
+ if not torch.allclose(wavelengths[0], wavelengths):
203
+ raise ValueError("DOFA currently expects identical wavelengths for all items in a batch.")
204
+ wavelengths = wavelengths[0]
205
+
206
+ if wavelengths.dim() != 1:
207
+ raise ValueError("`wavelengths` must be a 1D tensor/list with one value per input channel.")
208
+
209
+ if wavelengths.shape[0] != pixel_values.shape[1]:
210
+ raise ValueError(
211
+ f"Expected {pixel_values.shape[1]} wavelengths for {pixel_values.shape[1]} channels, "
212
+ f"but got {wavelengths.shape[0]}."
213
+ )
214
+
215
+ return wavelengths
216
+
217
+
218
+ class DOFAPreTrainedModel(PreTrainedModel):
219
+ config_class = DOFAConfig
220
+ base_model_prefix = "dofa"
221
+ main_input_name = "pixel_values"
222
+ input_modalities = ("image",)
223
+ supports_gradient_checkpointing = True
224
+ _no_split_modules = ["Block"]
225
+ _supports_sdpa = False
226
+
227
+ def _init_weights(self, module):
228
+ super()._init_weights(module)
229
+ if isinstance(module, DOFAModel):
230
+ if hasattr(module, "pos_embed"):
231
+ nn.init.trunc_normal_(module.pos_embed, std=self.config.initializer_range)
232
+ if hasattr(module, "cls_token"):
233
+ nn.init.trunc_normal_(module.cls_token, std=self.config.initializer_range)
234
+
235
+
236
+ class DOFAModel(DOFAPreTrainedModel):
237
+ def __init__(self, config: DOFAConfig, add_pooling_layer: bool = True):
238
+ super().__init__(config)
239
+ self.config = config
240
+
241
+ image_size = config.image_size if isinstance(config.image_size, int) else config.image_size[0]
242
+ self.patch_embed = DOFADynamicPatchEmbed(
243
+ wv_planes=config.wv_planes,
244
+ inter_dim=config.wv_planes,
245
+ kernel_size=config.patch_size,
246
+ embed_dim=config.hidden_size,
247
+ )
248
+ self.num_patches = (image_size // config.patch_size) ** 2
249
+ self.cls_token = nn.Parameter(torch.zeros(1, 1, config.hidden_size))
250
+ self.pos_embed = nn.Parameter(
251
+ torch.zeros(1, self.num_patches + 1, config.hidden_size), requires_grad=False
252
+ )
253
+
254
+ norm_layer = partial(nn.LayerNorm, eps=config.layer_norm_eps)
255
+ self.blocks = nn.ModuleList(
256
+ [
257
+ Block(
258
+ config.hidden_size,
259
+ config.num_attention_heads,
260
+ config.mlp_ratio,
261
+ qkv_bias=config.qkv_bias,
262
+ norm_layer=norm_layer,
263
+ )
264
+ for _ in range(config.num_hidden_layers)
265
+ ]
266
+ )
267
+
268
+ self.global_pool = config.global_pool
269
+ if self.global_pool:
270
+ self.fc_norm = norm_layer(config.hidden_size)
271
+ self.norm = None
272
+ else:
273
+ self.fc_norm = None
274
+ self.norm = norm_layer(config.hidden_size)
275
+
276
+ self.add_pooling_layer = add_pooling_layer
277
+ self.post_init()
278
+
279
+ def forward_features(self, pixel_values, wavelengths):
280
+ patch_tokens, _ = self.patch_embed(pixel_values, wavelengths)
281
+ patch_tokens = patch_tokens + self.pos_embed[:, 1:, :]
282
+
283
+ cls_token = self.cls_token + self.pos_embed[:, :1, :]
284
+ cls_tokens = cls_token.expand(pixel_values.shape[0], -1, -1)
285
+ hidden_states = torch.cat((cls_tokens, patch_tokens), dim=1)
286
+
287
+ for block in self.blocks:
288
+ hidden_states = block(hidden_states)
289
+
290
+ if self.global_pool:
291
+ pooled_output = self.fc_norm(hidden_states[:, 1:, :].mean(dim=1))
292
+ else:
293
+ pooled_output = self.norm(hidden_states)[:, 0]
294
+
295
+ return hidden_states, pooled_output
296
+
297
+ def forward(
298
+ self,
299
+ pixel_values: Optional[torch.Tensor] = None,
300
+ wavelengths: Optional[Union[torch.Tensor, list]] = None,
301
+ return_dict: Optional[bool] = None,
302
+ **kwargs: Unpack[TransformersKwargs],
303
+ ) -> BaseModelOutputWithPooling:
304
+ if pixel_values is None:
305
+ raise ValueError("You must specify `pixel_values`")
306
+
307
+ pixel_values = pixel_values.to(dtype=self.dtype)
308
+
309
+ if return_dict is None:
310
+ return_dict = self.config.use_return_dict
311
+
312
+ wavelengths = _prepare_wavelengths(wavelengths, pixel_values, self.config.default_wavelengths)
313
+ last_hidden_state, pooled_output = self.forward_features(pixel_values, wavelengths)
314
+
315
+ if not self.add_pooling_layer:
316
+ pooled_output = None
317
+
318
+ if not return_dict:
319
+ return (last_hidden_state, pooled_output)
320
+
321
+ return BaseModelOutputWithPooling(last_hidden_state=last_hidden_state, pooler_output=pooled_output)
322
+
323
+
324
+ class DOFAForImageClassification(DOFAPreTrainedModel):
325
+ def __init__(self, config: DOFAConfig):
326
+ super().__init__(config)
327
+ self.num_labels = config.num_labels
328
+ self.dofa = DOFAModel(config, add_pooling_layer=True)
329
+ self.head_drop = nn.Dropout(config.head_dropout)
330
+ self.classifier = (
331
+ nn.Linear(config.hidden_size, config.num_labels) if config.num_labels > 0 else nn.Identity()
332
+ )
333
+ self.post_init()
334
+
335
+ def forward(
336
+ self,
337
+ pixel_values: Optional[torch.Tensor] = None,
338
+ wavelengths: Optional[Union[torch.Tensor, list]] = None,
339
+ labels: Optional[torch.Tensor] = None,
340
+ return_dict: Optional[bool] = None,
341
+ **kwargs: Unpack[TransformersKwargs],
342
+ ) -> ImageClassifierOutput:
343
+ outputs = self.dofa(
344
+ pixel_values=pixel_values,
345
+ wavelengths=wavelengths,
346
+ return_dict=True,
347
+ **kwargs,
348
+ )
349
+ pooled_output = self.head_drop(outputs.pooler_output)
350
+ logits = self.classifier(pooled_output)
351
+
352
+ loss = None
353
+ if labels is not None:
354
+ loss = self.loss_function(labels, logits, self.config, **kwargs)
355
+
356
+ if not return_dict:
357
+ output = (logits,) + outputs[1:]
358
+ return ((loss,) + output) if loss is not None else output
359
+
360
+ return ImageClassifierOutput(
361
+ loss=loss,
362
+ logits=logits,
363
+ hidden_states=outputs.hidden_states,
364
+ attentions=outputs.attentions,
365
+ )
366
+
367
+
368
+ __all__ = ["DOFAConfig", "DOFAForImageClassification", "DOFAModel", "DOFAPreTrainedModel"]
dofa-large-patch16-224/pipeline_dofa.py ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 The DOFA Authors and The HuggingFace Inc. team.
2
+ """Custom DOFA feature extraction pipeline for trust_remote_code loading."""
3
+
4
+ from typing import Any, Union
5
+
6
+ from transformers.pipelines.base import GenericTensor, build_pipeline_init_args
7
+ from transformers.pipelines.image_feature_extraction import ImageFeatureExtractionPipeline
8
+ from transformers.utils import add_end_docstrings, is_vision_available
9
+
10
+
11
+ if is_vision_available():
12
+ from transformers.image_utils import load_image
13
+
14
+
15
+ @add_end_docstrings(
16
+ build_pipeline_init_args(has_image_processor=True),
17
+ """
18
+ wavelengths (`list[float]`, *optional*):
19
+ Per-channel wavelengths in micrometers. Defaults to Sentinel-2 bands from config.
20
+ image_mean (`list[float]`, *optional*):
21
+ Per-channel normalization mean values.
22
+ image_std (`list[float]`, *optional*):
23
+ Per-channel normalization standard deviation values.
24
+ pool (`bool`, *optional*, defaults to `False`):
25
+ Whether to return pooled features instead of sequence hidden states.
26
+ """,
27
+ )
28
+ class DOFAImageFeatureExtractionPipeline(ImageFeatureExtractionPipeline):
29
+ """
30
+ DOFA wavelength-conditioned multispectral feature extraction pipeline.
31
+
32
+ This pipeline can be loaded from [`pipeline`] using the task identifier `"dofa-feature-extraction"`
33
+ with `trust_remote_code=True`.
34
+
35
+ Example:
36
+
37
+ ```python
38
+ >>> from transformers import pipeline
39
+
40
+ >>> pipe = pipeline(
41
+ ... task="dofa-feature-extraction",
42
+ ... model="/path/to/dofa-base-patch16-224",
43
+ ... trust_remote_code=True,
44
+ ... )
45
+ >>> features = pipe(image_array, pool=True, return_tensors=True)
46
+ ```
47
+ """
48
+
49
+ def _sanitize_parameters(
50
+ self,
51
+ image_processor_kwargs=None,
52
+ return_tensors=None,
53
+ pool=None,
54
+ wavelengths=None,
55
+ image_mean=None,
56
+ image_std=None,
57
+ **kwargs,
58
+ ):
59
+ preprocess_params = {} if image_processor_kwargs is None else dict(image_processor_kwargs)
60
+
61
+ if wavelengths is not None:
62
+ preprocess_params["wavelengths"] = wavelengths
63
+ if image_mean is not None:
64
+ preprocess_params["image_mean"] = image_mean
65
+ if image_std is not None:
66
+ preprocess_params["image_std"] = image_std
67
+ if "timeout" in kwargs:
68
+ preprocess_params["timeout"] = kwargs["timeout"]
69
+
70
+ postprocess_params = {}
71
+ if pool is not None:
72
+ postprocess_params["pool"] = pool
73
+ if return_tensors is not None:
74
+ postprocess_params["return_tensors"] = return_tensors
75
+
76
+ return preprocess_params, {}, postprocess_params
77
+
78
+ def preprocess(self, image, timeout=None, **image_processor_kwargs) -> dict[str, GenericTensor]:
79
+ if not isinstance(image, (list, tuple)) and not hasattr(image, "shape"):
80
+ image = load_image(image, timeout=timeout)
81
+ model_inputs = self.image_processor(image, return_tensors="pt", **image_processor_kwargs)
82
+ model_inputs = model_inputs.to(self.dtype)
83
+ return model_inputs
84
+
85
+ def __call__(self, *args: Union[str, Any, list[Any]], **kwargs: Any) -> list[Any]:
86
+ """
87
+ Extract features from multispectral inputs.
88
+
89
+ Accepts numpy arrays (HWC), file paths, URLs, or PIL images in addition to the
90
+ standard image inputs supported by [`ImageFeatureExtractionPipeline`].
91
+ """
92
+ return super().__call__(*args, **kwargs)
93
+
94
+
95
+ __all__ = ["DOFAImageFeatureExtractionPipeline"]
dofa-large-patch16-224/preprocessor_config.json ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "image_processor_type": "DOFAImageProcessor",
3
+ "size": {
4
+ "height": 224,
5
+ "width": 224
6
+ },
7
+ "do_resize": true,
8
+ "do_rescale": true,
9
+ "do_normalize": true,
10
+ "do_convert_rgb": false,
11
+ "rescale_factor": 0.00392156862745098,
12
+ "auto_map": {
13
+ "AutoImageProcessor": "image_processing_dofa.DOFAImageProcessor"
14
+ },
15
+ "default_wavelengths": [
16
+ 0.665,
17
+ 0.56,
18
+ 0.49,
19
+ 0.705,
20
+ 0.74,
21
+ 0.783,
22
+ 0.842,
23
+ 1.61,
24
+ 2.19
25
+ ],
26
+ "image_mean": [
27
+ 114.11,
28
+ 114.82,
29
+ 126.64,
30
+ 84.34,
31
+ 97.85,
32
+ 103.94,
33
+ 101.44,
34
+ 72.33,
35
+ 56.67
36
+ ],
37
+ "image_std": [
38
+ 77.84,
39
+ 69.97,
40
+ 67.42,
41
+ 64.57,
42
+ 61.73,
43
+ 61.34,
44
+ 60.3,
45
+ 47.89,
46
+ 42.56
47
+ ]
48
+ }
test_dofa.py ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Quick CLI smoke test for a self-contained DOFA checkpoint folder."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ from pathlib import Path
9
+
10
+ import numpy as np
11
+ import torch
12
+ from transformers import pipeline
13
+
14
+
15
+ def parse_args() -> argparse.Namespace:
16
+ repo_root = Path(__file__).resolve().parent
17
+ parser = argparse.ArgumentParser(description="Run DOFA feature extraction on dummy or real input.")
18
+ parser.add_argument(
19
+ "--model",
20
+ type=Path,
21
+ default=repo_root / "dofa-base-patch16-224",
22
+ help="Path to a checkpoint folder (default: dofa-base-patch16-224)",
23
+ )
24
+ parser.add_argument(
25
+ "--image",
26
+ type=Path,
27
+ default=None,
28
+ help="Optional image path (.npy HWC array or image readable by rasterio/PIL)",
29
+ )
30
+ parser.add_argument(
31
+ "--pool",
32
+ action="store_true",
33
+ default=True,
34
+ help="Return pooled features (default: True)",
35
+ )
36
+ parser.add_argument(
37
+ "--no-pool",
38
+ action="store_true",
39
+ help="Return sequence features instead of pooled output",
40
+ )
41
+ parser.add_argument(
42
+ "--device",
43
+ default=None,
44
+ help="Torch device, e.g. cuda or cpu (default: auto)",
45
+ )
46
+ return parser.parse_args()
47
+
48
+
49
+ def load_image(path: Path, num_channels: int) -> np.ndarray:
50
+ if path.suffix == ".npy":
51
+ array = np.load(path)
52
+ if array.ndim != 3:
53
+ raise ValueError(f"Expected HWC numpy array, got shape {array.shape}")
54
+ return array
55
+
56
+ try:
57
+ import rasterio
58
+ except ImportError as exc:
59
+ raise ImportError("Install rasterio to load geospatial images, or pass a .npy file.") from exc
60
+
61
+ with rasterio.open(path) as src:
62
+ array = src.read()
63
+ array = np.transpose(array, (1, 2, 0))
64
+ return array
65
+
66
+
67
+ def main() -> None:
68
+ args = parse_args()
69
+ model_dir = args.model.resolve()
70
+ if not model_dir.is_dir():
71
+ raise SystemExit(f"Model folder not found: {model_dir}")
72
+
73
+ config_path = model_dir / "config.json"
74
+ with open(config_path, encoding="utf-8") as handle:
75
+ config = json.load(handle)
76
+
77
+ num_channels = config.get("num_channels") or len(config["default_wavelengths"])
78
+
79
+ if args.image is None:
80
+ image = np.random.randint(0, 255, (224, 224, num_channels), dtype=np.uint8)
81
+ source = f"random dummy array ({num_channels} channels)"
82
+ else:
83
+ image = load_image(args.image.resolve(), num_channels)
84
+ source = str(args.image)
85
+
86
+ device = args.device or ("cuda" if torch.cuda.is_available() else "cpu")
87
+ pool = not args.no_pool
88
+
89
+ print(f"model: {model_dir}")
90
+ print(f"input: {source}")
91
+ print(f"device: {device}")
92
+ print(f"pool: {pool}")
93
+
94
+ pipe = pipeline(
95
+ task="dofa-feature-extraction",
96
+ model=str(model_dir),
97
+ trust_remote_code=True,
98
+ device=device,
99
+ )
100
+
101
+ features = pipe(image, pool=pool, return_tensors=True)
102
+ print(f"output: {tuple(features.shape)} dtype={features.dtype}")
103
+ print("OK")
104
+
105
+
106
+ if __name__ == "__main__":
107
+ main()