0.3.1 stable release

This commit is contained in:
ykcheungab
2026-04-17 09:58:30 +08:00
parent 7a49fd76e6
commit fdc4ffd25f
11 changed files with 208 additions and 211 deletions
+6 -4
View File
@@ -73,7 +73,7 @@ conda install conda-forge::pyvkfft
* **vid_src** (`int | str`, 必填): 传感器 ID、相机编号或视频路径。默认为 `0`。
* **config_name** (`str`,可选): 传感器配置`json`文件路径,用于加载对应的参数配置,默认为 `./config/ddjx01`。
* **cal_path** (`str`,可选): 复眼拼接校准文件路径,默认为 `./config/cal.npy`。有多个传感器同时运行时建议传感器类创建时建议填写,避免校准文件覆写。在首次运行时程序会在找不到文件时自动创建。
* * **cal_path** (`str`,可选,已弃用): 该参数已弃用,当前版本中不再使用,传入后也会被忽略。为兼容旧代码暂时保留。: 复眼拼接校准文件路径,默认为 `./config/cal.npy`。有多个传感器同时运行时建议传感器类创建时建议填写,避免校准文件覆写。在首次运行时程序会在找不到文件时自动创建。
* **backend** (`str`,可选): 光流场解耦计算后端,默认为 `auto`,可选`cupy`,`opencl`,`cpu`。
* **verbose** (`bool`,可选): 是否输出初始化及运行过程中的详细日志信息,默认为 `False`。
@@ -99,12 +99,12 @@ sensor = Sensor("/data/data.mp4")
采集并预处理一帧图像并存到`Sensor` 类别中。
### 返回
* `np.ndarray`:处理后的BGR格式图像(`uint8`,尺寸为json文件中`resolution_stitch`对应分辨率)。
* `None`:当输入源读取失败(例如视频结束、摄像头断开)时返回。
* `bool`:是否成功读取图像。
### 状态更新
* 成功时更新`sensor`类里面的图像。
* 成功时更新`sensor`类里面的`sensor.img`。处理后的BGR格式图像(`uint8`,尺寸为json文件中`resolution_stitch`对应分辨率)。
### 示例
```python
@@ -123,6 +123,8 @@ sensor.get_img()
### 输入参数
* **check_motion**(`bool`,可选): 为 `True` 时先根据光流判断是否有运动,有运动才做 HHD 分解;为 `False` 时每帧都做 HHD。默认 `True`。
* **threshold**(`float`,可选): **check_motion** 功能的阈值,默认为`6`。
* **tail_frames** (`int`,可选): 光流变化低于阈值后继续解算多少帧,确保传感器回复,,默认为 `10`.
### 返回
* `bool`:是否接触到物体,如果**check_motion**为`False`时返回`None`。
+13 -2
View File
@@ -39,6 +39,16 @@ def find_compiled_extension(output_dir: Path) -> Path:
return sorted(candidates, key=lambda p: len(p.name))[0]
def move_wheel_to_sdk(wheel_path: Path, version: str) -> Path:
sdk_version_dir = Path("SDK") / version
sdk_version_dir.mkdir(parents=True, exist_ok=True)
dest_wheel = sdk_version_dir / wheel_path.name
if dest_wheel.exists():
dest_wheel.unlink()
shutil.move(str(wheel_path), str(dest_wheel))
return dest_wheel
def build():
install_nuitka()
@@ -143,9 +153,10 @@ setup(
print("\n=== Building Wheel ===")
subprocess.check_call([sys.executable, "setup.py", "bdist_wheel"], cwd=dist_dir)
wheels = list((dist_dir / "dist").glob("*.whl"))
wheels = sorted((dist_dir / "dist").glob("*.whl"))
if wheels:
print(f"\n[SUCCESS] Wheel created: {wheels[0]}")
final_wheel = move_wheel_to_sdk(wheels[0], version)
print(f"\n[SUCCESS] Wheel created and moved to: {final_wheel}")
else:
print("\n[ERROR] Wheel not found.")
+1 -1
View File
@@ -11,7 +11,7 @@
},
"stitcher": {
"min_area_frac": 0.01,
"max_area_frac": 0.04,
"max_area_frac": 0.05,
"max_aspect_ratio": 50,
"index": [1,2,3,6,7,8,11,12,13],
"margin": 10,
Binary file not shown.
+1 -1
View File
@@ -1,6 +1,6 @@
叠动科技软件使用手册
版本: 0.3.1
更新日期: 2026年3月6日
更新日期: 2026年4月16日
# 简介
本文件介绍如何上手和运行叠动视触觉传感器配套的开发套件。本套件是配合叠动视触觉传感器进行图像处理,计算形变、力以及接触信息。它可以从摄像头或视频文件中读取图像,处理后打印和展示处理后信息。
+42 -40
View File
@@ -1,13 +1,13 @@
"""
示例 1: 基础用法 - 单传感器同步处理
本示例演示 orisys 的基本使用流程:
1. 初始化传感器(视频文件或摄像头)
2. 循环获取图像并计算形变和触碰点
3. 读取各类传感器数据
4. 可视化显示
示例 1:单传感器基础流程
本示例演示 Orisys SDK 的标准处理流程:
1. 初始化传感器(摄像头或视频文件)
2. 获取图像并计算形变
3. 按需计算接触区域
4. 读取并可视化结果
SDK version: 0.3.1
"""
import os
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
@@ -18,39 +18,38 @@ import argparse
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--video","-v", type=str, default="0", help="Camera ID / Video file path")
parser.add_argument("--config","-c", type=str, default="./config/ddjx01.json", help="Configuration file path")
parser.add_argument("--cal","-cal", type=str, default="./config/ddjx01.npy", help="Calibration file path")
parser.add_argument("--verbose","-verbose", type=bool, default=True, help="Print detailed information")
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("--verbose","-verbose", type=bool, default=True, help="是否输出详细日志")
args = parser.parse_args()
# 步骤1:创建传感器对象 (必要步骤),
# config_name为配置文件名(内置配置文件名)或路径(自定义配置文件路径)
# cal_path为校准文件路径, 每一个设备需要创建/使用一个校准文件
# 步骤 1:创建传感器对象(必需)
# config_name 可以是内置配置名称,也可以是自定义配置文件路径
# cal_path 为标定文件路径;每台设备建议使用独立的标定文件
# 方式1: 从视频文件读取
# 输入既可以是摄像头编号,也可以是视频文件路径
try:
input = int(args.video)
is_camera = True
sensor = orisys.Sensor(input, config_name=args.config, cal_path= args.cal, verbose=args.verbose)
sensor = orisys.Sensor(input, config_name=args.config, verbose=args.verbose)
except:
is_camera = False
sensor = orisys.Sensor(args.video, config_name=args.config, cal_path= args.cal, verbose=args.verbose)
sensor = orisys.Sensor(args.video, config_name=args.config, verbose=args.verbose)
print("\n按 'q' 退出程序,按 'r' 重置传感器追踪器")
print("\n按键说明:'q' 退出程序,'r' 重置追踪器。")
while True:
# 步骤2:信息提取
# 步骤 2.1: 获取并拼接图像 (必要步骤)
# 步骤 2:数据处理
# 步骤 2.1:获取并拼接图像(必需)
sensor.get_img()
# 步骤 2.2: 计算形变场 (必要步骤)
# 步骤 2.2:计算形变场(必需)
is_touched = sensor.compute_deformation(check_motion=True, threshold=6)
# 步骤 2.3: 计算触碰点
# 步骤 2.3:检测到接触后再计算接触区域
if is_touched:
sensor.compute_contact()
#步骤 2.4: 读取信息
# 步骤 2.4:读取结果
fps, flow, vnormal, img, contour, centroid, depth_map, fnormal, fshearx, fsheary = sensor.read_info(
sensor.info.FPS,
sensor.info.VRAW,
@@ -64,8 +63,8 @@ def main():
sensor.info.FSHEARY
)
#步骤 3: 可视化显示
# 步骤 3.1: 绘制形变场
# 步骤 3:可视化显示
# 步骤 3.1:显示形变矢量场
arrows = orisys.util.draw_arrows(
img,
flow,
@@ -73,40 +72,43 @@ def main():
grid_spacing=20,
arrow_scale=1.0
)
cv2.imshow("Deformation", arrows)
cv2.imshow("形变矢量场", arrows)
# 步骤 3.2: 在图像上触碰点和区域
# 步骤 3.2:显示接触区域和质心
image_with_foe = orisys.util.draw_contact(img, contour, centroid)
cv2.imshow("Contact Centroid", image_with_foe)
cv2.imshow("接触区域与质心", image_with_foe)
# 步骤 3.3: 绘制深度图
# 归一化散度值到0-255范围(使用绝对值,然后归一化)
# 步骤 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)
# 应用颜色映射(使用JET:蓝色=低散度,红色=高散度)
cv2.imshow("Depth Map", cv2.applyColorMap(div_normalized, cv2.COLORMAP_JET))
# 应用伪彩色映射
cv2.imshow("深度图", cv2.applyColorMap(div_normalized, cv2.COLORMAP_JET))
#步骤 3.4: 打印资料
print(f"fps: {fps}, fnormal: {fnormal}, fshearx: {fshearx}, fsheary: {fsheary}, depth_shape: {depth_map.shape}, sensor.resolution_raw: {sensor.img_size_raw}")
# 步骤 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: 键盘控制
# 步骤 3.5:键盘控制
key = cv2.waitKey(1) & 0xFF
if key == ord("q"):
print("\n退出程序...")
print("\n正在退出示例程序...")
break
if key == ord("r"):
# 重置光流追踪器(当追踪漂移时使用)
# 当光流追踪出现漂移时,可手动重置追踪器
sensor.reset()
print("传感器已重置")
print("追踪器已重置。")
# =========================================================================
# 4. 清理资源
# 4. 释放资源
# =========================================================================
sensor.disconnect() # 释放视频源和相关资源
sensor.disconnect() # 释放视频源及相关资源
if __name__ == '__main__':
+64 -59
View File
@@ -1,22 +1,24 @@
"""
示例 4: 多传感器并行处理(多进程模式 - 不使用 MultiSensor)
示例 2:双传感器并行处理(多进程)
本示例演示如何使用 multiprocessing 同时运行两个传感器:
- 每个传感器运行在独立的进程中
- 每个传感器运行在独立进程中
- 每个进程拥有独立的 CUDA 上下文
- 使用 multiprocessing.Process 和 Queue 进行进程间通信
- 通过 Process 和 Queue 在主进程中汇总结果并显示
架构说明:
进程 1 (传感器 0): 采集 → 光流 → HHD → 接触检测 → 队列
进程 2 (传感器 1): 采集 → 光流 → HHD → 接触检测 → 队列
进程 1(传感器 0):采集 → 光流 → HHD → 接触检测 → 队列
进程 2(传感器 1):采集 → 光流 → HHD → 接触检测 → 队列
↓
主进程: 显示 ← ───────────────┘
主进程: 显示 ← ───────────────┘
适用场景:多指触觉传感器、多视角采集、需要 GPU 加速的多路处理
适用场景:多指触觉传感器、多视角采集,以及需要 GPU 加速的多路处理。
注意事项:
- Windows 下必须在 if __name__ == '__main__' 中调用 mp.freeze_support()
- 每个传感器进程会占用独立的 GPU 显存
- 每个传感器进程都会占用独立的 GPU 显存
SDK version: 0.3.1
"""
import os
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
@@ -26,20 +28,20 @@ import numpy as np
import multiprocessing as mp
from multiprocessing import Queue, Process
import time
import argparse
def sensor_worker(sensor_id, vid_src, config_name, cal_path, result_queue, stop_event, target_fps=None):
def sensor_worker(sensor_id, vid_src, config_name, result_queue, stop_event, target_fps=None):
"""
传感器工作进程函数
传感器工作进程。
Args:
sensor_id: 传感器编号
vid_src: 视频源(文件路径或摄像头索引)
config_name: 配置名称
cal_path: 标定路径
result_queue: 结果队列,用于向主进程发送数据
stop_event: 停止事件,用于通知工作进程退出
target_fps: 目标帧率;若设置则用定时器限速,使多传感器 FPS 接近一致
target_fps: 目标帧率;设置后将对处理循环限速,使多路传感器 FPS 更接近
"""
try:
# 在每个进程中创建独立的传感器实例
@@ -48,27 +50,26 @@ def sensor_worker(sensor_id, vid_src, config_name, cal_path, result_queue, stop_
isstitch=True,
cuda=True,
verbose=False, # 减少子进程输出
config_name=config_name,
cal_path=cal_path
config_name=config_name
)
print(f"传感器 {sensor_id} 已启动" + (f" (target_fps={target_fps})" if target_fps else ""))
print(f"传感器 {sensor_id} 已启动" + (f"(目标帧率:{target_fps} FPS)" if target_fps else ""))
frame_interval = 1.0 / target_fps if target_fps and target_fps > 0 else 0.0
while not stop_event.is_set():
t_start = time.perf_counter()
try:
# 步骤1: 获取图像
# 步骤 1:获取图像
sensor.get_img()
# 步骤2: 计算形变场
# 步骤 2:计算形变场
sensor.compute_deformation()
# 步骤3: 计算触碰点
# 步骤 3:计算接触区域(如需要可取消注释)
# sensor.compute_contact()
# 步骤4: 读取信息
# 步骤 4:读取结果
fps, flow, vnormal, img, contour, centroid, depth_map = sensor.read_info(
sensor.info.FPS,
sensor.info.VRAW,
@@ -79,7 +80,7 @@ def sensor_worker(sensor_id, vid_src, config_name, cal_path, result_queue, stop_
sensor.info.DEPTH
)
# 步骤5: 准备数据字典(确保所有数据都是可序列化的)
# 步骤 5:整理结果字典(确保数据可序列化)
data = {
'sensor_id': sensor_id,
'fps': fps,
@@ -92,100 +93,104 @@ def sensor_worker(sensor_id, vid_src, config_name, cal_path, result_queue, stop_
'timestamp': time.time()
}
# 步骤6: 发送数据到主进程(非阻塞)
# 步骤 6:将结果发送到主进程(非阻塞)
try:
result_queue.put_nowait(data)
except Exception:
# 如果队列已满,跳过这一帧
# 如果队列已满,则丢弃当前帧,避免阻塞工作进程
pass
except Exception as e:
print(f"传感器 {sensor_id} 处理错误: {e}")
print(f"传感器 {sensor_id} 处理失败:{e}")
time.sleep(0.1)
# 步骤7: 按 target_fps 限速,使多传感器 FPS 接近
# 步骤 7:按 target_fps 限速,使多路传感器 FPS 更接近
if frame_interval > 0:
elapsed = time.perf_counter() - t_start
sleep_time = frame_interval - elapsed
if sleep_time > 0.001: # 至少 1ms 才 sleep,避免无意义调度
time.sleep(sleep_time)
# 清理资源
# 释放资源
sensor.disconnect()
print(f"传感器 {sensor_id} 已停止")
print(f"传感器 {sensor_id} 已停止。")
except Exception as e:
print(f"传感器 {sensor_id} 初始化失败: {e}")
print(f"传感器 {sensor_id} 初始化失败:{e}")
result_queue.put({'sensor_id': sensor_id, 'error': str(e)})
def main():
# =========================================================================
# 1. 配置多个传感器
# 1. 配置多路传感器
# =========================================================================
# 目标帧率:设为正数时,两路传感器会按此帧率限速,使 FPS 接近一致;设为 None 则不限速
# 目标帧率:设为正数时,两路传感器会按该帧率限速;设为 None 则不限速
parser = argparse.ArgumentParser()
parser.add_argument("--video1","-v1", type=str, default="0", help="视频文件路径")
parser.add_argument("--video2","-v2", type=str, default="0", help="视频文件路径")
args = parser.parse_args()
target_fps = 30
num_sensors = 2
sensor_configs = [
{
'sensor_id': 0,
'vid_src': 2,
'config_name': './config/prototype_of3_720p.json',
'cal_path': './config/ddjx01.npy'
'vid_src': args.video1,
'config_name': './config/ddjx01.json'
},
{
'sensor_id': 1,
'vid_src': 4, # 可以改为不同的视频源或摄像头索引
'config_name': './config/prototype_of3_720p.json',
'cal_path': './config/ddjx01.npy'
'vid_src': args.video2, # 可替换为其他视频源或摄像头索引
'config_name': './config/ddjx01.json'
},
]
# =========================================================================
# 2. 创建进程间通信对象
# =========================================================================
# 增大队列大小以缓冲更多帧,避免因主进程处理延迟导致的丢帧
# 适当增大队列容量,以缓冲更多帧并减轻主进程偶发延迟带来的丢帧
result_queues = [Queue(maxsize=30) for _ in sensor_configs]
stop_events = [mp.Event() for _ in sensor_configs]
# =========================================================================
# 3. 启动传感器工作进程
# 3. 启动工作进程
# =========================================================================
processes = []
for i, config in enumerate(sensor_configs):
p = Process(
target=sensor_worker,
args=(i, config['vid_src'], config['config_name'], config['cal_path'],
args=(i, config['vid_src'], config['config_name'],
result_queues[i], stop_events[i], target_fps)
)
p.start()
processes.append(p)
print(f"\n已启动 {len(processes)} 个传感器进程")
print("按 'q' 退出程序")
print(f"\n已启动 {len(processes)} 个传感器进程。")
print("按键说明:'q' 退出程序。")
# 用于存储每个传感器的最新数据
# 保存每个传感器的最新一帧数据
latest_data = [None] * len(sensor_configs)
# 用于控制打印频率,减少I/O开销
# 控制打印频率,减少 I/O 开销
print_counter = 0
print_interval = 30 # 每30帧打印一次,进一步减少I/O
print_interval = 30 # 每 30 帧打印一次
# 用于控制按键检查频率
# 控制按键检查频率
key_check_counter = 0
key_check_interval = 5 # 每 5 帧检查一次按键
try:
# =====================================================================
# 4. 主循环:从队列获取数据并显示
# 4. 主循环:从队列获取结果并显示
# =====================================================================
while True:
# 从所有队列中快速获取最新数据(高效清空队列,只保留最新帧)
# 从所有队列中快速获取最新数据,只保留最后一帧
for i, queue in enumerate(result_queues):
# 快速清空队列,只保留最后一帧
# 清空队列,仅保留最后一帧
data = None
# 一次性获取所有可用数据,只保留最后一个
# 读取所有可用数据,只保留最新结果
while True:
try:
data = queue.get_nowait()
@@ -195,14 +200,14 @@ def main():
if data is not None:
latest_data[i] = data
# 显示所有传感器的数据
# 显示所有传感器的最新结果
for data in latest_data:
if data is None or 'error' in data:
continue
sid = data['sensor_id']
# 显示形变场
# 显示形变矢量场
if data['img'] is not None and data['flow'] is not None:
arrows = orisys.util.draw_arrows(
data['img'],
@@ -211,7 +216,7 @@ def main():
grid_spacing=10,
arrow_scale=1.0
)
cv2.imshow(f"Deformation {sid}", arrows)
cv2.imshow(f"传感器 {sid} - 形变矢量场", arrows)
# # 显示触碰点和区域
# if data['img'] is not None:
@@ -233,33 +238,33 @@ def main():
# cv2.imshow(f"Depth Map {sid}",
# cv2.applyColorMap(div_normalized, cv2.COLORMAP_JET))
# 降低打印频率以减少I/O开销(在主循环级别控制)
# 降低打印频率,减少主循环中的 I/O 开销
print_counter += 1
if print_counter >= print_interval:
for data in latest_data:
if data is not None and 'error' not in data:
sid = data['sensor_id']
print(f"传感器 {sid}: FPS={data['fps']:5.1f}")
print(f"传感器 {sid}:FPS={data['fps']:5.1f}")
print_counter = 0
# 降低按键检查频率以减少开销
# 降低按键检查频率,减少额外开销
key_check_counter += 1
if key_check_counter >= key_check_interval:
key = cv2.waitKey(1) & 0xFF
if key == ord('q'):
print("\n退出程序...")
print("\n正在退出示例程序...")
break
key_check_counter = 0
finally:
# =====================================================================
# 5. 清理资源
# 5. 释放资源
# =====================================================================
# 通知所有工作进程停止
for event in stop_events:
event.set()
# 等待所有进程结束
# 等待所有工作进程结束
for p in processes:
p.join(timeout=2)
if p.is_alive():
@@ -267,10 +272,10 @@ def main():
p.join()
cv2.destroyAllWindows()
print("所有传感器进程已停止")
print("所有传感器进程已停止。")
if __name__ == '__main__':
# Windows 下多进程必需的初始化
# Windows 下运行多进程示例所需的初始化
mp.freeze_support()
main()
+69 -54
View File
@@ -1,3 +1,15 @@
"""
示例 3:实时曲线与幅值图显示
本示例演示如何在读取传感器结果的同时:
1. 显示形变矢量场
2. 显示流场幅值热力图
3. 使用 PyQtGraph 实时绘制法向力与切向力曲线
4. 按键控制录像开始与结束
SDK version: 0.3.1
"""
import os
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
import cv2
@@ -5,10 +17,14 @@ 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 create_plot():
p1 = win.addPlot(title="法相力")
p1 = win.addPlot(title="法向力")
p1.setLabel('bottom', '时间', units='s')
win.nextRow()
p2 = win.addPlot(title="切向力 X")
@@ -33,53 +49,53 @@ def update(plot_af, plot_tfx, plot_tfy, data, p1, p2, p3):
# 深度图
def draw_magnitude_map(vfield, threshold=3.0, colormap=cv2.COLORMAP_JET) -> np.ndarray:
"""
Draw color map of pointwise magnitude of velocity field.
绘制流场幅值伪彩色图。
Args:
vfield: Velocity field to visualize (2D vector field)
threshold: Minimum magnitude to display (below threshold shows as zero/blue)
colormap: OpenCV colormap to use (default: COLORMAP_JET)
vfield: 需要可视化的二维矢量场
threshold: 最小显示阈值;低于阈值的区域显示为低值颜色
colormap: OpenCV 颜色映射类型(默认 COLORMAP_JET)
Returns:
np.ndarray: 400x400 BGR image with colored magnitude map
np.ndarray: 400x400 的 BGR 伪彩图像
"""
flow_field = vfield
# Compute magnitude at each point
# 计算每个像素点的幅值
magnitude = np.sqrt(flow_field[:, :, 0]**2 + flow_field[:, :, 1]**2)
# Smooth by downsampling and interpolating back
# 先降采样再插值回原尺寸,使显示更平滑
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)
# Apply threshold - set below-threshold values to zero
# 应用阈值,将低于阈值的区域置零
magnitude_thresholded = magnitude_smoothed.copy()
magnitude_thresholded[magnitude_smoothed < threshold] = 0
# Normalize to 0-255 range
# 归一化到 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)
# Apply colormap
# 应用伪彩色映射
magnitude_colored = cv2.applyColorMap(magnitude_norm, colormap)
# Resize to output size
# 缩放到固定输出尺寸
out_img = cv2.resize(magnitude_colored, (400, 400), interpolation=cv2.INTER_CUBIC)
return out_img
# 实时图表
npoints = 100 # 图表数据长度
npoints = 100 # 曲线缓冲区长度
pg.setConfigOption('background', 'w')
pg.setConfigOption('foreground', 'k')
app = pg.mkQApp("Plotting Example")
win = pg.GraphicsLayoutWidget(show=True, title="Basic plotting examples")
win.setWindowTitle('pyqtgraph example: Plotting')
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
@@ -91,17 +107,17 @@ for i in range(3):
def main():
global start_time
parser = argparse.ArgumentParser()
parser.add_argument("--video","-v", type=str, default="0", help="Camera ID / Video file path")
parser.add_argument("--config","-c", type=str, default="./config/ddjx01.json", help="Configuration file path")
parser.add_argument("--cal","-cal", type=str, default="./config/ddjx01.npy", help="Calibration file path")
parser.add_argument("--verbose","-verbose", type=bool, default=True, help="Print detailed information")
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为校准文件路径, 每一个设备需要创建/使用一个校准文件
# 步骤 1:创建传感器对象(必需)
# config_name 可以是内置配置名称,也可以是自定义配置文件路径
# cal_path 为标定文件路径;每台设备建议使用独立的标定文件
# 方式1: 从视频文件读取
# 输入既可以是摄像头编号,也可以是视频文件路径
try:
input = int(args.video)
is_camera = True
@@ -109,23 +125,23 @@ def main():
except:
is_camera = False
sensor = orisys.Sensor(args.video, config_name=args.config, cal_path= args.cal, verbose=args.verbose)
# 打开视频
# sensor1 = Sensor("./s1.mp4",isstitch=True,cuda = True,verbose = True)
# 方式1: 从视频文件读取
# 下面保留了历史示例写法,便于参考
# sensor1 = orisys.Sensor("./s1.mp4", config_name="./config/ddjx01.json", cal_path= "./config/ddjx01.npy", cuda=False, verbose=True)
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() # 提取并拼接复眼图像
sensor.get_img() # 获取并拼接复眼图像
if is_save and writer is not None and sensor.frame is not None:
frame_count += 1
save_img = sensor.frame
# Convert grayscale (1 channel) to BGR (3 channels) if needed
# 若为灰度图,则先转换为 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)
@@ -133,44 +149,43 @@ def main():
writer.write(save_img)
sensor.compute_deformation(check_motion=False) # 计算形变和分解
# 提取信息:
# 向量场: VRAW, VNORMAL, VSHEAR: 原始,法向,切向
# 帧率,时间戳: FPS, TIMESTAMP
# 变形量总和: FNORMAL , FSHEARX, FSHEARY : 法向,切向(X),切向(Y)
sensor.compute_deformation(check_motion=False) # 计算形变与分解结果
# 读取信息:
# 向量场: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(fps,fn,fx,fy)
# 显示向量场
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("Deformation", arrows)
# 显示深度图
cv2.imshow("形变矢量场", arrows)
# 显示流场幅值图
mag = draw_magnitude_map(flow)
cv2.imshow("Magnitude", mag)
cv2.imshow("流场幅值图", mag)
# 显示图表 - 更新时间轴
# 更新实时曲线
current_time = time.time() - start_time
for i in range(3):
data[:-1, 0, i] = data[1:, 0, i] # shift time data left
data[:-1, 1, i] = data[1:, 1, i] # shift force data left
data[-1, 0, :] = current_time # set current time for all plots
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
update(plot_af, plot_tfx, plot_tfy, data, p1, p2, p3) # Update plot
update(plot_af, plot_tfx, plot_tfy, data, p1, p2, p3) # 更新曲线
key = cv2.waitKey(1)
if key & 0xFF == ord("q"):
print("\n 停止程序")
print("\n正在退出示例程序...")
break
if key == ord("s"):
save_path = f"./data/pulse_test.mp4"
# Ensure directory exists
# 确保输出目录存在
os.makedirs(os.path.dirname(save_path), exist_ok=True)
# Get frame dimensions from the actual frame
# 从当前帧获取视频尺寸
save_img = sensor.frame
if save_img is not None:
# Convert grayscale (1 channel) to BGR (3 channels) if needed for dimension check
# 如为灰度图,先转换为 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:
@@ -181,24 +196,24 @@ def main():
writer = cv2.VideoWriter(save_path, cv2.VideoWriter_fourcc(*'mp4v'), 30, (frame_width, frame_height))
if writer.isOpened():
is_save = True
print(f"Start writing to {save_path}")
print(f"开始录像:{save_path}")
else:
print(f"Failed to open video writer for {save_path}")
print(f"无法创建录像文件:{save_path}")
writer = None
else:
print("No frame available to determine video dimensions")
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"Stop writing to {save_path}, total frames: {frame_count}")
print(f"录像结束:{save_path},总帧数:{frame_count}")
else:
print(f"Stop writing, total frames: {frame_count}")
print(f"录像结束,总帧数:{frame_count}")
frame_count = 0
sensor.disconnect() # 断开传感器连接
sensor.disconnect() # 断开传感器连接并释放资源
+9 -8
View File
@@ -1,6 +1,6 @@
{
"name": "prototype",
"version": "1.0",
"version": "0.3.1",
"camera": {
"resolution_raw": [1920, 1080],
"fps": 30,
@@ -11,14 +11,14 @@
},
"stitcher": {
"min_area_frac": 0.01,
"max_area_frac": 0.04,
"max_aspect_ratio": 100,
"max_area_frac": 0.05,
"max_aspect_ratio": 50,
"index": [1,2,3,6,7,8,11,12,13],
"margin": 10,
"nominal_ov_pos": 100,
"n": [3,3],
"row_tol": 0.6,
"box_size": [220, 220]
"box_size": [250, 250]
},
"tracker": {
"adaptive": true,
@@ -28,15 +28,16 @@
"algorithm": "dis",
"dis_preset": "medium",
"dis_mode": 3,
"algorithm_list": ["raft", "dis"]
"algorithm_list": ["raft", "dis"],
"hhd_downsample": 3
},
"contact": {
"min_contour_area": 3000,
"flow_threshold":1,
"div_threshold":0.02,
"flow_threshold":5,
"div_threshold":0.05,
"expansion_threshold": 0.01,
"kernel_size": [5,5],
"denoise_sigma": 15
"denoise_sigma": 12
}
}
-39
View File
@@ -1,39 +0,0 @@
{
"name": "prototype",
"version": "1.0",
"camera": {
"resolution_raw": [1920, 1080],
"fps": 30,
"units": 15,
"units_used": 9,
"resolution_stitch": [400, 400]
},
"stitcher": {
"min_area_frac": 0.01,
"max_area_frac": 0.04,
"max_aspect_ratio": 100,
"index": [1,2,3,6,7,8,11,12,13],
"margin": 10,
"nominal_ov_pos": 100,
"n": [3,3],
"row_tol": 0.6
},
"tracker": {
"adaptive": true,
"lower_threshold": 12.0,
"upper_threshold": 18.0,
"loop_closure_interval": 1,
"dis_preset": "medium",
"finest_scale": 3
},
"contact": {
"min_contour_area": 3000,
"flow_threshold":1,
"div_threshold":0.02,
"expansion_threshold": 0.01,
"kernel_size": [5,5],
"denoise_sigma": 15
}
}
+1 -1
View File
@@ -282,7 +282,7 @@ class Sensor:
self.compute_hhd()
return True
return False
else:
return None
# return True