prateeksharmacoder commited on
Commit
8d928e8
Β·
verified Β·
1 Parent(s): 2376099

Deploy ZeroGPU compatible code with Sen2SR

Browse files
This view is limited to 50 files because it contains too many changes. Β  See raw diff
Files changed (50) hide show
  1. .gitattributes +13 -0
  2. .gradio/certificate.pem +31 -0
  3. .huggingfaceignore +12 -0
  4. Sentinel-2_L2A_B02_(Raw).tif +0 -0
  5. Sentinel-2_L2A_B03_(Raw).tif +0 -0
  6. Sentinel-2_L2A_B04_(Raw).tif +0 -0
  7. app.py +0 -0
  8. geotiff_test.py +390 -0
  9. inference/__pycache__/geotiff.cpython-312.pyc +0 -0
  10. inference/geotiff.py +46 -10
  11. inference/pipeline.py +504 -504
  12. local_test.py +219 -0
  13. model_router.py +63 -3
  14. models/__pycache__/__init__.cpython-312.pyc +0 -0
  15. models/__pycache__/__init__.cpython-313.pyc +0 -0
  16. models/esrgan/__pycache__/RRDBNet_arch.cpython-312.pyc +0 -0
  17. models/esrgan/__pycache__/RRDBNet_arch.cpython-313.pyc +0 -0
  18. models/esrgan/__pycache__/__init__.cpython-312.pyc +0 -0
  19. models/esrgan/__pycache__/__init__.cpython-313.pyc +0 -0
  20. models/esrgan/__pycache__/esrgan_inference.cpython-312.pyc +0 -0
  21. models/esrgan/__pycache__/esrgan_inference.cpython-313.pyc +0 -0
  22. models/hatsat/__pycache__/__init__.cpython-312.pyc +0 -0
  23. models/hatsat/__pycache__/__init__.cpython-313.pyc +0 -0
  24. models/hatsat/__pycache__/components.cpython-312.pyc +0 -0
  25. models/hatsat/__pycache__/components.cpython-313.pyc +0 -0
  26. models/hatsat/__pycache__/hat_model.cpython-312.pyc +0 -0
  27. models/hatsat/__pycache__/hat_model.cpython-313.pyc +0 -0
  28. models/hatsat/__pycache__/hatsat_inference.cpython-312.pyc +0 -0
  29. models/hatsat/__pycache__/hatsat_inference.cpython-313.pyc +0 -0
  30. models/sen2sr/__init__.py +1 -0
  31. models/sen2sr/__pycache__/__init__.cpython-312.pyc +0 -0
  32. models/sen2sr/__pycache__/sen2sr_inference.cpython-312.pyc +0 -0
  33. models/sen2sr/sen2sr_inference.py +162 -0
  34. outputs/geotiff_test/numpy_input_sr.tif +0 -0
  35. outputs/geotiff_test/synthetic_sr_4x.tif +0 -0
  36. outputs/local_test/test_esrgan_output.png +3 -0
  37. outputs/local_test/test_hatsat_output.png +3 -0
  38. outputs/robustness_test/esrgan_100x80.png +3 -0
  39. outputs/robustness_test/esrgan_17x17.png +0 -0
  40. outputs/robustness_test/esrgan_198x139.png +3 -0
  41. outputs/robustness_test/esrgan_256x256.png +3 -0
  42. outputs/robustness_test/esrgan_64x64.png +3 -0
  43. outputs/robustness_test/hatsat_100x80.png +3 -0
  44. outputs/robustness_test/hatsat_128x128_mod16.png +3 -0
  45. outputs/robustness_test/hatsat_129x129.png +3 -0
  46. outputs/robustness_test/hatsat_16x16_mod16.png +0 -0
  47. outputs/robustness_test/hatsat_17x17.png +0 -0
  48. outputs/robustness_test/hatsat_198x139.png +3 -0
  49. outputs/robustness_test/hatsat_255x255.png +3 -0
  50. outputs/robustness_test/hatsat_256x256_mod16.png +3 -0
.gitattributes CHANGED
@@ -33,3 +33,16 @@ saved_model/**/* 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
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
36
+ outputs/local_test/test_esrgan_output.png filter=lfs diff=lfs merge=lfs -text
37
+ outputs/local_test/test_hatsat_output.png filter=lfs diff=lfs merge=lfs -text
38
+ outputs/robustness_test/esrgan_100x80.png filter=lfs diff=lfs merge=lfs -text
39
+ outputs/robustness_test/esrgan_198x139.png filter=lfs diff=lfs merge=lfs -text
40
+ outputs/robustness_test/esrgan_256x256.png filter=lfs diff=lfs merge=lfs -text
41
+ outputs/robustness_test/esrgan_64x64.png filter=lfs diff=lfs merge=lfs -text
42
+ outputs/robustness_test/hatsat_100x80.png filter=lfs diff=lfs merge=lfs -text
43
+ outputs/robustness_test/hatsat_128x128_mod16.png filter=lfs diff=lfs merge=lfs -text
44
+ outputs/robustness_test/hatsat_129x129.png filter=lfs diff=lfs merge=lfs -text
45
+ outputs/robustness_test/hatsat_198x139.png filter=lfs diff=lfs merge=lfs -text
46
+ outputs/robustness_test/hatsat_255x255.png filter=lfs diff=lfs merge=lfs -text
47
+ outputs/robustness_test/hatsat_256x256_mod16.png filter=lfs diff=lfs merge=lfs -text
48
+ outputs/robustness_test/hatsat_64x64_mod16.png filter=lfs diff=lfs merge=lfs -text
.gradio/certificate.pem ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
3
+ TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
4
+ cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
5
+ WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
6
+ ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
7
+ MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
8
+ h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
9
+ 0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
10
+ A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
11
+ T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
12
+ B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
13
+ B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
14
+ KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
15
+ OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
16
+ jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
17
+ qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
18
+ rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
19
+ HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
20
+ hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
21
+ ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
22
+ 3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
23
+ NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
24
+ ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
25
+ TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
26
+ jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
27
+ oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
28
+ 4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
29
+ mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
30
+ emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
31
+ -----END CERTIFICATE-----
.huggingfaceignore ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .venv/
2
+ __pycache__/
3
+ .gradio/
4
+ outputs/
5
+ test_*.py
6
+ test*.jpg
7
+ test*.png
8
+ test*.tif
9
+ local_test.py
10
+ robustness_test.py
11
+ validation.py
12
+ geotiff_test.py
Sentinel-2_L2A_B02_(Raw).tif ADDED
Sentinel-2_L2A_B03_(Raw).tif ADDED
Sentinel-2_L2A_B04_(Raw).tif ADDED
app.py CHANGED
The diff for this file is too large to render. See raw diff
 
