Spaces:
Running
Running
| 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 | |
| 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) | |
| def bounding_box(contour: np.ndarray) -> Tuple[int, int, int, int]: | |
| x, y, w, h = cv2.boundingRect(contour) | |
| return x, y, w, h | |
| 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) | |