lorenzoinnocenti commited on
Commit
d0bb692
·
1 Parent(s): 9a6abc8

first working version

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ 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
+ *.png filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # Distribution / packaging
7
+ .Python
8
+ build/
9
+ develop-eggs/
10
+ dist/
11
+ downloads/
12
+ eggs/
13
+ .eggs/
14
+ lib/
15
+ lib64/
16
+ parts/
17
+ sdist/
18
+ var/
19
+ wheels/
20
+ share/python-wheels/
21
+ *.egg-info/
22
+ .installed.cfg
23
+ *.egg
24
+ MANIFEST
25
+
26
+ # PyInstaller
27
+ # Usually these files are written by a python script from a template
28
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
29
+ *.manifest
30
+ *.spec
31
+
32
+ # Installer logs
33
+ pip-log.txt
34
+ pip-delete-this-directory.txt
35
+
36
+ # Unit test / coverage reports
37
+ htmlcov/
38
+ .tox/
39
+ .nox/
40
+ .coverage
41
+ .coverage.*
42
+ .cache
43
+ nosetests.xml
44
+ coverage.xml
45
+ *.cover
46
+ *.py,cover
47
+ .hypothesis/
48
+ .pytest_cache/
49
+ cover/
50
+
51
+ # pyenv
52
+ .python-version
53
+
54
+ # Environments
55
+ .env
56
+ .venv
57
+ env/
58
+ venv/
59
+ ENV/
60
+ env.bak/
61
+ venv.bak/
62
+
63
+ # vscode
64
+ .vscode/
65
+
66
+ # tmp folder
67
+ .tmp
README.md CHANGED
@@ -1,14 +1,14 @@
1
  ---
2
- title: Sar Oilspill Detection
3
- emoji: 🐢
4
- colorFrom: purple
5
- colorTo: indigo
6
  sdk: gradio
7
- sdk_version: 5.22.0
8
  app_file: app.py
9
  pinned: false
10
  license: mit
11
- short_description: Sar Oil Spill Detection
12
  ---
13
 
14
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: SAR Oil Spill Detection
3
+ emoji: 📡
4
+ colorFrom: blue
5
+ colorTo: blue
6
  sdk: gradio
7
+ sdk_version: 5.21.0
8
  app_file: app.py
9
  pinned: false
10
  license: mit
11
+ short_description: Marine oil spill detection using Synthetic Aperture Radar (SAR) satellite images.
12
  ---
13
 
14
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
app.py ADDED
@@ -0,0 +1,365 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import numpy as np
3
+ import os
4
+ from PIL import Image
5
+ from math import ceil, floor
6
+ from numpy import ndarray
7
+ from typing import Callable, List
8
+ import scipy.signal
9
+ import onnxruntime as ort
10
+ from tqdm import tqdm
11
+
12
+ # needed to run locally
13
+ os.environ["GRADIO_TEMP_DIR"] = ".tmp"
14
+
15
+ WINDOW_CACHE = dict()
16
+
17
+ def _spline_window(window_size: int, power: int = 2) -> np.ndarray:
18
+ """Generates a 1-dimensional spline of order 'power' (typically 2), in the designated
19
+ window.
20
+ Args:
21
+ window_size (int): size of the interested window
22
+ power (int, optional): Order of the spline. Defaults to 2.
23
+ Returns:
24
+ np.ndarray: 1D spline
25
+ """
26
+ intersection = int(window_size / 4)
27
+ wind_outer = (
28
+ abs(2 * (scipy.signal.windows.triang(window_size))) ** power) / 2
29
+ wind_outer[intersection:-intersection] = 0
30
+ wind_inner = (
31
+ 1 - (abs(2 * (scipy.signal.windows.triang(window_size) - 1)) ** power) / 2
32
+ )
33
+ wind_inner[:intersection] = 0
34
+ wind_inner[-intersection:] = 0
35
+ wind = wind_inner + wind_outer
36
+ wind = wind / np.average(wind)
37
+ return wind
38
+
39
+
40
+ def _spline_2d(window_size: int, power: int = 2) -> ndarray:
41
+ """Makes a 1D window spline function, then combines it to return a 2D window function.
42
+ The 2D window is useful to smoothly interpolate between patches.
43
+ Args:
44
+ window_size (int): size of the window (patch)
45
+ power (int, optional): Which order for the spline. Defaults to 2.
46
+ Returns:
47
+ np.ndarray: numpy array containing a 2D spline function
48
+ """
49
+ # Memorization to avoid remaking it for every call
50
+ # since the same window is needed multiple times
51
+ wind = _spline_window(window_size, power)
52
+ # make it 2d
53
+ wind2 = wind[:, None] * wind[None, :]
54
+ wind2 = wind2 / np.max(wind2)
55
+ return wind2
56
+
57
+
58
+ def _spline_4d(
59
+ window_size: int,
60
+ power: int = 2,
61
+ batch_size: int = 1,
62
+ channels: int = 1
63
+ ) -> ndarray:
64
+ """Makes a 4D window spline function
65
+ Same as the 2D version, but repeated across all channels and batch"""
66
+ global WINDOW_CACHE
67
+ key = f"{window_size}_{power}"
68
+ if key in WINDOW_CACHE:
69
+ wind4 = WINDOW_CACHE[key]
70
+ else:
71
+ wind2 = _spline_2d(window_size, power)
72
+ wind4 = wind2[None, None, :, :] * np.ones((batch_size, channels, 1, 1))
73
+ WINDOW_CACHE[key] = wind2
74
+ return wind4
75
+
76
+
77
+ def pad_image(image: np.array, tile_size: int, subdivisions: int) -> np.array:
78
+ """Add borders to the given image for a "valid" border pattern according to "window_size" and "subdivisions".
79
+ Image is expected as a numpy array with shape (width, height, channels).
80
+ Args:
81
+ image (torch.Tensor): input image, 3D channels-last tensor
82
+ tile_size (int): size of a single patch, useful to compute padding
83
+ subdivisions (int): amount of overlap, useful for padding
84
+ Returns:
85
+ torch.Tensor: same image, padded specularly by a certain amount in every direction
86
+ """
87
+ step = tile_size // subdivisions
88
+ _, in_h, in_w = image.shape
89
+ pad_h = step - (in_h % step)
90
+ pad_w = step - (in_w % step)
91
+ pad_h_l = pad_h // 2
92
+ pad_h_r = (pad_h // 2) + (pad_h % 2)
93
+ pad_w_l = pad_w // 2
94
+ pad_w_r = (pad_w // 2) + (pad_w % 2)
95
+ pad = int(round(tile_size * (1 - 1.0 / subdivisions)))
96
+ image = np.pad(
97
+ image,
98
+ ((0, 0), (pad + pad_h_l, pad + pad_h_r), (pad + pad_w_l, pad + pad_w_r)),
99
+ mode="reflect",
100
+ )
101
+ return image, [pad + pad_h_l, pad + pad_h_r, pad + pad_w_l, pad + pad_w_r]
102
+
103
+
104
+ def unpad_image(padded_image: ndarray, pads) -> ndarray:
105
+ """Reverts changes made by 'pad_image'. The same padding is removed, so tile_size and subdivisions
106
+ must be coherent.
107
+
108
+ Args:
109
+ padded_image (torch.Tensor): image with padding still applied
110
+ tile_size (int): size of a single patch
111
+ subdivisions (int): subdivisions to compute overlap
112
+
113
+ Returns:
114
+ torch.Tensor: image without padding, 2D channels-last tensor
115
+ """
116
+ pad_left, pad_right, pad_top, pad_bottom = pads
117
+ # crop the image left, right, top and bottom
118
+ # get number of dimensions of padded_image
119
+ n_dims = len(padded_image.shape)
120
+ # if padded_image is 2d
121
+ if n_dims == 2:
122
+ result = padded_image[pad_left:-pad_right, pad_top:-pad_bottom]
123
+ # if padded_image is 3d
124
+ elif n_dims == 3:
125
+ result = padded_image[:, pad_left:-pad_right, pad_top:-pad_bottom]
126
+ else:
127
+ raise ValueError(
128
+ f"padded_image has {n_dims} dimensions, expected 2 or 3.")
129
+ return result
130
+
131
+
132
+ def windowed_generator(
133
+ padded_image: ndarray, window_size: int, subdivisions: int, batch_size: int = None
134
+ ):
135
+ """Generator that yield tiles grouped by batch size.
136
+ Args:
137
+ padded_image (np.ndarray): input image to be processed (already padded), supposed channels-first
138
+ window_size (int): size of a single patch
139
+ subdivisions (int): subdivision count on each patch to compute the step
140
+ batch_size (int, optional): amount of patches in each batch. Defaults to None.
141
+
142
+ Yields:
143
+ Tuple[List[tuple], np.ndarray]: list of coordinates and respective patches as single batch array
144
+ """
145
+ step = window_size // subdivisions
146
+ channel, width, height = padded_image.shape
147
+ batch_size = batch_size or 1
148
+ batch = []
149
+ coords = []
150
+ for x in range(0, width - window_size + 1, step):
151
+ for y in range(0, height - window_size + 1, step):
152
+ coords.append((x, y))
153
+ # extract the tile, place channels first for batch
154
+ tile = padded_image[:, x: x + window_size, y: y + window_size]
155
+ batch.append(tile)
156
+ # yield the batch once full and restore lists right after
157
+ if len(batch) == batch_size:
158
+ yield coords, np.stack(batch)
159
+ coords.clear()
160
+ batch.clear()
161
+ # handle last (possibly unfinished) batch
162
+ if len(batch) > 0:
163
+ yield coords, np.stack(batch)
164
+
165
+
166
+ def reconstruct(
167
+ canvas: ndarray, tile_size: int, coords: List[tuple], predictions: ndarray
168
+ ) -> ndarray:
169
+ """Helper function that iterates the result batch onto the given canvas to reconstruct
170
+ the final result batch after batch.
171
+ Args:
172
+ canvas (torch.Tensor): container for the final image.
173
+ tile_size (int): size of a single patch.
174
+ coords (List[tuple]): list of pixel coordinates corresponding to the batch items
175
+ predictions (torch.Tensor): array containing patch predictions, shape (batch, tile_size, tile_size, num_classes)
176
+
177
+ Returns:
178
+ torch.Tensor: the updated canvas, shape (padded_w, padded_h, num_classes)
179
+ """
180
+ for (x, y), patch in zip(coords, predictions):
181
+ # get canvas number of dimensions
182
+ n_dims = len(canvas.shape)
183
+ # if canvas is 2d
184
+ if n_dims == 2:
185
+ canvas[x: x + tile_size, y: y + tile_size] += patch
186
+ # if canvas is 3d
187
+ elif n_dims == 3:
188
+ canvas[:, x: x + tile_size, y: y + tile_size] += patch
189
+ else:
190
+ raise ValueError(
191
+ f"Canvas has {n_dims} dimensions, expected 2 or 3.")
192
+ return canvas
193
+
194
+
195
+ def predict_smooth_windowing(
196
+ image: ndarray,
197
+ tile_size: int,
198
+ subdivisions: int,
199
+ prediction_fn: Callable,
200
+ batch_size: int = 1,
201
+ out_dim: int = 1,
202
+ ) -> np.ndarray:
203
+ """Allows to predict a large image in one go, dividing it in squared, fixed-size tiles and smoothly
204
+ interpolating over them to produce a single, coherent output with the same dimensions.
205
+ Args:
206
+ image (np.ndarray): input image, expected a 3D vector
207
+ tile_size (int): size of each squared tile
208
+ subdivisions (int): number of subdivisions over the single tile for overlaps
209
+ prediction_fn (Callable): callback that takes the input batch and returns an output tensor
210
+ batch_size (int, optional): size of each batch. Defaults to None.
211
+ channels_first (int, optional): whether the input image is channels-first or not
212
+ mirrored (bool, optional): whether to use dihedral predictions (every simmetry). Defaults to False.
213
+
214
+ Returns:
215
+ np.ndarray: numpy array with dimensions (w, h), containing smooth predictions
216
+ """
217
+ img, pads = pad_image(image=image, tile_size=tile_size,
218
+ subdivisions=subdivisions)
219
+ spline = _spline_4d(window_size=tile_size, power=2)
220
+ # canvas = np.zeros(img.shape[1], img.shape[2])
221
+ canvas = np.zeros((out_dim, img.shape[1], img.shape[2]))
222
+ loop = tqdm(windowed_generator(
223
+ padded_image=img,
224
+ window_size=tile_size,
225
+ subdivisions=subdivisions,
226
+ batch_size=batch_size,
227
+ ))
228
+ for coords, batch in loop:
229
+ pred_batch = prediction_fn(batch) # .permute(0, 2, 3, 1)
230
+ # must be 3d for reconstruction to work
231
+ pred_batch = pred_batch * spline
232
+ canvas = reconstruct(
233
+ canvas, tile_size=tile_size, coords=coords, predictions=pred_batch
234
+ )
235
+ prediction = unpad_image(canvas, pads=pads)
236
+ return prediction
237
+
238
+
239
+ def center_pad(x, padding, div_factor=32, mode="reflect"):
240
+ # center pad with different padding for each city
241
+ # pads the image with the same padding on all sides
242
+ # the output size must be at least the size + 2*padding
243
+ # and divisible by div_factor
244
+ # first, compute the size of the padded image
245
+ size_x = x.shape[3]
246
+ size_y = x.shape[2]
247
+ # get the min padding
248
+ min_padding_x = size_x + 2 * padding
249
+ min_padding_y = size_y + 2 * padding
250
+ # get the new size
251
+ new_size_x = int(ceil(min_padding_x / div_factor) * div_factor)
252
+ new_size_y = int(ceil(min_padding_y / div_factor) * div_factor)
253
+ # get the padding
254
+ pad_x = new_size_x - size_x
255
+ pad_y = new_size_y - size_y
256
+ pad_left = int(floor(pad_x / 2))
257
+ pad_right = int(ceil(pad_x / 2))
258
+ pad_top = int(floor(pad_y / 2))
259
+ pad_bottom = int(ceil(pad_y / 2))
260
+ if pad_x > size_x or pad_y > size_y:
261
+ padded = np.pad(
262
+ x,
263
+ (
264
+ (0, 0),
265
+ (0, 0),
266
+ (int(floor(size_x / 2)), int(ceil(size_x / 2))),
267
+ (int(floor(size_y / 2)), int(ceil(size_y / 2))),
268
+ ),
269
+ mode=mode,
270
+ )
271
+ # and then pad to size
272
+ padded = np.pad(
273
+ x,
274
+ (
275
+ (0, 0),
276
+ (0, 0),
277
+ (int(floor(new_size_x / 2)), int(ceil(new_size_x / 2))),
278
+ (int(floor(new_size_y / 2)), int(ceil(new_size_y / 2))),
279
+ ),
280
+ mode=mode,
281
+ )
282
+ else:
283
+ padded = np.pad(
284
+ x,
285
+ (
286
+ (0, 0),
287
+ (0, 0),
288
+ (pad_top, pad_bottom),
289
+ (pad_left, pad_right),
290
+ ),
291
+ mode=mode,
292
+ )
293
+ paddings = (pad_top, pad_bottom, pad_left, pad_right)
294
+ return padded, paddings
295
+
296
+
297
+ class Model:
298
+ def __init__(self):
299
+ path = "assets/models/model.onnx"
300
+ self.model = ort.InferenceSession(path)
301
+ self.size = 512
302
+ self.subdivisions = 2
303
+ self.batch_size = 2
304
+ self.out_dim = 1
305
+
306
+ def forward(self, x):
307
+ assert x.ndim == 3, "Expected 3D tensor"
308
+ # remove batch dimension
309
+ x = x/255
310
+ # cast to fp32
311
+ x = x.astype(np.float32)
312
+ pred = predict_smooth_windowing(
313
+ image=x,
314
+ tile_size=self.size,
315
+ subdivisions=self.subdivisions,
316
+ prediction_fn=self.callback,
317
+ batch_size=self.batch_size,
318
+ out_dim=self.out_dim
319
+ )
320
+ pred = pred > 0
321
+ return pred
322
+
323
+ def callback(self, x: ndarray) -> ndarray:
324
+ # run onnx inference
325
+ out = self.model.run(None, {"input": x})[0]
326
+ return out
327
+
328
+
329
+ def infer(image):
330
+ print("Infering")
331
+ model = Model()
332
+ image = np.array(image)[:,:,0]
333
+ # add batch dim
334
+ image = image[None, :, :]
335
+ output_image = model.forward(image)
336
+ output_image = output_image[0]
337
+ output_image_color = np.zeros((output_image.shape[0], output_image.shape[1], 3))
338
+ output_image_color[output_image == 0] = [0, 0, 0]
339
+ output_image_color[output_image == 1] = [255, 255, 255]
340
+ output_image = Image.fromarray(output_image_color.astype(np.uint8))
341
+ return output_image
342
+
343
+
344
+ sample_images = [
345
+ "assets/data/sample1.png",
346
+ "assets/data/sample2.png"
347
+ ]
348
+
349
+ # Create the Gradio interface
350
+ with gr.Blocks() as demo:
351
+ gr.Markdown("## Oil Spill Detection Demo")
352
+ gr.Markdown(
353
+ "This app allows you to detect oil spills in Synthetic Aperture Radar (SAR) images. Upload a SAR image or use the sample image provided below to detect oil spills."
354
+ )
355
+ with gr.Row():
356
+ input_image = gr.Image(label="Input Image", type="pil")
357
+ output_image = gr.Image(label="Model Output", type="pil")
358
+ submit_button = gr.Button("Run Inference")
359
+ examples = gr.Examples(
360
+ examples=[[img] for img in sample_images],
361
+ inputs=[input_image]
362
+ )
363
+ submit_button.click(fn=infer, inputs=input_image, outputs=output_image)
364
+
365
+ demo.launch()
assets/data/sample1.png ADDED

Git LFS Details

  • SHA256: 21f40dc58a8023c1ecf6b7ccc8ece4da5be3cce71fce8af7df0a58c05726f62c
  • Pointer size: 131 Bytes
  • Size of remote file: 363 kB
assets/data/sample2.png ADDED

Git LFS Details

  • SHA256: 6b70460cf95fb57dc79ee984e54c302389321961d17b4f516ec7da33af138511
  • Pointer size: 132 Bytes
  • Size of remote file: 1.36 MB
assets/models/model.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:df139f12361ae6619745a709cbc5039d0fc25e4ab2d16ae83046388ee5745b46
3
+ size 248611034
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ gradio==5.21.0
2
+ numpy==2.2.4
3
+ onnxruntime==1.21.0
4
+ pandas==2.2.3
5
+ pillow==11.1.0
6
+ scipy==1.15.2
7
+ tqdm==4.67.1