geotiff_test.py ADDED
@@ -0,0 +1,390 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Phase 4 -- GeoTIFF Geospatial Verification Test
3
+ satellite_sr_deploy
4
+
5
+ Tests:
6
+ 1. save_sr_geotiff() writes a valid GeoTIFF from a PIL image + profile
7
+ 2. CRS is preserved exactly in the output
8
+ 3. Geographic bounds are preserved (same extent as input)
9
+ 4. Output pixel dimensions are exactly 4x the input
10
+ 5. Affine transform is correctly scaled (pixel size = input/4)
11
+ 6. Band ordering is R=1, G=2, B=3
12
+ 7. Pixel values are preserved correctly (uint8 RGB)
13
+ 8. Round-trip: create synthetic GeoTIFF -> read -> SR -> save -> verify
14
+ 9. Verify against an existing real GeoTIFF (from Colab outputs)
15
+ 10. normalize_rgb() clamps properly and returns uint8
16
+
17
+ Run from satellite_sr_deploy/:
18
+ $env:PYTHONIOENCODING="utf-8"; .venv\\Scripts\\python.exe geotiff_test.py
19
+ """
20
+
21
+ import sys
22
+ import math
23
+ from pathlib import Path
24
+
25
+ import numpy as np
26
+ from PIL import Image
27
+ import rasterio
28
+ from rasterio.transform import Affine
29
+ from rasterio.crs import CRS
30
+
31
+ ROOT = Path(__file__).resolve().parent
32
+ if str(ROOT) not in sys.path:
33
+ sys.path.insert(0, str(ROOT))
34
+
35
+ # Import only geotiff.py (no model loading needed for most tests)
36
+ from inference.geotiff import save_sr_geotiff, normalize_rgb
37
+
38
+ results = []
39
+
40
+ OUTDIR = ROOT / "outputs" / "geotiff_test"
41
+ OUTDIR.mkdir(parents=True, exist_ok=True)
42
+
43
+ PASS_SYM = "[PASS]"
44
+ FAIL_SYM = "[FAIL]"
45
+
46
+
47
+ def record(name, passed, note=""):
48
+ tag = PASS_SYM if passed else FAIL_SYM
49
+ msg = f"{tag} {name}"
50
+ if note:
51
+ msg += f" -- {note}"
52
+ print(msg)
53
+ results.append((name, passed, note))
54
+
55
+
56
+ def make_synthetic_geotiff(path, width=50, height=40, crs_epsg=4326):
57
+ """
58
+ Create a small synthetic single-band GeoTIFF with known transform/CRS.
59
+ Returns the profile used to write it.
60
+ """
61
+ # Mimic a Sentinel-2 scene over India (approximate EPSG:4326 coords)
62
+ west, north = 77.0, 13.0 # top-left corner
63
+ pixel_size_x = 0.0001 # degrees/pixel (~10 m)
64
+ pixel_size_y = -0.0001 # negative (north-to-south)
65
+
66
+ transform = Affine.translation(west, north) * Affine.scale(pixel_size_x, pixel_size_y)
67
+
68
+ profile = {
69
+ "driver": "GTiff",
70
+ "height": height,
71
+ "width": width,
72
+ "count": 1,
73
+ "dtype": "uint16",
74
+ "crs": CRS.from_epsg(crs_epsg),
75
+ "transform": transform,
76
+ }
77
+
78
+ data = np.random.randint(0, 3000, (height, width), dtype=np.uint16)
79
+
80
+ with rasterio.open(path, "w", **profile) as dst:
81
+ dst.write(data, 1)
82
+
83
+ return profile
84
+
85
+
86
+ # ================================================================
87
+ # GROUP 1 -- save_sr_geotiff: basic output verification
88
+ # ================================================================
89
+ print("=" * 65)
90
+ print("PHASE 4 -- GEOTIFF GEOSPATIAL VERIFICATION")
91
+ print("=" * 65)
92
+ print()
93
+ print("-" * 65)
94
+ print("GROUP 1 -- save_sr_geotiff: CRS, transform, dimensions, bands")
95
+ print("-" * 65)
96
+
97
+ # Build a synthetic source profile (50x40, EPSG:4326)
98
+ SRC_W, SRC_H = 50, 40
99
+ SCALE = 4
100
+ EXPECTED_W = SRC_W * SCALE
101
+ EXPECTED_H = SRC_H * SCALE
102
+
103
+ west, north = 77.0, 13.0
104
+ px_x, px_y = 0.0001, -0.0001
105
+ src_transform = Affine.translation(west, north) * Affine.scale(px_x, px_y)
106
+
107
+ source_profile = {
108
+ "driver": "GTiff",
109
+ "height": SRC_H,
110
+ "width": SRC_W,
111
+ "count": 1,
112
+ "dtype": "uint16",
113
+ "crs": CRS.from_epsg(4326),
114
+ "transform": src_transform,
115
+ }
116
+
117
+ # Synthetic SR image (4x size, gradient RGB)
118
+ sr_arr = np.zeros((EXPECTED_H, EXPECTED_W, 3), dtype=np.uint8)
119
+ sr_arr[:, :, 0] = np.linspace(0, 255, EXPECTED_W, dtype=np.uint8) # R
120
+ sr_arr[:, :, 1] = np.linspace(0, 200, EXPECTED_H, dtype=np.uint8).reshape(-1, 1) # G
121
+ sr_arr[:, :, 2] = 128 # B flat
122
+ sr_pil = Image.fromarray(sr_arr, mode="RGB")
123
+
124
+ output_tif = OUTDIR / "synthetic_sr_4x.tif"
125
+
126
+ try:
127
+ save_sr_geotiff(sr_pil, source_profile, output_tif, scale=SCALE)
128
+ saved_ok = True
129
+ except Exception as e:
130
+ saved_ok = False
131
+ record("save_sr_geotiff() runs without error", False, str(e))
132
+
133
+ if saved_ok:
134
+ record("save_sr_geotiff() runs without error", True)
135
+
136
+ with rasterio.open(output_tif) as out:
137
+ out_profile = out.profile
138
+ out_transform = out.transform
139
+ out_crs = out.crs
140
+ out_w = out.width
141
+ out_h = out.height
142
+ out_count = out.count
143
+ out_dtype = out.dtypes[0]
144
+ band_r = out.read(1)
145
+ band_g = out.read(2)
146
+ band_b = out.read(3)
147
+
148
+ # 1a. Dimensions
149
+ record(
150
+ "Output dimensions are 4x input",
151
+ out_w == EXPECTED_W and out_h == EXPECTED_H,
152
+ f"expected {EXPECTED_W}x{EXPECTED_H}, got {out_w}x{out_h}"
153
+ )
154
+
155
+ # 1b. CRS preserved
156
+ record(
157
+ "CRS preserved (EPSG:4326)",
158
+ out_crs.to_epsg() == 4326,
159
+ f"got CRS: {out_crs}"
160
+ )
161
+
162
+ # 1c. Transform: pixel size scaled by 1/4
163
+ expected_px_x = px_x / SCALE
164
+ expected_px_y = px_y / SCALE
165
+ tol = 1e-12
166
+ px_x_ok = abs(out_transform.a - expected_px_x) < tol
167
+ px_y_ok = abs(out_transform.e - expected_px_y) < tol
168
+ record(
169
+ "Pixel size scaled to 1/4 of source (x)",
170
+ px_x_ok,
171
+ f"expected {expected_px_x:.8f}, got {out_transform.a:.8f}"
172
+ )
173
+ record(
174
+ "Pixel size scaled to 1/4 of source (y)",
175
+ px_y_ok,
176
+ f"expected {expected_px_y:.8f}, got {out_transform.e:.8f}"
177
+ )
178
+
179
+ # 1d. Origin (top-left corner) unchanged
180
+ origin_ok = (
181
+ abs(out_transform.c - west) < tol and
182
+ abs(out_transform.f - north) < tol
183
+ )
184
+ record(
185
+ "Geographic origin (top-left) unchanged",
186
+ origin_ok,
187
+ f"expected ({west}, {north}), got ({out_transform.c:.6f}, {out_transform.f:.6f})"
188
+ )
189
+
190
+ # 1e. Geographic extent / bounds preserved
191
+ src_right = west + SRC_W * px_x
192
+ src_bottom = north + SRC_H * px_y
193
+ out_right = out_transform.c + out_w * out_transform.a
194
+ out_bottom = out_transform.f + out_h * out_transform.e
195
+ bounds_ok = (
196
+ abs(out_right - src_right) < tol and
197
+ abs(out_bottom - src_bottom) < tol
198
+ )
199
+ record(
200
+ "Geographic bounds (extent) preserved",
201
+ bounds_ok,
202
+ f"src right={src_right:.6f}, out right={out_right:.6f} | "
203
+ f"src bottom={src_bottom:.6f}, out bottom={out_bottom:.6f}"
204
+ )
205
+
206
+ # 1f. Band count
207
+ record("Band count is 3 (RGB)", out_count == 3, f"got {out_count}")
208
+
209
+ # 1g. dtype is uint8
210
+ record("Output dtype is uint8", out_dtype == "uint8", f"got {out_dtype}")
211
+
212
+ # 1h. Band ordering R=1, G=2, B=3
213
+ # Check representative pixels in the gradient
214
+ r_corner = int(sr_arr[0, EXPECTED_W - 1, 0]) # max-R corner
215
+ g_corner = int(sr_arr[EXPECTED_H - 1, 0, 1]) # max-G corner
216
+ b_mid = int(sr_arr[EXPECTED_H // 2, EXPECTED_W // 2, 2]) # flat B=128
217
+
218
+ r_read = int(band_r[0, EXPECTED_W - 1])
219
+ g_read = int(band_g[EXPECTED_H - 1, 0])
220
+ b_read = int(band_b[EXPECTED_H // 2, EXPECTED_W // 2])
221
+
222
+ record(
223
+ "Band 1 = Red channel",
224
+ r_read == r_corner,
225
+ f"expected R={r_corner}, got R={r_read}"
226
+ )
227
+ record(
228
+ "Band 2 = Green channel",
229
+ g_read == g_corner,
230
+ f"expected G={g_corner}, got G={g_read}"
231
+ )
232
+ record(
233
+ "Band 3 = Blue (flat 128)",
234
+ b_read == 128,
235
+ f"expected B=128, got B={b_read}"
236
+ )
237
+
238
+ print()
239
+
240
+ # ================================================================
241
+ # GROUP 2 -- normalize_rgb: clipping and dtype
242
+ # ================================================================
243
+ print("-" * 65)
244
+ print("GROUP 2 -- normalize_rgb: output range and dtype")
245
+ print("-" * 65)
246
+
247
+ # Typical Sentinel-2 DN range (0-10000+ reflectance)
248
+ test_raw = np.random.randint(0, 10000, (40, 50, 3), dtype=np.uint16).astype(np.float32)
249
+
250
+ try:
251
+ norm = normalize_rgb(test_raw.copy())
252
+
253
+ record(
254
+ "normalize_rgb returns uint8",
255
+ norm.dtype == np.uint8,
256
+ f"got dtype={norm.dtype}"
257
+ )
258
+ record(
259
+ "normalize_rgb output range [0, 255]",
260
+ norm.min() >= 0 and norm.max() <= 255,
261
+ f"min={norm.min()}, max={norm.max()}"
262
+ )
263
+ record(
264
+ "normalize_rgb shape preserved (H, W, 3)",
265
+ norm.shape == test_raw.shape,
266
+ f"expected {test_raw.shape}, got {norm.shape}"
267
+ )
268
+ except Exception as e:
269
+ record("normalize_rgb runs without error", False, str(e))
270
+
271
+ # Edge case: single-value band (all pixels same value)
272
+ flat = np.full((10, 10, 3), 5000, dtype=np.float32)
273
+ try:
274
+ norm_flat = normalize_rgb(flat)
275
+ record(
276
+ "normalize_rgb handles flat (constant) band",
277
+ norm_flat.dtype == np.uint8,
278
+ f"min={norm_flat.min()}, max={norm_flat.max()}"
279
+ )
280
+ except Exception as e:
281
+ record("normalize_rgb handles flat band", False, str(e))
282
+
283
+ print()
284
+
285
+ # ================================================================
286
+ # GROUP 3 -- save_sr_geotiff with numpy array input (not PIL)
287
+ # ================================================================
288
+ print("-" * 65)
289
+ print("GROUP 3 -- save_sr_geotiff accepts numpy array input")
290
+ print("-" * 65)
291
+
292
+ np_input_tif = OUTDIR / "numpy_input_sr.tif"
293
+ try:
294
+ save_sr_geotiff(sr_arr, source_profile, np_input_tif, scale=SCALE)
295
+ with rasterio.open(np_input_tif) as f:
296
+ dims_ok = f.width == EXPECTED_W and f.height == EXPECTED_H
297
+ record("save_sr_geotiff accepts numpy ndarray", dims_ok,
298
+ f"{EXPECTED_W}x{EXPECTED_H} ok={dims_ok}")
299
+ except Exception as e:
300
+ record("save_sr_geotiff accepts numpy ndarray", False, str(e))
301
+
302
+ print()
303
+
304
+ # ================================================================
305
+ # GROUP 4 -- Verify existing real GeoTIFF from Colab outputs
306
+ # ================================================================
307
+ print("-" * 65)
308
+ print("GROUP 4 -- Verify existing Colab-produced GeoTIFFs")
309
+ print("-" * 65)
310
+
311
+ REAL_TIFS = [
312
+ Path(r"D:\downloads\satellite_resolution_increaser\satellite_resolution_increaser\outputs\hatsat\results\sentinel2_hatsat_4x.tif"),
313
+ Path(r"D:\downloads\satellite_resolution_increaser\satellite_resolution_increaser\outputs\hatsat\results\sentinel2_hatsat_4x_georef.tif"),
314
+ Path(r"D:\downloads\satellite_resolution_increaser\satellite_resolution_increaser\outputs\esrgan\results\sentinel2_esrgan_4x_georef.tif"),
315
+ ]
316
+
317
+ for tif_path in REAL_TIFS:
318
+ label = tif_path.name
319
+ if not tif_path.exists():
320
+ record(f"{label} -- file exists", False, "not found on disk")
321
+ continue
322
+
323
+ try:
324
+ with rasterio.open(tif_path) as src:
325
+ crs_epsg = src.crs.to_epsg() if src.crs else None
326
+ band_count = src.count
327
+ dtype = src.dtypes[0]
328
+ w, h = src.width, src.height
329
+ t = src.transform
330
+ bounds = src.bounds
331
+
332
+ record(
333
+ f"{label} -- opens without error",
334
+ True,
335
+ f"{w}x{h}, {band_count} bands, {dtype}, EPSG:{crs_epsg}"
336
+ )
337
+ record(
338
+ f"{label} -- has CRS",
339
+ crs_epsg is not None,
340
+ f"EPSG:{crs_epsg}"
341
+ )
342
+ record(
343
+ f"{label} -- has 3 bands (RGB)",
344
+ band_count == 3,
345
+ f"bands={band_count}"
346
+ )
347
+ record(
348
+ f"{label} -- dimensions multiple of 4 (4x SR)",
349
+ w % 4 == 0 and h % 4 == 0,
350
+ f"{w}x{h}"
351
+ )
352
+ # Check pixel size is positive x / negative y (north-up)
353
+ north_up = t.a > 0 and t.e < 0
354
+ record(
355
+ f"{label} -- north-up orientation",
356
+ north_up,
357
+ f"x_res={t.a:.8f}, y_res={t.e:.8f}"
358
+ )
359
+
360
+ except Exception as e:
361
+ record(f"{label} -- opens without error", False, str(e))
362
+
363
+ print()
364
+
365
+ # ================================================================
366
+ # SUMMARY
367
+ # ================================================================
368
+ print("=" * 65)
369
+ print("PHASE 4 SUMMARY")
370
+ print("=" * 65)
371
+
372
+ total = len(results)
373
+ passed = sum(1 for _, p, _ in results if p)
374
+ failed = total - passed
375
+
376
+ for name, p, note in results:
377
+ tag = "+" if p else "x"
378
+ print(f" [{tag}] {name:<50} {note}")
379
+
380
+ print()
381
+ print(f" Result: {passed}/{total} passed", end="")
382
+ if failed:
383
+ print(f" -- {failed} FAILED")
384
+ else:
385
+ print(" -- ALL PASSED")
386
+ print("=" * 65)
387
+ print(f" Output GeoTIFFs: {OUTDIR}")
388
+ print("=" * 65)
389
+
390
+ sys.exit(0 if failed == 0 else 1)
inference/__pycache__/geotiff.cpython-312.pyc ADDED
Binary file (6.62 kB). View file
 
inference/geotiff.py CHANGED
@@ -88,19 +88,55 @@ def normalize_rgb(
88
  ):
89
  """
90
  Convert Sentinel-2 reflectance/DN values to uint8 RGB.
91
- Global percentile normalization preserves color balance.
 
 
92
  """
 
93
  rgb = rgb.astype(np.float32)
94
-
95
- # Calculate global percentiles across all channels to preserve color balance
96
- low = np.percentile(rgb, lower_percentile)
97
- high = np.percentile(rgb, upper_percentile)
98
 
99
- if high <= low:
100
- result = np.clip(rgb, 0, 255).astype(np.uint8)
101
- else:
102
- normalized = (rgb - low) / (high - low)
103
- result = (np.clip(normalized, 0, 1) * 255).astype(np.uint8)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
 
105
  return result
106
 
 
88
  ):
89
  """
90
  Convert Sentinel-2 reflectance/DN values to uint8 RGB.
91
+
92
+ Percentile normalization is performed independently
93
+ for each channel.
94
  """
95
+
96
  rgb = rgb.astype(np.float32)
 
 
 
 
97
 
98
+ result = np.zeros_like(
99
+ rgb,
100
+ dtype=np.uint8
101
+ )
102
+
103
+ for channel in range(3):
104
+
105
+ band = rgb[:, :, channel]
106
+
107
+ low = np.percentile(
108
+ band,
109
+ lower_percentile
110
+ )
111
+
112
+ high = np.percentile(
113
+ band,
114
+ upper_percentile
115
+ )
116
+
117
+ if high <= low:
118
+ result[:, :, channel] = np.clip(
119
+ band,
120
+ 0,
121
+ 255
122
+ ).astype(np.uint8)
123
+
124
+ else:
125
+
126
+ normalized = (
127
+ (band - low) /
128
+ (high - low)
129
+ )
130
+
131
+ normalized = np.clip(
132
+ normalized,
133
+ 0,
134
+ 1
135
+ )
136
+
137
+ result[:, :, channel] = (
138
+ normalized * 255
139
+ ).astype(np.uint8)
140
 
141
  return result
142
 
inference/pipeline.py CHANGED
@@ -1,504 +1,504 @@
1
-
2
- from pathlib import Path
3
- from PIL import Image
4
-
5
- from .tiling import create_tiles
6
- from .stitching import stitch_tiles
7
-
8
- from models.hatsat.hatsat_inference import HATSATInference
9
- from models.esrgan.esrgan_inference import ESRGANInference
10
-
11
-
12
- # ==========================================================
13
- # Project paths
14
- # ==========================================================
15
-
16
- # satellite_sr_deploy/ is the parent of inference/
17
- ROOT = Path(__file__).resolve().parent.parent
18
-
19
- HATSAT_CHECKPOINT = (
20
- ROOT /
21
- "weights/hatsat/net_g_150000.pth"
22
- )
23
-
24
- ESRGAN_CHECKPOINT = (
25
- ROOT /
26
- "weights/esrgan/RRDB_ESRGAN_x4.pth"
27
- )
28
-
29
-
30
- # ==========================================================
31
- # Model cache
32
- # ==========================================================
33
-
34
- _MODEL_CACHE = {}
35
-
36
-
37
- def get_model(model_name, device=None):
38
- """
39
- Load a model once and reuse it.
40
-
41
- Parameters
42
- ----------
43
- model_name : str
44
- 'hatsat' or 'esrgan'
45
-
46
- device : optional
47
- 'cuda' or 'cpu'
48
-
49
- Returns
50
- -------
51
- Loaded inference model.
52
- """
53
-
54
- model_name = model_name.lower().strip()
55
-
56
- if model_name not in ("hatsat", "esrgan"):
57
- raise ValueError(
58
- "model_name must be 'hatsat' or 'esrgan'."
59
- )
60
-
61
- cache_key = (
62
- model_name,
63
- str(device) if device is not None else "auto"
64
- )
65
-
66
- if cache_key in _MODEL_CACHE:
67
- return _MODEL_CACHE[cache_key]
68
-
69
- # ------------------------------------------------------
70
- # HATSAT
71
- # ------------------------------------------------------
72
-
73
- if model_name == "hatsat":
74
-
75
- model = HATSATInference(
76
- checkpoint_path=HATSAT_CHECKPOINT,
77
- device=device
78
- )
79
-
80
- # ------------------------------------------------------
81
- # ESRGAN
82
- # ------------------------------------------------------
83
-
84
- else:
85
-
86
- model = ESRGANInference(
87
- checkpoint_path=ESRGAN_CHECKPOINT,
88
- device=device
89
- )
90
-
91
- _MODEL_CACHE[cache_key] = model
92
-
93
- return model
94
-
95
-
96
- # ==========================================================
97
- # Main pipeline
98
- # ==========================================================
99
-
100
- def super_resolve(
101
- image,
102
- model_name="hatsat",
103
- tile_size=128,
104
- overlap=32,
105
- device=None
106
- ):
107
- """
108
- Complete satellite super-resolution pipeline.
109
-
110
- Pipeline:
111
-
112
- Input image
113
- Γ’β€ β€œ
114
- Tiling
115
- Γ’β€ β€œ
116
- Selected model
117
- Γ’β€ β€œ
118
- 4x SR
119
- Γ’β€ β€œ
120
- Overlap stitching
121
- Γ’β€ β€œ
122
- Final SR image
123
-
124
- Parameters
125
- ----------
126
- image : PIL.Image.Image
127
-
128
- model_name : str
129
- 'hatsat' or 'esrgan'
130
-
131
- tile_size : int
132
- Input tile size.
133
-
134
- overlap : int
135
- Input overlap.
136
-
137
- device : optional
138
- 'cuda' or 'cpu'
139
-
140
- Returns
141
- -------
142
- PIL.Image.Image
143
- Final 4x super-resolved image.
144
- """
145
-
146
- # ------------------------------------------------------
147
- # Validate image
148
- # ------------------------------------------------------
149
-
150
- if not isinstance(image, Image.Image):
151
- raise TypeError(
152
- "image must be a PIL.Image.Image"
153
- )
154
-
155
- image = image.convert("RGB")
156
-
157
- original_size = image.size
158
-
159
- print(
160
- f"Pipeline input: "
161
- f"{original_size[0]} Γƒβ€” {original_size[1]}"
162
- )
163
-
164
- # ------------------------------------------------------
165
- # Get selected model
166
- # ------------------------------------------------------
167
-
168
- model = get_model(
169
- model_name,
170
- device=device
171
- )
172
-
173
- print(
174
- f"Selected model: "
175
- f"{model_name.upper()}"
176
- )
177
-
178
- # ------------------------------------------------------
179
- # Create tiles
180
- # ------------------------------------------------------
181
-
182
- tiles = create_tiles(
183
- image,
184
- tile_size=tile_size,
185
- overlap=overlap
186
- )
187
-
188
- print(
189
- f"Number of tiles: {len(tiles)}"
190
- )
191
-
192
- # ------------------------------------------------------
193
- # Run SR on each tile
194
- # ------------------------------------------------------
195
-
196
- sr_tiles = []
197
-
198
- for tile_info in tiles:
199
-
200
- print(
201
- f"Processing tile "
202
- f"{tile_info['index'] + 1}/"
203
- f"{len(tiles)} "
204
- f"(x={tile_info['x']}, "
205
- f"y={tile_info['y']})"
206
- )
207
-
208
- sr_image = model.predict(
209
- tile_info["image"]
210
- )
211
-
212
- sr_tiles.append(
213
- {
214
- "image": sr_image,
215
- "x": tile_info["x"],
216
- "y": tile_info["y"]
217
- }
218
- )
219
-
220
- # ------------------------------------------------------
221
- # Stitch SR tiles
222
- # ------------------------------------------------------
223
-
224
- result = stitch_tiles(
225
- sr_tiles,
226
- scale=4,
227
- original_size=original_size,
228
- overlap=overlap
229
- )
230
-
231
- print(
232
- f"Pipeline output: "
233
- f"{result.size[0]} Γƒβ€” "
234
- f"{result.size[1]}"
235
- )
236
-
237
- return result
238
-
239
-
240
- # ==========================================================
241
- # Convenience functions
242
- # ==========================================================
243
-
244
- def run_hatsat_pipeline(
245
- image,
246
- tile_size=128,
247
- overlap=32,
248
- device=None
249
- ):
250
- return super_resolve(
251
- image=image,
252
- model_name="hatsat",
253
- tile_size=tile_size,
254
- overlap=overlap,
255
- device=device
256
- )
257
-
258
-
259
- def run_esrgan_pipeline(
260
- image,
261
- tile_size=128,
262
- overlap=32,
263
- device=None
264
- ):
265
- return super_resolve(
266
- image=image,
267
- model_name="esrgan",
268
- tile_size=tile_size,
269
- overlap=overlap,
270
- device=device
271
- )
272
-
273
-
274
- # ==========================================================
275
- # COMPLETE SATELLITE PROCESSING
276
- # ==========================================================
277
-
278
- from .geotiff import (
279
- sentinel_rgb_from_bands,
280
- save_sr_geotiff
281
- )
282
-
283
-
284
- def process_satellite(
285
- b02_path,
286
- b03_path,
287
- b04_path,
288
- model_name="hatsat",
289
- output_dir=None,
290
- tile_size=128,
291
- overlap=32,
292
- device=None
293
- ):
294
- """
295
- Complete Sentinel-2 satellite super-resolution workflow.
296
-
297
- Input:
298
- B02, B03 and B04 Sentinel-2 GeoTIFF files.
299
-
300
- Processing:
301
- B02/B03/B04
302
- Γ’β€ β€œ
303
- RGB conversion
304
- Γ’β€ β€œ
305
- Tiling
306
- Γ’β€ β€œ
307
- HATSAT / ESRGAN
308
- Γ’β€ β€œ
309
- Stitching
310
- Γ’β€ β€œ
311
- 4x SR
312
-
313
- Output:
314
- PNG preview
315
- Georeferenced RGB GeoTIFF
316
-
317
- Returns
318
- -------
319
- dict
320
- {
321
- "model": model_name,
322
- "input_size": (...),
323
- "output_size": (...),
324
- "png": Path,
325
- "geotiff": Path
326
- }
327
- """
328
-
329
- # ------------------------------------------------------
330
- # Paths
331
- # ------------------------------------------------------
332
-
333
- b02_path = Path(b02_path)
334
- b03_path = Path(b03_path)
335
- b04_path = Path(b04_path)
336
-
337
- if output_dir is None:
338
- output_dir = (
339
- ROOT /
340
- "outputs" /
341
- model_name.lower() /
342
- "results"
343
- )
344
-
345
- output_dir = Path(output_dir)
346
- output_dir.mkdir(
347
- parents=True,
348
- exist_ok=True
349
- )
350
-
351
- # ------------------------------------------------------
352
- # Validate inputs
353
- # ------------------------------------------------------
354
-
355
- for path in [
356
- b02_path,
357
- b03_path,
358
- b04_path
359
- ]:
360
-
361
- if not path.exists():
362
- raise FileNotFoundError(
363
- f"Input file not found: {path}"
364
- )
365
-
366
- # ------------------------------------------------------
367
- # Read Sentinel-2 bands
368
- # ------------------------------------------------------
369
-
370
- print("\n========================================")
371
- print("SATELLITE SUPER-RESOLUTION PIPELINE")
372
- print("========================================")
373
-
374
- print("\nReading Sentinel-2 bands...")
375
-
376
- image, source_profile = sentinel_rgb_from_bands(
377
- b02_path,
378
- b03_path,
379
- b04_path
380
- )
381
-
382
- print(
383
- "Input RGB size:",
384
- image.size
385
- )
386
-
387
- print(
388
- "Source CRS:",
389
- source_profile["crs"]
390
- )
391
-
392
- # ------------------------------------------------------
393
- # Run existing SR pipeline
394
- # ------------------------------------------------------
395
-
396
- print(
397
- "\nRunning model:",
398
- model_name.upper()
399
- )
400
-
401
- sr_image = super_resolve(
402
- image=image,
403
- model_name=model_name,
404
- tile_size=tile_size,
405
- overlap=overlap,
406
- device=device
407
- )
408
-
409
- print(
410
- "\nSR image size:",
411
- sr_image.size
412
- )
413
-
414
- # ------------------------------------------------------
415
- # Output filenames
416
- # ------------------------------------------------------
417
-
418
- model_tag = model_name.lower()
419
-
420
- png_path = (
421
- output_dir /
422
- f"sentinel2_{model_tag}_4x.png"
423
- )
424
-
425
- geotiff_path = (
426
- output_dir /
427
- f"sentinel2_{model_tag}_4x_georef.tif"
428
- )
429
-
430
- # ------------------------------------------------------
431
- # Save PNG preview
432
- # ------------------------------------------------------
433
-
434
- sr_image.save(
435
- png_path,
436
- format="PNG"
437
- )
438
-
439
- print(
440
- "\nPNG saved:",
441
- png_path
442
- )
443
-
444
- # ------------------------------------------------------
445
- # Save georeferenced GeoTIFF
446
- # ------------------------------------------------------
447
-
448
- save_sr_geotiff(
449
- sr_image=sr_image,
450
- source_profile=source_profile,
451
- output_path=geotiff_path,
452
- scale=4
453
- )
454
-
455
- print(
456
- "GeoTIFF saved:",
457
- geotiff_path
458
- )
459
-
460
- # ------------------------------------------------------
461
- # Final summary
462
- # ------------------------------------------------------
463
-
464
- print("\n========================================")
465
- print("PROCESSING COMPLETE")
466
- print("========================================")
467
-
468
- print(
469
- "Model:",
470
- model_name.upper()
471
- )
472
-
473
- print(
474
- "Input:",
475
- image.size
476
- )
477
-
478
- print(
479
- "Output:",
480
- sr_image.size
481
- )
482
-
483
- print(
484
- "Scale: 4x"
485
- )
486
-
487
- print(
488
- "PNG:",
489
- png_path
490
- )
491
-
492
- print(
493
- "GeoTIFF:",
494
- geotiff_path
495
- )
496
-
497
- return {
498
- "model": model_name.lower(),
499
- "input_size": image.size,
500
- "output_size": sr_image.size,
501
- "png": png_path,
502
- "geotiff": geotiff_path
503
- }
504
-
 
1
+
2
+ from pathlib import Path
3
+ from PIL import Image
4
+
5
+ from .tiling import create_tiles
6
+ from .stitching import stitch_tiles
7
+
8
+ from models.hatsat.hatsat_inference import HATSATInference
9
+ from models.esrgan.esrgan_inference import ESRGANInference
10
+
11
+
12
+ # ==========================================================
13
+ # Project paths
14
+ # ==========================================================
15
+
16
+ # satellite_sr_deploy/ is the parent of inference/
17
+ ROOT = Path(__file__).resolve().parent.parent
18
+
19
+ HATSAT_CHECKPOINT = (
20
+ ROOT /
21
+ "weights/hatsat/net_g_150000.pth"
22
+ )
23
+
24
+ ESRGAN_CHECKPOINT = (
25
+ ROOT /
26
+ "weights/esrgan/RRDB_ESRGAN_x4.pth"
27
+ )
28
+
29
+
30
+ # ==========================================================
31
+ # Model cache
32
+ # ==========================================================
33
+
34
+ _MODEL_CACHE = {}
35
+
36
+
37
+ def get_model(model_name, device=None):
38
+ """
39
+ Load a model once and reuse it.
40
+
41
+ Parameters
42
+ ----------
43
+ model_name : str
44
+ 'hatsat' or 'esrgan'
45
+
46
+ device : optional
47
+ 'cuda' or 'cpu'
48
+
49
+ Returns
50
+ -------
51
+ Loaded inference model.
52
+ """
53
+
54
+ model_name = model_name.lower().strip()
55
+
56
+ if model_name not in ("hatsat", "esrgan"):
57
+ raise ValueError(
58
+ "model_name must be 'hatsat' or 'esrgan'."
59
+ )
60
+
61
+ cache_key = (
62
+ model_name,
63
+ str(device) if device is not None else "auto"
64
+ )
65
+
66
+ if cache_key in _MODEL_CACHE:
67
+ return _MODEL_CACHE[cache_key]
68
+
69
+ # ------------------------------------------------------
70
+ # HATSAT
71
+ # ------------------------------------------------------
72
+
73
+ if model_name == "hatsat":
74
+
75
+ model = HATSATInference(
76
+ checkpoint_path=HATSAT_CHECKPOINT,
77
+ device=device
78
+ )
79
+
80
+ # ------------------------------------------------------
81
+ # ESRGAN
82
+ # ------------------------------------------------------
83
+
84
+ else:
85
+
86
+ model = ESRGANInference(
87
+ checkpoint_path=ESRGAN_CHECKPOINT,
88
+ device=device
89
+ )
90
+
91
+ _MODEL_CACHE[cache_key] = model
92
+
93
+ return model
94
+
95
+
96
+ # ==========================================================
97
+ # Main pipeline
98
+ # ==========================================================
99
+
100
+ def super_resolve(
101
+ image,
102
+ model_name="hatsat",
103
+ tile_size=128,
104
+ overlap=32,
105
+ device=None
106
+ ):
107
+ """
108
+ Complete satellite super-resolution pipeline.
109
+
110
+ Pipeline:
111
+
112
+ Input image
113
+ Γ’β€ β€œ
114
+ Tiling
115
+ Γ’β€ β€œ
116
+ Selected model
117
+ Γ’β€ β€œ
118
+ 4x SR
119
+ Γ’β€ β€œ
120
+ Overlap stitching
121
+ Γ’β€ β€œ
122
+ Final SR image
123
+
124
+ Parameters
125
+ ----------
126
+ image : PIL.Image.Image
127
+
128
+ model_name : str
129
+ 'hatsat' or 'esrgan'
130
+
131
+ tile_size : int
132
+ Input tile size.
133
+
134
+ overlap : int
135
+ Input overlap.
136
+
137
+ device : optional
138
+ 'cuda' or 'cpu'
139
+
140
+ Returns
141
+ -------
142
+ PIL.Image.Image
143
+ Final 4x super-resolved image.
144
+ """
145
+
146
+ # ------------------------------------------------------
147
+ # Validate image
148
+ # ------------------------------------------------------
149
+
150
+ if not isinstance(image, Image.Image):
151
+ raise TypeError(
152
+ "image must be a PIL.Image.Image"
153
+ )
154
+
155
+ image = image.convert("RGB")
156
+
157
+ original_size = image.size
158
+
159
+ print(
160
+ f"Pipeline input: "
161
+ f"{original_size[0]} Γƒβ€” {original_size[1]}"
162
+ )
163
+
164
+ # ------------------------------------------------------
165
+ # Get selected model
166
+ # ------------------------------------------------------
167
+
168
+ model = get_model(
169
+ model_name,
170
+ device=device
171
+ )
172
+
173
+ print(
174
+ f"Selected model: "
175
+ f"{model_name.upper()}"
176
+ )
177
+
178
+ # ------------------------------------------------------
179
+ # Create tiles
180
+ # ------------------------------------------------------
181
+
182
+ tiles = create_tiles(
183
+ image,
184
+ tile_size=tile_size,
185
+ overlap=overlap
186
+ )
187
+
188
+ print(
189
+ f"Number of tiles: {len(tiles)}"
190
+ )
191
+
192
+ # ------------------------------------------------------
193
+ # Run SR on each tile
194
+ # ------------------------------------------------------
195
+
196
+ sr_tiles = []
197
+
198
+ for tile_info in tiles:
199
+
200
+ print(
201
+ f"Processing tile "
202
+ f"{tile_info['index'] + 1}/"
203
+ f"{len(tiles)} "
204
+ f"(x={tile_info['x']}, "
205
+ f"y={tile_info['y']})"
206
+ )
207
+
208
+ sr_image = model.predict(
209
+ tile_info["image"]
210
+ )
211
+
212
+ sr_tiles.append(
213
+ {
214
+ "image": sr_image,
215
+ "x": tile_info["x"],
216
+ "y": tile_info["y"]
217
+ }
218
+ )
219
+
220
+ # ------------------------------------------------------
221
+ # Stitch SR tiles
222
+ # ------------------------------------------------------
223
+
224
+ result = stitch_tiles(
225
+ sr_tiles,
226
+ scale=4,
227
+ original_size=original_size,
228
+ overlap=overlap
229
+ )
230
+
231
+ print(
232
+ f"Pipeline output: "
233
+ f"{result.size[0]} Γƒβ€” "
234
+ f"{result.size[1]}"
235
+ )
236
+
237
+ return result
238
+
239
+
240
+ # ==========================================================
241
+ # Convenience functions
242
+ # ==========================================================
243
+
244
+ def run_hatsat_pipeline(
245
+ image,
246
+ tile_size=128,
247
+ overlap=32,
248
+ device=None
249
+ ):
250
+ return super_resolve(
251
+ image=image,
252
+ model_name="hatsat",
253
+ tile_size=tile_size,
254
+ overlap=overlap,
255
+ device=device
256
+ )
257
+
258
+
259
+ def run_esrgan_pipeline(
260
+ image,
261
+ tile_size=128,
262
+ overlap=32,
263
+ device=None
264
+ ):
265
+ return super_resolve(
266
+ image=image,
267
+ model_name="esrgan",
268
+ tile_size=tile_size,
269
+ overlap=overlap,
270
+ device=device
271
+ )
272
+
273
+
274
+ # ==========================================================
275
+ # COMPLETE SATELLITE PROCESSING
276
+ # ==========================================================
277
+
278
+ from .geotiff import (
279
+ sentinel_rgb_from_bands,
280
+ save_sr_geotiff
281
+ )
282
+
283
+
284
+ def process_satellite(
285
+ b02_path,
286
+ b03_path,
287
+ b04_path,
288
+ model_name="hatsat",
289
+ output_dir=None,
290
+ tile_size=128,
291
+ overlap=32,
292
+ device=None
293
+ ):
294
+ """
295
+ Complete Sentinel-2 satellite super-resolution workflow.
296
+
297
+ Input:
298
+ B02, B03 and B04 Sentinel-2 GeoTIFF files.
299
+
300
+ Processing:
301
+ B02/B03/B04
302
+ Γ’β€ β€œ
303
+ RGB conversion
304
+ Γ’β€ β€œ
305
+ Tiling
306
+ Γ’β€ β€œ
307
+ HATSAT / ESRGAN
308
+ Γ’β€ β€œ
309
+ Stitching
310
+ Γ’β€ β€œ
311
+ 4x SR
312
+
313
+ Output:
314
+ PNG preview
315
+ Georeferenced RGB GeoTIFF
316
+
317
+ Returns
318
+ -------
319
+ dict
320
+ {
321
+ "model": model_name,
322
+ "input_size": (...),
323
+ "output_size": (...),
324
+ "png": Path,
325
+ "geotiff": Path
326
+ }
327
+ """
328
+
329
+ # ------------------------------------------------------
330
+ # Paths
331
+ # ------------------------------------------------------
332
+
333
+ b02_path = Path(b02_path)
334
+ b03_path = Path(b03_path)
335
+ b04_path = Path(b04_path)
336
+
337
+ if output_dir is None:
338
+ output_dir = (
339
+ ROOT /
340
+ "outputs" /
341
+ model_name.lower() /
342
+ "results"
343
+ )
344
+
345
+ output_dir = Path(output_dir)
346
+ output_dir.mkdir(
347
+ parents=True,
348
+ exist_ok=True
349
+ )
350
+
351
+ # ------------------------------------------------------
352
+ # Validate inputs
353
+ # ------------------------------------------------------
354
+
355
+ for path in [
356
+ b02_path,
357
+ b03_path,
358
+ b04_path
359
+ ]:
360
+
361
+ if not path.exists():
362
+ raise FileNotFoundError(
363
+ f"Input file not found: {path}"
364
+ )
365
+
366
+ # ------------------------------------------------------
367
+ # Read Sentinel-2 bands
368
+ # ------------------------------------------------------
369
+
370
+ print("\n========================================")
371
+ print("SATELLITE SUPER-RESOLUTION PIPELINE")
372
+ print("========================================")
373
+
374
+ print("\nReading Sentinel-2 bands...")
375
+
376
+ image, source_profile = sentinel_rgb_from_bands(
377
+ b02_path,
378
+ b03_path,
379
+ b04_path
380
+ )
381
+
382
+ print(
383
+ "Input RGB size:",
384
+ image.size
385
+ )
386
+
387
+ print(
388
+ "Source CRS:",
389
+ source_profile["crs"]
390
+ )
391
+
392
+ # ------------------------------------------------------
393
+ # Run existing SR pipeline
394
+ # ------------------------------------------------------
395
+
396
+ print(
397
+ "\nRunning model:",
398
+ model_name.upper()
399
+ )
400
+
401
+ sr_image = super_resolve(
402
+ image=image,
403
+ model_name=model_name,
404
+ tile_size=tile_size,
405
+ overlap=overlap,
406
+ device=device
407
+ )
408
+
409
+ print(
410
+ "\nSR image size:",
411
+ sr_image.size
412
+ )
413
+
414
+ # ------------------------------------------------------
415
+ # Output filenames
416
+ # ------------------------------------------------------
417
+
418
+ model_tag = model_name.lower()
419
+
420
+ png_path = (
421
+ output_dir /
422
+ f"sentinel2_{model_tag}_4x.png"
423
+ )
424
+
425
+ geotiff_path = (
426
+ output_dir /
427
+ f"sentinel2_{model_tag}_4x_georef.tif"
428
+ )
429
+
430
+ # ------------------------------------------------------
431
+ # Save PNG preview
432
+ # ------------------------------------------------------
433
+
434
+ sr_image.save(
435
+ png_path,
436
+ format="PNG"
437
+ )
438
+
439
+ print(
440
+ "\nPNG saved:",
441
+ png_path
442
+ )
443
+
444
+ # ------------------------------------------------------
445
+ # Save georeferenced GeoTIFF
446
+ # ------------------------------------------------------
447
+
448
+ save_sr_geotiff(
449
+ sr_image=sr_image,
450
+ source_profile=source_profile,
451
+ output_path=geotiff_path,
452
+ scale=4
453
+ )
454
+
455
+ print(
456
+ "GeoTIFF saved:",
457
+ geotiff_path
458
+ )
459
+
460
+ # ------------------------------------------------------
461
+ # Final summary
462
+ # ------------------------------------------------------
463
+
464
+ print("\n========================================")
465
+ print("PROCESSING COMPLETE")
466
+ print("========================================")
467
+
468
+ print(
469
+ "Model:",
470
+ model_name.upper()
471
+ )
472
+
473
+ print(
474
+ "Input:",
475
+ image.size
476
+ )
477
+
478
+ print(
479
+ "Output:",
480
+ sr_image.size
481
+ )
482
+
483
+ print(
484
+ "Scale: 4x"
485
+ )
486
+
487
+ print(
488
+ "PNG:",
489
+ png_path
490
+ )
491
+
492
+ print(
493
+ "GeoTIFF:",
494
+ geotiff_path
495
+ )
496
+
497
+ return {
498
+ "model": model_name.lower(),
499
+ "input_size": image.size,
500
+ "output_size": sr_image.size,
501
+ "png": png_path,
502
+ "geotiff": geotiff_path
503
+ }
504
+
local_test.py ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Local test script for satellite_sr_deploy.
3
+ Tests:
4
+ 1. Import checks for all modules
5
+ 2. HATSAT inference on a synthetic 198x139 image
6
+ 3. ESRGAN inference on a synthetic 198x139 image
7
+ 4. Router switch (HATSAT β†’ ESRGAN)
8
+ """
9
+
10
+ import sys
11
+ import time
12
+ from pathlib import Path
13
+
14
+ # -------------------------------------------------------
15
+ # Make sure we run from the deploy root
16
+ # -------------------------------------------------------
17
+ ROOT = Path(__file__).resolve().parent
18
+ if str(ROOT) not in sys.path:
19
+ sys.path.insert(0, str(ROOT))
20
+
21
+ print("=" * 60)
22
+ print("STEP 1 β€” IMPORT CHECK")
23
+ print("=" * 60)
24
+
25
+ try:
26
+ import torch
27
+ print(f" torch : {torch.__version__}")
28
+ except ImportError as e:
29
+ print(f" [FAIL] torch : {e}")
30
+ sys.exit(1)
31
+
32
+ try:
33
+ import numpy as np
34
+ print(f" numpy : {np.__version__}")
35
+ except ImportError as e:
36
+ print(f" [FAIL] numpy : {e}")
37
+ sys.exit(1)
38
+
39
+ try:
40
+ from PIL import Image
41
+ import PIL
42
+ print(f" Pillow : {PIL.__version__}")
43
+ except ImportError as e:
44
+ print(f" [FAIL] Pillow : {e}")
45
+ sys.exit(1)
46
+
47
+ try:
48
+ import einops
49
+ print(f" einops : {einops.__version__}")
50
+ except ImportError as e:
51
+ print(f" [FAIL] einops : {e}")
52
+ sys.exit(1)
53
+
54
+ try:
55
+ import gradio as gr
56
+ print(f" gradio : {gr.__version__}")
57
+ except ImportError as e:
58
+ print(f" [FAIL] gradio : {e}")
59
+ sys.exit(1)
60
+
61
+ try:
62
+ import rasterio
63
+ print(f" rasterio : {rasterio.__version__}")
64
+ except ImportError as e:
65
+ print(f" [FAIL] rasterio : {e}")
66
+ sys.exit(1)
67
+
68
+ print("\nAll imports OK.\n")
69
+
70
+ # -------------------------------------------------------
71
+ # Check weight files
72
+ # -------------------------------------------------------
73
+ print("=" * 60)
74
+ print("STEP 2 β€” WEIGHT FILE CHECK")
75
+ print("=" * 60)
76
+
77
+ hatsat_ckpt = ROOT / "weights" / "hatsat" / "net_g_150000.pth"
78
+ esrgan_ckpt = ROOT / "weights" / "esrgan" / "RRDB_ESRGAN_x4.pth"
79
+
80
+ if hatsat_ckpt.exists():
81
+ size_mb = hatsat_ckpt.stat().st_size / (1024 * 1024)
82
+ print(f" HATSAT : {hatsat_ckpt} ({size_mb:.1f} MB) OK")
83
+ else:
84
+ print(f" [FAIL] HATSAT checkpoint not found: {hatsat_ckpt}")
85
+ sys.exit(1)
86
+
87
+ if esrgan_ckpt.exists():
88
+ size_mb = esrgan_ckpt.stat().st_size / (1024 * 1024)
89
+ print(f" ESRGAN : {esrgan_ckpt} ({size_mb:.1f} MB) OK")
90
+ else:
91
+ print(f" [FAIL] ESRGAN checkpoint not found: {esrgan_ckpt}")
92
+ sys.exit(1)
93
+
94
+ print()
95
+
96
+ # -------------------------------------------------------
97
+ # Model router import
98
+ # -------------------------------------------------------
99
+ print("=" * 60)
100
+ print("STEP 3 β€” MODEL ROUTER IMPORT")
101
+ print("=" * 60)
102
+
103
+ try:
104
+ from model_router import SatelliteSRRouter
105
+ print(" SatelliteSRRouter imported OK")
106
+ except Exception as e:
107
+ print(f" [FAIL] model_router import: {e}")
108
+ sys.exit(1)
109
+
110
+ print()
111
+
112
+ # -------------------------------------------------------
113
+ # Create a synthetic test image (198 x 139, RGB)
114
+ # -------------------------------------------------------
115
+ print("=" * 60)
116
+ print("STEP 4 β€” CREATE SYNTHETIC TEST IMAGE (198x139 RGB)")
117
+ print("=" * 60)
118
+
119
+ import numpy as np
120
+ test_arr = np.random.randint(0, 255, (139, 198, 3), dtype=np.uint8)
121
+ test_image = Image.fromarray(test_arr, mode="RGB")
122
+ print(f" Input size: {test_image.size} (W x H)")
123
+ print()
124
+
125
+ # -------------------------------------------------------
126
+ # Instantiate router (lazy β€” no model loaded yet)
127
+ # -------------------------------------------------------
128
+ print("=" * 60)
129
+ print("STEP 5 β€” INIT ROUTER")
130
+ print("=" * 60)
131
+
132
+ device = "cuda" if torch.cuda.is_available() else "cpu"
133
+ print(f" Device: {device}")
134
+
135
+ router = SatelliteSRRouter(device=device)
136
+ print()
137
+
138
+ # -------------------------------------------------------
139
+ # HATSAT inference
140
+ # -------------------------------------------------------
141
+ print("=" * 60)
142
+ print("STEP 6 β€” HATSAT INFERENCE")
143
+ print("=" * 60)
144
+
145
+ t0 = time.time()
146
+ try:
147
+ hatsat_output = router.predict(test_image, model_name="hatsat")
148
+ elapsed = time.time() - t0
149
+ print(f" Input : {test_image.size}")
150
+ print(f" Output : {hatsat_output.size}")
151
+ expected = (test_image.width * 4, test_image.height * 4)
152
+ if hatsat_output.size == expected:
153
+ print(f" Scale check: OK (expected {expected})")
154
+ else:
155
+ print(f" [WARN] Scale mismatch: expected {expected}, got {hatsat_output.size}")
156
+ print(f" Time : {elapsed:.2f}s")
157
+ except Exception as e:
158
+ print(f" [FAIL] HATSAT inference: {e}")
159
+ import traceback; traceback.print_exc()
160
+ sys.exit(1)
161
+
162
+ print()
163
+
164
+ # -------------------------------------------------------
165
+ # ESRGAN inference (router should unload HATSAT first)
166
+ # -------------------------------------------------------
167
+ print("=" * 60)
168
+ print("STEP 7 β€” ESRGAN INFERENCE (router switches model)")
169
+ print("=" * 60)
170
+
171
+ t0 = time.time()
172
+ try:
173
+ esrgan_output = router.predict(test_image, model_name="esrgan")
174
+ elapsed = time.time() - t0
175
+ print(f" Input : {test_image.size}")
176
+ print(f" Output : {esrgan_output.size}")
177
+ expected = (test_image.width * 4, test_image.height * 4)
178
+ if esrgan_output.size == expected:
179
+ print(f" Scale check: OK (expected {expected})")
180
+ else:
181
+ print(f" [WARN] Scale mismatch: expected {expected}, got {esrgan_output.size}")
182
+ print(f" Time : {elapsed:.2f}s")
183
+ except Exception as e:
184
+ print(f" [FAIL] ESRGAN inference: {e}")
185
+ import traceback; traceback.print_exc()
186
+ sys.exit(1)
187
+
188
+ print()
189
+
190
+ # -------------------------------------------------------
191
+ # Save outputs
192
+ # -------------------------------------------------------
193
+ print("=" * 60)
194
+ print("STEP 8 β€” SAVE TEST OUTPUTS")
195
+ print("=" * 60)
196
+
197
+ out_dir = ROOT / "outputs" / "local_test"
198
+ out_dir.mkdir(parents=True, exist_ok=True)
199
+
200
+ hatsat_path = out_dir / "test_hatsat_output.png"
201
+ esrgan_path = out_dir / "test_esrgan_output.png"
202
+
203
+ hatsat_output.save(hatsat_path)
204
+ esrgan_output.save(esrgan_path)
205
+
206
+ print(f" HATSAT output saved : {hatsat_path}")
207
+ print(f" ESRGAN output saved : {esrgan_path}")
208
+ print()
209
+
210
+ # -------------------------------------------------------
211
+ # Summary
212
+ # -------------------------------------------------------
213
+ print("=" * 60)
214
+ print("LOCAL TEST COMPLETE")
215
+ print("=" * 60)
216
+ print(" All tests passed.")
217
+ print(f" HATSAT : {test_image.size} -> {hatsat_output.size}")
218
+ print(f" ESRGAN : {test_image.size} -> {esrgan_output.size}")
219
+ print("=" * 60)
model_router.py CHANGED
@@ -10,9 +10,11 @@ class SatelliteSRRouter:
10
 
