EnginDev commited on
Commit
0ea9d35
·
verified ·
1 Parent(s): c7dc44f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +140 -271
app.py CHANGED
@@ -1,299 +1,168 @@
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()
 
 
1
+ # app.py - Boostly SAM2 (AutomaticMaskGenerator, CPU friendly defaults)
2
+ import os
3
+ import urllib.request
4
+ import time
5
+ import traceback
6
+
7
  import numpy as np
 
 
8
  from PIL import Image
9
+ import cv2
10
+ import torch
11
+ import gradio as gr
12
+
13
+ print("=== Boostly SAM2 Space starting ===", time.asctime())
14
+
15
+ # ---------------------------
16
+ # Model download / settings
17
+ # ---------------------------
18
+ MODEL_URL = "https://dl.fbaipublicfiles.com/segment_anything/sam_vit_b_01ec64.pth"
19
+ MODEL_PATH = "sam_vit_b.pth"
20
+ MODEL_TYPE = "vit_b"
21
+
22
+ # Download model if not present
23
+ if not os.path.exists(MODEL_PATH):
24
+ print("Model not found — downloading (this can take a while)...")
25
+ urllib.request.urlretrieve(MODEL_URL, MODEL_PATH)
26
+ print("Model downloaded.")
27
+
28
+ device = "cuda" if torch.cuda.is_available() else "cpu"
29
+ print("Device:", device)
30
+
31
+ # Try to import segment_anything (needs to be in requirements.txt)
32
+ try:
33
+ from segment_anything import sam_model_registry, SamAutomaticMaskGenerator
34
+ except Exception as e:
35
+ print("ERROR importing segment_anything:", e)
36
+ raise
37
 
38
+ # Load model
39
+ sam = sam_model_registry[MODEL_TYPE](checkpoint=MODEL_PATH)
40
+ sam.to(device=device)
41
 
42
+ # Automatic mask generator parameters (tweak for CPU)
43
+ mask_generator = SamAutomaticMaskGenerator(
44
+ sam,
45
+ points_per_side=16, # lower -> fewer proposals (faster)
46
+ pred_iou_thresh=0.86, # confidence threshold
47
+ stability_score_thresh=0.7, # lower -> more masks
48
+ min_mask_region_area=200 # remove tiny specks
49
+ )
50
 
51
+ print("Mask generator ready.")
52
+
53
+ # ---------------------------
54
+ # Helpers
55
+ # ---------------------------
56
+ def prepare_image(image, max_size=1024):
57
+ """Convert and resize image to reduce memory on CPU."""
58
  if isinstance(image, np.ndarray):
59
  image_pil = Image.fromarray(image)
 
60
  else:
61
  image_pil = image
62
+ if image_pil.mode != "RGB":
63
+ image_pil = image_pil.convert("RGB")
64
+ w, h = image_pil.size
65
+ max_wh = max(w, h)
66
+ if max_wh > max_size:
67
+ scale = max_size / max_wh
68
+ new_w, new_h = int(w * scale), int(h * scale)
69
+ image_pil = image_pil.resize((new_w, new_h), Image.Resampling.LANCZOS)
70
+ return image_pil, np.array(image_pil)
71
 
72
+ def keep_top_masks(masks, top_k=12):
73
+ """Sort masks by area and keep top_k largest ones."""
74
+ if not masks:
75
+ return masks
76
+ areas = [np.sum(m["segmentation"]) for m in masks]
77
+ idx = np.argsort(areas)[::-1] # descending
78
+ top_idx = idx[:top_k]
79
+ return [masks[i] for i in top_idx]
80
 
