mlbench123 commited on
Commit
fcccc26
Β·
verified Β·
1 Parent(s): 7412522

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +98 -24
app.py CHANGED
@@ -59,7 +59,7 @@ def _text_size(draw, text, font):
59
  return bbox[2] - bbox[0], bbox[3] - bbox[1]
60
 
61
 
62
- # ─── Visual polygon helpers ───────────────────────────────────────────────────
63
 
64
  def _polygon_to_mask(pts_xy, h, w):
65
  """Rasterise raw polygon β†’ binary uint8 mask. BACKEND / measurements only."""
@@ -69,19 +69,90 @@ def _polygon_to_mask(pts_xy, h, w):
69
  return mask
70
 
71
 
72
- def _rotated_box_from_mask(mask_np):
73
  """
74
- Compute tight rotated bounding box from backend mask.
75
- Returns the 4 corner points as int32 array (4, 1, 2) β€” ready for
76
- cv2.polylines / fillPoly.
 
77
  """
78
- pts_y, pts_x = np.where(mask_np == 1)
79
- if len(pts_x) < 5:
80
- return None, None
81
- pts = np.column_stack([pts_x.astype(np.float32), pts_y.astype(np.float32)])
82
- rect = cv2.minAreaRect(pts)
83
- box = cv2.boxPoints(rect) # 4 corners (float)
84
- return box.astype(np.int32).reshape(-1, 1, 2), rect
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
 
86
 
87
  # ─────────────────────────────────────────────────────────────────────────────
@@ -93,6 +164,7 @@ def _rotated_box_from_mask(mask_np):
93
  # ──────────────────────────────────��──────────────────────────────────────────
94
  def run_segmentation(img_np):
95
  h, w = img_np.shape[:2]
 
96
  results = model(img_np, imgsz=1280, conf=0.25)[0]
97
 
98
  annotated = img_np.copy()
@@ -114,11 +186,12 @@ def run_segmentation(img_np):
114
  color = CLASS_COLORS.get(cls_id, (200, 200, 200))
115
  counts[cls_name] += 1
116
 
117
- # Backend mask: raw polygon fill (measurements only)
118
  mask_np = _polygon_to_mask(poly_xy, h, w)
119
 
120
- # Visual: tight rotated bounding box from the backend mask
121
- vis_box, rect = _rotated_box_from_mask(mask_np)
 
122
 
123
  # Bounding box from backend mask for zoom crop
124
  ys, xs = np.where(mask_np == 1)
@@ -128,24 +201,25 @@ def run_segmentation(img_np):
128
  all_x2 = max(all_x2, int(xs.max()))
129
  all_y2 = max(all_y2, int(ys.max()))
130
 
131
- if vis_box is not None:
132
- cv2.fillPoly(overlay, [vis_box], color)
133
 
134
  grain_boxes.append({
135
- "cls_id": cls_id,
136
- "cls_name": cls_name,
137
- "mask_np": mask_np, # backend only
138
- "vis_box": vis_box, # visual rotated bbox
 
139
  })
140
 
141
  # Blend fill
142
  annotated = cv2.addWeighted(annotated, 0.72, overlay, 0.28, 0)
143
 
144
- # Draw crisp rotated bbox outlines (no fill β€” just the border)
145
  for g in grain_boxes:
146
- if g["vis_box"] is not None:
147
  cv2.polylines(
148
- annotated, [g["vis_box"]],
149
  isClosed=True, color=CLASS_COLORS[g["cls_id"]], thickness=2,
150
  lineType=cv2.LINE_AA,
151
  )
 
59
  return bbox[2] - bbox[0], bbox[3] - bbox[1]
60
 
61
 
62
+ # ─── Mask helpers ─────────────────────────────────────────────────────────────
63
 
64
  def _polygon_to_mask(pts_xy, h, w):
65
  """Rasterise raw polygon β†’ binary uint8 mask. BACKEND / measurements only."""
 
69
  return mask
70
 
71
 
72
+ def _refine_mask_grabcut(img_bgr, coarse_mask):
73
  """
74
+ Refine a coarse binary mask to pixel-perfect grain boundary using GrabCut.
75
+ img_bgr : full BGR image
76
+ coarse_mask : uint8 binary mask (0/1), same size as img_bgr
77
+ Returns : refined binary uint8 mask (0/1)
78
  """
