#!/usr/bin/env python3 """ Real-time UV-based perspective correction for the exported finger mesh. This version replaces the previous cylindrical approximation with a remap built from the mesh UV parameterization. The remap is precomputed once and then applied to each live frame with cv2.remap(). """ import argparse import sys import time import cv2 import numpy as np import orisys from orisys.frame_processors import UVUnrollFrameProcessor from orisys.unroll import ( barycentric_grid, build_or_load_domain, compute_uv_distortion_metrics, face_uvs_from_loop_data, load_calib_json, load_exported_data, recommend_edge_expand, uv_triangle_to_image, ) class StepTimer: """Accumulate per-step wall times for rolling runtime averages.""" def __init__(self): self._starts = {} self.totals = {} self.counts = {} def start(self, name): self._starts[name] = time.perf_counter() def stop(self, name): dt = time.perf_counter() - self._starts.pop(name) self.totals[name] = self.totals.get(name, 0.0) + dt self.counts[name] = self.counts.get(name, 0) + 1 return dt def avg_ms(self, name): if self.counts.get(name, 0) == 0: return 0.0 return 1000.0 * self.totals[name] / self.counts[name] def benchmark_step(label, func, *args, **kwargs): """Run func once and print its wall time.""" t0 = time.perf_counter() result = func(*args, **kwargs) ms = (time.perf_counter() - t0) * 1000.0 print(f"[benchmark] {label}: {ms:.1f} ms") return result def print_runtime_benchmark(timer): """Format rolling average timings for the live loop.""" parts = [ f"capture={timer.avg_ms('capture'):.1f}ms", f"uv_prep={timer.avg_ms('uv_prepare'):.1f}ms", f"uv_remap={timer.avg_ms('uv_remap'):.1f}ms", f"get_img={timer.avg_ms('get_img'):.1f}ms", f"flow={timer.avg_ms('flow'):.1f}ms", f"hhd={timer.avg_ms('hhd'):.1f}ms", f"contact={timer.avg_ms('contact'):.1f}ms", f"viz={timer.avg_ms('viz'):.1f}ms", ] return "bench[" + ", ".join(parts) + "]" def resize_to_height(image, target_height): """Resize an image to a target height while preserving aspect ratio.""" if image.shape[0] == target_height: return image scale = target_height / float(image.shape[0]) target_width = max(1, int(round(image.shape[1] * scale))) return cv2.resize(image, (target_width, target_height), interpolation=cv2.INTER_AREA) def resize_to_fit(image, target_width, target_height): """Resize with aspect ratio preserved and pad to exact tile size.""" h, w = image.shape[:2] if h <= 0 or w <= 0: return np.zeros((target_height, target_width, 3), dtype=np.uint8) scale = min(target_width / float(w), target_height / float(h)) new_w = max(1, int(round(w * scale))) new_h = max(1, int(round(h * scale))) resized = cv2.resize(image, (new_w, new_h), interpolation=cv2.INTER_AREA) if resized.ndim == 2: resized = cv2.cvtColor(resized, cv2.COLOR_GRAY2BGR) elif resized.ndim == 3 and resized.shape[2] == 1: resized = cv2.cvtColor(resized[:, :, 0], cv2.COLOR_GRAY2BGR) canvas = np.zeros((target_height, target_width, 3), dtype=np.uint8) y0 = (target_height - new_h) // 2 x0 = (target_width - new_w) // 2 canvas[y0:y0 + new_h, x0:x0 + new_w] = resized return canvas def add_panel_title(image, title): """Overlay a simple title onto a display tile.""" out = image.copy() cv2.rectangle(out, (0, 0), (out.shape[1], 36), (0, 0, 0), thickness=-1) cv2.putText(out, title, (12, 24), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2, cv2.LINE_AA) return out def apply_display_mask(image, valid_mask, border_value=0): """Zero invalid UV pixels in visualization panels.""" if image is None or valid_mask is None: return image out = image.copy() mask = valid_mask.astype(np.uint8) if mask.shape != out.shape[:2]: mask = cv2.resize(mask, (out.shape[1], out.shape[0]), interpolation=cv2.INTER_NEAREST) invalid = ~mask.astype(bool) out[invalid] = border_value return out def contact_inside_mask(centroid, valid_mask): if centroid is None or valid_mask is None: return None if len(centroid) < 2: return None cx, cy = int(centroid[0]), int(centroid[1]) if cy < 0 or cy >= valid_mask.shape[0] or cx < 0 or cx >= valid_mask.shape[1]: return False return bool(valid_mask[cy, cx]) def overlay_runtime_status(image, sensor, centroid=None): """Annotate a panel with mask-aware runtime diagnostics.""" out = image.copy() stats = getattr(sensor, "mask_stats", {}) or {} coverage = 100.0 * float(stats.get("coverage", 0.0)) flow_valid = float(stats.get("flow_mean_valid", 0.0)) flow_invalid = float(stats.get("flow_mean_invalid", 0.0)) centroid_ok = contact_inside_mask(centroid, getattr(sensor, "valid_mask", None)) centroid_label = "n/a" if centroid_ok is None else ("inside" if centroid_ok else "outside") lines = [ f"mask {coverage:.1f}%", f"flow valid {flow_valid:.3f}", f"flow invalid {flow_invalid:.3f}", f"contact {centroid_label}", ] y = 54 for line in lines: cv2.putText(out, line, (12, y), cv2.FONT_HERSHEY_SIMPLEX, 0.55, (255, 255, 255), 2, cv2.LINE_AA) y += 24 return out def make_tiled_dashboard(panels, tile_width=640, tile_height=360): """Arrange four panels into a 2x2 dashboard.""" tiles = [ add_panel_title(resize_to_fit(image, tile_width, tile_height), title) for title, image in panels ] top_row = np.hstack(tiles[:2]) bottom_row = np.hstack(tiles[2:4]) return np.vstack([top_row, bottom_row]) def make_uv_debug_image(data, out_width, out_height, flip_v=True, edge_band=0.12, edge_expand=1.0): """Create a UV-domain reference image to verify orientation of the remap.""" face_uvs = face_uvs_from_loop_data(data["faces"], data["uv_data"], data["loop_to_vertex"]) uv_img = np.zeros((out_height, out_width, 3), dtype=np.uint8) label_color = (255, 255, 255) for uv_tri in face_uvs: tri = uv_triangle_to_image( uv_tri, out_width, out_height, flip_v=flip_v, edge_band=edge_band, edge_expand=edge_expand, ) pts = np.round(tri).astype(np.int32) mask = np.zeros((out_height, out_width), dtype=np.uint8) cv2.fillConvexPoly(mask, pts, 255) x0 = max(0, int(np.floor(np.min(tri[:, 0])))) x1 = min(out_width - 1, int(np.ceil(np.max(tri[:, 0])))) y0 = max(0, int(np.floor(np.min(tri[:, 1])))) y1 = min(out_height - 1, int(np.ceil(np.max(tri[:, 1])))) if x1 < x0 or y1 < y0: continue yy, xx = np.mgrid[y0:y1 + 1, x0:x1 + 1] inside = mask[y0:y1 + 1, x0:x1 + 1] > 0 if not np.any(inside): continue bary = barycentric_grid(xx + 0.5, yy + 0.5, tri) if bary is None: continue w0, w1, w2 = bary valid = inside & (w0 >= -1e-6) & (w1 >= -1e-6) & (w2 >= -1e-6) if not np.any(valid): continue uv = ( w0[valid, None] * uv_tri[0] + w1[valid, None] * uv_tri[1] + w2[valid, None] * uv_tri[2] ) colors = np.zeros((uv.shape[0], 3), dtype=np.uint8) colors[:, 0] = np.clip(uv[:, 0] * 255.0, 0, 255).astype(np.uint8) colors[:, 1] = np.clip(uv[:, 1] * 255.0, 0, 255).astype(np.uint8) colors[:, 2] = np.clip((1.0 - 0.5 * (uv[:, 0] + uv[:, 1])) * 255.0, 0, 255).astype(np.uint8) uv_img[yy[valid], xx[valid]] = colors cv2.polylines(uv_img, [pts.reshape(-1, 1, 2)], isClosed=True, color=(255, 255, 255), thickness=1) cv2.arrowedLine(uv_img, (20, out_height - 20), (min(out_width - 20, 140), out_height - 20), (0, 0, 255), 2, tipLength=0.15) cv2.arrowedLine(uv_img, (20, out_height - 20), (20, max(20, out_height - 140)), (0, 255, 0), 2, tipLength=0.15) cv2.putText(uv_img, "U", (min(out_width - 30, 145), out_height - 28), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2, cv2.LINE_AA) cv2.putText(uv_img, "V", (28, max(30, out_height - 145)), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2, cv2.LINE_AA) cv2.putText(uv_img, "UV Map", (20, 35), cv2.FONT_HERSHEY_SIMPLEX, 0.9, label_color, 2, cv2.LINE_AA) if edge_expand > 1.0: cv2.putText(uv_img, f"Edge expand {edge_expand:.2f}x", (20, 64), cv2.FONT_HERSHEY_SIMPLEX, 0.55, label_color, 2, cv2.LINE_AA) return uv_img def make_uv_distortion_image(data, distortion, out_width, out_height, flip_v=True, edge_band=0.12, edge_expand=1.0): """Render UV distortion heatmap; red means more 3D surface per UV area.""" face_uvs = face_uvs_from_loop_data(data["faces"], data["uv_data"], data["loop_to_vertex"]) uv_img = np.zeros((out_height, out_width, 3), dtype=np.uint8) rel = distortion["relative_scale"] edge_mask = distortion["edge_mask"] valid_rel = np.isfinite(rel) & (rel > 0) if not np.any(valid_rel): return uv_img log_rel = np.log2(rel[valid_rel]) clip = max(0.25, float(np.percentile(np.abs(log_rel), 95))) for idx, uv_tri in enumerate(face_uvs): tri = uv_triangle_to_image( uv_tri, out_width, out_height, flip_v=flip_v, edge_band=edge_band, edge_expand=edge_expand, ) pts = np.round(tri).astype(np.int32) if not np.isfinite(rel[idx]) or rel[idx] <= 0: color = (64, 64, 64) else: value = np.clip((np.log2(rel[idx]) + clip) / (2.0 * clip), 0.0, 1.0) color_idx = np.uint8(round(value * 255.0)) color = tuple(int(c) for c in cv2.applyColorMap(np.array([[color_idx]], dtype=np.uint8), cv2.COLORMAP_JET)[0, 0]) cv2.fillConvexPoly(uv_img, pts, color) edge_color = (255, 255, 255) if edge_mask[idx] else (180, 180, 180) edge_thickness = 2 if edge_mask[idx] else 1 cv2.polylines(uv_img, [pts.reshape(-1, 1, 2)], isClosed=True, color=edge_color, thickness=edge_thickness) cv2.putText(uv_img, "UV Distortion", (20, 35), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (255, 255, 255), 2, cv2.LINE_AA) cv2.putText(uv_img, "Blue: denser UV Red: sparser UV", (20, 64), cv2.FONT_HERSHEY_SIMPLEX, 0.55, (220, 220, 220), 2, cv2.LINE_AA) cv2.putText(uv_img, "White outlines: edge-band faces", (20, 92), cv2.FONT_HERSHEY_SIMPLEX, 0.55, (220, 220, 220), 2, cv2.LINE_AA) middle_stats = distortion["middle_stats"] edge_stats = distortion["edge_stats"] cv2.putText( uv_img, f"middle p05/p95: {middle_stats['relative_scale_p05']:.2f}x / {middle_stats['relative_scale_p95']:.2f}x", (20, 120), cv2.FONT_HERSHEY_SIMPLEX, 0.55, (220, 220, 220), 2, cv2.LINE_AA, ) cv2.putText( uv_img, f"edge p05/p95: {edge_stats['relative_scale_p05']:.2f}x / {edge_stats['relative_scale_p95']:.2f}x", (20, 148), cv2.FONT_HERSHEY_SIMPLEX, 0.55, (220, 220, 220), 2, cv2.LINE_AA, ) return uv_img def open_source(video_path, camera_id): return cv2.VideoCapture(video_path) if video_path else cv2.VideoCapture(camera_id) PERF_TUNING_EPILOG = """ Performance tuning (config JSON + CLI): tracker.dis_preset medium -> fast/ultrafast faster DIS optical flow tracker.loop_closure_interval 1 -> 3-5 fewer loop-closure remaps tracker.hhd_downsample 3 -> 4 smaller HHD Poisson grid contact.denoise_sigma 12 -> 6 or 0 faster FoE contact step --masked_poisson_solver sparse vs capfft benchmark HHD solve time --no-viz skip dashboard/imshow ~20 ms/frame savings """ def run_deformation_timed(sensor, timer, check_motion=True, threshold=0, tail_frames=10): """Run flow + gated HHD with separate benchmark buckets.""" timer.start("flow") sensor.compute_flow() timer.stop("flow") timer.start("hhd") run_hhd = True if check_motion: is_motion = sensor.detect_motion(threshold=threshold) try: tail_frames = max(0, int(tail_frames)) except (TypeError, ValueError): tail_frames = 0 if is_motion: sensor.tail_frames_count = 0 elif sensor.tail_frames_count < tail_frames: sensor.tail_frames_count += 1 else: run_hhd = False sensor.tail_frames_count = tail_frames if run_hhd: sensor.compute_hhd() timer.stop("hhd") def configure_masked_poisson(sensor, args): """Apply masked-HHD solver settings to the externally fed SDK sensor.""" sensor.masked_poisson_solver = args.masked_poisson_solver sensor.masked_poisson_max_boundary = args.masked_poisson_max_boundary sensor.masked_poisson_boundary_stride = args.masked_poisson_boundary_stride sensor.masked_poisson_max_iters = args.masked_poisson_max_iters sensor.masked_poisson_tol = args.masked_poisson_tol sensor.masked_poisson_reg = args.masked_poisson_reg def main(): parser = argparse.ArgumentParser( description="Real-time UV-based finger unrolling", epilog=PERF_TUNING_EPILOG, formatter_class=argparse.RawDescriptionHelpFormatter, ) parser.add_argument("--mesh", required=True, help="Exported mesh.npz path") parser.add_argument("--K", dest="intrinsics_path", required=True, help="Exported camera_intrinsics.npz path") parser.add_argument("--pose", required=True, help="Exported world_to_cam.npy path") parser.add_argument("--calib", default=None, help="OpenCV calibration JSON for the live camera") # With a 12% UV edge band on each side, only about 76% of each axis is the # lower-distortion middle region. Use a slightly larger full remap so the # useful center keeps roughly the same sampling density as the prior 506x550. parser.add_argument("--out_width", type=int, default=504) parser.add_argument("--out_height", type=int, default=506) parser.add_argument("--camera_id", type=int, default=0) parser.add_argument("--video", help="Video file path (overrides camera)") parser.add_argument("--map_cache", default="unroll_map_uv.npz") parser.add_argument("--no_flip_v", action="store_true", help="Keep UV V increasing downward") parser.add_argument("--no_flip_source_vertical", action="store_true", help="Do not vertically flip the live source before UV remap") parser.add_argument("--edge_band", type=float, default=0.12, help="Normalized UV band near the boundary treated as high-distortion edge") parser.add_argument("--edge_expand", type=float, default=0.0, help="Extra UV allocation for edge band; 0 chooses automatically from distortion stats") parser.add_argument("--sdk_config", default="./config/dd02-ov.json", help="Orisys SDK config path/name") parser.add_argument("--sdk_source", default=None, help="Dummy source used to initialize Sensor; defaults to --video or --camera_id") parser.add_argument("--masked_poisson_solver", choices=["sparse", "capfft"], default="sparse", help="Masked HHD Poisson solver used inside the SDK") parser.add_argument("--masked_poisson_max_boundary", type=int, default=96, help="Maximum number of UV-boundary samples used by the capfft solver") parser.add_argument("--masked_poisson_boundary_stride", type=int, default=1, help="Subsample stride applied before capfft boundary sampling") parser.add_argument("--masked_poisson_max_iters", type=int, default=3, help="Maximum capfft correction iterations per Poisson solve") parser.add_argument("--masked_poisson_tol", type=float, default=1e-4, help="Residual tolerance used by the capfft correction loop") parser.add_argument("--masked_poisson_reg", type=float, default=1e-8, help="Diagonal regularization applied to the capacitance matrix") parser.add_argument("--verbose", action="store_true", help="Enable verbose SDK logs") parser.add_argument( "--no-viz", action="store_true", help="Skip OpenCV dashboard; print forces and benchmark timings only", ) args = parser.parse_args() print("[benchmark] Startup timing:") data = benchmark_step("load_exported_data", load_exported_data, args.mesh, args.intrinsics_path, args.pose) flip_v = not args.no_flip_v flip_source_vertical = not args.no_flip_source_vertical distortion = benchmark_step( "compute_uv_distortion_metrics", compute_uv_distortion_metrics, data, edge_band=args.edge_band, ) distortion_stats = distortion["stats"] middle_stats = distortion["middle_stats"] edge_stats = distortion["edge_stats"] edge_expand = args.edge_expand if args.edge_expand > 1.0 else recommend_edge_expand(distortion) print( "UV distortion stats: " f"median mm/uv={distortion_stats['median_mm_per_uv']:.3f}, " f"relative scale p05/p95={distortion_stats['relative_scale_p05']:.2f}x/" f"{distortion_stats['relative_scale_p95']:.2f}x, " f"anisotropy p50/p95={distortion_stats['anisotropy_p50']:.2f}x/" f"{distortion_stats['anisotropy_p95']:.2f}x" ) print(f"Using UV edge warp: band={args.edge_band:.2f}, expand={edge_expand:.2f}x") print( "UV middle-region stats: " f"faces={middle_stats['face_count']}, " f"relative scale p05/p95={middle_stats['relative_scale_p05']:.2f}x/" f"{middle_stats['relative_scale_p95']:.2f}x, " f"anisotropy p50/p95={middle_stats['anisotropy_p50']:.2f}x/" f"{middle_stats['anisotropy_p95']:.2f}x" ) print( "UV edge-band stats: " f"faces={edge_stats['face_count']}, " f"relative scale p05/p95={edge_stats['relative_scale_p05']:.2f}x/" f"{edge_stats['relative_scale_p95']:.2f}x, " f"anisotropy p50/p95={edge_stats['anisotropy_p50']:.2f}x/" f"{edge_stats['anisotropy_p95']:.2f}x" ) if args.calib: source_K, dist_coeffs, img_size = benchmark_step("load_calib_json", load_calib_json, args.calib) source_width, source_height = img_size print(f"Calibration loaded. Live image size: {(source_width, source_height)}") else: source_K = data["K"] dist_coeffs = None source_width = data["width"] source_height = data["height"] print("No calibration file provided; using exported intrinsics for projection.") domain = benchmark_step( "build_or_load_domain", build_or_load_domain, args.mesh, args.intrinsics_path, args.pose, args.map_cache, source_K, source_width, source_height, args.out_width, args.out_height, flip_v=flip_v, edge_band=args.edge_band, edge_expand=edge_expand, include_surface=True, ) remap = domain.remap remap.ensure_fast_maps() edge_expand = float(remap.metadata.get("edge_expand", edge_expand)) uv_debug = benchmark_step( "make_uv_debug_image", make_uv_debug_image, data, out_width=args.out_width, out_height=args.out_height, flip_v=flip_v, edge_band=args.edge_band, edge_expand=edge_expand, ) uv_distortion = benchmark_step( "make_uv_distortion_image", make_uv_distortion_image, data, distortion, out_width=args.out_width, out_height=args.out_height, flip_v=flip_v, edge_band=args.edge_band, edge_expand=edge_expand, ) uv_reference = benchmark_step( "uv_reference_compose", lambda: np.hstack( [ resize_to_height(uv_debug, args.out_height), resize_to_height(uv_distortion, args.out_height), ] ), ) cap = open_source(args.video, args.camera_id) if not cap.isOpened(): print("Cannot open camera/video source") sys.exit(1) cap.set(cv2.CAP_PROP_FRAME_WIDTH, source_width) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, source_height) sensor = benchmark_step( "sensor_init", lambda: orisys.Sensor( "external", config_name=args.sdk_config, verbose=args.verbose, mirror=False, ), ) configure_masked_poisson(sensor, args) print( "Masked HHD solver: " f"{sensor.masked_poisson_solver} " f"(max_boundary={sensor.masked_poisson_max_boundary}, " f"stride={sensor.masked_poisson_boundary_stride}, " f"iters={sensor.masked_poisson_max_iters}, " f"tol={sensor.masked_poisson_tol:.1e})" ) frame_processor = UVUnrollFrameProcessor( domain.remap, camera_matrix=source_K if dist_coeffs is not None else None, dist_coeffs=dist_coeffs, source_width=source_width, source_height=source_height, flip_source_vertical=flip_source_vertical, border_value=(0, 0, 0), ) ok, warmup_frame = cap.read() if ok: warmup_prepared = frame_processor.prepare_source(warmup_frame) warmup_unrolled = frame_processor.flatten_prepared(warmup_prepared) sensor.get_img( frame=warmup_unrolled, already_processed=True, valid_mask=frame_processor.valid_mask, ) benchmark_step("hhd_warmup", sensor.warmup_deformation) else: print("[benchmark] hhd_warmup: skipped (no frame from source)") if args.no_viz: print("Running with --no-viz (dashboard disabled).") else: print("Press 'q' to quit.") runtime_timer = StepTimer() try: while True: runtime_timer.start("capture") ok, frame = cap.read() runtime_timer.stop("capture") if not ok: break runtime_timer.start("uv_prepare") frame_source = frame_processor.prepare_source(frame) runtime_timer.stop("uv_prepare") runtime_timer.start("uv_remap") unrolled = frame_processor.flatten_prepared(frame_source) runtime_timer.stop("uv_remap") runtime_timer.start("get_img") sensor.get_img( frame=unrolled, already_processed=True, valid_mask=frame_processor.valid_mask, ) runtime_timer.stop("get_img") run_deformation_timed(sensor, runtime_timer, check_motion=True, threshold=0) runtime_timer.start("contact") sensor.compute_contact() runtime_timer.stop("contact") fps = sensor._fps fnormal = sensor.fnormal fshearx = sensor.fshearx fsheary = sensor.fsheary contact_ok = contact_inside_mask(sensor.centroid, sensor.valid_mask) if not args.no_viz: runtime_timer.start("viz") fps, flow, vnormal, vshear, img, contour, centroid, depth_map, fnormal, fshearx, fsheary = sensor.read_info( sensor.info.FPS, sensor.info.VRAW, sensor.info.VNORMAL, sensor.info.VSHEAR, sensor.info.IMG, sensor.info.CONTOUR, sensor.info.CENTROID, sensor.info.DEPTH, sensor.info.FNORMAL, sensor.info.FSHEARX, sensor.info.FSHEARY, ) arrows = orisys.util.draw_arrows( img, flow, threshold=1, grid_spacing=15, arrow_scale=1.0, ) contact_img = orisys.util.draw_contact(img, contour, centroid) depth_norm = depth_map if depth_norm.size > 0 and depth_norm.max() > depth_norm.min(): depth_u8 = ((depth_norm - depth_norm.min()) / (depth_norm.max() - depth_norm.min()) * 255).astype(np.uint8) else: depth_u8 = np.zeros_like(depth_norm, dtype=np.uint8) depth_colored = cv2.applyColorMap(depth_u8, cv2.COLORMAP_JET) display_mask = sensor.valid_mask arrows = apply_display_mask(arrows, display_mask, border_value=(0, 0, 0)) contact_img = apply_display_mask(contact_img, display_mask, border_value=(0, 0, 0)) depth_colored = apply_display_mask(depth_colored, display_mask, border_value=(0, 0, 0)) arrows = overlay_runtime_status(arrows, sensor, centroid=centroid) contact_ok = contact_inside_mask(centroid, display_mask) dashboard = make_tiled_dashboard( [ ("Original", frame_source), ("UV Map | Distortion", uv_reference), ("UV Unrolled", unrolled), ("Flow", arrows), ], tile_width=640, tile_height=360, ) cv2.imshow("UV Unroll Dashboard", dashboard) runtime_timer.stop("viz") print( f"FPS={fps:.2f}, 法向力={fnormal:.4f}, 切向力X={fshearx:.4f}, " f"切向力Y={fsheary:.4f}, 展开图尺寸={unrolled.shape[:2]}, " f"solver={sensor.masked_poisson_solver}, " f"mask={sensor.mask_stats['coverage']:.3f}, " f"flow(valid/invalid)=({sensor.mask_stats['flow_mean_valid']:.3f}/" f"{sensor.mask_stats['flow_mean_invalid']:.3f}), " f"contact_in_mask={contact_ok}, " f"{print_runtime_benchmark(runtime_timer)}", end="\r", ) key = cv2.waitKey(1) & 0xFF if key == ord("q"): print("\n正在退出程序...") break if key == ord("r"): sensor.reset() print("\n追踪器已重置。") finally: cap.release() sensor.disconnect() cv2.destroyAllWindows() if __name__ == "__main__": main()