EnginDev commited on
Commit
21ec41f
·
verified ·
1 Parent(s): 30285ef

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +214 -76
app.py CHANGED
@@ -1,161 +1,299 @@
 
1
  import numpy as np
 
2
  import cv2
3
  from PIL import Image
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
  def safe_to_cpu_np(tensor):
6
  """Return numpy array or None if tensor is None"""
7
  if tensor is None:
8
  return None
9
- return tensor.detach().cpu().numpy()
 
 
10
 
11
  def postprocess_masks_safe(processor, pred_masks, original_sizes, reshaped_input_sizes):
12
  """
13
  Wrapper: versucht verschiedene Formen / Signaturen zu behandeln.
14
- Gibt eine Liste von mask arrays (H,W) für das erste Bild zurück.
15
  """
16
- # ensure numpy
17
- pred_masks_np = np.asarray(pred_masks)
18
- # call processor post_process in a robust way
19
  try:
 
20
  masks = processor.image_processor.post_process_masks(
21
- pred_masks_np, np.asarray(original_sizes), np.asarray(reshaped_input_sizes)
 
 
 
22
  )
 
23
  except Exception as e:
24
- # Try alternative signature (some transformers versions accept different args)
25
- try:
26
- masks = processor.post_process_masks(pred_masks_np, np.asarray(original_sizes), np.asarray(reshaped_input_sizes))
27
- except Exception as e2:
28
- print("postprocess_masks failed:", e, e2)
29
- return []
30
- # masks is a list per image - return first (for batch size 1)
31
- if isinstance(masks, (list, tuple)) and len(masks) > 0:
32
- return masks[0]
33
- return []
34
 
35
  def ensure_mask_bool(mask):
36
- """normalizes mask to boolean 2D ndarray"""
37
  if mask is None:
38
  return None
 
39
  a = np.asarray(mask)
40
- # if mask has channel dim, reduce
41
- if a.ndim == 3 and a.shape[0] in (1,):
 
42
  a = a[0]
 
43
  if a.dtype != bool:
44
  return (a > 0).astype(bool)
45
  return a
46
 
47
  def pick_best_mask_by_score(masks_list, scores):
48
- """Return index of chosen mask; handles missing/short scores"""
49
  if len(masks_list) == 0:
50
  return None
 
51
  if scores is None or len(scores) == 0:
52
  return 0
53
- scores = np.asarray(scores).flatten()
54
- if len(scores) == 1:
 
55
  return 0
56
- # prefer middle mask as before if at least 3
57
- if len(scores) >= 3:
58
- sorted_idx = np.argsort(scores)
59
- return sorted_idx[1] # middle
60
  return int(np.argmax(scores))
61
 
62
  def keep_largest_component_bool(mask_bool):
63
  """Return boolean mask with only largest connected component"""
64
  if mask_bool is None:
65
  return None
 
66
  mask_uint8 = (mask_bool.astype(np.uint8) * 255)
67
  num_labels, labels = cv2.connectedComponents(mask_uint8)
 
68
  if num_labels <= 1:
69
  return mask_bool
 
 
70
  largest = 1
71
  largest_size = 0
72
  for i in range(1, num_labels):
73
- s = int((labels == i).sum())
74
- if s > largest_size:
75
- largest_size = s
76
  largest = i
 
77
  return (labels == largest)
78
 
 
79
  def segment_automatic(image, quality="high", filter_largest=True):
 
80
  if image is None:
81
  return None, {"error": "Kein Bild hochgeladen"}
 
82
  try:
83
- model, processor = load_model()
84
  image_pil, image_np = prepare_image(image)
85
  h, w = image_np.shape[:2]
86
- cx, cy = w//2, h//2
87
-
88
- # Build inputs
 
 
89
  inputs = processor(
90
  image_pil,
91
  input_points=[[[cx, cy]]],
92
  input_labels=[[1]],
93
  return_tensors="pt"
94
  )
95
- inputs = {k: v.to(device) if hasattr(v, "to") else v for k, v in inputs.items()}
96
-
 
 
 
97
  with torch.no_grad():
98
  outputs = model(**inputs, multimask_output=True)
