EnginDev commited on
Commit
4e16899
·
verified ·
1 Parent(s): b56a179

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +408 -48
app.py CHANGED
@@ -1,87 +1,447 @@
1
- def select_fish_mask(masks, scores, image_shape, mode='fish'):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  """
3
- 🎣 INTELLIGENTE FISCH-AUSWAHL v2.0
 
 
 
 
 
4
  """
5
  h, w = image_shape
6
  image_center_x, image_center_y = w / 2, h / 2
 
7
 
8
- if mode == 'multi':
9
- # Multi-Object: Kombiniere alle guten Masken (unverändert)
10
- combined_mask = np.zeros((h, w), dtype=bool)
11
- masks_used = 0
12
-
13
- for idx, score in enumerate(scores):
14
- if score > 0.5:
15
- if masks.ndim == 4:
16
- mask = masks[0, idx].numpy()
17
- else:
18
- mask = masks[idx].numpy()
19
-
20
- combined_mask = combined_mask | (mask > 0)
21
- masks_used += 1
22
-
23
- return combined_mask, masks_used, scores
24
-
25
- # 🎣 FISH MODE v2.0: AGGRESSIVER FILTER
26
  valid_masks = []
27
 
28
- for idx, score in enumerate(scores):
29
- if score < 0.7: # Hoher Quality-Threshold
30
- continue
31
-
32
- if masks.ndim == 4:
33
- mask = masks[0, idx].numpy()
34
- else:
35
- mask = masks[idx].numpy()
36
 
 
37
  mask_area = np.sum(mask)
38
- coverage = mask_area / (h * w)
39
 
40
- # 🎯 KRITISCH: Hintergrund-Filter auf 15% gesenkt!
41
- if coverage > 0.15: # ✅ Alles über 15% ist zu groß für einen Fisch
42
- print(f" ⚠️ Mask {idx} rejected: Too large ({coverage*100:.1f}% coverage)")
43
  continue
44
 
45
- # Zu kleine Masken auch filtern (z.B. Reflektion, Köder)
46
- if coverage < 0.02: # Kleiner als 2% = Noise
47
- print(f" ⚠️ Mask {idx} rejected: Too small ({coverage*100:.1f}% coverage)")
48
  continue
49
 
50
- # Masken-Eigenschaften berechnen
51
- center_x, center_y = calculate_mask_center(mask)
 
 
52
 
 
 
53
  if center_x is None:
54
  continue
55
 
56
- # Distanz zum Bildzentrum
57
  distance_to_center = np.sqrt(
58
  (center_x - image_center_x)**2 +
59
  (center_y - image_center_y)**2
60
  )
61
 
62
  valid_masks.append({
63
- 'idx': idx,
64
  'mask': mask,
65
  'score': score,
66
  'area': mask_area,
67
  'coverage': coverage,
68
  'center': (center_x, center_y),
69
- 'distance_to_center': distance_to_center
 
70
  })
71
 
72
- print(f" ✅ Mask {idx}: score={score:.3f}, coverage={coverage*100:.1f}%, distance={distance_to_center:.1f}")
73
 
74
  if not valid_masks:
75
  print(" ❌ No valid fish masks found!")
76
- return None, 0, scores
77
 
78
- # 🎯 NEUE STRATEGIE: Wähle KLEINSTE Maske (= Fisch)
79
- # Sortiere nach Coverage (kleinste zuerst)
80
  valid_masks.sort(key=lambda m: m['coverage'])
81
-
82
- # Nimm die kleinste Maske mit gutem Score
83
  best_mask = valid_masks[0]
84
 
85
- print(f" 🏆 Selected smallest mask {best_mask['idx']} (coverage: {best_mask['coverage']*100:.1f}%)")
86
 
