This commit is contained in:
DuOtto
2025-11-04 21:40:06 +01:00
parent 1d63671fad
commit 225ecc2e17
16 changed files with 244 additions and 109 deletions
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+4 -1
View File
@@ -1,7 +1,7 @@
import numpy as np
# ----- Configuration -----
CAMERA_INDICES = [0] # List of camera device indices
CAMERA_INDICES_TO_CHECK = 10
FRAME_WIDTH = 3980
FRAME_HEIGHT = 2560
PIXELS_PER_INCH = 38 # will be set by calibration
@@ -25,3 +25,6 @@ LEFT_BTN_TOP = (TAB_X, TAB_Y0+2*(TAB_H+TAB_GAP)+20)
RIGHT_BTN_TOP = (TAB_X, LEFT_BTN_TOP[1]+TAB_H+TAB_GAP)
DEBUG = True
TARGET_FPS = 40
FRAME_INTERVAL = 1.0 / TARGET_FPS
+105 -1
View File
@@ -3,12 +3,18 @@ import cv2, numpy as np, time, fitz
from helper import compute_distance
from config import PINCH_THRESHOLD, RELEASE_THRESHOLD
# hand_state.py
from dataclasses import dataclass, field
from typing import Tuple, Dict, Optional
from helper import finger_straight
# ----- 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=2,
max_num_hands=8,
min_detection_confidence=0.7,
min_tracking_confidence=0.5
)
@@ -16,6 +22,102 @@ hands = mp_hands.Hands(
start_pt = None # measurement start point
measuring = False # measurement in progress
@dataclass
class HandState:
id: int # a persistent identifier for this hand
landmarks: Dict[str, Tuple[int,int]] = field(default_factory=dict)
finger_straightness: Dict[str, float] = field(default_factory=dict)
gesture: Optional[str] = None # e.g. "pinch", "fist", "open"
gesture_persistence: int = 0 # how many frames the current gesture has held
#handList = Dict[int, HandState] = {}
next_id = 0
def update(mp_results, frame_vis):
h, w, _ = frame_vis.shape
detected_centroids = []
landmarks_list = []
# 1) pull out centroids & raw landmarks
if mp_results.multi_hand_landmarks:
for hand in mp_results.multi_hand_landmarks:
pts = []
for lm in hand.landmark:
pts.append((int(lm.x*w), int(lm.y*h)))
centroid = np.mean(pts, axis=0)
detected_centroids.append(tuple(centroid.astype(int)))
landmarks_list.append((hand, pts))
# 2) match to existing by nearest centroid
new_hands = {}
used_ids = set()
for (hand, pts), centroid in zip(landmarks_list, detected_centroids):
# find best existing hand
best_id, best_dist = None, 1e9
for hid, state in handList.items():
dx, dy = np.array(state.landmarks['centroid']) - centroid
d = np.hypot(dx, dy)
if d < best_dist and d < 100: # 100px max match distance
best_dist, best_id = d, hid
if best_id is None:
hid = next_id
next_id += 1
state = HandState(id=hid)
else:
hid = best_id
state = handList[hid]
used_ids.add(hid)
# 3) update state
state.persistence += 1
state.landmarks['centroid'] = centroid
# compute fingertip positions
idx_tip = pts[mp.solutions.hands.HandLandmark.INDEX_FINGER_TIP]
mid_tip = pts[mp.solutions.hands.HandLandmark.MIDDLE_FINGER_TIP]
state.landmarks['index_tip'] = idx_tip
state.landmarks['middle_tip'] = mid_tip
for name, tip_i, pip_i, mcp_i in [
('index', mp.solutions.hands.HandLandmark.INDEX_FINGER_TIP,
mp.solutions.hands.HandLandmark.INDEX_FINGER_PIP,
mp.solutions.hands.HandLandmark.INDEX_FINGER_MCP),
('middle', mp.solutions.hands.HandLandmark.MIDDLE_FINGER_TIP,
mp.solutions.hands.HandLandmark.MIDDLE_FINGER_PIP,
mp.solutions.hands.HandLandmark.MIDDLE_FINGER_MCP),
# add ring, pinky, thumb similarly...
]:
p_tip = pts[tip_i]
p_pip = pts[pip_i]
p_mcp = pts[mcp_i]
state.finger_straightness[name] = finger_straight(p_tip, p_pip, p_mcp)
# 5) simple gesture detection
if state.finger_straightness['index'] < 0.2 and \
state.finger_straightness['middle'] < 0.2:
gesture = 'fist'
elif state.finger_straightness['index'] > 0.8 and \
state.finger_straightness['middle'] > 0.8:
gesture = 'open'
else:
gesture = None
if gesture == state.gesture:
state.gesture_persistence += 1
else:
state.gesture = gesture
state.gesture_persistence = 0
new_hands[hid] = state
# 6) drop hands not seen this frame
handList = new_hands
return list(hands.values())
def detect_hands(frame):
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
rgb.flags.writeable = False
@@ -47,6 +149,8 @@ def check_hand(hand, frame_vis, object_calibrating):
return fingertip_idx, measuring, start_pt
def is_measuring(frame_vis, hand, object_calibrating):
global measuring, start_pt
h, w, _ = frame_vis.shape
+15
View File
@@ -1,6 +1,21 @@
import numpy as np
import cv2
# ----- Helper Functions -----
def compute_distance(p1, p2):
return np.hypot(p2[0] - p1[0], p2[1] - p1[1])
# 4) compute straightness per finger
def finger_straight(i_tip, i_pip, i_mcp):
d1 = np.hypot(*(np.array(i_tip)-np.array(i_pip)))
d2 = np.hypot(*(np.array(i_pip)-np.array(i_mcp)))
return float(np.clip(d1/(d2+1e-3), 0, 1))
def yes_no():
while True:
key = cv2.waitKey(1) & 0xFF
if key == ord("y"):
return True
if key == ord("n"):
return False
-75
View File
@@ -45,81 +45,6 @@ def find_hud_placement(frame_vis):
return arrow_tip, dir_vec
# ——————————————————————————————————————————————
# PDF loading / rasterization at import time
# ——————————————————————————————————————————————
PDF_PATH = "DarkAngles.pdf" # change this to your file
PDF_DPI = 600 # controls resolution of rasterization
_doc = fitz.open(PDF_PATH)
_pdf_pages = []
for page in _doc:
# render page to pixmap at desired zoom
zoom = PDF_DPI / 72.0
mat = fitz.Matrix(zoom, zoom)
pix = page.get_pixmap(matrix=mat, alpha=False)
# convert pixmap to ndarray
img = np.frombuffer(pix.samples, dtype=np.uint8)
img = img.reshape(pix.height, pix.width, pix.n)
if pix.n == 4:
img = cv2.cvtColor(img, cv2.COLOR_RGBA2BGR)
_pdf_pages.append(img)
_doc.close()
# ——————————————————————————————————————————————
# HUD drawing routines
# ——————————————————————————————————————————————
# rotated rectangle → 4 pts
def _rect_to_pts(center, size, angle_rad):
cx, cy = center
w, h = size
# local corners
pts = np.array([
[-w/2, -h/2],
[ w/2, -h/2],
[ w/2, h/2],
[-w/2, h/2],
])
# rotation
c, s = np.cos(angle_rad), np.sin(angle_rad)
R = np.array([[c, -s],[s, c]])
pts = pts.dot(R.T)
pts += np.array([cx, cy])
return pts.astype(np.float32)
def draw_hud_box(frame, tip, angle_rad, size=(120, 60), color=(255,0,255), thickness=2):
"""Draw a rotated HUD rectangle at `tip` pointing along `angle_rad`."""
# compute box corners
pts = _rect_to_pts(center=tip, size=size, angle_rad=angle_rad)
cv2.drawContours(frame, [pts.astype(int)], -1, color, thickness)
# label
cv2.putText(frame, "HUD", (int(tip[0]+5), int(tip[1]+5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2)
return pts # return the quad for PDF warping
def draw_pdf_page(frame, page_index, dst_quad):
"""
Warp PDF page image #page_index into the quadrilateral dst_quad.
dst_quad: 4×2 float32 array of destination corners in clock-wise order.
"""
if page_index < 0 or page_index >= len(_pdf_pages):
return
src = _pdf_pages[page_index]
h, w = src.shape[:2]
# source corners (tl, tr, br, bl)
src_quad = np.array([[0,0], [w,0], [w,h], [0,h]], dtype=np.float32)
# compute homography
M = cv2.getPerspectiveTransform(src_quad, dst_quad)
# warp PDF page into place (transparent where outside)
warp = cv2.warpPerspective(src, M, (frame.shape[1], frame.shape[0]))
mask = cv2.warpPerspective(np.ones((h,w), dtype=np.uint8)*255, M, (frame.shape[1], frame.shape[0]))
# composite onto frame
inv = cv2.bitwise_not(mask)
bg = cv2.bitwise_and(frame, frame, mask=inv)
fg = cv2.bitwise_and(warp, warp, mask=mask)
np.copyto(frame, bg+fg)
# hud.py
class HUD:
def __init__(self, marker_id, pdf_pages, default_page=0):
+34 -27
View File
@@ -5,10 +5,12 @@ from helper import compute_distance
from hud import find_hud_placement
import math
from config import CAMERA_INDICES, FRAME_WIDTH, FRAME_HEIGHT, PIXELS_PER_INCH, CIRCLE_TOUCH_THRESHOLD
from config import PIXELS_PER_INCH, CIRCLE_TOUCH_THRESHOLD
from config import FRAME_INTERVAL, TARGET_FPS
from config import DEBUG
from hud import draw_hud_box, draw_pdf_page
from setup import InitCameras
#from hud import draw_hud_box, draw_pdf_page
import time
# ----- Global State -----
measuring = False # measurement in progress
@@ -22,7 +24,7 @@ hud_rot = None
register_hud = False
# ----- Per-camera Processing -----
def process_frame(frame):
def process_frame(frame, camera_id: int = 0):
global measuring, start_pt, calibrating
global object_calibrating
global fingertip_idx_global
@@ -53,8 +55,6 @@ def process_frame(frame):
else:
res = hud_pos, hud_rot
# create proj output
projection_out = np.zeros_like(frame)
@@ -76,8 +76,8 @@ def process_frame(frame):
dx, dy = dir_vec
arrow_angle = math.atan2(dy, dx) # result in radians
quad = draw_hud_box(projection_out, arrow_tip, arrow_angle, size=(400,400))
draw_pdf_page(projection_out, page_index=0, dst_quad=quad)
#quad = draw_hud_box(projection_out, arrow_tip, arrow_angle, size=(400,400))
#draw_pdf_page(projection_out, page_index=0, dst_quad=quad)
if register_hud:
hud_pos = arrow_tip
@@ -92,31 +92,30 @@ def process_frame(frame):
# ----- Main ----- -------------------------------------------------------------------------------------
def main():
all_captures = []
for idx in CAMERA_INDICES:
capture = cv2.VideoCapture(idx)
capture.set(cv2.CAP_PROP_FRAME_WIDTH, FRAME_WIDTH)
capture.set(cv2.CAP_PROP_FRAME_HEIGHT, FRAME_HEIGHT)
all_captures.append(capture)
if not all(capture.isOpened() for capture in all_captures):
print("Error: could not open all cameras")
return
all_captures = InitCameras()
print(f"Running at up to {TARGET_FPS} FPS (interval={FRAME_INTERVAL:.3f}s)")
print("Press 'o' for circle calib, 'q' to quit.")
while True:
frames = [capture.read()[1] for capture in all_captures]
frame = next((f for f in frames if f is not None), None)
if frame is None:
last_time = time.time()
while True:
processed_frames = []
for camera_id, capture in enumerate(all_captures):
ok, frame = capture.read()
if not ok or frame is None:
continue
projection, debug = process_frame(frame, camera_id)
processed_frames.append((camera_id, projection, debug))
if not processed_frames:
break
# process
projection, debug = process_frame(frame)
# show windows
for camera_id, projection, debug in processed_frames:
if DEBUG:
cv2.imshow('Debug Output', debug)
cv2.imshow('Projector Output', projection)
cv2.imshow(f'Debug Output (cam {camera_id})', debug)
cv2.imshow(f'Projector Output (cam {camera_id})', projection)
key = cv2.waitKey(1) & 0xFF
@@ -131,6 +130,14 @@ def main():
register_hud = True
hud_pos = None
# ——— framerate limiting ———
now = time.time()
elapsed = now - last_time
to_wait = FRAME_INTERVAL - elapsed
if to_wait > 0:
time.sleep(to_wait)
last_time = time.time()
for capture in all_captures:
capture.release()
+81
View File
@@ -0,0 +1,81 @@
import cv2
import numpy as np
from config import CAMERA_INDICES_TO_CHECK, FRAME_WIDTH, FRAME_HEIGHT
def _preview_camera(capture: cv2.VideoCapture, cameraID: int) -> str:
"""Show a live preview and return the user's choice: 'add', 'skip', or 'quit'."""
window_name = f"Camera {cameraID}"
cv2.namedWindow(window_name, cv2.WINDOW_NORMAL)
print(f"[Camera {cameraID}] Press 'y' to add, 'n' to skip, 'q' to stop scanning.")
try:
while True:
ok, frame = capture.read()
if not ok:
print(f"[Camera {cameraID}] Failed to read frame; skipping.")
return "skip"
cv2.imshow(window_name, frame)
key = cv2.waitKey(1) & 0xFF
if key == ord("y"):
print(f"[Camera {cameraID}] Selected.")
return "add"
if key == ord("n"):
print(f"[Camera {cameraID}] Skipped.")
return "skip"
if key == ord("q") or key == 27: # 27 == ESC
print(f"[Camera {cameraID}] Stopping camera scan.")
return "quit"
finally:
cv2.destroyWindow(window_name)
def InitCameras():
allCaptures = []
setup_window = "Camera Setup"
instructions = np.zeros((240, 560, 3), dtype=np.uint8)
cv2.putText(instructions, "Camera Setup", (20, 40), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 200, 255), 2)
cv2.putText(instructions, "Each camera will preview in its own window.", (20, 90), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (200, 200, 200), 1)
cv2.putText(instructions, "Use 'y' to add, 'n' to skip, 'q'/ESC to finish.", (20, 125), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (200, 200, 200), 1)
cv2.putText(instructions, "Close this window when you're done.", (20, 160), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (200, 200, 200), 1)
cv2.namedWindow(setup_window, cv2.WINDOW_AUTOSIZE)
cv2.imshow(setup_window, instructions)
cv2.waitKey(1)
try:
for cameraID in range(CAMERA_INDICES_TO_CHECK):
capture = cv2.VideoCapture(cameraID)
if not capture.isOpened():
capture.release()
continue
capture.set(cv2.CAP_PROP_FRAME_WIDTH, FRAME_WIDTH)
capture.set(cv2.CAP_PROP_FRAME_HEIGHT, FRAME_HEIGHT)
decision = _preview_camera(capture, cameraID)
if decision == "add":
allCaptures.append(capture)
else:
capture.release()
if decision == "quit":
break
if not all(capture.isOpened() for capture in allCaptures):
print("Error: could not open all selected cameras.")
return
if not allCaptures:
print("No cameras selected.")
return
return allCaptures
finally:
cv2.destroyWindow(setup_window)