11
  from models.hatsat.hatsat_inference import HATSATInference
12
  from models.esrgan.esrgan_inference import ESRGANInference
 
13
 
14
  self.HATSATInference = HATSATInference
15
  self.ESRGANInference = ESRGANInference
 
16
 
17
  if device is None:
18
  device = "cuda" if torch.cuda.is_available() else "cpu"
@@ -23,6 +25,7 @@ class SatelliteSRRouter:
23
  # Only the selected model is loaded.
24
  self.hatsat = None
25
  self.esrgan = None
 
26
 
27
  print("=" * 55)
28
  print("SATELLITE SR ROUTER")
@@ -71,6 +74,22 @@ class SatelliteSRRouter:
71
 
72
  self._clear_gpu()
73
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  def _load_hatsat(self):
75
 
76
  if self.hatsat is None:
@@ -103,6 +122,18 @@ class SatelliteSRRouter:
103
 
104
  print("ESRGAN loaded successfully.")
105
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  def available_models(self):
107
 
108
  return {
@@ -116,6 +147,12 @@ class SatelliteSRRouter:
116
  "name": "ESRGAN",
117
  "description": "General-purpose super-resolution baseline",
118
  "scale": 4
 
 
 
 
 
 
119
  }
120
  }
121
 
@@ -137,8 +174,9 @@ class SatelliteSRRouter:
137
 
