195 lines
8.4 KiB
Python
195 lines
8.4 KiB
Python
|
|
import cv2
|
|
import mediapipe as mp
|
|
import numpy as np
|
|
|
|
# ----- Helper Functions -----
|
|
def compute_distance(p1, p2):
|
|
"""Compute Euclidean distance between two points p1 and p2"""
|
|
return np.hypot(p2[0] - p1[0], p2[1] - p1[1])
|
|
|
|
# ----- Configuration -----
|
|
CAMERA_INDEX = 0 # Change if multiple cameras
|
|
FRAME_WIDTH = 1280
|
|
FRAME_HEIGHT = 720
|
|
# Initial calibration: approximate pixels per inch
|
|
PIXELS_PER_INCH = 20
|
|
# Gesture thresholds
|
|
PINCH_THRESHOLD = 40 # px distance index-middle to start action
|
|
RELEASE_THRESHOLD = 60 # px distance to end action
|
|
# Circle touch threshold for object calibration
|
|
CIRCLE_TOUCH_THRESHOLD = 20 # px tolerance to detect finger on circle
|
|
|
|
# ----- Initialize Hand Detector -----
|
|
mp_hands = mp.solutions.hands
|
|
mp_draw = mp.solutions.drawing_utils
|
|
hands = mp_hands.Hands(
|
|
static_image_mode=False,
|
|
max_num_hands=1,
|
|
min_detection_confidence=0.7,
|
|
min_tracking_confidence=0.5
|
|
)
|
|
|
|
# ----- State Variables -----
|
|
measuring = False # Flag for measurement gesture
|
|
start_pt = None
|
|
calibrating = False # Flag for pinch-based calibration mode
|
|
cal_start = None
|
|
object_calibrating = False # Flag for object-based calibration mode
|
|
cal_circle = None # Stores calibrated circle (x, y, r)
|
|
|
|
# ----- Main Loop -----
|
|
def main():
|
|
global measuring, start_pt, calibrating, cal_start, object_calibrating, PIXELS_PER_INCH
|
|
|
|
cap = cv2.VideoCapture(CAMERA_INDEX)
|
|
cap.set(cv2.CAP_PROP_FRAME_WIDTH, FRAME_WIDTH)
|
|
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, FRAME_HEIGHT)
|
|
|
|
if not cap.isOpened():
|
|
print(f"Error: cannot open camera {CAMERA_INDEX}")
|
|
return
|
|
|
|
print("Press 'c' for pinch calibration, 'o' for object circle calibration, 'q' to quit.")
|
|
|
|
while True:
|
|
ret, frame = cap.read()
|
|
if not ret:
|
|
break
|
|
|
|
frame = cv2.flip(frame, 1)
|
|
h, w, _ = frame.shape
|
|
|
|
# Hand detection
|
|
img_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
|
img_rgb.flags.writeable = False
|
|
results = hands.process(img_rgb)
|
|
img_rgb.flags.writeable = True
|
|
frame = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2BGR)
|
|
|
|
fingertip_idx = None
|
|
fingertip_mid = None
|
|
index_extended = False
|
|
middle_extended = False
|
|
|
|
if results.multi_hand_landmarks:
|
|
hand = results.multi_hand_landmarks[0]
|
|
mp_draw.draw_landmarks(frame, hand, mp_hands.HAND_CONNECTIONS)
|
|
# get index and middle finger tips and PIP to check extension
|
|
idx_tip = hand.landmark[mp_hands.HandLandmark.INDEX_FINGER_TIP]
|
|
idx_pip = hand.landmark[mp_hands.HandLandmark.INDEX_FINGER_PIP]
|
|
mid_tip = hand.landmark[mp_hands.HandLandmark.MIDDLE_FINGER_TIP]
|
|
mid_pip = hand.landmark[mp_hands.HandLandmark.MIDDLE_FINGER_PIP]
|
|
ix, iy = int(idx_tip.x * w), int(idx_tip.y * h)
|
|
mx, my = int(mid_tip.x * w), int(mid_tip.y * h)
|
|
fingertip_idx = (ix, iy)
|
|
fingertip_mid = (mx, my)
|
|
# draw fingertips
|
|
cv2.circle(frame, fingertip_idx, 8, (0,255,0), -1)
|
|
cv2.circle(frame, fingertip_mid, 8, (0,255,0), -1)
|
|
# determine if fingers are extended (tip above PIP)
|
|
index_extended = idx_tip.y < idx_pip.y
|
|
middle_extended = mid_tip.y < mid_pip.y
|
|
# pinch distance between index and middle
|
|
pinch_dist = compute_distance(fingertip_idx, fingertip_mid)
|
|
cv2.putText(frame, f"Pinch: {int(pinch_dist)}px", (10,30),
|
|
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0,255,0), 2)
|
|
|
|
# Pinch-based calibration
|
|
if calibrating:
|
|
if pinch_dist < PINCH_THRESHOLD and cal_start is None:
|
|
cal_start = fingertip_idx
|
|
print("Pinch calibration start point set.")
|
|
elif pinch_dist > RELEASE_THRESHOLD and cal_start is not None:
|
|
cal_end = fingertip_idx
|
|
px_dist = compute_distance(cal_start, cal_end)
|
|
inches = float(input("Enter actual distance between points in inches: "))
|
|
PIXELS_PER_INCH = px_dist / inches
|
|
print(f"Pinch calibration done: {PIXELS_PER_INCH:.2f} pixels/inch")
|
|
calibrating = False
|
|
cal_start = None
|
|
|
|
# Measurement gesture (only when not calibrating)
|
|
elif not object_calibrating and index_extended and middle_extended:
|
|
if pinch_dist < PINCH_THRESHOLD and not measuring:
|
|
measuring = True
|
|
start_pt = fingertip_idx
|
|
elif pinch_dist > RELEASE_THRESHOLD and measuring:
|
|
measuring = False
|
|
|
|
# Object-based calibration
|
|
if object_calibrating:
|
|
# Mask for orange color to find printed reference circle
|
|
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
|
|
# HSV range for orange (tune as needed)
|
|
lower_orange = np.array([10, 100, 100])
|
|
upper_orange = np.array([25, 255, 255])
|
|
color_mask = cv2.inRange(hsv, lower_orange, upper_orange)
|
|
masked_frame = cv2.bitwise_and(frame, frame, mask=color_mask)
|
|
# Convert masked area to grayscale for Hough
|
|
gray = cv2.cvtColor(masked_frame, cv2.COLOR_BGR2GRAY)
|
|
gray = cv2.medianBlur(gray, 5)
|
|
circles = cv2.HoughCircles(gray, cv2.HOUGH_GRADIENT, dp=1.2, minDist=100,
|
|
param1=50, param2=30, minRadius=10, maxRadius=300)
|
|
if circles is not None and fingertip_idx is not None:
|
|
circles = np.round(circles[0, :]).astype(int)
|
|
# Filter circles by proximity of fingertip to circumference
|
|
touched = []
|
|
for x, y, r in circles:
|
|
dist_c = compute_distance((x, y), fingertip_idx)
|
|
if abs(dist_c - r) < CIRCLE_TOUCH_THRESHOLD:
|
|
touched.append((x, y, r))
|
|
if touched:
|
|
# choose circle closest to exact touch point
|
|
touched.sort(key=lambda c: abs(compute_distance((c[0], c[1]), fingertip_idx) - c[2]))
|
|
x, y, r = touched[0]
|
|
# store calibrated circle permanently
|
|
cal_circle = (x, y, r)
|
|
# draw selected calibration circle
|
|
cv2.circle(frame, (x, y), r, (0, 0, 255), 3)
|
|
cv2.drawMarker(frame, (x, y), (0, 0, 255), markerType=cv2.MARKER_CROSS, markerSize=20, thickness=2)
|
|
cv2.line(frame, (x - r, y), (x + r, y), (0, 0, 255), 2)
|
|
cv2.putText(frame, f"Cal Circle r={r}px", (x - r, y - r - 10),
|
|
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0,0,255), 2)
|
|
# compute pixels per inch from diameter
|
|
PIXELS_PER_INCH = (2 * r) / 1.0
|
|
print(f"Object calibration done: {PIXELS_PER_INCH:.2f} pixels/inch")
|
|
object_calibrating = False
|
|
|
|
# Overlay mode text
|
|
if calibrating:
|
|
cv2.putText(frame, "PINCH CALIBRATING...", (10,60), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0,0,255), 2)
|
|
if object_calibrating:
|
|
cv2.putText(frame, 'PLACE 1" CIRCLE & POINT AT IT', (10,90),
|
|
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0,0,255), 2)
|
|
|
|
# Draw measurement line and values
|
|
if measuring and start_pt and fingertip_idx:
|
|
cv2.line(frame, start_pt, fingertip_idx, (255,0,0), 2)
|
|
px = compute_distance(start_pt, fingertip_idx)
|
|
inch = px / PIXELS_PER_INCH
|
|
cm = inch * 2.54
|
|
midpt = ((start_pt[0] + fingertip_idx[0])//2,
|
|
(start_pt[1] + fingertip_idx[1])//2)
|
|
cv2.putText(frame, f"{inch:.2f} in / {cm:.1f} cm", (midpt[0]+10, midpt[1]-10),
|
|
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255,0,0), 2)
|
|
|
|
# Display
|
|
cv2.imshow('Hand Measure', frame)
|
|
key = cv2.waitKey(1) & 0xFF
|
|
if key == ord('q'):
|
|
break
|
|
elif key == ord('c'):
|
|
calibrating = True
|
|
cal_start = None
|
|
print("Entered pinch calibration mode.")
|
|
elif key == ord('o'):
|
|
object_calibrating = True
|
|
print("Entered object calibration mode. Present a 1-inch circle & point at it.")
|
|
|
|
cap.release()
|
|
cv2.destroyAllWindows()
|
|
|
|
if __name__ == '__main__':
|
|
main()
|