diff --git a/.gitignore b/.gitignore index fc1eda8..b18cd66 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,7 @@ orisys.egg-info/ /dist_nuitka/ /build_nuitka /data/ -/models/ \ No newline at end of file +/models/ +/doc/.quarto/ +*.npy +*.png \ No newline at end of file diff --git a/API文档-0.3.0.pdf b/API文档-0.3.0.pdf new file mode 100644 index 0000000..bbeb140 Binary files /dev/null and b/API文档-0.3.0.pdf differ diff --git a/Readme.docx b/Readme.docx new file mode 100644 index 0000000..76f19cf Binary files /dev/null and b/Readme.docx differ diff --git a/Readme.md b/Readme.md index f22f13f..b59c8b9 100644 --- a/Readme.md +++ b/Readme.md @@ -25,7 +25,7 @@ pip install orisys-{version}-{python-version}-{python-version}-{os}_{machine}.wh ``` 如 ```bash -pip install orisys-0.2.3-cp310-cp310-win_amd64.whl +pip install orisys-0.3.0-cp310-cp310-win_amd64.whl ``` 3. **安装 CuPy(根据您的 CUDA 版本选择):** @@ -53,17 +53,18 @@ pip install orisys-0.2.3-cp310-cp310-win_amd64.whl import orisys print(orisys.__version__) ``` ---- +*** + ## `Sensor` 类别 ### 描述 `orisys`套件的核心类,用于从视频文件或实时摄像头中读取传感器图像,创建一个具体的传感器对象(实例)。并完成形变、光流及力信息的计算与管理。 ### 输入参数 * **vid_src** (`int | str`, 必填): 传感器 ID、相机编号或视频路径。默认为 `0`。 -* **isstitch** (`bool`,可选): 是否启用图像拼接,默认为 `True`。 -* **cuda** (`bool`,可选): 是否使用 GPU cuda 加速,默认为 `False`。 -* **verbose** (`bool`,可选): 是否输出初始化及运行过程中的详细日志信息,默认为 `False`。 -* **config_name** (`str`,可选): 内部算法或传感器配置`json`文件路径,用于加载对应的参数配置,默认为 `prototype`。 +* **config_name** (`str`,可选): 传感器配置`json`文件路径,用于加载对应的参数配置,默认为 `./config/ddjx01`。 * **cal_path** (`str`,可选): 复眼拼接校准文件路径,默认为 `./config/cal.npy`。有多个传感器同时运行时建议传感器类创建时建议填写,避免校准文件覆写。在首次运行时程序会在找不到文件时自动创建。 +* **backend** (`str`,可选): 光流场解耦计算后端,默认为 `auto`,可选`cupy`,`opencl`,`cpu`。 +* **verbose** (`bool`,可选): 是否输出初始化及运行过程中的详细日志信息,默认为 `False`。 + ### 返回 * `Sensor` 实例,用于后续图像采集、形变计算和数据读取操作。 @@ -78,27 +79,26 @@ sensor = Sensor(0) sensor = Sensor("/data/data.mp4") ``` ---- +*** ## 2. `Sensor.get_img` 方法 ### 描述 -采集并预处理一帧图像。 +采集并预处理一帧图像并存到`Sensor` 类别中。 ### 返回 -* `np.ndarray`:处理后的灰度图(`uint8`,尺寸为`config_resolution_stitch`对应分辨率)。 +* `np.ndarray`:处理后的BGR格式图像(`uint8`,尺寸为json文件中`resolution_stitch`对应分辨率)。 * `None`:当输入源读取失败(例如视频结束、摄像头断开)时返回。 + ### 状态更新 -* 成功时更新`sensor`类里面的灰度图。 +* 成功时更新`sensor`类里面的图像。 ### 示例 ```python -img = sensor.get_img() -if img is None: - break +sensor.get_img() ``` ---- +*** ## 3. `Sensor.compute_deformation` 方法 @@ -108,35 +108,31 @@ if img is None: `sensor`的形变场与力相关状态,并返回当前帧的光流场。 调用前置条件:需要先通过`get_img()`成功更新图像。 +### 输入参数 +* **check_motion**(`bool`,可选): 为 `True` 时先根据光流判断是否有运动,有运动才做 HHD 分解;为 `False` 时每帧都做 HHD。默认 `True`。 +* **threshold**(`float`,可选): **check_motion** 功能的阈值,默认为`6`。 ### 返回 -无 +* `bool`:是否接触到物体,如果**check_motion**为`False`时返回`None`。 ### 示例 ```python -img = sensor.get_img() -if img is None: - break +sensor.get_img() -sensor.compute_deformation() +is_touched = sensor.compute_deformation(check_motion=True, threshold=6) +# 步骤 2.3: 计算触碰点 +if is_touched: + sensor.compute_contact() ``` ---- +*** ## 4. `Sensor.compute_contact` 方法 ### 描述 -基于已计算的法向形变场进行接触区域分析,输出接触质心与轮廓等信息,并生成接触强度图。 +基于已计算的法向形变场进行接触区域分析,输出接触质心与轮廓等信息,并生成接触深度图,默认配置在`json`文件中,可按需调节。 调用前置条件:需先执行`compute_deformation()`。 -### 输入参数 -* **min_contour_area** (`int`,可选): 多接触点模式下的最小轮廓面积阈值,默认为 `3000`。 -* **min_magnitude** (`float`,可选): 最小光流幅度阈值,低于该阈值的区域不参与接触分析,默认为 `1.0`。 -* **expansion_threshold** (`float`,可选): 扩张/收缩判定阈值,用于接触区域分割,默认为 `0.01`。 -* **downsample_factor** (`int`,可选): 下采样因子,用于平滑与加速计算,默认为 `1`。 -* **kernel_size** (`tuple`,可选): 形态学操作核大小,默认为 `(5, 5)`。 -* **denoise_sigma** (`float`,可选): 高斯降噪强度;设为` 0 `可禁用降噪,默认为 `15`。 - ### 返回 `dict`为接触分析结果字典,至少包含以下键: * **expansion_mask**: 扩张区域掩码(`np.ndarray`)。 @@ -160,7 +156,7 @@ centroid, contour, depth = sensor.read_info( sensor.info.DEPTH, ) ``` ---- +*** ## 4. `Sensor.read_info` 方法 ### 描述 @@ -190,7 +186,7 @@ centroid, contour, depth = sensor.read_info( 时间戳 * TIMESTAMP: 时间戳字符串(HH:MM:SS),`str`。 -注:H/W 由当前配置分辨率决定。 +注:H/W 由当前`json`文件中的`resolution_stitch`配置分辨率决定。 ### 返回 @@ -217,7 +213,7 @@ fps, fn, fx, fy = sensor.read_info( # Example 2: 读取单个向量场(注意解包方式) flow = sensor.read_info(sensor.info.VRAW) ``` ---- +*** ## 5. `Sensor.disconnect` 方法 @@ -227,8 +223,30 @@ flow = sensor.read_info(sensor.info.VRAW) 建议在程序退出前调用以确保资源被正确释放。 ---- ---- +*** +*** +# 可视化相关 + +## 1. `util.draw_arrows` 方法 + +### 输入参数 +**frame**:背景图像 +**optical_flow**:光流场 +**threshold**:光流箭头显示阈值 +**grid_spaceing**:光流箭头间距(pixel) +**arrow_scale**:箭头大小 +**margin**:箭头最大长度/间距的比例 +**colormap**:色图,如`cv2.COLORMAP_JET` +**below_threshold_color**:RGB格式的`uint8`颜色,如(0, 0, 255) +**edge_margin**: 不显示箭头边距(pixel) + +### 返回 +`np.ndarray` :箭头光流可视化图像 + +## 2. `util.draw_contact` 方法 +*** +*** + # 配置标定相关 ## 1. `list_config` 方法 @@ -239,7 +257,7 @@ flow = sensor.read_info(sensor.info.VRAW) ### 返回 * `list`: 内置传感器配置参数字典的名字列表。 ---- +*** ## 2. `export_config` 方法 ### 描述 @@ -257,8 +275,8 @@ flow = sensor.read_info(sensor.info.VRAW) * 导出内置的`config_name`配置参数字典至`path` ---- ---- +*** +*** ## 常见问题解答 (FAQ) ### Q1:为什么必须先调用`get_img()`,再调用`compute_deformation()`? @@ -284,4 +302,4 @@ A:若未指定或未找到标定文件,系统会在需要时创建新的拼 拼接结果异常通常与标定数据、拼接配置或输入图像质量有关,建议重新生成拼接标定并检查配置参数。 ### Q6: 运行报错:“AttributeError: 'Stitcher' object has no attribute 'pos_default'” -A: 请检查视频源的编号是否正确,以及是否能正常打开视频源。在linux 使用`v4l2-ctl --list-devices` 列出设备 \ No newline at end of file +A: 请检查视频源的编号是否正确,以及是否能正常打开视频源。在linux 使用`v4l2-ctl --list-devices` 或 Windows使用`Get-PnpDevice -PresentOnly | Where-Object { $_.Class -match '^Camera' }` 列出摄像头设备。 \ No newline at end of file diff --git a/benchmark.py b/benchmark.py index 1c55ea5..8f06491 100644 --- a/benchmark.py +++ b/benchmark.py @@ -20,7 +20,7 @@ def main(): # cal_path为校准文件路径, 每一个设备需要创建/使用一个校准文件 # 方式1: 从视频文件读取 - sensor = orisys.Sensor("s1_720p.mp4", config_name="./config/prototype_of3_720p.json", cal_path= "./config/dd01a_0_720p.npy", isstitch=True, cuda=False, verbose=True, poisson_backend="opencl") + sensor = orisys.Sensor("s1.mp4", config_name="./config/prototype_of3.json", cal_path= "./config/dd01a_0.npy", isstitch=True, cuda=False, verbose=True, poisson_backend="cupy") # 方式2: 从摄像头读取(取消注释使用) # sensor = orisys.Sensor(0, config_name="./config/prototype_of3.json", cal_path= "./config/cal.npy", isstitch=True, cuda=True, verbose=True) diff --git a/build_nuitka.py b/build_nuitka.py index bbb0a6e..d052c86 100644 --- a/build_nuitka.py +++ b/build_nuitka.py @@ -16,7 +16,7 @@ def install_nuitka(): def build(): install_nuitka() - version = "0.2.3" + version = "0.3.0" src_dir = Path("src/orisys") output_dir = Path("build_nuitka") dist_dir = Path("dist_nuitka") diff --git a/build_nuitka_linux.py b/build_nuitka_linux.py index e9d7043..9c76f31 100644 --- a/build_nuitka_linux.py +++ b/build_nuitka_linux.py @@ -16,7 +16,7 @@ def install_nuitka(): def build(): install_nuitka() - version = "0.2.3" + version = "0.3.0" src_dir = Path("src/orisys") output_dir = Path("build_nuitka") dist_dir = Path("dist_nuitka") diff --git a/config/compound_eye.png b/config/compound_eye.png index d60ac8f..117fba7 100644 Binary files a/config/compound_eye.png and b/config/compound_eye.png differ diff --git a/config/ddjx01.json b/config/ddjx01.json new file mode 100644 index 0000000..dc1e174 --- /dev/null +++ b/config/ddjx01.json @@ -0,0 +1,43 @@ +{ + "name": "prototype", + "version": "1.0", + "camera": { + "resolution_raw": [1920, 1080], + "fps": 30, + "units": 15, + "units_used": 9, + "resolution_stitch": [365, 365], + "stitch_res_list": [365,263,123,63] + }, + "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, + "box_size": [220, 220] + }, + "tracker": { + "adaptive": true, + "lower_threshold": 12.0, + "upper_threshold": 18.0, + "loop_closure_interval": 1, + "algorithm": "dis", + "dis_preset": "medium", + "dis_mode": 3, + "algorithm_list": ["raft", "dis"] + }, + "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/doc/get_started.docx b/doc/get_started.docx new file mode 100644 index 0000000..bbc941d Binary files /dev/null and b/doc/get_started.docx differ diff --git a/doc/get_started.md b/doc/get_started.md new file mode 100644 index 0000000..d1993eb --- /dev/null +++ b/doc/get_started.md @@ -0,0 +1,137 @@ +叠动科技软件使用手册 +版本: 0.3.0 +编写日期: 2026年3月3日 + +# 简介 +本文件介绍如何上手和运行叠动视触觉传感器配套的开发套件。本套件是配合叠动视触觉传感器进行图像处理,计算形变、力以及接触信息。它可以从摄像头或视频文件中读取图像,处理后打印和展示处理后信息。 +支持操作系统:Windows、Ubuntu +建议Python 版本: 3.10 + +# 基础环境设置 +## 安装 Python 与 Anaconda +1. 访问 [Anaconda 官网](https://www.anaconda.com/download)下载适合您操作系统的安装包(Windows、Linux)。 +2. 按照提示完成安装。(Widnows:建议在安装过程中勾选“将 Anaconda 添加到系统环境变量”选项,方便后续使用)。 +3. 安装完成后,打开命令行终端(Windows 用户可在”Anaconda Navigator”打开“Anaconda Prompt”),输入 `conda info` ,若能显示 conda 的信息,则说明安装成功。 + +## 创建conda虚拟环境 +虚拟环境相当于一个独立的 Python 工作空间,可以避免不同项目所需的库版本冲突。 +打开命令行终端(或Anaconda Prompt),依次输入以下命令(以 python 3.10 为例): +``` bash +conda create -n orisys python=3.10 +conda activate orisys +``` + +激活后,命令行提示符前会出现 `(orisys)` 字样,表示当前已进入虚拟环境。 + +## 创建工作区文件夹 +“Anaconda Prompt”启动后的默认工作路径在用户根文件夹中,建议创建工作区文件夹方便管理,可以手动创建或者运行`mkdir `命令:, 例如 + +``` bash +mkdir orisys_ws +``` + +创建后如果要更换工作区,使用 `cd `命令,例如: + +``` bash +cd C:\Users\orisys\orisys_ws +``` +
+ +## 安装 CUDA 和 cuDNN(可选) +如果您拥有 NVIDIA 显卡,并希望利用 GPU 加速计算,则需要安装 CUDA 工具包和 cuDNN。 + +1. 方法一(官网下载): +访问 [NVIDIA CUDA Toolkit官网](https://developer.nvidia.com/cuda/toolkit)下载对应系统的 CUDA Toolkit 安装包,并按照提示安装, 注意12.X 和 13.X 的版本对应不同的 CuPy 库版本。 + +2. 方法二(使用 Anaconda 安装,推荐): +在已激活的 orisys 环境中直接运行以下命令(以 CUDA 12.9 为例): +`conda install nvidia/label/cuda-12.9.0::cuda-toolkit nvidia::cudnn` +该方法会自动配置好 CUDA 和 cuDNN,无需手动下载。 + +注意:如果您的计算机没有 NVIDIA 显卡,或者不想使用 GPU 加速,可以跳过此步骤。套件也支持纯 CPU 运行(但速度可能较慢)。 + +# 安装 orisys SDK + +orisys SDK 以 .whl 文件形式提供,您需要根据操作系统、Python 版本和硬件平台选择正确的文件。 + +## 下载正确的 .whl 文件 +从官方渠道获取 .whl 文件,文件名格式通常为: +orisys-{版本}-{Python版本}-{Python版本}-{操作系统}_{硬件架构}.whl +例如:orisys-0.3.0-cp310-cp310-win_amd64.whl 表示: + +版本:0.3.0 +Python 版本:3.10(cp310) +操作系统:Windows(win) +硬件架构:64位(amd64) + +请根据您的系统选择对应的文件,或者咨询客服索取: + +## 安装 .whl 文件 +将下载好的 .whl 文件放在一个文件夹中(如上述`orisys_ws`文件夹),打开命令行(确保已激活 orisys 环境或使用"Anaconda Prompt"),使用 `cd` 命令切换到该文件夹,然后运行: + +``` bash +# 请将文件名替换为您实际下载的文件名 +pip install orisys-0.3.0-cp310-cp310-win_amd64.whl +``` + +## 安装 CuPy(可选) +注意:如果您没有安装 CUDA,或者使用 CPU 运行,可以跳过这一步。 + +CuPy 是一个与 NumPy 类似但支持 GPU 加速的库,您需要根据您安装的 CUDA 版本选择对应的 CuPy 版本。 + +方法一(使用 extras 参数,推荐): +在安装 orisys 时,可以直接指定 CUDA 版本,pip 会自动安装配套的 CuPy: + +```bash +# 如果您的 CUDA 版本是 12.x +pip install orisys[cuda12] + +# 如果您的 CUDA 版本是 13.x +pip install orisys[cuda13] +``` +如果之前已经安装了 orisys,可以单独安装 CuPy: + +方法二(手动安装): + +``` bash +# CUDA 12.x +pip install cupy-cuda12x>=12.3.0 + +# CUDA 13.x +pip install cupy-cuda13x>=13.0.0 +``` + +## 测试安装 +安装完成后,在命令行输入” python“命令进入 Python 环境, 并导入 orisys 库并查看版本号,验证是否成功: +``` bash +python +import orisys +print(orisys.__version__) +``` +如果输出版本号(例如 0.3.0),则说明安装成功。 + +# 使用方法 +## 运行示例程序 +套件里面包含了例子展示用法,运行步骤如下: +### Windows: +1. 检查是否有其他正在连接的USB摄像头设备:打开powershell, 运行命令`Get-PnpDevice -PresentOnly | Where-Object { $_.Class -match '^Camera' }`,如果没有的话不会有输出。 +2. 把传感器的USB头与电脑连接,再次运行步骤一的命令,会新增一行输出 +3. 如果第一步没有输出,传感器的ID就是“0”,如果第一步有一个设备,传感器的ID就是“1”,如此类推。 +4. 在确定了传感器的ID(下方例子假设为“0”)后,打开Anaconda Prompt, 切换到工作路径,然后在命令行中输入: + ``` bash + python example1_basic_pipeline.py -v 0 + ``` +5. 程序会运行,并自动打开可视化窗口和打印资料,在窗口上按键盘”q”键可以停止运行,按”r”键可以重设数据 + +### Ubuntu: +1. 检查是否有其他正在连接的USB摄像头设备:打开终端, 运行命令`v4l2 –list-devices`,如果没有的话不会有输出。 +2. 把传感器的USB头与电脑连接,再次运行步骤一的命令,会新增输出 +3. 根据新增的输出,例如”/dev/video0”,确定传感器的ID为“0”, 如果是”/dev/video3”的话ID为“3” ,如此类推。 +4. 在确定了传感器的ID(下方例子假设为“0”)后,打开终端, 激活相应的python环境,切换到工作路径,然后在命令行中输入: + ``` bash + python example1_basic_pipeline.py -v 0 + ``` +5. 程序会运行,并自动打开可视化窗口和打印资料,在窗口上按键盘”q”键可以停止运行,按”r”键可以重设数据 + +## 开发套件使用方法 +参考开发套件附带的开发文档。 \ No newline at end of file diff --git a/doc/render_command.txt b/doc/render_command.txt new file mode 100644 index 0000000..83c7f91 --- /dev/null +++ b/doc/render_command.txt @@ -0,0 +1 @@ +quarto render get_started.md --to docx \ No newline at end of file diff --git a/doc/软件使用说明-0.3.0.pdf b/doc/软件使用说明-0.3.0.pdf new file mode 100644 index 0000000..ae0ab06 Binary files /dev/null and b/doc/软件使用说明-0.3.0.pdf differ diff --git a/example1_basic_pipeline.py b/example1_basic_pipeline.py index 0c444dd..b8b340d 100644 --- a/example1_basic_pipeline.py +++ b/example1_basic_pipeline.py @@ -19,8 +19,8 @@ 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/prototype_of3.json", help="Configuration file path") - parser.add_argument("--cal","-cal", type=str, default="./config/dd01a_0.npy", help="Calibration 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") args = parser.parse_args() @@ -32,40 +32,36 @@ def main(): try: input = int(args.video) is_camera = True - sensor = orisys.Sensor(input, config_name=args.config, cal_path= args.cal, cuda=True, verbose=args.verbose) + 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, cuda=True, verbose=args.verbose) - - # sensor = orisys.Sensor(args.input, config_name=args.config, cal_path= args.cal, cuda=True, verbose=args.verbose) - - # 方式2: 从摄像头读取(取消注释使用) - # sensor = orisys.Sensor(0, config_name="./config/prototype_of3.json", cal_path= "./config/cal.npy", isstitch=True, cuda=True, verbose=True) - - print("\n按 'q' 退出程序,按 'r' 重置光流追踪器") + sensor = orisys.Sensor(args.video, config_name=args.config, cal_path= args.cal, verbose=args.verbose) + print("\n按 'q' 退出程序,按 'r' 重置传感器追踪器") while True: # 步骤2:信息提取 # 步骤 2.1: 获取并拼接图像 (必要步骤) sensor.get_img() - # if sensor.detect_motion(threshold=2): - # print("motion detected") # 步骤 2.2: 计算形变场 (必要步骤) - sensor.compute_deformation() + is_touched = sensor.compute_deformation(check_motion=True, threshold=6) # 步骤 2.3: 计算触碰点 - sensor.compute_contact() + if is_touched: + sensor.compute_contact() #步骤 2.4: 读取信息 - fps, flow, vnormal, img, contour, centroid, depth_map = sensor.read_info( + 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.DEPTH, + sensor.info.FNORMAL, + sensor.info.FSHEARX, + sensor.info.FSHEARY ) #步骤 3: 可视化显示 @@ -95,7 +91,7 @@ def main(): cv2.imshow("Depth Map", cv2.applyColorMap(div_normalized, cv2.COLORMAP_JET)) #步骤 3.4: 打印资料 - print(f"fps: {fps}") + print(f"fps: {fps}, fnormal: {fnormal}, fshearx: {fshearx}, fsheary: {fsheary}, depth_shape: {depth_map.shape}") #步骤 3.5: 键盘控制 key = cv2.waitKey(1) & 0xFF diff --git a/example3_plot_data.py b/example3_plot_data.py index 473ec8b..e2ef63f 100644 --- a/example3_plot_data.py +++ b/example3_plot_data.py @@ -94,7 +94,7 @@ def main(): # 打开视频 # sensor1 = Sensor("./s1.mp4",isstitch=True,cuda = True,verbose = True) # 方式1: 从视频文件读取 - sensor1 = orisys.Sensor(0, config_name="./config/prototype_of3.json", cal_path= "./config/dd01a_98.npy", isstitch=True, cuda=False, verbose=True) + sensor1 = orisys.Sensor(0, config_name="./config/prototype_of3.json", cal_path= "./config/dd01a_0.npy", isstitch=True, cuda=False, verbose=True) start_time = time.time() is_save = False diff --git a/promote_video.py b/promote_video.py index 5344d5a..625cc20 100644 --- a/promote_video.py +++ b/promote_video.py @@ -9,12 +9,6 @@ import pyqtgraph as pg from PyQt5.QtGui import QImage, QFont from pyqtgraph.Qt import QtGui -# 实时图表 -bg_color = (46, 41, 36) -bg_color_hex = "#24292E" -text_color_hex = "#D1D5DA" -text_color = (209, 213, 218) - def make_gradient_brush(color_hex, max_alpha=150): """ 创建一个从上到下渐变的笔刷 (兼容 PyQt5 和 PyQt6) @@ -41,35 +35,32 @@ def make_gradient_brush(color_hex, max_alpha=150): def create_plot(): - pg.setConfigOption('background', bg_color_hex) # 深蓝灰背景 - pg.setConfigOption('foreground', text_color_hex) # 浅灰文字 + pg.setConfigOption('background', '#0B1016') # 深蓝灰背景 + pg.setConfigOption('foreground', '#A0A0A0') # 浅灰文字 pg.setConfigOptions(antialias=True) # 抗锯齿 - # theme_color = '#00E5FF' - theme_color = '#50abff' + theme_color = '#00E5FF' line_pen = pg.mkPen(color=theme_color, width=2) grad_brush = make_gradient_brush(theme_color, max_alpha=120) - show_values = False - # title_font = QFont("Segoe UI", 14, QFont.Bold) - # axis_font = QFont("Segoe UI", 10, QFont.Bold) - p1 = win.addPlot(title="N") - # p1.titleLabel.item.setFont(title_font) - p1.getAxis('left').setStyle(showValues=show_values) - p1.getAxis('bottom').setStyle(showValues=show_values) + axis_font = QFont("Segoe UI", 11) + p1 = win.addPlot() + p1.setLabel('left', 'N') + p1.getAxis('left').label.setFont(axis_font) + p1.getAxis('left').label.setRotation(-90) p1.showGrid(x=True, y=True, alpha=0.3) win.nextRow() - p2 = win.addPlot(title="X") - # p2.titleLabel.item.setFont(title_font) - p2.getAxis('left').setStyle(showValues=show_values) - p2.getAxis('bottom').setStyle(showValues=show_values) + p2 = win.addPlot() + p2.setLabel('left', 'X') + p2.getAxis('left').label.setFont(axis_font) + p2.getAxis('left').label.setRotation(-90) p2.showGrid(x=True, y=True, alpha=0.3) win.nextRow() - p3 = win.addPlot(title="Y") - # p3.titleLabel.item.setFont(title_font) - p3.getAxis('left').setStyle(showValues=show_values) - p3.getAxis('bottom').setStyle(showValues=show_values) - # p3.setLabel('bottom', 'Time', units='s') - # p3.getAxis('bottom').label.setFont(axis_font) + p3 = win.addPlot() + p3.setLabel('left', 'Y') + p3.setLabel('bottom', 'Time', units='s') + p3.getAxis('left').label.setFont(axis_font) + p3.getAxis('left').label.setRotation(-90) + p3.getAxis('bottom').label.setFont(axis_font) p3.showGrid(x=True, y=True, alpha=0.3) plot_af = p1.plot(pen=line_pen, fillLevel=0, fillBrush=grad_brush) plot_tfx = p2.plot(pen=line_pen, fillLevel=0, fillBrush=grad_brush) @@ -78,18 +69,12 @@ def create_plot(): def update(plot_af, plot_tfx, plot_tfy, data, p1, p2, p3): # global plot_af, plot_tfx, plot_tfy, data, p1, p2, p3 - max1 = max(data[:,1,0]) - max2 = max(data[:,1,1]) - max3 = max(data[:,1,2]) - # max1 = 110000 - # max2 = 260000 - # max3 = 400000 plot_af.setData(data[:,:,0]) - p1.setYRange(min(data[:,1,0]),max1) + p1.setYRange(min(data[:,1,0]),max(data[:,1,0])) plot_tfx.setData(data[:,:,1]) - p2.setYRange(min(data[:,1,1]),max2) + p2.setYRange(min(data[:,1,1]),max(data[:,1,1])) plot_tfy.setData(data[:,:,2]) - p3.setYRange(min(data[:,1,2]),max3) + p3.setYRange(min(data[:,1,2]),max(data[:,1,2])) # 深度图 def draw_magnitude_map(vfield, threshold=3.0, colormap=cv2.COLORMAP_JET) -> np.ndarray: @@ -134,12 +119,10 @@ def draw_magnitude_map(vfield, threshold=3.0, colormap=cv2.COLORMAP_JET) -> np.n return out_img - - -npoints = 100 # 图表数据长度 -plot_threshold = 0 # only plot force values above this (below → 0) -pg.setConfigOption('background', bg_color_hex) -pg.setConfigOption('foreground', bg_color_hex) +# 实时图表 +npoints = 100 # 图表数据长度 +pg.setConfigOption('background', 'k') +pg.setConfigOption('foreground', 'w') app = pg.mkQApp("Plotting Example") win = pg.GraphicsLayoutWidget(show=True, title="Basic plotting examples") win.setWindowTitle('pyqtgraph example: Plotting') @@ -147,7 +130,7 @@ 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 + data[:,0,i] = np.linspace(-npoints,0,npoints) @@ -156,24 +139,21 @@ def main(): parser = argparse.ArgumentParser() parser.add_argument("--video","-v", type=str, default="s1.mp4", help="视频文件路径") - parser.add_argument("--fps", type=float, default=15) args = parser.parse_args() - fps_video = args.fps - arrow_writer = cv2.VideoWriter(args.video.replace(".mp4", "_arrows.mp4"), cv2.VideoWriter_fourcc(*'mp4v'), fps_video, (384, 384)) - pyqt_writer = cv2.VideoWriter(args.video.replace(".mp4", "_pyqt.mp4"), cv2.VideoWriter_fourcc(*'mp4v'), fps_video, (1600, 1200)) - depth_writer = cv2.VideoWriter(args.video.replace(".mp4", "_depth.mp4"), cv2.VideoWriter_fourcc(*'mp4v'),fps_video, (384, 384)) - sensor1 = orisys.Sensor(args.video, config_name="./config/prototype_raft.json", cal_path= args.video.replace(".mp4", ".npy"), isstitch=True, cuda=True, verbose=True) + arrow_writer = cv2.VideoWriter(args.video.replace(".mp4", "_arrows.mp4"), cv2.VideoWriter_fourcc(*'mp4v'), 30, (384, 384)) + pyqt_writer = cv2.VideoWriter(args.video.replace(".mp4", "_pyqt.mp4"), cv2.VideoWriter_fourcc(*'mp4v'), 30, (1600, 1200)) + depth_writer = cv2.VideoWriter(args.video.replace(".mp4", "_depth.mp4"), cv2.VideoWriter_fourcc(*'mp4v'), 30, (384, 384)) + + sensor1 = orisys.Sensor(args.video, config_name="./config/prototype_of3.json", cal_path= "./config/dd01a_98.npy", isstitch=True, cuda=False, verbose=True) start_time = time.time() is_save = False frame_count = 0 writer = None while True: - sensor1.get_img() # 提取并拼接复眼图像 - img = sensor1.img - if img is None: - break; + sensor1.get_img() # 提取并拼接复眼图像 + sensor1.compute_deformation() # 计算形变和分解 sensor1.compute_contact() @@ -182,14 +162,10 @@ def main(): # 帧率,时间戳: FPS, TIMESTAMP # 变形量总和: FNORMAL , FSHEARX, FSHEARY : 法向,切向(X),切向(Y) fps, fn, fx, fy, flow, depth_map = sensor1.read_info(sensor1.info.FPS, sensor1.info.FNORMAL, sensor1.info.FSHEARX, sensor1.info.FSHEARY, sensor1.info.VRAW, sensor1.info.DEPTH) - frame_count += 1 - print(fps,fn,fx,fy,frame_count) - if sensor1.img is None: - break + print(fps,fn,fx,fy) # 显示向量场 - # Background color #24292E (BGR) - black_img = np.full_like(sensor1.img, bg_color) - arrows = orisys.util.draw_arrows(black_img, flow, threshold=2, grid_spacing=20, arrow_scale=0.5, colormap=cv2.COLORMAP_PARULA, below_threshold_color=text_color) + black_img = np.zeros_like(sensor1.img) + arrows = orisys.util.draw_arrows(black_img, flow, threshold=2, grid_spacing=20, arrow_scale=0.5) cv2.imshow("Deformation", arrows) # 显示深度图 @@ -200,40 +176,21 @@ def main(): div_normalized = np.zeros_like(div_abs, dtype=np.uint8) # 应用颜色映射(使用JET:蓝色=低散度,红色=高散度) - depth_colored = cv2.applyColorMap(div_normalized, cv2.COLORMAP_PARULA) - # Set background color for zero/low values (BGR color #24292E) - depth_colored[div_normalized < 5] = bg_color + depth_colored = cv2.applyColorMap(div_normalized, cv2.COLORMAP_JET) cv2.imshow("Depth Map", depth_colored) # cv2.imshow("Depth Map", cv2.applyColorMap(div_normalized, cv2.COLORMAP_JET)) # 显示图表 - 更新时间轴 - elapsed_time = time.time() - start_time + 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, :] = frame_count/fps_video # set current time for all plots + data[-1, 0, :] = frame_count/30.0 # set current time for all plots data[-1, 1, 0] = fn - if abs(fx) - data[-2, 1, 1] > 100000: - fx = data[-2, 1, 1] + data[-2, 1, 1]*0.5 - if abs(fy) - data[-2, 1, 2] > 100000: - fy = data[-2, 1, 2] + data[-2, 1, 2]*0.5 data[-1, 1, 1] = abs(fx) data[-1, 1, 2] = abs(fy) - # Moving average on force data (column 1): simple loop, no singularities at ends - ma_window = 5 - data_plot = data.copy() - for i in range(3): - x = data_plot[:, 1, i] - n = len(x) - out = np.empty_like(x, dtype=np.float64) - for j in range(n): - start = max(0, j - ma_window + 1) - out[j] = np.mean(x[start : j + 1]) - data_plot[:, 1, i] = out - below = data_plot[:, 1, :] < plot_threshold - data_plot[:, 1, :] = np.where(below, 0, data_plot[:, 1, :]) - update(plot_af, plot_tfx, plot_tfy, data_plot, p1, p2, p3) # Update plot - # frame_count += 1 + update(plot_af, plot_tfx, plot_tfy, data, p1, p2, p3) # Update plot + frame_count += 1 # Convert pyqtgraph plot to image # Grab the GraphicsLayoutWidget as QPixmap @@ -241,10 +198,7 @@ def main(): # Convert QPixmap to QImage qimg = pixmap.toImage() # Convert QImage to numpy array (RGB format) - # Qt6/PySide6: QImage.Format.Format_RGB888; Qt5: QImage.Format_RGB888 - QImg = QtGui.QImage - fmt = getattr(QImg.Format, 'Format_RGB888', None) or getattr(QImg, 'Format_RGB888') - qimg = qimg.convertToFormat(fmt) + qimg = qimg.convertToFormat(QImage.Format_RGB888) width = qimg.width() height = qimg.height() # Convert QImage to numpy array diff --git a/setup.py b/setup.py index 2d3402d..fa00e9e 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ if configs_dir.exists(): setup( name="orisys", - version="0.2.3", + version="0.3.0", description="Orisys - Optical Tactile Sensor SDK", author="Your Name", package_dir={"": "src"}, diff --git a/src/orisys/HHD.py b/src/orisys/HHD.py index 0e1c58a..225f712 100644 --- a/src/orisys/HHD.py +++ b/src/orisys/HHD.py @@ -91,12 +91,18 @@ class nHHD(object): if self.poisson_backend == 'cpu' or self.poisson_backend == 'opencl': return self.h elif self.poisson_backend == 'cupy': + # Use CPU copy from get_force_cuda when set (stable snapshot; avoids jumps) + if hasattr(self, 'h') and self.h is not None: + return self.h return cp.asnumpy(self._h_gpu) def get_normal(self): if self.poisson_backend == 'cpu' or self.poisson_backend == 'opencl': return self.d elif self.poisson_backend == 'cupy': + # Use CPU copy from get_force_cuda when set (stable snapshot; avoids jumps) + if hasattr(self, 'd') and self.d is not None: + return self.d return cp.asnumpy(self._d_gpu) def _solve_poisson_backend(self, field, verbose=False): @@ -249,12 +255,11 @@ class nHHD(object): print('div =', div_gpu.shape, float(div_gpu.min()), float(div_gpu.max())) print('curlw =', curlw_gpu.shape, float(curlw_gpu.min()), float(curlw_gpu.max())) - with self.stream1: - nD_gpu = self.psolver._solve_gpu_internal(div_gpu) - with self.stream2: - nRu_gpu = self.psolver._solve_gpu_internal(curlw_gpu) - self.stream1.synchronize() - self.stream2.synchronize() + # Use a single stream for both solves. Running concurrent FFT-based + # solves on separate streams can cause occasional long-run jitter. + nD_gpu = self.psolver._solve_gpu_internal(div_gpu) + nRu_gpu = self.psolver._solve_gpu_internal(curlw_gpu) + cp.cuda.get_current_stream().synchronize() if verbose > 0: print('nD =', nD_gpu.shape, float(nD_gpu.min()), float(nD_gpu.max())) @@ -282,6 +287,9 @@ class nHHD(object): if verbose > 0: print('h =', h_gpu.shape, float(h_gpu.min()), float(h_gpu.max())) + # Ensure all default-stream work (gradient, etc.) is complete before use + cp.cuda.get_current_stream().synchronize() + # Store GPU arrays self._d_gpu = d_gpu self._h_gpu = h_gpu @@ -467,6 +475,11 @@ class nHHD(object): self.Sn, self.Stx, self.Sty, self.Stor = Sn, Stx, Sty, Stor output = numpy.array([Sn, Stx, Sty, Stor], dtype=numpy.float32) + + # Keep stable CPU copies for get_normal()/get_shear() so callers get this frame's + # result instead of reading GPU memory that can change (avoids CuPy-only jumps) + self.d = cp.asnumpy(self._d_gpu) + self.h = cp.asnumpy(self._h_gpu) flag = (Sn >= 300 and Stx >= 300 and Sty >= 300 and abs(Stor) >= 1000) diff --git a/src/orisys/__init__.py b/src/orisys/__init__.py index 12003ea..cb90086 100644 --- a/src/orisys/__init__.py +++ b/src/orisys/__init__.py @@ -5,7 +5,7 @@ A Python library for processing and analyzing optical tactile sensor data, including image stitching, optical flow tracking, and deformation decomposition. """ -__version__ = "0.2.3" +__version__ = "0.3.0" __author__ = "Orisys" from .sensor import Sensor diff --git a/src/orisys/sensor.py b/src/orisys/sensor.py index cfce9d1..5f4d0b3 100644 --- a/src/orisys/sensor.py +++ b/src/orisys/sensor.py @@ -60,7 +60,7 @@ class Sensor: # ========================================================================= def __init__(self, vid_src, cuda=True, isstitch=True, verbose=True, debug=False, - config_name="prototype", cal_path=None, override=False, box=None, mirror=True, poisson_backend='auto'): + config_name="prototype", cal_path=None, override=False, box=None, mirror=True, backend='auto'): """ Initialize Sensor with video source and configuration. @@ -73,6 +73,11 @@ class Sensor: cal_path: Path to calibration file (default: None) override: Skip auto-detection, use provided box (default: False) box: Manual bounding boxes when override=True (default: None) + backend: Poisson backend (default: "auto") + "auto": Automatically select the best backend based on availability + "cupy": Use CuPy for GPU acceleration + "opencl": Use OpenCL for GPU acceleration + "cpu": Use CPU for computation """ self.CUPY_AVAILABLE = check_cupy_available() @@ -112,8 +117,9 @@ class Sensor: # Optical Flow Data Variables (public) self.flow = np.zeros(shape + (2,), dtype=np.float32) - self.vnormal = np.zeros_like(self.flow) - self.vshear = np.zeros_like(self.flow) + self.vtemplate = np.zeros_like(self.flow) + self.vnormal = self.vtemplate + self.vshear = self.vtemplate self.n_boxes = None self.fnormal = self.fshearx = self.fsheary = self.ftor = 0.0 @@ -134,7 +140,7 @@ class Sensor: self.__tracker = Tracker(adaptive=True, cuda=cuda, verbose=self.verbose, config_tracker=self.__config_tracker) self.__contact3d = Contact3D(config_contact=self.__config_contact, verbose=self.verbose) self.__hhd = None - self.poisson_backend = poisson_backend + self.poisson_backend = backend self._use_pyav = False self._av_container = None self._av_stream = None @@ -211,15 +217,16 @@ class Sensor: self._fps = 1.0 / (time.perf_counter() - self.last_log_time) self.last_log_time = time.perf_counter() - return None + return self.img.copy() - def detect_motion(self, threshold=4): - if self.initial_img is None: - return False - diff = cv2.absdiff(self.img, self.initial_img) - return diff.mean() > threshold + def detect_motion(self, threshold=6): - def compute_deformation(self, decompose=True): + # detect motion by optical flow magnitude + mag = np.linalg.norm(self.flow, axis=1) + # print(f"mag: {mag.max()}, {mag.min()}, {mag.mean()}") + return mag.mean() > threshold + + def compute_deformation(self, decompose=True, check_motion=True, threshold=6): """ Compute optical flow and decompose into force components. @@ -230,10 +237,19 @@ class Sensor: raise Exception("Warning: Get image first") self.compute_flow() - if decompose: - self.compute_hhd() + # self.compute_hhd() + if check_motion: + if self.detect_motion(threshold=threshold): + self.compute_hhd() + return True + else: + return False + else: + if decompose: + self.compute_hhd() - return True + return None + # return True def compute_flow(self): if self.img is None: @@ -326,14 +342,14 @@ class Sensor: if self.img is None: raise Exception("Warning: Get image first") - if any(arg in [self.info.VRAW, self.info.VNORMAL, self.info.VSHEAR, self.info.FNORMAL, self.info.FSHEARX, self.info.FSHEARY] for arg in args): - if not self.compute_deformation_valid: - print("Warning: Compute deformation first") - # raise Exception("Warning: Compute deformation first") + # if any(arg in [self.info.VRAW, self.info.VNORMAL, self.info.VSHEAR, self.info.FNORMAL, self.info.FSHEARX, self.info.FSHEARY] for arg in args): + # if not self.compute_deformation_valid: + # print("Warning: Compute deformation first") + # # raise Exception("Warning: Compute deformation first") - if any(arg in [self.info.CENTROID, self.info.CONTOUR, self.info.DEPTH] for arg in args): - if not self.compute_contact_valid: - print("Warning: Compute contact first") + # if any(arg in [self.info.CENTROID, self.info.CONTOUR, self.info.DEPTH] for arg in args): + # if not self.compute_contact_valid: + # print("Warning: Compute contact first") # raise Exception("Warning: Compute contact first") mapping = {