138
  if model_name == "hatsat":
139
 
140
- # Free GPU memory used by ESRGAN
141
  self._unload_esrgan()
 
142
 
143
  # Load HATSAT only when required
144
  self._load_hatsat()
@@ -157,8 +195,9 @@ class SatelliteSRRouter:
157
 
158
  elif model_name == "esrgan":
159
 
160
- # Free GPU memory used by HATSAT
161
  self._unload_hatsat()
 
162
 
163
  # Load ESRGAN only when required
164
  self._load_esrgan()
@@ -171,9 +210,30 @@ class SatelliteSRRouter:
171
 
172
  return result
173
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
  else:
175
 
176
  raise ValueError(
177
  f"Unknown model '{model_name}'. "
178
- f"Choose 'hatsat' or 'esrgan'."
179
  )
 
10
 
11
  from models.hatsat.hatsat_inference import HATSATInference
12
  from models.esrgan.esrgan_inference import ESRGANInference
13
+ from models.sen2sr.sen2sr_inference import Sen2SRInference
14
 
15
  self.HATSATInference = HATSATInference
16
  self.ESRGANInference = ESRGANInference
17
+ self.Sen2SRInference = Sen2SRInference
18
 
19
  if device is None:
20
  device = "cuda" if torch.cuda.is_available() else "cpu"
 
