Download src/app3d.py from ayzeksalimli/hand-tracking-drawing: direct link, hf CLI and curl.
- Browser
- Download file 10.1 kB
-
https://huggingface.co/ayzeksalimli/hand-tracking-drawing/resolve/main/src/app3d.py
- Command line
-
hf download hf://ayzeksalimli/hand-tracking-drawing/src/app3d.py
-
curl -L -o app3d.py https://huggingface.co/ayzeksalimli/hand-tracking-drawing/resolve/main/src/app3d.py
10.1 kB
| """3D drawing mode built on monocular depth.""" | |
| from __future__ import annotations | |
| import argparse | |
| import time | |
| import cv2 | |
| import numpy as np | |
| import ui | |
| from canvas3d import Canvas3D, View | |
| from depth import DepthEstimator, DepthWorker | |
| from gestures import Gesture | |
| from main import App | |
| RING_HOLD_SECONDS = 0.35 | |
| PALM_RESET_SECONDS = 0.6 | |
| DEPTH_MIX = 0.78 | |
| SCENE_ALPHA = 0.02 | |
| class App3D(App): | |
| """Draw real 3D strokes using estimated depth.""" | |
| def __init__(self, args: argparse.Namespace) -> None: | |
| super().__init__(args) | |
| self.canvas = Canvas3D(self.w, self.h, args.output) | |
| self.depth = DepthWorker(args.depth_model, imgsz=args.depth_size) | |
| print(f"[i] depth model: {args.depth_model}", flush=True) | |
| print(f"[i] depth device: {self.depth.device} imgsz: {args.depth_size}", flush=True) | |
| self.depth_map: np.ndarray | None = None | |
| self.depth_norm: np.ndarray | None = None | |
| self.depth_view = False | |
| self._view = View() | |
| self._depth_seq = -1 | |
| self._scene_ref: float | None = None | |
| self._scene_gain = 1.0 | |
| self._depth_announced = False | |
| self._frame_index = 0 | |
| self._ring_since: float | None = None | |
| self._ring_latched = False | |
| self._palm_since: float | None = None | |
| self._palm_latched = False | |
| self.yaw, self.pitch, self.roll, self.scale = 0.0, 0.0, 0.0, 1.0 | |
| self.yaw_s.set(0.0) | |
| self.pitch_s.set(0.0) | |
| self.scale_s.set(1.0) | |
| def view(self) -> View: | |
| """Current smoothed camera.""" | |
| return self._view | |
| def step_view(self) -> View: | |
| """Advance the smoothed camera.""" | |
| self._view = View(self.yaw_s(self.yaw), self.pitch_s(self.pitch), | |
| self.roll, self.scale_s(self.scale)) | |
| self.canvas.set_view(self._view) | |
| return self._view | |
| def on_frame(self, frame: np.ndarray) -> None: | |
| """Feed the depth worker.""" | |
| self.step_view() | |
| self._frame_index += 1 | |
| if self._frame_index % max(1, self.args.depth_every) == 0: | |
| self.depth.submit(frame) | |
| latest = self.depth.latest | |
| if latest is None or self.depth.frames == self._depth_seq: | |
| return | |
| self._depth_seq = self.depth.frames | |
| self.depth_map = latest | |
| self.depth_norm = self.depth.latest_norm | |
| self.track_scene(latest) | |
| if not self._depth_announced: | |
| self._depth_announced = True | |
| self.toast("Depth ready. Move your hand closer or further to draw in depth") | |
| def track_scene(self, depth_map: np.ndarray) -> None: | |
| """Cancel global depth drift.""" | |
| sample = depth_map[::8, ::8] | |
| sample = sample[np.isfinite(sample)] | |
| if sample.size == 0: | |
| return | |
| median = float(np.median(sample)) | |
| if median <= 0.0: | |
| return | |
| if self._scene_ref is None: | |
| self._scene_ref = median | |
| else: | |
| self._scene_ref += SCENE_ALPHA * (median - self._scene_ref) | |
| self._scene_gain = float(np.clip(self._scene_ref / median, 0.5, 2.0)) | |
| def depth_at(self, point) -> float: | |
| """Metric depth under a screen point.""" | |
| if self.depth_map is None: | |
| return float("nan") | |
| d = DepthEstimator.sample(self.depth_map, float(point[0]), float(point[1]), | |
| default=float("nan")) | |
| return d * self._scene_gain | |
| def depth_readout(self) -> str: | |
| """Depth status line.""" | |
| if self.depth_map is None: | |
| return "depth: warming up" | |
| if self.canvas.depth_ref is None: | |
| return f"depth: {self.canvas.last_depth:.2f} m draw to set the zero plane" | |
| offset = (self.canvas.last_depth - self.canvas.depth_ref) * 100.0 | |
| turn = "yaw %+.0f pitch %+.0f" % (np.degrees(self._view.yaw), np.degrees(self._view.pitch)) | |
| return f"depth: {self.canvas.last_depth:.2f} m z {offset:+.0f} cm {turn}" | |
| def handle_ring(self, states: list) -> None: | |
| """Toggle depth colors on two ring fingers.""" | |
| rings = [s for s in states if s.gesture == Gesture.RING and s.stable] | |
| if len(rings) < 2: | |
| self._ring_since = None | |
| self._ring_latched = False | |
| return | |
| now = time.time() | |
| self._ring_since = self._ring_since or now | |
| if not self._ring_latched and now - self._ring_since >= RING_HOLD_SECONDS: | |
| self._ring_latched = True | |
| self.depth_view = not self.depth_view | |
| self.toast("Depth colors " + ("on" if self.depth_view else "off")) | |
| def handle_palm(self, states: list) -> None: | |
| """Reset the view on open palm.""" | |
| palms = [s for s in states if s.gesture == Gesture.OPEN_PALM and s.stable] | |
| if not palms: | |
| self._palm_since = None | |
| self._palm_latched = False | |
| return | |
| now = time.time() | |
| self._palm_since = self._palm_since or now | |
| if not self._palm_latched and now - self._palm_since >= PALM_RESET_SECONDS: | |
| self._palm_latched = True | |
| self.reset_view() | |
| self.toast("View reset") | |
| def reset_view(self) -> None: | |
| """Back to the front view.""" | |
| self.yaw, self.pitch, self.roll, self.scale = 0.0, 0.0, 0.0, 1.0 | |
| self.yaw_s.set(0.0) | |
| self.pitch_s.set(0.0) | |
| self.scale_s.set(1.0) | |
| self._grab = None | |
| self._grab_span = None | |
| self.step_view() | |
| def rotate_view(self, grabbing: list) -> None: | |
| """Rotate and scale the drawing.""" | |
| p = grabbing[0].pinch_point | |
| if self._grab is None: | |
| self._grab = (p.copy(), self.yaw, self.pitch) | |
| anchor, yaw0, pitch0 = self._grab | |
| dx = (p[0] - anchor[0]) / self.w | |
| dy = (p[1] - anchor[1]) / self.h | |
| self.yaw = yaw0 + dx * 2.0 * np.pi * 1.1 | |
| self.pitch = float(np.clip(pitch0 + dy * np.pi * 1.1, -1.3, 1.3)) | |
| if len(grabbing) >= 2: | |
| span = float(np.linalg.norm(grabbing[0].pinch_point - grabbing[1].pinch_point)) | |
| if self._grab_span is None: | |
| self._grab_span = (span, self.scale) | |
| span0, scale0 = self._grab_span | |
| if span0 > 1e-3: | |
| self.scale = float(np.clip(scale0 * (span / span0), 0.25, 4.0)) | |
| else: | |
| self._grab_span = None | |
| def handle_draw_mode(self, states: list, dt: float) -> None: | |
| """Draw in 3D, grab to rotate.""" | |
| self.handle_ring(states) | |
| self.handle_palm(states) | |
| drawing = [s for s in states if s.gesture == Gesture.DRAW] | |
| grabbing = [s for s in states if s.gesture == Gesture.GRAB] | |
| if grabbing: | |
| self.stop_stroke(force=True) | |
| self.rotate_view(grabbing) | |
| self.grab_since = None | |
| return | |
| self._grab = None | |
| self._grab_span = None | |
| if drawing: | |
| point = self.tip_filter(drawing[0].cursor, dt) | |
| self.canvas.begin(self._view) | |
| self.canvas.add_point(point, self.depth_at(point), dt, self._view) | |
| self._draw_lost_since = None | |
| self.grab_since = None | |
| return | |
| self.stop_stroke() | |
| self.grab_since = None | |
| def clear_canvas(self) -> None: | |
| """Wipe the drawing.""" | |
| self.stop_stroke(force=True) | |
| self.canvas.clear() | |
| self._grab = None | |
| self._grab_span = None | |
| self.grab_since = None | |
| self.toast("Canvas cleared") | |
| def background(self, frame: np.ndarray) -> np.ndarray: | |
| """Camera or depth colored frame.""" | |
| if not self.depth_view or self.depth_norm is None: | |
| return frame | |
| colored = DepthEstimator.colorize(self.depth_norm) | |
| return cv2.addWeighted(colored, DEPTH_MIX, frame, 1.0 - DEPTH_MIX, 0.0) | |
| def render(self, frame: np.ndarray, hands, states) -> np.ndarray: | |
| """Compose the output frame.""" | |
| view = self._view | |
| out = self.canvas.render_over(self.background(frame), view) | |
| if self.show_skeleton: | |
| for hand, st in zip(hands, states): | |
| ui.draw_hand(out, hand, st) | |
| pen = self.canvas.pen_color() | |
| active = self.active_state(states) | |
| if active is not None: | |
| ui.draw_cursor(out, active.cursor, active.gesture, pen, self.canvas.thickness) | |
| shown = Gesture.NONE if active is None else active.gesture | |
| if len([s for s in states if s.gesture == Gesture.GRAB]) >= 2: | |
| shown = Gesture.ZOOM | |
| ui.draw_hud( | |
| out, | |
| gesture=shown, | |
| hands_info=[(s.handedness, s.gesture) for s in states], | |
| mode_3d=False, | |
| fps=self.fps, | |
| device=self.device, | |
| color=pen, | |
| thickness=self.canvas.thickness, | |
| strokes=len(self.canvas.strokes), | |
| clear_progress=0.0, | |
| toast=self.toast_text if time.time() < self.toast_until else "", | |
| mode_label="3D DRAW", | |
| hint="Finger draw Fist rotate Two rings depth", | |
| ) | |
| ui.put_text(out, self.depth_readout(), (16, 84), 16, (170, 220, 255)) | |
| if self.show_debug and states: | |
| ui.draw_debug(out, hands, states) | |
| if self.show_help: | |
| ui.draw_help(out, ui.HELP_LINES_3D) | |
| return out | |
| def handle_key(self, key: int) -> bool: | |
| """Handle one keypress.""" | |
| if key == ord("g"): | |
| self.canvas.use_gradient() | |
| self.toast("Depth gradient color") | |
| return True | |
| if key == ord(" "): | |
| self.depth_view = not self.depth_view | |
| self.toast("Depth colors " + ("on" if self.depth_view else "off")) | |
| return True | |
| if key == ord("r"): | |
| self.reset_view() | |
| self.toast("View reset") | |
| return True | |
| return super().handle_key(key) | |
| def run(self) -> None: | |
| """Run the main loop.""" | |
| try: | |
| super().run() | |
| finally: | |
| self.depth.close() | |