update doc

This commit is contained in:
ykcheungab
2026-03-06 15:05:24 +08:00
parent c95e810e87
commit 44b14cded1
20 changed files with 359 additions and 178 deletions
+3
View File
@@ -12,3 +12,6 @@ orisys.egg-info/
/build_nuitka
/data/
/models/
/doc/.quarto/
*.npy
*.png
Binary file not shown.
BIN
View File
Binary file not shown.
+56 -38
View File
@@ -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` 列出设备
A: 请检查视频源的编号是否正确,以及是否能正常打开视频源。在linux 使用`v4l2-ctl --list-devices` 或 Windows使用`Get-PnpDevice -PresentOnly | Where-Object { $_.Class -match '^Camera' }` 列出摄像头设备。
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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")
+1 -1
View File
@@ -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")
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 1.8 MiB

+43
View File
@@ -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
}
}
Binary file not shown.
+137
View File
@@ -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 <path>`命令:, 例如
``` bash
mkdir orisys_ws
```
创建后如果要更换工作区,使用 `cd <path>`命令,例如:
``` bash
cd C:\Users\orisys\orisys_ws
```
<div style="page-break-after: always;"></div>
## 安装 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”键可以重设数据
## 开发套件使用方法
参考开发套件附带的开发文档。
+1
View File
@@ -0,0 +1 @@
quarto render get_started.md --to docx
Binary file not shown.
+14 -18
View File
@@ -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
+1 -1
View File
@@ -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
+41 -87
View File
@@ -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,14 +139,13 @@ 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
@@ -171,9 +153,7 @@ def main():
writer = None
while True:
sensor1.get_img() # 提取并拼接复眼图像
img = sensor1.img
if img is None:
break;
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
+1 -1
View File
@@ -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"},
+19 -6
View File
@@ -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
@@ -468,6 +476,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)
return flag, output
+1 -1
View File
@@ -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
+37 -21
View File
@@ -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 = {