25
  # Only the selected model is loaded.
26
  self.hatsat = None
27
  self.esrgan = None
28
+ self.sen2sr = None
29
 
30
  print("=" * 55)
31
  print("SATELLITE SR ROUTER")
 
74
 
75
  self._clear_gpu()
76
 
77
+ def _unload_sen2sr(self):
78
+
79
+ if self.sen2sr is not None:
80
+
81
+ print("Unloading Sen2SR...")
82
+
83
+ try:
84
+ if hasattr(self.sen2sr, "model") and hasattr(self.sen2sr.model, "model"):
85
+ self.sen2sr.model.model.cpu()
86
+ except Exception:
87
+ pass
88
+
89
+ self.sen2sr = None
90
+
91
+ self._clear_gpu()
92
+
93
  def _load_hatsat(self):
94
 
95
  if self.hatsat is None:
 
122
 
123
  print("ESRGAN loaded successfully.")
124
 
125
+ def _load_sen2sr(self):
126
+
127
+ if self.sen2sr is None:
128
+
129
+ print("Loading Sen2SR...")
130
+
131
+ self.sen2sr = self.Sen2SRInference(
132
+ device=self.device
133
+ )
134
+
135
+ print("Sen2SR loaded successfully.")
136
+
137
  def available_models(self):