99
-
100
- # safe extraction
101
- pred_masks = safe_to_cpu_np(getattr(outputs, "pred_masks", None))
102
- iou_scores = safe_to_cpu_np(getattr(outputs, "iou_scores", None))
103
-
104
- # Get original/reshaped sizes from inputs (move to cpu/numpy if tensors)
105
- orig_sizes = safe_to_cpu_np(inputs.get("original_sizes", None))
106
- reshaped_sizes = safe_to_cpu_np(inputs.get("reshaped_input_sizes", None))
107
-
108
- # postprocess masks robustly
109
- masks_post = postprocess_masks_safe(processor, pred_masks, orig_sizes if orig_sizes is not None else [[h, w]], reshaped_sizes if reshaped_sizes is not None else [[h, w]])
110
- if not masks_post:
111
- return image, {"error": "keine masken erzeugt (postprocess returned empty)"}
112
-
113
- # masks_post is list/array: choose best
114
- idx = pick_best_mask_by_score(masks_post, iou_scores)
115
- if idx is None:
116
- return image, {"error": "keine passende maske gefunden"}
117
-
118
- # normalize mask
119
- # masks_post might be array of masks or dict -> handle both
120
- mask_candidate = masks_post[idx] if isinstance(masks_post, (list, tuple, np.ndarray)) else masks_post
121
- mask_bool = ensure_mask_bool(mask_candidate)
122
-
123
- # filter largest component
 
 
 
124
  if filter_largest and mask_bool is not None:
125
  mask_bool = keep_largest_component_bool(mask_bool)
126
-
127
- # refine edges if requested
128
  if quality == "high" and mask_bool is not None:
129
  mask_bool = refine_mask(mask_bool, kernel_size=7)
130
-
131
- # overlay coloring
132
  overlay = image_np.copy()
 
133
  if mask_bool is None:
134
- return image, {"error": "maske ist None nach Verarbeitung"}
135
-
 
136
  mask_float = mask_bool.astype(float)
137
  if quality == "high":
138
- mask_float = cv2.GaussianBlur(mask_float, (5,5), 0)
139
-
140
  color = np.array([255, 80, 180], dtype=np.uint8)
141
  for c in range(3):
