markus-42 commited on
Commit
94cbfa6
·
0 Parent(s):

Initial commit

Browse files
.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ .ipynb_checkpoints/
5
+ .vscode/
6
+
7
+ # Weights in output/scratch directories (tracked files in model subfolders use LFS via .gitattributes)
8
+ eval_output/**/*.safetensors
9
+ eval_output/**/*.pth
10
+ eval_output/**/*.bin
11
+ eval_output/**/*.ckpt
12
+
13
+ # Output directories
14
+ eval_output/
15
+ infer_output/
16
+ visualizations/
17
+
18
+ # Virtual environment
19
+ .venv/
20
+
21
+ # External Repositories
22
+ dinov3/
Firefly_RGB/config.json ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": ["FireflyForSemanticSegmentationRGB"],
3
+ "model_type": "firefly-rgb",
4
+ "num_labels": 15,
5
+ "backbone": "dinov3_vitb16",
6
+ "image_size": 640,
7
+ "embedding_dim": 256,
8
+ "backbone_embed_dim": 768,
9
+ "patch_size": 16,
10
+ "dropout_ratio": 0.1,
11
+ "semantic_loss_ignore_index": 255,
12
+ "id2label": {
13
+ "0": "road",
14
+ "1": "walkway",
15
+ "2": "dirt",
16
+ "3": "gravel",
17
+ "4": "grass",
18
+ "5": "vegetation",
19
+ "6": "tree",
20
+ "7": "ground_obstacles",
21
+ "8": "vehicle",
22
+ "9": "water",
23
+ "10": "building",
24
+ "11": "roof",
25
+ "12": "parking_lot",
26
+ "13": "constructions",
27
+ "14": "truck"
28
+ },
29
+ "label2id": {
30
+ "road": 0,
31
+ "walkway": 1,
32
+ "dirt": 2,
33
+ "gravel": 3,
34
+ "grass": 4,
35
+ "vegetation": 5,
36
+ "tree": 6,
37
+ "ground_obstacles": 7,
38
+ "vehicle": 8,
39
+ "water": 9,
40
+ "building": 10,
41
+ "roof": 11,
42
+ "parking_lot": 12,
43
+ "constructions": 13,
44
+ "truck": 14
45
+ },
46
+ "transformers_version": "4.37.2"
47
+ }
Firefly_RGB/model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:71d74d0fb68508e515c97a09081ca6e613f9bef4dfae8fc00f0b6f3fcdf2be41
3
+ size 352263468
Firefly_RGB/preprocessor_config.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "processor_class": "SegformerImageProcessor",
3
+ "do_normalize": true,
4
+ "do_reduce_labels": true,
5
+ "do_rescale": true,
6
+ "do_resize": true,
7
+ "image_mean": [
8
+ 0.485,
9
+ 0.456,
10
+ 0.406
11
+ ],
12
+ "image_processor_type": "SegformerImageProcessor",
13
+ "image_std": [
14
+ 0.229,
15
+ 0.224,
16
+ 0.225
17
+ ],
18
+ "resample": 0,
19
+ "rescale_factor": 0.00392156862745098,
20
+ "size": {
21
+ "height": 640,
22
+ "width": 640
23
+ }
24
+ }
Firefly_Thermal/config.json ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": ["FireflyForSemanticSegmentationThermal"],
3
+ "model_type": "firefly-thermal",
4
+ "num_labels": 15,
5
+ "backbone": "dinov3_vitb16",
6
+ "image_size": 640,
7
+ "embedding_dim": 256,
8
+ "backbone_embed_dim": 768,
9
+ "patch_size": 16,
10
+ "num_layers": 12,
11
+ "rein_token_length": 100,
12
+ "feature_layers": [2, 5, 8, 11],
13
+ "dropout_ratio": 0.1,
14
+ "semantic_loss_ignore_index": 255,
15
+ "id2label": {
16
+ "0": "road",
17
+ "1": "walkway",
18
+ "2": "dirt",
19
+ "3": "gravel",
20
+ "4": "grass",
21
+ "5": "vegetation",
22
+ "6": "tree",
23
+ "7": "ground_obstacles",
24
+ "8": "vehicle",
25
+ "9": "water",
26
+ "10": "building",
27
+ "11": "roof",
28
+ "12": "parking_lot",
29
+ "13": "constructions",
30
+ "14": "truck"
31
+ },
32
+ "label2id": {
33
+ "road": 0,
34
+ "walkway": 1,
35
+ "dirt": 2,
36
+ "gravel": 3,
37
+ "grass": 4,
38
+ "vegetation": 5,
39
+ "tree": 6,
40
+ "ground_obstacles": 7,
41
+ "vehicle": 8,
42
+ "water": 9,
43
+ "building": 10,
44
+ "roof": 11,
45
+ "parking_lot": 12,
46
+ "constructions": 13,
47
+ "truck": 14
48
+ },
49
+ "transformers_version": "4.37.2"
50
+ }
Firefly_Thermal/model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:cc3e5f8c6e3426dd8379ae7ca476138e76e76adfbd890e23b77ce49077eb69f0
3
+ size 362250440
Firefly_Thermal/preprocessor_config.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "processor_class": "SegformerImageProcessor",
3
+ "do_normalize": true,
4
+ "do_reduce_labels": true,
5
+ "do_rescale": true,
6
+ "do_resize": true,
7
+ "image_mean": [
8
+ 0.3990,
9
+ 0.3990,
10
+ 0.3990
11
+ ],
12
+ "image_processor_type": "SegformerImageProcessor",
13
+ "image_std": [
14
+ 0.2009,
15
+ 0.2009,
16
+ 0.2009
17
+ ],
18
+ "resample": 0,
19
+ "rescale_factor": 0.00392156862745098,
20
+ "size": {
21
+ "height": 640,
22
+ "width": 640
23
+ }
24
+ }
LICENSE ADDED
@@ -0,0 +1,438 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Attribution-NonCommercial-ShareAlike 4.0 International
2
+
3
+ =======================================================================
4
+
5
+ Creative Commons Corporation ("Creative Commons") is not a law firm and
6
+ does not provide legal services or legal advice. Distribution of
7
+ Creative Commons public licenses does not create a lawyer-client or
8
+ other relationship. Creative Commons makes its licenses and related
9
+ information available on an "as-is" basis. Creative Commons gives no
10
+ warranties regarding its licenses, any material licensed under their
11
+ terms and conditions, or any related information. Creative Commons
12
+ disclaims all liability for damages resulting from their use to the
13
+ fullest extent possible.
14
+
15
+ Using Creative Commons Public Licenses
16
+
17
+ Creative Commons public licenses provide a standard set of terms and
18
+ conditions that creators and other rights holders may use to share
19
+ original works of authorship and other material subject to copyright
20
+ and certain other rights specified in the public license below. The
21
+ following considerations are for informational purposes only, are not
22
+ exhaustive, and do not form part of our licenses.
23
+
24
+ Considerations for licensors: Our public licenses are
25
+ intended for use by those authorized to give the public
26
+ permission to use material in ways otherwise restricted by
27
+ copyright and certain other rights. Our licenses are
28
+ irrevocable. Licensors should read and understand the terms
29
+ and conditions of the license they choose before applying it.
30
+ Licensors should also secure all rights necessary before
31
+ applying our licenses so that the public can reuse the
32
+ material as expected. Licensors should clearly mark any
33
+ material not subject to the license. This includes other CC-
34
+ licensed material, or material used under an exception or
35
+ limitation to copyright. More considerations for licensors:
36
+ wiki.creativecommons.org/Considerations_for_licensors
37
+
38
+ Considerations for the public: By using one of our public
39
+ licenses, a licensor grants the public permission to use the
40
+ licensed material under specified terms and conditions. If
41
+ the licensor's permission is not necessary for any reason--for
42
+ example, because of any applicable exception or limitation to
43
+ copyright--then that use is not regulated by the license. Our
44
+ licenses grant only permissions under copyright and certain
45
+ other rights that a licensor has authority to grant. Use of
46
+ the licensed material may still be restricted for other
47
+ reasons, including because others have copyright or other
48
+ rights in the material. A licensor may make special requests,
49
+ such as asking that all changes be marked or described.
50
+ Although not required by our licenses, you are encouraged to
51
+ respect those requests where reasonable. More considerations
52
+ for the public:
53
+ wiki.creativecommons.org/Considerations_for_licensees
54
+
55
+ =======================================================================
56
+
57
+ Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
58
+ Public License
59
+
60
+ By exercising the Licensed Rights (defined below), You accept and agree
61
+ to be bound by the terms and conditions of this Creative Commons
62
+ Attribution-NonCommercial-ShareAlike 4.0 International Public License
63
+ ("Public License"). To the extent this Public License may be
64
+ interpreted as a contract, You are granted the Licensed Rights in
65
+ consideration of Your acceptance of these terms and conditions, and the
66
+ Licensor grants You such rights in consideration of benefits the
67
+ Licensor receives from making the Licensed Material available under
68
+ these terms and conditions.
69
+
70
+
71
+ Section 1 -- Definitions.
72
+
73
+ a. Adapted Material means material subject to Copyright and Similar
74
+ Rights that is derived from or based upon the Licensed Material
75
+ and in which the Licensed Material is translated, altered,
76
+ arranged, transformed, or otherwise modified in a manner requiring
77
+ permission under the Copyright and Similar Rights held by the
78
+ Licensor. For purposes of this Public License, where the Licensed
79
+ Material is a musical work, performance, or sound recording,
80
+ Adapted Material is always produced where the Licensed Material is
81
+ synched in timed relation with a moving image.
82
+
83
+ b. Adapter's License means the license You apply to Your Copyright
84
+ and Similar Rights in Your contributions to Adapted Material in
85
+ accordance with the terms and conditions of this Public License.
86
+
87
+ c. BY-NC-SA Compatible License means a license listed at
88
+ creativecommons.org/compatiblelicenses, approved by Creative
89
+ Commons as essentially the equivalent of this Public License.
90
+
91
+ d. Copyright and Similar Rights means copyright and/or similar rights
92
+ closely related to copyright including, without limitation,
93
+ performance, broadcast, sound recording, and Sui Generis Database
94
+ Rights, without regard to how the rights are labeled or
95
+ categorized. For purposes of this Public License, the rights
96
+ specified in Section 2(b)(1)-(2) are not Copyright and Similar
97
+ Rights.
98
+
99
+ e. Effective Technological Measures means those measures that, in the
100
+ absence of proper authority, may not be circumvented under laws
101
+ fulfilling obligations under Article 11 of the WIPO Copyright
102
+ Treaty adopted on December 20, 1996, and/or similar international
103
+ agreements.
104
+
105
+ f. Exceptions and Limitations means fair use, fair dealing, and/or
106
+ any other exception or limitation to Copyright and Similar Rights
107
+ that applies to Your use of the Licensed Material.
108
+
109
+ g. License Elements means the license attributes listed in the name
110
+ of a Creative Commons Public License. The License Elements of this
111
+ Public License are Attribution, NonCommercial, and ShareAlike.
112
+
113
+ h. Licensed Material means the artistic or literary work, database,
114
+ or other material to which the Licensor applied this Public
115
+ License.
116
+
117
+ i. Licensed Rights means the rights granted to You subject to the
118
+ terms and conditions of this Public License, which are limited to
119
+ all Copyright and Similar Rights that apply to Your use of the
120
+ Licensed Material and that the Licensor has authority to license.
121
+
122
+ j. Licensor means the individual(s) or entity(ies) granting rights
123
+ under this Public License.
124
+
125
+ k. NonCommercial means not primarily intended for or directed towards
126
+ commercial advantage or monetary compensation. For purposes of
127
+ this Public License, the exchange of the Licensed Material for
128
+ other material subject to Copyright and Similar Rights by digital
129
+ file-sharing or similar means is NonCommercial provided there is
130
+ no payment of monetary compensation in connection with the
131
+ exchange.
132
+
133
+ l. Share means to provide material to the public by any means or
134
+ process that requires permission under the Licensed Rights, such
135
+ as reproduction, public display, public performance, distribution,
136
+ dissemination, communication, or importation, and to make material
137
+ available to the public including in ways that members of the
138
+ public may access the material from a place and at a time
139
+ individually chosen by them.
140
+
141
+ m. Sui Generis Database Rights means rights other than copyright
142
+ resulting from Directive 96/9/EC of the European Parliament and of
143
+ the Council of 11 March 1996 on the legal protection of databases,
144
+ as amended and/or succeeded, as well as other essentially
145
+ equivalent rights anywhere in the world.
146
+
147
+ n. You means the individual or entity exercising the Licensed Rights
148
+ under this Public License. Your has a corresponding meaning.
149
+
150
+
151
+ Section 2 -- Scope.
152
+
153
+ a. License grant.
154
+
155
+ 1. Subject to the terms and conditions of this Public License,
156
+ the Licensor hereby grants You a worldwide, royalty-free,
157
+ non-sublicensable, non-exclusive, irrevocable license to
158
+ exercise the Licensed Rights in the Licensed Material to:
159
+
160
+ a. reproduce and Share the Licensed Material, in whole or
161
+ in part, for NonCommercial purposes only; and
162
+
163
+ b. produce, reproduce, and Share Adapted Material for
164
+ NonCommercial purposes only.
165
+
166
+ 2. Exceptions and Limitations. For the avoidance of doubt, where
167
+ Exceptions and Limitations apply to Your use, this Public
168
+ License does not apply, and You do not need to comply with
169
+ its terms and conditions.
170
+
171
+ 3. Term. The term of this Public License is specified in Section
172
+ 6(a).
173
+
174
+ 4. Media and formats; technical modifications allowed. The
175
+ Licensor authorizes You to exercise the Licensed Rights in
176
+ all media and formats whether now known or hereafter created,
177
+ and to make technical modifications necessary to do so. The
178
+ Licensor waives and/or agrees not to assert any right or
179
+ authority to forbid You from making technical modifications
180
+ necessary to exercise the Licensed Rights, including
181
+ technical modifications necessary to circumvent Effective
182
+ Technological Measures. For purposes of this Public License,
183
+ simply making modifications authorized by this Section 2(a)
184
+ (4) never produces Adapted Material.
185
+
186
+ 5. Downstream recipients.
187
+
188
+ a. Offer from the Licensor -- Licensed Material. Every
189
+ recipient of the Licensed Material automatically
190
+ receives an offer from the Licensor to exercise the
191
+ Licensed Rights under the terms and conditions of this
192
+ Public License.
193
+
194
+ b. Additional offer from the Licensor -- Adapted Material.
195
+ Every recipient of Adapted Material from You
196
+ automatically receives an offer from the Licensor to
197
+ exercise the Licensed Rights in the Adapted Material
198
+ under the conditions of the Adapter's License You apply.
199
+
200
+ c. No downstream restrictions. You may not offer or impose
201
+ any additional or different terms or conditions on, or
202
+ apply any Effective Technological Measures to, the
203
+ Licensed Material if doing so restricts exercise of the
204
+ Licensed Rights by any recipient of the Licensed
205
+ Material.
206
+
207
+ 6. No endorsement. Nothing in this Public License constitutes or
208
+ may be construed as permission to assert or imply that You
209
+ are, or that Your use of the Licensed Material is, connected
210
+ with, or sponsored, endorsed, or granted official status by,
211
+ the Licensor or others designated to receive attribution as
212
+ provided in Section 3(a)(1)(A)(i).
213
+
214
+ b. Other rights.
215
+
216
+ 1. Moral rights, such as the right of integrity, are not
217
+ licensed under this Public License, nor are publicity,
218
+ privacy, and/or other similar personality rights; however, to
219
+ the extent possible, the Licensor waives and/or agrees not to
220
+ assert any such rights held by the Licensor to the limited
221
+ extent necessary to allow You to exercise the Licensed
222
+ Rights, but not otherwise.
223
+
224
+ 2. Patent and trademark rights are not licensed under this
225
+ Public License.
226
+
227
+ 3. To the extent possible, the Licensor waives any right to
228
+ collect royalties from You for the exercise of the Licensed
229
+ Rights, whether directly or through a collecting society
230
+ under any voluntary or waivable statutory or compulsory
231
+ licensing scheme. In all other cases the Licensor expressly
232
+ reserves any right to collect such royalties, including when
233
+ the Licensed Material is used other than for NonCommercial
234
+ purposes.
235
+
236
+
237
+ Section 3 -- License Conditions.
238
+
239
+ Your exercise of the Licensed Rights is expressly made subject to the
240
+ following conditions.
241
+
242
+ a. Attribution.
243
+
244
+ 1. If You Share the Licensed Material (including in modified
245
+ form), You must:
246
+
247
+ a. retain the following if it is supplied by the Licensor
248
+ with the Licensed Material:
249
+
250
+ i. identification of the creator(s) of the Licensed
251
+ Material and any others designated to receive
252
+ attribution, in any reasonable manner requested by
253
+ the Licensor (including by pseudonym if
254
+ designated);
255
+
256
+ ii. a copyright notice;
257
+
258
+ iii. a notice that refers to this Public License;
259
+
260
+ iv. a notice that refers to the disclaimer of
261
+ warranties;
262
+
263
+ v. a URI or hyperlink to the Licensed Material to the
264
+ extent reasonably practicable;
265
+
266
+ b. indicate if You modified the Licensed Material and
267
+ retain an indication of any previous modifications; and
268
+
269
+ c. indicate the Licensed Material is licensed under this
270
+ Public License, and include the text of, or the URI or
271
+ hyperlink to, this Public License.
272
+
273
+ 2. You may satisfy the conditions in Section 3(a)(1) in any
274
+ reasonable manner based on the medium, means, and context in
275
+ which You Share the Licensed Material. For example, it may be
276
+ reasonable to satisfy the conditions by providing a URI or
277
+ hyperlink to a resource that includes the required
278
+ information.
279
+ 3. If requested by the Licensor, You must remove any of the
280
+ information required by Section 3(a)(1)(A) to the extent
281
+ reasonably practicable.
282
+
283
+ b. ShareAlike.
284
+
285
+ In addition to the conditions in Section 3(a), if You Share
286
+ Adapted Material You produce, the following conditions also apply.
287
+
288
+ 1. The Adapter's License You apply must be a Creative Commons
289
+ license with the same License Elements, this version or
290
+ later, or a BY-NC-SA Compatible License.
291
+
292
+ 2. You must include the text of, or the URI or hyperlink to, the
293
+ Adapter's License You apply. You may satisfy this condition
294
+ in any reasonable manner based on the medium, means, and
295
+ context in which You Share Adapted Material.
296
+
297
+ 3. You may not offer or impose any additional or different terms
298
+ or conditions on, or apply any Effective Technological
299
+ Measures to, Adapted Material that restrict exercise of the
300
+ rights granted under the Adapter's License You apply.
301
+
302
+
303
+ Section 4 -- Sui Generis Database Rights.
304
+
305
+ Where the Licensed Rights include Sui Generis Database Rights that
306
+ apply to Your use of the Licensed Material:
307
+
308
+ a. for the avoidance of doubt, Section 2(a)(1) grants You the right
309
+ to extract, reuse, reproduce, and Share all or a substantial
310
+ portion of the contents of the database for NonCommercial purposes
311
+ only;
312
+
313
+ b. if You include all or a substantial portion of the database
314
+ contents in a database in which You have Sui Generis Database
315
+ Rights, then the database in which You have Sui Generis Database
316
+ Rights (but not its individual contents) is Adapted Material,
317
+ including for purposes of Section 3(b); and
318
+
319
+ c. You must comply with the conditions in Section 3(a) if You Share
320
+ all or a substantial portion of the contents of the database.
321
+
322
+ For the avoidance of doubt, this Section 4 supplements and does not
323
+ replace Your obligations under this Public License where the Licensed
324
+ Rights include other Copyright and Similar Rights.
325
+
326
+
327
+ Section 5 -- Disclaimer of Warranties and Limitation of Liability.
328
+
329
+ a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
330
+ EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
331
+ AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
332
+ ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
333
+ IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
334
+ WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
335
+ PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
336
+ ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
337
+ KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
338
+ ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
339
+
340
+ b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
341
+ TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
342
+ NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
343
+ INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
344
+ COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
345
+ USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
346
+ ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
347
+ DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
348
+ IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
349
+
350
+ c. The disclaimer of warranties and limitation of liability provided
351
+ above shall be interpreted in a manner that, to the extent
352
+ possible, most closely approximates an absolute disclaimer and
353
+ waiver of all liability.
354
+
355
+
356
+ Section 6 -- Term and Termination.
357
+
358
+ a. This Public License applies for the term of the Copyright and
359
+ Similar Rights licensed here. However, if You fail to comply with
360
+ this Public License, then Your rights under this Public License
361
+ terminate automatically.
362
+
363
+ b. Where Your right to use the Licensed Material has terminated under
364
+ Section 6(a), it reinstates:
365
+
366
+ 1. automatically as of the date the violation is cured, provided
367
+ it is cured within 30 days of Your discovery of the
368
+ violation; or
369
+
370
+ 2. upon express reinstatement by the Licensor.
371
+
372
+ For the avoidance of doubt, this Section 6(b) does not affect any
373
+ right the Licensor may have to seek remedies for Your violations
374
+ of this Public License.
375
+
376
+ c. For the avoidance of doubt, the Licensor may also offer the
377
+ Licensed Material under separate terms or conditions or stop
378
+ distributing the Licensed Material at any time; however, doing so
379
+ will not terminate this Public License.
380
+
381
+ d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
382
+ License.
383
+
384
+
385
+ Section 7 -- Other Terms and Conditions.
386
+
387
+ a. The Licensor shall not be bound by any additional or different
388
+ terms or conditions communicated by You unless expressly agreed.
389
+
390
+ b. Any arrangements, understandings, or agreements regarding the
391
+ Licensed Material not stated herein are separate from and
392
+ independent of the terms and conditions of this Public License.
393
+
394
+
395
+ Section 8 -- Interpretation.
396
+
397
+ a. For the avoidance of doubt, this Public License does not, and
398
+ shall not be interpreted to, reduce, limit, restrict, or impose
399
+ conditions on any use of the Licensed Material that could lawfully
400
+ be made without permission under this Public License.
401
+
402
+ b. To the extent possible, if any provision of this Public License is
403
+ deemed unenforceable, it shall be automatically reformed to the
404
+ minimum extent necessary to make it enforceable. If the provision
405
+ cannot be reformed, it shall be severed from this Public License
406
+ without affecting the enforceability of the remaining terms and
407
+ conditions.
408
+
409
+ c. No term or condition of this Public License will be waived and no
410
+ failure to comply consented to unless expressly agreed to by the
411
+ Licensor.
412
+
413
+ d. Nothing in this Public License constitutes or may be interpreted
414
+ as a limitation upon, or waiver of, any privileges and immunities
415
+ that apply to the Licensor or You, including from the legal
416
+ processes of any jurisdiction or authority.
417
+
418
+ =======================================================================
419
+
420
+ Creative Commons is not a party to its public
421
+ licenses. Notwithstanding, Creative Commons may elect to apply one of
422
+ its public licenses to material it publishes and in those instances
423
+ will be considered the “Licensor.” The text of the Creative Commons
424
+ public licenses is dedicated to the public domain under the CC0 Public
425
+ Domain Dedication. Except for the limited purpose of indicating that
426
+ material is shared under a Creative Commons public license or as
427
+ otherwise permitted by the Creative Commons policies published at
428
+ creativecommons.org/policies, Creative Commons does not authorize the
429
+ use of the trademark "Creative Commons" or any other trademark or logo
430
+ of Creative Commons without its prior written consent including,
431
+ without limitation, in connection with any unauthorized modifications
432
+ to any of its public licenses or any other arrangements,
433
+ understandings, or agreements concerning use of licensed material. For
434
+ the avoidance of doubt, this paragraph does not form part of the
435
+ public licenses.
436
+
437
+ Creative Commons may be contacted at creativecommons.org.
438
+
README.md ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: cc-by-nc-sa-4.0
3
+ library_name: transformers
4
+ pipeline_tag: image-segmentation
5
+ tags:
6
+ - semantic-segmentation
7
+ - drone
8
+ - rgb
9
+ - thermal
10
+ - infrared
11
+ - dinov3
12
+ - aerial
13
+ datasets:
14
+ - markus-42/SegFly
15
+ model-index:
16
+ - name: Firefly-RGB
17
+ results: []
18
+ - name: Firefly-Thermal
19
+ results: []
20
+ ---
21
+
22
+ # Introduction
23
+
24
+ <div align="center">
25
+
26
+ [![GitHub Documentation](https://img.shields.io/badge/GitHub-Documentation-green?logo=github&logoColor=white&labelColor=555)](https://github.com/markus-42/SegFly)&nbsp;&nbsp;
27
+ [![Project](https://img.shields.io/badge/Project-Page-blue.svg)](https://markus-42.github.io/publications/2026/segfly/)&nbsp;&nbsp;
28
+ [![arXiv](https://img.shields.io/badge/arXiv-Paper-red.svg)](https://arxiv.org/abs/2603.17920)&nbsp;&nbsp;
29
+ [![SegFly Dataset](https://img.shields.io/badge/HuggingFace-SegFly%20Dataset%20-e58f00?logo=huggingface&logoColor=white&labelColor=555)](https://huggingface.co/datasets/markus-42/SegFly)
30
+
31
+ </div>
32
+
33
+ Following the acceptance of [SegFly](https://markus-42.github.io/publications/2026/segfly/) at the ECCV 2026 computer vision conference, we not only release the [SegFly dataset](https://huggingface.co/datasets/markus-42/SegFly) on HuggingFace, but also our `Firefly` semantic segmentation model, specialized for RGB and thermal imagery from aerial perspectives. Firefly was trained on our SegFly dataset.
34
+
35
+ #### Key Features:
36
+ - **Aerial-specialized**: Fine-tuned on diverse aerial imagery from urban, industrial, and rural environments.
37
+ - **Multi-altitude performance**: Trained on data from 50m, 40m, and 30m altitudes.
38
+ - **Seasonal robustness**: Covers data across all seasons for improved generalization.
39
+ - **Semantic Categories**: Both models segment 15 semantic classes: road, walkway, dirt, gravel, grass, vegetation, tree, ground obstacles, vehicle, water, building, roof, parking lot, constructions, and truck.
40
+
41
+
42
+ # Checkpoints
43
+
44
+ Two pretrained checkpoints are provided:
45
+
46
+ | Variant | Subfolder | Modality | Architecture |
47
+ | :--- | :--- | :--- | :--- |
48
+ | Firefly-RGB | `Firefly_RGB/` | RGB drone imagery | DINOv3 ViT-B/16 + MLP head |
49
+ | Firefly-Thermal | `Firefly_Thermal/` | Thermal drone imagery | DINOv3 ViT-B/16 + Rein adapter + MLP head |
50
+
51
+
52
+ # Setup
53
+
54
+ ```bash
55
+ git clone https://huggingface.co/markus-42/SegFly-Firefly
56
+ cd SegFly-Firefly
57
+
58
+ # clone DINOv3 backbone inside the repository
59
+ git clone https://github.com/facebookresearch/dinov3.git
60
+
61
+ # create virtual environment and install dependencies
62
+ uv venv && uv pip install -r requirements.txt
63
+
64
+ # activate virtual environment
65
+ source .venv/bin/activate
66
+ ```
67
+
68
+ # Inference
69
+
70
+ Run single-image inference with `infer.py`. Weights are auto-detected from the modality:
71
+
72
+ ```bash
73
+ # RGB model
74
+ python infer.py --image example.jpg --modality rgb
75
+
76
+ # Thermal model
77
+ python infer.py --image example_thermal.jpg --modality thermal
78
+ ```
79
+
80
+ The colorized segmentation map is saved to `./infer_output/` by default.
81
+
82
+ | Argument | Type | Default | Description |
83
+ | :--- | :--- | :--- | :--- |
84
+ | `--image` | `str` | *(required)* | Path to the input image. |
85
+ | `--modality` | `str` | `rgb` | Model to use: `rgb` or `thermal`. |
86
+ | `--output` | `str` | `./infer_output` | Directory for the colorized segmentation output. |
87
+ | `--weights_path` | `str` | `""` | Path to model weights. Auto-detected from modality if not set. |
88
+ | `--dinov3_repo_dir` | `str` | `./dinov3` | Path to the local DINOv3 repository. |
89
+ | `--image_size` | `int` | `640` | Resolution at which the image is fed to the model. |
90
+
91
+ # Batch Evaluation
92
+
93
+ `eval.py` computes segmentation metrics (mIoU, Frequency-Weighted IoU, Pixel Accuracy) against ground truth masks for a set of images.
94
+
95
+ ```bash
96
+ # RGB model
97
+ python eval.py --data_dir /path/to/dataset --modality rgb
98
+
99
+ # Thermal model
100
+ python eval.py --data_dir /path/to/dataset --modality thermal
101
+ ```
102
+
103
+ `--data_dir` can point to any directory. The loader walks it recursively and collects images and masks by subfolder name — no specific top-level structure is required:
104
+
105
+ ```
106
+ data_dir/
107
+ └── <any nesting>/
108
+ ├── src/ ← RGB images (.png / .jpg / .jpeg) [--modality rgb]
109
+ ├── thermal_src/ ← thermal images (.png / .jpg / .jpeg) [--modality thermal]
110
+ └── gt/ ← grayscale segmentation masks (.png, raw SegFly class IDs)
111
+ ```
112
+
113
+ Images and masks are matched by alphabetical sort order, so filenames must correspond 1-to-1.
114
+
115
+ Append `--visualize` to save colorized segmentation maps to `<output_dir>/visualizations`.
116
+
117
+ | Argument | Type | Default | Description |
118
+ | :--- | :--- | :--- | :--- |
119
+ | `--data_dir` | `str` | `./data` | Root directory of the dataset. Walked recursively; images are collected from `src/` or `thermal_src/` subfolders and masks from `gt/` subfolders. |
120
+ | `--weights_path` | `str` | `""` | Path to the saved weights. Auto-detected from modality if not set. |
121
+ | `--class_dict_path` | `str` | `./classes_segfly.csv` | Path to the CSV file defining the dataset's class mapping. |
122
+ | `--modality` | `str` | `rgb` | Modality of the dataset: `rgb` or `thermal`. |
123
+ | `--output_dir` | `str` | `./eval_output` | Directory to save `per_image_iou.csv`, `per_class_scores.txt`, and visualizations. |
124
+ | `--visualize` | `flag` | `False` | If provided, generates side-by-side visualizations of Image, Ground Truth, and Prediction. |
125
+ | `--image_size` | `int` | `640` | Input resolution for evaluation. |
126
+ | `--dinov3_repo_dir` | `str` | `./dinov3` | Path to the local DINOv3 repository. |
127
+
128
+
129
+ # Reference
130
+ If our work was helpful to you, we would appreciate citing our papers and giving the repository a like ❤️
131
+
132
+ ```bibtex
133
+ @inproceedings{gross2026segfly,
134
+ title={{SegFly: A Dataset and 2D-3D-2D Paradigm for Aerial RGB-Thermal Semantic Segmentation at Scale}},
135
+ author={Markus Gross and Sai Bharadhwaj Matha and Rui Song and Viswanathan Muthuveerappan and Conrad Christoph and Julius Huber and Daniel Cremers},
136
+ booktitle = {Proceedings of the European Conference on Computer Vision (ECCV)},
137
+ year={2026},
138
+ }
139
+ ```
140
+
141
+ Since SegFly is based on the [OccuFly dataset](https://markus-42.github.io/publications/2026/occufly/), consider citing this work as well:
142
+
143
+ ```bibtex
144
+ @inproceedings{gross2026occufly,
145
+ title={{OccuFly: A 3D Vision Benchmark for Semantic Scene Completion from the Aerial Perspective}},
146
+ author={Markus Gross and Sai B. Matha and Aya Fahmy and Rui Song and Daniel Cremers and Henri Meess},
147
+ booktitle = {Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR)},
148
+ year={2026},
149
+ }
150
+ ```
151
+
152
+ # License
153
+ This work is licensed under the [CC BY-NC-SA 4.0 license](https://creativecommons.org/licenses/by-nc-sa/4.0/). See the LICENSE file for the full legal terms.
classes_segfly.csv ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ id,name
2
+ 0,"empty"
3
+ 1,"road"
4
+ 2,"walkway"
5
+ 3,"dirt"
6
+ 4,"gravel"
7
+ 6,"grass"
8
+ 7,"vegetation"
9
+ 8,"tree"
10
+ 9,"ground_obstacles"
11
+ 13,"vehicle"
12
+ 14,"water"
13
+ 16,"building"
14
+ 17,"roof"
15
+ 33,"parking_lot"
16
+ 34,"constructions"
17
+ 36,"truck"
eval.py ADDED
@@ -0,0 +1,447 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import argparse
3
+ import evaluate
4
+ import numpy as np
5
+ import torch
6
+ import csv
7
+ import pandas as pd
8
+ from tqdm.auto import tqdm
9
+ import cv2
10
+ import re
11
+
12
+ from lib.utils_segfly import InferenceDataset, InferenceDatasetThermal, Timing, ID2COLOR, mask2label
13
+ from safetensors.torch import load_file
14
+ from transformers import AutoImageProcessor
15
+ from lib.firefly_rgb import FireflyForSemanticSegmentationRGB, FireflyConfigRGB
16
+ from lib.firefly_thermal import FireflyForSemanticSegmentationThermal, FireflyConfigThermal
17
+
18
+ timing = Timing()
19
+
20
+
21
+ @timing
22
+ def segment_image(image, _model, _device, target_size, image_processor):
23
+ with torch.no_grad():
24
+ pixel_values = image_processor(image, return_tensors="pt").pixel_values.to(_device, dtype=torch.float32)
25
+ outputs = _model(pixel_values)
26
+
27
+ if hasattr(outputs, "logits"):
28
+ outputs = outputs.logits
29
+
30
+ upsampled_logits = torch.nn.functional.interpolate(
31
+ outputs.float(), size=target_size, mode="bilinear", align_corners=False
32
+ )
33
+ return upsampled_logits.argmax(dim=1).detach().cpu().numpy()
34
+
35
+ def run_inference(image, _model, _device, target_size, image_processor):
36
+ torch.cuda.empty_cache()
37
+ torch.cuda.reset_peak_memory_stats()
38
+
39
+ predicted = segment_image(image, _model, _device, target_size, image_processor)
40
+
41
+ mem_used = torch.cuda.max_memory_allocated() / 1024**2
42
+ return predicted, mem_used
43
+
44
+ def get_image_path_info(dataset, idx):
45
+ path = ""
46
+ for attr in ['images', 'image_paths', 'filepaths', 'samples', 'img_files', 'data']:
47
+ if hasattr(dataset, attr):
48
+ val = getattr(dataset, attr)
49
+ if isinstance(val, (list, tuple, np.ndarray)) and len(val) > idx:
50
+ path = str(val[idx])
51
+ break
52
+
53
+ subfolder_name = f"pred_img_{idx:04d}"
54
+ if path:
55
+ scene_match = re.search(r'(scene_\d+)', path, re.IGNORECASE)
56
+ alt_match = re.search(r'(\d+m)', path, re.IGNORECASE)
57
+
58
+ if scene_match or alt_match:
59
+ scene_str = scene_match.group(1) if scene_match else "scene_unk"
60
+ alt_str = alt_match.group(1) if alt_match else "unk_m"
61
+ subfolder_name = f"pred_{scene_str}_{alt_str}_{idx:04d}"
62
+
63
+ actual_path = path
64
+ if path and not os.path.exists(path):
65
+ if hasattr(dataset, 'root_dir') and os.path.exists(os.path.join(dataset.root_dir, path)):
66
+ actual_path = os.path.join(dataset.root_dir, path)
67
+ elif hasattr(dataset, '_root_dir') and os.path.exists(os.path.join(dataset._root_dir, path)):
68
+ actual_path = os.path.join(dataset._root_dir, path)
69
+
70
+ return subfolder_name, actual_path
71
+
72
+ def generate_metrics_for_dataset(loader, _model, _device, _metric, config, image_processor, visualize=False):
73
+ mem_usage = []
74
+ global_gt_counts = np.zeros(config["num_classes"], dtype=np.int64)
75
+ ignore_label = config.get("ignore_label", 255)
76
+
77
+ if visualize:
78
+ vis_dir = os.path.join(config.get("output_dir", "./"), "visualizations")
79
+ os.makedirs(vis_dir, exist_ok=True)
80
+ palette = ID2COLOR
81
+
82
+ csv_path = os.path.join(config.get("output_dir", "./"), "per_image_iou.csv")
83
+
84
+ print("Running inference and accumulating batches...")
85
+ pbar = tqdm(total=len(loader))
86
+
87
+ with open(csv_path, mode="w", newline="", encoding="utf-8") as csv_file:
88
+ csv_writer = csv.writer(csv_file)
89
+ csv_writer.writerow(["Image_Index", "mIoU"])
90
+
91
+ for idx, (images, masks) in enumerate(loader):
92
+ image = images.squeeze(0)
93
+ mask = masks.squeeze(0)
94
+
95
+ if len(mask.shape) == 2:
96
+ target_h, target_w = mask.shape
97
+ else:
98
+ target_h, target_w = mask.shape[-2:]
99
+
100
+ target_size = (target_h, target_w)
101
+
102
+ prediction, mem = run_inference(image, _model, _device, target_size, image_processor)
103
+
104
+ if isinstance(mask, torch.Tensor):
105
+ mask_np = mask.detach().cpu().numpy().astype(int)
106
+ else:
107
+ mask_np = np.array(mask).astype(int)
108
+
109
+ if getattr(image_processor, 'do_reduce_labels', False):
110
+ mask_np = mask_np.copy()
111
+ mask_np[mask_np == 0] = 255
112
+ mask_np = mask_np - 1
113
+ mask_np[mask_np == 254] = 255
114
+
115
+ valid_pixels = mask_np[mask_np != ignore_label]
116
+ valid_pixels = valid_pixels[valid_pixels >= 0]
117
+ counts = np.bincount(valid_pixels.flatten(), minlength=config["num_classes"])
118
+ global_gt_counts += counts[:config["num_classes"]]
119
+
120
+ pred_np = prediction.squeeze()
121
+ if isinstance(pred_np, torch.Tensor):
122
+ pred_np = pred_np.cpu().numpy()
123
+
124
+ valid = (mask_np != ignore_label) & (mask_np >= 0)
125
+ pred_valid = pred_np[valid]
126
+ mask_valid = mask_np[valid]
127
+
128
+ if len(mask_valid) > 0:
129
+ classes_in_img = np.unique(np.concatenate([mask_valid, pred_valid]))
130
+ else:
131
+ classes_in_img = []
132
+
133
+ image_ious = []
134
+ for c in classes_in_img:
135
+ intersection = np.sum((pred_valid == c) & (mask_valid == c))
136
+ union = np.sum((pred_valid == c) | (mask_valid == c))
137
+ if union > 0:
138
+ image_ious.append(intersection / union)
139
+ else:
140
+ image_ious.append(0.0)
141
+
142
+ image_miou = np.mean(image_ious) if len(image_ious) > 0 else 0.0
143
+ csv_writer.writerow([idx, f"{image_miou:.4f}"])
144
+ csv_file.flush()
145
+
146
+ if visualize:
147
+ subfolder_name, actual_image_path = get_image_path_info(loader.dataset, idx)
148
+
149
+ true_img_bgr = None
150
+ if actual_image_path and os.path.exists(actual_image_path):
151
+ true_img_bgr = cv2.imread(actual_image_path)
152
+
153
+ if true_img_bgr is not None:
154
+ orig_h, orig_w = true_img_bgr.shape[:2]
155
+ img_bgr = true_img_bgr
156
+ else:
157
+ print(f"\n[Warning] Could not load raw image from disk for idx {idx}. Falling back to tensor size.")
158
+ if isinstance(image, torch.Tensor):
159
+ img_vis = image.cpu().numpy()
160
+ else:
161
+ img_vis = np.array(image)
162
+
163
+ if img_vis.ndim == 3:
164
+ if img_vis.shape[0] in [1, 3]:
165
+ img_vis = np.transpose(img_vis, (1, 2, 0))
166
+ elif img_vis.shape[1] in [1, 3]:
167
+ img_vis = np.transpose(img_vis, (0, 2, 1))
168
+
169
+ if img_vis.dtype.kind == 'f' and img_vis.max() <= 1.0:
170
+ img_vis = img_vis * 255.0
171
+
172
+ img_vis = np.ascontiguousarray(img_vis).astype(np.uint8)
173
+
174
+ if img_vis.ndim == 2:
175
+ img_vis = cv2.cvtColor(img_vis, cv2.COLOR_GRAY2RGB)
176
+ elif img_vis.ndim == 3 and img_vis.shape[2] == 1:
177
+ img_vis = cv2.cvtColor(img_vis[:, :, 0], cv2.COLOR_GRAY2RGB)
178
+ elif img_vis.ndim == 3 and img_vis.shape[2] > 3:
179
+ img_vis = img_vis[:, :, :3]
180
+
181
+ orig_h, orig_w = img_vis.shape[:2]
182
+ img_bgr = cv2.cvtColor(img_vis, cv2.COLOR_RGB2BGR)
183
+
184
+ gt_color = mask2label(mask_np, palette)
185
+ pred_color = mask2label(pred_np, palette)
186
+
187
+ gt_color[mask_np == ignore_label] = [0, 0, 0]
188
+
189
+ try:
190
+ if gt_color.shape[:2] != (orig_h, orig_w):
191
+ gt_color = cv2.resize(gt_color, (orig_w, orig_h), interpolation=cv2.INTER_NEAREST)
192
+ if pred_color.shape[:2] != (orig_h, orig_w):
193
+ pred_color = cv2.resize(pred_color, (orig_w, orig_h), interpolation=cv2.INTER_NEAREST)
194
+ except cv2.error as e:
195
+ print(f"\n[Error] OpenCV failed to resize masks. Original mask shape: {gt_color.shape[:2]}, Target image size: ({orig_w}, {orig_h})")
196
+ raise e
197
+
198
+ instance_dir = os.path.join(vis_dir, subfolder_name)
199
+ os.makedirs(instance_dir, exist_ok=True)
200
+
201
+ gt_bgr = cv2.cvtColor(gt_color, cv2.COLOR_RGB2BGR)
202
+ pred_bgr = cv2.cvtColor(pred_color, cv2.COLOR_RGB2BGR)
203
+
204
+ cv2.imwrite(os.path.join(instance_dir, "image.png"), img_bgr)
205
+ cv2.imwrite(os.path.join(instance_dir, "gt.png"), gt_bgr)
206
+ cv2.imwrite(os.path.join(instance_dir, "pred.png"), pred_bgr)
207
+
208
+ if len(prediction.shape) == 2:
209
+ prediction = prediction[None, :, :]
210
+ if len(mask.shape) == 2:
211
+ mask = mask[None, :, :]
212
+
213
+ _metric.add_batch(
214
+ predictions=prediction,
215
+ references=mask
216
+ )
217
+
218
+ mem_usage.append(mem)
219
+ pbar.update()
220
+
221
+ pbar.close()
222
+
223
+ print("Computing global metrics...")
224
+ results = _metric.compute(
225
+ num_labels=config["num_classes"],
226
+ ignore_index=config.get("ignore_label"),
227
+ reduce_labels=image_processor.do_reduce_labels,
228
+ )
229
+
230
+ return results, mem_usage, global_gt_counts
231
+
232
+
233
+ if __name__ == "__main__":
234
+ parser = argparse.ArgumentParser(description="Evaluation script for Firefly RGB / Thermal models")
235
+ parser.add_argument("--data_dir", type=str, default="./data", help="Path to the root directory of the dataset.")
236
+ parser.add_argument("--weights_path", type=str, default="", help="Path to the .safetensors or .bin or .pth trained weights file.")
237
+ parser.add_argument("--class_dict_path", type=str, default="./classes_segfly.csv", help="Path to the class dictionary CSV file.")
238
+ parser.add_argument("--modality", type=str, default="rgb", choices=["rgb", "thermal"], help="Modality of the dataset (rgb or thermal).")
239
+ parser.add_argument("--output_dir", type=str, default="./eval_output", help="Directory where evaluation results and visualizations will be saved.")
240
+ parser.add_argument("--visualize", action="store_true", help="Generate and save side-by-side visualizations of image, ground truth, and prediction.")
241
+ parser.add_argument("--image_size", type=int, default=640, help="Image size for inference.")
242
+ parser.add_argument("--dinov3_repo_dir", type=str, default="./dinov3", help="Path to the local dinov3 repository.")
243
+
244
+ args = parser.parse_args()
245
+
246
+ model_type = "thermal" if args.modality.lower() == "thermal" else "rgb"
247
+
248
+ if not args.weights_path:
249
+ if args.modality == "rgb":
250
+ args.weights_path = "./Firefly_RGB/model.safetensors"
251
+ elif args.modality == "thermal":
252
+ args.weights_path = "./Firefly_Thermal/model.safetensors"
253
+ print(f"No weights path provided, defaulting to: {args.weights_path}")
254
+
255
+ os.makedirs(args.output_dir, exist_ok=True)
256
+
257
+ device = torch.device("cuda")
258
+
259
+ df = pd.read_csv(args.class_dict_path)
260
+ df = df.iloc[1:].reset_index(drop=True)
261
+ classes = df["name"]
262
+ id2label = classes.to_dict()
263
+ label2id = {v: k for k, v in id2label.items()}
264
+
265
+ num_classes = len(classes)
266
+ print(f"Loaded {num_classes} classes from {args.class_dict_path}")
267
+ print(label2id)
268
+
269
+ config_dict = {
270
+ "num_classes": num_classes,
271
+ "ignore_label": 255,
272
+ "class_dict_path": args.class_dict_path,
273
+ "image_size": args.image_size,
274
+ "output_dir": args.output_dir
275
+ }
276
+
277
+ print("Loading dataset...")
278
+ if args.modality.lower() == "thermal":
279
+ val_set = InferenceDatasetThermal(
280
+ _root_dir=args.data_dir,
281
+ config=config_dict,
282
+ )
283
+ else:
284
+ val_set = InferenceDataset(
285
+ _root_dir=args.data_dir,
286
+ config=config_dict,
287
+ )
288
+
289
+ print(f"Number of images in validation set: {len(val_set)}")
290
+
291
+ weights_dir = os.path.dirname(args.weights_path) if os.path.isfile(args.weights_path) else args.weights_path
292
+ image_processor_loaded = False
293
+
294
+ if weights_dir and os.path.exists(os.path.join(weights_dir, "preprocessor_config.json")):
295
+ try:
296
+ print(f"Loading image processor config from {weights_dir}...")
297
+ image_processor = AutoImageProcessor.from_pretrained(weights_dir)
298
+ image_processor_loaded = True
299
+ except Exception as e:
300
+ print(f"Warning: Failed to load image processor from {weights_dir}: {e}")
301
+
302
+ if not image_processor_loaded:
303
+ print("Falling back to standard image processor initialization...")
304
+ image_processor_args = {
305
+ "size": {"height": args.image_size, "width": args.image_size},
306
+ "crop_size": {"height": args.image_size, "width": args.image_size},
307
+ "image_mean": [0.485, 0.456, 0.406],
308
+ "image_std": [0.229, 0.224, 0.225],
309
+ "do_center_crop": False,
310
+ "do_normalize": True,
311
+ "do_resize": True,
312
+ "do_rescale": True,
313
+ "do_reduce_labels": True,
314
+ }
315
+ try:
316
+ image_processor = AutoImageProcessor.from_pretrained("nvidia/mit-b3", **image_processor_args)
317
+ except:
318
+ image_processor = AutoImageProcessor.from_pretrained("nvidia/segformer-b0-finetuned-ade-512-512", **image_processor_args)
319
+
320
+ print(f"Initializing {model_type} model...")
321
+ if model_type == "rgb":
322
+ dino_config = FireflyConfigRGB(
323
+ num_labels=num_classes,
324
+ image_size=args.image_size,
325
+ embedding_dim=256,
326
+ backbone_embed_dim=768,
327
+ patch_size=16,
328
+ repo_dir=args.dinov3_repo_dir,
329
+ model_name="dinov3_vitb16",
330
+ semantic_loss_ignore_index=255,
331
+ )
332
+ model = FireflyForSemanticSegmentationRGB(dino_config)
333
+
334
+ elif model_type == "thermal":
335
+ dino_config = FireflyConfigThermal(
336
+ num_labels=num_classes,
337
+ image_size=args.image_size,
338
+ embedding_dim=256,
339
+ backbone_embed_dim=768,
340
+ patch_size=16,
341
+ num_layers=12,
342
+ rein_token_length=100,
343
+ feature_layers=[2, 5, 8, 11],
344
+ repo_dir=args.dinov3_repo_dir,
345
+ model_name="dinov3_vitb16",
346
+ semantic_loss_ignore_index=255,
347
+ )
348
+ model = FireflyForSemanticSegmentationThermal(dino_config)
349
+
350
+ model.print_trainable_params()
351
+
352
+ if args.weights_path and os.path.exists(args.weights_path):
353
+ print(f"Loading weights from {args.weights_path}...")
354
+ try:
355
+ if args.weights_path.endswith('.safetensors'):
356
+ state_dict = load_file(args.weights_path)
357
+ else:
358
+ checkpoint = torch.load(args.weights_path, map_location='cpu')
359
+ if isinstance(checkpoint, dict):
360
+ if "state_dict" in checkpoint:
361
+ state_dict = checkpoint["state_dict"]
362
+ elif "model" in checkpoint:
363
+ state_dict = checkpoint["model"]
364
+ elif "student_model" in checkpoint:
365
+ state_dict = checkpoint["student_model"]
366
+ else:
367
+ state_dict = checkpoint
368
+ else:
369
+ state_dict = checkpoint
370
+
371
+ new_state_dict = {}
372
+ for k, v in state_dict.items():
373
+ if k.startswith("module."):
374
+ new_state_dict[k[7:]] = v
375
+ else:
376
+ new_state_dict[k] = v
377
+
378
+ msg = model.load_state_dict(new_state_dict, strict=False)
379
+ print(f"Weights Loaded. Missing keys: {len(msg.missing_keys)}, Unexpected keys: {len(msg.unexpected_keys)}")
380
+ if len(msg.missing_keys) > 0:
381
+ print(f"First 5 missing: {msg.missing_keys[:5]}")
382
+ except Exception as e:
383
+ print(f"Failed to load weights file: {e}")
384
+ raise
385
+ else:
386
+ print(f"Warning: No valid checkpoint found at {args.weights_path}. Proceeding with initialized weights.")
387
+
388
+ model.eval()
389
+ model.to(device)
390
+
391
+ torch.backends.cudnn.benchmark = True
392
+
393
+ loader = torch.utils.data.DataLoader(
394
+ val_set,
395
+ batch_size=1,
396
+ num_workers=4,
397
+ pin_memory=True
398
+ )
399
+
400
+ metric = evaluate.load("mean_iou")
401
+
402
+ results, memory, global_gt_counts = generate_metrics_for_dataset(
403
+ loader, model, device, metric, config_dict, image_processor, visualize=args.visualize
404
+ )
405
+
406
+ global_mean_iou = results["mean_iou"]
407
+ global_mean_acc = results["mean_accuracy"]
408
+ per_category_iou = results["per_category_iou"]
409
+
410
+ ground_truth_set = global_gt_counts
411
+ iou = np.array(per_category_iou)
412
+ iou = np.nan_to_num(iou, nan=0.0)
413
+
414
+ gt_present_mask = (ground_truth_set > 0)
415
+ total_gt_freq = np.sum(ground_truth_set[gt_present_mask])
416
+
417
+ if total_gt_freq > 0:
418
+ fwIoU = np.sum(ground_truth_set[gt_present_mask] * iou[gt_present_mask]) / total_gt_freq
419
+ else:
420
+ fwIoU = 0.0
421
+
422
+ print("=" * 40)
423
+ print("EVALUATION RESULTS")
424
+ print("=" * 40)
425
+ print(f"Mean mIoU: {global_mean_iou:.4f}")
426
+ print(f"Freq Weighted IoU: {fwIoU:.4f}")
427
+ print(f"Mean pixel accuracy: {global_mean_acc:.4f}")
428
+ print("-" * 40)
429
+
430
+ for class_index, class_iou in enumerate(per_category_iou):
431
+ label = id2label.get(class_index, f"Class {class_index}")
432
+ print(f"{label}: Mean IoU = {class_iou:.4f}")
433
+
434
+ print("-" * 40)
435
+ print(f"Mean memory usage: {np.mean(memory):.2f} MB")
436
+ print(f"Max. GPU memory usage: {torch.cuda.max_memory_allocated() / 1024**2:.2f} MB")
437
+
438
+ with open(os.path.join(args.output_dir, "per_class_scores.txt"), "w", encoding="utf-8") as f:
439
+ f.write(f"Mean mIoU: {global_mean_iou:.4f}\n")
440
+ f.write(f"Freq Weighted IoU: {fwIoU:.4f}\n")
441
+ f.write(f"Mean pixel accuracy: {global_mean_acc:.4f}\n")
442
+ f.write("-" * 40 + "\n")
443
+ for i, score in enumerate(per_category_iou):
444
+ label = id2label.get(i, f"Class {i}")
445
+ f.write(f"{label}: {score:.4f}\n")
446
+
447
+ print(f"\nAll results saved to {args.output_dir}")
infer.py ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import argparse
3
+
4
+ import cv2
5
+ import numpy as np
6
+ import torch
7
+ from safetensors.torch import load_file
8
+ from transformers import AutoImageProcessor
9
+
10
+ from lib.utils_segfly import ID2COLOR, mask2label
11
+ from lib.firefly_rgb import FireflyForSemanticSegmentationRGB, FireflyConfigRGB
12
+ from lib.firefly_thermal import FireflyForSemanticSegmentationThermal, FireflyConfigThermal
13
+
14
+
15
+ def run_infer(args):
16
+ if not args.weights_path:
17
+ if args.modality == "rgb":
18
+ args.weights_path = "./Firefly_RGB/model.safetensors"
19
+ else:
20
+ args.weights_path = "./Firefly_Thermal/model.safetensors"
21
+ print(f"No weights path provided, defaulting to: {args.weights_path}")
22
+
23
+ weights_dir = os.path.dirname(args.weights_path) if os.path.isfile(args.weights_path) else args.weights_path
24
+
25
+ print(f"Loading image processor from {weights_dir}...")
26
+ image_processor = AutoImageProcessor.from_pretrained(weights_dir)
27
+
28
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
29
+
30
+ print(f"Initializing {args.modality} model...")
31
+ if args.modality == "rgb":
32
+ cfg = FireflyConfigRGB(
33
+ num_labels=15,
34
+ image_size=args.image_size,
35
+ embedding_dim=256,
36
+ backbone_embed_dim=768,
37
+ patch_size=16,
38
+ repo_dir=args.dinov3_repo_dir,
39
+ model_name="dinov3_vitb16",
40
+ semantic_loss_ignore_index=255,
41
+ )
42
+ model = FireflyForSemanticSegmentationRGB(cfg)
43
+ else:
44
+ cfg = FireflyConfigThermal(
45
+ num_labels=15,
46
+ image_size=args.image_size,
47
+ embedding_dim=256,
48
+ backbone_embed_dim=768,
49
+ patch_size=16,
50
+ num_layers=12,
51
+ rein_token_length=100,
52
+ feature_layers=[2, 5, 8, 11],
53
+ repo_dir=args.dinov3_repo_dir,
54
+ model_name="dinov3_vitb16",
55
+ semantic_loss_ignore_index=255,
56
+ )
57
+ model = FireflyForSemanticSegmentationThermal(cfg)
58
+
59
+ print(f"Loading weights from {args.weights_path}...")
60
+ state_dict = load_file(args.weights_path)
61
+ new_state_dict = {
62
+ k[7:] if k.startswith("module.") else k: v
63
+ for k, v in state_dict.items()
64
+ }
65
+ msg = model.load_state_dict(new_state_dict, strict=False)
66
+ print(f"Weights loaded. Missing: {len(msg.missing_keys)}, Unexpected: {len(msg.unexpected_keys)}")
67
+
68
+ model.eval()
69
+ model.to(device)
70
+
71
+ image_bgr = cv2.imread(args.image)
72
+ if image_bgr is None:
73
+ raise FileNotFoundError(f"Could not read image: {args.image}")
74
+ image_rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)
75
+ orig_h, orig_w = image_rgb.shape[:2]
76
+
77
+ inputs = image_processor(images=image_rgb, return_tensors="pt")
78
+ pixel_values = inputs.pixel_values.to(device, dtype=torch.float32)
79
+
80
+ with torch.no_grad():
81
+ logits = model(pixel_values).logits
82
+ upsampled = torch.nn.functional.interpolate(
83
+ logits.float(), size=(orig_h, orig_w), mode="bilinear", align_corners=False
84
+ )
85
+ pred = upsampled.argmax(dim=1).squeeze(0).cpu().numpy()
86
+
87
+ os.makedirs(args.output, exist_ok=True)
88
+
89
+ pred_color = mask2label(pred, ID2COLOR)
90
+ pred_bgr = cv2.cvtColor(pred_color, cv2.COLOR_RGB2BGR)
91
+
92
+ stem = os.path.splitext(os.path.basename(args.image))[0]
93
+ out_path = os.path.join(args.output, f"{stem}_pred.png")
94
+ cv2.imwrite(out_path, pred_bgr)
95
+ print(f"Segmentation saved to {out_path}")
96
+
97
+
98
+ if __name__ == "__main__":
99
+ parser = argparse.ArgumentParser(description="Single-image inference for Firefly RGB / Thermal models")
100
+ parser.add_argument("--image", type=str, required=True, help="Path to the input image.")
101
+ parser.add_argument("--modality", type=str, default="rgb", choices=["rgb", "thermal"],
102
+ help="Model modality: 'rgb' or 'thermal'.")
103
+ parser.add_argument("--output", type=str, default="./infer_output",
104
+ help="Directory to save the colorized segmentation output.")
105
+ parser.add_argument("--weights_path", type=str, default="",
106
+ help="Path to model weights (.safetensors). Auto-detected from modality if not set.")
107
+ parser.add_argument("--dinov3_repo_dir", type=str, default="./dinov3",
108
+ help="Path to the local DINOv3 repository.")
109
+ parser.add_argument("--image_size", type=int, default=640,
110
+ help="Resolution at which the image is fed to the model.")
111
+ args = parser.parse_args()
112
+ run_infer(args)
lib/__init__.py ADDED
File without changes
lib/firefly_rgb.py ADDED
@@ -0,0 +1,220 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import torch
3
+ import torch.nn as nn
4
+ import torch.nn.functional as F
5
+ from transformers import PreTrainedModel, PretrainedConfig
6
+ from transformers.modeling_outputs import SemanticSegmenterOutput
7
+
8
+ # ==============================================================================
9
+ # SHARED UTILITIES & LOSS FUNCTIONS
10
+ # ==============================================================================
11
+
12
+ def dice_loss(pred, target, skip_classes=None, eps=1e-6):
13
+ N, C = pred.shape
14
+ if N == 0:
15
+ return pred.sum() * 0.0
16
+
17
+ p = torch.softmax(pred, dim=1)
18
+ t = torch.nn.functional.one_hot(target, C).float()
19
+
20
+ if skip_classes is None:
21
+ skip_classes = []
22
+
23
+ present = (t.sum(dim=0) > 0)
24
+ for sc in skip_classes:
25
+ if 0 <= sc < C:
26
+ present[sc] = False
27
+
28
+ if present.sum() == 0:
29
+ return pred.new_tensor(0.)
30
+
31
+ p_sel = p[:, present]
32
+ t_sel = t[:, present]
33
+
34
+ inter = (p_sel * t_sel).sum(dim=0)
35
+ union = p_sel.sum(dim=0) + t_sel.sum(dim=0)
36
+ dice = (2 * inter + eps) / (union + eps)
37
+
38
+ return 1 - dice.mean()
39
+
40
+
41
+ class DiceLoss(nn.Module):
42
+ def __init__(self, skip_classes=None, eps=1e-6):
43
+ super(DiceLoss, self).__init__()
44
+ self.skip_classes = skip_classes
45
+ self.eps = eps
46
+
47
+ def forward(self, pred, target):
48
+ return dice_loss(pred, target, skip_classes=self.skip_classes, eps=self.eps)
49
+
50
+
51
+ def pad_to_multiple(x, multiple):
52
+ _, _, h, w = x.shape
53
+ pad_h = (multiple - h % multiple) % multiple
54
+ pad_w = (multiple - w % multiple) % multiple
55
+ padding = (0, pad_w, 0, pad_h)
56
+ x_padded = F.pad(x, padding, mode='reflect')
57
+ return x_padded, padding
58
+
59
+
60
+ def crop_to_shape(x, target_h, target_w):
61
+ return x[:, :, :target_h, :target_w]
62
+
63
+ # ==============================================================================
64
+ # BASE CONFIGURATION & MODEL
65
+ # ==============================================================================
66
+
67
+ class FireflyBaseConfig(PretrainedConfig):
68
+ """Base Configuration for all Firefly models."""
69
+ def __init__(
70
+ self,
71
+ num_labels: int = 2,
72
+ image_size: int = 640,
73
+ embedding_dim: int = 256,
74
+ backbone_embed_dim: int = 768,
75
+ patch_size: int = 16,
76
+ repo_dir: str = None,
77
+ model_name: str = "dinov3_vitb16",
78
+ weights_path: str = None,
79
+ semantic_loss_ignore_index: int = 255,
80
+ dropout_ratio: float = 0.1,
81
+ **kwargs
82
+ ):
83
+ super().__init__(**kwargs)
84
+ self.num_labels = num_labels
85
+ self.image_size = image_size
86
+ self.embedding_dim = embedding_dim
87
+ self.backbone_embed_dim = backbone_embed_dim
88
+ self.patch_size = patch_size
89
+ self.repo_dir = repo_dir
90
+ self.model_name = model_name
91
+ self.weights_path = weights_path
92
+ self.semantic_loss_ignore_index = semantic_loss_ignore_index
93
+ self.dropout_ratio = dropout_ratio
94
+
95
+
96
+ class FireflyBaseModel(PreTrainedModel):
97
+ """
98
+ Base Model for Firefly. Handles backbone initialization,
99
+ weight loading, and parameter freezing.
100
+ """
101
+ def __init__(self, config: FireflyBaseConfig):
102
+ super().__init__(config)
103
+ self.config = config
104
+ self.num_labels = config.num_labels
105
+ self.patch_size = config.patch_size
106
+
107
+ if config.repo_dir and config.model_name:
108
+ print(f"Loading backbone: {config.model_name} from {config.repo_dir}")
109
+ self.backbone = torch.hub.load(
110
+ config.repo_dir,
111
+ config.model_name,
112
+ source="local",
113
+ weights=config.weights_path,
114
+ pretrained=False
115
+ )
116
+ else:
117
+ raise ValueError("repo_dir and model_name must be specified in the config.")
118
+
119
+ if config.weights_path and os.path.exists(config.weights_path):
120
+ print(f"Loading fine-tuned backbone weights from: {config.weights_path}")
121
+ checkpoint = torch.load(config.weights_path, map_location='cpu')
122
+
123
+ if isinstance(checkpoint, dict):
124
+ if "student_model" in checkpoint:
125
+ state_dict = checkpoint["student_model"]
126
+ elif "model" in checkpoint:
127
+ state_dict = checkpoint["model"]
128
+ else:
129
+ state_dict = checkpoint
130
+ else:
131
+ state_dict = checkpoint
132
+
133
+ new_state_dict = {k.replace("module.", ""): v for k, v in state_dict.items()}
134
+ msg = self.backbone.load_state_dict(new_state_dict, strict=False)
135
+ print(f"Weight loading result: {msg}")
136
+ else:
137
+ print(f"Warning: Weights path '{config.weights_path}' not found or not provided. Backbone initialized randomly/default.")
138
+
139
+ self.backbone.eval()
140
+ self._setup_trainable_params()
141
+
142
+ def _setup_trainable_params(self):
143
+ """Freeze backbone parameters initially."""
144
+ for param in self.backbone.parameters():
145
+ param.requires_grad = False
146
+
147
+ def print_trainable_params(self):
148
+ total = sum(p.numel() for p in self.parameters())
149
+ trainable = sum(p.numel() for p in self.parameters() if p.requires_grad)
150
+ print(f"Total Parameters: {total / 1e6:.2f}M")
151
+ print(f"Trainable Parameters: {trainable / 1e6:.2f}M")
152
+ print(f"Trainable Ratio: {100 * trainable / total:.2f}%")
153
+
154
+ # ==============================================================================
155
+ # SEGMENTATION HEAD & MODEL
156
+ # ==============================================================================
157
+
158
+ class MLPHead(nn.Module):
159
+ def __init__(self, in_channels=768, hidden_dim=256, num_classes=12, dropout_prob=0.1):
160
+ super().__init__()
161
+ self.model = nn.Sequential(
162
+ nn.Conv2d(in_channels, hidden_dim, kernel_size=3, padding=1),
163
+ nn.ReLU(inplace=True),
164
+ nn.Dropout2d(p=dropout_prob),
165
+ nn.Conv2d(hidden_dim, hidden_dim, kernel_size=3, padding=1),
166
+ nn.ReLU(inplace=True),
167
+ nn.Dropout2d(p=dropout_prob),
168
+ nn.Conv2d(hidden_dim, num_classes, kernel_size=1)
169
+ )
170
+ def forward(self, x):
171
+ return self.model(x)
172
+
173
+ class FireflyConfigRGB(FireflyBaseConfig):
174
+ model_type = "firefly-rgb"
175
+ def __init__(self, **kwargs):
176
+ super().__init__(**kwargs)
177
+
178
+ class FireflyForSemanticSegmentationRGB(FireflyBaseModel):
179
+ config_class = FireflyConfigRGB
180
+
181
+ def __init__(self, config: FireflyConfigRGB):
182
+ super().__init__(config)
183
+ self.head = MLPHead(
184
+ in_channels=config.backbone_embed_dim,
185
+ hidden_dim=config.embedding_dim,
186
+ num_classes=config.num_labels,
187
+ dropout_prob=config.dropout_ratio
188
+ )
189
+ self.loss_fn = DiceLoss()
190
+
191
+ def _extract_features(self, pixel_values: torch.Tensor, h_pad: int, w_pad: int):
192
+ features_raw = self.backbone.get_intermediate_layers(pixel_values, n=1)
193
+ feat = features_raw[0]
194
+ if isinstance(feat, tuple): feat = feat[0]
195
+
196
+ if feat.ndim == 3:
197
+ B, N, Dim = feat.shape
198
+ H_grid, W_grid = h_pad // self.config.patch_size, w_pad // self.config.patch_size
199
+ num_spatial_tokens = H_grid * W_grid
200
+ if N > num_spatial_tokens:
201
+ feat = feat[:, -num_spatial_tokens:, :]
202
+ feat = feat.permute(0, 2, 1).view(B, Dim, H_grid, W_grid)
203
+ return feat
204
+
205
+ def forward(self, pixel_values: torch.Tensor, labels: torch.Tensor = None, **kwargs):
206
+ original_h, original_w = pixel_values.shape[-2:]
207
+ x_padded, padding = pad_to_multiple(pixel_values, self.config.patch_size)
208
+ pad_h, pad_w = x_padded.shape[-2], x_padded.shape[-1]
209
+
210
+ features = self._extract_features(x_padded, pad_h, pad_w)
211
+ out_upscaled = F.interpolate(self.head(features), size=(pad_h, pad_w), mode='bilinear', align_corners=False)
212
+ logits = crop_to_shape(out_upscaled, original_h, original_w)
213
+
214
+ loss = None
215
+ if labels is not None:
216
+ valid_mask = labels != self.config.semantic_loss_ignore_index
217
+ logits_masked = logits.permute(0, 2, 3, 1)[valid_mask]
218
+ loss = self.loss_fn(logits_masked, labels[valid_mask])
219
+
220
+ return SemanticSegmenterOutput(loss=loss, logits=logits)
lib/firefly_thermal.py ADDED
@@ -0,0 +1,428 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import math
3
+ from functools import reduce
4
+ from operator import mul
5
+
6
+ import torch
7
+ import torch.nn as nn
8
+ import torch.nn.functional as F
9
+ from torch import Tensor
10
+ from transformers import PreTrainedModel, PretrainedConfig
11
+ from transformers.modeling_outputs import SemanticSegmenterOutput
12
+
13
+ # ===================================================================
14
+ # 1. SHARED UTILITIES & LOSS FUNCTIONS
15
+ # ===================================================================
16
+
17
+ def dice_loss(pred, target, skip_classes=None, eps=1e-6):
18
+ N, C = pred.shape
19
+
20
+ if N == 0:
21
+ return pred.sum() * 0.0
22
+
23
+ p = torch.softmax(pred, dim=1)
24
+
25
+ t = torch.nn.functional.one_hot(target, C).float()
26
+
27
+ if skip_classes is None:
28
+ skip_classes = []
29
+
30
+ present = (t.sum(dim=0) > 0)
31
+
32
+ for sc in skip_classes:
33
+ if 0 <= sc < C:
34
+ present[sc] = False
35
+
36
+ if present.sum() == 0:
37
+ return pred.new_tensor(0.)
38
+
39
+ p_sel = p[:, present]
40
+ t_sel = t[:, present]
41
+
42
+ inter = (p_sel * t_sel).sum(dim=0)
43
+ union = p_sel.sum(dim=0) + t_sel.sum(dim=0)
44
+ dice = (2 * inter + eps) / (union + eps)
45
+
46
+ return 1 - dice.mean()
47
+
48
+ class DiceLoss(nn.Module):
49
+ def __init__(self, skip_classes=None, eps=1e-6):
50
+ super(DiceLoss, self).__init__()
51
+ self.skip_classes = skip_classes
52
+ self.eps = eps
53
+
54
+ def forward(self, pred, target):
55
+ return dice_loss(pred, target, skip_classes=self.skip_classes, eps=self.eps)
56
+
57
+ def pad_to_multiple(x, multiple):
58
+ """Pads the input tensor so its spatial dimensions are multiples of 'multiple'."""
59
+ _, _, h, w = x.shape
60
+ pad_h = (multiple - h % multiple) % multiple
61
+ pad_w = (multiple - w % multiple) % multiple
62
+ padding = (0, pad_w, 0, pad_h)
63
+ x_padded = F.pad(x, padding, mode='reflect')
64
+ return x_padded, padding
65
+
66
+ def crop_to_shape(x, target_h, target_w):
67
+ """Crops the input tensor back to the target spatial dimensions."""
68
+ return x[:, :, :target_h, :target_w]
69
+
70
+ class MLPHead(nn.Module):
71
+ def __init__(self, in_channels=768, hidden_dim=256, num_classes=12, dropout_prob=0.1):
72
+ super().__init__()
73
+ self.model = nn.Sequential(
74
+ nn.Conv2d(in_channels, hidden_dim, kernel_size=3, padding=1),
75
+ nn.ReLU(inplace=True),
76
+ nn.Dropout2d(p=dropout_prob),
77
+ nn.Conv2d(hidden_dim, hidden_dim, kernel_size=3, padding=1),
78
+ nn.ReLU(inplace=True),
79
+ nn.Dropout2d(p=dropout_prob),
80
+ nn.Conv2d(hidden_dim, num_classes, kernel_size=1)
81
+ )
82
+ self.upscale = True
83
+
84
+ def forward(self, x):
85
+ return self.model(x)
86
+
87
+ # ===================================================================
88
+ # 2. REIN IMPLEMENTATION
89
+ # ===================================================================
90
+
91
+ class Reins(nn.Module):
92
+ def __init__(
93
+ self,
94
+ num_layers: int,
95
+ embed_dims: int,
96
+ patch_size: int,
97
+ query_dims: int = 256,
98
+ token_length: int = 100,
99
+ use_softmax: bool = True,
100
+ scale_init: float = 0.001,
101
+ ) -> None:
102
+ super().__init__()
103
+ self.num_layers = num_layers
104
+ self.embed_dims = embed_dims
105
+ self.patch_size = patch_size
106
+ self.query_dims = query_dims
107
+ self.token_length = token_length
108
+ self.scale_init = scale_init
109
+ self.use_softmax = use_softmax
110
+ self.create_model()
111
+
112
+ def create_model(self):
113
+ self.learnable_tokens = nn.Parameter(
114
+ torch.empty([self.num_layers, self.token_length, self.embed_dims])
115
+ )
116
+ self.scale = nn.Parameter(torch.tensor(self.scale_init))
117
+
118
+ self.mlp_token2feat = nn.Linear(self.embed_dims, self.embed_dims)
119
+ self.mlp_delta_f = nn.Linear(self.embed_dims, self.embed_dims)
120
+
121
+ val = math.sqrt(
122
+ 6.0
123
+ / float(
124
+ 3 * reduce(mul, (self.patch_size, self.patch_size), 1) + self.embed_dims
125
+ )
126
+ )
127
+ nn.init.uniform_(self.learnable_tokens.data, -val, val)
128
+ nn.init.kaiming_uniform_(self.mlp_delta_f.weight, a=math.sqrt(5))
129
+ nn.init.kaiming_uniform_(self.mlp_token2feat.weight, a=math.sqrt(5))
130
+
131
+ self.transform = nn.Linear(self.embed_dims, self.query_dims)
132
+ self.merge = nn.Linear(self.query_dims * 3, self.query_dims)
133
+
134
+ def get_tokens(self, layer: int) -> Tensor:
135
+ if layer == -1:
136
+ return self.learnable_tokens
137
+ else:
138
+ return self.learnable_tokens[layer]
139
+
140
+ def forward(
141
+ self, feats: Tensor, layer: int, batch_first=False, has_cls_token=True
142
+ ) -> Tensor:
143
+ if batch_first:
144
+ feats = feats.permute(1, 0, 2)
145
+
146
+ if has_cls_token:
147
+ cls_token, feats = torch.tensor_split(feats, [1], dim=0)
148
+
149
+ tokens = self.get_tokens(layer)
150
+
151
+ delta_feat = self.forward_delta_feat(
152
+ feats,
153
+ tokens,
154
+ layer,
155
+ )
156
+
157
+ delta_feat = delta_feat * self.scale
158
+ feats = feats + delta_feat
159
+
160
+ if has_cls_token:
161
+ feats = torch.cat([cls_token, feats], dim=0)
162
+
163
+ if batch_first:
164
+ feats = feats.permute(1, 0, 2)
165
+
166
+ return feats
167
+
168
+ def forward_delta_feat(self, feats: Tensor, tokens: Tensor, layers: int) -> Tensor:
169
+ attn = torch.einsum("nbc,mc->nbm", feats, tokens)
170
+
171
+ if self.use_softmax:
172
+ attn = attn * (self.embed_dims**-0.5)
173
+ attn = F.softmax(attn, dim=-1)
174
+
175
+ delta_f = torch.einsum(
176
+ "nbm,mc->nbc",
177
+ attn[:, :, 1:],
178
+ self.mlp_token2feat(tokens[1:, :]),
179
+ )
180
+
181
+ delta_f = self.mlp_delta_f(delta_f + feats)
182
+ return delta_f
183
+
184
+ class ReinHook:
185
+ def __init__(self, rein_module, layer_idx):
186
+ self.rein_module = rein_module
187
+ self.layer_idx = layer_idx
188
+
189
+ def __call__(self, module, input, output):
190
+ is_tuple = isinstance(output, tuple)
191
+ is_list = isinstance(output, list)
192
+
193
+ if is_tuple:
194
+ x = output[0]
195
+ elif is_list:
196
+ x = output[0]
197
+ else:
198
+ x = output
199
+
200
+ refined_x = self.rein_module(x, self.layer_idx, batch_first=True, has_cls_token=True)
201
+
202
+ if is_tuple:
203
+ return (refined_x,) + output[1:]
204
+ elif is_list:
205
+ output[0] = refined_x
206
+ return output
207
+ else:
208
+ return refined_x
209
+
210
+ class FireflyConfigThermal(PretrainedConfig):
211
+ model_type = "firefly-thermal"
212
+
213
+ def __init__(
214
+ self,
215
+ num_labels: int = 2,
216
+ image_size: int = 512,
217
+ embedding_dim: int = 256,
218
+ backbone_embed_dim: int = 768,
219
+ patch_size: int = 16,
220
+ num_layers: int = 12,
221
+ rein_token_length: int = 100,
222
+ feature_layers: list = None,
223
+ repo_dir: str = None,
224
+ model_name: str = "dinov3_vitb16",
225
+ backbone_weights_path: str = None,
226
+ finetuned_weights_path: str = None,
227
+ semantic_loss_ignore_index: int = 255,
228
+ dropout_ratio: float = 0.1,
229
+ **kwargs
230
+ ):
231
+ super().__init__(**kwargs)
232
+ self.num_labels = num_labels
233
+ self.image_size = image_size
234
+ self.embedding_dim = embedding_dim
235
+ self.backbone_embed_dim = backbone_embed_dim
236
+ self.patch_size = patch_size
237
+ self.num_layers = num_layers
238
+ self.rein_token_length = rein_token_length
239
+ self.feature_layers = feature_layers or [2, 5, 8, 11]
240
+ self.repo_dir = repo_dir
241
+ self.model_name = model_name
242
+ self.backbone_weights_path = backbone_weights_path
243
+ self.finetuned_weights_path = finetuned_weights_path
244
+ self.semantic_loss_ignore_index = semantic_loss_ignore_index
245
+ self.dropout_ratio = dropout_ratio
246
+
247
+ class FireflyForSemanticSegmentationThermal(PreTrainedModel):
248
+ config_class = FireflyConfigThermal
249
+
250
+ def __init__(self, config: FireflyConfigThermal):
251
+ super().__init__(config)
252
+ self.config = config
253
+ self.num_labels = config.num_labels
254
+
255
+ print(f"Loading backbone architecture: {config.model_name}")
256
+ self.backbone = torch.hub.load(
257
+ config.repo_dir,
258
+ config.model_name,
259
+ source="local",
260
+ weights=config.backbone_weights_path,
261
+ pretrained=False
262
+ )
263
+
264
+ print(f"Injecting Rein Module (Tokens={config.rein_token_length})...")
265
+ self.rein = Reins(
266
+ num_layers=config.num_layers,
267
+ embed_dims=config.backbone_embed_dim,
268
+ patch_size=config.patch_size,
269
+ token_length=config.rein_token_length
270
+ )
271
+
272
+ if hasattr(self.backbone, 'blocks'):
273
+ blocks = self.backbone.blocks
274
+ elif hasattr(self.backbone, 'transformer') and hasattr(self.backbone.transformer, 'blocks'):
275
+ blocks = self.backbone.transformer.blocks
276
+ else:
277
+ raise AttributeError("Could not find '.blocks' in backbone model. Check model structure.")
278
+
279
+ self.hooks = []
280
+ for i, block in enumerate(blocks):
281
+ hook_fn = ReinHook(self.rein, i)
282
+ handle = block.register_forward_hook(hook_fn)
283
+ self.hooks.append(handle)
284
+
285
+ if config.finetuned_weights_path and os.path.exists(config.finetuned_weights_path):
286
+ print(f"Loading finetuned rein adapter weights: {config.finetuned_weights_path}")
287
+ checkpoint = torch.load(config.finetuned_weights_path, map_location='cpu')
288
+
289
+ state_dict = checkpoint
290
+ if isinstance(checkpoint, dict):
291
+ if "rein_model" in checkpoint:
292
+ state_dict = checkpoint["rein_model"]
293
+ elif "student_model" in checkpoint:
294
+ state_dict = checkpoint["student_model"]
295
+ elif "state_dict" in checkpoint:
296
+ state_dict = checkpoint["state_dict"]
297
+ elif "model" in checkpoint:
298
+ state_dict = checkpoint["model"]
299
+
300
+ new_state_dict = {}
301
+ for k, v in state_dict.items():
302
+ clean_k = k
303
+ for prefix in ["module.", "rein_model.", "rein.", "student_model.", "model."]:
304
+ if clean_k.startswith(prefix):
305
+ clean_k = clean_k.replace(prefix, "", 1)
306
+ new_state_dict[clean_k] = v
307
+
308
+ msg = self.rein.load_state_dict(new_state_dict, strict=False)
309
+ print(msg)
310
+ if len(msg.missing_keys) > 0:
311
+ print(f"[Diagnostic] Found {len(msg.missing_keys)} missing keys in Rein module loading.")
312
+ print(f"[Warning] Missing keys: {msg.missing_keys}")
313
+ else:
314
+ print("[Success] All Rein Adapter weights perfectly matched and loaded!")
315
+ else:
316
+ print(f"Warning: Finetuned weights not found at {config.finetuned_weights_path}. Using base/random weights.")
317
+
318
+ self.head = MLPHead(
319
+ in_channels=config.backbone_embed_dim,
320
+ hidden_dim=config.embedding_dim,
321
+ num_classes=config.num_labels,
322
+ dropout_prob=config.dropout_ratio
323
+ )
324
+
325
+ self.loss_fn = DiceLoss()
326
+
327
+ self.feature_layers = config.feature_layers
328
+ self._setup_trainable_params()
329
+
330
+ def _setup_trainable_params(self):
331
+ for param in self.backbone.parameters():
332
+ param.requires_grad = False
333
+
334
+ for param in self.rein.parameters():
335
+ param.requires_grad = True
336
+
337
+ for param in self.head.parameters():
338
+ param.requires_grad = True
339
+
340
+ def _extract_features(self, pixel_values: torch.Tensor, h_pad: int, w_pad: int):
341
+ features_raw = self.backbone.get_intermediate_layers(
342
+ pixel_values,
343
+ n=1
344
+ )
345
+
346
+ feat = features_raw[0]
347
+ if isinstance(feat, tuple):
348
+ feat = feat[0]
349
+
350
+ if feat.ndim == 3:
351
+ B, N, Dim = feat.shape
352
+ H_grid = h_pad // self.config.patch_size
353
+ W_grid = w_pad // self.config.patch_size
354
+
355
+ num_spatial_tokens = H_grid * W_grid
356
+ if N > num_spatial_tokens:
357
+ feat = feat[:, -num_spatial_tokens:, :]
358
+
359
+ feat = feat.permute(0, 2, 1).view(B, Dim, H_grid, W_grid)
360
+
361
+ return feat
362
+
363
+ def forward(self, pixel_values: torch.Tensor, labels: torch.Tensor = None, **kwargs):
364
+ original_h, original_w = pixel_values.shape[-2:]
365
+
366
+ x_padded, padding = pad_to_multiple(pixel_values, self.config.patch_size)
367
+ pad_h, pad_w = x_padded.shape[-2], x_padded.shape[-1]
368
+
369
+ features = self._extract_features(x_padded, pad_h, pad_w)
370
+ out = self.head(features)
371
+
372
+ out_upscaled = F.interpolate(
373
+ out,
374
+ size=(pad_h, pad_w),
375
+ mode='bilinear',
376
+ align_corners=False
377
+ )
378
+
379
+ logits = crop_to_shape(out_upscaled, original_h, original_w)
380
+
381
+ loss = None
382
+ if labels is not None:
383
+ valid_mask = labels != self.config.semantic_loss_ignore_index
384
+ logits_masked = logits.permute(0, 2, 3, 1)[valid_mask]
385
+ labels_masked = labels[valid_mask]
386
+ loss = self.loss_fn(logits_masked, labels_masked)
387
+
388
+ return SemanticSegmenterOutput(
389
+ loss=loss,
390
+ logits=logits,
391
+ )
392
+
393
+ @classmethod
394
+ def from_pretrained(cls, pretrained_model_name_or_path=None, **kwargs):
395
+ config_kwargs = {
396
+ "num_labels": kwargs.pop("num_labels", 2),
397
+ "image_size": kwargs.pop("image_size", 512),
398
+ "embedding_dim": kwargs.pop("embedding_dim", 256),
399
+ "backbone_embed_dim": kwargs.pop("backbone_embed_dim", 768),
400
+ "patch_size": kwargs.pop("patch_size", 16),
401
+ "num_layers": kwargs.pop("num_layers", 12),
402
+ "rein_token_length": kwargs.pop("rein_token_length", 100),
403
+ "feature_layers": kwargs.pop("feature_layers", [2, 5, 8, 11]),
404
+ "repo_dir": kwargs.pop("repo_dir", None),
405
+ "model_name": kwargs.pop("model_name", "dinov3_vitb16"),
406
+ "backbone_weights_path": kwargs.pop("backbone_weights_path", ""),
407
+ "finetuned_weights_path": kwargs.pop("finetuned_weights_path", pretrained_model_name_or_path),
408
+ "semantic_loss_ignore_index": kwargs.pop("semantic_loss_ignore_index", 255),
409
+ "dropout_ratio": kwargs.pop("dropout_ratio", 0.1),
410
+ "id2label": kwargs.pop("id2label", None),
411
+ "label2id": kwargs.pop("label2id", None),
412
+ }
413
+
414
+ kwargs.pop("ignore_mismatched_sizes", None)
415
+ config = kwargs.pop("config", None)
416
+
417
+ if config is None:
418
+ config = FireflyConfigThermal(**config_kwargs)
419
+
420
+ model = cls(config)
421
+ return model
422
+
423
+ def print_trainable_params(self):
424
+ total = sum(p.numel() for p in self.parameters())
425
+ trainable = sum(p.numel() for p in self.parameters() if p.requires_grad)
426
+ print(f"Total Parameters: {total / 1e6:.2f}M")
427
+ print(f"Trainable Parameters: {trainable / 1e6:.2f}M")
428
+ print(f"Trainable Ratio: {100 * trainable / total:.2f}%")
lib/utils_segfly.py ADDED
@@ -0,0 +1,251 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Utility functions for dataset loading, visualization, and timing."""
2
+
3
+ import os
4
+ from functools import wraps
5
+ from time import perf_counter
6
+ from typing import List
7
+
8
+ import cv2
9
+ import numpy as np
10
+ import pandas as pd
11
+ import torch
12
+ import albumentations as aug
13
+ from torch.utils.data import Dataset
14
+
15
+
16
+ # ==============================================================================
17
+ # VISUALIZATION PALETTE
18
+ # ==============================================================================
19
+
20
+ ID2COLOR = {
21
+ 0: [128, 0, 128],
22
+ 1: [204, 163, 72],
23
+ 2: [128, 0, 0],
24
+ 3: [192, 192, 192],
25
+ 4: [0, 255, 0],
26
+ 5: [112, 148, 32],
27
+ 6: [64, 64, 0],
28
+ 7: [255, 255, 0],
29
+ 8: [0, 128, 128],
30
+ 9: [0, 0, 255],
31
+ 10: [255, 0, 0],
32
+ 11: [64, 160, 120],
33
+ 12: [128, 64, 128],
34
+ 13: [240, 120, 120],
35
+ 14: [128, 128, 64],
36
+ 255: [0, 0, 0],
37
+ }
38
+
39
+
40
+ def mask2label(mask, palette):
41
+ """Convert a class-index mask to a colorized RGB image."""
42
+ if mask.ndim == 3 and mask.shape[0] == 1:
43
+ mask = mask.squeeze(0)
44
+ if isinstance(mask, torch.Tensor):
45
+ mask = mask.detach().cpu().numpy()
46
+
47
+ color_seg = np.zeros((mask.shape[0], mask.shape[1], 3), dtype=np.uint8)
48
+ for label, color in palette.items():
49
+ color_seg[mask == label, :] = color
50
+ return color_seg
51
+
52
+
53
+ # ==============================================================================
54
+ # DATASET CLASSES
55
+ # ==============================================================================
56
+
57
+ class InferenceDataset(Dataset):
58
+ """Dataset for RGB inference with class remapping and dropping."""
59
+
60
+ def __init__(
61
+ self,
62
+ _root_dir: str,
63
+ config: dict,
64
+ _ignore_index: int = 255,
65
+ _original_num_classes: int = 22,
66
+ ):
67
+ """
68
+ Args:
69
+ _root_dir (str): Path to the root directory. Walked recursively;
70
+ images are collected from any subfolder named `src/` and masks
71
+ from any subfolder named `gt/`. No specific top-level structure
72
+ is required.
73
+ config (dict): Dictionary containing the configuration.
74
+ _ignore_index (int): Index used for dropped classes.
75
+ _original_num_classes (int): Number of original classes.
76
+ """
77
+ self.root_dir = _root_dir
78
+ self.transforms = aug.Compose(
79
+ [aug.Resize(config["image_size"], config["image_size"], interpolation=cv2.INTER_NEAREST)]
80
+ )
81
+ self.ignore_index = _ignore_index
82
+ self.mapping_lut = np.full(256, self.ignore_index, dtype=np.uint8)
83
+
84
+ df = pd.read_csv(config["class_dict_path"])
85
+
86
+ new_class_id = 0
87
+ raw_to_new_dict = {}
88
+
89
+ for _, row in df.iterrows():
90
+ raw_id = int(row['id'])
91
+ if raw_id == self.ignore_index:
92
+ continue
93
+ self.mapping_lut[raw_id] = new_class_id
94
+ raw_to_new_dict[raw_id] = new_class_id
95
+ new_class_id += 1
96
+
97
+ if 9 in raw_to_new_dict:
98
+ target_id_for_merges = raw_to_new_dict[9]
99
+ self.mapping_lut[5] = target_id_for_merges
100
+ self.mapping_lut[22] = target_id_for_merges
101
+
102
+ self.num_new_classes = new_class_id
103
+
104
+ image_file_names = []
105
+ annotation_file_names = []
106
+ for root, _, files in os.walk(self.root_dir):
107
+ for name in files:
108
+ if ".ipynb_checkpoints" in root:
109
+ continue
110
+ if "src" in root and name.lower().endswith((".png", ".jpeg", ".jpg")):
111
+ image_file_names.append(os.path.join(root, name))
112
+ elif "gt" in root and name.lower().endswith(".png"):
113
+ if "vis_rem" not in os.path.join(root, name):
114
+ annotation_file_names.append(os.path.join(root, name))
115
+
116
+ self.images = sorted(image_file_names)
117
+ self.annotations = sorted(annotation_file_names)
118
+
119
+ assert len(self.images) == len(self.annotations), (
120
+ f"There must be as many images as there are segmentation maps. "
121
+ f"Image = {len(self.images)}, Masks = {len(self.annotations)}"
122
+ )
123
+
124
+ def __len__(self):
125
+ return len(self.images)
126
+
127
+ def __getitem__(self, idx):
128
+ image = cv2.imread(self.images[idx])
129
+ image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
130
+ segmentation_map = cv2.imread(self.annotations[idx], cv2.IMREAD_GRAYSCALE)
131
+
132
+ segmentation_map = self.mapping_lut[segmentation_map]
133
+
134
+ if self.transforms is not None:
135
+ augmented = self.transforms(image=image, mask=segmentation_map)
136
+ return augmented["image"], augmented["mask"]
137
+
138
+ return image, segmentation_map
139
+
140
+
141
+ class InferenceDatasetThermal(Dataset):
142
+ """Dataset for thermal inference with class remapping and dropping."""
143
+
144
+ def __init__(
145
+ self,
146
+ _root_dir: str,
147
+ config: dict,
148
+ _ignore_index: int = 255,
149
+ ):
150
+ """
151
+ Args:
152
+ _root_dir (str): Path to the root directory. Walked recursively;
153
+ images are collected from any subfolder named `thermal_src/`
154
+ and masks from any subfolder named `gt/`. No specific top-level
155
+ structure is required.
156
+ config (dict): Dictionary containing the configuration.
157
+ _ignore_index (int): Index used for dropped classes.
158
+ """
159
+ self.root_dir = _root_dir
160
+ self.transforms = aug.Compose(
161
+ [aug.Resize(config["image_size"], config["image_size"], interpolation=cv2.INTER_NEAREST)]
162
+ )
163
+ self.ignore_index = _ignore_index
164
+ self.mapping_lut = np.full(256, self.ignore_index, dtype=np.uint8)
165
+
166
+ df = pd.read_csv(config["class_dict_path"])
167
+
168
+ new_class_id = 0
169
+ raw_to_new_dict = {}
170
+
171
+ for _, row in df.iterrows():
172
+ raw_id = int(row['id'])
173
+ self.mapping_lut[raw_id] = new_class_id
174
+ raw_to_new_dict[raw_id] = new_class_id
175
+ new_class_id += 1
176
+
177
+ if 9 in raw_to_new_dict:
178
+ target_id_for_merges = raw_to_new_dict[9]
179
+ self.mapping_lut[5] = target_id_for_merges
180
+ self.mapping_lut[22] = target_id_for_merges
181
+
182
+ self.num_new_classes = new_class_id
183
+
184
+ image_file_names = []
185
+ annotation_file_names = []
186
+ for root, _, files in os.walk(self.root_dir):
187
+ for name in files:
188
+ if ".ipynb_checkpoints" in root:
189
+ continue
190
+ if "thermal_src" in root and name.lower().endswith((".png", ".jpeg", ".jpg")):
191
+ image_file_names.append(os.path.join(root, name))
192
+ elif "gt" in root and name.lower().endswith(".png"):
193
+ annotation_file_names.append(os.path.join(root, name))
194
+
195
+ self.images = sorted(image_file_names)
196
+ self.annotations = sorted(annotation_file_names)
197
+
198
+ assert len(self.images) == len(self.annotations), (
199
+ f"There must be as many images as there are segmentation maps."
200
+ f"Image = {len(self.images)}, Masks = {len(self.annotations)}"
201
+ )
202
+
203
+ def __len__(self):
204
+ return len(self.images)
205
+
206
+ def __getitem__(self, idx):
207
+ image = cv2.imread(self.images[idx])
208
+ image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
209
+ segmentation_map = cv2.imread(self.annotations[idx], cv2.IMREAD_GRAYSCALE)
210
+
211
+ segmentation_map = self.mapping_lut[segmentation_map]
212
+
213
+ if self.transforms is not None:
214
+ augmented = self.transforms(image=image, mask=segmentation_map)
215
+ return augmented["image"], augmented["mask"]
216
+
217
+ return image, segmentation_map
218
+
219
+
220
+ # ==============================================================================
221
+ # TIMING UTILITY
222
+ # ==============================================================================
223
+
224
+ class Timing:
225
+ """Class to time functions and methods."""
226
+
227
+ def __init__(self):
228
+ self.inf_time = []
229
+
230
+ def __call__(self, func):
231
+ @wraps(func)
232
+ def wrapper(*args, **kwargs):
233
+ start = perf_counter()
234
+ result = func(*args, **kwargs)
235
+ end = perf_counter()
236
+ self.inf_time.append(end - start)
237
+ return result
238
+
239
+ return wrapper
240
+
241
+ def print_average(self):
242
+ """Print the average timing of the function."""
243
+ print(f"---\tFunction took {np.mean(self.inf_time):.4f} seconds to run!")
244
+
245
+ def get_average(self) -> float:
246
+ """Return the average timing of the function."""
247
+ return float(np.mean(self.inf_time))
248
+
249
+ def reset(self):
250
+ """Reset the timing of the function."""
251
+ self.inf_time = []
requirements.txt ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ torch>=2.9
2
+ transformers==4.37.2
3
+ evaluate>=0.4.0
4
+ datasets>=2.20.0
5
+ numpy>=1.24.4
6
+ pandas>=1.5.3
7
+ albumentations>=1.3.1
8
+ opencv-python-headless>=4.9.0
9
+ safetensors
10
+ tqdm
11
+ pyyaml
12
+ matplotlib>=3.8.2
13
+ torchmetrics>=1.8.2
14
+ torchvision
15
+ termcolor