87
- return best_mask['mask'], 1, [best_mask['score']]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import torch
3
+ import numpy as np
4
+ from PIL import Image
5
+ import cv2
6
+
7
+ print("🚀 Starting SAM2 FishBoost Edition v4.0 - ULTRA OPTIMIZED...")
8
+
9
+ device = "cuda" if torch.cuda.is_available() else "cpu"
10
+ print(f"📱 Using device: {device}")
11
+
12
+ model = None
13
+ processor = None
14
+
15
+ def load_model():
16
+ global model, processor
17
+ if model is None:
18
+ print("📦 Loading SAM model...")
19
+ try:
20
+ from transformers import SamModel, SamProcessor
21
+
22
+ model_name = "facebook/sam-vit-large"
23
+
24
+ processor = SamProcessor.from_pretrained(model_name)
25
+ model = SamModel.from_pretrained(model_name)
26
+ model.to(device)
27
+ print(f"✅ Model loaded: {model_name}")
28
+ except Exception as e:
29
+ print(f"❌ Error: {e}, falling back to base model")
30
+ model_name = "facebook/sam-vit-base"
31
+ processor = SamProcessor.from_pretrained(model_name)
32
+ model = SamModel.from_pretrained(model_name)
33
+ model.to(device)
34
+ return model, processor
35
+
36
+ def prepare_image(image, max_size=1024):
37
+ if isinstance(image, np.ndarray):
38
+ image_pil = Image.fromarray(image)
39
+ else:
40
+ image_pil = image
41
+
42
+ if image_pil.mode != 'RGB':
43
+ image_pil = image_pil.convert('RGB')
44
+
45
+ image_np = np.array(image_pil)
46
+ h, w = image_np.shape[:2]
47
+
48
+ if max(h, w) > max_size:
49
+ scale = max_size / max(h, w)
50
+ new_h, new_w = int(h * scale), int(w * scale)
51
+ image_pil = image_pil.resize((new_w, new_h), Image.Resampling.LANCZOS)
52
+ image_np = np.array(image_pil)
53
+
54
+ return image_pil, image_np
55
+
56
+ def refine_mask(mask, kernel_size=5):
57
+ """Glättet Maskenkanten"""
58
+ mask_uint8 = (mask > 0).astype(np.uint8) * 255
59
+ kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (kernel_size, kernel_size))
60
+ mask_closed = cv2.morphologyEx(mask_uint8, cv2.MORPH_CLOSE, kernel)
61
+ mask_refined = cv2.morphologyEx(mask_closed, cv2.MORPH_OPEN, kernel)
62
+ return mask_refined > 0
63
+
64
+ def calculate_mask_center(mask):
65
+ """Berechnet Schwerpunkt der Maske"""
66
+ y_coords, x_coords = np.where(mask)
67
+ if len(x_coords) == 0:
68
+ return None, None
69
+ return np.mean(x_coords), np.mean(y_coords)
70
+
71
+ def extract_contours_from_mask(mask):
72
+ """Extrahiert Konturen als [{x, y}, ...] Format"""
73
+ contours, _ = cv2.findContours(
74
+ mask.astype(np.uint8),
75
+ cv2.RETR_EXTERNAL,
76
+ cv2.CHAIN_APPROX_SIMPLE
77
+ )
78
+
79
+ if not contours:
80
+ return []
81
+
82
+ # Größte Kontur wählen
83
+ largest_contour = max(contours, key=cv2.contourArea)
84
+
85
+ # Format konvertieren: [[x, y]] -> [{x: int, y: int}]
86
+ points = []
87
+ for point in largest_contour:
88
+ x, y = point[0]
89
+ points.append({"x": int(x), "y": int(y)})
90
+
91
+ return points
92
+
93
+ def generate_grid_points(w, h, grid_size=3):
94
+ """Generiert Grid-Punkte über das Bild verteilt"""
95
+ points = []
96
+ for i in range(1, grid_size + 1):
97
+ for j in range(1, grid_size + 1):
98
+ x = int(w * i / (grid_size + 1))
99
+ y = int(h * j / (grid_size + 1))
100
+ points.append([x, y])
101
+ return points
102
+
103
+ def select_best_fish_mask(all_masks, all_scores, image_shape):
104
  """
105
+ 🎣 ULTRA-INTELLIGENTE FISCH-AUSWAHL
106
+
107
+ Strategie:
108
+ 1. Filtere sehr große Masken (>15% = Hintergrund/Angler)
109
+ 2. Filtere sehr kleine Masken (<2% = Noise)
110
+ 3. Wähle KLEINSTE verbleibende Maske (= Fisch)
111
  """
112
  h, w = image_shape
113
  image_center_x, image_center_y = w / 2, h / 2
114
+ total_pixels = h * w
115
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  valid_masks = []
117
 
118
+ print(f"\n🔍 Analyzing {len(all_masks)} candidate masks...")
119
+
120
+ for mask_data in all_masks:
121
+ mask = mask_data['mask']
122
+ score = mask_data['score']
123
+ point = mask_data['point']
 
 
124
 
125
+ # Coverage berechnen
126
  mask_area = np.sum(mask)
127
+ coverage = mask_area / total_pixels
128
 
