FINGERQUALITYAPI / finger_detector.py
sol9x-sagar's picture
initial setup
e735bf3
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)