from typing import Optional, Tuple import cv2 import numpy as np class FingerDetector: """ Skin-based finger segmentation + largest contour extraction. """ def __init__(self, min_contour_area_ratio: float = 0.02): self.min_contour_area_ratio = min_contour_area_ratio @staticmethod def segment_skin_ycbcr(img_bgr: np.ndarray) -> np.ndarray: """ Returns binary mask (uint8 0/255) using YCbCr thresholds. Commonly used range: 77≤Cb≤127 and 133≤Cr≤173. [page:2] """ ycrcb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2YCrCb) # OpenCV order is Y, Cr, Cb in COLOR_BGR2YCrCb lower = np.array([0, 133, 77], dtype=np.uint8) upper = np.array([255, 173, 127], dtype=np.uint8) mask = cv2.inRange(ycrcb, lower, upper) kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel, iterations=2) mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations=2) return mask def find_largest_contour( self, mask: np.ndarray, frame_area: float ) -> Optional[np.ndarray]: contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return None min_area = self.min_contour_area_ratio * frame_area valid = [c for c in contours if cv2.contourArea(c) >= min_area] if not valid: return None return max(valid, key=cv2.contourArea) @staticmethod def bounding_box(contour: np.ndarray) -> Tuple[int, int, int, int]: x, y, w, h = cv2.boundingRect(contour) return x, y, w, h @staticmethod def orientation_pca_deg(contour: np.ndarray) -> float: pts = contour.reshape(-1, 2).astype(np.float64) mean, eigenvectors, _ = cv2.PCACompute2(pts, mean=np.empty(0)) vx, vy = eigenvectors[0] angle_rad = np.arctan2(vy, vx) angle_deg = float(np.degrees(angle_rad)) # Normalize to [-90, 90] if angle_deg < -90: angle_deg += 180 elif angle_deg > 90: angle_deg -= 180 return float(angle_deg)