129
+ # 🚫 FILTER 1: Zu groß (Hintergrund/Angler)
130
+ if coverage > 0.15: # 15% Threshold (vorher 60%)
131
+ print(f" ❌ Rejected: Coverage {coverage*100:.1f}% > 15% (Background)")
132
  continue
133
 
134
+ # 🚫 FILTER 2: Zu klein (Noise)
135
+ if coverage < 0.02: # 2% Minimum
136
+ print(f" ❌ Rejected: Coverage {coverage*100:.1f}% < 2% (Noise)")
137
  continue
138
 
139
+ # 🚫 FILTER 3: Schlechter Score
140
+ if score < 0.7:
141
+ print(f" ❌ Rejected: Score {score:.3f} < 0.7")
142
+ continue
143
 
144
+ # Center Distance berechnen
145
+ center_x, center_y = calculate_mask_center(mask)
146
  if center_x is None:
147
  continue
148
 
 
149
  distance_to_center = np.sqrt(
150
  (center_x - image_center_x)**2 +
151
  (center_y - image_center_y)**2
152
  )
153
 
154
  valid_masks.append({
 
155
  'mask': mask,
156
  'score': score,
157
  'area': mask_area,
158
  'coverage': coverage,
159
  'center': (center_x, center_y),
160
+ 'distance_to_center': distance_to_center,
161
+ 'point': point
162
  })
163
 
164
+ print(f" ✅ Valid: coverage={coverage*100:.1f}%, score={score:.3f}, dist={distance_to_center:.0f}px")
165
 
166
  if not valid_masks:
167
  print(" ❌ No valid fish masks found!")
168
+ return None
169
 
170
+ # 🎯 STRATEGIE: Wähle KLEINSTE Maske (= Fisch, nicht Angler)
 
171
  valid_masks.sort(key=lambda m: m['coverage'])
 
 
172
  best_mask = valid_masks[0]
173
 
174
+ print(f"\n 🏆 SELECTED: Smallest mask (coverage: {best_mask['coverage']*100:.1f}%)")
175
 