138
 
139
  return {
 
147
  "name": "ESRGAN",
148
  "description": "General-purpose super-resolution baseline",
149
  "scale": 4
150
+ },
151
+
152
+ "sen2sr": {
153
+ "name": "Sen2SR",
154
+ "description": "WEO-SAS Sentinel-2 CNN super-resolution (HuggingFace)",
155
+ "scale": 4
156
  }
157
  }
158
 
 
174
 
175
  if model_name == "hatsat":
176
 
177
+ # Free GPU memory used by other models
178
  self._unload_esrgan()
179
+ self._unload_sen2sr()
180
 
181
  # Load HATSAT only when required
182
  self._load_hatsat()
 
195
 
196
  elif model_name == "esrgan":
197
 
198
+ # Free GPU memory used by HATSAT and Sen2SR
199
  self._unload_hatsat()
200
+ self._unload_sen2sr()
201
 
202
  # Load ESRGAN only when required
203
  self._load_esrgan()
 
210
 
211
  return result
212
 
213
+ # ==========================================
214
+ # SEN2SR
215
+ # ==========================================
216
+
217
+ elif model_name == "sen2sr":
218
+
219
+ # Free GPU memory used by other models
220
+ self._unload_hatsat()
221
+ self._unload_esrgan()
222
+
223
+ # Load Sen2SR only when required
224
+ self._load_sen2sr()
225
+
226
+ print("Running Sen2SR...")
227
+
228
+ result = self.sen2sr.predict(image)
229
+
230
+ print("Sen2SR inference complete.")
231
+
232
+ return result
233
+
234
  else:
235
 
236
  raise ValueError(
237
  f"Unknown model '{model_name}'. "
238
+ f"Choose 'hatsat', 'esrgan', or 'sen2sr'."
239
  )
models/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (210 Bytes). View file
 
models/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (189 Bytes). View file
 
models/esrgan/__pycache__/RRDBNet_arch.cpython-312.pyc ADDED
Binary file (5.8 kB). View file
 
models/esrgan/__pycache__/RRDBNet_arch.cpython-313.pyc ADDED
Binary file (5.84 kB). View file
 
models/esrgan/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (217 Bytes). View file
 
models/esrgan/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (196 Bytes). View file
 
models/esrgan/__pycache__/esrgan_inference.cpython-312.pyc ADDED
Binary file (4.81 kB). View file
 
models/esrgan/__pycache__/esrgan_inference.cpython-313.pyc ADDED
Binary file (4.79 kB). View file
 
models/hatsat/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (217 Bytes). View file
 
models/hatsat/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (196 Bytes). View file
 
models/hatsat/__pycache__/components.cpython-312.pyc ADDED
Binary file (26.9 kB). View file
 
models/hatsat/__pycache__/components.cpython-313.pyc ADDED
Binary file (27.1 kB). View file
 
models/hatsat/__pycache__/hat_model.cpython-312.pyc ADDED
Binary file (12.8 kB). View file
 
models/hatsat/__pycache__/hat_model.cpython-313.pyc ADDED
Binary file (13 kB). View file
 
models/hatsat/__pycache__/hatsat_inference.cpython-312.pyc ADDED
Binary file (4.94 kB). View file
 
models/hatsat/__pycache__/hatsat_inference.cpython-313.pyc ADDED
Binary file (4.89 kB). View file
 
models/sen2sr/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Sen2SR model package
models/sen2sr/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (217 Bytes). View file
 
models/sen2sr/__pycache__/sen2sr_inference.cpython-312.pyc ADDED
Binary file (5.68 kB). View file
 
