split example visualizaiton pipeline
This commit is contained in:
@@ -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