176
+ return best_mask
177
+
178
+ def segment_automatic(image, quality="high", mode="fish"):
179
+ """
180
+ 🎣 ULTRA-OPTIMIZED Fish Detection
181
+ - Multi-Point Grid (9 Punkte statt nur Mitte)
182
+ - 15% Coverage Filter (statt 60%)
183
+ - Kleinste Maske = Fisch
184
+ """
185
+ if image is None:
186
+ return None, {"error": "Kein Bild hochgeladen"}
187
+
188
+ try:
189
+ print(f"\n{'='*60}")
190
+ print(f"🔄 Starting ULTRA segmentation (mode: {mode}, quality: {quality})")
191
+ print(f"{'='*60}")
192
+
193
+ model, processor = load_model()
194
+
195
+ image_pil, image_np = prepare_image(image)
196
+ h, w = image_np.shape[:2]
197
+
198
+ if mode == "fish":
199
+ # 🆕 MULTI-POINT GRID (statt nur Bildmitte)
200
+ grid_points = generate_grid_points(w, h, grid_size=3)
201
+ print(f"📍 Using {len(grid_points)} grid points for detection")
202
+ else:
203
+ # Fallback: nur Bildmitte
204
+ grid_points = [[w // 2, h // 2]]
205
+
206
+ all_masks = []
207
+
208
+ # Für jeden Grid-Punkt: Maske generieren
209
+ for idx, point in enumerate(grid_points):
210
+ inputs = processor(
211
+ image_pil,
212
+ input_points=[[point]],
213
+ input_labels=[[1]],
214
+ return_tensors="pt"
215
+ ).to(device)
216
+
217
+ with torch.no_grad():
218
+ outputs = model(**inputs, multimask_output=True)
219
+
220
+ masks = processor.image_processor.post_process_masks(
221
+ outputs.pred_masks.cpu(),
222
+ inputs["original_sizes"].cpu(),
223
+ inputs["reshaped_input_sizes"].cpu()
224
+ )[0]
225
+
226
+ scores = outputs.iou_scores.cpu().numpy().flatten()
227
+
228
+ # Beste Maske dieses Punktes
229
+ best_idx = np.argmax(scores)
230
+ if masks.ndim == 4:
231
+ mask = masks[0, best_idx].numpy()
232
+ else:
233
+ mask = masks[best_idx].numpy()
234
+
235
+ all_masks.append({
236
+ 'mask': mask > 0,
237
+ 'score': scores[best_idx],
238
+ 'point': point
239
+ })
240
+
241
+ print(f"✅ Generated {len(all_masks)} masks from grid points")
242
+
243
+ # 🎣 BESTE FISCH-MASKE WÄHLEN
244
+ best_fish = select_best_fish_mask(all_masks, None, (h, w))
245
+
246
+ if best_fish is None:
247
+ return None, {
248
+ "error": "No fish detected. Image might contain only background/angler.",
249
+ "suggestion": "Try 'Multi-Object' mode or use a different image."
250
+ }
251
+
252
+ final_mask = best_fish['mask']
253
+
254
+ # Refinement
255
+ if quality == "high":
256
+ print("🎨 Refining mask edges...")
257
+ final_mask = refine_mask(final_mask, kernel_size=7)
258
+
259
+ # 🆕 KONTUREN EXTRAHIEREN
260
+ contours_list = extract_contours_from_mask(final_mask)
261
+
262
+ # Overlay erstellen
263
+ overlay = image_np.copy()
264
+ color = np.array([0, 255, 100]) # Grün für Fisch
265
+
266
+ mask_float = final_mask.astype(float)
267
+ if quality == "high":
268
+ mask_float = cv2.GaussianBlur(mask_float, (5, 5), 0)
269
+
270
+ for c in range(3):
271
+ overlay[:, :, c] = (
272
+ overlay[:, :, c] * (1 - mask_float * 0.65) +
273
+ color[c] * mask_float * 0.65
274
+ )
275
+
276
+ # Kontur zeichnen
277
+ contours_cv, _ = cv2.findContours(
278
+ final_mask.astype(np.uint8),
279
+ cv2.RETR_EXTERNAL,
280
+ cv2.CHAIN_APPROX_SIMPLE
281
+ )
282
+ cv2.drawContours(overlay, contours_cv, -1, (255, 255, 0), 3)
283
+
284
+ # Metadata
285
+ mask_area = int(np.sum(final_mask))
286
+ mask_percentage = float(mask_area / (h * w) * 100)
287
+
288
+ metadata = {
289
+ "success": True,
290
+ "mode": "automatic_fish_ultra_optimized",
291
+ "quality": quality,
292
+ "detection_method": "multi_point_grid" if mode == "fish" else "center_point",
293
+ "grid_points_used": len(grid_points),
294
+ "image_size": [w, h],
295
+ "mask_area": mask_area,
296
+ "mask_percentage": mask_percentage,
297
+ "num_contours": len(contours_cv),
298
+ "fish_score": float(best_fish['score']),
299
+ "fish_center": [float(best_fish['center'][0]), float(best_fish['center'][1])],
300
+ "device": device,
301
+ "contours": contours_list
302
+ }
303
+
304
+ print(f"\n{'='*60}")
305
+ print(f"✅ SEGMENTATION COMPLETE!")
306
+ print(f" Fish coverage: {mask_percentage:.1f}%")
307
+ print(f" Confidence: {best_fish['score']*100:.1f}%")
308
+ print(f" Contour points: {len(contours_list)}")
309
+ print(f"{'='*60}\n")
310
+
311
+ return Image.fromarray(overlay.astype(np.uint8)), metadata
312
+
313
+ except Exception as e:
314
+ import traceback
315
+ print(f"❌ ERROR:\n{traceback.format_exc()}")
316
+ return image, {"error": str(e)}
317
+
318
+ # Gradio Interface
319
+ demo = gr.Blocks(title="SAM2 FishBoost ULTRA", theme=gr.themes.Soft())
320
+
321
+ with demo:
322
+ gr.Markdown("# 🎣 SAM2 FishBoost ULTRA v4.0")
323
+ gr.Markdown("### Multi-Point Grid Detection + 15% Coverage Filter")
324
+
325
+ with gr.Tab("🎣 Fish Detection (ULTRA)"):
326
+ gr.Markdown("""
327
+ **🚀 NEUE FEATURES:**
328
+ - ✅ 9-Punkt Grid Detection (nicht nur Bildmitte!)
329
+ - ✅ 15% Coverage Filter (filtert Angler/Hintergrund)
330
+ - ✅ Kleinste Maske = Fisch
331
+ """)
332
+
333
+ with gr.Row():
334
+ with gr.Column():
335
+ input_fish = gr.Image(type="pil", label="📸 Bild hochladen")
336
+
337
+ quality_radio = gr.Radio(
338
+ choices=["high", "fast"],
339
+ value="high",
340
+ label="⚙️ Qualität"
341
+ )
342
+
343
+ mode_radio = gr.Radio(
344
+ choices=["fish", "multi"],
345
+ value="fish",
346
+ label="🎯 Modus",
347
+ info="Fish = Multi-Point Grid, Multi = Center Only"
348
+ )
349
+
350
+ btn_fish = gr.Button("🎣 Fisch segmentieren", variant="primary", size="lg")
351
+
352
+ gr.Markdown("""
353
+ **💡 Wie es funktioniert:**
354
+
355
+ **Fish Mode (ULTRA):**
356
+ 1. Scannt Bild mit 9 Punkten (3x3 Grid)
357
+ 2. Ignoriert große Objekte (>15% = Angler)
358
+ 3. Ignoriert kleine Objekte (<2% = Noise)
359
+ 4. Wählt kleinste Maske (= Fisch!)
360
+
361
+ **Multi Mode:**
362
+ - Alte Methode (nur Bildmitte)
363
+ - Für allgemeine Objekte
364
+ """)
365
+
366
+ with gr.Column():
367
+ output_fish = gr.Image(label="✨ Segmentierter Fisch")
368
+ json_fish = gr.JSON(label="📊 Metadata")
369
+
370
+ btn_fish.click(
371
+ fn=segment_automatic,
372
+ inputs=[input_fish, quality_radio, mode_radio],
373
+ outputs=[output_fish, json_fish]
374
+ )
375
+
376
+ gr.Examples(
377
+ examples=[],
378
+ inputs=input_fish,
379
+ label="💡 Upload dein Angelfoto!"
380
+ )
381
+
382
+ with gr.Tab("📡 API Integration (Lovable)"):
383
+ gr.Markdown("### 🔗 API Endpoint")
384
+ gr.Code("https://EnginDev-Boostly.hf.space/api/predict", label="Base URL")
385
+
386
+ gr.Markdown("### 📝 JavaScript Code")
387
+ gr.Code('''
388
+ // Fish Detection ULTRA
389
+ const response = await fetch('https://EnginDev-Boostly.hf.space/api/predict', {
390
+ method: 'POST',
391
+ headers: {'Content-Type': 'application/json'},
392
+ body: JSON.stringify({
393
+ data: [
394
+ base64Image, // Base64 image
395
+ "high", // quality: "high" | "fast"
396
+ "fish" // mode: "fish" (ULTRA) | "multi"
397
+ ],
398
+ fn_index: 0
399
+ })
400
+ });
401
+
402
+ const result = await response.json();
403
+
404
+ // Expected Response:
405
+ {
406
+ "data": [
407
+ "data:image/png;base64,iVBORw...", // Segmented overlay
408
+ {
409
+ "success": true,
410
+ "mode": "automatic_fish_ultra_optimized",
411
+ "detection_method": "multi_point_grid",
412
+ "grid_points_used": 9,
413
+ "mask_percentage": 8.2, // Nur der Fisch! (nicht 86%)
414
+ "fish_score": 0.98,
415
+ "fish_center": [385, 520],
416
+ "contours": [
417
+ {"x": 350, "y": 450},
418
+ {"x": 351, "y": 451},
419
+ // ... präzise Fisch-Kontur
420
+ ]
421
+ }
422
+ ]
423
+ }
424
+ ''', language="javascript")
425
+
426
+ gr.Markdown("""
427
+ ### ⚙️ Parameter Erklärung
428
+
429
+ **mode: "fish"** (ULTRA - EMPFOHLEN für Angelfotos)
430
+ - Multi-Point Grid (9 Erkennungspunkte)
431
+ - 15% Coverage Filter
432
+ - Kleinste Maske = Fisch
433
+ - ✅ Perfekt für: Angler mit Fisch im Bild
434
+
435
+ **mode: "multi"**
436
+ - Center-Point Only (alte Methode)
437
+ - 60% Coverage Filter
438
+ - ✅ Für allgemeine Objekte
439
+
440
+ **quality:**
441
+ - `"high"` = Präzise Kanten, Gaussian Blur (~20s)
442
+ - `"fast"` = Schneller, weniger Nachbearbeitung (~10s)
443
+ """)
444
+
445
+ if __name__ == "__main__":
446
+ print("🌐 Launching FishBoost SAM2 ULTRA v4.0...")
447
+ demo.launch(server_name="0.0.0.0", server_port=7860, show_error=True)