142
- overlay[:,:,c] = (overlay[:,:,c] * (1 - mask_float * 0.65) + color[c] * mask_float * 0.65).astype(np.uint8)
143
-
144
- contours, _ = cv2.findContours((mask_bool.astype(np.uint8)), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
145
- cv2.drawContours(overlay, contours, -1, (255,255,0), 3)
146
-
 
 
 
 
 
147
  meta = {
148
  "success": True,
149
  "mode": "automatic_smart",
150
  "image_size": [w, h],
151
  "mask_area": int(mask_bool.sum()),
152
- "mask_percentage": float(mask_bool.sum() / (w*h) * 100),
153
  "num_contours": len(contours),
154
- "device": device
155
  }
 
156
  return Image.fromarray(overlay), meta
157
-
158
  except Exception as e:
159
  import traceback
160
  print(traceback.format_exc())
161
  return image, {"error": str(e)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
  import numpy as np
3
+ import torch
4
  import cv2
5
  from PIL import Image
6
+ from transformers import SamModel, SamProcessor
7
+ import spaces
8
+
9
+ # GPU setup
10
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
11
+
12
+ # Model loading
13
+ print("Loading SAM2 model...")
14
+ model_id = "facebook/sam-vit-huge"
15
+ processor = SamProcessor.from_pretrained(model_id)
16
+ model = SamModel.from_pretrained(model_id).to(device)
17
+ model.eval()
18
+ print(f"Model loaded on {device}")
19
+
20
+ def prepare_image(image):
21
+ """Convert image to PIL and numpy formats"""
22
+ if isinstance(image, np.ndarray):
23
+ image_pil = Image.fromarray(image)
24
+ image_np = image
25
+ else:
26
+ image_pil = image
27
+ image_np = np.array(image)
28
+
29
+ # Ensure RGB
30
+ if image_np.ndim == 2:
31
+ image_np = cv2.cvtColor(image_np, cv2.COLOR_GRAY2RGB)
32
+ elif image_np.shape[2] == 4:
33
+ image_np = image_np[:, :, :3]
34
+
35
+ return image_pil, image_np
36
+
37
+ def refine_mask(mask, kernel_size=5):
38
+ """Refine mask edges using morphological operations"""
39
+ kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (kernel_size, kernel_size))
40
+ mask = cv2.morphologyEx(mask.astype(np.uint8), cv2.MORPH_CLOSE, kernel)
41
+ mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
42
+ return mask.astype(bool)
43
 
44
  def safe_to_cpu_np(tensor):
45
  """Return numpy array or None if tensor is None"""
46
  if tensor is None:
47
  return None
48
+ if isinstance(tensor, torch.Tensor):
49
+ return tensor.detach().cpu().numpy()
50
+ return np.asarray(tensor)
51
 
52
  def postprocess_masks_safe(processor, pred_masks, original_sizes, reshaped_input_sizes):
53
  """
54
  Wrapper: versucht verschiedene Formen / Signaturen zu behandeln.
 
55
  """
56
+ pred_masks_np = safe_to_cpu_np(pred_masks)
57
+
 
58
  try:
59
+ # Try the standard post-processing
60
  masks = processor.image_processor.post_process_masks(
61
+ pred_masks_np,
62
+ original_sizes,
63
+ reshaped_input_sizes,
64
+ return_tensors="np"
65
  )
66
+ return masks
67
  except Exception as e:
68
+ print(f"Postprocess error: {e}")
69
+ # Return raw masks if post-processing fails
70
+ return pred_masks_np
 
 
 
 
 
 
 
71
 
72
  def ensure_mask_bool(mask):
73
+ """Normalize mask to boolean 2D ndarray"""
74
  if mask is None:
75
  return None
76
+
77
  a = np.asarray(mask)
78
+
79
+ # Handle different shapes
80
+ while a.ndim > 2:
81
  a = a[0]
82
+
83
  if a.dtype != bool:
84
  return (a > 0).astype(bool)
85
  return a
86
 
87
  def pick_best_mask_by_score(masks_list, scores):
88
+ """Return index of chosen mask"""
89
  if len(masks_list) == 0:
90
  return None
91
+
92
  if scores is None or len(scores) == 0:
93
  return 0
94
+
95
+ scores = safe_to_cpu_np(scores)
96
+ if scores is None:
97
  return 0
98
+
99
+ scores = scores.flatten()
100
+
101
+ # Pick the mask with highest IoU score
102
  return int(np.argmax(scores))
103
 
104
  def keep_largest_component_bool(mask_bool):
105
  """Return boolean mask with only largest connected component"""
106
  if mask_bool is None:
107
  return None
108
+
109
  mask_uint8 = (mask_bool.astype(np.uint8) * 255)
110
  num_labels, labels = cv2.connectedComponents(mask_uint8)
111
+
112
  if num_labels <= 1:
113
  return mask_bool
114
+
115
+ # Find largest component (excluding background)
116
  largest = 1
117
  largest_size = 0
118
  for i in range(1, num_labels):
119
+ size = int((labels == i).sum())
120
+ if size > largest_size:
121
+ largest_size = size
122
  largest = i
123
+
124
  return (labels == largest)
125
 
126
+ @spaces.GPU
127
  def segment_automatic(image, quality="high", filter_largest=True):
128
+ """Main segmentation function"""
129
  if image is None:
130
  return None, {"error": "Kein Bild hochgeladen"}
131
+
132
  try:
133
+ # Prepare image
134
  image_pil, image_np = prepare_image(image)
135
  h, w = image_np.shape[:2]
136
+
137
+ # Use center point as default
138
+ cx, cy = w // 2, h // 2
139
+
140
+ # Prepare inputs
141
  inputs = processor(
142
  image_pil,
143
  input_points=[[[cx, cy]]],
144
  input_labels=[[1]],
145
  return_tensors="pt"
146
  )
147
+
148
+ # Move to device
149
+ inputs = {k: v.to(device) if hasattr(v, 'to') else v for k, v in inputs.items()}
150
+
151
+ # Run model
152
  with torch.no_grad():
153
  outputs = model(**inputs, multimask_output=True)
154
+
155
+ # Extract predictions
156
+ pred_masks = outputs.pred_masks.squeeze(0) # Remove batch dimension
157
+ iou_scores = outputs.iou_scores.squeeze(0) if hasattr(outputs, 'iou_scores') else None
158
+
159
+ # Convert to numpy
160
+ pred_masks_np = safe_to_cpu_np(pred_masks)
161
+ iou_scores_np = safe_to_cpu_np(iou_scores)
162
+
163
+ # Pick best mask
164
+ if pred_masks_np.ndim == 3: # Multiple masks
165
+ idx = pick_best_mask_by_score(pred_masks_np, iou_scores_np)
166
+ if idx is not None:
167
+ mask = pred_masks_np[idx]
168
+ else:
169
+ mask = pred_masks_np[0]
170
+ else:
171
+ mask = pred_masks_np
172
+
173
+ # Ensure boolean mask
174
+ mask_bool = ensure_mask_bool(mask)
175
+
176
+ # Resize mask to original image size if needed
177
+ if mask_bool.shape != (h, w):
178
+ mask_bool = cv2.resize(mask_bool.astype(np.uint8), (w, h),
179
+ interpolation=cv2.INTER_NEAREST).astype(bool)
180
+
181
+ # Filter largest component
182
  if filter_largest and mask_bool is not None:
183
  mask_bool = keep_largest_component_bool(mask_bool)
184
+
185
+ # Refine edges
186
  if quality == "high" and mask_bool is not None:
187
  mask_bool = refine_mask(mask_bool, kernel_size=7)
188
+
189
+ # Create overlay
190
  overlay = image_np.copy()
191
+
192
  if mask_bool is None:
193
+ return image, {"error": "Maske ist None nach Verarbeitung"}
194
+
195
+ # Apply colored overlay
196
  mask_float = mask_bool.astype(float)
197
  if quality == "high":
198
+ mask_float = cv2.GaussianBlur(mask_float, (5, 5), 0)
199
+
200
  color = np.array([255, 80, 180], dtype=np.uint8)
201
  for c in range(3):
202
+ overlay[:, :, c] = (overlay[:, :, c] * (1 - mask_float * 0.65) +
203
+ color[c] * mask_float * 0.65).astype(np.uint8)
204
+
205
+ # Draw contours
206
+ contours, _ = cv2.findContours(mask_bool.astype(np.uint8),
207
+ cv2.RETR_EXTERNAL,
208
+ cv2.CHAIN_APPROX_SIMPLE)
209
+ cv2.drawContours(overlay, contours, -1, (255, 255, 0), 3)
210
+
211
+ # Metadata
212
  meta = {
213
  "success": True,
214
  "mode": "automatic_smart",
215
  "image_size": [w, h],
216
  "mask_area": int(mask_bool.sum()),
217
+ "mask_percentage": float(mask_bool.sum() / (w * h) * 100),
218
  "num_contours": len(contours),
219
+ "device": str(device)
220
  }
221
+
222
  return Image.fromarray(overlay), meta
223
+
224
  except Exception as e:
225
  import traceback
226
  print(traceback.format_exc())
227
  return image, {"error": str(e)}
228
+
229
+ # Gradio Interface
230
+ def segment_image(image, quality, filter_largest):
231
+ """Gradio wrapper function"""
232
+ result_image, metadata = segment_automatic(image, quality, filter_largest)
233
+
234
+ # Format metadata for display
235
+ if "error" in metadata:
236
+ info_text = f"❌ Fehler: {metadata['error']}"
237
+ else:
238
+ info_text = f"""
239
+ ✅ Segmentierung erfolgreich!
240
+ 📏 Bildgröße: {metadata['image_size'][0]} x {metadata['image_size'][1]}
241
+ 🎯 Maskierte Fläche: {metadata['mask_area']:,} Pixel ({metadata['mask_percentage']:.1f}%)
242
+ 📍 Anzahl Konturen: {metadata['num_contours']}
243
+ 🖥️ Gerät: {metadata['device']}
244
+ """
245
+
246
+ return result_image, info_text
247
+
248
+ # Create Gradio interface
249
+ with gr.Blocks(title="SAM2 Segmentierung") as demo:
250
+ gr.Markdown("""
251
+ # 🎯 SAM2 Automatische Segmentierung
252
+
253
+ Lädt ein Bild hoch und segmentiert automatisch das zentrale Objekt.
254
+ """)
255
+
256
+ with gr.Row():
257
+ with gr.Column():
258
+ input_image = gr.Image(label="Eingabebild", type="pil")
259
+
260
+ with gr.Row():
261
+ quality_dropdown = gr.Dropdown(
262
+ choices=["standard", "high"],
263
+ value="high",
264
+ label="Qualität",
265
+ info="High = bessere Kanten, aber langsamer"
266
+ )
267
+ filter_checkbox = gr.Checkbox(
268
+ value=True,
269
+ label="Nur größte Komponente",
270
+ info="Entfernt kleine isolierte Bereiche"
271
+ )
272
+
273
+ segment_btn = gr.Button("🚀 Segmentieren", variant="primary")
274
+
275
+ with gr.Column():
276
+ output_image = gr.Image(label="Segmentiertes Bild")
277
+ info_text = gr.Textbox(label="Informationen", lines=6)
278
+
279
+ # Examples
280
+ gr.Examples(
281
+ examples=[
282
+ ["example1.jpg", "high", True],
283
+ ["example2.jpg", "standard", False],
284
+ ],
285
+ inputs=[input_image, quality_dropdown, filter_checkbox],
286
+ outputs=[output_image, info_text],
287
+ fn=segment_image,
288
+ cache_examples=False
289
+ )
290
+
291
+ # Connect button
292
+ segment_btn.click(
293
+ fn=segment_image,
294
+ inputs=[input_image, quality_dropdown, filter_checkbox],
295
+ outputs=[output_image, info_text]
296
+ )
297
+
298
+ if __name__ == "__main__":
299
+ demo.launch()