DeepNAPSI / nail_detection.py
lfolle's picture
Upload folder using huggingface_hub
d67f2dd verified
"""
Self-contained nail detection pipeline.
Inlined from hand-ki-model so that the HF Space has no git-clone dependency.
Source: nail_detection/{main,hand_detection,extract_nails}.py + utils/{rotate,polygon,angle,valid_crop,draw_hand}.py
"""
from __future__ import annotations
import math
from typing import List
import cv2
import mediapipe as mp
import numpy as np
from PIL import Image, ImageDraw
from scipy import ndimage
# ---------------------------------------------------------------------------
# Geometry helpers (from utils/)
# ---------------------------------------------------------------------------
def _rotate(origin, point, angle: float):
"""Rotate *point* counter-clockwise by *angle* (radians) around *origin*."""
ox, oy = origin
px, py = point
qx = ox + math.cos(angle) * (px - ox) - math.sin(angle) * (py - oy)
qy = oy + math.sin(angle) * (px - ox) + math.cos(angle) * (py - oy)
return qx, qy
def _unit_vector(v):
return v / np.linalg.norm(v)
def _angle_between(v1, v2) -> float:
return float(np.arccos(np.clip(np.dot(_unit_vector(v1), _unit_vector(v2)), -1.0, 1.0)))
def _get_polygon_mask(width: int, height: int, polygon_idx) -> np.ndarray:
polygon_idx = [tuple(xy) for xy in polygon_idx]
img = Image.new("L", (width, height), 0)
ImageDraw.Draw(img).polygon(polygon_idx, outline=1, fill=1)
return np.array(img)
def _valid_crop(image: np.ndarray, mask: np.ndarray, offset: int = 10):
true_points = np.argwhere(mask)
top_left = true_points.min(axis=0)
bottom_right = true_points.max(axis=0)
x_low = max(top_left[0] - offset, 0)
x_high = min(bottom_right[0] + offset, image.shape[0])
y_low = max(top_left[1] - offset, 0)
y_high = min(bottom_right[1] + offset + 1, image.shape[1])
return image[x_low:x_high, y_low:y_high], mask[x_low:x_high, y_low:y_high]
# ---------------------------------------------------------------------------
# Hand detection (from nail_detection/hand_detection.py)
# ---------------------------------------------------------------------------
def detect_hand(image: np.ndarray):
"""Return MediaPipe hand landmarks for the first detected hand, or None."""
mp_hands = mp.solutions.hands
with mp_hands.Hands(
static_image_mode=True, max_num_hands=1, min_detection_confidence=0.0
) as hands:
results = hands.process(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
if results.multi_handedness is None:
return None
return results.multi_hand_landmarks[0]
# ---------------------------------------------------------------------------
# Nail extraction (from nail_detection/extract_nails.py)
# ---------------------------------------------------------------------------
def extract_nails(image: np.ndarray, hand_landmarks) -> List[np.ndarray]:
"""Return list of 5 nail crop arrays (thumb → pinky), BGR."""
mp_hands = mp.solutions.hands
image_height, image_width, _ = image.shape
nails: List[np.ndarray] = []
for tip in [
mp_hands.HandLandmark.THUMB_TIP,
mp_hands.HandLandmark.INDEX_FINGER_TIP,
mp_hands.HandLandmark.MIDDLE_FINGER_TIP,
mp_hands.HandLandmark.RING_FINGER_TIP,
mp_hands.HandLandmark.PINKY_TIP,
]:
tip_coords = np.array([
hand_landmarks.landmark[tip].x * image_width,
hand_landmarks.landmark[tip].y * image_height,
])
dip_coords = np.array([
hand_landmarks.landmark[tip - 1].x * image_width,
hand_landmarks.landmark[tip - 1].y * image_height,
])
dt = tip_coords - dip_coords
ext = np.array([tip_coords + 3 / 4 * dt, tip_coords - 3 / 4 * dt])
origin = 0.5 * (ext[0] - ext[1]) + ext[1]
orth_p1 = _rotate(origin, ext[0], np.deg2rad(90))
orth_p2 = _rotate(origin, ext[1], np.deg2rad(90))
orth = np.array([orth_p1, orth_p2])
half = 0.5 * (ext[0] - ext[1])
p1 = orth[0] + half
p2 = orth[0] - half
p3 = orth[1] + half
p4 = orth[1] - half
angle = 90 - np.rad2deg(_angle_between(ext[0] - ext[1], [1, 0]))
mask = _get_polygon_mask(image_width, image_height, [p2, p1, p3, p4])
mask3 = np.tile(mask[:, :, None], (1, 1, 3))
masked = mask3 * image[:, :, ::-1]
masked, mask3 = _valid_crop(masked, mask3)
masked = ndimage.rotate(masked, angle)
mask3 = ndimage.rotate(mask3, angle)
masked, mask3 = _valid_crop(masked, mask3, offset=0)
masked = masked[5:-5, 5:-5]
masked = np.ascontiguousarray(masked)
nails.append(masked)
return nails
# ---------------------------------------------------------------------------
# Draw hand skeleton (from utils/draw_hand.py)
# ---------------------------------------------------------------------------
def draw_hand(annotated_image: np.ndarray, hand_landmarks) -> None:
mp_drawing = mp.solutions.drawing_utils
mp_drawing_styles = mp.solutions.drawing_styles
mp_hands = mp.solutions.hands
mp_drawing.draw_landmarks(
annotated_image,
hand_landmarks,
mp_hands.HAND_CONNECTIONS,
mp_drawing_styles.get_default_hand_landmarks_style(),
mp_drawing_styles.get_default_hand_connections_style(),
)
# ---------------------------------------------------------------------------
# Top-level entry point
# ---------------------------------------------------------------------------
def get_nails_and_landmarks(image_bgr: np.ndarray):
"""
Detect hand and extract all 5 nail crops.
Args:
image_bgr: BGR image as numpy array (as returned by cv2.imread or
cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR)).
Returns:
(nails, hand_landmarks) or (None, None) if no hand detected.
"""
if image_bgr.shape[0] < image_bgr.shape[1]: # landscape → rotate
image_bgr = cv2.rotate(image_bgr, cv2.ROTATE_90_CLOCKWISE)
landmarks = detect_hand(image_bgr)
if landmarks is None:
return None, None
# Flip upside-down images
mp_hands = mp.solutions.hands
if (
landmarks.landmark[mp_hands.HandLandmark.MIDDLE_FINGER_PIP].y
< landmarks.landmark[mp_hands.HandLandmark.MIDDLE_FINGER_TIP].y
):
image_bgr = cv2.rotate(image_bgr, cv2.ROTATE_180)
landmarks = detect_hand(image_bgr)
nails = extract_nails(image_bgr, landmarks)
return nails, landmarks