From fdc4ffd25fc3b0bc9a2587b65dab8fd5777207cc Mon Sep 17 00:00:00 2001 From: ykcheungab Date: Fri, 17 Apr 2026 09:58:30 +0800 Subject: [PATCH] 0.3.1 stable release --- Readme.md | 10 ++- build_nuitka.py | 15 +++- config/ddjx01.json | 2 +- doc/get_started.docx | Bin 21170 -> 14952 bytes doc/get_started.md | 2 +- example1_basic_pipeline.py | 82 ++++++++++---------- example2_two_sensor_pipeline.py | 125 ++++++++++++++++-------------- example3_plot_data.py | 125 +++++++++++++++++------------- src/orisys/configs/ddjx01.json | 17 ++-- src/orisys/configs/prototype.json | 39 ---------- src/orisys/sensor.py | 2 +- 11 files changed, 208 insertions(+), 211 deletions(-) delete mode 100644 src/orisys/configs/prototype.json diff --git a/Readme.md b/Readme.md index b731d13..665bc80 100644 --- a/Readme.md +++ b/Readme.md @@ -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`。 diff --git a/build_nuitka.py b/build_nuitka.py index 51adb6d..e58c054 100644 --- a/build_nuitka.py +++ b/build_nuitka.py @@ -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.") diff --git a/config/ddjx01.json b/config/ddjx01.json index 988ab1e..21b5e96 100644 --- a/config/ddjx01.json +++ b/config/ddjx01.json @@ -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, diff --git a/doc/get_started.docx b/doc/get_started.docx index 54261174b8075f7c4f0a7d49099cdc5a0503038d..8fad3c33ad5fa988f62e7b212bf50b5a640b10c2 100644 GIT binary patch literal 14952 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 literal 21170 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 diff --git a/doc/get_started.md b/doc/get_started.md index 5ef11c0..053ce13 100644 --- a/doc/get_started.md +++ b/doc/get_started.md @@ -1,6 +1,6 @@ 叠动科技软件使用手册 版本: 0.3.1 -更新日期: 2026年3月6日 +更新日期: 2026年4月16日 # 简介 本文件介绍如何上手和运行叠动视触觉传感器配套的开发套件。本套件是配合叠动视触觉传感器进行图像处理,计算形变、力以及接触信息。它可以从摄像头或视频文件中读取图像,处理后打印和展示处理后信息。 diff --git a/example1_basic_pipeline.py b/example1_basic_pipeline.py index d8763c6..8faaa95 100644 --- a/example1_basic_pipeline.py +++ b/example1_basic_pipeline.py @@ -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__': diff --git a/example2_two_sensor_pipeline.py b/example2_two_sensor_pipeline.py index 60e5fd4..0bf7c5a 100644 --- a/example2_two_sensor_pipeline.py +++ b/example2_two_sensor_pipeline.py @@ -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帧检查一次按键 + 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() diff --git a/example3_plot_data.py b/example3_plot_data.py index 1463545..ca28bb5 100644 --- a/example3_plot_data.py +++ b/example3_plot_data.py @@ -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 -import pyqtgraph as pg +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() # 断开传感器连接并释放资源 diff --git a/src/orisys/configs/ddjx01.json b/src/orisys/configs/ddjx01.json index dc1e174..ac8eba6 100644 --- a/src/orisys/configs/ddjx01.json +++ b/src/orisys/configs/ddjx01.json @@ -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 } } diff --git a/src/orisys/configs/prototype.json b/src/orisys/configs/prototype.json deleted file mode 100644 index b4aecb7..0000000 --- a/src/orisys/configs/prototype.json +++ /dev/null @@ -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 - } -} - - diff --git a/src/orisys/sensor.py b/src/orisys/sensor.py index 04fc6d6..577d223 100644 --- a/src/orisys/sensor.py +++ b/src/orisys/sensor.py @@ -282,7 +282,7 @@ class Sensor: self.compute_hhd() return True return False - + else: return None # return True