Diagram-Structure-Extractor / src /arrow_detection /pixel_scan_detector.py
IamBatman07's picture
Sync with dev 539d30a: draw.io accuracy (GitHub PR #18)
a4d7d23 verified
Raw History Blame Contribute Delete
5.16 kB
"""Scan-line dashed arrow detector β€” wraps the legacy `_find_dash_groups`
logic from `diagram_analysis.detect_arrows`.
Pure detector: returns endpoint coordinates only. Does NOT apply the
legacy `box_inside_text` / `inside_box` filters β€” those need texts + boxes
+ icons and would conflate the arrow-detector's quality with the box-detector's
quality. For Phase 2b we want to isolate the arrow detector itself.
Output format (shared across all arrow detectors):
{
"arrows": [{"x1", "y1", "x2", "y2", "direction"}],
"runtime_seconds": float,
}
"""
from __future__ import annotations
import time
from typing import List
import cv2
import numpy as np
def _find_dash_groups(line_pixels, dark_thresh=145, min_dash=3, max_dash=15,
min_gap=5, max_gap=16):
"""Identical to diagram_analysis._find_dash_groups β€” duplicated here so
the detector module is self-contained and can be re-tuned in Phase 4
without mutating the legacy file."""
n = len(line_pixels)
if n < 15:
return []
dark_runs = []
in_dark = False
start = 0
for i in range(n):
if line_pixels[i] < dark_thresh:
if not in_dark:
start = i
in_dark = True
else:
if in_dark:
dark_runs.append((start, i, i - start))
in_dark = False
if in_dark:
dark_runs.append((start, n, n - start))
dashes = [(s, e, l) for s, e, l in dark_runs if min_dash <= l <= max_dash]
if len(dashes) < 3:
return []
groups = [[dashes[0]]]
for k in range(1, len(dashes)):
gap = dashes[k][0] - dashes[k-1][1]
if gap > 30:
groups.append([dashes[k]])
else:
groups[-1].append(dashes[k])
result = []
for grp in groups:
if len(grp) < 2:
continue
good_gaps = sum(1 for k in range(1, len(grp))
if min_gap <= grp[k][0] - grp[k-1][1] <= max_gap)
if good_gaps >= 1:
result.append([(s, e) for s, e, _ in grp])
return result
def detect(image_path: str) -> dict:
img = cv2.imread(image_path)
if img is None:
raise FileNotFoundError(image_path)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
h_img, w_img = gray.shape
start = time.perf_counter()
# ── Horizontal scan ──
h_hits = []
for y in range(h_img):
for dashes in _find_dash_groups(gray[y, :].astype(int)):
x_start, x_end = dashes[0][0], dashes[-1][1]
span = x_end - x_start
if 20 <= span <= 800:
h_hits.append((y, x_start, x_end, len(dashes)))
h_hits.sort(key=lambda x: x[0])
h_arrows = []
if h_hits:
cluster = [h_hits[0]]
for hit in h_hits[1:]:
prev = cluster[-1]
if hit[0] - prev[0] <= 3 and abs(hit[1] - prev[1]) < 50:
cluster.append(hit)
else:
y_avg = int(np.mean([c[0] for c in cluster]))
x_min = min(c[1] for c in cluster)
x_max = max(c[2] for c in cluster)
h_arrows.append((x_min, y_avg, x_max, y_avg))
cluster = [hit]
y_avg = int(np.mean([c[0] for c in cluster]))
x_min = min(c[1] for c in cluster)
x_max = max(c[2] for c in cluster)
h_arrows.append((x_min, y_avg, x_max, y_avg))
# ── Vertical scan ──
v_hits = []
for x in range(w_img):
for dashes in _find_dash_groups(gray[:, x].astype(int)):
y_start, y_end = dashes[0][0], dashes[-1][1]
span = y_end - y_start
if 20 <= span <= 200:
v_hits.append((x, y_start, y_end, len(dashes)))
v_hits.sort(key=lambda x: x[0])
v_arrows = []
if v_hits:
cluster = [v_hits[0]]
for hit in v_hits[1:]:
prev = cluster[-1]
if hit[0] - prev[0] <= 3 and abs(hit[1] - prev[1]) < 50:
cluster.append(hit)
else:
x_avg = int(np.mean([c[0] for c in cluster]))
y_min = min(c[1] for c in cluster)
y_max = max(c[2] for c in cluster)
v_arrows.append((x_avg, y_min, x_avg, y_max))
cluster = [hit]
x_avg = int(np.mean([c[0] for c in cluster]))
y_min = min(c[1] for c in cluster)
y_max = max(c[2] for c in cluster)
v_arrows.append((x_avg, y_min, x_avg, y_max))
# ── Light filters: drop tiny v-arrows + dedupe near-duplicates ──
v_arrows = [a for a in v_arrows if (a[3] - a[1]) >= 25]
arrows: List[dict] = []
for x1, y1, x2, y2 in h_arrows:
arrows.append({
"x1": int(x1), "y1": int(y1),
"x2": int(x2), "y2": int(y2),
"direction": "horizontal",
})
for x1, y1, x2, y2 in v_arrows:
arrows.append({
"x1": int(x1), "y1": int(y1),
"x2": int(x2), "y2": int(y2),
"direction": "vertical",
})
elapsed = time.perf_counter() - start
return {"arrows": arrows, "runtime_seconds": round(elapsed, 3)}