Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -11,7 +11,8 @@ import pandas as pd
|
|
| 11 |
# -----------------------------
|
| 12 |
|
| 13 |
# Maximum allowed image side (pixels) to avoid OOM / heavy CPU usage
|
| 14 |
-
|
|
|
|
| 15 |
|
| 16 |
|
| 17 |
# -----------------------------
|
|
@@ -20,7 +21,7 @@ MAX_SIDE = 2048
|
|
| 20 |
|
| 21 |
def downscale_bgr(img: np.ndarray) -> Tuple[np.ndarray, float]:
|
| 22 |
"""Downscale image so that the longest side is <= MAX_SIDE.
|
| 23 |
-
|
| 24 |
Returns
|
| 25 |
-------
|
| 26 |
img_resized : np.ndarray
|
|
@@ -39,7 +40,7 @@ def downscale_bgr(img: np.ndarray) -> Tuple[np.ndarray, float]:
|
|
| 39 |
|
| 40 |
def normalize_angle(angle: float, size_w: float, size_h: float) -> float:
|
| 41 |
"""Normalize OpenCV minAreaRect angle to [0, 180) degrees.
|
| 42 |
-
|
| 43 |
OpenCV returns angles depending on whether width < height. We fix it so that
|
| 44 |
the *long side* is treated as length and angle is always in [0, 180).
|
| 45 |
"""
|
|
@@ -54,92 +55,112 @@ def normalize_angle(angle: float, size_w: float, size_h: float) -> float:
|
|
| 54 |
# Reference object detection
|
| 55 |
# -----------------------------
|
| 56 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
def detect_reference(
|
| 58 |
img_bgr: np.ndarray,
|
| 59 |
mode: str,
|
| 60 |
ref_size_mm: Optional[float],
|
| 61 |
) -> Tuple[float, Optional[Tuple[int, int]], Optional[str], Optional[Tuple[int, int, int, int]]]:
|
| 62 |
-
"""
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
"""
|
| 70 |
h, w = img_bgr.shape[:2]
|
| 71 |
|
| 72 |
-
#
|
| 73 |
-
|
| 74 |
-
# has a noticeable color difference from the background.
|
| 75 |
-
lab = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2LAB).astype(np.float32)
|
| 76 |
-
# Use the median color of the whole image as background estimate (robust to small objects)
|
| 77 |
-
bg_color = np.median(lab.reshape(-1, 3), axis=0)
|
| 78 |
-
|
| 79 |
-
# Compute per-pixel Euclidean distance in LAB space
|
| 80 |
-
diff = lab - bg_color # shape (H, W, 3)
|
| 81 |
-
dist = np.sqrt(np.sum(diff * diff, axis=2)).astype(np.float32)
|
| 82 |
-
|
| 83 |
-
# Threshold on color distance: pixels far from background color are foreground
|
| 84 |
-
# You can tune 8.0 -> 6.0 or 10.0 depending on image contrast.
|
| 85 |
-
_, mask = cv2.threshold(dist, 8.0, 255, cv2.THRESH_BINARY)
|
| 86 |
-
mask = mask.astype(np.uint8)
|
| 87 |
-
|
| 88 |
-
# 2. Small morphological opening to remove noise
|
| 89 |
-
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
|
| 90 |
-
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel, iterations=1)
|
| 91 |
|
| 92 |
-
#
|
| 93 |
num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(mask)
|
| 94 |
|
| 95 |
-
#
|
| 96 |
candidates = []
|
| 97 |
-
|
|
|
|
|
|
|
|
|
|
| 98 |
x, y, ww, hh, area = stats[i]
|
| 99 |
-
|
| 100 |
-
|
|
|
|
| 101 |
continue
|
| 102 |
-
|
| 103 |
-
#
|
| 104 |
-
if x > w * 0.
|
| 105 |
continue
|
| 106 |
-
|
| 107 |
-
#
|
| 108 |
-
|
| 109 |
-
if
|
| 110 |
continue
|
| 111 |
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
ref_type: Optional[str] = None
|
| 117 |
-
bbox: Optional[Tuple[int, int, int, int]] = None
|
| 118 |
-
|
| 119 |
-
if candidates:
|
| 120 |
-
# 4. Pick the one closest to the top-left corner (smallest x + y)
|
| 121 |
-
label_id, x, y, ww, hh, area = min(candidates, key=lambda t: t[1] + t[2])
|
| 122 |
-
bbox = (int(x), int(y), int(ww), int(hh))
|
| 123 |
-
center = (int(x + ww // 2), int(y + hh // 2))
|
| 124 |
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
# For both circles and squares, the max side of the bounding box
|
| 129 |
-
# can be treated as "diameter/side" in pixels.
|
| 130 |
-
side_or_diam_px = float(max(ww, hh))
|
| 131 |
-
px_per_mm = max(side_or_diam_px / ref_mm, 1e-6)
|
| 132 |
-
|
| 133 |
-
# Roughly classify reference type; optional, not used in scaling
|
| 134 |
-
ref_type = "square"
|
| 135 |
-
else:
|
| 136 |
-
# If no reference found, use a safe default scale to avoid division by zero.
|
| 137 |
px_per_mm = 4.0
|
| 138 |
center = None
|
| 139 |
ref_type = None
|
| 140 |
bbox = None
|
|
|
|
| 141 |
|
| 142 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
|
| 144 |
|
| 145 |
# -----------------------------
|
|
@@ -153,11 +174,7 @@ def build_mask_hsv(
|
|
| 153 |
hsv_high_h: int,
|
| 154 |
color_tol: int,
|
| 155 |
) -> np.ndarray:
|
| 156 |
-
"""Build binary mask using HSV thresholds.
|
| 157 |
-
|
| 158 |
-
For leaves we use a typical green range on H and stronger S/V filtering.
|
| 159 |
-
For seeds / grains, we mainly use S/V to remove the white background.
|
| 160 |
-
"""
|
| 161 |
hsv = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV)
|
| 162 |
h_channel = hsv[:, :, 0]
|
| 163 |
s_channel = hsv[:, :, 1]
|
|
@@ -189,51 +206,157 @@ def segment(
|
|
| 189 |
sample_type: str,
|
| 190 |
hsv_low_h: int,
|
| 191 |
hsv_high_h: int,
|
| 192 |
-
color_tol: int,
|
| 193 |
min_area_px: float,
|
| 194 |
max_area_px: float,
|
| 195 |
) -> List[Dict[str, Any]]:
|
| 196 |
"""Segment objects and compute basic geometric descriptors.
|
| 197 |
-
|
| 198 |
-
Returns a list of component dictionaries with contour, bounding box, etc.
|
| 199 |
"""
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
|
|
|
|
|
|
|
|
|
| 203 |
components: List[Dict[str, Any]] = []
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
continue
|
| 208 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
rect = cv2.minAreaRect(cnt)
|
| 210 |
box = cv2.boxPoints(rect).astype(np.int32)
|
| 211 |
-
|
| 212 |
-
if m["m00"] == 0:
|
| 213 |
-
cx, cy = 0, 0
|
| 214 |
-
else:
|
| 215 |
-
cx = int(m["m10"] / m["m00"])
|
| 216 |
-
cy = int(m["m01"] / m["m00"])
|
| 217 |
-
|
| 218 |
peri = cv2.arcLength(cnt, True)
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 237 |
return components
|
| 238 |
|
| 239 |
|
|
@@ -261,17 +384,18 @@ def compute_metrics(
|
|
| 261 |
rows: List[Dict[str, Any]] = []
|
| 262 |
|
| 263 |
for i, comp in enumerate(components, start=1):
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
length_mm = w_px / px_per_mm
|
| 268 |
-
width_mm = h_px / px_per_mm
|
| 269 |
area_mm2 = comp["area_px"] / (px_per_mm * px_per_mm)
|
| 270 |
perimeter_mm = comp["peri_px"] / px_per_mm
|
| 271 |
|
| 272 |
aspect_ratio = length_mm / (width_mm + 1e-6)
|
|
|
|
|
|
|
| 273 |
circularity = (4.0 * np.pi * area_mm2) / (perimeter_mm * perimeter_mm + 1e-6)
|
| 274 |
|
|
|
|
| 275 |
mask_single = np.zeros(img_bgr.shape[:2], dtype=np.uint8)
|
| 276 |
cv2.drawContours(mask_single, [comp["contour"]], -1, 255, thickness=-1)
|
| 277 |
mean_r, mean_g, mean_b, h, s, v, gi, bi = compute_color_metrics(img_bgr, mask_single)
|
|
@@ -312,13 +436,15 @@ def render_overlay(
|
|
| 312 |
df: pd.DataFrame,
|
| 313 |
ref_bbox: Optional[Tuple[int, int, int, int]] = None,
|
| 314 |
) -> np.ndarray:
|
| 315 |
-
"""Draw reference + sample annotations on the image.
|
|
|
|
|
|
|
| 316 |
out = img_bgr.copy()
|
| 317 |
|
|
|
|
| 318 |
ref_center, ref_type = ref
|
| 319 |
if ref_bbox is not None:
|
| 320 |
x, y, w, h = ref_bbox
|
| 321 |
-
# Red rectangle for reference object
|
| 322 |
cv2.rectangle(out, (int(x), int(y)), (int(x + w), int(y + h)), (0, 0, 255), 2)
|
| 323 |
cv2.putText(
|
| 324 |
out,
|
|
@@ -331,47 +457,94 @@ def render_overlay(
|
|
| 331 |
cv2.LINE_AA,
|
| 332 |
)
|
| 333 |
|
|
|
|
| 334 |
for i, comp in enumerate(components, start=1):
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 344 |
cv2.putText(
|
| 345 |
out,
|
| 346 |
f"s{i}",
|
| 347 |
-
(int(
|
| 348 |
cv2.FONT_HERSHEY_SIMPLEX,
|
| 349 |
0.5,
|
| 350 |
(255, 255, 255),
|
| 351 |
-
|
| 352 |
cv2.LINE_AA,
|
| 353 |
)
|
| 354 |
|
| 355 |
-
# Draw major (long) and minor (short) axes in blue
|
| 356 |
-
pts = box.astype(np.float32)
|
| 357 |
-
p0, p1, p2, p3 = pts
|
| 358 |
-
d01 = np.linalg.norm(p0 - p1)
|
| 359 |
-
d12 = np.linalg.norm(p1 - p2)
|
| 360 |
-
|
| 361 |
-
if d01 >= d12:
|
| 362 |
-
long_mid1 = (p0 + p1) / 2.0
|
| 363 |
-
long_mid2 = (p2 + p3) / 2.0
|
| 364 |
-
short_mid1 = (p1 + p2) / 2.0
|
| 365 |
-
short_mid2 = (p3 + p0) / 2.0
|
| 366 |
-
else:
|
| 367 |
-
long_mid1 = (p1 + p2) / 2.0
|
| 368 |
-
long_mid2 = (p3 + p0) / 2.0
|
| 369 |
-
short_mid1 = (p0 + p1) / 2.0
|
| 370 |
-
short_mid2 = (p2 + p3) / 2.0
|
| 371 |
-
|
| 372 |
-
cv2.line(out, tuple(long_mid1.astype(int)), tuple(long_mid2.astype(int)), (255, 0, 0), 2)
|
| 373 |
-
cv2.line(out, tuple(short_mid1.astype(int)), tuple(short_mid2.astype(int)), (255, 0, 0), 2)
|
| 374 |
-
|
| 375 |
return cv2.cvtColor(out, cv2.COLOR_BGR2RGB)
|
| 376 |
|
| 377 |
|
|
@@ -387,29 +560,65 @@ def analyze(
|
|
| 387 |
hsv_low_h: int,
|
| 388 |
hsv_high_h: int,
|
| 389 |
) -> Tuple[Optional[np.ndarray], pd.DataFrame, Optional[str], List[Dict[str, Any]], Dict[str, Any]]:
|
|
|
|
| 390 |
try:
|
| 391 |
if image is None:
|
| 392 |
return None, pd.DataFrame(), None, [], {}
|
|
|
|
|
|
|
| 393 |
img_rgb = np.array(image)
|
| 394 |
img_bgr = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2BGR)
|
|
|
|
|
|
|
| 395 |
img_bgr, scale = downscale_bgr(img_bgr)
|
|
|
|
|
|
|
| 396 |
px_per_mm, ref_center, ref_type, ref_bbox = detect_reference(img_bgr, ref_mode, ref_size_mm)
|
| 397 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 398 |
if sample_type == "leaves":
|
| 399 |
comps.sort(key=lambda c: c["center"][0])
|
| 400 |
else:
|
| 401 |
comps.sort(key=lambda c: c["center"][1] * 0.3 + c["center"][0] * 0.7)
|
|
|
|
|
|
|
| 402 |
if expected_count and expected_count > 0:
|
| 403 |
comps = comps[:int(expected_count)]
|
|
|
|
|
|
|
| 404 |
df = compute_metrics(img_bgr, comps, px_per_mm)
|
| 405 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 406 |
csv = df.to_csv(index=False)
|
| 407 |
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".csv")
|
| 408 |
tmp.write(csv.encode("utf-8"))
|
| 409 |
tmp.close()
|
|
|
|
|
|
|
| 410 |
js = df.to_dict(orient="records")
|
| 411 |
-
|
| 412 |
-
#
|
| 413 |
state_dict: Dict[str, Any] = {
|
| 414 |
"img_bgr": img_bgr,
|
| 415 |
"sample_type": sample_type,
|
|
@@ -421,7 +630,7 @@ def analyze(
|
|
| 421 |
"expected_count": expected_count,
|
| 422 |
"ref_size_mm": ref_size_mm,
|
| 423 |
}
|
| 424 |
-
#
|
| 425 |
state_dict["active_indices"] = list(range(len(comps)))
|
| 426 |
|
| 427 |
return overlay, df, tmp.name, js, state_dict
|
|
@@ -437,7 +646,6 @@ def apply_corrections(
|
|
| 437 |
) -> Tuple[Dict[str, Any], Optional[np.ndarray], pd.DataFrame, Optional[str], List[Dict[str, Any]]]:
|
| 438 |
"""
|
| 439 |
Apply interactive corrections based on a click on the annotated image.
|
| 440 |
-
|
| 441 |
correction_mode:
|
| 442 |
- "none": do nothing
|
| 443 |
- "set-ref": treat the clicked object as the new reference
|
|
@@ -575,6 +783,7 @@ with gr.Blocks(theme=gr.themes.Default()) as demo:
|
|
| 575 |
table = gr.Dataframe(label="Metrics", wrap=True)
|
| 576 |
csv_out = gr.File(label="CSV export")
|
| 577 |
json_out = gr.JSON(label="JSON preview")
|
|
|
|
| 578 |
def _analyze(image, sample_type, expected, ref_mode, ref_size, min_area, max_area, color_tol, hsv_low, hsv_high):
|
| 579 |
overlay_img, df, csv_path, js, state_dict = analyze(
|
| 580 |
image, sample_type, expected, ref_mode, ref_size, min_area, max_area, color_tol, hsv_low, hsv_high
|
|
|
|
| 11 |
# -----------------------------
|
| 12 |
|
| 13 |
# Maximum allowed image side (pixels) to avoid OOM / heavy CPU usage
|
| 14 |
+
# Reduced from 2048 to 1024 for better performance (as in demo.py)
|
| 15 |
+
MAX_SIDE = 1024
|
| 16 |
|
| 17 |
|
| 18 |
# -----------------------------
|
|
|
|
| 21 |
|
| 22 |
def downscale_bgr(img: np.ndarray) -> Tuple[np.ndarray, float]:
|
| 23 |
"""Downscale image so that the longest side is <= MAX_SIDE.
|
| 24 |
+
|
| 25 |
Returns
|
| 26 |
-------
|
| 27 |
img_resized : np.ndarray
|
|
|
|
| 40 |
|
| 41 |
def normalize_angle(angle: float, size_w: float, size_h: float) -> float:
|
| 42 |
"""Normalize OpenCV minAreaRect angle to [0, 180) degrees.
|
| 43 |
+
|
| 44 |
OpenCV returns angles depending on whether width < height. We fix it so that
|
| 45 |
the *long side* is treated as length and angle is always in [0, 180).
|
| 46 |
"""
|
|
|
|
| 55 |
# Reference object detection
|
| 56 |
# -----------------------------
|
| 57 |
|
| 58 |
+
def build_foreground_mask(img_bgr: np.ndarray) -> np.ndarray:
|
| 59 |
+
"""简单的前景掩码构建(来自demo.py)"""
|
| 60 |
+
h, w = img_bgr.shape[:2]
|
| 61 |
+
|
| 62 |
+
# 转换到LAB颜色空间
|
| 63 |
+
lab = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2LAB)
|
| 64 |
+
|
| 65 |
+
# 使用四个角落估计背景颜色
|
| 66 |
+
corner_size = min(h, w) // 10
|
| 67 |
+
corners = [
|
| 68 |
+
lab[:corner_size, :corner_size],
|
| 69 |
+
lab[:corner_size, -corner_size:],
|
| 70 |
+
lab[-corner_size:, :corner_size],
|
| 71 |
+
lab[-corner_size:, -corner_size:]
|
| 72 |
+
]
|
| 73 |
+
corner_pixels = np.vstack([c.reshape(-1, 3) for c in corners])
|
| 74 |
+
bg_color = np.mean(corner_pixels, axis=0)
|
| 75 |
+
|
| 76 |
+
# 计算每个像素与背景的距离
|
| 77 |
+
diff = lab.astype(np.float32) - bg_color
|
| 78 |
+
dist = np.sqrt(np.sum(diff * diff, axis=2))
|
| 79 |
+
|
| 80 |
+
# 使用Otsu阈值分割
|
| 81 |
+
dist_uint8 = np.clip(dist * 3, 0, 255).astype(np.uint8)
|
| 82 |
+
_, mask = cv2.threshold(dist_uint8, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
|
| 83 |
+
|
| 84 |
+
# 形态学处理
|
| 85 |
+
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
|
| 86 |
+
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
|
| 87 |
+
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
|
| 88 |
+
|
| 89 |
+
return mask
|
| 90 |
+
|
| 91 |
+
|
| 92 |
def detect_reference(
|
| 93 |
img_bgr: np.ndarray,
|
| 94 |
mode: str,
|
| 95 |
ref_size_mm: Optional[float],
|
| 96 |
) -> Tuple[float, Optional[Tuple[int, int]], Optional[str], Optional[Tuple[int, int, int, int]]]:
|
| 97 |
+
"""检测参考物:左上角第一个物体(简化版)
|
| 98 |
+
|
| 99 |
+
参数:
|
| 100 |
+
img_bgr: BGR图像
|
| 101 |
+
mode: 参考物模式 ("auto", "coin", "square")
|
| 102 |
+
ref_size_mm: 参考物包围框边长(毫米)
|
| 103 |
+
|
| 104 |
+
返回:
|
| 105 |
+
px_per_mm: 像素/毫米比例
|
| 106 |
+
ref_center: 参考物中心
|
| 107 |
+
ref_type: 参考物类型
|
| 108 |
+
ref_bbox: 参考物外接矩形
|
| 109 |
"""
|
| 110 |
h, w = img_bgr.shape[:2]
|
| 111 |
|
| 112 |
+
# 使用简单的前景掩码
|
| 113 |
+
mask = build_foreground_mask(img_bgr)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
|
| 115 |
+
# 连通域分析
|
| 116 |
num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(mask)
|
| 117 |
|
| 118 |
+
# 寻找左上角的参考物
|
| 119 |
candidates = []
|
| 120 |
+
min_area = (h * w) // 500 # 最小面积
|
| 121 |
+
max_area = (h * w) // 20 # 最大面积
|
| 122 |
+
|
| 123 |
+
for i in range(1, num_labels):
|
| 124 |
x, y, ww, hh, area = stats[i]
|
| 125 |
+
|
| 126 |
+
# 面积过滤
|
| 127 |
+
if area < min_area or area > max_area:
|
| 128 |
continue
|
| 129 |
+
|
| 130 |
+
# 位置过滤:必须在左上角区域
|
| 131 |
+
if x > w * 0.4 or y > h * 0.4:
|
| 132 |
continue
|
| 133 |
+
|
| 134 |
+
# 形状过滤:参考物应该接近正方形
|
| 135 |
+
aspect_ratio = max(ww, hh) / (min(ww, hh) + 1e-6)
|
| 136 |
+
if aspect_ratio > 3.0:
|
| 137 |
continue
|
| 138 |
|
| 139 |
+
cx, cy = centroids[i]
|
| 140 |
+
# 按位置排序:越靠近左上角越好
|
| 141 |
+
score = x + y
|
| 142 |
+
candidates.append((score, i, (x, y, ww, hh), area, (int(cx), int(cy))))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
|
| 144 |
+
if not candidates:
|
| 145 |
+
# 如果没有找到参考物,使用安全的默认值
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
px_per_mm = 4.0
|
| 147 |
center = None
|
| 148 |
ref_type = None
|
| 149 |
bbox = None
|
| 150 |
+
return px_per_mm, center, ref_type, bbox
|
| 151 |
|
| 152 |
+
# 选择最左上角的候选物
|
| 153 |
+
candidates.sort(key=lambda c: c[0])
|
| 154 |
+
score, label_idx, bbox, area, center = candidates[0]
|
| 155 |
+
|
| 156 |
+
x, y, ww, hh = bbox
|
| 157 |
+
|
| 158 |
+
# 计算像素/毫米比例
|
| 159 |
+
ref_size = ref_size_mm if ref_size_mm and ref_size_mm > 0 else 25.0
|
| 160 |
+
ref_bbox_size_px = max(ww, hh)
|
| 161 |
+
px_per_mm = ref_bbox_size_px / ref_size
|
| 162 |
+
|
| 163 |
+
return px_per_mm, center, "square", (x, y, ww, hh)
|
| 164 |
|
| 165 |
|
| 166 |
# -----------------------------
|
|
|
|
| 174 |
hsv_high_h: int,
|
| 175 |
color_tol: int,
|
| 176 |
) -> np.ndarray:
|
| 177 |
+
"""Build binary mask using HSV thresholds."""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
hsv = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV)
|
| 179 |
h_channel = hsv[:, :, 0]
|
| 180 |
s_channel = hsv[:, :, 1]
|
|
|
|
| 206 |
sample_type: str,
|
| 207 |
hsv_low_h: int,
|
| 208 |
hsv_high_h: int,
|
| 209 |
+
color_tol: int,
|
| 210 |
min_area_px: float,
|
| 211 |
max_area_px: float,
|
| 212 |
) -> List[Dict[str, Any]]:
|
| 213 |
"""Segment objects and compute basic geometric descriptors.
|
| 214 |
+
采用demo.py的简化分割算法,但保留HSV参数兼容性
|
|
|
|
| 215 |
"""
|
| 216 |
+
# 使用简单的前景掩码(demo.py方法)
|
| 217 |
+
mask = build_foreground_mask(img_bgr)
|
| 218 |
+
|
| 219 |
+
# 连通域分析
|
| 220 |
+
num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(mask)
|
| 221 |
+
|
| 222 |
components: List[Dict[str, Any]] = []
|
| 223 |
+
|
| 224 |
+
# 按位置排序,跳过第一个(通常是参考物)
|
| 225 |
+
all_objects = []
|
| 226 |
+
for i in range(1, num_labels):
|
| 227 |
+
x, y, ww, hh, area = stats[i]
|
| 228 |
+
|
| 229 |
+
# 面积过滤
|
| 230 |
+
if area < min_area_px or area > max_area_px:
|
| 231 |
continue
|
| 232 |
+
|
| 233 |
+
cx, cy = centroids[i]
|
| 234 |
+
# 简单的位置评分:从左到右
|
| 235 |
+
score = x + y * 0.1 # 优先考虑x坐标
|
| 236 |
+
all_objects.append((score, i, (x, y, ww, hh), area, (int(cx), int(cy))))
|
| 237 |
+
|
| 238 |
+
if len(all_objects) == 0:
|
| 239 |
+
return []
|
| 240 |
+
|
| 241 |
+
# 排序并跳过第一个(参考物)
|
| 242 |
+
all_objects.sort(key=lambda obj: obj[0])
|
| 243 |
+
|
| 244 |
+
# 简单判断是否跳过第一个对象
|
| 245 |
+
skip_first = False
|
| 246 |
+
if len(all_objects) > 0:
|
| 247 |
+
_, _, (x, y, ww, hh), area, _ = all_objects[0]
|
| 248 |
+
h, w = img_bgr.shape[:2]
|
| 249 |
+
|
| 250 |
+
# 如果第一个对象在左上角且形状合理,跳过它
|
| 251 |
+
is_topleft = (x < w * 0.3 and y < h * 0.3)
|
| 252 |
+
aspect_ratio = max(ww, hh) / (min(ww, hh) + 1e-6)
|
| 253 |
+
is_reasonable_shape = aspect_ratio < 3.0
|
| 254 |
+
|
| 255 |
+
skip_first = is_topleft and is_reasonable_shape
|
| 256 |
+
|
| 257 |
+
# 处理对象
|
| 258 |
+
start_idx = 1 if skip_first else 0
|
| 259 |
+
for obj_data in all_objects[start_idx:]:
|
| 260 |
+
_, label_idx, bbox, area, center = obj_data
|
| 261 |
+
|
| 262 |
+
# 提取轮廓
|
| 263 |
+
component_mask = (labels == label_idx).astype(np.uint8) * 255
|
| 264 |
+
cnts, _ = cv2.findContours(component_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
| 265 |
+
|
| 266 |
+
if len(cnts) == 0:
|
| 267 |
+
continue
|
| 268 |
+
|
| 269 |
+
cnt = cnts[0]
|
| 270 |
+
|
| 271 |
+
# 计算几何特征
|
| 272 |
rect = cv2.minAreaRect(cnt)
|
| 273 |
box = cv2.boxPoints(rect).astype(np.int32)
|
| 274 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 275 |
peri = cv2.arcLength(cnt, True)
|
| 276 |
+
|
| 277 |
+
# 修复OpenCV minAreaRect的长短轴对应问题(使用PCA)
|
| 278 |
+
# 提取轮廓点
|
| 279 |
+
contour_points = cnt.reshape(-1, 2).astype(np.float32)
|
| 280 |
+
|
| 281 |
+
# 计算质心
|
| 282 |
+
cx = np.mean(contour_points[:, 0])
|
| 283 |
+
cy = np.mean(contour_points[:, 1])
|
| 284 |
+
|
| 285 |
+
# 计算协方差矩阵
|
| 286 |
+
centered_points = contour_points - np.array([cx, cy])
|
| 287 |
+
cov_matrix = np.cov(centered_points.T)
|
| 288 |
+
|
| 289 |
+
# 计算特征值和特征向量
|
| 290 |
+
eigenvalues, eigenvectors = np.linalg.eigh(cov_matrix)
|
| 291 |
+
|
| 292 |
+
# 按特征值大小排序(降序)
|
| 293 |
+
idx = np.argsort(eigenvalues)[::-1]
|
| 294 |
+
eigenvalues = eigenvalues[idx]
|
| 295 |
+
eigenvectors = eigenvectors[:, idx]
|
| 296 |
+
|
| 297 |
+
# 主方向(最大特征值对应的特征向量)
|
| 298 |
+
main_direction = eigenvectors[:, 0]
|
| 299 |
+
|
| 300 |
+
# 投影到主方向和次方向
|
| 301 |
+
proj_main = np.dot(centered_points, main_direction)
|
| 302 |
+
proj_secondary = np.dot(centered_points, eigenvectors[:, 1])
|
| 303 |
+
|
| 304 |
+
# 计算投影边界
|
| 305 |
+
min_main = np.min(proj_main)
|
| 306 |
+
max_main = np.max(proj_main)
|
| 307 |
+
min_secondary = np.min(proj_secondary)
|
| 308 |
+
max_secondary = np.max(proj_secondary)
|
| 309 |
+
|
| 310 |
+
# 计算真实的长短轴长度
|
| 311 |
+
length_main = max_main - min_main
|
| 312 |
+
length_secondary = max_secondary - min_secondary
|
| 313 |
+
|
| 314 |
+
# 确保长轴对应较长的方向,并保存正确的投影边界
|
| 315 |
+
if length_main >= length_secondary:
|
| 316 |
+
w_obb = length_main
|
| 317 |
+
h_obb = length_secondary
|
| 318 |
+
angle = np.arctan2(main_direction[1], main_direction[0]) * 180.0 / np.pi
|
| 319 |
+
# 长轴是主方向
|
| 320 |
+
long_direction = main_direction
|
| 321 |
+
short_direction = eigenvectors[:, 1]
|
| 322 |
+
min_long_proj = min_main
|
| 323 |
+
max_long_proj = max_main
|
| 324 |
+
min_short_proj = min_secondary
|
| 325 |
+
max_short_proj = max_secondary
|
| 326 |
+
else:
|
| 327 |
+
w_obb = length_secondary
|
| 328 |
+
h_obb = length_main
|
| 329 |
+
secondary_direction = eigenvectors[:, 1]
|
| 330 |
+
angle = np.arctan2(secondary_direction[1], secondary_direction[0]) * 180.0 / np.pi
|
| 331 |
+
# 长轴是次方向
|
| 332 |
+
long_direction = eigenvectors[:, 1]
|
| 333 |
+
short_direction = main_direction
|
| 334 |
+
min_long_proj = min_secondary
|
| 335 |
+
max_long_proj = max_secondary
|
| 336 |
+
min_short_proj = min_main
|
| 337 |
+
max_short_proj = max_main
|
| 338 |
+
|
| 339 |
+
# 标准化角度到[0, 180)
|
| 340 |
+
angle = ((angle % 180.0) + 180.0) % 180.0
|
| 341 |
+
|
| 342 |
+
components.append({
|
| 343 |
+
"contour": cnt,
|
| 344 |
+
"rect": rect,
|
| 345 |
+
"box": box,
|
| 346 |
+
"area_px": float(area),
|
| 347 |
+
"peri_px": float(peri),
|
| 348 |
+
"center": (int(cx), int(cy)), # 使用PCA计算的质心
|
| 349 |
+
"pca_center": (cx, cy), # 保存精确的PCA质心
|
| 350 |
+
"angle": float(angle),
|
| 351 |
+
"length_px": float(w_obb),
|
| 352 |
+
"width_px": float(h_obb),
|
| 353 |
+
# 保存投影边界信息用于正确的包围框绘制
|
| 354 |
+
"min_long_proj": float(min_long_proj),
|
| 355 |
+
"max_long_proj": float(max_long_proj),
|
| 356 |
+
"min_short_proj": float(min_short_proj),
|
| 357 |
+
"max_short_proj": float(max_short_proj),
|
| 358 |
+
})
|
| 359 |
+
|
| 360 |
return components
|
| 361 |
|
| 362 |
|
|
|
|
| 384 |
rows: List[Dict[str, Any]] = []
|
| 385 |
|
| 386 |
for i, comp in enumerate(components, start=1):
|
| 387 |
+
# 使用新的length_px和width_px字段
|
| 388 |
+
length_mm = comp["length_px"] / px_per_mm
|
| 389 |
+
width_mm = comp["width_px"] / px_per_mm
|
|
|
|
|
|
|
| 390 |
area_mm2 = comp["area_px"] / (px_per_mm * px_per_mm)
|
| 391 |
perimeter_mm = comp["peri_px"] / px_per_mm
|
| 392 |
|
| 393 |
aspect_ratio = length_mm / (width_mm + 1e-6)
|
| 394 |
+
|
| 395 |
+
# 计算圆形度 (4π*面积/周长²)
|
| 396 |
circularity = (4.0 * np.pi * area_mm2) / (perimeter_mm * perimeter_mm + 1e-6)
|
| 397 |
|
| 398 |
+
# 计算颜色指标
|
| 399 |
mask_single = np.zeros(img_bgr.shape[:2], dtype=np.uint8)
|
| 400 |
cv2.drawContours(mask_single, [comp["contour"]], -1, 255, thickness=-1)
|
| 401 |
mean_r, mean_g, mean_b, h, s, v, gi, bi = compute_color_metrics(img_bgr, mask_single)
|
|
|
|
| 436 |
df: pd.DataFrame,
|
| 437 |
ref_bbox: Optional[Tuple[int, int, int, int]] = None,
|
| 438 |
) -> np.ndarray:
|
| 439 |
+
"""Draw reference + sample annotations on the image.
|
| 440 |
+
采用demo.py的清晰可视化方法
|
| 441 |
+
"""
|
| 442 |
out = img_bgr.copy()
|
| 443 |
|
| 444 |
+
# 绘制参考物(红色矩形框)
|
| 445 |
ref_center, ref_type = ref
|
| 446 |
if ref_bbox is not None:
|
| 447 |
x, y, w, h = ref_bbox
|
|
|
|
| 448 |
cv2.rectangle(out, (int(x), int(y)), (int(x + w), int(y + h)), (0, 0, 255), 2)
|
| 449 |
cv2.putText(
|
| 450 |
out,
|
|
|
|
| 457 |
cv2.LINE_AA,
|
| 458 |
)
|
| 459 |
|
| 460 |
+
# 绘制样品物体(完整标注)
|
| 461 |
for i, comp in enumerate(components, start=1):
|
| 462 |
+
# 1. 绘制完整轮廓(蓝色,加粗)
|
| 463 |
+
cv2.drawContours(out, [comp["contour"]], -1, (255, 0, 0), 3)
|
| 464 |
+
|
| 465 |
+
# 2. 绘制修正后的OBB包围框
|
| 466 |
+
# 使用PCA计算的精确质心
|
| 467 |
+
cx, cy = comp["pca_center"]
|
| 468 |
+
length_px = comp["length_px"] # 长轴长度
|
| 469 |
+
width_px = comp["width_px"] # 短轴长度
|
| 470 |
+
angle_deg = comp["angle"] # 长轴角度(度)
|
| 471 |
+
|
| 472 |
+
# 转换为弧度
|
| 473 |
+
angle_rad = np.radians(angle_deg)
|
| 474 |
+
|
| 475 |
+
# 使用实际的投影边界构建包围框
|
| 476 |
+
corners = []
|
| 477 |
+
|
| 478 |
+
# 获取保存的投影边界
|
| 479 |
+
min_long_proj = comp["min_long_proj"]
|
| 480 |
+
max_long_proj = comp["max_long_proj"]
|
| 481 |
+
min_short_proj = comp["min_short_proj"]
|
| 482 |
+
max_short_proj = comp["max_short_proj"]
|
| 483 |
+
|
| 484 |
+
# 获取长轴和短轴方向向量
|
| 485 |
+
long_dir = np.array([np.cos(angle_rad), np.sin(angle_rad)])
|
| 486 |
+
short_dir = np.array([-np.sin(angle_rad), np.cos(angle_rad)])
|
| 487 |
+
|
| 488 |
+
# 使用实际投影边界构建包围框的四个角点
|
| 489 |
+
for long_proj, short_proj in [(max_long_proj, max_short_proj), # 右上
|
| 490 |
+
(min_long_proj, max_short_proj), # 左上
|
| 491 |
+
(min_long_proj, min_short_proj), # 左下
|
| 492 |
+
(max_long_proj, min_short_proj)]: # 右下
|
| 493 |
+
# 从质心出发,沿长轴和短轴方向移动到角点
|
| 494 |
+
corner_point = np.array([cx, cy]) + long_proj * long_dir + short_proj * short_dir
|
| 495 |
+
corners.append([int(corner_point[0]), int(corner_point[1])])
|
| 496 |
+
|
| 497 |
+
# 绘制OBB包围框
|
| 498 |
+
corners = np.array(corners, dtype=np.int32)
|
| 499 |
+
cv2.drawContours(out, [corners], -1, (255, 0, 0), 2)
|
| 500 |
+
|
| 501 |
+
# 3. 绘制长短轴(包围框的边界线)
|
| 502 |
+
# 计算包围框各边的中点
|
| 503 |
+
edge_mids = []
|
| 504 |
+
for edge_idx in range(4):
|
| 505 |
+
next_edge_idx = (edge_idx + 1) % 4
|
| 506 |
+
mid_x = (corners[edge_idx][0] + corners[next_edge_idx][0]) / 2
|
| 507 |
+
mid_y = (corners[edge_idx][1] + corners[next_edge_idx][1]) / 2
|
| 508 |
+
edge_mids.append((int(mid_x), int(mid_y)))
|
| 509 |
+
|
| 510 |
+
# 计算各边的长度来确定哪条是长边
|
| 511 |
+
edge_lengths = []
|
| 512 |
+
for edge_idx in range(4):
|
| 513 |
+
next_edge_idx = (edge_idx + 1) % 4
|
| 514 |
+
length = np.sqrt((corners[next_edge_idx][0] - corners[edge_idx][0])**2 + (corners[next_edge_idx][1] - corners[edge_idx][1])**2)
|
| 515 |
+
edge_lengths.append(length)
|
| 516 |
+
|
| 517 |
+
# 找到最长的边
|
| 518 |
+
max_edge_idx = np.argmax(edge_lengths)
|
| 519 |
+
opposite_edge_idx = (max_edge_idx + 2) % 4
|
| 520 |
+
|
| 521 |
+
# 绘制长轴(连接最长边的中点和对边中点)
|
| 522 |
+
long_mid1 = edge_mids[max_edge_idx]
|
| 523 |
+
long_mid2 = edge_mids[opposite_edge_idx]
|
| 524 |
+
cv2.line(out, long_mid1, long_mid2, (255, 0, 0), 3)
|
| 525 |
+
|
| 526 |
+
# 绘制短轴(连接另外两边的中点)
|
| 527 |
+
short_edge1_idx = (max_edge_idx + 1) % 4
|
| 528 |
+
short_edge2_idx = (max_edge_idx + 3) % 4
|
| 529 |
+
short_mid1 = edge_mids[short_edge1_idx]
|
| 530 |
+
short_mid2 = edge_mids[short_edge2_idx]
|
| 531 |
+
cv2.line(out, short_mid1, short_mid2, (255, 0, 0), 2)
|
| 532 |
+
|
| 533 |
+
# 4. 绘制中心点和标签
|
| 534 |
+
# 使用PCA计算的精确质心
|
| 535 |
+
label_cx, label_cy = comp["pca_center"]
|
| 536 |
+
cv2.circle(out, (int(label_cx), int(label_cy)), 15, (0, 0, 0), -1)
|
| 537 |
cv2.putText(
|
| 538 |
out,
|
| 539 |
f"s{i}",
|
| 540 |
+
(int(label_cx) - 10, int(label_cy) + 5),
|
| 541 |
cv2.FONT_HERSHEY_SIMPLEX,
|
| 542 |
0.5,
|
| 543 |
(255, 255, 255),
|
| 544 |
+
2,
|
| 545 |
cv2.LINE_AA,
|
| 546 |
)
|
| 547 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 548 |
return cv2.cvtColor(out, cv2.COLOR_BGR2RGB)
|
| 549 |
|
| 550 |
|
|
|
|
| 560 |
hsv_low_h: int,
|
| 561 |
hsv_high_h: int,
|
| 562 |
) -> Tuple[Optional[np.ndarray], pd.DataFrame, Optional[str], List[Dict[str, Any]], Dict[str, Any]]:
|
| 563 |
+
"""主分析函数,整合demo.py的优化算法"""
|
| 564 |
try:
|
| 565 |
if image is None:
|
| 566 |
return None, pd.DataFrame(), None, [], {}
|
| 567 |
+
|
| 568 |
+
# 转换为BGR
|
| 569 |
img_rgb = np.array(image)
|
| 570 |
img_bgr = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2BGR)
|
| 571 |
+
|
| 572 |
+
# 适度降采样
|
| 573 |
img_bgr, scale = downscale_bgr(img_bgr)
|
| 574 |
+
|
| 575 |
+
# 检测参考物(左上角第一个物体)
|
| 576 |
px_per_mm, ref_center, ref_type, ref_bbox = detect_reference(img_bgr, ref_mode, ref_size_mm)
|
| 577 |
+
|
| 578 |
+
# 分割所有样品
|
| 579 |
+
comps = segment(
|
| 580 |
+
img_bgr,
|
| 581 |
+
sample_type=sample_type,
|
| 582 |
+
hsv_low_h=hsv_low_h,
|
| 583 |
+
hsv_high_h=hsv_high_h,
|
| 584 |
+
color_tol=color_tol,
|
| 585 |
+
min_area_px=min_area_px,
|
| 586 |
+
max_area_px=max_area_px,
|
| 587 |
+
)
|
| 588 |
+
|
| 589 |
+
# 根据样品类型排序
|
| 590 |
if sample_type == "leaves":
|
| 591 |
comps.sort(key=lambda c: c["center"][0])
|
| 592 |
else:
|
| 593 |
comps.sort(key=lambda c: c["center"][1] * 0.3 + c["center"][0] * 0.7)
|
| 594 |
+
|
| 595 |
+
# 限制数量
|
| 596 |
if expected_count and expected_count > 0:
|
| 597 |
comps = comps[:int(expected_count)]
|
| 598 |
+
|
| 599 |
+
# 计算测量指标
|
| 600 |
df = compute_metrics(img_bgr, comps, px_per_mm)
|
| 601 |
+
|
| 602 |
+
# 绘制标注图像
|
| 603 |
+
overlay = render_overlay(
|
| 604 |
+
img_bgr.copy(),
|
| 605 |
+
px_per_mm,
|
| 606 |
+
(ref_center, ref_type),
|
| 607 |
+
comps,
|
| 608 |
+
df,
|
| 609 |
+
ref_bbox
|
| 610 |
+
)
|
| 611 |
+
|
| 612 |
+
# 保存CSV
|
| 613 |
csv = df.to_csv(index=False)
|
| 614 |
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".csv")
|
| 615 |
tmp.write(csv.encode("utf-8"))
|
| 616 |
tmp.close()
|
| 617 |
+
|
| 618 |
+
# 转换为JSON
|
| 619 |
js = df.to_dict(orient="records")
|
| 620 |
+
|
| 621 |
+
# 存储状态用于交互修正
|
| 622 |
state_dict: Dict[str, Any] = {
|
| 623 |
"img_bgr": img_bgr,
|
| 624 |
"sample_type": sample_type,
|
|
|
|
| 630 |
"expected_count": expected_count,
|
| 631 |
"ref_size_mm": ref_size_mm,
|
| 632 |
}
|
| 633 |
+
# 默认所有组件都是活跃样品
|
| 634 |
state_dict["active_indices"] = list(range(len(comps)))
|
| 635 |
|
| 636 |
return overlay, df, tmp.name, js, state_dict
|
|
|
|
| 646 |
) -> Tuple[Dict[str, Any], Optional[np.ndarray], pd.DataFrame, Optional[str], List[Dict[str, Any]]]:
|
| 647 |
"""
|
| 648 |
Apply interactive corrections based on a click on the annotated image.
|
|
|
|
| 649 |
correction_mode:
|
| 650 |
- "none": do nothing
|
| 651 |
- "set-ref": treat the clicked object as the new reference
|
|
|
|
| 783 |
table = gr.Dataframe(label="Metrics", wrap=True)
|
| 784 |
csv_out = gr.File(label="CSV export")
|
| 785 |
json_out = gr.JSON(label="JSON preview")
|
| 786 |
+
|
| 787 |
def _analyze(image, sample_type, expected, ref_mode, ref_size, min_area, max_area, color_tol, hsv_low, hsv_high):
|
| 788 |
overlay_img, df, csv_path, js, state_dict = analyze(
|
| 789 |
image, sample_type, expected, ref_mode, ref_size, min_area, max_area, color_tol, hsv_low, hsv_high
|