Added Hand Tracking, a calibration option, start of a HUD and Blender Files
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
import cv2
|
||||
import mediapipe as mp
|
||||
import numpy as np
|
||||
|
||||
# ----- Helper Functions -----
|
||||
def compute_distance(p1, p2):
|
||||
return np.hypot(p2[0] - p1[0], p2[1] - p1[1])
|
||||
|
||||
# ----- Configuration -----
|
||||
CAMERA_INDICES = [0] # List of camera device indices
|
||||
FRAME_WIDTH = 1280
|
||||
FRAME_HEIGHT = 720
|
||||
PIXELS_PER_INCH = 20 # will be set by calibration
|
||||
PINCH_THRESHOLD = 40 # px to start touch
|
||||
RELEASE_THRESHOLD = 60 # px to end touch
|
||||
CIRCLE_TOUCH_THRESHOLD = 20 # px tolerance for circle touch
|
||||
|
||||
# ----- 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
|
||||
)
|
||||
|
||||
# ----- Global State -----
|
||||
measuring = False # measurement in progress
|
||||
start_pt = None # measurement start point
|
||||
calibrating = False # pinch-based calibration flag
|
||||
cal_start = None # pinch calibration start point
|
||||
object_calibrating = False # object calibration flag
|
||||
cal_circle = None # reference circle (x,y,r)
|
||||
fingertip_idx_global = None # last detected fingertip position
|
||||
|
||||
# ----- Per-camera Processing -----
|
||||
def process_frame(frame):
|
||||
global measuring, start_pt, calibrating, cal_start, object_calibrating, cal_circle, PIXELS_PER_INCH, fingertip_idx_global
|
||||
frame_out = cv2.flip(frame, 1)
|
||||
h, w, _ = frame_out.shape
|
||||
|
||||
# Hand detection
|
||||
rgb = cv2.cvtColor(frame_out, cv2.COLOR_BGR2RGB)
|
||||
rgb.flags.writeable = False
|
||||
results = hands.process(rgb)
|
||||
rgb.flags.writeable = True
|
||||
frame_vis = cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR)
|
||||
|
||||
fingertip_idx = None
|
||||
fingertip_mid = None
|
||||
|
||||
if results.multi_hand_landmarks:
|
||||
hand = results.multi_hand_landmarks[0]
|
||||
mp_draw.draw_landmarks(frame_vis, hand, mp_hands.HAND_CONNECTIONS)
|
||||
# get index and middle finger tips and PIP joints
|
||||
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)
|
||||
fingertip_idx_global = fingertip_idx
|
||||
# draw fingertips
|
||||
cv2.circle(frame_vis, fingertip_idx, 8, (0,255,0), -1)
|
||||
cv2.circle(frame_vis, fingertip_mid, 8, (0,255,0), -1)
|
||||
# check extension
|
||||
index_ext = idx_tip.y < idx_pip.y
|
||||
middle_ext = mid_tip.y < mid_pip.y
|
||||
# if measurement in progress but fingers no longer both extended, stop measuring
|
||||
if measuring and not (index_ext and middle_ext):
|
||||
measuring = False
|
||||
# pinch distance
|
||||
pinch = compute_distance(fingertip_idx, fingertip_mid)
|
||||
cv2.putText(frame_vis, f"Pinch: {int(pinch)} px", (10,30), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0,255,0),2)
|
||||
|
||||
# pinch calibration
|
||||
if calibrating:
|
||||
if pinch < PINCH_THRESHOLD and cal_start is None:
|
||||
cal_start = fingertip_idx
|
||||
print("Pinch calibration start set")
|
||||
elif pinch > RELEASE_THRESHOLD and cal_start is not None:
|
||||
cal_end = fingertip_idx
|
||||
px = compute_distance(cal_start, cal_end)
|
||||
inches = float(input("Enter actual distance between points (inches): "))
|
||||
PIXELS_PER_INCH = px / inches
|
||||
print(f"Calibrated: {PIXELS_PER_INCH:.2f} px/inch")
|
||||
calibrating = False
|
||||
cal_start = None
|
||||
# measurement gesture
|
||||
elif not object_calibrating and index_ext and middle_ext:
|
||||
if pinch < PINCH_THRESHOLD and not measuring:
|
||||
measuring = True
|
||||
start_pt = fingertip_idx
|
||||
elif pinch > RELEASE_THRESHOLD and measuring:
|
||||
measuring = False
|
||||
|
||||
# object calibration
|
||||
if object_calibrating and fingertip_idx is not None:
|
||||
hsv = cv2.cvtColor(frame_vis, cv2.COLOR_BGR2HSV)
|
||||
mask = cv2.inRange(hsv, np.array([10,100,100]), np.array([25,255,255]))
|
||||
masked = cv2.bitwise_and(frame_vis, frame_vis, mask=mask)
|
||||
gray = cv2.cvtColor(masked, cv2.COLOR_BGR2GRAY)
|
||||
gray = cv2.medianBlur(gray,5)
|
||||
circles = cv2.HoughCircles(gray, cv2.HOUGH_GRADIENT, 1.2, 100,
|
||||
param1=50, param2=30, minRadius=10, maxRadius=300)
|
||||
if circles is not None:
|
||||
circles = np.round(circles[0]).astype(int)
|
||||
touched = [(x,y,r) for x,y,r in circles
|
||||
if abs(compute_distance((x,y), fingertip_idx)-r) < CIRCLE_TOUCH_THRESHOLD]
|
||||
if touched:
|
||||
touched.sort(key=lambda c: abs(compute_distance((c[0],c[1]), fingertip_idx)-c[2]))
|
||||
x,y,r = touched[0]
|
||||
cal_circle = (x,y,r)
|
||||
PIXELS_PER_INCH = 2 * r
|
||||
print(f"Circle calib: {PIXELS_PER_INCH:.2f} px/inch")
|
||||
object_calibrating = False
|
||||
|
||||
# permanent reference circle
|
||||
if cal_circle:
|
||||
cx,cy,cr = cal_circle
|
||||
cv2.circle(frame_vis,(cx,cy),cr,(0,0,255),2)
|
||||
cv2.drawMarker(frame_vis,(cx,cy),(0,0,255),cv2.MARKER_TILTED_CROSS,15,1)
|
||||
cv2.putText(frame_vis,f"Ref r={cr} px",(cx-cr,cy+cr+20),cv2.FONT_HERSHEY_SIMPLEX,0.5,(0,0,255),1)
|
||||
|
||||
return frame_vis
|
||||
|
||||
# ----- Main -----
|
||||
def main():
|
||||
caps = []
|
||||
for idx in CAMERA_INDICES:
|
||||
cap = cv2.VideoCapture(idx)
|
||||
cap.set(cv2.CAP_PROP_FRAME_WIDTH, FRAME_WIDTH)
|
||||
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, FRAME_HEIGHT)
|
||||
caps.append(cap)
|
||||
if not all(cap.isOpened() for cap in caps):
|
||||
print("Error: could not open all cameras")
|
||||
return
|
||||
|
||||
print("Press 'c' for pinch calib, 'o' for circle calib, 'q' to quit.")
|
||||
while True:
|
||||
frames = [cap.read()[1] for cap in caps]
|
||||
frame = next((f for f in frames if f is not None), None)
|
||||
if frame is None:
|
||||
break
|
||||
# process
|
||||
full_view = process_frame(frame)
|
||||
# create proj output
|
||||
proj = np.zeros_like(full_view)
|
||||
if cal_circle:
|
||||
cx,cy,cr = cal_circle
|
||||
cv2.circle(proj,(cx,cy),cr,(0,0,255),2)
|
||||
if measuring and start_pt and fingertip_idx_global:
|
||||
cv2.line(proj, start_pt, fingertip_idx_global, (255,0,0),2)
|
||||
px = compute_distance(start_pt, fingertip_idx_global)
|
||||
inch = px/PIXELS_PER_INCH; cm = inch*2.54
|
||||
mid = ((start_pt[0]+fingertip_idx_global[0])//2,(start_pt[1]+fingertip_idx_global[1])//2)
|
||||
cv2.putText(proj,f"{inch:.2f}in/{cm:.1f}cm",(mid[0]+10,mid[1]-10),
|
||||
cv2.FONT_HERSHEY_SIMPLEX,0.7,(255,0,0),2)
|
||||
# overlay proj onto debug
|
||||
debug = full_view.copy()
|
||||
# overlay ref circle
|
||||
if cal_circle:
|
||||
cx,cy,cr = cal_circle
|
||||
cv2.circle(debug,(cx,cy),cr,(0,0,255),2)
|
||||
# overlay measurement
|
||||
if measuring and start_pt and fingertip_idx_global:
|
||||
cv2.line(debug, start_pt, fingertip_idx_global, (255,0,0),2)
|
||||
cv2.putText(debug,f"{inch:.2f}in/{cm:.1f}cm",(mid[0]+10,mid[1]-10),
|
||||
cv2.FONT_HERSHEY_SIMPLEX,0.7,(255,0,0),2)
|
||||
|
||||
# show windows
|
||||
cv2.imshow('Hand Measure', debug)
|
||||
cv2.imshow('Projector Output', proj)
|
||||
|
||||
key = cv2.waitKey(1) & 0xFF
|
||||
if key == ord('q'):
|
||||
break
|
||||
elif key == ord('c'):
|
||||
global calibrating
|
||||
calibrating = True
|
||||
cal_start = None
|
||||
print("Entered pinch calibration mode.")
|
||||
elif key == ord('o'):
|
||||
global object_calibrating
|
||||
object_calibrating = True
|
||||
print("Entered circle calibration mode.")
|
||||
|
||||
for cap in caps:
|
||||
cap.release()
|
||||
cv2.destroyAllWindows()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,194 @@
|
||||
|
||||
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()
|
||||
BIN
Binary file not shown.
+279
@@ -0,0 +1,279 @@
|
||||
import cv2
|
||||
import mediapipe as mp
|
||||
import numpy as np
|
||||
|
||||
# ----- Helper Functions -----
|
||||
def compute_distance(p1, p2):
|
||||
return np.hypot(p2[0] - p1[0], p2[1] - p1[1])
|
||||
|
||||
# ----- Configuration -----
|
||||
CAMERA_INDICES = [0] # List of camera device indices
|
||||
FRAME_WIDTH = 1280
|
||||
FRAME_HEIGHT = 720
|
||||
PIXELS_PER_INCH = 38 # will be set by calibration
|
||||
PINCH_THRESHOLD = 40 # px to start touch
|
||||
RELEASE_THRESHOLD = 60 # px to end touch
|
||||
CIRCLE_TOUCH_THRESHOLD = 20 # px tolerance for circle touch
|
||||
|
||||
# HSV range for shape color (tune for your arrow: now tailored for orange)
|
||||
LOWER_SHAPE = np.array([10, 100, 100]) # hue from 10° (orange) to
|
||||
UPPER_SHAPE = np.array([30, 255, 255]) # hue up to 30°, full sat/val range
|
||||
|
||||
# dynamic list of registered arrows
|
||||
# each entry will be {'lower': np.array, 'upper': np.array, 'tip': (x,y) or None}
|
||||
shape_ranges = []
|
||||
|
||||
# ----- 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
|
||||
)
|
||||
|
||||
# ----- Global State -----
|
||||
measuring = False # measurement in progress
|
||||
start_pt = None # measurement start point
|
||||
calibrating = False # pinch-based calibration flag
|
||||
cal_start = None # pinch calibration start point
|
||||
object_calibrating = False # object calibration flag
|
||||
cal_circle = None # reference circle (x,y,r)
|
||||
fingertip_idx_global = None # last detected fingertip position
|
||||
arrow_tip = None # detected arrow tip position
|
||||
|
||||
# ----- Per-camera Processing -----
|
||||
def process_frame(frame):
|
||||
global measuring, start_pt, calibrating, cal_start
|
||||
global object_calibrating, cal_circle, PIXELS_PER_INCH
|
||||
global fingertip_idx_global, arrow_tip
|
||||
|
||||
frame_out = cv2.flip(frame, 1)
|
||||
h, w, _ = frame_out.shape
|
||||
|
||||
# Hand detection
|
||||
rgb = cv2.cvtColor(frame_out, cv2.COLOR_BGR2RGB)
|
||||
rgb.flags.writeable = False
|
||||
results = hands.process(rgb)
|
||||
rgb.flags.writeable = True
|
||||
frame_vis = cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR)
|
||||
|
||||
fingertip_idx = None
|
||||
fingertip_mid = None
|
||||
|
||||
if results.multi_hand_landmarks:
|
||||
hand = results.multi_hand_landmarks[0]
|
||||
mp_draw.draw_landmarks(frame_vis, hand, mp_hands.HAND_CONNECTIONS)
|
||||
# get index and middle finger tips and PIP joints
|
||||
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)
|
||||
fingertip_idx_global = fingertip_idx
|
||||
# draw fingertips
|
||||
cv2.circle(frame_vis, fingertip_idx, 8, (0,255,0), -1)
|
||||
cv2.circle(frame_vis, fingertip_mid, 8, (0,255,0), -1)
|
||||
# check extension
|
||||
index_ext = idx_tip.y < idx_pip.y
|
||||
middle_ext = mid_tip.y < mid_pip.y
|
||||
# if measurement in progress but fingers no longer both extended, stop measuring
|
||||
if measuring and not (index_ext and middle_ext):
|
||||
measuring = False
|
||||
# pinch distance
|
||||
pinch = compute_distance(fingertip_idx, fingertip_mid)
|
||||
cv2.putText(frame_vis, f"Pinch: {int(pinch)} px", (10,30), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0,255,0),2)
|
||||
|
||||
# pinch calibration
|
||||
if calibrating:
|
||||
if pinch < PINCH_THRESHOLD and cal_start is None:
|
||||
cal_start = fingertip_idx
|
||||
print("Pinch calibration start set")
|
||||
elif pinch > RELEASE_THRESHOLD and cal_start is not None:
|
||||
cal_end = fingertip_idx
|
||||
px = compute_distance(cal_start, cal_end)
|
||||
inches = float(input("Enter actual distance between points (inches): "))
|
||||
PIXELS_PER_INCH = px / inches
|
||||
print(f"Calibrated: {PIXELS_PER_INCH:.2f} px/inch")
|
||||
calibrating = False
|
||||
cal_start = None
|
||||
# measurement gesture
|
||||
elif not object_calibrating and index_ext and middle_ext:
|
||||
if pinch < PINCH_THRESHOLD and not measuring:
|
||||
measuring = True
|
||||
start_pt = fingertip_idx
|
||||
elif pinch > RELEASE_THRESHOLD and measuring:
|
||||
measuring = False
|
||||
|
||||
|
||||
|
||||
# object calibration
|
||||
if object_calibrating and fingertip_idx is not None:
|
||||
hsv = cv2.cvtColor(frame_vis, cv2.COLOR_BGR2HSV)
|
||||
mask = cv2.inRange(hsv, np.array([10,100,100]), np.array([25,255,255]))
|
||||
masked = cv2.bitwise_and(frame_vis, frame_vis, mask=mask)
|
||||
gray = cv2.cvtColor(masked, cv2.COLOR_BGR2GRAY)
|
||||
gray = cv2.medianBlur(gray,5)
|
||||
circles = cv2.HoughCircles(gray, cv2.HOUGH_GRADIENT, 1.2, 100,
|
||||
param1=50, param2=30, minRadius=10, maxRadius=300)
|
||||
if circles is not None:
|
||||
circles = np.round(circles[0]).astype(int)
|
||||
touched = [(x,y,r) for x,y,r in circles
|
||||
if abs(compute_distance((x,y), fingertip_idx)-r) < CIRCLE_TOUCH_THRESHOLD]
|
||||
if touched:
|
||||
touched.sort(key=lambda c: abs(compute_distance((c[0],c[1]), fingertip_idx)-c[2]))
|
||||
x,y,r = touched[0]
|
||||
cal_circle = (x,y,r)
|
||||
PIXELS_PER_INCH = 2 * r
|
||||
print(f"Circle calib: {PIXELS_PER_INCH:.2f} px/inch")
|
||||
object_calibrating = False
|
||||
|
||||
# permanent reference circle
|
||||
if cal_circle:
|
||||
cx,cy,cr = cal_circle
|
||||
cv2.circle(frame_vis,(cx,cy),cr,(0,0,255),2)
|
||||
cv2.drawMarker(frame_vis,(cx,cy),(0,0,255),cv2.MARKER_TILTED_CROSS,15,1)
|
||||
cv2.putText(frame_vis,f"Ref r={cr} px",(cx-cr,cy+cr+20),cv2.FONT_HERSHEY_SIMPLEX,0.5,(0,0,255),1)
|
||||
|
||||
# ----- Arrow Shape Detection & HUD Placement -----
|
||||
# 1) Build a clean orange mask
|
||||
hsv = cv2.cvtColor(frame_vis, cv2.COLOR_BGR2HSV)
|
||||
mask = cv2.inRange(hsv, LOWER_SHAPE, UPPER_SHAPE)
|
||||
kern = cv2.getStructuringElement(cv2.MORPH_RECT, (5,5))
|
||||
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kern)
|
||||
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kern)
|
||||
|
||||
# 2) Find and filter contours
|
||||
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
||||
for cnt in contours:
|
||||
area = cv2.contourArea(cnt)
|
||||
if area < 1000:
|
||||
continue
|
||||
|
||||
# Approximate to polygon and require exactly 5 corners
|
||||
peri = cv2.arcLength(cnt, True)
|
||||
approx = cv2.approxPolyDP(cnt, 0.02 * peri, True)
|
||||
if len(approx) != 5:
|
||||
continue
|
||||
|
||||
pts = approx.reshape(-1,2)
|
||||
centroid = np.mean(pts, axis=0)
|
||||
|
||||
# Find the arrow tip as the corner farthest from centroid
|
||||
dists = [np.linalg.norm(pt - centroid) for pt in pts]
|
||||
tip_pt = pts[int(np.argmax(dists))]
|
||||
raw_tip = (int(tip_pt[0]), int(tip_pt[1]))
|
||||
|
||||
# 3) Smooth the tip over time
|
||||
alpha = 0.2
|
||||
if arrow_tip is None:
|
||||
arrow_tip = raw_tip
|
||||
else:
|
||||
arrow_tip = (
|
||||
int(alpha * raw_tip[0] + (1-alpha) * arrow_tip[0]),
|
||||
int(alpha * raw_tip[1] + (1-alpha) * arrow_tip[1])
|
||||
)
|
||||
|
||||
# Draw the arrow & tip
|
||||
cv2.drawContours(frame_vis, [pts], -1, (0,255,255), 2)
|
||||
cv2.circle(frame_vis, arrow_tip, 8, (0,255,255), -1)
|
||||
cv2.putText(frame_vis, "Arrow Tip", arrow_tip,
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0,255,255), 2)
|
||||
|
||||
# Place the HUD box in the arrow’s pointing direction
|
||||
dir_vec = tip_pt - centroid
|
||||
norm = np.linalg.norm(dir_vec)
|
||||
if norm>0:
|
||||
dir_unit = dir_vec / norm
|
||||
offset = 50
|
||||
hud_center = (int(arrow_tip[0] + dir_unit[0]*offset),
|
||||
int(arrow_tip[1] + dir_unit[1]*offset))
|
||||
|
||||
# Build and rotate a 120×60 HUD rectangle
|
||||
w2, h2 = 60, 30
|
||||
theta = np.arctan2(dir_unit[1], dir_unit[0])
|
||||
R = np.array([[ np.cos(theta), -np.sin(theta)],
|
||||
[ np.sin(theta), np.cos(theta)]])
|
||||
corners = np.array([[-w2,-h2], [w2,-h2], [w2,h2], [-w2,h2]])
|
||||
hud_pts = (corners @ R.T) + np.array(hud_center)
|
||||
hud_pts = hud_pts.astype(int)
|
||||
|
||||
cv2.drawContours(frame_vis, [hud_pts], -1, (255,0,255), 2)
|
||||
cv2.putText(frame_vis, "HUD", hud_center,
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255,0,255), 2)
|
||||
|
||||
|
||||
|
||||
return frame_vis
|
||||
|
||||
# ----- Main -----
|
||||
def main():
|
||||
caps = []
|
||||
for idx in CAMERA_INDICES:
|
||||
cap = cv2.VideoCapture(idx)
|
||||
cap.set(cv2.CAP_PROP_FRAME_WIDTH, FRAME_WIDTH)
|
||||
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, FRAME_HEIGHT)
|
||||
caps.append(cap)
|
||||
if not all(cap.isOpened() for cap in caps):
|
||||
print("Error: could not open all cameras")
|
||||
return
|
||||
|
||||
print("Press 'c' for pinch calib, 'o' for circle calib, 'q' to quit.")
|
||||
while True:
|
||||
frames = [cap.read()[1] for cap in caps]
|
||||
frame = next((f for f in frames if f is not None), None)
|
||||
if frame is None:
|
||||
break
|
||||
# process
|
||||
full_view = process_frame(frame)
|
||||
# create proj output
|
||||
proj = np.zeros_like(full_view)
|
||||
if cal_circle:
|
||||
cx,cy,cr = cal_circle
|
||||
cv2.circle(proj,(cx,cy),cr,(0,0,255),2)
|
||||
if measuring and start_pt and fingertip_idx_global:
|
||||
cv2.line(proj, start_pt, fingertip_idx_global, (255,0,0),2)
|
||||
px = compute_distance(start_pt, fingertip_idx_global)
|
||||
inch = px/PIXELS_PER_INCH; cm = inch*2.54
|
||||
mid = ((start_pt[0]+fingertip_idx_global[0])//2,(start_pt[1]+fingertip_idx_global[1])//2)
|
||||
cv2.putText(proj,f"{inch:.2f}in/{cm:.1f}cm",(mid[0]+10,mid[1]-10),
|
||||
cv2.FONT_HERSHEY_SIMPLEX,0.7,(255,0,0),2)
|
||||
# overlay proj onto debug
|
||||
debug = full_view.copy()
|
||||
# overlay ref circle
|
||||
if cal_circle:
|
||||
cx,cy,cr = cal_circle
|
||||
cv2.circle(debug,(cx,cy),cr,(0,0,255),2)
|
||||
# overlay measurement
|
||||
if measuring and start_pt and fingertip_idx_global:
|
||||
cv2.line(debug, start_pt, fingertip_idx_global, (255,0,0),2)
|
||||
cv2.putText(debug,f"{inch:.2f}in/{cm:.1f}cm",(mid[0]+10,mid[1]-10),
|
||||
cv2.FONT_HERSHEY_SIMPLEX,0.7,(255,0,0),2)
|
||||
|
||||
# show windows
|
||||
cv2.imshow('Hand Measure', debug)
|
||||
cv2.imshow('Projector Output', proj)
|
||||
|
||||
key = cv2.waitKey(1) & 0xFF
|
||||
if key == ord('q'):
|
||||
break
|
||||
elif key == ord('c'):
|
||||
global calibrating
|
||||
calibrating = True
|
||||
cal_start = None
|
||||
print("Entered pinch calibration mode.")
|
||||
elif key == ord('o'):
|
||||
global object_calibrating
|
||||
object_calibrating = True
|
||||
print("Entered circle calibration mode.")
|
||||
|
||||
for cap in caps:
|
||||
cap.release()
|
||||
cv2.destroyAllWindows()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Binary file not shown.
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
Binary file not shown.
Reference in New Issue
Block a user