File size: 2,223 Bytes
e735bf3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
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)