Update app.py
Browse files
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 |
-
# βββ
|
| 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
|
| 73 |
"""
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
|
|
|
| 77 |
"""
|
| 78 |
-
|
| 79 |
-
if len(
|
| 80 |
-
return
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 118 |
mask_np = _polygon_to_mask(poly_xy, h, w)
|
| 119 |
|
| 120 |
-
# Visual:
|
| 121 |
-
|
|
|
|
| 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 |
-
|
| 132 |
-
|
| 133 |
|
| 134 |
grain_boxes.append({
|
| 135 |
-
"cls_id":
|
| 136 |
-
"cls_name":
|
| 137 |
-
"mask_np":
|
| 138 |
-
"
|
|
|
|
| 139 |
})
|
| 140 |
|
| 141 |
# Blend fill
|
| 142 |
annotated = cv2.addWeighted(annotated, 0.72, overlay, 0.28, 0)
|
| 143 |
|
| 144 |
-
# Draw
|
| 145 |
for g in grain_boxes:
|
| 146 |
-
if g["
|
| 147 |
cv2.polylines(
|
| 148 |
-
annotated, [g["
|
| 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 |
)
|