81
+ def draw_masks_overlay(image_np, masks, draw_labels=True):
82
+ overlay = image_np.copy()
83
+ for i, mask in enumerate(masks):
84
+ m = mask["segmentation"]
85
+ color = np.random.randint(40, 230, size=(3,))
86
+ alpha = 0.55
87
+ overlay[m] = (overlay[m] * (1 - alpha) + color * alpha).astype(np.uint8)
88
+ # Draw contour
89
+ contours, _ = cv2.findContours(m.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
90
+ cv2.drawContours(overlay, contours, -1, (255,255,255), 2)
91
+ # Label
92
+ if draw_labels:
93
+ ys, xs = np.where(m)
94
+ if len(xs) and len(ys):
95
+ cx, cy = int(np.mean(xs)), int(np.mean(ys))
96
+ cv2.putText(overlay, f"Obj {i+1}", (cx, cy), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255,255,255), 2)
97
+ return overlay
98
 
99
+ # ---------------------------
100
+ # Main function: segment all
101
+ # ---------------------------
102
+ def segment_all_objects(image, max_size=1024, top_k=12, cpu_mode=True):
103
  """
104
+ image: PIL or ndarray
105
+ max_size: resize largest side to this for CPU
106
+ top_k: keep top_k biggest masks (to reduce noise)
107
+ cpu_mode: if True, uses conservative params (already set in mask_generator)
108
  """
 
 
109
  try:
110
+ if image is None:
111
+ return None, {"error": "Kein Bild hochgeladen"}
112
+ # prepare image
113
+ image_pil, image_np = prepare_image(image, max_size=max_size)
114
+ h, w = image_np.shape[:2]
115
+ print(f"Starting mask generation — image size: {w}x{h}, device: {device}")
116
+ t0 = time.time()
 
 
 
 
 
117
 
118
+ # generate masks (this is the heavy call)
119
+ masks = mask_generator.generate(image_np)
 
 
 
 
 
 
 
 
 
 
 
 
120
 
121
+ t1 = time.time()
122
+ print(f"Generated {len(masks)} masks in {t1-t0:.1f}s")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
 
124
+ # filter: keep largest top_k masks
125
+ masks = keep_top_masks(masks, top_k=top_k)
126
+ print(f"Kept top {len(masks)} masks by area")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
 
128
+ # Convert overlay
129
+ overlay = draw_masks_overlay(image_np, masks, draw_labels=True)
130
+
131
+ metadata = {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  "success": True,
133
+ "num_masks_generated": len(masks),
134
+ "generation_time_s": float(t1 - t0),
135
  "image_size": [w, h],
136
+ "device": device
 
 
 
137
  }
138
+ return Image.fromarray(overlay), metadata
139
+
 
140
  except Exception as e:
141
+ tb = traceback.format_exc()
142
+ print("ERROR in segment_all_objects:", e)
143
+ print(tb)
144
+ return image, {"error": str(e), "trace": tb}
145
 
146
+ # ---------------------------
147
+ # Gradio UI
148
+ # ---------------------------
149
+ title = "Boostly SAM2 - AutomaticMaskGenerator"
150
+ desc = "Upload an image — SAM AutomaticMaskGenerator will propose many segmentations. On CPU it's slower; use small images for tests."
 
 
 
 
 
 
 
 
 
 
 
 
 
151
 
152
+ with gr.Blocks(title=title) as demo:
153
+ gr.Markdown("# 🎨 Boostly - Auto segmentation (SAM)")
 
 
 
 
 
 
154
  with gr.Row():
155
+ with gr.Column(scale=1):
156
+ inp = gr.Image(type="pil", label="Upload image")
157
+ max_size = gr.Slider(minimum=256, maximum=1600, value=1024, step=128, label="Max image side (resize)")
158
+ top_k = gr.Slider(minimum=1, maximum=40, value=12, step=1, label="Keep top K masks (largest)")
159
+ btn = gr.Button("Segment (Auto)")
160
+ with gr.Column(scale=1):
161
+ out_img = gr.Image(label="Segmented overlay")
162
+ out_meta = gr.JSON(label="Metadata")
163
+
164
+ btn.click(fn=segment_all_objects, inputs=[inp, max_size, top_k, gr.State(True)], outputs=[out_img, out_meta])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
 
166
  if __name__ == "__main__":
167
+ print("Launching Gradio app...")
168
+ demo.launch(server_name="0.0.0.0", server_port=7860, show_error=True)