included low pass filter in package and add to plot_data example

This commit is contained in:
ykcheungab
2026-08-17 22:11:17 +08:00
parent 9a69f01278
commit 2e09c9360e
50 changed files with 1055 additions and 1873 deletions
+13 -17
View File
@@ -1,54 +1,50 @@
# Examples
Product-facing demos live under `examples/products/`. Shared helpers stay in
Runnable demos live at the `examples/` root. Shared helpers stay in
`pipeline/`, `viz/`, and `gui/`. Run commands from the repository root.
## Product Entry Points
| Product | Entry | Default config |
|---------|-------|----------------|
| ORY-BOX | `products/ory_box/basic_pipeline.py` | `ddjx01` |
| ORY-MINI | `products/ory_mini/basic_pipeline.py` | `mini` |
| ORY-FINGER (2D) | `products/ory_finger/finger_pipeline.py` | `dd02` |
| ORY-FINGER (3D Qt) | `products/ory_finger/finger_pipeline_3dview.py` | `dd02` |
| ORY-BOX | `basic_pipeline.py` | `box` |
| ORY-MINI | `mini_pipeline.py` | `mini` |
| ORY-FINGER (2D) | `finger_pipeline.py` | `finger` |
| ORY-FINGER (3D Qt) | `finger_pipeline_3dview.py` | `finger` |
`dd02` is the stable ORY-FINGER alias and currently resolves to `dd02-ov`.
Compatibility wrappers still exist at the `examples/` root
(`basic_pipeline.py`, `finger_pipeline.py`, `finger_pipeline_3dview.py`) and
forward to the product entries above.
`finger` is the public ORY-FINGER config stem. `dd02` remains a compatibility alias for `dd02-ov`.
## Typical Usage
ORY-BOX:
```bash
python examples/products/ory_box/basic_pipeline.py -v 0 -c ddjx01
python examples/basic_pipeline.py -v 0 -c box
```
ORY-MINI:
```bash
python examples/products/ory_mini/basic_pipeline.py -v 0 -c mini
python examples/mini_pipeline.py -v 0 -c mini
```
ORY-FINGER 2D:
```bash
python examples/products/ory_finger/finger_pipeline.py --camera_id 0 --config dd02
python examples/finger_pipeline.py --camera_id 0 --config finger
```
ORY-FINGER video file:
```bash
python examples/products/ory_finger/finger_pipeline.py --video ./data/sample.mp4 --config dd02
python examples/finger_pipeline.py --video ./data/sample.mp4 --config finger
```
ORY-FINGER 3D Qt dashboard:
```bash
pip install -e ".[gui]"
python examples/products/ory_finger/finger_pipeline_3dview.py --video 0 --config dd02
python examples/finger_pipeline_3dview.py --video 0 --config finger
```
## Other Root Examples
@@ -70,10 +66,10 @@ pip install -e ".[gui]"
## Notes
- `products/ory_finger/finger_pipeline_3dview.py` is the main interactive finger demo.
- `finger_pipeline_3dview.py` is the main interactive finger demo.
It uses `gui/app_paths.py` so the same entry works in editable and packaged runs.
- Dashboard controls: click **启动** to start acquisition; use **Record CSV / Stop CSV**
and the CSV save-directory browser to log contact/force data; `Q` quits, `R` resets,
`S` starts video recording, `E` stops video recording, and `Esc` toggles fullscreen.
- `products/ory_finger/finger_pipeline.py` is the leanest finger example if you want to
- `finger_pipeline.py` is the leanest finger example if you want to
validate UV unroll + force/contact processing without the full dashboard.
+107 -13
View File
@@ -1,21 +1,115 @@
#!/usr/bin/env python3
"""Compatibility wrapper for ORY-BOX.
Prefer:
python examples/products/ory_box/basic_pipeline.py -v 0 -c ddjx01
"""
ORY-BOX:单传感器基础流程
from __future__ import annotations
本示例演示 Orisys SDK 的标准处理流程:
1. 初始化传感器(摄像头或视频文件)
2. 获取图像并计算形变
3. 按需计算接触区域
4. 读取并可视化结果
import sys
from pathlib import Path
SDK version: 0.4.3
"""
import os
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
import cv2
import orisys
import numpy as np
import argparse
_EXAMPLES_DIR = Path(__file__).resolve().parent
if str(_EXAMPLES_DIR) not in sys.path:
sys.path.insert(0, str(_EXAMPLES_DIR))
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--video","-v", type=str, default="0", help="视频源:摄像头编号或视频文件路径")
parser.add_argument("--config","-c", type=str, default="box", help="配置文件名称或路径")
parser.add_argument("--verbose","-verbose", type=bool, default=True, help="是否输出详细日志")
args = parser.parse_args()
from products.ory_box.basic_pipeline import main
# 步骤 1:创建传感器对象(必需)
# config_name 可以是内置配置名称,也可以是自定义配置文件路径
# cal_path 为标定文件路径;每台设备建议使用独立的标定文件
# 输入既可以是摄像头编号,也可以是视频文件路径
try:
input = int(args.video)
is_camera = True
sensor = orisys.Sensor(input, config_name=args.config, verbose=args.verbose)
except:
is_camera = False
sensor = orisys.Sensor(args.video, config_name=args.config, verbose=args.verbose)
print("\n按键说明:'q' 退出程序,'r' 重置追踪器。")
while True:
# 步骤 2:数据处理
# 步骤 2.1:获取并拼接图像(必需)
sensor.get_img()
# 步骤 2.2:计算形变场(必需);返回值表示当前帧是否检测到接触/运动
is_contact_now = sensor.compute_deformation(check_motion=True, threshold=0)
# 步骤 2.3:仅在当前帧检测到接触/运动时计算接触区域
# if is_contact_now:
sensor.compute_contact()
# 步骤 2.4:读取结果
fps, flow, vnormal, img, contour, centroid, depth_map, fnormal, fshearx, fsheary = sensor.read_info(
sensor.info.FPS,
sensor.info.VRAW,
sensor.info.VNORMAL,
sensor.info.IMG,
sensor.info.CONTOUR,
sensor.info.CENTROID,
sensor.info.DEPTH,
sensor.info.FNORMAL,
sensor.info.FSHEARX,
sensor.info.FSHEARY
)
# 步骤 3:可视化显示
# 步骤 3.1:显示形变矢量场
arrows = orisys.util.draw_arrows(
img,
flow,
threshold=2,
grid_spacing=20,
arrow_scale=1.0
)
cv2.imshow("arrow", arrows)
# 步骤 3.2:显示接触区域和质心
image_with_foe = orisys.util.draw_contact(img, contour, centroid)
cv2.imshow("contact", image_with_foe)
# 步骤 3.3:显示深度图
# 将深度值归一化到 0-255 范围
div_abs = depth_map
if div_abs.max() > div_abs.min():
div_normalized = ((div_abs - div_abs.min()) / (div_abs.max() - div_abs.min()) * 255).astype(np.uint8)
else:
div_normalized = np.zeros_like(div_abs, dtype=np.uint8)
# 应用伪彩色映射
cv2.imshow("depth", cv2.applyColorMap(div_normalized, cv2.COLORMAP_JET))
# 步骤 3.4:打印关键结果
print(
f"FPS={fps:.2f}, 法向力={fnormal:.4f}, 切向力X={fshearx:.4f}, "
f"切向力Y={fsheary:.4f}, 深度图尺寸={depth_map.shape}, 原始分辨率={sensor.img_size_raw}"
)
# 步骤 3.5:键盘控制
key = cv2.waitKey(1) & 0xFF
if key == ord("q"):
print("\n正在退出示例程序...")
break
if key == ord("r"):
# 当光流追踪出现漂移时,可手动重置追踪器
sensor.reset()
print("追踪器已重置。")
# =========================================================================
# 4. 释放资源
# =========================================================================
sensor.disconnect() # 释放视频源及相关资源
if __name__ == "__main__":
if __name__ == '__main__':
main()
+100 -10
View File
@@ -1,20 +1,110 @@
#!/usr/bin/env python3
"""Compatibility wrapper for ORY-FINGER 2D pipeline.
"""
ORY-FINGER:Minimum UV-unroll finger tactile pipeline.
Prefer:
python examples/products/ory_finger/finger_pipeline.py --camera_id 0 --config dd02
Offline: load Blender export -> build_or_load_domain -> unroll_map_uv.npz
Runtime: camera frame -> FrameUnwarper -> TactileProcessor -> forces/contact
Recommended entry:
python examples/finger_pipeline.py --camera_id 0 --config finger
Current product config: ``finger`` (``dd02`` still resolves to ``dd02-ov``).
"""
from __future__ import annotations
import argparse
import os
import sys
from pathlib import Path
_EXAMPLES_DIR = Path(__file__).resolve().parent
if str(_EXAMPLES_DIR) not in sys.path:
sys.path.insert(0, str(_EXAMPLES_DIR))
os.environ.setdefault("OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS", "0")
from products.ory_finger.finger_pipeline import main
import cv2
import orisys
from orisys.finger import FingerRuntime
def main():
parser = argparse.ArgumentParser(description="Minimum UV-unroll finger tactile pipeline")
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="finger", help="Orisys SDK config name or path")
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()
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)
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"output={geometry.out_width}x{geometry.out_height} "
)
runtime.warmup(color=False)
tactile = runtime.tactile
print("Press 'q' to quit, 'r' to reset tracker.")
try:
while True:
ok, unrolled = runtime.grab_and_process(color=False)
if not ok:
break
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,
)
status = (
f"FPS={fps:.2f} normal={fnormal:.2f} shear=({fshearx:.2f}, {fsheary:.2f}) "
f"mask={tactile.mask_stats['coverage']:.2f}"
)
if args.show:
arrows = orisys.util.draw_arrows(
unrolled,
flow,
threshold=0.4,
grid_spacing=10,
arrow_scale=1.0,
)
cv2.imshow("UV Unrolled", arrows)
print(status, end="\r")
key = cv2.waitKey(1) & 0xFF
if key == ord("q"):
print("\nExiting.")
break
if key == ord("r"):
tactile.reset()
print("\nTracker reset.")
finally:
runtime.close()
cv2.destroyAllWindows()
if __name__ == "__main__":
+76 -6
View File
@@ -1,20 +1,90 @@
#!/usr/bin/env python3
"""Compatibility wrapper for ORY-FINGER 3D Qt dashboard.
"""
ORY-FINGER:UV-unroll finger tactile pipeline with unified Qt dashboard.
Prefer:
python examples/products/ory_finger/finger_pipeline_3dview.py --video 0 --config dd02
Where to customize the GUI:
Layout (sizes, proportions) -> examples/gui/viewer3d/layout.py
Colors / QSS -> examples/gui/viewer3d/styles.py
Widget rendering logic -> examples/gui/viewer3d/widgets/
Window behavior -> examples/gui/viewer3d/dashboard.py
Recommended entry:
python examples/finger_pipeline_3dview.py --video 0 --config finger
"""
from __future__ import annotations
import argparse
import os
import sys
from pathlib import Path
os.environ.setdefault("OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS", "0")
# Make shared packages importable when launched as a file path.
_EXAMPLES_DIR = Path(__file__).resolve().parent
if str(_EXAMPLES_DIR) not in sys.path:
sys.path.insert(0, str(_EXAMPLES_DIR))
from products.ory_finger.finger_pipeline_3dview import main
from gui.app_paths import (
configure_frozen_runtime,
default_config_path,
ensure_examples_import_path,
require_gui_extras,
)
configure_frozen_runtime()
ensure_examples_import_path()
require_gui_extras()
from gui import run_dashboard
from pipeline import PipelineConfig
from viz import DEFAULT_FINGER_VIZ_POSE
def _parse_args():
parser = argparse.ArgumentParser(
description="UV-unroll finger tactile pipeline with unified Qt dashboard"
)
parser.add_argument("--video", "-v", default="0", help="Camera index or video file path")
parser.add_argument("--config", "-c", default=default_config_path(), help="Orisys SDK config")
parser.add_argument("--cal", default="", help="Optional calibration file path")
parser.add_argument("--verbose", action="store_true", help="SDK verbose logs")
parser.add_argument(
"--mesh-map",
choices=["flow", "depth"],
default="depth",
help="Color the 3D mesh from flow magnitude or contact depth.",
)
parser.add_argument("--threshold", type=float, default=None, help="Mesh/arrow threshold")
parser.add_argument("--no-3d", action="store_true", help="Disable 3D mesh view")
parser.add_argument(
"--viz-pose",
default=DEFAULT_FINGER_VIZ_POSE,
help="world_to_cam.npy for the default view; pass 'none' to skip",
)
parser.add_argument(
"--cutoff",
type=float,
default=5.0,
help="Low-pass cutoff for force curves (Hz)",
)
parser.add_argument(
"--profile",
action="store_true",
help="Print per-stage timing to the log panel / console",
)
parser.add_argument(
"--profile-every",
type=int,
default=30,
help="When --profile is set, print rolling averages every N frames",
)
return parser.parse_args()
def main():
args = _parse_args()
config = PipelineConfig.from_args(args)
sys.exit(run_dashboard(config))
if __name__ == "__main__":
+11 -2
View File
@@ -27,8 +27,17 @@ def resource_path(*parts: str) -> Path:
def default_config_path() -> str:
"""Default ORY-FINGER config name (resolves to dd02-ov via alias)."""
return "dd02"
"""Default ORY-FINGER config name (packaged stem ``finger``)."""
return "finger"
def require_gui_extras() -> None:
try:
import open3d # noqa: F401
from PyQt5 import QtWidgets # noqa: F401
import pyqtgraph # noqa: F401
except ImportError as exc:
raise SystemExit('GUI example requires: pip install "orisys[gui]"') from exc
def configure_frozen_runtime() -> None:
+1 -9
View File
@@ -11,6 +11,7 @@ from typing import Any, Optional
import cv2
import numpy as np
import orisys
from orisys.signal_processing import calibrate_normal_force
from .roi_geometry import (
build_roi_warp,
@@ -22,10 +23,6 @@ from .roi_geometry import (
)
NORMAL_FORCE_CAL_SLOPE = 3.92685554668795e-05
NORMAL_FORCE_CAL_OFFSET = 0.0217596967278535
@dataclass
class FrameSnapshot:
"""Normalized per-frame payload consumed by the Qt viewer."""
@@ -49,11 +46,6 @@ class FrameSnapshot:
record_frame: Optional[np.ndarray] = None
def calibrate_normal_force(raw_value: float) -> float:
"""Convert raw normal-force reading to Newtons using y = mx + c."""
return NORMAL_FORCE_CAL_SLOPE * float(raw_value) + NORMAL_FORCE_CAL_OFFSET
class SensorAdapter(ABC):
"""Common interface between Qt and concrete sensor backends."""
+1 -27
View File
@@ -13,7 +13,7 @@ from typing import Literal, Optional, Sequence, Tuple
import cv2
import numpy as np
import orisys
from scipy.signal import butter, filtfilt
from orisys.signal_processing import lowpass_filter
FORCE_SAMPLE_RATE_HZ = 30.0
PLOT_BUFFER_POINTS = 120
@@ -61,32 +61,6 @@ class ForcePlotState:
y_ranges: Tuple[Tuple[float, float], Tuple[float, float], Tuple[float, float]]
def lowpass_filter(
data,
cutoff_hz: float,
sample_rate_hz: float = FORCE_SAMPLE_RATE_HZ,
order: int = 2,
) -> np.ndarray:
"""Zero-phase Butterworth low-pass 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)
def ensure_bgr(img: Optional[np.ndarray]) -> Optional[np.ndarray]:
if img is None:
return None
+1 -1
View File
@@ -111,7 +111,7 @@ class QtViewerWindow(QMainWindow):
self.btn_config.setObjectName("btnConfig")
self.btn_config.clicked.connect(self._select_config)
bar.addWidget(self.btn_config)
self.config_edit = QLineEdit("./config/ddjx01.json")
self.config_edit = QLineEdit("box")
bar.addWidget(self.config_edit, 1)
self.btn_reset = QPushButton("重置")
@@ -137,7 +137,7 @@ class SettingsPanelWidget(QWidget):
root.addWidget(self._caption("Config"))
config_row = QHBoxLayout()
config_row.setSpacing(6)
self.config_edit = QLineEdit("dd02")
self.config_edit = QLineEdit("finger")
config_row.addWidget(self.config_edit, 1)
self.btn_browse_config = QPushButton("…")
self.btn_browse_config.setFixedWidth(32)
@@ -7,7 +7,7 @@ ORY-MINI:单传感器基础流程
3. 按需计算接触区域
4. 读取并可视化结果
SDK version: 0.4.2
SDK version: 0.4.3
"""
import os
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
+1 -1
View File
@@ -17,7 +17,7 @@ def mesh_threshold(mesh_map: str, threshold: float | None) -> float:
@dataclass
class PipelineConfig:
video: str = "0"
config: str = "dd02"
config: str = "finger"
calib: str | None = None
export_dir: str = DEFAULT_FINGER_EXPORT_DIR
csv_save_dir: str = "./outputs/csv"
+24 -23
View File
@@ -1,29 +1,24 @@
"""
示例 3:实时曲线与幅值图显示
示例 3:实时曲线与深度图显示
本示例演示如何在读取传感器结果的同时:
1. 显示形变矢量场
2. 显示流场幅值热力图
2. 显示接触深度图
3. 使用 PyQtGraph 实时绘制法向力与切向力曲线
4. 按键控制录像开始与结束
"""
import argparse
import os
import sys
import time
from pathlib import Path
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
import cv2
import numpy as np
import orisys
_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, draw_magnitude_map, lowpass_filter
from viz import ForceChartWindow, FrameRecorder
def open_sensor(video: str, config: str, cal: str, verbose: bool):
@@ -37,14 +32,21 @@ def open_sensor(video: str, config: str, cal: str, verbose: bool):
def main():
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("--config", "-c", type=str, default="box", help="配置文件名称或路径")
parser.add_argument("--cal", "-cal", type=str, default="", help="标定文件路径")
parser.add_argument(
"--cutoff",
type=float,
default=5.0,
help="力曲线低通滤波截止频率(Hz),30 Hz 采样时必须小于 15;设为 0 可关闭滤波",
)
parser.add_argument("--verbose", "-verbose", type=bool, default=True, help="是否输出详细日志")
args = parser.parse_args()
charts = ForceChartWindow()
recorder = FrameRecorder()
sensor = open_sensor(args.video, args.config, args.cal, args.verbose)
sensor.configure(lowpass_cutoff_hz=args.cutoff, lowpass_sample_rate_hz=30.0)
start_time = time.time()
print("\n按键说明:'q' 退出程序,'s' 开始录像,'e' 结束录像。")
@@ -54,30 +56,29 @@ def main():
recorder.write_if_recording(sensor.frame)
sensor.compute_deformation(check_motion=True, threshold=0)
fps, fn, fx, fy, flow, vnormal, vshear = sensor.read_info(
sensor.compute_contact()
fps, fn, fx, fy, flow, depth_map = sensor.read_info(
sensor.info.FPS,
sensor.info.FNORMAL,
sensor.info.FSHEARX,
sensor.info.FSHEARY,
sensor.info.VRAW,
sensor.info.VNORMAL,
sensor.info.VSHEAR,
sensor.info.DEPTH,
)
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)
cv2.imshow("流场幅值图", draw_magnitude_map(flow))
if depth_map.max() > depth_map.min():
depth_normalized = (
(depth_map - depth_map.min()) / (depth_map.max() - depth_map.min()) * 255
).astype(np.uint8)
else:
depth_normalized = np.zeros_like(depth_map, dtype=np.uint8)
cv2.imshow("depth", cv2.applyColorMap(depth_normalized, cv2.COLORMAP_JET))
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)
charts.refresh()
key = cv2.waitKey(1)
if key & 0xFF == ord("q"):
-1
View File
@@ -1 +0,0 @@
"""Product-facing example entry points for ORY-BOX / ORY-MINI / ORY-FINGER."""
-115
View File
@@ -1,115 +0,0 @@
"""
ORY-BOX:单传感器基础流程
本示例演示 Orisys SDK 的标准处理流程:
1. 初始化传感器(摄像头或视频文件)
2. 获取图像并计算形变
3. 按需计算接触区域
4. 读取并可视化结果
SDK version: 0.4.2
"""
import os
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
import cv2
import orisys
import numpy as np
import argparse
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--video","-v", type=str, default="0", help="视频源:摄像头编号或视频文件路径")
parser.add_argument("--config","-c", type=str, default="ddjx01", 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, verbose=args.verbose)
except:
is_camera = False
sensor = orisys.Sensor(args.video, config_name=args.config, verbose=args.verbose)
print("\n按键说明:'q' 退出程序,'r' 重置追踪器。")
while True:
# 步骤 2:数据处理
# 步骤 2.1:获取并拼接图像(必需)
sensor.get_img()
# 步骤 2.2:计算形变场(必需);返回值表示当前帧是否检测到接触/运动
is_contact_now = sensor.compute_deformation(check_motion=True, threshold=0)
# 步骤 2.3:仅在当前帧检测到接触/运动时计算接触区域
# if is_contact_now:
sensor.compute_contact()
# 步骤 2.4:读取结果
fps, flow, vnormal, img, contour, centroid, depth_map, fnormal, fshearx, fsheary = sensor.read_info(
sensor.info.FPS,
sensor.info.VRAW,
sensor.info.VNORMAL,
sensor.info.IMG,
sensor.info.CONTOUR,
sensor.info.CENTROID,
sensor.info.DEPTH,
sensor.info.FNORMAL,
sensor.info.FSHEARX,
sensor.info.FSHEARY
)
# 步骤 3:可视化显示
# 步骤 3.1:显示形变矢量场
arrows = orisys.util.draw_arrows(
img,
flow,
threshold=2,
grid_spacing=20,
arrow_scale=1.0
)
cv2.imshow("arrow", arrows)
# 步骤 3.2:显示接触区域和质心
image_with_foe = orisys.util.draw_contact(img, contour, centroid)
cv2.imshow("contact", image_with_foe)
# 步骤 3.3:显示深度图
# 将深度值归一化到 0-255 范围
div_abs = depth_map
if div_abs.max() > div_abs.min():
div_normalized = ((div_abs - div_abs.min()) / (div_abs.max() - div_abs.min()) * 255).astype(np.uint8)
else:
div_normalized = np.zeros_like(div_abs, dtype=np.uint8)
# 应用伪彩色映射
cv2.imshow("depth", cv2.applyColorMap(div_normalized, cv2.COLORMAP_JET))
# 步骤 3.4:打印关键结果
print(
f"FPS={fps:.2f}, 法向力={fnormal:.4f}, 切向力X={fshearx:.4f}, "
f"切向力Y={fsheary:.4f}, 深度图尺寸={depth_map.shape}, 原始分辨率={sensor.img_size_raw}"
)
# 步骤 3.5:键盘控制
key = cv2.waitKey(1) & 0xFF
if key == ord("q"):
print("\n正在退出示例程序...")
break
if key == ord("r"):
# 当光流追踪出现漂移时,可手动重置追踪器
sensor.reset()
print("追踪器已重置。")
# =========================================================================
# 4. 释放资源
# =========================================================================
sensor.disconnect() # 释放视频源及相关资源
if __name__ == '__main__':
main()
@@ -1,111 +0,0 @@
#!/usr/bin/env python3
"""
ORY-FINGER:Minimum UV-unroll finger tactile pipeline.
Offline: load Blender export -> build_or_load_domain -> unroll_map_uv.npz
Runtime: camera frame -> FrameUnwarper -> TactileProcessor -> forces/contact
Recommended entry:
python examples/products/ory_finger/finger_pipeline.py --camera_id 0 --config dd02
Current config alias: ``dd02`` resolves to ``dd02-ov`` for compatibility.
"""
import argparse
import os
import sys
os.environ.setdefault("OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS", "0")
import cv2
import orisys
from orisys.finger import FingerRuntime
def main():
parser = argparse.ArgumentParser(description="Minimum UV-unroll finger tactile pipeline")
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="dd02", help="Orisys SDK config name or path")
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()
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)
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"output={geometry.out_width}x{geometry.out_height} "
)
runtime.warmup(color=False)
tactile = runtime.tactile
print("Press 'q' to quit, 'r' to reset tracker.")
try:
while True:
ok, unrolled = runtime.grab_and_process(color=False)
if not ok:
break
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,
)
status = (
f"FPS={fps:.2f} normal={fnormal:.2f} shear=({fshearx:.2f}, {fsheary:.2f}) "
f"mask={tactile.mask_stats['coverage']:.2f}"
)
if args.show:
arrows = orisys.util.draw_arrows(
unrolled,
flow,
threshold=0.4,
grid_spacing=10,
arrow_scale=1.0,
)
cv2.imshow("UV Unrolled", arrows)
print(status, end="\r")
key = cv2.waitKey(1) & 0xFF
if key == ord("q"):
print("\nExiting.")
break
if key == ord("r"):
tactile.reset()
print("\nTracker reset.")
finally:
runtime.close()
cv2.destroyAllWindows()
if __name__ == "__main__":
main()
@@ -1,85 +0,0 @@
#!/usr/bin/env python3
"""
ORY-FINGER:UV-unroll finger tactile pipeline with unified Qt dashboard.
Where to customize the GUI:
Layout (sizes, proportions) -> examples/gui/viewer3d/layout.py
Colors / QSS -> examples/gui/viewer3d/styles.py
Widget rendering logic -> examples/gui/viewer3d/widgets/
Window behavior -> examples/gui/viewer3d/dashboard.py
Recommended entry:
python examples/products/ory_finger/finger_pipeline_3dview.py --video 0 --config dd02
"""
import argparse
import os
import sys
from pathlib import Path
os.environ.setdefault("OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS", "0")
# Make shared packages importable when launched as a file path.
_EXAMPLES_DIR = Path(__file__).resolve().parents[2]
if str(_EXAMPLES_DIR) not in sys.path:
sys.path.insert(0, str(_EXAMPLES_DIR))
from gui.app_paths import configure_frozen_runtime, default_config_path, ensure_examples_import_path
configure_frozen_runtime()
ensure_examples_import_path()
from gui import run_dashboard
from pipeline import PipelineConfig
from viz import DEFAULT_FINGER_VIZ_POSE
def _parse_args():
parser = argparse.ArgumentParser(
description="UV-unroll finger tactile pipeline with unified Qt dashboard"
)
parser.add_argument("--video", "-v", default="0", help="Camera index or video file path")
parser.add_argument("--config", "-c", default=default_config_path(), help="Orisys SDK config")
parser.add_argument("--cal", default="", help="Optional calibration file path")
parser.add_argument("--verbose", action="store_true", help="SDK verbose logs")
parser.add_argument(
"--mesh-map",
choices=["flow", "depth"],
default="depth",
help="Color the 3D mesh from flow magnitude or contact depth.",
)
parser.add_argument("--threshold", type=float, default=None, help="Mesh/arrow threshold")
parser.add_argument("--no-3d", action="store_true", help="Disable 3D mesh view")
parser.add_argument(
"--viz-pose",
default=DEFAULT_FINGER_VIZ_POSE,
help="world_to_cam.npy for the default view; pass 'none' to skip",
)
parser.add_argument(
"--cutoff",
type=float,
default=5.0,
help="Low-pass cutoff for force curves (Hz)",
)
parser.add_argument(
"--profile",
action="store_true",
help="Print per-stage timing to the log panel / console",
)
parser.add_argument(
"--profile-every",
type=int,
default=30,
help="When --profile is set, print rolling averages every N frames",
)
return parser.parse_args()
def main():
args = _parse_args()
config = PipelineConfig.from_args(args)
sys.exit(run_dashboard(config))
if __name__ == "__main__":
main()
+3 -3
View File
@@ -18,7 +18,7 @@
- Windows 下必须在 if __name__ == '__main__' 中调用 mp.freeze_support()
- 每个传感器进程都会占用独立的 GPU 显存
SDK version: 0.4.2
SDK version: 0.4.3
"""
import os
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
@@ -138,12 +138,12 @@ def main():
{
'sensor_id': 0,
'vid_src': args.video1,
'config_name': './config/ddjx01.json'
'config_name': 'box'
},
{
'sensor_id': 1,
'vid_src': args.video2, # 可替换为其他视频源或摄像头索引
'config_name': './config/ddjx01.json'
'config_name': 'box'
},
]
+2 -29
View File
@@ -1,32 +1,5 @@
"""Signal filtering helpers for example force charts."""
from __future__ import annotations
from orisys.signal_processing import lowpass_filter
import numpy as np
from scipy.signal import butter, filtfilt
def lowpass_filter(data, cutoff_hz=5, 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")
taplen = max(len(a), len(b))
# scipy.signal.filtfilt requires len(x) > padlen (default 3 * taplen for recent scipy).
padlen = 3 * taplen
if y.size <= padlen:
return y.copy()
try:
return filtfilt(b, a, y)
except ValueError:
return y.copy()
__all__ = ["lowpass_filter"]