diff --git a/src/atm/vision.py b/src/atm/vision.py index fa9f378..2a1016c 100644 --- a/src/atm/vision.py +++ b/src/atm/vision.py @@ -101,30 +101,27 @@ def find_rightmost_dot( """ bgr_bg = np.array([bg_rgb[2], bg_rgb[1], bg_rgb[0]], dtype=np.float32) diff = np.linalg.norm(roi_img.astype(np.float32) - bgr_bg, axis=2) - mask = diff > bg_tol # True = non-background - _h, w = mask.shape - col_counts = mask.sum(axis=0) + mask = (diff > bg_tol).astype(np.uint8) - right_col = None - for x in range(w - 1, -1, -1): - if col_counts[x] >= min_cluster_px: - right_col = x - break - if right_col is None: + # Connected components → one component per dot. Anti-aliasing bridges + # between adjacent dots are small enough that 8-connectivity still + # separates them cleanly. + n_labels, _labels, stats, centroids = cv2.connectedComponentsWithStats( + mask, connectivity=8, + ) + best_idx: int | None = None + best_right = -1 + for i in range(1, n_labels): # skip background (label 0) + if int(stats[i, cv2.CC_STAT_AREA]) < min_cluster_px: + continue + right = int(stats[i, cv2.CC_STAT_LEFT] + stats[i, cv2.CC_STAT_WIDTH]) + if right > best_right: + best_right = right + best_idx = i + if best_idx is None: return None - - # Walk LEFT until a gap to find the cluster's left edge. - left_col = right_col - for x in range(right_col - 1, -1, -1): - if col_counts[x] < min_cluster_px: - break - left_col = x - - cx = (left_col + right_col) // 2 - cluster = mask[:, left_col:right_col + 1] - ys = np.where(cluster.any(axis=1))[0] - cy = int(ys.mean()) if ys.size > 0 else 0 - return (cx, cy) + cx, cy = centroids[best_idx] + return (int(cx), int(cy)) def pixel_rgb(roi_img: np.ndarray, x: int, y: int, box: int = 3) -> tuple[int, int, int]: