from typing import Any, Dict, Tuple import cv2 import numpy as np from .common import to_bgr, to_rgb def detect_classical( image: np.ndarray, detector: str, canny_low: int, canny_high: int, harris_k: float, harris_block: int, harris_ksize: int, hough_thresh: int, hough_min_len: int, hough_max_gap: int, ellipse_min_area: int, max_ellipses: int, line_detector: str = "hough", ) -> Tuple[np.ndarray, Dict[str, Any]]: bgr = to_bgr(image) gray = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY) overlay = bgr.copy() meta: Dict[str, Any] = {"path": "classical"} if detector == "Edges (Canny)": edges = cv2.Canny(gray, canny_low, canny_high, L2gradient=True) overlay[edges > 0] = (0, 255, 0) meta["num_edge_pixels"] = int(np.count_nonzero(edges)) elif detector == "Corners (Harris)": gray32 = np.float32(gray) dst = cv2.cornerHarris(gray32, blockSize=harris_block, ksize=harris_ksize, k=harris_k) dst = cv2.dilate(dst, None) thresh = 0.01 * dst.max() if dst.max() > 0 else 0.0 corners = np.argwhere(dst > thresh) for (y, x) in corners: cv2.circle(overlay, (int(x), int(y)), 2, (0, 255, 255), -1) meta["num_corners"] = int(len(corners)) elif detector == "Lines (Hough/LSD)": method = (line_detector or "hough").lower() if method not in {"hough", "lsd"}: method = "hough" meta["line_detector"] = method if method == "lsd": if not hasattr(cv2, "createLineSegmentDetector"): meta["error"] = "OpenCV build lacks Line Segment Detector (LSD) support." return to_rgb(overlay), meta lsd = cv2.createLineSegmentDetector(refine=cv2.LSD_REFINE_ADV) lines = lsd.detect(gray)[0] n = 0 if lines is not None: for seg in lines: x1, y1, x2, y2 = map(int, np.round(seg[0])) cv2.line(overlay, (x1, y1), (x2, y2), (0, 255, 255), 2) n = len(lines) meta["num_lines"] = int(n) else: edges = cv2.Canny(gray, canny_low, canny_high, L2gradient=True) lines = cv2.HoughLinesP( edges, rho=1, theta=np.pi / 180, threshold=hough_thresh, minLineLength=hough_min_len, maxLineGap=hough_max_gap, ) n = 0 if lines is not None: for l in lines: x1, y1, x2, y2 = l[0] cv2.line(overlay, (x1, y1), (x2, y2), (255, 128, 0), 2) n = len(lines) meta["num_lines"] = int(n) elif detector == "Ellipses (Contours + fitEllipse)": edges = cv2.Canny(gray, canny_low, canny_high, L2gradient=True) contours, _ = cv2.findContours(edges, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE) ellipses = [] for cnt in contours: if len(cnt) < 5: continue try: (cx, cy), (MA, ma), angle = cv2.fitEllipse(cnt) area = float(np.pi * (MA / 2) * (ma / 2)) if area >= ellipse_min_area: ellipses.append(((cx, cy), (MA, ma), angle, area)) except cv2.error: continue ellipses.sort(key=lambda e: e[3], reverse=True) kept = [] for e in ellipses: if len(kept) >= max_ellipses: break (cx, cy), (MA, ma), angle, area = e if all((cx - kx) ** 2 + (cy - ky) ** 2 > 100 for ((kx, ky), _, _, _) in kept): kept.append(e) for (cx, cy), (MA, ma), angle, area in kept: cv2.ellipse(overlay, ((int(cx), int(cy)), (int(MA), int(ma)), float(angle)), (0, 200, 255), 2) cv2.circle(overlay, (int(cx), int(cy)), 2, (0, 200, 255), -1) meta["num_ellipses"] = int(len(kept)) else: meta["error"] = f"Unknown detector: {detector}" return to_rgb(overlay), meta