DefendIntelligence commited on
Commit
abc8b09
·
verified ·
1 Parent(s): a5d6fd2

Upload vessel detection code and fine-tuned YOLOv8 model

Browse files
.gitattributes CHANGED
@@ -1,35 +1 @@
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
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  *.pt filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ *.log
README.md ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Vessel Detection
3
+ sdk: gradio
4
+ app_file: app.py
5
+ python_version: 3.11
6
+ pinned: false
7
+ license: mit
8
+ ---
9
+
10
+ # Vessel Detection
11
+
12
+ Gradio Space for detecting vessels in satellite imagery with a fine-tuned YOLOv8 model.
13
+
14
+ ## Included Model
15
+
16
+ - File: `models/best.pt`
17
+ - Checkpoint source: `train-20260417T124314Z-fad9d3ed_best.pt`
18
+ - Run source: `infer-b88a2887`
19
+ - Training name: `super-visible-y8s-newlabels-focuslite-e45`
20
+ - Family: YOLOv8s
21
+ - Main dataset: `sentinel-2-rgb`
22
+ - Local index mAP50: `0.7912`
23
+
24
+ ## Usage
25
+
26
+ 1. Upload an RGB satellite image or select an example.
27
+ 2. Adjust the confidence threshold if needed.
28
+ 3. Click `Detect vessels`.
29
+
30
+ The app tiles large images before inference so small vessels remain visible to the model.
31
+
32
+ ## Hugging Face Deployment
33
+
34
+ Depuis ce dossier:
35
+
36
+ ```bash
37
+ git init
38
+ git lfs install
39
+ git remote add origin https://huggingface.co/spaces/DefendIntelligence/vessel-detection
40
+ git add .
41
+ git commit -m "Add YOLOv8 satellite boat detector Space"
42
+ git push -u origin main
43
+ ```
44
+
45
+ If the Space already exists, clone it and copy this folder's contents to the Space repository root.
app.py ADDED
@@ -0,0 +1,269 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from functools import lru_cache
4
+ from pathlib import Path
5
+
6
+ import gradio as gr
7
+ from PIL import Image, ImageDraw, ImageFont
8
+ from ultralytics import YOLO
9
+
10
+
11
+ ROOT = Path(__file__).resolve().parent
12
+ MODEL_PATH = ROOT / "models" / "best.pt"
13
+ EXAMPLES_DIR = ROOT / "examples"
14
+ MAX_TILES = 196
15
+ BATCH_SIZE = 8
16
+
17
+
18
+ @lru_cache(maxsize=1)
19
+ def load_model() -> YOLO:
20
+ if not MODEL_PATH.exists():
21
+ raise FileNotFoundError(f"Model not found: {MODEL_PATH}")
22
+ return YOLO(str(MODEL_PATH))
23
+
24
+
25
+ def _tile_starts(length: int, tile_size: int, overlap: int) -> list[int]:
26
+ if length <= tile_size:
27
+ return [0]
28
+ stride = max(1, tile_size - overlap)
29
+ starts = list(range(0, max(1, length - tile_size + 1), stride))
30
+ last = length - tile_size
31
+ if starts[-1] != last:
32
+ starts.append(last)
33
+ return starts
34
+
35
+
36
+ def _iter_tiles(image: Image.Image, tile_size: int, overlap: int) -> list[tuple[Image.Image, int, int]]:
37
+ width, height = image.size
38
+ x_starts = _tile_starts(width, tile_size, overlap)
39
+ y_starts = _tile_starts(height, tile_size, overlap)
40
+ tiles: list[tuple[Image.Image, int, int]] = []
41
+ for y in y_starts:
42
+ for x in x_starts:
43
+ right = min(width, x + tile_size)
44
+ bottom = min(height, y + tile_size)
45
+ tiles.append((image.crop((x, y, right, bottom)), x, y))
46
+ return tiles
47
+
48
+
49
+ def _box_iou(a: list[float], b: list[float]) -> float:
50
+ ax1, ay1, ax2, ay2 = a
51
+ bx1, by1, bx2, by2 = b
52
+ inter_x1 = max(ax1, bx1)
53
+ inter_y1 = max(ay1, by1)
54
+ inter_x2 = min(ax2, bx2)
55
+ inter_y2 = min(ay2, by2)
56
+ inter_w = max(0.0, inter_x2 - inter_x1)
57
+ inter_h = max(0.0, inter_y2 - inter_y1)
58
+ inter_area = inter_w * inter_h
59
+ if inter_area <= 0:
60
+ return 0.0
61
+ area_a = max(0.0, ax2 - ax1) * max(0.0, ay2 - ay1)
62
+ area_b = max(0.0, bx2 - bx1) * max(0.0, by2 - by1)
63
+ union = area_a + area_b - inter_area
64
+ return inter_area / union if union > 0 else 0.0
65
+
66
+
67
+ def _nms(detections: list[dict], iou_threshold: float) -> list[dict]:
68
+ remaining = sorted(detections, key=lambda item: float(item["confidence"]), reverse=True)
69
+ kept: list[dict] = []
70
+ while remaining:
71
+ current = remaining.pop(0)
72
+ kept.append(current)
73
+ remaining = [
74
+ item
75
+ for item in remaining
76
+ if item["class_id"] != current["class_id"]
77
+ or _box_iou(item["box"], current["box"]) < iou_threshold
78
+ ]
79
+ return kept
80
+
81
+
82
+ def _model_names(model: YOLO) -> dict[int, str]:
83
+ names = getattr(model, "names", None) or {}
84
+ if isinstance(names, dict):
85
+ return {int(key): str(value) for key, value in names.items()}
86
+ return {index: str(name) for index, name in enumerate(names)}
87
+
88
+
89
+ def _predict_tiles(
90
+ image: Image.Image,
91
+ *,
92
+ confidence: float,
93
+ iou: float,
94
+ tile_size: int,
95
+ overlap: int,
96
+ max_det: int,
97
+ ) -> tuple[list[dict], int]:
98
+ model = load_model()
99
+ names = _model_names(model)
100
+ rgb_image = image.convert("RGB")
101
+ safe_tile_size = max(320, int(tile_size))
102
+ safe_overlap = max(0, min(int(overlap), safe_tile_size - 32))
103
+ tiles = _iter_tiles(rgb_image, safe_tile_size, safe_overlap)
104
+
105
+ if len(tiles) > MAX_TILES:
106
+ raise ValueError(
107
+ f"Image too large for this CPU Space: {len(tiles)} tiles. "
108
+ f"Resize the image or increase the tile size."
109
+ )
110
+
111
+ detections: list[dict] = []
112
+ for start in range(0, len(tiles), BATCH_SIZE):
113
+ batch = tiles[start : start + BATCH_SIZE]
114
+ batch_images = [tile for tile, _, _ in batch]
115
+ results = model.predict(
116
+ source=batch_images,
117
+ conf=float(confidence),
118
+ iou=float(iou),
119
+ imgsz=safe_tile_size,
120
+ max_det=int(max_det),
121
+ verbose=False,
122
+ )
123
+ for result, (_, offset_x, offset_y) in zip(results, batch):
124
+ boxes = getattr(result, "boxes", None)
125
+ if boxes is None or len(boxes) == 0:
126
+ continue
127
+ xyxy = boxes.xyxy.cpu().numpy()
128
+ confs = boxes.conf.cpu().numpy()
129
+ classes = boxes.cls.cpu().numpy().astype(int)
130
+ for box, score, class_id in zip(xyxy, confs, classes):
131
+ x1, y1, x2, y2 = box.tolist()
132
+ detections.append(
133
+ {
134
+ "label": names.get(int(class_id), f"class_{int(class_id)}"),
135
+ "class_id": int(class_id),
136
+ "confidence": float(score),
137
+ "box": [
138
+ float(x1 + offset_x),
139
+ float(y1 + offset_y),
140
+ float(x2 + offset_x),
141
+ float(y2 + offset_y),
142
+ ],
143
+ }
144
+ )
145
+
146
+ detections = _nms(detections, float(iou))
147
+ detections = detections[: int(max_det)]
148
+ return detections, len(tiles)
149
+
150
+
151
+ def _draw_detections(image: Image.Image, detections: list[dict]) -> Image.Image:
152
+ annotated = image.convert("RGB").copy()
153
+ draw = ImageDraw.Draw(annotated)
154
+ font = ImageFont.load_default()
155
+ line_width = max(2, round(max(annotated.size) / 420))
156
+
157
+ for detection in detections:
158
+ x1, y1, x2, y2 = detection["box"]
159
+ label = f"{detection['label']} {detection['confidence']:.2f}"
160
+ draw.rectangle((x1, y1, x2, y2), outline=(255, 64, 48), width=line_width)
161
+ text_box = draw.textbbox((x1, y1), label, font=font)
162
+ text_w = text_box[2] - text_box[0]
163
+ text_h = text_box[3] - text_box[1]
164
+ label_y = max(0, y1 - text_h - 6)
165
+ draw.rectangle((x1, label_y, x1 + text_w + 8, label_y + text_h + 6), fill=(255, 64, 48))
166
+ draw.text((x1 + 4, label_y + 3), label, fill=(255, 255, 255), font=font)
167
+
168
+ return annotated
169
+
170
+
171
+ def _table_rows(detections: list[dict]) -> list[list[object]]:
172
+ rows: list[list[object]] = []
173
+ for index, detection in enumerate(detections, start=1):
174
+ x1, y1, x2, y2 = detection["box"]
175
+ rows.append(
176
+ [
177
+ index,
178
+ detection["label"],
179
+ round(float(detection["confidence"]), 4),
180
+ round(x1, 1),
181
+ round(y1, 1),
182
+ round(x2, 1),
183
+ round(y2, 1),
184
+ round(x2 - x1, 1),
185
+ round(y2 - y1, 1),
186
+ ]
187
+ )
188
+ return rows
189
+
190
+
191
+ def detect_boats(
192
+ image: Image.Image | None,
193
+ confidence: float,
194
+ iou: float,
195
+ tile_size: int,
196
+ overlap: int,
197
+ max_det: int,
198
+ ) -> tuple[Image.Image | None, list[list[object]], str]:
199
+ if image is None:
200
+ return None, [], "Upload a satellite image to run detection."
201
+
202
+ try:
203
+ detections, tile_count = _predict_tiles(
204
+ image,
205
+ confidence=confidence,
206
+ iou=iou,
207
+ tile_size=tile_size,
208
+ overlap=overlap,
209
+ max_det=max_det,
210
+ )
211
+ except Exception as exc:
212
+ return image, [], f"Inference error: {exc}"
213
+
214
+ annotated = _draw_detections(image, detections)
215
+ rows = _table_rows(detections)
216
+ if detections:
217
+ summary = f"{len(detections)} detection(s) above {confidence:.2f}. Tiles analyzed: {tile_count}."
218
+ else:
219
+ summary = f"No detections above {confidence:.2f}. Tiles analyzed: {tile_count}."
220
+ return annotated, rows, summary
221
+
222
+
223
+ def _example_paths() -> list[list[str]]:
224
+ paths = sorted(EXAMPLES_DIR.glob("*.png"))
225
+ return [[str(path)] for path in paths[:10]]
226
+
227
+
228
+ with gr.Blocks(title="Vessel Detection") as demo:
229
+ gr.Markdown(
230
+ """
231
+ # Vessel Detection
232
+
233
+ Fine-tuned YOLOv8s model for detecting vessels in RGB satellite imagery.
234
+ Upload a satellite image or select an example, then run detection.
235
+ """
236
+ )
237
+ with gr.Row():
238
+ with gr.Column(scale=1):
239
+ image_input = gr.Image(type="pil", label="Satellite image")
240
+ confidence_input = gr.Slider(0.01, 0.95, value=0.20, step=0.01, label="Confidence threshold")
241
+ iou_input = gr.Slider(0.05, 0.90, value=0.45, step=0.05, label="IoU NMS")
242
+ tile_size_input = gr.Slider(320, 1024, value=640, step=32, label="Tile size")
243
+ overlap_input = gr.Slider(0, 256, value=96, step=16, label="Tile overlap")
244
+ max_det_input = gr.Slider(1, 200, value=80, step=1, label="Max detections")
245
+ run_button = gr.Button("Detect vessels", variant="primary")
246
+ with gr.Column(scale=1):
247
+ output_image = gr.Image(type="pil", label="Annotated image")
248
+ summary_output = gr.Markdown()
249
+ table_output = gr.Dataframe(
250
+ headers=["#", "label", "confidence", "x1", "y1", "x2", "y2", "width", "height"],
251
+ datatype=["number", "str", "number", "number", "number", "number", "number", "number", "number"],
252
+ label="Detections",
253
+ )
254
+
255
+ run_button.click(
256
+ fn=detect_boats,
257
+ inputs=[image_input, confidence_input, iou_input, tile_size_input, overlap_input, max_det_input],
258
+ outputs=[output_image, table_output, summary_output],
259
+ )
260
+
261
+ gr.Examples(
262
+ examples=_example_paths(),
263
+ inputs=[image_input],
264
+ label="Example images",
265
+ )
266
+
267
+
268
+ if __name__ == "__main__":
269
+ demo.launch()
examples/example-01-20260301.png ADDED
examples/example-02-20260303.png ADDED
examples/example-03-20260303.png ADDED
examples/example-04-20260307.png ADDED
examples/example-05-20260307.png ADDED
examples/example-06-20260309.png ADDED
examples/example-07-20260312.png ADDED
examples/example-08-20260312.png ADDED
examples/example-09-20260312.png ADDED
examples/example-10-20260313.png ADDED
models/best.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:db4ddd5e9603d31051dcee57a8f035180cedd30834a41da44619d8c020695cf9
3
+ size 22550346
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ ultralytics>=8.3.0
2
+ opencv-python-headless>=4.10.0.84
3
+ pillow>=10.0.0
4
+ numpy>=1.26.0