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()