79
+ ys, xs = np.where(coarse_mask == 1)
80
+ if len(xs) < 5:
81
+ return coarse_mask
82
+
83
+ # Tight crop with small padding so GrabCut has background context
84
+ x1, y1 = max(0, int(xs.min()) - 6), max(0, int(ys.min()) - 6)
85
+ x2, y2 = min(img_bgr.shape[1], int(xs.max()) + 6), min(img_bgr.shape[0], int(ys.max()) + 6)
86
+ crop = img_bgr[y1:y2, x1:x2]
87
+ ch, cw = crop.shape[:2]
88
+ if ch < 8 or cw < 8:
89
+ return coarse_mask
90
+
91
+ # Build GrabCut init mask from coarse mask crop
92
+ gc_mask = np.full((ch, cw), cv2.GC_BGD, dtype=np.uint8)
93
+ local_fg = coarse_mask[y1:y2, x1:x2]
94
+
95
+ # Erode to get definite FG core, dilate to get probable FG ring
96
+ k_sm = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
97
+ k_lg = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (11, 11))
98
+ def_fg = cv2.erode(local_fg, k_sm, iterations=2)
99
+ prob_fg = cv2.dilate(local_fg, k_lg, iterations=2)
100
+
101
+ gc_mask[prob_fg == 1] = cv2.GC_PR_FGD
102
+ gc_mask[def_fg == 1] = cv2.GC_FGD
103
+ # Border strip = definite background
104
+ gc_mask[:3, :] = cv2.GC_BGD
105
+ gc_mask[-3:, :] = cv2.GC_BGD
106
+ gc_mask[:, :3] = cv2.GC_BGD
107
+ gc_mask[:, -3:] = cv2.GC_BGD
108
+
109
+ try:
110
+ bgd_model = np.zeros((1, 65), np.float64)
111
+ fgd_model = np.zeros((1, 65), np.float64)
112
+ cv2.grabCut(crop, gc_mask, None, bgd_model, fgd_model, 4, cv2.GC_INIT_WITH_MASK)
113
+ refined_local = np.where((gc_mask == cv2.GC_FGD) | (gc_mask == cv2.GC_PR_FGD), 1, 0).astype(np.uint8)
114
+ except Exception:
115
+ return coarse_mask
116
+
117
+ # Clean up with morphology: close small holes, smooth jagged edges
118
+ k_cl = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
119
+ refined_local = cv2.morphologyEx(refined_local, cv2.MORPH_CLOSE, k_cl, iterations=2)
120
+ refined_local = cv2.morphologyEx(refined_local, cv2.MORPH_OPEN, k_cl, iterations=1)
121
+
122
+ # Put refined crop back into full-size mask
123
+ refined_full = np.zeros_like(coarse_mask)
124
+ refined_full[y1:y2, x1:x2] = refined_local
125
+ return refined_full
126
+
127
+
128
+ def _mask_to_smooth_contour(mask_np):
129
+ """
130
+ Extract the outer contour of a binary mask and smooth it with
131
+ spline-like resampling β†’ returns int32 array (N,1,2) for cv2 drawing.
132
+ """
133
+ contours, _ = cv2.findContours(mask_np, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
134
+ if not contours:
135
+ return None
136
+ cnt = max(contours, key=cv2.contourArea).astype(np.float32).reshape(-1, 2)
137
+ if len(cnt) < 6:
138
+ return cnt.astype(np.int32).reshape(-1, 1, 2)
139
+
140
+ # Resample to ~120 evenly-spaced points for a smooth outline
141
+ n_target = min(120, max(40, len(cnt)))
142
+ indices = np.linspace(0, len(cnt) - 1, n_target).astype(int)
143
+ sampled = cnt[indices]
144
+
145
+ # Circular Gaussian smooth
146
+ window = 9
147
+ half = window // 2
148
+ padded = np.vstack([sampled[-half:], sampled, sampled[:half]])
149
+ kernel = cv2.getGaussianKernel(window, 0).flatten()
150
+ kernel /= kernel.sum()
151
+ smoothed = np.zeros_like(sampled)
152
+ for i in range(len(sampled)):
153
+ smoothed[i] = (padded[i:i + window] * kernel[:, None]).sum(axis=0)
154
+
155
+ return smoothed.astype(np.int32).reshape(-1, 1, 2)
156
 
157
 
158
  # ─────────────────────────────────────────────────────────────────────────────
 
164
  # ──────────────────────────────────��──────────────────────────────────────────
165
  def run_segmentation(img_np):
166
  h, w = img_np.shape[:2]
167
+ img_bgr = cv2.cvtColor(img_np, cv2.COLOR_RGB2BGR)
168
  results = model(img_np, imgsz=1280, conf=0.25)[0]
169
 
170
  annotated = img_np.copy()
 
186
  color = CLASS_COLORS.get(cls_id, (200, 200, 200))
187
  counts[cls_name] += 1
188
 
189
+ # Backend mask: raw polygon fill (used for measurements β€” never changed)
190
  mask_np = _polygon_to_mask(poly_xy, h, w)
191
 
192
+ # Visual mask: GrabCut-refined β†’ hugs actual grain pixels perfectly
193
+ vis_mask = _refine_mask_grabcut(img_bgr, mask_np)
194
+ vis_contour = _mask_to_smooth_contour(vis_mask)
195
 
196
  # Bounding box from backend mask for zoom crop
197
  ys, xs = np.where(mask_np == 1)
 
201
  all_x2 = max(all_x2, int(xs.max()))
202
  all_y2 = max(all_y2, int(ys.max()))
203
 
204
+ # Fill overlay using the refined visual mask directly (pixel-perfect fill)
205
+ overlay[vis_mask == 1] = color
206
 
207
  grain_boxes.append({
208
+ "cls_id": cls_id,
209
+ "cls_name": cls_name,
210
+ "mask_np": mask_np, # backend only β€” measurements
211
+ "vis_mask": vis_mask, # refined visual mask
212
+ "vis_contour": vis_contour, # smooth contour for outline
213
  })
214
 
215
  # Blend fill
216
  annotated = cv2.addWeighted(annotated, 0.72, overlay, 0.28, 0)
217
 
218
+ # Draw smooth anti-aliased contour outlines over the blend
219
  for g in grain_boxes:
220
+ if g["vis_contour"] is not None:
221
  cv2.polylines(
222
+ annotated, [g["vis_contour"]],
223
  isClosed=True, color=CLASS_COLORS[g["cls_id"]], thickness=2,
224
  lineType=cv2.LINE_AA,
225
  )