organize exampels according to products

This commit is contained in:
ykcheungab
2026-08-03 15:52:15 +08:00
parent 8b2f76c76f
commit 3117ddd5b9
21 changed files with 657 additions and 365 deletions
+22 -4
View File
@@ -8,7 +8,7 @@ and several runnable examples for camera and video-based workflows.
- `src/orisys/`: installable SDK package
- `config/`: default and lab/runtime JSON configurations
- `examples/`: user-facing example scripts
- `examples/`: product demos (`products/ory_box`, `ory_mini`, `ory_finger`) plus shared `gui/`, `pipeline/`, `viz/`
- `scripts/`: maintenance, calibration, packaging, and demo scripts
- `tools/`: non-package tooling such as patch-generation utilities
- `tests/`: automated pytest suite
@@ -49,18 +49,36 @@ pytest
### 4. Run an example
ORY-BOX:
```bash
python examples/basic_pipeline.py -v 0 -c ./config/ddjx01.json
python examples/products/ory_box/basic_pipeline.py -v 0 -c ddjx01
```
ORY-MINI:
```bash
python examples/products/ory_mini/basic_pipeline.py -v 0 -c mini
```
ORY-FINGER:
```bash
python examples/products/ory_finger/finger_pipeline.py --camera_id 0 --config dd02
```
Compatibility wrappers remain at `examples/basic_pipeline.py` and
`examples/finger_pipeline*.py`.
## Configuration
The canonical configuration files now live in the repo-level `config/` folder.
The package loader `orisys.configs` resolves configs from:
1. an explicit JSON file path
2. a config file stem under `config/`
3. a backward-compatible JSON `"name"` match, such as `"prototype"`
2. a config file stem under `config/` (for example `ddjx01`, `mini`, `dd02`)
3. a product alias such as `dd02` -> `dd02-ov`
4. a backward-compatible JSON `"name"` match, such as `"prototype"`
Examples:
+1 -1
View File
@@ -18,7 +18,7 @@ import sys
from pathlib import Path
# ---------- Configuration ----------
ENTRY_SCRIPT = Path("examples/finger_pipeline_3dview.py").resolve()
ENTRY_SCRIPT = Path("examples/products/ory_finger/finger_pipeline_3dview.py").resolve()
EXE_NAME = "OrisysDashboard.exe"
DIST_NAME = "OrisysDashboard" # final folder name
repo_root = Path(__file__).parent.parent
+12 -13
View File
@@ -2,24 +2,24 @@
"name": "prototype",
"version": "1.0",
"camera": {
"resolution_raw": [1280, 720],
"fps": 15,
"resolution_raw": [1920, 1080],
"fps": 30,
"units": 15,
"units_used": 9,
"resolution_stitch": [263, 263],
"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": 25,
"max_area_frac": 0.05,
"max_aspect_ratio": 50,
"index": [1,2,3,6,7,8,11,12,13],
"margin": 10,
"nominal_ov_pos": 67,
"nominal_ov_pos": 100,
"n": [3,3],
"row_tol": 0.6,
"box_size": [180, 180],
"blend":"numpy",
"box_size": [250, 250],
"blend":"numba",
"blend_list": ["numpy", "numba","none"]
},
"tracker": {
@@ -31,16 +31,15 @@
"dis_preset": "medium",
"dis_mode": 3,
"algorithm_list": ["raft", "dis"],
"hhd_downsample": 3,
"backend": "cpu"
"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": 10
"denoise_sigma": 12
}
}
+35
View File
@@ -0,0 +1,35 @@
{
"name": "dd02-ov",
"type": "single_camera",
"version": "1.0",
"camera": {
"resolution_raw": [1280, 720],
"fps": 30,
"resolution_stitch": [420, 450],
"source_scale": 0.4,
"old":[672, 728, 1,0.5],
"work": [504, 546,1],
"work2": [420, 450,0.8],
"work3": [350, 375,4,0.7],
"work4": [280, 300,4,0.4,0.2]
},
"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"],
"hhd_downsample": 5
},
"contact": {
"min_contour_area": 3000,
"flow_threshold": 0.2,
"div_threshold": 0.022,
"expansion_threshold": 0.01,
"kernel_size": [5, 5],
"denoise_sigma": 12
}
}
+10 -10
View File
@@ -238,7 +238,7 @@ flow = sensor.read_info(sensor.info.VRAW)
## `orisys.finger` 手指 UV pipeline
`examples/finger_pipeline.py` 展示了一个更适合手指/单相机场景的高层 API:
`examples/products/ory_finger/finger_pipeline.py` 展示了一个更适合手指/单相机场景的高层 API:
1. 使用 `FingerRuntime.open()` 打开视频源、加载 Blender 导出的资产,并构建 UV 展开映射。
2. 用 `grab_and_process()` 或 `read_source() + process_frame()` 获取一帧展开后的触觉图像。
@@ -262,7 +262,7 @@ flow = sensor.read_info(sensor.info.VRAW)
#### 输入参数
* **export_dir** (`str`,可选): 手指导出资产目录,默认值来自 `orisys.finger.DEFAULT_FINGER_EXPORT_DIR`。目录下通常需要包含 `mesh.npz`、`camera_intrinsics.npz`、`world_to_cam.npy`。
* **config** (`str`,可选): Orisys 配置文件路径,默认 `./config/dd02-ov.json`。
* **config** (`str`,可选): Orisys 配置文件名称或路径,默认 `dd02`(兼容解析到 `dd02-ov`)。
* **map_cache** (`str`,可选): UV 展开缓存文件路径,默认值来自 `orisys.finger.DEFAULT_FINGER_MAP_CACHE`。
* **source_scale** (`float`,可选): 输入相机分辨率缩放比例;未提供时根据配置自动推断。
* **calib** (`str | None`,可选): 外部相机标定文件路径;未提供时优先使用导出资产中的内参。
@@ -282,7 +282,7 @@ flow = sensor.read_info(sensor.info.VRAW)
from orisys.finger import FingerRuntime
runtime = FingerRuntime.open(
config="./config/dd02-ov.json",
config="dd02",
video=0,
verbose=True,
)
@@ -312,7 +312,7 @@ runtime = FingerRuntime.open(
#### 描述
对一帧原始输入执行源图预处理、UV 展开,并把展开后的结果送入内部 `TactileProcessor`。这是 `examples/finger_pipeline.py` 中 profile 模式使用的方法。
对一帧原始输入执行源图预处理、UV 展开,并把展开后的结果送入内部 `TactileProcessor`。这是 `examples/products/ory_finger/finger_pipeline.py` 中 profile 模式使用的方法。
#### 输入参数
@@ -327,7 +327,7 @@ runtime = FingerRuntime.open(
#### 描述
先读取一帧视频源,再调用 `process_frame()` 完成 UV 展开并推入内部触觉处理器。非 profile 模式下,`examples/finger_pipeline.py` 使用该方法作为主循环入口。
先读取一帧视频源,再调用 `process_frame()` 完成 UV 展开并推入内部触觉处理器。非 profile 模式下,`examples/products/ory_finger/finger_pipeline.py` 使用该方法作为主循环入口。
#### 输入参数
@@ -357,7 +357,7 @@ runtime = FingerRuntime.open(
#### 描述
先执行一次采集与 UV 展开,然后调用 `runtime.tactile.warmup()` 预热内部光流/HHD 相关缓存。`examples/finger_pipeline.py` 在进入主循环前会先执行一次 warmup。
先执行一次采集与 UV 展开,然后调用 `runtime.tactile.warmup()` 预热内部光流/HHD 相关缓存。`examples/products/ory_finger/finger_pipeline.py` 在进入主循环前会先执行一次 warmup。
#### 输入参数
@@ -386,7 +386,7 @@ runtime = FingerRuntime.open(
* **raw_sensor**: 底层 `Sensor` 对象。
* **info**: 与 `Sensor.info` 相同的枚举入口。
* **img / flow / vnormal / vshear / depth_map**: 当前帧处理结果。
* **mask_stats**: 有效 UV 区域统计信息;`examples/finger_pipeline.py` 会读取 `mask_stats["coverage"]` 输出当前掩码覆盖率。
* **mask_stats**: 有效 UV 区域统计信息;`examples/products/ory_finger/finger_pipeline.py` 会读取 `mask_stats["coverage"]` 输出当前掩码覆盖率。
* **valid_mask**: 当前有效区域掩码。
#### 说明
@@ -395,7 +395,7 @@ runtime = FingerRuntime.open(
### 10. 最小手指 pipeline 示例
下面的流程与 `examples/finger_pipeline.py` 一致:
下面的流程与 `examples/products/ory_finger/finger_pipeline.py` 一致:
```python
import time
@@ -404,7 +404,7 @@ import orisys
from orisys.finger import FingerRuntime
runtime = FingerRuntime.open(
config="./config/dd02-ov.json",
config="dd02",
video=0,
verbose=True,
)
@@ -462,7 +462,7 @@ finally:
### 11. 关于 profile 模式
`examples/finger_pipeline.py` 中的 `--profile` / `--profile-every` 是示例脚本参数,不属于 `orisys` SDK 核心 API。本质上它只是把主循环拆成:
`examples/products/ory_finger/finger_pipeline.py` 中的 `--profile` / `--profile-every` 是示例脚本参数,不属于 `orisys` SDK 核心 API。本质上它只是把主循环拆成:
1. `runtime.read_source()`
2. `runtime.process_frame(...)`
+17 -14
View File
@@ -112,13 +112,16 @@ print(orisys.__version__)
# 使用方法
## 运行示例程序
套件里面包含了多个例子,建议都在仓库根目录运行(即 `config/`、`examples/`、`assets/` 同级目录)。常见入口分为两类:
套件里面包含了多个例子,建议都在仓库根目录运行(即 `config/`、`examples/`、`assets/` 同级目录)。按产品入口如下:
1. `examples/basic_pipeline.py`:复眼/拼接传感器基础 OpenCV 示例
2. `examples/finger_pipeline.py`:手指传感器2维显示最小流程示例
3. `examples/finger_pipeline_3dview.py`:手指传感器 Qt 3D 可视化界面
1. `examples/products/ory_box/basic_pipeline.py`:ORY-BOX 复眼/拼接传感器基础 OpenCV 示例(默认配置 `ddjx01`)
2. `examples/products/ory_mini/basic_pipeline.py`:ORY-MINI 基础 OpenCV 示例(默认配置 `mini`)
3. `examples/products/ory_finger/finger_pipeline.py`:ORY-FINGER 2D UV 最小流程(默认配置 `dd02`)
4. `examples/products/ory_finger/finger_pipeline_3dview.py`:ORY-FINGER Qt 3D 可视化界面(默认配置 `dd02`)
下面先介绍最基础的 `basic_pipeline.py` 运行方式。
`dd02` 是当前 ORY-FINGER 的稳定别名,实际会解析到 `dd02-ov`。旧路径 `examples/basic_pipeline.py` / `examples/finger_pipeline*.py` 仍可作为兼容入口。
下面先介绍最基础的 ORY-BOX 运行方式。
### Windows:
1. 检查是否有其他正在连接的USB摄像头设备:打开powershell, 运行命令`Get-PnpDevice -PresentOnly | Where-Object { $_.Class -match '^Camera' }`,如果没有的话不会有输出。
@@ -126,7 +129,7 @@ print(orisys.__version__)
3. 如果第一步没有输出,传感器的ID就是“0”,如果第一步有一个设备,传感器的ID就是“1”,如此类推。
4. 在确定了传感器的ID(下方例子假设为“0”)后,打开Anaconda Prompt, 切换到工作路径,然后在命令行中输入:
``` bash
python examples/basic_pipeline.py -v 0
python examples/products/ory_box/basic_pipeline.py -v 0 -c ddjx01
```
5. 程序会运行,并自动打开可视化窗口和打印资料,在窗口上按键盘”q”键可以停止运行,按”r”键可以重设数据
@@ -136,22 +139,22 @@ print(orisys.__version__)
3. 根据新增的输出,例如”/dev/video0”,确定传感器的ID为“0”, 如果是”/dev/video3”的话ID为“3” ,如此类推。
4. 在确定了传感器的ID(下方例子假设为“0”)后,打开终端, 激活相应的python环境,切换到工作路径,然后在命令行中输入:
``` bash
python examples/basic_pipeline.py -v 0
python examples/products/ory_box/basic_pipeline.py -v 0 -c ddjx01
```
5. 程序会运行,并自动打开可视化窗口和打印资料,在窗口上按键盘”q”键可以停止运行,按”r”键可以重设数据
### 手指传感器 UV 示例
### ORY-FINGER UV 示例
如果您使用的是手指/单相机 UV 展开流程,可运行:
如果您使用的是 ORY-FINGER(当前配置别名 `dd02`),可运行:
``` bash
python examples/finger_pipeline.py --camera_id 0 --config ./config/dd02-ov.json
python examples/products/ory_finger/finger_pipeline.py --camera_id 0 --config dd02
```
如果输入来自视频文件,也可以改用:
``` bash
python examples/finger_pipeline.py --video ./data/sample.mp4 --config ./config/dd02-ov.json
python examples/products/ory_finger/finger_pipeline.py --video ./data/sample.mp4 --config dd02
```
该示例会:
@@ -168,10 +171,10 @@ python examples/finger_pipeline.py --video ./data/sample.mp4 --config ./config/d
例如:
``` bash
python examples/finger_pipeline.py --camera_id 0 --config ./config/dd02-ov.json
python examples/products/ory_finger/finger_pipeline.py --camera_id 0 --config dd02
```
### 手指 3D 可视化界面
### ORY-FINGER 3D 可视化界面
如果要使用 GUI ,安装GUI依赖:
``` bash
@@ -180,7 +183,7 @@ pip install pyqt5 pyqtgraph
```
然后运行:
``` bash
python examples/finger_pipeline_3dview.py --video 0 --config ./config/dd02-ov.json
python examples/products/ory_finger/finger_pipeline_3dview.py --video 0 --config dd02
```
该界面会提供:
+52 -38
View File
@@ -1,54 +1,66 @@
# Examples
Top-level demo scripts under `examples/`. Run them from the repository root.
Product-facing demos live under `examples/products/`. Shared helpers stay in
`pipeline/`, `viz/`, and `gui/`. Run commands from the repository root.
## Top-Level Files
## Product Entry Points
- `README.md` — this overview of the root-level example entry points
- `basic_pipeline.py` — basic single-sensor OpenCV demo for the compound-eye pipeline
- `plot_data.py` — single-sensor demo with OpenCV views plus PyQtGraph force charts
- `two_sensor_pipeline.py` — two-sensor multiprocessing demo for parallel acquisition / display
- `finger_pipeline.py` — minimal finger UV-unroll pipeline with optional profiling
- `finger_pipeline_3dview.py` — main finger dashboard entry point with the unified Qt UI
| Product | Entry | Default config |
|---------|-------|----------------|
| ORY-BOX | `products/ory_box/basic_pipeline.py` | `ddjx01` |
| ORY-MINI | `products/ory_mini/basic_pipeline.py` | `mini` |
| ORY-FINGER (2D) | `products/ory_finger/finger_pipeline.py` | `dd02` |
| ORY-FINGER (3D Qt) | `products/ory_finger/finger_pipeline_3dview.py` | `dd02` |
`dd02` is the stable ORY-FINGER alias and currently resolves to `dd02-ov`.
Compatibility wrappers still exist at the `examples/` root
(`basic_pipeline.py`, `finger_pipeline.py`, `finger_pipeline_3dview.py`) and
forward to the product entries above.
## Typical Usage
For the Qt dashboard:
ORY-BOX:
```bash
python examples/products/ory_box/basic_pipeline.py -v 0 -c ddjx01
```
ORY-MINI:
```bash
python examples/products/ory_mini/basic_pipeline.py -v 0 -c mini
```
ORY-FINGER 2D:
```bash
python examples/products/ory_finger/finger_pipeline.py --camera_id 0 --config dd02
```
ORY-FINGER video file:
```bash
python examples/products/ory_finger/finger_pipeline.py --video ./data/sample.mp4 --config dd02
```
ORY-FINGER 3D Qt dashboard:
```bash
pip install -e ".[gui]"
python examples/finger_pipeline_3dview.py --video 0 --config ./config/dd02-ov.json
python examples/products/ory_finger/finger_pipeline_3dview.py --video 0 --config dd02
```
For the minimal finger pipeline:
## Other Root Examples
```bash
python examples/finger_pipeline.py --camera_id 0 --config ./config/dd02-ov.json
```
For the minimal finger pipeline with profiling:
```bash
python examples/finger_pipeline.py --camera_id 0 --config ./config/dd02-ov.json --profile --profile-every 30
```
For a video-file finger run:
```bash
python examples/finger_pipeline.py --video ./data/sample.mp4 --config ./config/dd02-ov.json
```
For the basic compound-eye pipeline:
```bash
python examples/basic_pipeline.py -v 0 -c ./config/ddjx01.json
```
- `plot_data.py` — single-sensor OpenCV views plus PyQtGraph force charts
- `two_sensor_pipeline.py` — two-sensor multiprocessing demo
## Requirements
- SDK installed from the repository root, for example `pip install -e .`
- camera index or test video path
- matching config files under `./config/`
- matching config names under `./config/` (or packaged `orisys/configs/`)
Qt / PyQtGraph examples additionally require:
@@ -58,8 +70,10 @@ pip install -e ".[gui]"
## Notes
- `finger_pipeline_3dview.py` is the main interactive demo. It uses `gui/app_paths.py` so the same entry point works in editable and packaged runs.
- Dashboard controls: click **启动** to start acquisition; use **Record CSV / Stop CSV** and the CSV save-directory browser to log contact/force data; `Q` quits, `R` resets, `S` starts video recording, `E` stops video recording, and `Esc` toggles fullscreen.
- `finger_pipeline.py` is the leanest finger example if you want to validate UV unroll + force/contact processing without the full dashboard.
- `finger_pipeline.py` uses `orisys.finger.FingerRuntime` as the high-level entry point, then runs tactile analysis through `runtime.tactile`.
- In `--profile` mode, the example splits the loop into capture, UV unroll, deformation, contact, and display stages so you can measure each step separately.
- `products/ory_finger/finger_pipeline_3dview.py` is the main interactive finger demo.
It uses `gui/app_paths.py` so the same entry works in editable and packaged runs.
- Dashboard controls: click **启动** to start acquisition; use **Record CSV / Stop CSV**
and the CSV save-directory browser to log contact/force data; `Q` quits, `R` resets,
`S` starts video recording, `E` stops video recording, and `Esc` toggles fullscreen.
- `products/ory_finger/finger_pipeline.py` is the leanest finger example if you want to
validate UV unroll + force/contact processing without the full dashboard.
+13 -107
View File
@@ -1,115 +1,21 @@
#!/usr/bin/env python3
"""Compatibility wrapper for ORY-BOX.
Prefer:
python examples/products/ory_box/basic_pipeline.py -v 0 -c ddjx01
"""
示例 1:单传感器基础流程
本示例演示 Orisys SDK 的标准处理流程:
1. 初始化传感器(摄像头或视频文件)
2. 获取图像并计算形变
3. 按需计算接触区域
4. 读取并可视化结果
from __future__ import annotations
SDK version: 0.4.2
"""
import os
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
import cv2
import orisys
import numpy as np
import argparse
import sys
from pathlib import Path
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--video","-v", type=str, default="0", help="视频源:摄像头编号或视频文件路径")
parser.add_argument("--config","-c", type=str, default="./config/ddjx01.json", help="配置文件名称或路径")
parser.add_argument("--verbose","-verbose", type=bool, default=True, help="是否输出详细日志")
args = parser.parse_args()
_EXAMPLES_DIR = Path(__file__).resolve().parent
if str(_EXAMPLES_DIR) not in sys.path:
sys.path.insert(0, str(_EXAMPLES_DIR))
# 步骤 1:创建传感器对象(必需)
# config_name 可以是内置配置名称,也可以是自定义配置文件路径
# cal_path 为标定文件路径;每台设备建议使用独立的标定文件
# 输入既可以是摄像头编号,也可以是视频文件路径
try:
input = int(args.video)
is_camera = True
sensor = orisys.Sensor(input, config_name=args.config, verbose=args.verbose)
except:
is_camera = False
sensor = orisys.Sensor(args.video, config_name=args.config, verbose=args.verbose)
print("\n按键说明:'q' 退出程序,'r' 重置追踪器。")
while True:
# 步骤 2:数据处理
# 步骤 2.1:获取并拼接图像(必需)
sensor.get_img()
# 步骤 2.2:计算形变场(必需);返回值表示当前帧是否检测到接触/运动
is_contact_now = sensor.compute_deformation(check_motion=True, threshold=0)
# 步骤 2.3:仅在当前帧检测到接触/运动时计算接触区域
# if is_contact_now:
sensor.compute_contact()
# 步骤 2.4:读取结果
fps, flow, vnormal, img, contour, centroid, depth_map, fnormal, fshearx, fsheary = sensor.read_info(
sensor.info.FPS,
sensor.info.VRAW,
sensor.info.VNORMAL,
sensor.info.IMG,
sensor.info.CONTOUR,
sensor.info.CENTROID,
sensor.info.DEPTH,
sensor.info.FNORMAL,
sensor.info.FSHEARX,
sensor.info.FSHEARY
)
# 步骤 3:可视化显示
# 步骤 3.1:显示形变矢量场
arrows = orisys.util.draw_arrows(
img,
flow,
threshold=2,
grid_spacing=20,
arrow_scale=1.0
)
cv2.imshow("形变矢量场", arrows)
# 步骤 3.2:显示接触区域和质心
image_with_foe = orisys.util.draw_contact(img, contour, centroid)
cv2.imshow("接触区域与质心", image_with_foe)
# 步骤 3.3:显示深度图
# 将深度值归一化到 0-255 范围
div_abs = depth_map
if div_abs.max() > div_abs.min():
div_normalized = ((div_abs - div_abs.min()) / (div_abs.max() - div_abs.min()) * 255).astype(np.uint8)
else:
div_normalized = np.zeros_like(div_abs, dtype=np.uint8)
# 应用伪彩色映射
cv2.imshow("深度图", cv2.applyColorMap(div_normalized, cv2.COLORMAP_JET))
# 步骤 3.4:打印关键结果
print(
f"FPS={fps:.2f}, 法向力={fnormal:.4f}, 切向力X={fshearx:.4f}, "
f"切向力Y={fsheary:.4f}, 深度图尺寸={depth_map.shape}, 原始分辨率={sensor.img_size_raw}"
)
# 步骤 3.5:键盘控制
key = cv2.waitKey(1) & 0xFF
if key == ord("q"):
print("\n正在退出示例程序...")
break
if key == ord("r"):
# 当光流追踪出现漂移时,可手动重置追踪器
sensor.reset()
print("追踪器已重置。")
# =========================================================================
# 4. 释放资源
# =========================================================================
sensor.disconnect() # 释放视频源及相关资源
from products.ory_box.basic_pipeline import main
if __name__ == '__main__':
if __name__ == "__main__":
main()
+10 -98
View File
@@ -1,108 +1,20 @@
#!/usr/bin/env python3
"""
Minimum UV-unroll finger tactile pipeline.
"""Compatibility wrapper for ORY-FINGER 2D pipeline.
Offline: load Blender export -> build_or_load_domain -> unroll_map_uv.npz
Runtime: camera frame -> FrameUnwarper -> TactileProcessor -> forces/contact
For the full demo (UV debug panels, dashboard, benchmarks) see:
tools/3d_patch_generation_blender/unroll_realtime.py
Prefer:
python examples/products/ory_finger/finger_pipeline.py --camera_id 0 --config dd02
"""
import argparse
import os
from __future__ import annotations
import sys
from pathlib import Path
os.environ.setdefault("OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS", "0")
_EXAMPLES_DIR = Path(__file__).resolve().parent
if str(_EXAMPLES_DIR) not in sys.path:
sys.path.insert(0, str(_EXAMPLES_DIR))
import cv2
import orisys
from orisys.finger import FingerRuntime
def main():
parser = argparse.ArgumentParser(description="Minimum UV-unroll finger tactile pipeline")
parser.add_argument("--camera_id", type=int, default=0)
parser.add_argument("--video", help="Video file path (overrides camera)")
parser.add_argument("--config", "-c", default="./config/dd02-ov.json", help="Orisys SDK config")
parser.add_argument("--show", action="store_true", default=True, help="Show unrolled image window")
parser.add_argument("--verbose", action="store_true", help="SDK verbose logs")
args = parser.parse_args()
try:
video = args.camera_id
if args.video:
video = args.video
runtime = FingerRuntime.open(
config=args.config,
video=video,
verbose=args.verbose,
)
except RuntimeError as exc:
print(str(exc), file=sys.stderr)
sys.exit(1)
if args.verbose:
geometry = runtime.geometry
print(
f"UV geometry: native={geometry.native_width}x{geometry.native_height}, "
f"source={geometry.source_width}x{geometry.source_height} "
f"output={geometry.out_width}x{geometry.out_height} "
)
runtime.warmup(color=False)
tactile = runtime.tactile
print("Press 'q' to quit, 'r' to reset tracker.")
try:
while True:
ok, unrolled = runtime.grab_and_process(color=False)
if not ok:
break
tactile.compute_deformation(check_motion=True, threshold=0)
tactile.compute_contact()
fps, flow, vnormal, img, contour, centroid, depth_map, fnormal, fshearx, fsheary = tactile.read_info(
tactile.info.FPS,
tactile.info.VRAW,
tactile.info.VNORMAL,
tactile.info.IMG,
tactile.info.CONTOUR,
tactile.info.CENTROID,
tactile.info.DEPTH,
tactile.info.FNORMAL,
tactile.info.FSHEARX,
tactile.info.FSHEARY,
)
status = (
f"FPS={fps:.2f} normal={fnormal:.2f} shear=({fshearx:.2f}, {fsheary:.2f}) "
f"mask={tactile.mask_stats['coverage']:.2f}"
)
if args.show:
arrows = orisys.util.draw_arrows(
unrolled,
flow,
threshold=0.4,
grid_spacing=10,
arrow_scale=1.0,
)
cv2.imshow("UV Unrolled", arrows)
print(status, end="\r")
key = cv2.waitKey(1) & 0xFF
if key == ord("q"):
print("\nExiting.")
break
if key == ord("r"):
tactile.reset()
print("\nTracker reset.")
finally:
runtime.close()
cv2.destroyAllWindows()
from products.ory_finger.finger_pipeline import main
if __name__ == "__main__":
+10 -65
View File
@@ -1,75 +1,20 @@
#!/usr/bin/env python3
"""
UV-unroll finger tactile pipeline with unified Qt dashboard.
"""Compatibility wrapper for ORY-FINGER 3D Qt dashboard.
Where to customize the GUI:
Layout (sizes, proportions) -> examples/gui/layout.py <-- edit here
Colors / QSS -> examples/gui/styles.py
Widget rendering logic -> examples/gui/widgets/
Window behavior -> examples/gui/dashboard.py
Prefer:
python examples/products/ory_finger/finger_pipeline_3dview.py --video 0 --config dd02
"""
import argparse
import os
from __future__ import annotations
import sys
from pathlib import Path
os.environ.setdefault("OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS", "0")
_EXAMPLES_DIR = Path(__file__).resolve().parent
if str(_EXAMPLES_DIR) not in sys.path:
sys.path.insert(0, str(_EXAMPLES_DIR))
from gui.app_paths import configure_frozen_runtime, default_config_path, ensure_examples_import_path
configure_frozen_runtime()
ensure_examples_import_path()
from gui import run_dashboard
from pipeline import PipelineConfig
from viz import DEFAULT_FINGER_VIZ_POSE
def _parse_args():
parser = argparse.ArgumentParser(
description="UV-unroll finger tactile pipeline with unified Qt dashboard"
)
parser.add_argument("--video", "-v", default="0", help="Camera index or video file path")
parser.add_argument("--config", "-c", default=default_config_path(), help="Orisys SDK config")
parser.add_argument("--cal", default="", help="Optional calibration file path")
parser.add_argument("--verbose", action="store_true", help="SDK verbose logs")
parser.add_argument(
"--mesh-map",
choices=["flow", "depth"],
default="depth",
help="Color the 3D mesh from flow magnitude or contact depth.",
)
parser.add_argument("--threshold", type=float, default=None, help="Mesh/arrow threshold")
parser.add_argument("--no-3d", action="store_true", help="Disable 3D mesh view")
parser.add_argument(
"--viz-pose",
default=DEFAULT_FINGER_VIZ_POSE,
help="world_to_cam.npy for the default view; pass 'none' to skip",
)
parser.add_argument(
"--cutoff",
type=float,
default=5.0,
help="Low-pass cutoff for force curves (Hz)",
)
parser.add_argument(
"--profile",
action="store_true",
help="Print per-stage timing to the log panel / console",
)
parser.add_argument(
"--profile-every",
type=int,
default=30,
help="When --profile is set, print rolling averages every N frames",
)
return parser.parse_args()
def main():
args = _parse_args()
config = PipelineConfig.from_args(args)
sys.exit(run_dashboard(config))
from products.ory_finger.finger_pipeline_3dview import main
if __name__ == "__main__":
+2 -1
View File
@@ -27,7 +27,8 @@ def resource_path(*parts: str) -> Path:
def default_config_path() -> str:
return str(resource_path("config", "dd02-ov.json"))
"""Default ORY-FINGER config name (resolves to dd02-ov via alias)."""
return "dd02"
def configure_frozen_runtime() -> None:
@@ -137,7 +137,7 @@ class SettingsPanelWidget(QWidget):
root.addWidget(self._caption("Config"))
config_row = QHBoxLayout()
config_row.setSpacing(6)
self.config_edit = QLineEdit("./config/dd02-ov.json")
self.config_edit = QLineEdit("dd02")
config_row.addWidget(self.config_edit, 1)
self.btn_browse_config = QPushButton("…")
self.btn_browse_config.setFixedWidth(32)
+1 -1
View File
@@ -17,7 +17,7 @@ def mesh_threshold(mesh_map: str, threshold: float | None) -> float:
@dataclass
class PipelineConfig:
video: str = "0"
config: str = "./config/dd02-ov.json"
config: str = "dd02"
calib: str | None = None
export_dir: str = DEFAULT_FINGER_EXPORT_DIR
csv_save_dir: str = "./outputs/csv"
+1
View File
@@ -0,0 +1 @@
"""Product-facing example entry points for ORY-BOX / ORY-MINI / ORY-FINGER."""
+115
View File
@@ -0,0 +1,115 @@
"""
ORY-BOX:单传感器基础流程
本示例演示 Orisys SDK 的标准处理流程:
1. 初始化传感器(摄像头或视频文件)
2. 获取图像并计算形变
3. 按需计算接触区域
4. 读取并可视化结果
SDK version: 0.4.2
"""
import os
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
import cv2
import orisys
import numpy as np
import argparse
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--video","-v", type=str, default="0", help="视频源:摄像头编号或视频文件路径")
parser.add_argument("--config","-c", type=str, default="ddjx01", help="配置文件名称或路径")
parser.add_argument("--verbose","-verbose", type=bool, default=True, help="是否输出详细日志")
args = parser.parse_args()
# 步骤 1:创建传感器对象(必需)
# config_name 可以是内置配置名称,也可以是自定义配置文件路径
# cal_path 为标定文件路径;每台设备建议使用独立的标定文件
# 输入既可以是摄像头编号,也可以是视频文件路径
try:
input = int(args.video)
is_camera = True
sensor = orisys.Sensor(input, config_name=args.config, verbose=args.verbose)
except:
is_camera = False
sensor = orisys.Sensor(args.video, config_name=args.config, verbose=args.verbose)
print("\n按键说明:'q' 退出程序,'r' 重置追踪器。")
while True:
# 步骤 2:数据处理
# 步骤 2.1:获取并拼接图像(必需)
sensor.get_img()
# 步骤 2.2:计算形变场(必需);返回值表示当前帧是否检测到接触/运动
is_contact_now = sensor.compute_deformation(check_motion=True, threshold=0)
# 步骤 2.3:仅在当前帧检测到接触/运动时计算接触区域
# if is_contact_now:
sensor.compute_contact()
# 步骤 2.4:读取结果
fps, flow, vnormal, img, contour, centroid, depth_map, fnormal, fshearx, fsheary = sensor.read_info(
sensor.info.FPS,
sensor.info.VRAW,
sensor.info.VNORMAL,
sensor.info.IMG,
sensor.info.CONTOUR,
sensor.info.CENTROID,
sensor.info.DEPTH,
sensor.info.FNORMAL,
sensor.info.FSHEARX,
sensor.info.FSHEARY
)
# 步骤 3:可视化显示
# 步骤 3.1:显示形变矢量场
arrows = orisys.util.draw_arrows(
img,
flow,
threshold=2,
grid_spacing=20,
arrow_scale=1.0
)
cv2.imshow("形变矢量场", arrows)
# 步骤 3.2:显示接触区域和质心
image_with_foe = orisys.util.draw_contact(img, contour, centroid)
cv2.imshow("接触区域与质心", image_with_foe)
# 步骤 3.3:显示深度图
# 将深度值归一化到 0-255 范围
div_abs = depth_map
if div_abs.max() > div_abs.min():
div_normalized = ((div_abs - div_abs.min()) / (div_abs.max() - div_abs.min()) * 255).astype(np.uint8)
else:
div_normalized = np.zeros_like(div_abs, dtype=np.uint8)
# 应用伪彩色映射
cv2.imshow("深度图", cv2.applyColorMap(div_normalized, cv2.COLORMAP_JET))
# 步骤 3.4:打印关键结果
print(
f"FPS={fps:.2f}, 法向力={fnormal:.4f}, 切向力X={fshearx:.4f}, "
f"切向力Y={fsheary:.4f}, 深度图尺寸={depth_map.shape}, 原始分辨率={sensor.img_size_raw}"
)
# 步骤 3.5:键盘控制
key = cv2.waitKey(1) & 0xFF
if key == ord("q"):
print("\n正在退出示例程序...")
break
if key == ord("r"):
# 当光流追踪出现漂移时,可手动重置追踪器
sensor.reset()
print("追踪器已重置。")
# =========================================================================
# 4. 释放资源
# =========================================================================
sensor.disconnect() # 释放视频源及相关资源
if __name__ == '__main__':
main()
@@ -0,0 +1,111 @@
#!/usr/bin/env python3
"""
ORY-FINGER:Minimum UV-unroll finger tactile pipeline.
Offline: load Blender export -> build_or_load_domain -> unroll_map_uv.npz
Runtime: camera frame -> FrameUnwarper -> TactileProcessor -> forces/contact
Recommended entry:
python examples/products/ory_finger/finger_pipeline.py --camera_id 0 --config dd02
Current config alias: ``dd02`` resolves to ``dd02-ov`` for compatibility.
"""
import argparse
import os
import sys
os.environ.setdefault("OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS", "0")
import cv2
import orisys
from orisys.finger import FingerRuntime
def main():
parser = argparse.ArgumentParser(description="Minimum UV-unroll finger tactile pipeline")
parser.add_argument("--camera_id", type=int, default=0)
parser.add_argument("--video", help="Video file path (overrides camera)")
parser.add_argument("--config", "-c", default="dd02", help="Orisys SDK config name or path")
parser.add_argument("--show", action="store_true", default=True, help="Show unrolled image window")
parser.add_argument("--verbose", action="store_true", help="SDK verbose logs")
args = parser.parse_args()
try:
video = args.camera_id
if args.video:
video = args.video
runtime = FingerRuntime.open(
config=args.config,
video=video,
verbose=args.verbose,
)
except RuntimeError as exc:
print(str(exc), file=sys.stderr)
sys.exit(1)
if args.verbose:
geometry = runtime.geometry
print(
f"UV geometry: native={geometry.native_width}x{geometry.native_height}, "
f"source={geometry.source_width}x{geometry.source_height} "
f"output={geometry.out_width}x{geometry.out_height} "
)
runtime.warmup(color=False)
tactile = runtime.tactile
print("Press 'q' to quit, 'r' to reset tracker.")
try:
while True:
ok, unrolled = runtime.grab_and_process(color=False)
if not ok:
break
tactile.compute_deformation(check_motion=True, threshold=0)
tactile.compute_contact()
fps, flow, vnormal, img, contour, centroid, depth_map, fnormal, fshearx, fsheary = tactile.read_info(
tactile.info.FPS,
tactile.info.VRAW,
tactile.info.VNORMAL,
tactile.info.IMG,
tactile.info.CONTOUR,
tactile.info.CENTROID,
tactile.info.DEPTH,
tactile.info.FNORMAL,
tactile.info.FSHEARX,
tactile.info.FSHEARY,
)
status = (
f"FPS={fps:.2f} normal={fnormal:.2f} shear=({fshearx:.2f}, {fsheary:.2f}) "
f"mask={tactile.mask_stats['coverage']:.2f}"
)
if args.show:
arrows = orisys.util.draw_arrows(
unrolled,
flow,
threshold=0.4,
grid_spacing=10,
arrow_scale=1.0,
)
cv2.imshow("UV Unrolled", arrows)
print(status, end="\r")
key = cv2.waitKey(1) & 0xFF
if key == ord("q"):
print("\nExiting.")
break
if key == ord("r"):
tactile.reset()
print("\nTracker reset.")
finally:
runtime.close()
cv2.destroyAllWindows()
if __name__ == "__main__":
main()
@@ -0,0 +1,85 @@
#!/usr/bin/env python3
"""
ORY-FINGER:UV-unroll finger tactile pipeline with unified Qt dashboard.
Where to customize the GUI:
Layout (sizes, proportions) -> examples/gui/viewer3d/layout.py
Colors / QSS -> examples/gui/viewer3d/styles.py
Widget rendering logic -> examples/gui/viewer3d/widgets/
Window behavior -> examples/gui/viewer3d/dashboard.py
Recommended entry:
python examples/products/ory_finger/finger_pipeline_3dview.py --video 0 --config dd02
"""
import argparse
import os
import sys
from pathlib import Path
os.environ.setdefault("OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS", "0")
# Make shared packages importable when launched as a file path.
_EXAMPLES_DIR = Path(__file__).resolve().parents[2]
if str(_EXAMPLES_DIR) not in sys.path:
sys.path.insert(0, str(_EXAMPLES_DIR))
from gui.app_paths import configure_frozen_runtime, default_config_path, ensure_examples_import_path
configure_frozen_runtime()
ensure_examples_import_path()
from gui import run_dashboard
from pipeline import PipelineConfig
from viz import DEFAULT_FINGER_VIZ_POSE
def _parse_args():
parser = argparse.ArgumentParser(
description="UV-unroll finger tactile pipeline with unified Qt dashboard"
)
parser.add_argument("--video", "-v", default="0", help="Camera index or video file path")
parser.add_argument("--config", "-c", default=default_config_path(), help="Orisys SDK config")
parser.add_argument("--cal", default="", help="Optional calibration file path")
parser.add_argument("--verbose", action="store_true", help="SDK verbose logs")
parser.add_argument(
"--mesh-map",
choices=["flow", "depth"],
default="depth",
help="Color the 3D mesh from flow magnitude or contact depth.",
)
parser.add_argument("--threshold", type=float, default=None, help="Mesh/arrow threshold")
parser.add_argument("--no-3d", action="store_true", help="Disable 3D mesh view")
parser.add_argument(
"--viz-pose",
default=DEFAULT_FINGER_VIZ_POSE,
help="world_to_cam.npy for the default view; pass 'none' to skip",
)
parser.add_argument(
"--cutoff",
type=float,
default=5.0,
help="Low-pass cutoff for force curves (Hz)",
)
parser.add_argument(
"--profile",
action="store_true",
help="Print per-stage timing to the log panel / console",
)
parser.add_argument(
"--profile-every",
type=int,
default=30,
help="When --profile is set, print rolling averages every N frames",
)
return parser.parse_args()
def main():
args = _parse_args()
config = PipelineConfig.from_args(args)
sys.exit(run_dashboard(config))
if __name__ == "__main__":
main()
@@ -0,0 +1,115 @@
"""
ORY-MINI:单传感器基础流程
本示例演示 Orisys SDK 的标准处理流程:
1. 初始化传感器(摄像头或视频文件)
2. 获取图像并计算形变
3. 按需计算接触区域
4. 读取并可视化结果
SDK version: 0.4.2
"""
import os
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
import cv2
import orisys
import numpy as np
import argparse
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--video","-v", type=str, default="0", help="视频源:摄像头编号或视频文件路径")
parser.add_argument("--config","-c", type=str, default="mini", help="配置文件名称或路径")
parser.add_argument("--verbose","-verbose", type=bool, default=True, help="是否输出详细日志")
args = parser.parse_args()
# 步骤 1:创建传感器对象(必需)
# config_name 可以是内置配置名称,也可以是自定义配置文件路径
# cal_path 为标定文件路径;每台设备建议使用独立的标定文件
# 输入既可以是摄像头编号,也可以是视频文件路径
try:
input = int(args.video)
is_camera = True
sensor = orisys.Sensor(input, config_name=args.config, verbose=args.verbose)
except:
is_camera = False
sensor = orisys.Sensor(args.video, config_name=args.config, verbose=args.verbose)
print("\n按键说明:'q' 退出程序,'r' 重置追踪器。")
while True:
# 步骤 2:数据处理
# 步骤 2.1:获取并拼接图像(必需)
sensor.get_img()
# 步骤 2.2:计算形变场(必需);返回值表示当前帧是否检测到接触/运动
is_contact_now = sensor.compute_deformation(check_motion=True, threshold=0)
# 步骤 2.3:仅在当前帧检测到接触/运动时计算接触区域
# if is_contact_now:
sensor.compute_contact()
# 步骤 2.4:读取结果
fps, flow, vnormal, img, contour, centroid, depth_map, fnormal, fshearx, fsheary = sensor.read_info(
sensor.info.FPS,
sensor.info.VRAW,
sensor.info.VNORMAL,
sensor.info.IMG,
sensor.info.CONTOUR,
sensor.info.CENTROID,
sensor.info.DEPTH,
sensor.info.FNORMAL,
sensor.info.FSHEARX,
sensor.info.FSHEARY
)
# 步骤 3:可视化显示
# 步骤 3.1:显示形变矢量场
arrows = orisys.util.draw_arrows(
img,
flow,
threshold=2,
grid_spacing=20,
arrow_scale=1.0
)
cv2.imshow("形变矢量场", arrows)
# 步骤 3.2:显示接触区域和质心
image_with_foe = orisys.util.draw_contact(img, contour, centroid)
cv2.imshow("接触区域与质心", image_with_foe)
# 步骤 3.3:显示深度图
# 将深度值归一化到 0-255 范围
div_abs = depth_map
if div_abs.max() > div_abs.min():
div_normalized = ((div_abs - div_abs.min()) / (div_abs.max() - div_abs.min()) * 255).astype(np.uint8)
else:
div_normalized = np.zeros_like(div_abs, dtype=np.uint8)
# 应用伪彩色映射
cv2.imshow("深度图", cv2.applyColorMap(div_normalized, cv2.COLORMAP_JET))
# 步骤 3.4:打印关键结果
print(
f"FPS={fps:.2f}, 法向力={fnormal:.4f}, 切向力X={fshearx:.4f}, "
f"切向力Y={fsheary:.4f}, 深度图尺寸={depth_map.shape}, 原始分辨率={sensor.img_size_raw}"
)
# 步骤 3.5:键盘控制
key = cv2.waitKey(1) & 0xFF
if key == ord("q"):
print("\n正在退出示例程序...")
break
if key == ord("r"):
# 当光流追踪出现漂移时,可手动重置追踪器
sensor.reset()
print("追踪器已重置。")
# =========================================================================
# 4. 释放资源
# =========================================================================
sensor.disconnect() # 释放视频源及相关资源
if __name__ == '__main__':
main()
+25 -6
View File
@@ -4,25 +4,44 @@ build-backend = "setuptools.build_meta"
[project]
name = "orisys"
description = "Orisys - Vision Based Tactile Sensor SDK"
description = "SDK for Orisys Technology Visuotactile Sensors"
readme = "README.md"
requires-python = ">=3.10"
authors = [{ name = "Orisys" }]
authors = [{ name = "Orisys-Tech" }]
maintainers = [
{name = "Yijian Zhang", email = "zhangyijian@orisys-tech.cn"}
]
dynamic = ["version"]
dependencies = [
"numpy>=1.24.4",
"opencv-python>=4.10.0.84",
"scipy>=1.10.1",
"open3d>=0.17.0",
"numba>=0.59.0",
]
classifiers = [
# How mature is this project? Common values are
# 3 - Alpha
# 4 - Beta
# 5 - Production/Stable
"Development Status :: 4 - Beta",
# Indicate who your project is intended for
"Intended Audience :: Developers",
"Topic :: Software Development :: SDL",
# Specify the Python versions you support here.
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Private :: Do Not Upload",
]
[project.optional-dependencies]
cuda12 = ["cupy-cuda12x>=12.3.0"]
cuda13 = ["cupy-cuda13x>=13.0.0"]
gui = ["open3d>=0.17.0","pyqt5>=5.15", "pyqtgraph>=0.13"]
# Backward compatibility: 'cuda' defaults to CUDA 12.x
cuda = ["cupy-cuda12x>=12.3.0"]
gui = ["pyqt5>=5.15", "pyqtgraph>=0.13"]
numba = ["numba>=0.59.0"]
[tool.setuptools]
package-dir = { "" = "src" }
@@ -52,7 +71,7 @@ dev_install = true
name = "examples"
source = "examples"
target = "examples"
patterns = ["*.py", "README.md", "viz/**/*.py", "pipeline/**/*.py", "gui/**/*.py"]
patterns = ["*.py", "README.md", "products/**/*.py", "viz/**/*.py", "pipeline/**/*.py", "gui/**/*.py"]
preserve_tree = true
[[tool.orisys.bundle.items]]
+1 -1
View File
@@ -22,7 +22,7 @@ def main():
parser = argparse.ArgumentParser(description="Orisys finger Qt dashboard")
parser.add_argument("--video", "-v", type=str, default="0")
parser.add_argument("--config", "-c", type=str, default="./config/dd02-ov.json")
parser.add_argument("--config", "-c", type=str, default="dd02")
parser.add_argument("--cal", type=str, default="")
parser.add_argument("--verbose", action="store_true")
parser.add_argument("--cutoff", type=float, default=10.0)
+18 -5
View File
@@ -1,6 +1,12 @@
import json
from pathlib import Path
# Product-facing names that map to on-disk config stems.
# ORY-FINGER currently ships as dd02-ov.json; keep "dd02" as the stable alias.
CONFIG_ALIASES = {
"dd02": "dd02-ov",
}
def _candidate_config_dirs() -> list[Path]:
"""
@@ -62,9 +68,16 @@ def load_config(config_name_or_path):
if config_path.is_absolute():
raise FileNotFoundError(f"Config file not found at absolute path: {config_path}")
direct = CONFIG_DIR / f"{config_name_or_path}.json"
if direct.exists():
return _load_json(direct)
requested = str(config_name_or_path)
candidates = [requested]
alias = CONFIG_ALIASES.get(requested)
if alias and alias not in candidates:
candidates.append(alias)
for name in candidates:
direct = CONFIG_DIR / f"{name}.json"
if direct.exists():
return _load_json(direct)
# Backward-compatible alias resolution: support old names via JSON "name" field.
for candidate in _config_files():
@@ -72,7 +85,7 @@ def load_config(config_name_or_path):
data = _load_json(candidate)
except Exception:
continue
if data.get("name") == config_name_or_path:
if data.get("name") in candidates:
return data
available = [f.stem for f in _config_files()]
@@ -80,7 +93,7 @@ def load_config(config_name_or_path):
raise FileNotFoundError(
f"Input config file not found with name or path: {config_name_or_path}\n"
f" Tried as file path: {config_path} (not found)\n"
f" Tried as config file: {direct} (not found)\n"
f" Tried as config files: {', '.join(candidates)} (not found)\n"
f" Available config files: {available_str}"
)