models/sen2sr/sen2sr_inference.py ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import sys
3
+ import os
4
+ import numpy as np
5
+ import torch
6
+ from pathlib import Path
7
+ from PIL import Image
8
+
9
+
10
+ class Sen2SRInference:
11
+ """
12
+ Inference wrapper for WEO-SAS/sen2sr β€” a Sentinel-2 super-resolution model
13
+ hosted on Hugging Face (https://huggingface.co/WEO-SAS/sen2sr).
14
+
15
+ The model's `main` branch expects 4-channel RGBN input (B04, B03, B02, B08)
16
+ as a float32 NumPy array in [0, 1] with shape (C, H, W) and returns a
17
+ 4Γ— upscaled output of the same channel order.
18
+
19
+ This wrapper accepts standard 3-channel RGB PIL images and adapts them
20
+ by treating the R channel as a proxy for NIR (B08), since single-image
21
+ workflows won't generally have a separate NIR band.
22
+ """
23
+
24
+ MODEL_REPO = "WEO-SAS/sen2sr"
25
+
26
+ def __init__(self, device=None, local_dir=None):
27
+
28
+ if device is None:
29
+ device = "cuda" if torch.cuda.is_available() else "cpu"
30
+
31
+ self.device = str(device)
32
+ self.local_dir = local_dir
33
+ self.model = None
34
+
35
+ self._load_model()
36
+
37
+ # ------------------------------------------------------------------
38
+ # Model loading
39
+ # ------------------------------------------------------------------
40
+
41
+ def _load_model(self):
42
+ """Download and load the sen2sr model from Hugging Face Hub."""
43
+
44
+ try:
45
+ from huggingface_hub import snapshot_download
46
+ except ImportError:
47
+ raise ImportError(
48
+ "huggingface_hub is required for Sen2SR. "
49
+ "Install it with: pip install huggingface-hub"
50
+ )
51
+
52
+ try:
53
+ from safetensors import safe_open # noqa: F401 β€” ensure available
54
+ except ImportError:
55
+ raise ImportError(
56
+ "safetensors is required for Sen2SR. "
57
+ "Install it with: pip install safetensors"
58
+ )
59
+
60
+ print("Sen2SR: downloading / verifying model from Hugging Face Hub …")
61
+
62
+ if self.local_dir is None:
63
+ self.local_dir = snapshot_download(
64
+ repo_id=self.MODEL_REPO,
65
+ revision="main",
66
+ )
67
+
68
+ print(f"Sen2SR: model files at {self.local_dir}")
69
+
70
+ # The model repo exposes a `model.py` with a `Model` class.
71
+ if str(self.local_dir) not in sys.path:
72
+ sys.path.insert(0, str(self.local_dir))
73
+
74
+ from model import Model # type: ignore # loaded dynamically
75
+
76
+ self.model = Model(local_dir=str(self.local_dir))
77
+
78
+ print("Sen2SR: model loaded successfully.")
79
+
80
+ # ------------------------------------------------------------------
81
+ # Inference
82
+ # ------------------------------------------------------------------
83
+
84
+ @torch.no_grad()
85
+ def predict(self, image) -> Image.Image:
86
+ """
87
+ Run 4Γ— super-resolution on a PIL RGB image.
88
+
89
+ Parameters
90
+ ----------
91
+ image : PIL.Image.Image | np.ndarray
92
+ Input satellite image (RGB, any uint8 size).
93
+
94
+ Returns
95
+ -------
96
+ PIL.Image.Image
97
+ 4Γ— upscaled RGB image.
98
+ """
99
+
100
+ if self.model is None:
101
+ raise RuntimeError("Sen2SR model is not loaded.")
102
+
103
+ # ── Normalise input ────────────────────────────────────────────
104
+ if not isinstance(image, Image.Image):
105
+ image = Image.fromarray(image)
106
+
107
+ image = image.convert("RGB")
108
+
109
+ img_np = np.array(image).astype(np.float32) / 255.0 # (H, W, 3)
110
+
111
+ # ── Build RGBN array: (4, H, W) ───────────────────────────────
112
+ # Model expects: B04(R), B03(G), B02(B), B08(NIR).
113
+ # Approximate NIR with the Red channel as the best available proxy.
114
+ r = img_np[:, :, 0]
115
+ g = img_np[:, :, 1]
116
+ b = img_np[:, :, 2]
117
+ nir_proxy = r
118
+
119
+ rgbn = np.stack([r, g, b, nir_proxy], axis=0) # (4, H, W)
120
+
121
+ # ── Pad input to a multiple of patch_size=128 ─────────────────
122
+ # The HardConstraint FFT low-pass mask is baked at patch_size=128
123
+ # (output size 512). Both the direct path (image ≀ 128) and the
124
+ # tiled predict_large path need H and W to be exact multiples of
125
+ # 128 so every tile lands on a 128Γ—128 boundary.
126
+ PATCH = 128
127
+ _, orig_h, orig_w = rgbn.shape
128
+
129
+ pad_h = (PATCH - orig_h % PATCH) % PATCH
130
+ pad_w = (PATCH - orig_w % PATCH) % PATCH
131
+
132
+ if pad_h > 0 or pad_w > 0:
133
+ rgbn = np.pad(
134
+ rgbn,
135
+ ((0, 0), (0, pad_h), (0, pad_w)),
136
+ mode="reflect",
137
+ )
138
+
139
+ # ── Run model ─────────────────────────────────────────────────
140
+ sr_rgbn = self.model.predict(rgbn) # (4, H_padded*4, W_padded*4)
141
+
142
+ # ── Crop back to the true 4Γ— output size ──────────────────────
143
+ out_h = orig_h * 4
144
+ out_w = orig_w * 4
145
+ sr_rgbn = sr_rgbn[:, :out_h, :out_w]
146
+
147
+ # ── Convert to uint8 RGB PIL image ────────────────────────────
148
+ sr_rgb = sr_rgbn[:3, :, :] # (3, H*4, W*4)
149
+ sr_rgb = np.clip(sr_rgb, 0.0, 1.0)
150
+ sr_rgb = (sr_rgb * 255.0).round().astype(np.uint8)
151
+ sr_rgb = sr_rgb.transpose(1, 2, 0) # (H*4, W*4, 3)
152
+
153
+ return Image.fromarray(sr_rgb, mode="RGB")
154
+
155
+
156
+ # -----------------------------------------------------------------------
157
+ # Convenience function
158
+ # -----------------------------------------------------------------------
159
+
160
+ def run_sen2sr(image, device=None, local_dir=None):
161
+ model = Sen2SRInference(device=device, local_dir=local_dir)
162
+ return model.predict(image)
outputs/geotiff_test/numpy_input_sr.tif ADDED
outputs/geotiff_test/synthetic_sr_4x.tif ADDED
outputs/local_test/test_esrgan_output.png ADDED

Git LFS Details

  • SHA256: a7e6ebe38f5a5bd8910704d3c2cddbf95a481de8f3c6fe02819de8e464f1e901
  • Pointer size: 132 Bytes
  • Size of remote file: 1.08 MB
outputs/local_test/test_hatsat_output.png ADDED

Git LFS Details

  • SHA256: d65d0c29252160dbbcc348657152b190184f6f2979f23aa53b5dd0bef17aca39
  • Pointer size: 131 Bytes
  • Size of remote file: 807 kB
outputs/robustness_test/esrgan_100x80.png ADDED

Git LFS Details

  • SHA256: 046729ba2ad978629a4affe39e0e30471bf5e03e3b838cb42b6fcdd972d6a604
  • Pointer size: 131 Bytes
  • Size of remote file: 314 kB
outputs/robustness_test/esrgan_17x17.png ADDED
outputs/robustness_test/esrgan_198x139.png ADDED

Git LFS Details

  • SHA256: 5bfe1a443e5e3a3b5a10845e0aff676bb3df0272e9c7601a44738503e9e0205f
  • Pointer size: 132 Bytes
  • Size of remote file: 1.08 MB
outputs/robustness_test/esrgan_256x256.png ADDED

Git LFS Details

  • SHA256: d22e4f65c58fddf963224397ea78025f4b1c82581298621ef2d21fa04ef42d55
  • Pointer size: 132 Bytes
  • Size of remote file: 2.57 MB
outputs/robustness_test/esrgan_64x64.png ADDED

Git LFS Details

  • SHA256: 4b390d5dbda123adde562a74089c1556b22ec0267a40ba0b905acb2a57599457
  • Pointer size: 131 Bytes
  • Size of remote file: 162 kB
outputs/robustness_test/hatsat_100x80.png ADDED

Git LFS Details

  • SHA256: 3a315f2cc1c20dbae34fa6a097d2760ab4c9bb05a1365ccdbd63b7e64312c8cc
  • Pointer size: 131 Bytes
  • Size of remote file: 235 kB
outputs/robustness_test/hatsat_128x128_mod16.png ADDED

Git LFS Details

  • SHA256: b85de116d5bce8141b3d3546db08723d6ed42ce94108f6f1ef67568bce252e5b
  • Pointer size: 131 Bytes
  • Size of remote file: 481 kB
outputs/robustness_test/hatsat_129x129.png ADDED

Git LFS Details

  • SHA256: 3fe1bb0774550484947ffda5b46ac7dbcb211957bb29e58159d2efc40854f056
  • Pointer size: 131 Bytes
  • Size of remote file: 488 kB
outputs/robustness_test/hatsat_16x16_mod16.png ADDED
outputs/robustness_test/hatsat_17x17.png ADDED
outputs/robustness_test/hatsat_198x139.png ADDED

Git LFS Details

  • SHA256: e0fe9c366049e261e4d9e848daa5c16ad34e8d064012a7899d6f27e5861b6046
  • Pointer size: 131 Bytes
  • Size of remote file: 805 kB
outputs/robustness_test/hatsat_255x255.png ADDED

Git LFS Details

  • SHA256: 21f38f13a9024bbd2d6abd35533a0961d0a9f61a882652d6c3eb461fa0b1b71c
  • Pointer size: 132 Bytes
  • Size of remote file: 1.9 MB
outputs/robustness_test/hatsat_256x256_mod16.png ADDED

Git LFS Details

  • SHA256: 71df3e77b1a656f164a48352315040c99b7bb7065fb82c38446999744c124ce4
  • Pointer size: 132 Bytes
  • Size of remote file: 1.92 MB