split example visualizaiton pipeline
This commit is contained in:
+41
-50
@@ -1,50 +1,41 @@
|
||||
# Examples
|
||||
|
||||
This folder contains user-facing example scripts for the Orisys SDK.
|
||||
|
||||
## Files
|
||||
|
||||
- `basic_pipeline.py`: single-sensor basic processing and visualization flow
|
||||
- `two_sensor_pipeline.py`: dual-sensor multiprocessing example
|
||||
- `plot_data.py`: real-time plotting example with PyQtGraph
|
||||
|
||||
## Typical usage
|
||||
|
||||
Run examples from the repository root so relative paths such as `./config/ddjx01.json`
|
||||
resolve correctly.
|
||||
|
||||
### Basic pipeline
|
||||
|
||||
```bash
|
||||
python examples/basic_pipeline.py -v 0 -c ./config/ddjx01.json
|
||||
```
|
||||
|
||||
### Two-sensor pipeline
|
||||
|
||||
```bash
|
||||
python examples/two_sensor_pipeline.py
|
||||
```
|
||||
|
||||
### Real-time plotting
|
||||
|
||||
```bash
|
||||
python examples/plot_data.py -v 0 -c ./config/ddjx01.json
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python environment with the SDK dependencies installed
|
||||
- a supported camera or test video
|
||||
- valid JSON config in `./config/`
|
||||
|
||||
Some examples also require:
|
||||
|
||||
- `pyqtgraph`
|
||||
- `PyQt5`
|
||||
- multiple cameras for multi-sensor demos
|
||||
|
||||
## Notes
|
||||
|
||||
- Most examples assume a tactile sensor camera is connected.
|
||||
- Press `q` in the display window to quit where applicable.
|
||||
- If your camera index is not `0`, change `-v` accordingly.
|
||||
# Examples
|
||||
|
||||
User-facing demo scripts for the Orisys SDK. Run from the repository root.
|
||||
|
||||
## Layout
|
||||
|
||||
- `basic_pipeline.py` — compound-eye sensor, OpenCV visualization
|
||||
- `plot_data.py` — compound-eye sensor, force charts (PyQtGraph)
|
||||
- `two_sensor_pipeline.py` — dual-sensor multiprocessing demo
|
||||
- `finger_pipeline.py` — finger UV minimal pipeline
|
||||
- `finger_pipeline_3dview.py` — finger UV + Open3D mesh viewer
|
||||
- `finger_plot_data.py` — finger UV + force charts
|
||||
- `viz/` — shared visualization helpers used by plot and finger examples
|
||||
- `mesh_viewer.py` — re-exports `UVMeshViewer` / `build_uv_surface_mesh` from `orisys.finger`
|
||||
- `finger_panels.py` — 2D OpenCV panels for finger demos
|
||||
|
||||
## Typical usage
|
||||
|
||||
```bash
|
||||
python examples/basic_pipeline.py -v 0 -c ./config/ddjx01.json
|
||||
python examples/plot_data.py -v 0 -c ./config/ddjx01.json
|
||||
python examples/finger_plot_data.py -v 0 -c ./config/dd02-ov.json
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- SDK installed (`pip install -e .`)
|
||||
- camera or test video
|
||||
- JSON config under `./config/`
|
||||
|
||||
Plot examples additionally require:
|
||||
|
||||
```bash
|
||||
pip install pyqt5 pyqtgraph
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Press `q` to quit OpenCV windows where applicable.
|
||||
- Plot examples: `s` starts recording, `e` stops recording.
|
||||
|
||||
+41
-162
@@ -3,7 +3,7 @@
|
||||
Minimum UV-unroll finger tactile pipeline.
|
||||
|
||||
Offline: load Blender export -> build_or_load_domain -> unroll_map_uv.npz
|
||||
Runtime: camera frame -> UVUnrollFrameProcessor -> Sensor -> forces/contact
|
||||
Runtime: camera frame -> UVUnrollFrameProcessor -> TactileProcessor -> forces/contact
|
||||
|
||||
For the full demo (UV debug panels, dashboard, benchmarks) see:
|
||||
tools/3d_patch_generation_blender/unroll_realtime.py
|
||||
@@ -17,190 +17,67 @@ os.environ.setdefault("OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS", "0")
|
||||
|
||||
import cv2
|
||||
import orisys
|
||||
from orisys.configs import load_config
|
||||
from orisys.frame_processors import UVUnrollFrameProcessor
|
||||
from orisys.unroll import (
|
||||
build_or_load_domain,
|
||||
compute_uv_distortion_metrics,
|
||||
load_calib_json,
|
||||
load_exported_data,
|
||||
recommend_edge_expand,
|
||||
resolve_uv_runtime_sizes,
|
||||
scale_camera_geometry,
|
||||
scaled_map_cache_path,
|
||||
)
|
||||
|
||||
|
||||
def open_source(video_path, camera_id):
|
||||
return cv2.VideoCapture(video_path) if video_path else cv2.VideoCapture(camera_id)
|
||||
|
||||
|
||||
def resolve_export_paths(export_dir):
|
||||
"""Return Blender export paths expected inside one folder."""
|
||||
export_dir = os.path.abspath(export_dir)
|
||||
paths = {
|
||||
"mesh": os.path.join(export_dir, "mesh.npz"),
|
||||
"intrinsics": os.path.join(export_dir, "camera_intrinsics.npz"),
|
||||
"pose": os.path.join(export_dir, "world_to_cam.npy"),
|
||||
}
|
||||
missing = [name for name, path in paths.items() if not os.path.isfile(path)]
|
||||
if missing:
|
||||
raise FileNotFoundError(
|
||||
f"Missing export file(s) in {export_dir}: {', '.join(missing)}"
|
||||
)
|
||||
return paths["mesh"], paths["intrinsics"], paths["pose"]
|
||||
from orisys.finger import FingerRuntime
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Minimum UV-unroll finger tactile pipeline")
|
||||
parser.add_argument(
|
||||
"--export-dir",
|
||||
required=True,
|
||||
help="Blender export folder containing mesh.npz, camera_intrinsics.npz, world_to_cam.npy",
|
||||
)
|
||||
parser.add_argument("--calib", default=None, help="OpenCV calibration JSON for the live camera")
|
||||
parser.add_argument("--map_cache", default="unroll_map_uv.npz", help="Cached UV domain .npz")
|
||||
parser.add_argument("--camera_id", type=int, default=0)
|
||||
parser.add_argument("--video", help="Video file path (overrides camera)")
|
||||
parser.add_argument("--config", "-c", default="./config/dd02-ov.json", help="Orisys SDK config")
|
||||
parser.add_argument(
|
||||
"--source_scale",
|
||||
type=float,
|
||||
default=None,
|
||||
help="Resize live source before UV remap (default: camera.source_scale in config)",
|
||||
)
|
||||
parser.add_argument("--no_flip_source_vertical", action="store_true", help="Skip vertical flip before UV remap")
|
||||
parser.add_argument("--show", action="store_true", help="Show unrolled image window")
|
||||
parser.add_argument("--show", action="store_true", default=True, help="Show unrolled image window")
|
||||
parser.add_argument("--verbose", action="store_true", help="SDK verbose logs")
|
||||
args = parser.parse_args()
|
||||
|
||||
sdk_config = load_config(args.config)
|
||||
camera_config = sdk_config.get("camera", {})
|
||||
out_width, out_height, source_scale = resolve_uv_runtime_sizes(
|
||||
camera_config,
|
||||
source_scale=args.source_scale,
|
||||
)
|
||||
map_cache = scaled_map_cache_path(args.map_cache, source_scale)
|
||||
try:
|
||||
video = args.camera_id
|
||||
if args.video:
|
||||
video = args.video
|
||||
runtime = FingerRuntime.open(
|
||||
config=args.config,
|
||||
video=video,
|
||||
verbose=args.verbose,
|
||||
)
|
||||
except RuntimeError as exc:
|
||||
print(str(exc), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
mesh_path, intrinsics_path, pose_path = resolve_export_paths(args.export_dir)
|
||||
|
||||
data = load_exported_data(mesh_path, intrinsics_path, pose_path)
|
||||
distortion = compute_uv_distortion_metrics(data)
|
||||
edge_expand = recommend_edge_expand(distortion)
|
||||
|
||||
if args.calib:
|
||||
source_K, dist_coeffs, img_size = load_calib_json(args.calib)
|
||||
native_width, native_height = img_size
|
||||
else:
|
||||
source_K = data["K"]
|
||||
dist_coeffs = None
|
||||
native_width = data["width"]
|
||||
native_height = data["height"]
|
||||
|
||||
source_K, source_width, source_height = scale_camera_geometry(
|
||||
source_K,
|
||||
native_width,
|
||||
native_height,
|
||||
source_scale,
|
||||
)
|
||||
if args.verbose:
|
||||
geometry = runtime.geometry
|
||||
print(
|
||||
f"UV geometry: native={native_width}x{native_height}, "
|
||||
f"source={source_width}x{source_height} (scale={source_scale:g}), "
|
||||
f"output={out_width}x{out_height}, map_cache={map_cache}"
|
||||
f"UV geometry: native={geometry.native_width}x{geometry.native_height}, "
|
||||
f"source={geometry.source_width}x{geometry.source_height} "
|
||||
f"output={geometry.out_width}x{geometry.out_height} "
|
||||
)
|
||||
|
||||
domain = build_or_load_domain(
|
||||
mesh_path,
|
||||
intrinsics_path,
|
||||
pose_path,
|
||||
map_cache,
|
||||
source_K,
|
||||
source_width,
|
||||
source_height,
|
||||
out_width,
|
||||
out_height,
|
||||
edge_expand=edge_expand,
|
||||
include_surface=True,
|
||||
verbose=args.verbose,
|
||||
source_scale=source_scale,
|
||||
)
|
||||
domain.remap.ensure_fast_maps()
|
||||
|
||||
cap = open_source(args.video, args.camera_id)
|
||||
if not cap.isOpened():
|
||||
print("Cannot open camera/video source", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
cap.set(cv2.CAP_PROP_FRAME_WIDTH, native_width)
|
||||
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, native_height)
|
||||
|
||||
sensor = orisys.Sensor("external", config_name=args.config, verbose=args.verbose, mirror=False)
|
||||
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=not args.no_flip_source_vertical,
|
||||
)
|
||||
|
||||
ok, frame = cap.read()
|
||||
if ok:
|
||||
if args.show:
|
||||
prepared = processor.prepare_source(frame)
|
||||
sensor.get_img(
|
||||
frame=processor.flatten_prepared(prepared),
|
||||
already_processed=True,
|
||||
valid_mask=processor.valid_mask,
|
||||
)
|
||||
else:
|
||||
prepared_gray = processor.prepare_source_gray(frame)
|
||||
sensor.get_img(
|
||||
frame=processor.flatten_prepared_gray(prepared_gray),
|
||||
already_processed=True,
|
||||
already_gray=True,
|
||||
valid_mask=processor.valid_mask,
|
||||
)
|
||||
sensor.warmup_deformation()
|
||||
runtime.warmup(color=False)
|
||||
tactile = runtime.tactile
|
||||
|
||||
print("Press 'q' to quit, 'r' to reset tracker.")
|
||||
try:
|
||||
while True:
|
||||
ok, frame = cap.read()
|
||||
ok, unrolled = runtime.grab_and_process(color=False)
|
||||
if not ok:
|
||||
break
|
||||
tactile.compute_deformation(check_motion=True, threshold=0)
|
||||
tactile.compute_contact()
|
||||
|
||||
if args.show:
|
||||
prepared = processor.prepare_source(frame)
|
||||
unrolled = processor.flatten_prepared(prepared)
|
||||
sensor.get_img(
|
||||
frame=unrolled,
|
||||
already_processed=True,
|
||||
valid_mask=processor.valid_mask,
|
||||
)
|
||||
else:
|
||||
prepared_gray = processor.prepare_source_gray(frame)
|
||||
unrolled = processor.flatten_prepared_gray(prepared_gray)
|
||||
sensor.get_img(
|
||||
frame=unrolled,
|
||||
already_processed=True,
|
||||
already_gray=True,
|
||||
valid_mask=processor.valid_mask,
|
||||
)
|
||||
sensor.compute_deformation(check_motion=True, threshold=0)
|
||||
sensor.compute_contact()
|
||||
|
||||
fps, flow,fnormal, fshearx, fsheary = sensor.read_info(
|
||||
sensor.info.FPS,
|
||||
sensor.info.VRAW,
|
||||
sensor.info.FNORMAL,
|
||||
sensor.info.FSHEARX,
|
||||
sensor.info.FSHEARY,
|
||||
fps, flow, vnormal, img, contour, centroid, depth_map, fnormal, fshearx, fsheary = tactile.read_info(
|
||||
tactile.info.FPS,
|
||||
tactile.info.VRAW,
|
||||
tactile.info.VNORMAL,
|
||||
tactile.info.IMG,
|
||||
tactile.info.CONTOUR,
|
||||
tactile.info.CENTROID,
|
||||
tactile.info.DEPTH,
|
||||
tactile.info.FNORMAL,
|
||||
tactile.info.FSHEARX,
|
||||
tactile.info.FSHEARY
|
||||
)
|
||||
|
||||
print(
|
||||
f"FPS={fps:.2f} normal={fnormal:.2f} shear=({fshearx:.2f}, {fsheary:.2f}) "
|
||||
f"mask={sensor.mask_stats['coverage']:.2f}",
|
||||
f"mask={tactile.mask_stats['coverage']:.2f}",
|
||||
end="\r",
|
||||
)
|
||||
|
||||
@@ -214,6 +91,9 @@ def main():
|
||||
grid_spacing=10,
|
||||
arrow_scale=1.0,
|
||||
)
|
||||
# 步骤 3.2:显示接触区域和质心
|
||||
image_with_foe = orisys.util.draw_contact(img, contour, centroid)
|
||||
cv2.imshow("接触区域与质心", image_with_foe)
|
||||
cv2.imshow("UV Unrolled", arrows)
|
||||
|
||||
key = cv2.waitKey(1) & 0xFF
|
||||
@@ -221,11 +101,10 @@ def main():
|
||||
print("\nExiting.")
|
||||
break
|
||||
if key == ord("r"):
|
||||
sensor.reset()
|
||||
tactile.reset()
|
||||
print("\nTracker reset.")
|
||||
finally:
|
||||
cap.release()
|
||||
sensor.disconnect()
|
||||
runtime.close()
|
||||
cv2.destroyAllWindows()
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
UV-unroll finger tactile pipeline with 3D mesh visualization.
|
||||
|
||||
Offline: load Blender export -> build_or_load_domain -> unroll_map_uv.npz
|
||||
Runtime: camera frame -> UVUnrollFrameProcessor -> TactileProcessor -> forces/contact
|
||||
UV-space flow/HHD -> grey-white 3D finger mesh (Open3D)
|
||||
|
||||
For the full 2D dashboard demo see:
|
||||
tools/3d_patch_generation_blender/unroll_realtime.py
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
os.environ.setdefault("OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS", "0")
|
||||
|
||||
import cv2
|
||||
from orisys.finger import DEFAULT_FINGER_EXPORT_DIR, DEFAULT_FINGER_MAP_CACHE, FingerRuntime
|
||||
|
||||
_EXAMPLES_DIR = Path(__file__).resolve().parent
|
||||
if str(_EXAMPLES_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(_EXAMPLES_DIR))
|
||||
|
||||
from viz import UVMeshViewer, build_uv_surface_mesh, show_finger_uv_panels
|
||||
|
||||
|
||||
def open_mesh_viewer(runtime, args):
|
||||
domain = runtime.domain
|
||||
if domain.surface is None:
|
||||
raise RuntimeError(
|
||||
"UV domain has no world_xyz surface map. Rebuild cache with include_surface=True."
|
||||
)
|
||||
|
||||
uv_mesh = build_uv_surface_mesh(domain.surface.world_xyz, domain.valid_mask)
|
||||
if args.no_3d:
|
||||
return None, uv_mesh
|
||||
|
||||
viewer = UVMeshViewer(
|
||||
uv_mesh,
|
||||
max_magnitude=args.magnitude_max,
|
||||
threshold=args.depth_threshold if args.mesh_map == "depth" else args.flow_threshold,
|
||||
color_mode=args.mesh_map,
|
||||
)
|
||||
print(
|
||||
f"3D mesh viewer: {len(uv_mesh.rest_vertices)} vertices, "
|
||||
f"{len(uv_mesh.triangles)} triangles, {args.mesh_map} surface mode"
|
||||
)
|
||||
return viewer, uv_mesh
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="UV-unroll finger tactile pipeline with 3D mesh viewer"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--export-dir",
|
||||
default=DEFAULT_FINGER_EXPORT_DIR,
|
||||
help="Blender export folder containing mesh.npz, camera_intrinsics.npz, world_to_cam.npy",
|
||||
)
|
||||
parser.add_argument("--map_cache", default=DEFAULT_FINGER_MAP_CACHE, help="Cached UV domain .npz")
|
||||
parser.add_argument("--camera_id", type=int, default=0)
|
||||
parser.add_argument("--video", help="Video file path (overrides camera)")
|
||||
parser.add_argument("--config", "-c", default="./config/dd02-ov.json", help="Orisys SDK config")
|
||||
parser.add_argument(
|
||||
"--source_scale",
|
||||
type=float,
|
||||
default=None,
|
||||
help="Resize live source before UV remap (default: camera.source_scale in config)",
|
||||
)
|
||||
parser.add_argument("--no_flip_source_vertical", action="store_true", help="Skip vertical flip before UV remap")
|
||||
parser.add_argument("--show", action="store_true", help="Show 2D unrolled image window with flow arrows")
|
||||
parser.add_argument("--verbose", action="store_true", help="SDK verbose logs")
|
||||
parser.add_argument(
|
||||
"--magnitude-max",
|
||||
type=float,
|
||||
default=None,
|
||||
help="Upper optical-flow magnitude for 3D surface heat-map scaling; default uses frame percentile.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--mesh-map",
|
||||
choices=["flow", "depth"],
|
||||
default="depth",
|
||||
help="Color the 3D mesh from optical-flow magnitude or the contact depth/divergence map.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--flow-threshold",
|
||||
type=float,
|
||||
default=0.4,
|
||||
help="Minimum optical-flow magnitude to color 3D surface / draw 2D arrows (matches draw_arrows).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--depth-threshold",
|
||||
type=float,
|
||||
default=0.0,
|
||||
help="Minimum depth/divergence value to color on the 3D mesh.",
|
||||
)
|
||||
parser.add_argument("--no-3d", action="store_true", help="Disable Open3D mesh viewer")
|
||||
args = parser.parse_args()
|
||||
|
||||
video = args.camera_id
|
||||
if args.video:
|
||||
video = args.video
|
||||
|
||||
try:
|
||||
runtime = FingerRuntime.open(
|
||||
export_dir=args.export_dir,
|
||||
config=args.config,
|
||||
video=video,
|
||||
verbose=args.verbose,
|
||||
)
|
||||
except RuntimeError as exc:
|
||||
print(str(exc), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if args.verbose:
|
||||
geometry = runtime.geometry
|
||||
print(
|
||||
f"UV geometry: native={geometry.native_width}x{geometry.native_height}, "
|
||||
f"source={geometry.source_width}x{geometry.source_height} "
|
||||
f"(scale={geometry.source_scale:g}), output={geometry.out_width}x{geometry.out_height}, "
|
||||
f"map_cache={geometry.map_cache}"
|
||||
)
|
||||
|
||||
mesh_viewer, _uv_mesh = open_mesh_viewer(runtime, args)
|
||||
runtime.warmup(color=False)
|
||||
tactile = runtime.tactile
|
||||
|
||||
print("Press 'q' to quit, 'r' to reset tracker. Close the 3D window to stop.")
|
||||
try:
|
||||
while True:
|
||||
ok, unrolled = runtime.grab_and_process(color=False)
|
||||
if not ok:
|
||||
break
|
||||
|
||||
tactile.compute_deformation(decompose=True, check_motion=True, threshold=0)
|
||||
tactile.compute_contact()
|
||||
|
||||
fps, flow, vnormal, img, contour, centroid, depth_map, fnormal, fshearx, fsheary = tactile.read_info(
|
||||
tactile.info.FPS,
|
||||
tactile.info.VRAW,
|
||||
tactile.info.VNORMAL,
|
||||
tactile.info.IMG,
|
||||
tactile.info.CONTOUR,
|
||||
tactile.info.CENTROID,
|
||||
tactile.info.DEPTH,
|
||||
tactile.info.FNORMAL,
|
||||
tactile.info.FSHEARX,
|
||||
tactile.info.FSHEARY,
|
||||
)
|
||||
flow_mag = float((flow[:, :, 0] ** 2 + flow[:, :, 1] ** 2).mean() ** 0.5) if flow.size else 0.0
|
||||
|
||||
print(
|
||||
f"FPS={fps:.2f} flow_rms={flow_mag:.2f} "
|
||||
f"mask={tactile.mask_stats['coverage']:.2f} normal={fnormal:.2f} shear=({fshearx:.2f}, {fsheary:.2f})",
|
||||
end="\r",
|
||||
)
|
||||
|
||||
if mesh_viewer is not None:
|
||||
if not mesh_viewer.update(flow=flow, scalar_map=depth_map):
|
||||
print("\n3D viewer closed.")
|
||||
break
|
||||
|
||||
if args.show:
|
||||
show_finger_uv_panels(
|
||||
unrolled,
|
||||
vnormal,
|
||||
depth_map,
|
||||
flow_threshold=args.flow_threshold,
|
||||
)
|
||||
|
||||
key = cv2.waitKey(1) & 0xFF
|
||||
if key == ord("q"):
|
||||
print("\nExiting.")
|
||||
break
|
||||
if key == ord("r"):
|
||||
tactile.reset()
|
||||
print("\nTracker reset.")
|
||||
finally:
|
||||
runtime.close()
|
||||
if mesh_viewer is not None:
|
||||
mesh_viewer.close()
|
||||
cv2.destroyAllWindows()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,101 @@
|
||||
"""
|
||||
Finger UV pipeline with real-time force charts and OpenCV panels.
|
||||
|
||||
Demonstrates:
|
||||
1. UV-unrolled deformation arrows
|
||||
2. Contact region overlay
|
||||
3. PyQtGraph normal/shear force plots
|
||||
4. Optional raw-frame recording (s/e keys)
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
||||
|
||||
import cv2
|
||||
import orisys
|
||||
from orisys.finger import FingerRuntime
|
||||
|
||||
_EXAMPLES_DIR = Path(__file__).resolve().parent
|
||||
if str(_EXAMPLES_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(_EXAMPLES_DIR))
|
||||
|
||||
from viz import ForceChartWindow, FrameRecorder, lowpass_filter
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--video", "-v", type=str, default="0", help="视频源:摄像头编号或视频文件路径")
|
||||
parser.add_argument("--config", "-c", type=str, default="./config/dd02-ov.json", help="配置文件名称或路径")
|
||||
parser.add_argument("--verbose", "-verbose", type=bool, default=True, help="是否输出详细日志")
|
||||
args = parser.parse_args()
|
||||
|
||||
charts = ForceChartWindow()
|
||||
recorder = FrameRecorder()
|
||||
|
||||
runtime = FingerRuntime.open(
|
||||
config=args.config,
|
||||
video=args.video,
|
||||
verbose=args.verbose,
|
||||
)
|
||||
runtime.warmup(color=True)
|
||||
tactile = runtime.tactile
|
||||
start_time = time.time()
|
||||
|
||||
print("\n按键说明:'q' 退出程序,'s' 开始录像,'e' 结束录像。")
|
||||
try:
|
||||
while True:
|
||||
ok, unrolled = runtime.grab_and_process(color=True)
|
||||
if not ok:
|
||||
break
|
||||
|
||||
recorder.write_if_recording(tactile.frame)
|
||||
|
||||
tactile.compute_deformation(check_motion=True, threshold=0)
|
||||
tactile.compute_contact()
|
||||
fps, flow, vnormal, img, contour, centroid, depth_map, fnormal, fshearx, fsheary = tactile.read_info(
|
||||
tactile.info.FPS,
|
||||
tactile.info.VRAW,
|
||||
tactile.info.VNORMAL,
|
||||
tactile.info.IMG,
|
||||
tactile.info.CONTOUR,
|
||||
tactile.info.CENTROID,
|
||||
tactile.info.DEPTH,
|
||||
tactile.info.FNORMAL,
|
||||
tactile.info.FSHEARX,
|
||||
tactile.info.FSHEARY,
|
||||
)
|
||||
print(f"FPS={fps:.2f}, 法向力={fnormal:.4f}, 切向力X={fshearx:.4f}, 切向力Y={fsheary:.4f}")
|
||||
|
||||
arrows = orisys.util.draw_arrows(unrolled, flow, threshold=3, grid_spacing=10, arrow_scale=1.0)
|
||||
cv2.imshow("arrows", arrows)
|
||||
cv2.imshow("contact", orisys.util.draw_contact(img, contour, centroid))
|
||||
|
||||
charts.push(time.time() - start_time, fnormal, fshearx, fsheary)
|
||||
display = charts.data.copy()
|
||||
for channel in range(3):
|
||||
display[:, 1, channel] = lowpass_filter(
|
||||
charts.data[:, 1, channel],
|
||||
cutoff_hz=5,
|
||||
sample_rate_hz=30,
|
||||
)
|
||||
charts.refresh(display)
|
||||
|
||||
key = cv2.waitKey(1)
|
||||
if key & 0xFF == ord("q"):
|
||||
print("\n正在退出示例程序...")
|
||||
break
|
||||
if key == ord("s"):
|
||||
recorder.start(tactile.frame)
|
||||
if key == ord("e"):
|
||||
recorder.stop()
|
||||
finally:
|
||||
runtime.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+63
-225
@@ -6,252 +6,90 @@
|
||||
2. 显示流场幅值热力图
|
||||
3. 使用 PyQtGraph 实时绘制法向力与切向力曲线
|
||||
4. 按键控制录像开始与结束
|
||||
|
||||
SDK version: 0.3.1
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
||||
|
||||
import cv2
|
||||
import orisys
|
||||
import numpy as np
|
||||
import time
|
||||
import argparse
|
||||
try:
|
||||
import pyqtgraph as pg
|
||||
except:
|
||||
print("PyQtGraph is not installed. Please install it using 'pip install pyqt5 pyqtgraph'.")
|
||||
exit()
|
||||
|
||||
def lp_filter(series, alpha=0.5):
|
||||
series = np.asarray(series)
|
||||
if series.ndim == 0:
|
||||
return float(series)
|
||||
if series.shape[0] < 2:
|
||||
return float(series[-1])
|
||||
return float(series[-1] * alpha + series[-2] * (1 - alpha))
|
||||
_EXAMPLES_DIR = Path(__file__).resolve().parent
|
||||
if str(_EXAMPLES_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(_EXAMPLES_DIR))
|
||||
|
||||
def median_filter(series, window_size=5):
|
||||
series = np.asarray(series)
|
||||
if series.ndim == 0:
|
||||
return float(series)
|
||||
if series.shape[0] == 0:
|
||||
return 0.0
|
||||
from viz import ForceChartWindow, FrameRecorder, draw_magnitude_map, lowpass_filter
|
||||
|
||||
|
||||
def open_sensor(video: str, config: str, cal: str, verbose: bool):
|
||||
try:
|
||||
window_n = max(1, int(window_size))
|
||||
except (TypeError, ValueError):
|
||||
window_n = 1
|
||||
return float(np.median(series[-window_n:]))
|
||||
def moving_average(series, window_size=5):
|
||||
series = np.asarray(series)
|
||||
if series.ndim == 0:
|
||||
return float(series)
|
||||
if series.shape[0] == 0:
|
||||
return 0.0
|
||||
try:
|
||||
window_n = max(1, int(window_size))
|
||||
except (TypeError, ValueError):
|
||||
window_n = 1
|
||||
return float(np.mean(series[-window_n:]))
|
||||
|
||||
def create_plot():
|
||||
p1 = win.addPlot(title="法向力")
|
||||
p1.setLabel('bottom', '时间', units='s')
|
||||
win.nextRow()
|
||||
p2 = win.addPlot(title="切向力 X")
|
||||
p2.setLabel('bottom', '时间', units='s')
|
||||
win.nextRow()
|
||||
p3 = win.addPlot(title="切向力 Y")
|
||||
p3.setLabel('bottom', '时间', units='s')
|
||||
plot_af = p1.plot(pen='k')
|
||||
plot_tfx = p2.plot(pen='k')
|
||||
plot_tfy = p3.plot(pen='k')
|
||||
return p1, p2, p3, plot_af, plot_tfx, plot_tfy
|
||||
|
||||
def update(plot_af, plot_tfx, plot_tfy, data, p1, p2, p3):
|
||||
# global plot_af, plot_tfx, plot_tfy, data, p1, p2, p3
|
||||
plot_af.setData(data[:,:,0])
|
||||
p1.setYRange(min(data[:,1,0]),max(data[:,1,0]))
|
||||
plot_tfx.setData(data[:,:,1])
|
||||
p2.setYRange(min(data[:,1,1]),max(data[:,1,1]))
|
||||
plot_tfy.setData(data[:,:,2])
|
||||
p3.setYRange(min(data[:,1,2]),max(data[:,1,2]))
|
||||
|
||||
# 深度图
|
||||
def draw_magnitude_map(vfield, threshold=3.0, colormap=cv2.COLORMAP_JET) -> np.ndarray:
|
||||
"""
|
||||
绘制流场幅值伪彩色图。
|
||||
|
||||
Args:
|
||||
vfield: 需要可视化的二维矢量场
|
||||
threshold: 最小显示阈值;低于阈值的区域显示为低值颜色
|
||||
colormap: OpenCV 颜色映射类型(默认 COLORMAP_JET)
|
||||
|
||||
Returns:
|
||||
np.ndarray: 400x400 的 BGR 伪彩图像
|
||||
"""
|
||||
flow_field = vfield
|
||||
|
||||
# 计算每个像素点的幅值
|
||||
magnitude = np.sqrt(flow_field[:, :, 0]**2 + flow_field[:, :, 1]**2)
|
||||
|
||||
# 先降采样再插值回原尺寸,使显示更平滑
|
||||
downsample_factor = 10
|
||||
h, w = magnitude.shape
|
||||
magnitude_downsampled = magnitude[::downsample_factor, ::downsample_factor]
|
||||
magnitude_smoothed = cv2.resize(magnitude_downsampled, (w, h), interpolation=cv2.INTER_CUBIC)
|
||||
|
||||
# 应用阈值,将低于阈值的区域置零
|
||||
magnitude_thresholded = magnitude_smoothed.copy()
|
||||
magnitude_thresholded[magnitude_smoothed < threshold] = 0
|
||||
|
||||
# 归一化到 0-255
|
||||
mag_min, mag_max = magnitude_thresholded.min(), magnitude_thresholded.max()
|
||||
if mag_max > mag_min:
|
||||
magnitude_norm = ((magnitude_thresholded - mag_min) / (mag_max - mag_min) * 255).astype(np.uint8)
|
||||
else:
|
||||
magnitude_norm = np.zeros_like(magnitude_thresholded, dtype=np.uint8)
|
||||
|
||||
# 应用伪彩色映射
|
||||
magnitude_colored = cv2.applyColorMap(magnitude_norm, colormap)
|
||||
|
||||
# 缩放到固定输出尺寸
|
||||
out_img = cv2.resize(magnitude_colored, (400, 400), interpolation=cv2.INTER_CUBIC)
|
||||
|
||||
return out_img
|
||||
|
||||
# 实时图表
|
||||
npoints = 100 # 曲线缓冲区长度
|
||||
pg.setConfigOption('background', 'w')
|
||||
pg.setConfigOption('foreground', 'k')
|
||||
app = pg.mkQApp("Orisys Plot Example")
|
||||
win = pg.GraphicsLayoutWidget(show=True, title="Orisys 实时力曲线")
|
||||
win.setWindowTitle('Orisys 实时力曲线')
|
||||
p1, p2, p3, plot_af, plot_tfx, plot_tfy = create_plot()
|
||||
data = np.zeros([npoints,2,3])
|
||||
start_time = None # Will be set when sensor starts
|
||||
for i in range(3):
|
||||
data[:,0,i] = np.linspace(-npoints,0,npoints)/30
|
||||
|
||||
source = int(video)
|
||||
except ValueError:
|
||||
source = video
|
||||
return orisys.Sensor(source, config_name=config, cal_path=cal, verbose=verbose)
|
||||
|
||||
|
||||
def main():
|
||||
global start_time
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--video","-v", type=str, default="0", help="视频源:摄像头编号或视频文件路径")
|
||||
parser.add_argument("--config","-c", type=str, default="./config/ddjx01.json", help="配置文件名称或路径")
|
||||
parser.add_argument("--cal","-cal", type=str, default="./config/ddjx01.npy", help="标定文件路径")
|
||||
parser.add_argument("--verbose","-verbose", type=bool, default=True, help="是否输出详细日志")
|
||||
parser.add_argument("--video", "-v", type=str, default="0", help="视频源:摄像头编号或视频文件路径")
|
||||
parser.add_argument("--config", "-c", type=str, default="./config/ddjx01.json", help="配置文件名称或路径")
|
||||
parser.add_argument("--cal", "-cal", type=str, default="./config/ddjx01.npy", help="标定文件路径")
|
||||
parser.add_argument("--verbose", "-verbose", type=bool, default=True, help="是否输出详细日志")
|
||||
args = parser.parse_args()
|
||||
|
||||
# 步骤 1:创建传感器对象(必需)
|
||||
# config_name 可以是内置配置名称,也可以是自定义配置文件路径
|
||||
# cal_path 为标定文件路径;每台设备建议使用独立的标定文件
|
||||
|
||||
# 输入既可以是摄像头编号,也可以是视频文件路径
|
||||
try:
|
||||
input = int(args.video)
|
||||
is_camera = True
|
||||
sensor = orisys.Sensor(input, config_name=args.config, cal_path= args.cal, verbose=args.verbose)
|
||||
except:
|
||||
is_camera = False
|
||||
sensor = orisys.Sensor(args.video, config_name=args.config, cal_path= args.cal, verbose=args.verbose)
|
||||
# 下面保留了历史示例写法,便于参考
|
||||
# sensor1 = orisys.Sensor("./s1.mp4", config_name="./config/ddjx01.json", cal_path= "./config/ddjx01.npy", cuda=False, verbose=True)
|
||||
|
||||
charts = ForceChartWindow()
|
||||
recorder = FrameRecorder()
|
||||
sensor = open_sensor(args.video, args.config, args.cal, args.verbose)
|
||||
start_time = time.time()
|
||||
is_save = False
|
||||
frame_count = 0
|
||||
writer = None
|
||||
save_path = None
|
||||
|
||||
print("\n按键说明:'q' 退出程序,'s' 开始录像,'e' 结束录像。")
|
||||
while True:
|
||||
sensor.get_img() # 获取并拼接复眼图像
|
||||
|
||||
if is_save and writer is not None and sensor.frame is not None:
|
||||
frame_count += 1
|
||||
save_img = sensor.frame
|
||||
|
||||
# 若为灰度图,则先转换为 BGR 再写入视频
|
||||
if len(save_img.shape) == 2 or (len(save_img.shape) == 3 and save_img.shape[2] == 1):
|
||||
if len(save_img.shape) == 3:
|
||||
save_img = save_img.squeeze(2)
|
||||
save_img = cv2.cvtColor(save_img, cv2.COLOR_GRAY2BGR)
|
||||
|
||||
writer.write(save_img)
|
||||
try:
|
||||
while True:
|
||||
sensor.get_img()
|
||||
recorder.write_if_recording(sensor.frame)
|
||||
|
||||
sensor.compute_deformation(check_motion=True, threshold=0) # 计算形变与分解结果
|
||||
# 读取信息:
|
||||
# 向量场:VRAW、VNORMAL、VSHEAR -> 原始 / 法向 / 切向
|
||||
# 标量:FNORMAL、FSHEARX、FSHEARY -> 法向力 / X 向切向力 / Y 向切向力
|
||||
fps, fn, fx, fy, flow, vnormal, vshear = sensor.read_info(sensor.info.FPS, sensor.info.FNORMAL, sensor.info.FSHEARX, sensor.info.FSHEARY, sensor.info.VRAW, sensor.info.VNORMAL,sensor.info.VSHEAR)
|
||||
print(f"FPS={fps:.2f}, 法向力={fn:.4f}, 切向力X={fx:.4f}, 切向力Y={fy:.4f}")
|
||||
# 显示形变矢量场
|
||||
arrows = orisys.util.draw_arrows(sensor.img, flow, threshold=5, grid_spacing=10, arrow_scale=1.0)
|
||||
cv2.imshow("形变矢量场", arrows)
|
||||
# 显示流场幅值图
|
||||
mag = draw_magnitude_map(flow)
|
||||
cv2.imshow("流场幅值图", mag)
|
||||
sensor.compute_deformation(check_motion=True, threshold=0)
|
||||
fps, fn, fx, fy, flow, vnormal, vshear = sensor.read_info(
|
||||
sensor.info.FPS,
|
||||
sensor.info.FNORMAL,
|
||||
sensor.info.FSHEARX,
|
||||
sensor.info.FSHEARY,
|
||||
sensor.info.VRAW,
|
||||
sensor.info.VNORMAL,
|
||||
sensor.info.VSHEAR,
|
||||
)
|
||||
print(f"FPS={fps:.2f}, 法向力={fn:.4f}, 切向力X={fx:.4f}, 切向力Y={fy:.4f}")
|
||||
|
||||
# 更新实时曲线
|
||||
current_time = time.time() - start_time
|
||||
data_filtered = data.copy()
|
||||
for i in range(3):
|
||||
data[:-1, 0, i] = data[1:, 0, i] # 时间轴左移
|
||||
data[:-1, 1, i] = data[1:, 1, i] # 力数据左移
|
||||
data[-1, 0, :] = current_time # 写入当前时间
|
||||
data[-1, 1, 0] = fn
|
||||
data[-1, 1, 1] = fx
|
||||
data[-1, 1, 2] = fy
|
||||
# 对最新采样点做一阶低通,滤波对象是历史曲线而不是单个标量。
|
||||
data_filtered[-1, 1, 0] = moving_average(data[:, 1, 0])
|
||||
data_filtered[-1, 1, 1] = moving_average(data[:, 1, 1])
|
||||
data_filtered[-1, 1, 2] = moving_average(data[:, 1, 2])
|
||||
update(plot_af, plot_tfx, plot_tfy, data_filtered, p1, p2, p3) # 更新曲线
|
||||
arrows = orisys.util.draw_arrows(sensor.img, flow, threshold=5, grid_spacing=10, arrow_scale=1.0)
|
||||
cv2.imshow("形变矢量场", arrows)
|
||||
cv2.imshow("流场幅值图", draw_magnitude_map(flow))
|
||||
|
||||
key = cv2.waitKey(1)
|
||||
if key & 0xFF == ord("q"):
|
||||
print("\n正在退出示例程序...")
|
||||
break
|
||||
if key == ord("s"):
|
||||
save_path = f"./data/pulse_test.mp4"
|
||||
# 确保输出目录存在
|
||||
os.makedirs(os.path.dirname(save_path), exist_ok=True)
|
||||
# 从当前帧获取视频尺寸
|
||||
save_img = sensor.frame
|
||||
charts.push(time.time() - start_time, fn, fx, fy)
|
||||
display = charts.data.copy()
|
||||
for channel in range(3):
|
||||
display[:, 1, channel] = lowpass_filter(
|
||||
charts.data[:, 1, channel],
|
||||
cutoff_hz=5,
|
||||
sample_rate_hz=30,
|
||||
)
|
||||
charts.refresh(display)
|
||||
|
||||
if save_img is not None:
|
||||
# 如为灰度图,先转换为 BGR 以获取视频尺寸
|
||||
temp_img = save_img.copy()
|
||||
if len(temp_img.shape) == 2 or (len(temp_img.shape) == 3 and temp_img.shape[2] == 1):
|
||||
if len(temp_img.shape) == 3:
|
||||
temp_img = temp_img.squeeze(2)
|
||||
temp_img = cv2.cvtColor(temp_img, cv2.COLOR_GRAY2BGR)
|
||||
|
||||
frame_height, frame_width = temp_img.shape[:2]
|
||||
writer = cv2.VideoWriter(save_path, cv2.VideoWriter_fourcc(*'mp4v'), 30, (frame_width, frame_height))
|
||||
if writer.isOpened():
|
||||
is_save = True
|
||||
print(f"开始录像:{save_path}")
|
||||
else:
|
||||
print(f"无法创建录像文件:{save_path}")
|
||||
writer = None
|
||||
else:
|
||||
print("当前没有可用帧,无法确定录像尺寸。")
|
||||
if key == ord("e"):
|
||||
is_save = False
|
||||
if writer is not None:
|
||||
writer.release()
|
||||
writer = None
|
||||
if save_path is not None:
|
||||
print(f"录像结束:{save_path},总帧数:{frame_count}")
|
||||
else:
|
||||
print(f"录像结束,总帧数:{frame_count}")
|
||||
frame_count = 0
|
||||
|
||||
sensor.disconnect() # 断开传感器连接并释放资源
|
||||
|
||||
key = cv2.waitKey(1)
|
||||
if key & 0xFF == ord("q"):
|
||||
print("\n正在退出示例程序...")
|
||||
break
|
||||
if key == ord("s"):
|
||||
recorder.start(sensor.frame)
|
||||
if key == ord("e"):
|
||||
recorder.stop()
|
||||
finally:
|
||||
sensor.disconnect()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
"""Shared visualization helpers for Orisys example scripts."""
|
||||
|
||||
from .finger_panels import show_finger_uv_panels
|
||||
from .force_charts import ForceChartWindow
|
||||
from .mesh_viewer import UVMeshViewer, build_uv_surface_mesh
|
||||
from .opencv_maps import depth_colormap, draw_magnitude_map, frame_to_bgr
|
||||
from .recording import FrameRecorder
|
||||
from .signal_filters import lowpass_filter
|
||||
|
||||
__all__ = [
|
||||
"ForceChartWindow",
|
||||
"FrameRecorder",
|
||||
"UVMeshViewer",
|
||||
"build_uv_surface_mesh",
|
||||
"depth_colormap",
|
||||
"draw_magnitude_map",
|
||||
"frame_to_bgr",
|
||||
"lowpass_filter",
|
||||
"show_finger_uv_panels",
|
||||
]
|
||||
@@ -0,0 +1,29 @@
|
||||
"""Finger-specific OpenCV panels for example scripts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import cv2
|
||||
import orisys
|
||||
|
||||
from .opencv_maps import depth_colormap
|
||||
|
||||
|
||||
def show_finger_uv_panels(
|
||||
unrolled,
|
||||
vnormal,
|
||||
depth_map,
|
||||
*,
|
||||
flow_threshold: float = 0.4,
|
||||
grid_spacing: int = 20,
|
||||
arrow_scale: float = 1.0,
|
||||
) -> None:
|
||||
"""Show depth colormap and UV unrolled flow arrows."""
|
||||
cv2.imshow("Depth", depth_colormap(depth_map))
|
||||
arrows = orisys.util.draw_arrows(
|
||||
unrolled,
|
||||
vnormal,
|
||||
threshold=flow_threshold,
|
||||
grid_spacing=grid_spacing,
|
||||
arrow_scale=arrow_scale,
|
||||
)
|
||||
cv2.imshow("UV Unrolled", arrows)
|
||||
@@ -0,0 +1,69 @@
|
||||
"""PyQtGraph real-time force chart window for examples."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
def _require_pyqtgraph():
|
||||
try:
|
||||
import pyqtgraph as pg
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"PyQtGraph is not installed. Install with: pip install pyqt5 pyqtgraph"
|
||||
) from exc
|
||||
return pg
|
||||
|
||||
|
||||
class ForceChartWindow:
|
||||
"""Rolling normal/shear force charts backed by PyQtGraph."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
npoints: int = 100,
|
||||
sample_rate_hz: float = 30.0,
|
||||
title: str = "Orisys 实时力曲线",
|
||||
app_name: str = "Orisys Plot Example",
|
||||
) -> None:
|
||||
pg = _require_pyqtgraph()
|
||||
pg.setConfigOption("background", "w")
|
||||
pg.setConfigOption("foreground", "k")
|
||||
|
||||
self._pg = pg
|
||||
self.npoints = npoints
|
||||
self.sample_rate_hz = sample_rate_hz
|
||||
self.app = pg.mkQApp(app_name)
|
||||
self.win = pg.GraphicsLayoutWidget(show=True, title=title)
|
||||
self.win.setWindowTitle(title)
|
||||
|
||||
self.data = np.zeros((npoints, 2, 3), dtype=np.float64)
|
||||
for channel in range(3):
|
||||
self.data[:, 0, channel] = np.linspace(-npoints, 0, npoints) / sample_rate_hz
|
||||
|
||||
p1 = self.win.addPlot(title="法向力")
|
||||
p1.setLabel("bottom", "时间", units="s")
|
||||
self.win.nextRow()
|
||||
p2 = self.win.addPlot(title="切向力 X")
|
||||
p2.setLabel("bottom", "时间", units="s")
|
||||
self.win.nextRow()
|
||||
p3 = self.win.addPlot(title="切向力 Y")
|
||||
p3.setLabel("bottom", "时间", units="s")
|
||||
|
||||
self._plots = (p1, p2, p3)
|
||||
self._curves = (p1.plot(pen="k"), p2.plot(pen="k"), p3.plot(pen="k"))
|
||||
|
||||
def push(self, timestamp: float, fn: float, fx: float, fy: float) -> None:
|
||||
for channel in range(3):
|
||||
self.data[:-1, 0, channel] = self.data[1:, 0, channel]
|
||||
self.data[:-1, 1, channel] = self.data[1:, 1, channel]
|
||||
self.data[-1, 0, :] = timestamp
|
||||
self.data[-1, 1, 0] = fn
|
||||
self.data[-1, 1, 1] = fx
|
||||
self.data[-1, 1, 2] = fy
|
||||
|
||||
def refresh(self, display_data: np.ndarray | None = None) -> None:
|
||||
plot_data = self.data if display_data is None else display_data
|
||||
for channel, (plot, curve) in enumerate(zip(self._plots, self._curves)):
|
||||
curve.setData(plot_data[:, :, channel])
|
||||
plot.setYRange(float(plot_data[:, 1, channel].min()), float(plot_data[:, 1, channel].max()))
|
||||
@@ -0,0 +1,4 @@
|
||||
"""Re-exports for finger 3D mesh examples; implementation lives in orisys.finger."""
|
||||
|
||||
from orisys.finger.mesh import * # noqa: F403
|
||||
from orisys.finger.viewer import * # noqa: F403
|
||||
@@ -0,0 +1,57 @@
|
||||
"""OpenCV visualization helpers for example scripts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
|
||||
def depth_colormap(depth_map, colormap=cv2.COLORMAP_JET) -> np.ndarray:
|
||||
"""Normalize a scalar map to uint8 and apply an OpenCV colormap."""
|
||||
values = np.asarray(depth_map)
|
||||
if values.size == 0:
|
||||
return np.zeros((1, 1, 3), dtype=np.uint8)
|
||||
if values.max() > values.min():
|
||||
normalized = ((values - values.min()) / (values.max() - values.min()) * 255).astype(np.uint8)
|
||||
else:
|
||||
normalized = np.zeros_like(values, dtype=np.uint8)
|
||||
return cv2.applyColorMap(normalized, colormap)
|
||||
|
||||
|
||||
def draw_magnitude_map(
|
||||
vfield,
|
||||
threshold=3.0,
|
||||
colormap=cv2.COLORMAP_JET,
|
||||
output_size=(400, 400),
|
||||
) -> np.ndarray:
|
||||
"""Render a smoothed optical-flow magnitude map as a BGR pseudo-color image."""
|
||||
magnitude = np.sqrt(vfield[:, :, 0] ** 2 + vfield[:, :, 1] ** 2)
|
||||
|
||||
downsample_factor = 10
|
||||
h, w = magnitude.shape
|
||||
magnitude_downsampled = magnitude[::downsample_factor, ::downsample_factor]
|
||||
magnitude_smoothed = cv2.resize(magnitude_downsampled, (w, h), interpolation=cv2.INTER_CUBIC)
|
||||
|
||||
magnitude_thresholded = magnitude_smoothed.copy()
|
||||
magnitude_thresholded[magnitude_smoothed < threshold] = 0
|
||||
|
||||
mag_min, mag_max = magnitude_thresholded.min(), magnitude_thresholded.max()
|
||||
if mag_max > mag_min:
|
||||
magnitude_norm = ((magnitude_thresholded - mag_min) / (mag_max - mag_min) * 255).astype(np.uint8)
|
||||
else:
|
||||
magnitude_norm = np.zeros_like(magnitude_thresholded, dtype=np.uint8)
|
||||
|
||||
magnitude_colored = cv2.applyColorMap(magnitude_norm, colormap)
|
||||
out_w, out_h = output_size
|
||||
return cv2.resize(magnitude_colored, (out_w, out_h), interpolation=cv2.INTER_CUBIC)
|
||||
|
||||
|
||||
def frame_to_bgr(frame: np.ndarray) -> np.ndarray:
|
||||
"""Convert a grayscale or single-channel frame to BGR for display/recording."""
|
||||
if frame is None:
|
||||
raise ValueError("frame is None")
|
||||
if len(frame.shape) == 2:
|
||||
return cv2.cvtColor(frame, cv2.COLOR_GRAY2BGR)
|
||||
if len(frame.shape) == 3 and frame.shape[2] == 1:
|
||||
return cv2.cvtColor(frame.squeeze(2), cv2.COLOR_GRAY2BGR)
|
||||
return frame
|
||||
@@ -0,0 +1,71 @@
|
||||
"""Simple frame recording helper for example scripts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import cv2
|
||||
|
||||
from .opencv_maps import frame_to_bgr
|
||||
|
||||
|
||||
class FrameRecorder:
|
||||
"""Write camera frames to disk on demand (start/stop via keyboard hooks)."""
|
||||
|
||||
def __init__(self, save_path: str | Path = "./data/pulse_test.mp4", fps: int = 30) -> None:
|
||||
self.save_path = Path(save_path)
|
||||
self.fps = fps
|
||||
self._writer: cv2.VideoWriter | None = None
|
||||
self._is_recording = False
|
||||
self._frame_count = 0
|
||||
|
||||
@property
|
||||
def is_recording(self) -> bool:
|
||||
return self._is_recording
|
||||
|
||||
def write_if_recording(self, frame) -> None:
|
||||
if not self._is_recording or self._writer is None or frame is None:
|
||||
return
|
||||
self._writer.write(frame_to_bgr(frame))
|
||||
self._frame_count += 1
|
||||
|
||||
def start(self, frame) -> bool:
|
||||
if frame is None:
|
||||
print("当前没有可用帧,无法确定录像尺寸。")
|
||||
return False
|
||||
|
||||
self.save_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
bgr = frame_to_bgr(frame)
|
||||
height, width = bgr.shape[:2]
|
||||
writer = cv2.VideoWriter(
|
||||
str(self.save_path),
|
||||
cv2.VideoWriter_fourcc(*"mp4v"),
|
||||
self.fps,
|
||||
(width, height),
|
||||
)
|
||||
if not writer.isOpened():
|
||||
print(f"无法创建录像文件:{self.save_path}")
|
||||
return False
|
||||
|
||||
self._writer = writer
|
||||
self._is_recording = True
|
||||
self._frame_count = 0
|
||||
print(f"开始录像:{self.save_path}")
|
||||
return True
|
||||
|
||||
def stop(self) -> tuple[Path | None, int]:
|
||||
self._is_recording = False
|
||||
if self._writer is not None:
|
||||
self._writer.release()
|
||||
self._writer = None
|
||||
|
||||
path = self.save_path if self._frame_count else None
|
||||
if path is not None:
|
||||
print(f"录像结束:{path},总帧数:{self._frame_count}")
|
||||
else:
|
||||
print(f"录像结束,总帧数:{self._frame_count}")
|
||||
|
||||
count = self._frame_count
|
||||
self._frame_count = 0
|
||||
return path, count
|
||||
@@ -0,0 +1,27 @@
|
||||
"""Signal filtering helpers for example force charts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
from scipy.signal import butter, filtfilt
|
||||
|
||||
def lowpass_filter(data, cutoff_hz, sample_rate_hz=30, order=2) -> np.ndarray:
|
||||
"""Zero-phase Butterworth low-pass filter for uniformly sampled 1-D data."""
|
||||
y = np.asarray(data, dtype=np.float64)
|
||||
if cutoff_hz == 0:
|
||||
return y.copy()
|
||||
if y.size == 0:
|
||||
return y
|
||||
|
||||
nyquist = 0.5 * sample_rate_hz
|
||||
if cutoff_hz < 0 or cutoff_hz >= nyquist:
|
||||
raise ValueError(
|
||||
f"cutoff_hz must be in (0, {nyquist}) for sample_rate_hz={sample_rate_hz}"
|
||||
)
|
||||
|
||||
b, a = butter(order, cutoff_hz / nyquist, btype="low")
|
||||
padlen = 3 * (max(len(b), len(a)) - 1)
|
||||
if y.size <= padlen:
|
||||
return y.copy()
|
||||
|
||||
return filtfilt(b, a, y)
|
||||
Reference in New Issue
Block a user