update sensor bench
This commit is contained in:
@@ -6,6 +6,7 @@ __pycache__/
|
|||||||
venv/
|
venv/
|
||||||
/dist/
|
/dist/
|
||||||
/build/pyinstaller_work/
|
/build/pyinstaller_work/
|
||||||
|
/build/pyinstaller_work_operator/
|
||||||
/recordings/*
|
/recordings/*
|
||||||
!/recordings/.gitkeep
|
!/recordings/.gitkeep
|
||||||
/tools/ffmpeg/bin/
|
/tools/ffmpeg/bin/
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
# Sensor Bench
|
# Sensor Bench
|
||||||
|
|
||||||
Sensor Bench 是一款用于触觉传感器测试的 Windows 桌面工具。它基于 Orisys SDK,可预览原始相机画面、显示处理后的传感器图像、录制原始视频,并在各测试步骤中监控 FN / FX / FY 力曲线。
|
Sensor Bench 是一款用于触觉传感器**展示**的 Windows 桌面工具。它基于 Orisys SDK,可预览处理后的传感器图像、录制原始视频,并实时显示 FN / FX / FY 力曲线。
|
||||||
|
|
||||||
|
默认启动为展示端界面(参考 `qt_viewer_2` 布局)。
|
||||||
|
|
||||||
|
> 内部检测 / 流程引导界面不在本公开包中。实验室内部工具见仓库内私有目录 [`internal/`](internal/README.md)(请勿随客户包发布)。
|
||||||
|
|
||||||
## 界面示意
|
## 界面示意
|
||||||
|
|
||||||
@@ -10,12 +14,22 @@ Sensor Bench 是一款用于触觉传感器测试的 Windows 桌面工具。它
|
|||||||
|
|
||||||
## 功能
|
## 功能
|
||||||
|
|
||||||
- 通过左侧流程引导操作员完成检测,支持 **下一步**、**上一步**、**新样品 / 重置**。
|
展示端:
|
||||||
- 从 `config/options.json` 加载可选项:传感器类型、弹性体、CMOS、标记、压头、平台。
|
|
||||||
- 默认使用 DDJX01 Orisys 采集:原始画面 `1920x1080 @ 30 FPS`,处理后输出约 `365x365`。
|
- 菜单栏 **帮助 → 关于** 显示 Sensor Bench / Orisys 版本
|
||||||
- 显示原始 `sensor.frame`、处理后的 `Sensor.get_img()`,以及实时力曲线。
|
- 顶部工具栏:传感器类型(Orisys 拼接 / DD06 ROI)、搜索摄像机、摄像机(视频文件)、配置、启动 / 暂停、重置(传感器追踪器)、录像
|
||||||
- 将 SDK 原始相机帧录制为 MP4,保存到 `recordings/` 或自定义文件夹。
|
- 默认画面为光流箭头绘制在现有背景色(`#121316`)上;也可切换到采集图像箭头或原始画面
|
||||||
- 界面文案存放在 `config/locales/zh_CN.json` 与 `config/locales/en_US.json`。
|
- 右侧三路力曲线:FN / FX / FY(与 qt_viewer_2 相同的滤波与 Y 轴最小量程)
|
||||||
|
- 底部日志区,支持 ESC 全屏、S/E 录像、R 重置追踪器
|
||||||
|
|
||||||
|
通用能力:
|
||||||
|
|
||||||
|
- Orisys / DDJX01:原始画面 `1920x1080 @ 30 FPS`,处理后输出约 `365x365`
|
||||||
|
- DD06:外置采集 + 多边形 ROI 矫正后送入 `Sensor.get_img(..., already_processed=True)`
|
||||||
|
- 配置下拉框优先使用程序旁 `config/` 中的传感器 JSON(构建前从 Orisys wheel 同步到仓库 `config/`)
|
||||||
|
- 展示端可用“搜索”探测本机摄像机,也可用“摄像机”选择视频文件;也可用“配置…”浏览自定义 JSON
|
||||||
|
- 录制原始相机帧到 `recordings/`
|
||||||
|
- 界面文案存放在 `config/locales/zh_CN.json` 与 `config/locales/en_US.json`
|
||||||
|
|
||||||
## 运行环境
|
## 运行环境
|
||||||
|
|
||||||
@@ -25,17 +39,21 @@ Sensor Bench 是一款用于触觉传感器测试的 Windows 桌面工具。它
|
|||||||
- Python 3.10
|
- Python 3.10
|
||||||
- 从 PyPI 安装 `orisys`,或在开发时使用本地 `orisys-sdk` 源码
|
- 从 PyPI 安装 `orisys`,或在开发时使用本地 `orisys-sdk` 源码
|
||||||
|
|
||||||
源码运行时安装依赖:
|
源码运行:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
cd D:\repo\sensor-bench
|
cd D:\repo\sensor-bench
|
||||||
python -m pip install numpy opencv-python PyQt5 pyqtgraph
|
python -m pip install numpy opencv-python PyQt5 pyqtgraph
|
||||||
python -m pip install orisys
|
python -m pip install orisys
|
||||||
$env:PYTHONPATH = "D:\repo\orisys-sdk\src;$env:PYTHONPATH"
|
|
||||||
python run.py
|
python run.py
|
||||||
```
|
```
|
||||||
|
|
||||||
使用 PyPI 包时可不设置 `PYTHONPATH`。若存在同级目录 `D:\repo\orisys-sdk\src`,`run.py` 也会自动检测并加入路径。
|
使用本地 SDK 源码开发时:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$env:PYTHONPATH = "D:\repo\orisys-sdk\src;$env:PYTHONPATH"
|
||||||
|
python run.py
|
||||||
|
```
|
||||||
|
|
||||||
运行冒烟测试:
|
运行冒烟测试:
|
||||||
|
|
||||||
@@ -43,9 +61,15 @@ python run.py
|
|||||||
python tests/test_smoke.py
|
python tests/test_smoke.py
|
||||||
```
|
```
|
||||||
|
|
||||||
|
内部操作员界面(私有,勿发布):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python internal/run_operator.py
|
||||||
|
```
|
||||||
|
|
||||||
## 打包 EXE
|
## 打包 EXE
|
||||||
|
|
||||||
发布包使用 PyInstaller `onedir` 模式,并依赖 Python 3.10 的 conda 环境。
|
发布包使用 PyInstaller `onedir` 模式,并依赖 Python 3.10 的 conda 环境。构建时从 PyPI 安装 `orisys`。
|
||||||
|
|
||||||
1. 创建构建环境:
|
1. 创建构建环境:
|
||||||
|
|
||||||
@@ -53,16 +77,30 @@ python tests/test_smoke.py
|
|||||||
.\build\create_env.bat
|
.\build\create_env.bat
|
||||||
```
|
```
|
||||||
|
|
||||||
2. 构建可执行文件:
|
2. 构建**公开**展示端:
|
||||||
|
|
||||||
```bat
|
```bat
|
||||||
.\build\build_exe.bat
|
.\build\build_exe.bat
|
||||||
```
|
```
|
||||||
|
|
||||||
输出路径:
|
输出:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
dist/SensorBench/SensorBench.exe
|
dist/SensorBench/SensorBench.exe
|
||||||
```
|
```
|
||||||
|
|
||||||
打包结果包含 `config/`、`config/locales/` 和 `steps/`,操作员无需改代码即可编辑选项、翻译文案和工作流程。
|
公开构建**仅**包含展示端,不包含 `internal/`、`steps/` 或操作员流程文案。
|
||||||
|
|
||||||
|
3. 构建**内部**操作员端(勿发给客户):
|
||||||
|
|
||||||
|
```bat
|
||||||
|
.\build\build_operator_exe.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
输出:
|
||||||
|
|
||||||
|
```text
|
||||||
|
dist/SensorBenchOperator/SensorBenchOperator.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
内部包会附带 `steps/`、`config/options.json` 与完整操作员文案,可直接编辑。
|
||||||
|
|||||||
+81
-11
@@ -1,6 +1,9 @@
|
|||||||
# Sensor Bench EXE Build
|
# Sensor Bench EXE Build
|
||||||
|
|
||||||
This folder contains the Windows PyInstaller build setup for Sensor Bench.
|
This folder contains Windows PyInstaller build setups for:
|
||||||
|
|
||||||
|
- **Public** exhibition app → `dist/SensorBench/`
|
||||||
|
- **Private** operator app → `dist/SensorBenchOperator/` (internal use only)
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
@@ -9,7 +12,8 @@ This folder contains the Windows PyInstaller build setup for Sensor Bench.
|
|||||||
- Python 3.10 build environment
|
- Python 3.10 build environment
|
||||||
- Internet access to install `orisys` from PyPI
|
- Internet access to install `orisys` from PyPI
|
||||||
|
|
||||||
The build installs `orisys` from PyPI. Do not use `PYTHONPATH` or an editable `orisys-sdk` checkout for release builds.
|
The build installs `orisys` from PyPI. Do not use `PYTHONPATH` or an editable
|
||||||
|
`orisys-sdk` checkout for release builds.
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
@@ -20,7 +24,8 @@ From the repository root (cmd or PowerShell):
|
|||||||
.\build\build_exe.bat
|
.\build\build_exe.bat
|
||||||
```
|
```
|
||||||
|
|
||||||
The `.bat` wrappers bypass the local PowerShell execution policy. If you prefer calling PowerShell directly:
|
The `.bat` wrappers bypass the local PowerShell execution policy. If you prefer
|
||||||
|
calling PowerShell directly:
|
||||||
|
|
||||||
```bat
|
```bat
|
||||||
powershell -NoProfile -ExecutionPolicy Bypass -File .\build\create_env.ps1
|
powershell -NoProfile -ExecutionPolicy Bypass -File .\build\create_env.ps1
|
||||||
@@ -33,7 +38,23 @@ Output:
|
|||||||
dist/SensorBench/SensorBench.exe
|
dist/SensorBench/SensorBench.exe
|
||||||
```
|
```
|
||||||
|
|
||||||
The build is `onedir` rather than `onefile` so startup is faster and bundled DLL/resource issues are easier to diagnose.
|
After installing/upgrading `orisys`, the build syncs wheel sensor `*.json` into
|
||||||
|
repo `config/` via `build/sync_wheel_configs.py` (for example `ddjx01.json`)
|
||||||
|
before PyInstaller, never touching `locales/`. The helper reads from the
|
||||||
|
installed wheel's bundled `orisys/configs/` directory instead of
|
||||||
|
`orisys.configs.CONFIG_DIR`, which can point at the repo's local `config/`
|
||||||
|
during builds. PyInstaller bundles that `config/`, then the script copies repo
|
||||||
|
`config/` beside the EXE so operators can edit sensor JSON and locales.
|
||||||
|
|
||||||
|
The public build intentionally excludes:
|
||||||
|
|
||||||
|
- `internal/` (private operator UI)
|
||||||
|
- `steps/` workflow JSON
|
||||||
|
- `config/options.json` lab component metadata
|
||||||
|
- `sensor_bench_operator` / former `sensor_bench.app` modules
|
||||||
|
|
||||||
|
The build is `onedir` rather than `onefile` so startup is faster and bundled
|
||||||
|
DLL/resource issues are easier to diagnose.
|
||||||
|
|
||||||
## Validation
|
## Validation
|
||||||
|
|
||||||
@@ -44,15 +65,64 @@ python tests/test_smoke.py
|
|||||||
python build/verify_dist.py
|
python build/verify_dist.py
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`verify_dist.py` checks the expected public layout **and** fails if internal
|
||||||
|
paths, operator modules, workflow locale keys, or private workflow text appear
|
||||||
|
in the dist tree.
|
||||||
|
|
||||||
Manual hardware check:
|
Manual hardware check:
|
||||||
|
|
||||||
1. Run `dist\SensorBench\SensorBench.exe`.
|
1. Run `dist\SensorBench\SensorBench.exe`.
|
||||||
2. Confirm the UI opens without setting `PYTHONPATH`.
|
2. Confirm the exhibition UI opens without setting `PYTHONPATH`.
|
||||||
3. Confirm the UI defaults to Simplified Chinese and can switch to English.
|
3. Confirm the UI defaults to Simplified Chinese and can switch to English.
|
||||||
4. Confirm Orisys status is available and fixed mode shows DDJX01.
|
4. Confirm Orisys status is available, the config dropdown lists local editable
|
||||||
5. Use Next / Revert / New Sample Reset on the left workflow panel.
|
sensor configs (for example `ddjx01`), and Start uses DDJX01.
|
||||||
6. Start preview with camera index `0`.
|
5. Confirm Search finds cameras (or logs none) and Camera still opens a
|
||||||
7. Confirm raw preview and processed preview update.
|
video-file picker. There is no Cal (.npy) field; Reset resets the Orisys
|
||||||
8. Record a short clip and verify it appears under `dist\SensorBench\recordings`.
|
tracker while preview is running.
|
||||||
|
6. Start preview with camera index `0` (or a searched index / video file).
|
||||||
|
7. Confirm the image panel and FN/FX/FY plots update.
|
||||||
|
8. Toggle processed/raw view and start/stop recording under
|
||||||
|
`dist\SensorBench\recordings`.
|
||||||
|
|
||||||
Locale files are bundled under `_internal/config/locales/` (and can also be placed beside the EXE under `config/locales/` for overrides). Edit those JSON files to change operator-facing text without rebuilding.
|
Locale files are bundled under `_internal/config/locales/` (and can also be
|
||||||
|
placed beside the EXE under `config/locales/` for overrides). Edit those JSON
|
||||||
|
files to change exhibition-facing text without rebuilding.
|
||||||
|
|
||||||
|
## Private operator EXE (internal only)
|
||||||
|
|
||||||
|
Do **not** ship `SensorBenchOperator` to customers. It includes workflow steps,
|
||||||
|
lab `options.json`, and operator locale copy.
|
||||||
|
|
||||||
|
```bat
|
||||||
|
.\build\create_env.bat
|
||||||
|
.\build\build_operator_exe.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
Or:
|
||||||
|
|
||||||
|
```bat
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -File .\build\build_operator_exe.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
|
||||||
|
```text
|
||||||
|
dist/SensorBenchOperator/SensorBenchOperator.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python tests/test_smoke.py
|
||||||
|
python internal/tests/test_operator_smoke.py
|
||||||
|
python build/verify_operator_dist.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Manual check:
|
||||||
|
|
||||||
|
1. Run `dist\SensorBenchOperator\SensorBenchOperator.exe`.
|
||||||
|
2. Confirm the workflow/steps panel and component options appear.
|
||||||
|
3. Confirm Start Preview / record / force curves still work with a camera.
|
||||||
|
4. Confirm `config/options.json` and `steps/default.json` beside the EXE are editable.
|
||||||
|
|
||||||
|
Source-level operator docs: [`../internal/README.md`](../internal/README.md).
|
||||||
|
|||||||
+20
-3
@@ -8,6 +8,7 @@ $RepoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
|
|||||||
$DistDir = Join-Path $RepoRoot "dist"
|
$DistDir = Join-Path $RepoRoot "dist"
|
||||||
$WorkDir = Join-Path $RepoRoot "build\pyinstaller_work"
|
$WorkDir = Join-Path $RepoRoot "build\pyinstaller_work"
|
||||||
$SpecPath = Join-Path $PSScriptRoot "sensor_bench.spec"
|
$SpecPath = Join-Path $PSScriptRoot "sensor_bench.spec"
|
||||||
|
$RepoConfig = Join-Path $RepoRoot "config"
|
||||||
|
|
||||||
if (-not (Get-Command conda -ErrorAction SilentlyContinue)) {
|
if (-not (Get-Command conda -ErrorAction SilentlyContinue)) {
|
||||||
throw "conda was not found on PATH. Open an Anaconda Prompt or initialize conda for PowerShell."
|
throw "conda was not found on PATH. Open an Anaconda Prompt or initialize conda for PowerShell."
|
||||||
@@ -16,10 +17,20 @@ if (-not (Get-Command conda -ErrorAction SilentlyContinue)) {
|
|||||||
Push-Location $RepoRoot
|
Push-Location $RepoRoot
|
||||||
try {
|
try {
|
||||||
conda run -n $EnvName python -m pip install --upgrade pip
|
conda run -n $EnvName python -m pip install --upgrade pip
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "pip upgrade failed in env '$EnvName'." }
|
||||||
|
|
||||||
conda run -n $EnvName python -m pip install -e ".[pack]"
|
conda run -n $EnvName python -m pip install -e ".[pack]"
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "Editable install failed in env '$EnvName'." }
|
||||||
|
|
||||||
conda run -n $EnvName python -m pip install --upgrade orisys
|
conda run -n $EnvName python -m pip install --upgrade orisys
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "orisys install failed in env '$EnvName'." }
|
||||||
|
|
||||||
|
# Sync Orisys wheel sensor configs into repo config/ before packaging.
|
||||||
|
conda run -n $EnvName python build/sync_wheel_configs.py
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "Config sync failed." }
|
||||||
|
|
||||||
conda run -n $EnvName python tests/test_smoke.py
|
conda run -n $EnvName python tests/test_smoke.py
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "Smoke tests failed." }
|
||||||
|
|
||||||
if (Test-Path $WorkDir) {
|
if (Test-Path $WorkDir) {
|
||||||
Remove-Item -Recurse -Force $WorkDir
|
Remove-Item -Recurse -Force $WorkDir
|
||||||
@@ -29,14 +40,20 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
conda run -n $EnvName python -m PyInstaller --noconfirm --clean --distpath "$DistDir" --workpath "$WorkDir" "$SpecPath"
|
conda run -n $EnvName python -m PyInstaller --noconfirm --clean --distpath "$DistDir" --workpath "$WorkDir" "$SpecPath"
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "PyInstaller failed." }
|
||||||
|
|
||||||
|
# Public package only: copy exhibition config/. Never ship internal/ operator assets.
|
||||||
|
Copy-Item -Recurse -Force $RepoConfig (Join-Path $DistDir "SensorBench")
|
||||||
|
$InternalLeak = Join-Path $DistDir "SensorBench\internal"
|
||||||
|
if (Test-Path $InternalLeak) {
|
||||||
|
throw "Public dist must not contain internal/: $InternalLeak"
|
||||||
|
}
|
||||||
|
|
||||||
Copy-Item -Recurse -Force (Join-Path $RepoRoot "config") (Join-Path $DistDir "SensorBench")
|
|
||||||
Copy-Item -Recurse -Force (Join-Path $RepoRoot "steps") (Join-Path $DistDir "SensorBench")
|
|
||||||
conda run -n $EnvName python build/verify_dist.py
|
conda run -n $EnvName python build/verify_dist.py
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "Distribution verification failed." }
|
||||||
|
|
||||||
Write-Host "Build complete: dist\SensorBench\SensorBench.exe"
|
Write-Host "Build complete: dist\SensorBench\SensorBench.exe"
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
Pop-Location
|
Pop-Location
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
@echo off
|
||||||
|
cd /d "%~dp0.."
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0build_operator_exe.ps1" %*
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
param(
|
||||||
|
[string]$EnvName = "sensor-bench-build-py310"
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
$RepoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
|
||||||
|
$DistDir = Join-Path $RepoRoot "dist"
|
||||||
|
$WorkDir = Join-Path $RepoRoot "build\pyinstaller_work_operator"
|
||||||
|
$SpecPath = Join-Path $PSScriptRoot "sensor_bench_operator.spec"
|
||||||
|
$RepoConfig = Join-Path $RepoRoot "config"
|
||||||
|
$InternalRoot = Join-Path $RepoRoot "internal"
|
||||||
|
$DistApp = Join-Path $DistDir "SensorBenchOperator"
|
||||||
|
|
||||||
|
if (-not (Get-Command conda -ErrorAction SilentlyContinue)) {
|
||||||
|
throw "conda was not found on PATH. Open an Anaconda Prompt or initialize conda for PowerShell."
|
||||||
|
}
|
||||||
|
|
||||||
|
Push-Location $RepoRoot
|
||||||
|
try {
|
||||||
|
conda run -n $EnvName python -m pip install --upgrade pip
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "pip upgrade failed in env '$EnvName'." }
|
||||||
|
|
||||||
|
conda run -n $EnvName python -m pip install -e ".[pack]"
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "Editable install failed in env '$EnvName'." }
|
||||||
|
|
||||||
|
conda run -n $EnvName python -m pip install --upgrade orisys
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "orisys install failed in env '$EnvName'." }
|
||||||
|
|
||||||
|
# Sync Orisys wheel sensor configs into repo config/ before packaging.
|
||||||
|
conda run -n $EnvName python build/sync_wheel_configs.py
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "Config sync failed." }
|
||||||
|
|
||||||
|
conda run -n $EnvName python tests/test_smoke.py
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "Public smoke tests failed." }
|
||||||
|
|
||||||
|
conda run -n $EnvName python internal/tests/test_operator_smoke.py
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "Operator smoke tests failed." }
|
||||||
|
|
||||||
|
if (Test-Path $WorkDir) {
|
||||||
|
Remove-Item -Recurse -Force $WorkDir
|
||||||
|
}
|
||||||
|
if (Test-Path $DistApp) {
|
||||||
|
Remove-Item -Recurse -Force $DistApp
|
||||||
|
}
|
||||||
|
|
||||||
|
conda run -n $EnvName python -m PyInstaller --noconfirm --clean --distpath "$DistDir" --workpath "$WorkDir" "$SpecPath"
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "PyInstaller (operator) failed." }
|
||||||
|
|
||||||
|
# Editable sidecars beside the EXE: public sensor configs + operator overlays.
|
||||||
|
# Copy sensor JSON first, then replace locales/options/steps so public
|
||||||
|
# exhibition locales (no workflow) cannot win over operator catalogs.
|
||||||
|
$DistConfig = Join-Path $DistApp "config"
|
||||||
|
$DistLocales = Join-Path $DistConfig "locales"
|
||||||
|
$DistSteps = Join-Path $DistApp "steps"
|
||||||
|
if (Test-Path $DistConfig) {
|
||||||
|
Remove-Item -Recurse -Force $DistConfig
|
||||||
|
}
|
||||||
|
if (Test-Path $DistSteps) {
|
||||||
|
Remove-Item -Recurse -Force $DistSteps
|
||||||
|
}
|
||||||
|
Copy-Item -Recurse -Force $RepoConfig $DistConfig
|
||||||
|
Copy-Item -Force (Join-Path $InternalRoot "config\options.json") (Join-Path $DistConfig "options.json")
|
||||||
|
if (Test-Path $DistLocales) {
|
||||||
|
Remove-Item -Recurse -Force $DistLocales
|
||||||
|
}
|
||||||
|
Copy-Item -Recurse -Force (Join-Path $InternalRoot "config\locales") $DistLocales
|
||||||
|
Copy-Item -Recurse -Force (Join-Path $InternalRoot "steps") $DistSteps
|
||||||
|
|
||||||
|
conda run -n $EnvName python build/verify_operator_dist.py
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "Operator distribution verification failed." }
|
||||||
|
|
||||||
|
Write-Host "Build complete: dist\SensorBenchOperator\SensorBenchOperator.exe"
|
||||||
|
Write-Host "INTERNAL USE ONLY - do not ship this folder to customers."
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
Pop-Location
|
||||||
|
}
|
||||||
@@ -11,13 +11,16 @@ work_dir = repo_root / "build" / "pyinstaller_work"
|
|||||||
|
|
||||||
datas = [
|
datas = [
|
||||||
(str(repo_root / "config"), "config"),
|
(str(repo_root / "config"), "config"),
|
||||||
(str(repo_root / "steps"), "steps"),
|
|
||||||
]
|
]
|
||||||
binaries = []
|
binaries = []
|
||||||
hiddenimports = [
|
hiddenimports = [
|
||||||
"sensor_bench.main",
|
"sensor_bench.main",
|
||||||
"sensor_bench.app",
|
"sensor_bench.exhibition_app",
|
||||||
"sensor_bench.acquisition",
|
"sensor_bench.acquisition",
|
||||||
|
"sensor_bench.dd06_acquisition",
|
||||||
|
"sensor_bench.roi_geometry",
|
||||||
|
"sensor_bench.camera_discovery",
|
||||||
|
"sensor_bench.force_calibration",
|
||||||
"sensor_bench.config_loader",
|
"sensor_bench.config_loader",
|
||||||
"sensor_bench.i18n",
|
"sensor_bench.i18n",
|
||||||
"sensor_bench.paths",
|
"sensor_bench.paths",
|
||||||
@@ -50,6 +53,8 @@ excludes = [
|
|||||||
"pytest",
|
"pytest",
|
||||||
"pip",
|
"pip",
|
||||||
"wheel",
|
"wheel",
|
||||||
|
"sensor_bench_operator",
|
||||||
|
"sensor_bench.app",
|
||||||
]
|
]
|
||||||
|
|
||||||
a = Analysis(
|
a = Analysis(
|
||||||
@@ -95,4 +100,3 @@ coll = COLLECT(
|
|||||||
upx_exclude=[],
|
upx_exclude=[],
|
||||||
name="SensorBench",
|
name="SensorBench",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
# -*- mode: python ; coding: utf-8 -*-
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from PyInstaller.utils.hooks import collect_all, collect_submodules
|
||||||
|
|
||||||
|
|
||||||
|
repo_root = Path.cwd()
|
||||||
|
src_dir = repo_root / "src"
|
||||||
|
operator_src = repo_root / "internal" / "src"
|
||||||
|
work_dir = repo_root / "build" / "pyinstaller_work_operator"
|
||||||
|
|
||||||
|
datas = [
|
||||||
|
# Operator-owned options / locales / workflow steps (private).
|
||||||
|
(str(repo_root / "internal" / "config"), "config"),
|
||||||
|
(str(repo_root / "internal" / "steps"), "steps"),
|
||||||
|
# Public sensor JSON configs (ddjx01, mini, …) for the shared acquisition path.
|
||||||
|
(str(repo_root / "config"), "config"),
|
||||||
|
]
|
||||||
|
binaries = []
|
||||||
|
hiddenimports = [
|
||||||
|
"sensor_bench_operator",
|
||||||
|
"sensor_bench_operator.app",
|
||||||
|
"sensor_bench_operator.paths",
|
||||||
|
"sensor_bench.main",
|
||||||
|
"sensor_bench.exhibition_app",
|
||||||
|
"sensor_bench.acquisition",
|
||||||
|
"sensor_bench.dd06_acquisition",
|
||||||
|
"sensor_bench.roi_geometry",
|
||||||
|
"sensor_bench.camera_discovery",
|
||||||
|
"sensor_bench.force_calibration",
|
||||||
|
"sensor_bench.config_loader",
|
||||||
|
"sensor_bench.i18n",
|
||||||
|
"sensor_bench.paths",
|
||||||
|
"sensor_bench.plot_history",
|
||||||
|
"sensor_bench.recorder",
|
||||||
|
"orisys",
|
||||||
|
"orisys_blend",
|
||||||
|
"scipy.fft",
|
||||||
|
"scipy.ndimage",
|
||||||
|
"scipy.sparse.linalg",
|
||||||
|
"numba",
|
||||||
|
"llvmlite",
|
||||||
|
]
|
||||||
|
|
||||||
|
for package in ("PyQt5", "pyqtgraph", "cv2", "orisys", "orisys_blend", "numba", "llvmlite"):
|
||||||
|
pkg_datas, pkg_binaries, pkg_hidden = collect_all(package)
|
||||||
|
datas += pkg_datas
|
||||||
|
binaries += pkg_binaries
|
||||||
|
hiddenimports += pkg_hidden
|
||||||
|
|
||||||
|
hiddenimports += collect_submodules("orisys_blend")
|
||||||
|
hiddenimports += collect_submodules("sensor_bench")
|
||||||
|
hiddenimports += collect_submodules("sensor_bench_operator")
|
||||||
|
|
||||||
|
excludes = [
|
||||||
|
"cupy",
|
||||||
|
"cupyx",
|
||||||
|
"pyopencl",
|
||||||
|
"open3d",
|
||||||
|
"matplotlib",
|
||||||
|
"tkinter",
|
||||||
|
"pytest",
|
||||||
|
"pip",
|
||||||
|
"wheel",
|
||||||
|
]
|
||||||
|
|
||||||
|
a = Analysis(
|
||||||
|
[str(repo_root / "internal" / "run_operator.py")],
|
||||||
|
pathex=[str(repo_root), str(src_dir), str(operator_src)],
|
||||||
|
binaries=binaries,
|
||||||
|
datas=datas,
|
||||||
|
hiddenimports=hiddenimports,
|
||||||
|
hookspath=[],
|
||||||
|
hooksconfig={},
|
||||||
|
runtime_hooks=[],
|
||||||
|
excludes=excludes,
|
||||||
|
noarchive=False,
|
||||||
|
optimize=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
pyz = PYZ(a.pure)
|
||||||
|
|
||||||
|
exe = EXE(
|
||||||
|
pyz,
|
||||||
|
a.scripts,
|
||||||
|
[],
|
||||||
|
exclude_binaries=True,
|
||||||
|
name="SensorBenchOperator",
|
||||||
|
debug=False,
|
||||||
|
bootloader_ignore_signals=False,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
console=False,
|
||||||
|
disable_windowed_traceback=False,
|
||||||
|
argv_emulation=False,
|
||||||
|
target_arch=None,
|
||||||
|
codesign_identity=None,
|
||||||
|
entitlements_file=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
coll = COLLECT(
|
||||||
|
exe,
|
||||||
|
a.binaries,
|
||||||
|
a.datas,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
upx_exclude=[],
|
||||||
|
name="SensorBenchOperator",
|
||||||
|
)
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
"""Copy Orisys wheel sensor JSON configs into repo config/ (exclude options.json)."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def wheel_config_dir() -> Path:
|
||||||
|
import orisys
|
||||||
|
|
||||||
|
inner_pkg = Path(orisys.__file__).resolve().parent
|
||||||
|
for candidate in (inner_pkg / "configs", inner_pkg.parent / "configs"):
|
||||||
|
if candidate.is_dir() and any(candidate.glob("*.json")):
|
||||||
|
return candidate
|
||||||
|
return inner_pkg.parent / "configs"
|
||||||
|
|
||||||
|
|
||||||
|
def sync_configs(repo_root: Path) -> int:
|
||||||
|
src = wheel_config_dir()
|
||||||
|
if not src.is_dir():
|
||||||
|
print(f"Orisys wheel configs directory not found: {src}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
dst = repo_root / "config"
|
||||||
|
dst.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
copied = 0
|
||||||
|
for path in sorted(src.glob("*.json")):
|
||||||
|
if path.stem.lower() == "options":
|
||||||
|
continue
|
||||||
|
shutil.copy2(path, dst / path.name)
|
||||||
|
copied += 1
|
||||||
|
|
||||||
|
print(f"Synced {copied} config(s) from {src} to {dst}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
repo_root = Path(__file__).resolve().parents[1]
|
||||||
|
return sync_configs(repo_root)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
+91
-5
@@ -1,7 +1,8 @@
|
|||||||
"""Verify the PyInstaller onedir output has the expected portable layout."""
|
"""Verify the public PyInstaller onedir output and reject internal artifacts."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -9,23 +10,108 @@ from pathlib import Path
|
|||||||
ROOT = Path(__file__).resolve().parents[1]
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
DIST = ROOT / "dist" / "SensorBench"
|
DIST = ROOT / "dist" / "SensorBench"
|
||||||
|
|
||||||
|
# Paths / substrings that must never appear in a public customer build.
|
||||||
|
FORBIDDEN_RELATIVE_PATHS = (
|
||||||
|
"steps",
|
||||||
|
"steps/default.json",
|
||||||
|
"config/options.json",
|
||||||
|
"internal",
|
||||||
|
)
|
||||||
|
|
||||||
|
FORBIDDEN_NAME_FRAGMENTS = (
|
||||||
|
"sensor_bench_operator",
|
||||||
|
"sensor_bench.app",
|
||||||
|
"operator-ui",
|
||||||
|
)
|
||||||
|
|
||||||
|
FORBIDDEN_LOCALE_KEYS = (
|
||||||
|
"workflow",
|
||||||
|
)
|
||||||
|
|
||||||
|
FORBIDDEN_TEXT_SNIPPETS = (
|
||||||
|
"Inspection Workflow",
|
||||||
|
"检测流程",
|
||||||
|
"新样品",
|
||||||
|
"冯总",
|
||||||
|
"operator-ui",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def require(path: Path) -> None:
|
def require(path: Path) -> None:
|
||||||
if not path.exists():
|
if not path.exists():
|
||||||
raise FileNotFoundError(f"Missing build artifact: {path}")
|
raise FileNotFoundError(f"Missing build artifact: {path}")
|
||||||
|
|
||||||
|
|
||||||
|
def forbid(path: Path, *, reason: str) -> None:
|
||||||
|
if path.exists():
|
||||||
|
raise RuntimeError(f"Public dist must not include {reason}: {path}")
|
||||||
|
|
||||||
|
|
||||||
|
def _iter_text_files(root: Path) -> list[Path]:
|
||||||
|
files: list[Path] = []
|
||||||
|
for path in root.rglob("*"):
|
||||||
|
if not path.is_file():
|
||||||
|
continue
|
||||||
|
if path.suffix.lower() in {".json", ".txt", ".md", ".py", ".toc", ".html"}:
|
||||||
|
files.append(path)
|
||||||
|
# PyInstaller may leave .pyc / .py modules under _internal
|
||||||
|
if path.suffix.lower() == ".pyc" and "operator" in path.name.lower():
|
||||||
|
files.append(path)
|
||||||
|
return files
|
||||||
|
|
||||||
|
|
||||||
|
def _assert_no_forbidden_names(root: Path) -> None:
|
||||||
|
for path in root.rglob("*"):
|
||||||
|
rel = path.relative_to(root).as_posix().lower()
|
||||||
|
name = path.name.lower()
|
||||||
|
for fragment in FORBIDDEN_NAME_FRAGMENTS:
|
||||||
|
if fragment.lower() in rel or fragment.lower() in name:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Public dist contains forbidden name '{fragment}': {path}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _assert_locale_public(path: Path) -> None:
|
||||||
|
data = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
for key in FORBIDDEN_LOCALE_KEYS:
|
||||||
|
if key in data:
|
||||||
|
raise RuntimeError(f"Public locale {path.name} still contains '{key}'")
|
||||||
|
status = data.get("status")
|
||||||
|
if isinstance(status, dict) and "workflow_done" in status:
|
||||||
|
raise RuntimeError(f"Public locale {path.name} still contains status.workflow_done")
|
||||||
|
|
||||||
|
|
||||||
|
def _assert_no_forbidden_text(root: Path) -> None:
|
||||||
|
for path in _iter_text_files(root):
|
||||||
|
try:
|
||||||
|
text = path.read_text(encoding="utf-8", errors="ignore")
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
continue
|
||||||
|
for snippet in FORBIDDEN_TEXT_SNIPPETS:
|
||||||
|
if snippet in text:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Public dist text contains forbidden snippet {snippet!r} in {path}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
require(DIST / "SensorBench.exe")
|
require(DIST / "SensorBench.exe")
|
||||||
require(DIST / "config" / "options.json")
|
|
||||||
require(DIST / "config" / "locales" / "zh_CN.json")
|
require(DIST / "config" / "locales" / "zh_CN.json")
|
||||||
require(DIST / "config" / "locales" / "en_US.json")
|
require(DIST / "config" / "locales" / "en_US.json")
|
||||||
require(DIST / "steps" / "default.json")
|
require(DIST / "config" / "ddjx01.json")
|
||||||
require(DIST / "_internal")
|
require(DIST / "_internal")
|
||||||
print(f"Verified SensorBench dist layout: {DIST}")
|
|
||||||
|
for rel in FORBIDDEN_RELATIVE_PATHS:
|
||||||
|
forbid(DIST / rel, reason=rel)
|
||||||
|
|
||||||
|
_assert_no_forbidden_names(DIST)
|
||||||
|
_assert_locale_public(DIST / "config" / "locales" / "zh_CN.json")
|
||||||
|
_assert_locale_public(DIST / "config" / "locales" / "en_US.json")
|
||||||
|
_assert_no_forbidden_text(DIST / "config")
|
||||||
|
|
||||||
|
print(f"Verified public SensorBench dist layout: {DIST}")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
raise SystemExit(main())
|
raise SystemExit(main())
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
"""Verify the private SensorBenchOperator onedir layout."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
DIST = ROOT / "dist" / "SensorBenchOperator"
|
||||||
|
|
||||||
|
|
||||||
|
def require(path: Path) -> None:
|
||||||
|
if not path.exists():
|
||||||
|
raise FileNotFoundError(f"Missing operator build artifact: {path}")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
require(DIST / "SensorBenchOperator.exe")
|
||||||
|
require(DIST / "_internal")
|
||||||
|
require(DIST / "steps" / "default.json")
|
||||||
|
require(DIST / "config" / "options.json")
|
||||||
|
require(DIST / "config" / "locales" / "zh_CN.json")
|
||||||
|
require(DIST / "config" / "locales" / "en_US.json")
|
||||||
|
require(DIST / "config" / "ddjx01.json")
|
||||||
|
|
||||||
|
options = json.loads((DIST / "config" / "options.json").read_text(encoding="utf-8"))
|
||||||
|
if "sensor_types" not in options:
|
||||||
|
raise RuntimeError("Operator options.json missing sensor_types")
|
||||||
|
|
||||||
|
locale = json.loads(
|
||||||
|
(DIST / "config" / "locales" / "en_US.json").read_text(encoding="utf-8")
|
||||||
|
)
|
||||||
|
if "workflow" not in locale:
|
||||||
|
raise RuntimeError("Operator locale missing workflow section")
|
||||||
|
if "Inspection Workflow" not in json.dumps(locale, ensure_ascii=False):
|
||||||
|
# Chinese catalog may be the default UI language; English must still ship.
|
||||||
|
raise RuntimeError("Operator en_US locale missing Inspection Workflow title")
|
||||||
|
|
||||||
|
steps = json.loads((DIST / "steps" / "default.json").read_text(encoding="utf-8"))
|
||||||
|
if not steps.get("steps"):
|
||||||
|
raise RuntimeError("Operator steps/default.json has no steps")
|
||||||
|
|
||||||
|
# Guard against accidentally packaging the whole internal/ source tree.
|
||||||
|
if (DIST / "internal").exists():
|
||||||
|
raise RuntimeError(f"Operator dist should not nest internal/: {DIST / 'internal'}")
|
||||||
|
if (DIST / "src").exists():
|
||||||
|
raise RuntimeError(f"Operator dist should not ship raw src/: {DIST / 'src'}")
|
||||||
|
|
||||||
|
print(f"Verified internal SensorBenchOperator dist layout: {DIST}")
|
||||||
|
print("INTERNAL USE ONLY - do not publish to customers.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
{
|
||||||
|
"name": "dd06-ov",
|
||||||
|
"type": "single_camera",
|
||||||
|
"version": "1.0",
|
||||||
|
"camera": {
|
||||||
|
"resolution_raw": [
|
||||||
|
1280,
|
||||||
|
720
|
||||||
|
],
|
||||||
|
"fps": 30,
|
||||||
|
"resolution_stitch": [
|
||||||
|
195,
|
||||||
|
275
|
||||||
|
],
|
||||||
|
"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": 3
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"min_contour_area": 3000,
|
||||||
|
"flow_threshold": 0.2,
|
||||||
|
"div_threshold": 0.022,
|
||||||
|
"expansion_threshold": 0.01,
|
||||||
|
"kernel_size": [
|
||||||
|
5,
|
||||||
|
5
|
||||||
|
],
|
||||||
|
"denoise_sigma": 12
|
||||||
|
},
|
||||||
|
"roi": {
|
||||||
|
"type": "polygon",
|
||||||
|
"image_size": [
|
||||||
|
1280,
|
||||||
|
720
|
||||||
|
],
|
||||||
|
"created_at": "2026-07-26T11:56:15",
|
||||||
|
"points": [
|
||||||
|
[
|
||||||
|
242,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
[
|
||||||
|
402,
|
||||||
|
597
|
||||||
|
],
|
||||||
|
[
|
||||||
|
799,
|
||||||
|
606
|
||||||
|
],
|
||||||
|
[
|
||||||
|
976,
|
||||||
|
2
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"mask_path": "dd06-ov_mask.png",
|
||||||
|
"metadata": {
|
||||||
|
"source": "./data/dd06/0726.mp4"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"colors": {
|
||||||
|
"window_bg": "#1e1f22",
|
||||||
|
"panel_bg": "#121316",
|
||||||
|
"arrow_canvas_hex": "#121316"
|
||||||
|
},
|
||||||
|
"arrow_draw": {
|
||||||
|
"threshold": 2,
|
||||||
|
"grid_spacing": 20,
|
||||||
|
"arrow_scale": 0.5,
|
||||||
|
"below_threshold_color_bgr": [160, 160, 160]
|
||||||
|
},
|
||||||
|
"plot": {
|
||||||
|
"background": "#121316",
|
||||||
|
"curve_colors": ["#8ab4f8", "#34a853", "#fbbc04"]
|
||||||
|
},
|
||||||
|
"mesh": {
|
||||||
|
"background_rgb": [18, 19, 22]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"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.05,
|
||||||
|
"max_aspect_ratio": 50,
|
||||||
|
"index": [1,2,3,6,7,8,11,12,13],
|
||||||
|
"margin": 10,
|
||||||
|
"nominal_ov_pos": 100,
|
||||||
|
"n": [3,3],
|
||||||
|
"row_tol": 0.6,
|
||||||
|
"box_size": [250, 250],
|
||||||
|
"blend":"numba",
|
||||||
|
"blend_list": ["numpy", "numba","none"]
|
||||||
|
},
|
||||||
|
"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": 3
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"min_contour_area": 3000,
|
||||||
|
"flow_threshold":5,
|
||||||
|
"div_threshold":0.05,
|
||||||
|
"expansion_threshold": 0.01,
|
||||||
|
"kernel_size": [5,5],
|
||||||
|
"denoise_sigma": 12
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.4 MiB After Width: | Height: | Size: 814 KiB |
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
{
|
||||||
|
"name": "dd06-ov",
|
||||||
|
"type": "single_camera",
|
||||||
|
"version": "1.0",
|
||||||
|
"camera": {
|
||||||
|
"resolution_raw": [
|
||||||
|
1280,
|
||||||
|
720
|
||||||
|
],
|
||||||
|
"fps": 30,
|
||||||
|
"resolution_stitch": [
|
||||||
|
195,
|
||||||
|
275
|
||||||
|
],
|
||||||
|
"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": 3
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"min_contour_area": 3000,
|
||||||
|
"flow_threshold": 0.2,
|
||||||
|
"div_threshold": 0.022,
|
||||||
|
"expansion_threshold": 0.01,
|
||||||
|
"kernel_size": [
|
||||||
|
5,
|
||||||
|
5
|
||||||
|
],
|
||||||
|
"denoise_sigma": 12
|
||||||
|
},
|
||||||
|
"roi": {
|
||||||
|
"type": "polygon",
|
||||||
|
"image_size": [
|
||||||
|
1280,
|
||||||
|
720
|
||||||
|
],
|
||||||
|
"points": [
|
||||||
|
[
|
||||||
|
628,
|
||||||
|
532
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1145,
|
||||||
|
694
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1161,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
630,
|
||||||
|
126
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"created_at": "2026-08-10T14:26:31",
|
||||||
|
"mask_path": "dd06-ov-0810_mask.png",
|
||||||
|
"metadata": {
|
||||||
|
"source": ".\\data\\dd06\\2026-08-10-142316.webm"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
{
|
||||||
|
"name": "dd06-ov",
|
||||||
|
"type": "single_camera",
|
||||||
|
"version": "1.0",
|
||||||
|
"camera": {
|
||||||
|
"resolution_raw": [
|
||||||
|
1280,
|
||||||
|
720
|
||||||
|
],
|
||||||
|
"fps": 30,
|
||||||
|
"resolution_stitch": [
|
||||||
|
195,
|
||||||
|
275
|
||||||
|
],
|
||||||
|
"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": 3
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"min_contour_area": 3000,
|
||||||
|
"flow_threshold": 0.2,
|
||||||
|
"div_threshold": 0.022,
|
||||||
|
"expansion_threshold": 0.01,
|
||||||
|
"kernel_size": [
|
||||||
|
5,
|
||||||
|
5
|
||||||
|
],
|
||||||
|
"denoise_sigma": 12
|
||||||
|
},
|
||||||
|
"roi": {
|
||||||
|
"type": "polygon",
|
||||||
|
"image_size": [
|
||||||
|
1280,
|
||||||
|
720
|
||||||
|
],
|
||||||
|
"created_at": "2026-07-26T11:56:15",
|
||||||
|
"points": [
|
||||||
|
[
|
||||||
|
242,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
[
|
||||||
|
402,
|
||||||
|
597
|
||||||
|
],
|
||||||
|
[
|
||||||
|
799,
|
||||||
|
606
|
||||||
|
],
|
||||||
|
[
|
||||||
|
976,
|
||||||
|
2
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"mask_path": "dd06-ov_mask.png",
|
||||||
|
"metadata": {
|
||||||
|
"source": "./data/dd06/0726.mp4"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"name": "prototype",
|
||||||
|
"version": "1.0",
|
||||||
|
"camera": {
|
||||||
|
"resolution_raw": [854, 480],
|
||||||
|
"fps": 30,
|
||||||
|
"units": 15,
|
||||||
|
"units_used": 9,
|
||||||
|
"resolution_stitch": [123, 123],
|
||||||
|
"stitch_res_list": [365,263,123,63]
|
||||||
|
},
|
||||||
|
"stitcher": {
|
||||||
|
"min_area_frac": 0.01,
|
||||||
|
"max_area_frac": 0.08,
|
||||||
|
"max_aspect_ratio": 100,
|
||||||
|
"index": [1,2,3,6,7,8,11,12,13],
|
||||||
|
"margin": 4,
|
||||||
|
"nominal_ov_pos": 44,
|
||||||
|
"n": [3,3],
|
||||||
|
"row_tol": 0.6,
|
||||||
|
"box_size": [98, 98],
|
||||||
|
"blend":"numpy",
|
||||||
|
"blend_list": ["numpy", "numba","none"]
|
||||||
|
},
|
||||||
|
"tracker": {
|
||||||
|
"adaptive": true,
|
||||||
|
"lower_threshold": 12.0,
|
||||||
|
"upper_threshold": 18.0,
|
||||||
|
"loop_closure_interval": 1,
|
||||||
|
"algorithm": "dis",
|
||||||
|
"dis_preset": "medium",
|
||||||
|
"dis_mode": 2,
|
||||||
|
"algorithm_list": ["raft", "dis"],
|
||||||
|
"hhd_downsample": 3
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"min_contour_area": 3000,
|
||||||
|
"flow_threshold":1,
|
||||||
|
"div_threshold":0.02,
|
||||||
|
"expansion_threshold": 0.01,
|
||||||
|
"kernel_size": [5,5],
|
||||||
|
"denoise_sigma": 6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"name": "prototype",
|
||||||
|
"version": "1.0",
|
||||||
|
"camera": {
|
||||||
|
"resolution_raw": [1280, 720],
|
||||||
|
"fps": 30,
|
||||||
|
"units": 15,
|
||||||
|
"units_used": 9,
|
||||||
|
"resolution_stitch": [263, 263],
|
||||||
|
"stitch_res_list": [365,263,123,63]
|
||||||
|
},
|
||||||
|
"stitcher": {
|
||||||
|
"min_area_frac": 0.01,
|
||||||
|
"max_area_frac": 0.04,
|
||||||
|
"max_aspect_ratio": 25,
|
||||||
|
"index": [1,2,3,6,7,8,11,12,13],
|
||||||
|
"margin": 10,
|
||||||
|
"nominal_ov_pos": 67,
|
||||||
|
"n": [3,3],
|
||||||
|
"row_tol": 0.6,
|
||||||
|
"box_size": [180, 180],
|
||||||
|
"blend":"numpy",
|
||||||
|
"blend_list": ["numpy", "numba","none"]
|
||||||
|
},
|
||||||
|
"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": 1,
|
||||||
|
"backend": "cpu"
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"min_contour_area": 3000,
|
||||||
|
"flow_threshold":1,
|
||||||
|
"div_threshold":0.02,
|
||||||
|
"expansion_threshold": 0.01,
|
||||||
|
"kernel_size": [5,5],
|
||||||
|
"denoise_sigma": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"name": "prototype",
|
||||||
|
"version": "1.0",
|
||||||
|
"camera": {
|
||||||
|
"resolution_raw": [1280, 720],
|
||||||
|
"fps": 15,
|
||||||
|
"units": 15,
|
||||||
|
"units_used": 9,
|
||||||
|
"resolution_stitch": [263, 263],
|
||||||
|
"stitch_res_list": [365,263,123,63]
|
||||||
|
},
|
||||||
|
"stitcher": {
|
||||||
|
"min_area_frac": 0.01,
|
||||||
|
"max_area_frac": 0.04,
|
||||||
|
"max_aspect_ratio": 25,
|
||||||
|
"index": [1,2,3,6,7,8,11,12,13],
|
||||||
|
"margin": 10,
|
||||||
|
"nominal_ov_pos": 67,
|
||||||
|
"n": [3,3],
|
||||||
|
"row_tol": 0.6,
|
||||||
|
"box_size": [180, 180],
|
||||||
|
"blend":"numpy",
|
||||||
|
"blend_list": ["numpy", "numba","none"]
|
||||||
|
},
|
||||||
|
"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": 3,
|
||||||
|
"backend": "cpu"
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"min_contour_area": 3000,
|
||||||
|
"flow_threshold":1,
|
||||||
|
"div_threshold":0.02,
|
||||||
|
"expansion_threshold": 0.01,
|
||||||
|
"kernel_size": [5,5],
|
||||||
|
"denoise_sigma": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "dd02-ov",
|
||||||
|
"type": "single_camera",
|
||||||
|
"version": "1.0",
|
||||||
|
"camera": {
|
||||||
|
"resolution_raw": [1280, 720],
|
||||||
|
"fps": 30,
|
||||||
|
"resolution_stitch": [280, 300],
|
||||||
|
"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": 1,
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
+44
-84
@@ -5,65 +5,26 @@
|
|||||||
"lang_zh": "中文",
|
"lang_zh": "中文",
|
||||||
"lang_en": "English"
|
"lang_en": "English"
|
||||||
},
|
},
|
||||||
"panels": {
|
|
||||||
"instructions": "Instructions",
|
|
||||||
"options": "Sensor Component Types",
|
|
||||||
"camera": "Camera (Orisys SDK)",
|
|
||||||
"recording": "Recording",
|
|
||||||
"raw": "Raw Image (sensor.frame)",
|
|
||||||
"processed": "Processed Image (Sensor.get_img)",
|
|
||||||
"force": "Force Curves"
|
|
||||||
},
|
|
||||||
"labels": {
|
"labels": {
|
||||||
"sensor_type": "Sensor type",
|
|
||||||
"elastomer": "Elastomer",
|
|
||||||
"cmos": "CMOS",
|
|
||||||
"marker": "Marker",
|
|
||||||
"indentor": "Indenter",
|
|
||||||
"platform": "Platform",
|
|
||||||
"camera_index": "Camera index",
|
|
||||||
"backend": "Backend",
|
|
||||||
"fixed_mode": "Fixed mode: {mode}",
|
"fixed_mode": "Fixed mode: {mode}",
|
||||||
"fixed_mode_unset": "Fixed mode: set config path",
|
"fixed_mode_unset": "Fixed mode: set config path",
|
||||||
"fixed_mode_invalid": "Fixed mode: invalid config ({error})",
|
"fixed_mode_invalid": "Fixed mode: invalid config ({error})",
|
||||||
"requested": "Requested",
|
"no_preview": "No preview"
|
||||||
"actual": "Actual",
|
|
||||||
"capture_fps": "Capture FPS",
|
|
||||||
"render_fps": "Render FPS",
|
|
||||||
"proc_ms": "Proc ms",
|
|
||||||
"fn": "FN",
|
|
||||||
"fx": "FX",
|
|
||||||
"fy": "FY",
|
|
||||||
"no_preview": "No preview",
|
|
||||||
"processed_placeholder": "Start Preview to show Orisys output",
|
|
||||||
"not_recording": "Not recording",
|
|
||||||
"force_plot": "Force"
|
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"config": "Config…",
|
"config": "Config…",
|
||||||
"cal": "Cal…",
|
|
||||||
"start_preview": "Start Preview",
|
|
||||||
"stop_preview": "Stop Preview",
|
|
||||||
"browse": "Browse…",
|
|
||||||
"start_record": "Start Record",
|
"start_record": "Start Record",
|
||||||
"stop_record": "Stop Record",
|
"stop_record": "Stop Record"
|
||||||
"next": "Next",
|
|
||||||
"revert": "Revert",
|
|
||||||
"reset": "New Sample / Reset"
|
|
||||||
},
|
},
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"config": "DDJX01 config JSON path or name",
|
"config": "Select an installed Orisys config, or browse a JSON file"
|
||||||
"cal": "Optional calibration .npy"
|
|
||||||
},
|
},
|
||||||
"dialogs": {
|
"dialogs": {
|
||||||
"select_config": "Select config JSON",
|
"select_config": "Select config JSON",
|
||||||
"select_cal": "Select calibration",
|
|
||||||
"select_recordings": "Select recordings folder",
|
|
||||||
"orisys_title": "Orisys",
|
"orisys_title": "Orisys",
|
||||||
"orisys_required": "orisys SDK is required for preview.\n\n{detail}",
|
"orisys_required": "orisys SDK is required for preview.\n\n{detail}",
|
||||||
"config_title": "Config",
|
"config_title": "Config",
|
||||||
"config_required": "Select a DDJX01 config JSON first.",
|
"config_required": "Select a DDJX01 config JSON first.",
|
||||||
"config_invalid": "Config could not be loaded.",
|
|
||||||
"capture_title": "Capture",
|
"capture_title": "Capture",
|
||||||
"recording_title": "Recording",
|
"recording_title": "Recording",
|
||||||
"no_frame": "No frame yet."
|
"no_frame": "No frame yet."
|
||||||
@@ -76,48 +37,47 @@
|
|||||||
"preview_stopped": "Preview stopped.",
|
"preview_stopped": "Preview stopped.",
|
||||||
"capture_error": "Capture error: {message}",
|
"capture_error": "Capture error: {message}",
|
||||||
"frame_info": "{raw_w}x{raw_h} raw / {proc_w}x{proc_h} processed @ {fps:.1f} FPS",
|
"frame_info": "{raw_w}x{raw_h} raw / {proc_w}x{proc_h} processed @ {fps:.1f} FPS",
|
||||||
"recording": "Recording… {name}\nOptions: sensor={sensor_type}, elastomer={elastomer}, cmos={cmos}, marker={marker}, indenter={indentor}, platform={platform}",
|
"saved": "Saved {count} frames → {path}"
|
||||||
"recording_frames": "Recording… {name}\nFrames: {count}",
|
|
||||||
"saved": "Saved {count} frames → {path}",
|
|
||||||
"workflow_done": "All steps completed. Click New Sample / Reset for the next unit."
|
|
||||||
},
|
},
|
||||||
"workflow": {
|
"exhibition": {
|
||||||
"title": "Sensor Bench Inspection Workflow",
|
"title": "Sensor Bench Exhibition",
|
||||||
"current_marker": "Current",
|
"btn_search": "Search",
|
||||||
"done_marker": "Done",
|
"btn_camera": "Camera",
|
||||||
"steps": {
|
"btn_config": "Config",
|
||||||
"power_on": {
|
"btn_start": "Start",
|
||||||
"title": "Power on and connect",
|
"btn_starting": "Starting…",
|
||||||
"body": "Connect the sensor USB cable and wait until Windows recognizes the device."
|
"btn_stop": "Pause",
|
||||||
},
|
"btn_reset": "Reset",
|
||||||
"select_camera": {
|
"btn_start_record": "Start Record",
|
||||||
"title": "Start Orisys preview",
|
"btn_stop_record": "Stop Record",
|
||||||
"body": "Confirm camera index (usually 0) and DDJX01 config, then click Start Preview. Format is fixed by the Orisys config (1920x1080 @ 30)."
|
"show_log": "Show log panel",
|
||||||
},
|
"view": "View",
|
||||||
"set_options": {
|
"view_pure_color": "Arrows on solid bg",
|
||||||
"title": "Set sensor options",
|
"view_captured": "Arrows on captured",
|
||||||
"body": "Choose elastomer type, CMOS type, and marker type that match the physical unit under test."
|
"view_processed": "Processed image",
|
||||||
},
|
"view_raw": "Raw image",
|
||||||
"check_preview": {
|
"sensor_type": "Sensor",
|
||||||
"title": "Check raw and processed preview",
|
"sensor_orisys": "ORY-BOX/ORY-MINI",
|
||||||
"body": "Confirm raw frame is ~1920x1080 and processed Orisys image is ~365x365. Inspect focus, lighting, and image cleanliness."
|
"sensor_dd06": "ORY-X",
|
||||||
},
|
"sensor_switched": "Switched sensor type: {sensor}",
|
||||||
"record": {
|
"placeholder_camera": "Camera index or video path",
|
||||||
"title": "Record raw data",
|
"dialog_video": "Select video file",
|
||||||
"body": "Choose a storage folder, start recording, exercise the sensor, then stop recording."
|
"camera_found": "Found {count} camera(s): {names}",
|
||||||
},
|
"camera_none": "No usable camera found. Click Search to rescan, or use Camera to pick a video file.",
|
||||||
"review_curves": {
|
"plot_fn": "Normal Force FN",
|
||||||
"title": "Review performance curves",
|
"plot_fx": "Shear Force FX",
|
||||||
"body": "Watch capture/render FPS and force curves for drops or unstable exposure before signing off."
|
"plot_fy": "Shear Force FY",
|
||||||
},
|
"reset_done": "Tracker reset.",
|
||||||
"sensitivity": {
|
"reset_need_start": "Start the sensor before resetting the tracker.",
|
||||||
"title": "Sensitivity test",
|
"reset_failed": "Reset failed: {message}",
|
||||||
"body": "Choose a small indenter and apply a 2g weight, then watch for a clear force-curve change."
|
"force_fn_n": "Normal force (N)={value:.6f}",
|
||||||
},
|
"recording_started": "Recording started: {name}",
|
||||||
"max_load": {
|
"hints": "Keys: Q quit | R reset tracker | S start record | E stop record | ESC fullscreen",
|
||||||
"title": "Maximum load test",
|
"menu_help": "Help",
|
||||||
"body": "Choose a flat plate indenter and apply weights from 10g to 500g to watch for curve changes. Use 10g to 500g for MINI and 2kg for BOX."
|
"menu_about": "About",
|
||||||
}
|
"about_title": "About Sensor Bench",
|
||||||
}
|
"about_body": "Sensor Bench Exhibition\nVersion: {app_version}\nOrisys: {orisys_version}\n\nOrisys-Tech",
|
||||||
|
"config_dir_tip": "Installed configs dir: {path}\nSelected: {selected}",
|
||||||
|
"config_dir_missing": "Installed Orisys configs directory not found. Use Config… to browse a local JSON."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+44
-84
@@ -5,65 +5,26 @@
|
|||||||
"lang_zh": "中文",
|
"lang_zh": "中文",
|
||||||
"lang_en": "English"
|
"lang_en": "English"
|
||||||
},
|
},
|
||||||
"panels": {
|
|
||||||
"instructions": "操作步骤",
|
|
||||||
"options": "传感器组件类型",
|
|
||||||
"camera": "相机(Orisys SDK)",
|
|
||||||
"recording": "录制",
|
|
||||||
"raw": "原始图像(sensor.frame)",
|
|
||||||
"processed": "处理后图像(Sensor.get_img)",
|
|
||||||
"force": "力曲线"
|
|
||||||
},
|
|
||||||
"labels": {
|
"labels": {
|
||||||
"sensor_type": "传感器类型",
|
|
||||||
"elastomer": "弹性体",
|
|
||||||
"cmos": "CMOS",
|
|
||||||
"marker": "图案",
|
|
||||||
"indentor": "压头",
|
|
||||||
"platform": "平台",
|
|
||||||
"camera_index": "相机索引",
|
|
||||||
"backend": "后端",
|
|
||||||
"fixed_mode": "固定模式:{mode}",
|
"fixed_mode": "固定模式:{mode}",
|
||||||
"fixed_mode_unset": "固定模式:请设置配置路径",
|
"fixed_mode_unset": "固定模式:请设置配置路径",
|
||||||
"fixed_mode_invalid": "固定模式:配置无效({error})",
|
"fixed_mode_invalid": "固定模式:配置无效({error})",
|
||||||
"requested": "请求",
|
"no_preview": "暂无预览"
|
||||||
"actual": "实际",
|
|
||||||
"capture_fps": "采集 FPS",
|
|
||||||
"render_fps": "渲染 FPS",
|
|
||||||
"proc_ms": "处理 ms",
|
|
||||||
"fn": "FN",
|
|
||||||
"fx": "FX",
|
|
||||||
"fy": "FY",
|
|
||||||
"no_preview": "暂无预览",
|
|
||||||
"processed_placeholder": "点击开始预览以显示 Orisys 输出",
|
|
||||||
"not_recording": "未在录制",
|
|
||||||
"force_plot": "力"
|
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"config": "配置…",
|
"config": "配置…",
|
||||||
"cal": "标定…",
|
|
||||||
"start_preview": "开始预览",
|
|
||||||
"stop_preview": "停止预览",
|
|
||||||
"browse": "浏览…",
|
|
||||||
"start_record": "开始录制",
|
"start_record": "开始录制",
|
||||||
"stop_record": "停止录制",
|
"stop_record": "停止录制"
|
||||||
"next": "下一步",
|
|
||||||
"revert": "上一步",
|
|
||||||
"reset": "新样品 / 重置"
|
|
||||||
},
|
},
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"config": "DDJX01 配置 JSON 路径或名称",
|
"config": "从已安装 Orisys 配置中选择,或浏览 JSON"
|
||||||
"cal": "可选标定文件 .npy"
|
|
||||||
},
|
},
|
||||||
"dialogs": {
|
"dialogs": {
|
||||||
"select_config": "选择配置 JSON",
|
"select_config": "选择配置 JSON",
|
||||||
"select_cal": "选择标定文件",
|
|
||||||
"select_recordings": "选择录制文件夹",
|
|
||||||
"orisys_title": "Orisys",
|
"orisys_title": "Orisys",
|
||||||
"orisys_required": "预览需要 Orisys SDK。\n\n{detail}",
|
"orisys_required": "预览需要 Orisys SDK。\n\n{detail}",
|
||||||
"config_title": "配置",
|
"config_title": "配置",
|
||||||
"config_required": "请先选择 DDJX01 配置 JSON。",
|
"config_required": "请先选择 DDJX01 配置 JSON。",
|
||||||
"config_invalid": "无法加载配置。",
|
|
||||||
"capture_title": "采集",
|
"capture_title": "采集",
|
||||||
"recording_title": "录制",
|
"recording_title": "录制",
|
||||||
"no_frame": "尚无可用帧。"
|
"no_frame": "尚无可用帧。"
|
||||||
@@ -76,48 +37,47 @@
|
|||||||
"preview_stopped": "预览已停止。",
|
"preview_stopped": "预览已停止。",
|
||||||
"capture_error": "采集错误:{message}",
|
"capture_error": "采集错误:{message}",
|
||||||
"frame_info": "{raw_w}x{raw_h} 原始 / {proc_w}x{proc_h} 处理 @ {fps:.1f} FPS",
|
"frame_info": "{raw_w}x{raw_h} 原始 / {proc_w}x{proc_h} 处理 @ {fps:.1f} FPS",
|
||||||
"recording": "正在录制… {name}\n选项:传感器={sensor_type},弹性体={elastomer},CMOS={cmos},标记={marker},压头={indentor},平台={platform}",
|
"saved": "已保存 {count} 帧 → {path}"
|
||||||
"recording_frames": "正在录制… {name}\n帧数:{count}",
|
|
||||||
"saved": "已保存 {count} 帧 → {path}",
|
|
||||||
"workflow_done": "全部步骤已完成。可点击“新样品 / 重置”开始下一个样品。"
|
|
||||||
},
|
},
|
||||||
"workflow": {
|
"exhibition": {
|
||||||
"title": "传感器检测流程",
|
"title": "Sensor Bench 展示端",
|
||||||
"current_marker": "当前",
|
"btn_search": "搜索",
|
||||||
"done_marker": "已完成",
|
"btn_camera": "摄像机",
|
||||||
"steps": {
|
"btn_config": "配置",
|
||||||
"power_on": {
|
"btn_start": "启动",
|
||||||
"title": "开机并连接",
|
"btn_starting": "启动中…",
|
||||||
"body": "连接传感器 USB 线缆,等待 Windows 识别设备。"
|
"btn_stop": "暂停",
|
||||||
},
|
"btn_reset": "重置",
|
||||||
"select_camera": {
|
"btn_start_record": "开始录像",
|
||||||
"title": "启动 Orisys 预览",
|
"btn_stop_record": "结束录像",
|
||||||
"body": "确认相机索引(通常为 0)和 DDJX01 配置,然后点击开始预览。格式由 Orisys 配置固定(1920x1080 @ 30)。"
|
"show_log": "显示日志面板",
|
||||||
},
|
"view": "画面",
|
||||||
"set_options": {
|
"view_pure_color": "纯色背景箭头",
|
||||||
"title": "设置传感器选项",
|
"view_captured": "采集图像箭头",
|
||||||
"body": "选择与当前被测实物一致的弹性体类型、CMOS 类型和标记类型。"
|
"view_processed": "处理后图像",
|
||||||
},
|
"view_raw": "原始图像",
|
||||||
"check_preview": {
|
"sensor_type": "传感器",
|
||||||
"title": "检查原始与处理后预览",
|
"sensor_orisys": "ORY-BOX/ORY-MINI",
|
||||||
"body": "确认原始画面约为 1920x1080,处理后 Orisys 图像约为 365x365。检查对焦、光照和画面清洁度。"
|
"sensor_dd06": "ORY-X",
|
||||||
},
|
"sensor_switched": "已切换传感器类型:{sensor}",
|
||||||
"record": {
|
"placeholder_camera": "摄像头编号或视频路径",
|
||||||
"title": "录制原始数据",
|
"dialog_video": "选择视频文件",
|
||||||
"body": "选择存储文件夹,开始录制,操作传感器,然后停止录制。"
|
"camera_found": "已发现 {count} 路摄像机:{names}",
|
||||||
},
|
"camera_none": "未发现可用摄像机。可点击“搜索”重扫,或通过“摄像机”选择视频文件。",
|
||||||
"review_curves": {
|
"plot_fn": "法向力 FN",
|
||||||
"title": "查看性能曲线",
|
"plot_fx": "切向力 FX",
|
||||||
"body": "观察采集/渲染 FPS 与力曲线,确认无明显掉帧或曝光不稳定后再签字确认。"
|
"plot_fy": "切向力 FY",
|
||||||
},
|
"reset_done": "追踪器已重置。",
|
||||||
"sensitivity": {
|
"reset_need_start": "请先启动传感器后再重置追踪器。",
|
||||||
"title": "灵敏度测试",
|
"reset_failed": "重置失败:{message}",
|
||||||
"body": "选择小压头,并加载 2g 砝码,观察力曲线是否有清晰变化。"
|
"force_fn_n": "法向力(N)={value:.6f}",
|
||||||
},
|
"recording_started": "开始录像:{name}",
|
||||||
"max_load": {
|
"hints": "按键:Q 退出 | R 重置追踪器 | S 开始录像 | E 结束录像 | ESC 全屏",
|
||||||
"title": "最大载荷测试",
|
"menu_help": "帮助",
|
||||||
"body": "选择平板压头,并加载 10g 到 500g 的砝码观察曲线变化;MINI 使用 10g 到 500g,BOX 使用 2kg。"
|
"menu_about": "关于",
|
||||||
}
|
"about_title": "关于 Sensor Bench",
|
||||||
}
|
"about_body": "Sensor Bench 展示端\n版本:{app_version}\nOrisys:{orisys_version}\n\nOrisys-Tech",
|
||||||
|
"config_dir_tip": "已安装配置目录:{path}\n当前:{selected}",
|
||||||
|
"config_dir_missing": "未找到已安装的 Orisys 配置目录。可用“配置…”浏览本地 JSON。"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"name": "prototype",
|
||||||
|
"version": "1.0",
|
||||||
|
"camera": {
|
||||||
|
"resolution_raw": [1280, 720],
|
||||||
|
"fps": 30,
|
||||||
|
"units": 15,
|
||||||
|
"units_used": 9,
|
||||||
|
"resolution_stitch": [263, 263],
|
||||||
|
"stitch_res_list": [365,263,123,63]
|
||||||
|
},
|
||||||
|
"stitcher": {
|
||||||
|
"min_area_frac": 0.01,
|
||||||
|
"max_area_frac": 0.04,
|
||||||
|
"max_aspect_ratio": 25,
|
||||||
|
"index": [1,2,3,6,7,8,11,12,13],
|
||||||
|
"margin": 10,
|
||||||
|
"nominal_ov_pos": 67,
|
||||||
|
"n": [3,3],
|
||||||
|
"row_tol": 0.6,
|
||||||
|
"box_size": [180, 180],
|
||||||
|
"blend":"numpy",
|
||||||
|
"blend_list": ["numpy", "numba","none"]
|
||||||
|
},
|
||||||
|
"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": 3
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"min_contour_area": 3000,
|
||||||
|
"flow_threshold":1,
|
||||||
|
"div_threshold":0.02,
|
||||||
|
"expansion_threshold": 0.01,
|
||||||
|
"kernel_size": [5,5],
|
||||||
|
"denoise_sigma": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"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.05,
|
||||||
|
"max_aspect_ratio": 50,
|
||||||
|
"index": [1,2,3,6,7,8,11,12,13],
|
||||||
|
"margin": 10,
|
||||||
|
"nominal_ov_pos": 100,
|
||||||
|
"n": [3,3],
|
||||||
|
"row_tol": 0.6,
|
||||||
|
"box_size": [250, 250],
|
||||||
|
"blend":"numba",
|
||||||
|
"blend_list": ["numpy", "numba","none"]
|
||||||
|
},
|
||||||
|
"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": 3
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"min_contour_area": 3000,
|
||||||
|
"flow_threshold":5,
|
||||||
|
"div_threshold":0.05,
|
||||||
|
"expansion_threshold": 0.01,
|
||||||
|
"kernel_size": [5,5],
|
||||||
|
"denoise_sigma": 12
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"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"],
|
||||||
|
"hhd_downsample": 3
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"min_contour_area": 3000,
|
||||||
|
"flow_threshold":1,
|
||||||
|
"div_threshold":0.02,
|
||||||
|
"expansion_threshold": 0.01,
|
||||||
|
"kernel_size": [5,5],
|
||||||
|
"denoise_sigma": 15
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"name": "prototype",
|
||||||
|
"version": "1.0",
|
||||||
|
"camera": {
|
||||||
|
"resolution_raw": [854, 480],
|
||||||
|
"fps": 30,
|
||||||
|
"units": 15,
|
||||||
|
"units_used": 9,
|
||||||
|
"resolution_stitch": [123, 123],
|
||||||
|
"stitch_res_list": [365,263,123,63]
|
||||||
|
},
|
||||||
|
"stitcher": {
|
||||||
|
"min_area_frac": 0.01,
|
||||||
|
"max_area_frac": 0.06,
|
||||||
|
"max_aspect_ratio": 100,
|
||||||
|
"index": [1,2,3,6,7,8,11,12,13],
|
||||||
|
"margin": 4,
|
||||||
|
"nominal_ov_pos": 44,
|
||||||
|
"n": [3,3],
|
||||||
|
"row_tol": 0.6,
|
||||||
|
"box_size": [98, 98]
|
||||||
|
},
|
||||||
|
"tracker": {
|
||||||
|
"adaptive": true,
|
||||||
|
"lower_threshold": 12.0,
|
||||||
|
"upper_threshold": 18.0,
|
||||||
|
"loop_closure_interval": 1,
|
||||||
|
"algorithm": "dis",
|
||||||
|
"dis_preset": "medium",
|
||||||
|
"dis_mode": 2,
|
||||||
|
"algorithm_list": ["raft", "dis"],
|
||||||
|
"hhd_downsample": 3
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"min_contour_area": 3000,
|
||||||
|
"flow_threshold":1,
|
||||||
|
"div_threshold":0.02,
|
||||||
|
"expansion_threshold": 0.01,
|
||||||
|
"kernel_size": [5,5],
|
||||||
|
"denoise_sigma": 6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"name": "prototype",
|
||||||
|
"version": "1.0",
|
||||||
|
"camera": {
|
||||||
|
"resolution_raw": [1280, 720],
|
||||||
|
"fps": 30,
|
||||||
|
"units": 15,
|
||||||
|
"units_used": 9,
|
||||||
|
"resolution_stitch": [263, 263],
|
||||||
|
"stitch_res_list": [365,263,123,63]
|
||||||
|
},
|
||||||
|
"stitcher": {
|
||||||
|
"min_area_frac": 0.01,
|
||||||
|
"max_area_frac": 0.04,
|
||||||
|
"max_aspect_ratio": 25,
|
||||||
|
"index": [1,2,3,6,7,8,11,12,13],
|
||||||
|
"margin": 10,
|
||||||
|
"nominal_ov_pos": 67,
|
||||||
|
"n": [3,3],
|
||||||
|
"row_tol": 0.6,
|
||||||
|
"box_size": [180, 180]
|
||||||
|
},
|
||||||
|
"tracker": {
|
||||||
|
"adaptive": true,
|
||||||
|
"lower_threshold": 12.0,
|
||||||
|
"upper_threshold": 18.0,
|
||||||
|
"loop_closure_interval": 1,
|
||||||
|
"algorithm": "dis",
|
||||||
|
"dis_preset": "medium",
|
||||||
|
"dis_mode": 2,
|
||||||
|
"algorithm_list": ["raft", "dis"],
|
||||||
|
"hhd_downsample": 3
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"min_contour_area": 3000,
|
||||||
|
"flow_threshold":1,
|
||||||
|
"div_threshold":0.02,
|
||||||
|
"expansion_threshold": 0.01,
|
||||||
|
"kernel_size": [5,5],
|
||||||
|
"denoise_sigma": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"name": "prototype",
|
||||||
|
"version": "1.0",
|
||||||
|
"camera": {
|
||||||
|
"resolution_raw": [1920, 1080],
|
||||||
|
"fps": 30,
|
||||||
|
"units": 15,
|
||||||
|
"units_used": 9,
|
||||||
|
"resolution_stitch": [384, 384]
|
||||||
|
},
|
||||||
|
"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": "raft",
|
||||||
|
"dis_preset": "medium",
|
||||||
|
"dis_mode": 3,
|
||||||
|
"algorithm_list": ["raft", "dis"],
|
||||||
|
"hhd_downsample": 3
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"min_contour_area": 3000,
|
||||||
|
"flow_threshold":1.5,
|
||||||
|
"div_threshold":0.08,
|
||||||
|
"expansion_threshold": 0.01,
|
||||||
|
"kernel_size": [5,5],
|
||||||
|
"denoise_sigma": 15
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# Sensor Bench Operator (Internal)
|
||||||
|
|
||||||
|
Private operator / inspection workflow UI. **Do not publish** this folder or its
|
||||||
|
resources in customer EXEs, public docs, or release zips.
|
||||||
|
|
||||||
|
## What lives here
|
||||||
|
|
||||||
|
- Workflow UI (`sensor_bench_operator.app.MainWindow`)
|
||||||
|
- Inspection steps (`steps/default.json`)
|
||||||
|
- Lab component options (`config/options.json`, including Chinese lab labels)
|
||||||
|
- Operator locale catalogs (`config/locales/`)
|
||||||
|
|
||||||
|
The public exhibition app lives under the repository root (`run.py`,
|
||||||
|
`src/sensor_bench/exhibition_app.py`) and must not import this package.
|
||||||
|
|
||||||
|
## Run (source)
|
||||||
|
|
||||||
|
From the repository root:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python internal/run_operator.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Or:
|
||||||
|
|
||||||
|
```bat
|
||||||
|
.\internal\run_operator.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires the public `sensor-bench` package sources on `PYTHONPATH` (the launcher
|
||||||
|
adds `src/` and `internal/src/` automatically) and an importable `orisys`.
|
||||||
|
|
||||||
|
## Build private EXE
|
||||||
|
|
||||||
|
Uses the same conda env as the public build (`sensor-bench-build-py310`):
|
||||||
|
|
||||||
|
```bat
|
||||||
|
.\build\create_env.bat
|
||||||
|
.\build\build_operator_exe.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
Output (internal only — do not ship to customers):
|
||||||
|
|
||||||
|
```text
|
||||||
|
dist/SensorBenchOperator/SensorBenchOperator.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
The build copies public sensor configs plus operator `options.json`, locales, and
|
||||||
|
`steps/` beside the EXE. See [`../build/README.md`](../build/README.md).
|
||||||
|
|
||||||
|
## Relationship to public Sensor Bench
|
||||||
|
|
||||||
|
```text
|
||||||
|
orisys (public SDK)
|
||||||
|
├── sensor-bench → dist/SensorBench/ (public exhibition EXE)
|
||||||
|
└── internal/ → dist/SensorBenchOperator/ (private operator EXE)
|
||||||
|
```
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
{
|
||||||
|
"app": {
|
||||||
|
"title": "Sensor Bench",
|
||||||
|
"language": "Language",
|
||||||
|
"lang_zh": "中文",
|
||||||
|
"lang_en": "English"
|
||||||
|
},
|
||||||
|
"panels": {
|
||||||
|
"instructions": "Instructions",
|
||||||
|
"options": "Sensor Component Types",
|
||||||
|
"camera": "Camera (Orisys SDK)",
|
||||||
|
"recording": "Recording",
|
||||||
|
"raw": "Raw Image (sensor.frame)",
|
||||||
|
"processed": "Processed Image (Sensor.get_img)",
|
||||||
|
"force": "Force Curves"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"sensor_type": "Sensor type",
|
||||||
|
"elastomer": "Elastomer",
|
||||||
|
"cmos": "CMOS",
|
||||||
|
"marker": "Marker",
|
||||||
|
"indentor": "Indenter",
|
||||||
|
"platform": "Platform",
|
||||||
|
"camera_index": "Camera index",
|
||||||
|
"backend": "Backend",
|
||||||
|
"fixed_mode": "Fixed mode: {mode}",
|
||||||
|
"fixed_mode_unset": "Fixed mode: set config path",
|
||||||
|
"fixed_mode_invalid": "Fixed mode: invalid config ({error})",
|
||||||
|
"requested": "Requested",
|
||||||
|
"actual": "Actual",
|
||||||
|
"capture_fps": "Capture FPS",
|
||||||
|
"render_fps": "Render FPS",
|
||||||
|
"proc_ms": "Proc ms",
|
||||||
|
"fn": "FN",
|
||||||
|
"fx": "FX",
|
||||||
|
"fy": "FY",
|
||||||
|
"no_preview": "No preview",
|
||||||
|
"processed_placeholder": "Start Preview to show Orisys output",
|
||||||
|
"not_recording": "Not recording",
|
||||||
|
"force_plot": "Force",
|
||||||
|
"config_dir_tip": "Installed configs dir: {path}\nSelected: {selected}",
|
||||||
|
"config_dir_missing": "Installed Orisys configs directory not found. Use Config… to browse a local JSON."
|
||||||
|
},
|
||||||
|
"buttons": {
|
||||||
|
"config": "Config…",
|
||||||
|
"cal": "Cal…",
|
||||||
|
"start_preview": "Start Preview",
|
||||||
|
"stop_preview": "Stop Preview",
|
||||||
|
"browse": "Browse…",
|
||||||
|
"start_record": "Start Record",
|
||||||
|
"stop_record": "Stop Record",
|
||||||
|
"next": "Next",
|
||||||
|
"revert": "Revert",
|
||||||
|
"reset": "New Sample / Reset"
|
||||||
|
},
|
||||||
|
"placeholders": {
|
||||||
|
"config": "Select an installed Orisys config, or browse a JSON file",
|
||||||
|
"cal": "Optional calibration .npy"
|
||||||
|
},
|
||||||
|
"dialogs": {
|
||||||
|
"select_config": "Select config JSON",
|
||||||
|
"select_cal": "Select calibration",
|
||||||
|
"select_recordings": "Select recordings folder",
|
||||||
|
"orisys_title": "Orisys",
|
||||||
|
"orisys_required": "orisys SDK is required for preview.\n\n{detail}",
|
||||||
|
"config_title": "Config",
|
||||||
|
"config_required": "Select a DDJX01 config JSON first.",
|
||||||
|
"config_invalid": "Config could not be loaded.",
|
||||||
|
"capture_title": "Capture",
|
||||||
|
"recording_title": "Recording",
|
||||||
|
"no_frame": "No frame yet."
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"ready": "Ready — Start Preview uses Orisys DDJX01 fixed format.",
|
||||||
|
"orisys_ok": "orisys available (version {version})",
|
||||||
|
"orisys_missing": "orisys not importable — add orisys-sdk/src to PYTHONPATH.",
|
||||||
|
"opening": "Opening Orisys ({mode})…",
|
||||||
|
"preview_stopped": "Preview stopped.",
|
||||||
|
"capture_error": "Capture error: {message}",
|
||||||
|
"frame_info": "{raw_w}x{raw_h} raw / {proc_w}x{proc_h} processed @ {fps:.1f} FPS",
|
||||||
|
"recording": "Recording… {name}\nOptions: sensor={sensor_type}, elastomer={elastomer}, cmos={cmos}, marker={marker}, indenter={indentor}, platform={platform}",
|
||||||
|
"recording_frames": "Recording… {name}\nFrames: {count}",
|
||||||
|
"saved": "Saved {count} frames → {path}",
|
||||||
|
"workflow_done": "All steps completed. Click New Sample / Reset for the next unit."
|
||||||
|
},
|
||||||
|
"exhibition": {
|
||||||
|
"title": "Sensor Bench Exhibition",
|
||||||
|
"btn_search": "Search",
|
||||||
|
"btn_camera": "Camera",
|
||||||
|
"btn_config": "Config",
|
||||||
|
"btn_start": "Start",
|
||||||
|
"btn_starting": "Starting…",
|
||||||
|
"btn_stop": "Pause",
|
||||||
|
"btn_reset": "Reset",
|
||||||
|
"btn_start_record": "Start Record",
|
||||||
|
"btn_stop_record": "Stop Record",
|
||||||
|
"show_log": "Show log panel",
|
||||||
|
"view": "View",
|
||||||
|
"view_pure_color": "Arrows on solid bg",
|
||||||
|
"view_captured": "Arrows on captured",
|
||||||
|
"view_processed": "Processed image",
|
||||||
|
"view_raw": "Raw image",
|
||||||
|
"sensor_type": "Sensor",
|
||||||
|
"sensor_orisys": "ORY-BOX/ORY-MINI",
|
||||||
|
"sensor_dd06": "ORY-X",
|
||||||
|
"sensor_switched": "Switched sensor type: {sensor}",
|
||||||
|
"placeholder_camera": "Camera index or video path",
|
||||||
|
"dialog_video": "Select video file",
|
||||||
|
"camera_found": "Found {count} camera(s): {names}",
|
||||||
|
"camera_none": "No usable camera found. Click Search to rescan, or use Camera to pick a video file.",
|
||||||
|
"plot_fn": "Normal Force FN",
|
||||||
|
"plot_fx": "Shear Force FX",
|
||||||
|
"plot_fy": "Shear Force FY",
|
||||||
|
"reset_done": "Tracker reset.",
|
||||||
|
"reset_need_start": "Start the sensor before resetting the tracker.",
|
||||||
|
"reset_failed": "Reset failed: {message}",
|
||||||
|
"force_fn_n": "Normal force (N)={value:.6f}",
|
||||||
|
"recording_started": "Recording started: {name}",
|
||||||
|
"hints": "Keys: Q quit | R reset tracker | S start record | E stop record | ESC fullscreen",
|
||||||
|
"menu_help": "Help",
|
||||||
|
"menu_about": "About",
|
||||||
|
"about_title": "About Sensor Bench",
|
||||||
|
"about_body": "Sensor Bench Exhibition\nVersion: {app_version}\nOrisys: {orisys_version}\n\nOrisys-Tech",
|
||||||
|
"config_dir_tip": "Installed configs dir: {path}\nSelected: {selected}",
|
||||||
|
"config_dir_missing": "Installed Orisys configs directory not found. Use Config… to browse a local JSON."
|
||||||
|
},
|
||||||
|
"workflow": {
|
||||||
|
"title": "Sensor Bench Inspection Workflow",
|
||||||
|
"current_marker": "Current",
|
||||||
|
"done_marker": "Done",
|
||||||
|
"steps": {
|
||||||
|
"power_on": {
|
||||||
|
"title": "Power on and connect",
|
||||||
|
"body": "Connect the sensor USB cable and wait until Windows recognizes the device."
|
||||||
|
},
|
||||||
|
"select_camera": {
|
||||||
|
"title": "Start Orisys preview",
|
||||||
|
"body": "Confirm camera index (usually 0) and DDJX01 config, then click Start Preview. Format is fixed by the Orisys config (1920x1080 @ 30)."
|
||||||
|
},
|
||||||
|
"set_options": {
|
||||||
|
"title": "Set sensor options",
|
||||||
|
"body": "Choose elastomer type, CMOS type, and marker type that match the physical unit under test."
|
||||||
|
},
|
||||||
|
"check_preview": {
|
||||||
|
"title": "Check raw and processed preview",
|
||||||
|
"body": "Confirm raw frame is ~1920x1080 and processed Orisys image is ~365x365. Inspect focus, lighting, and image cleanliness."
|
||||||
|
},
|
||||||
|
"record": {
|
||||||
|
"title": "Record raw data",
|
||||||
|
"body": "Choose a storage folder, start recording, exercise the sensor, then stop recording."
|
||||||
|
},
|
||||||
|
"review_curves": {
|
||||||
|
"title": "Review performance curves",
|
||||||
|
"body": "Watch capture/render FPS and force curves for drops or unstable exposure before signing off."
|
||||||
|
},
|
||||||
|
"sensitivity": {
|
||||||
|
"title": "Sensitivity test",
|
||||||
|
"body": "Choose a small indenter and apply a 2g weight, then watch for a clear force-curve change."
|
||||||
|
},
|
||||||
|
"max_load": {
|
||||||
|
"title": "Maximum load test",
|
||||||
|
"body": "Choose a flat plate indenter and apply weights from 10g to 500g to watch for curve changes. Use 10g to 500g for MINI and 2kg for BOX."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
{
|
||||||
|
"app": {
|
||||||
|
"title": "传感器检测台",
|
||||||
|
"language": "语言",
|
||||||
|
"lang_zh": "中文",
|
||||||
|
"lang_en": "English"
|
||||||
|
},
|
||||||
|
"panels": {
|
||||||
|
"instructions": "操作步骤",
|
||||||
|
"options": "传感器组件类型",
|
||||||
|
"camera": "相机(Orisys SDK)",
|
||||||
|
"recording": "录制",
|
||||||
|
"raw": "原始图像(sensor.frame)",
|
||||||
|
"processed": "处理后图像(Sensor.get_img)",
|
||||||
|
"force": "力曲线"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"sensor_type": "传感器类型",
|
||||||
|
"elastomer": "弹性体",
|
||||||
|
"cmos": "CMOS",
|
||||||
|
"marker": "图案",
|
||||||
|
"indentor": "压头",
|
||||||
|
"platform": "平台",
|
||||||
|
"camera_index": "相机索引",
|
||||||
|
"backend": "后端",
|
||||||
|
"fixed_mode": "固定模式:{mode}",
|
||||||
|
"fixed_mode_unset": "固定模式:请设置配置路径",
|
||||||
|
"fixed_mode_invalid": "固定模式:配置无效({error})",
|
||||||
|
"requested": "请求",
|
||||||
|
"actual": "实际",
|
||||||
|
"capture_fps": "采集 FPS",
|
||||||
|
"render_fps": "渲染 FPS",
|
||||||
|
"proc_ms": "处理 ms",
|
||||||
|
"fn": "FN",
|
||||||
|
"fx": "FX",
|
||||||
|
"fy": "FY",
|
||||||
|
"no_preview": "暂无预览",
|
||||||
|
"processed_placeholder": "点击开始预览以显示 Orisys 输出",
|
||||||
|
"not_recording": "未在录制",
|
||||||
|
"force_plot": "力",
|
||||||
|
"config_dir_tip": "已安装配置目录:{path}\n当前:{selected}",
|
||||||
|
"config_dir_missing": "未找到已安装的 Orisys 配置目录。可用“配置…”浏览本地 JSON。"
|
||||||
|
},
|
||||||
|
"buttons": {
|
||||||
|
"config": "配置…",
|
||||||
|
"cal": "标定…",
|
||||||
|
"start_preview": "开始预览",
|
||||||
|
"stop_preview": "停止预览",
|
||||||
|
"browse": "浏览…",
|
||||||
|
"start_record": "开始录制",
|
||||||
|
"stop_record": "停止录制",
|
||||||
|
"next": "下一步",
|
||||||
|
"revert": "上一步",
|
||||||
|
"reset": "新样品 / 重置"
|
||||||
|
},
|
||||||
|
"placeholders": {
|
||||||
|
"config": "从已安装 Orisys 配置中选择,或浏览 JSON",
|
||||||
|
"cal": "可选标定文件 .npy"
|
||||||
|
},
|
||||||
|
"dialogs": {
|
||||||
|
"select_config": "选择配置 JSON",
|
||||||
|
"select_cal": "选择标定文件",
|
||||||
|
"select_recordings": "选择录制文件夹",
|
||||||
|
"orisys_title": "Orisys",
|
||||||
|
"orisys_required": "预览需要 Orisys SDK。\n\n{detail}",
|
||||||
|
"config_title": "配置",
|
||||||
|
"config_required": "请先选择 DDJX01 配置 JSON。",
|
||||||
|
"config_invalid": "无法加载配置。",
|
||||||
|
"capture_title": "采集",
|
||||||
|
"recording_title": "录制",
|
||||||
|
"no_frame": "尚无可用帧。"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"ready": "就绪 — 开始预览将使用 Orisys DDJX01 固定格式。",
|
||||||
|
"orisys_ok": "orisys 可用(版本 {version})",
|
||||||
|
"orisys_missing": "无法导入 orisys — 请将 orisys-sdk/src 加入 PYTHONPATH。",
|
||||||
|
"opening": "正在打开 Orisys({mode})…",
|
||||||
|
"preview_stopped": "预览已停止。",
|
||||||
|
"capture_error": "采集错误:{message}",
|
||||||
|
"frame_info": "{raw_w}x{raw_h} 原始 / {proc_w}x{proc_h} 处理 @ {fps:.1f} FPS",
|
||||||
|
"recording": "正在录制… {name}\n选项:传感器={sensor_type},弹性体={elastomer},CMOS={cmos},标记={marker},压头={indentor},平台={platform}",
|
||||||
|
"recording_frames": "正在录制… {name}\n帧数:{count}",
|
||||||
|
"saved": "已保存 {count} 帧 → {path}",
|
||||||
|
"workflow_done": "全部步骤已完成。可点击“新样品 / 重置”开始下一个样品。"
|
||||||
|
},
|
||||||
|
"exhibition": {
|
||||||
|
"title": "Sensor Bench 展示端",
|
||||||
|
"btn_search": "搜索",
|
||||||
|
"btn_camera": "摄像机",
|
||||||
|
"btn_config": "配置",
|
||||||
|
"btn_start": "启动",
|
||||||
|
"btn_starting": "启动中…",
|
||||||
|
"btn_stop": "暂停",
|
||||||
|
"btn_reset": "重置",
|
||||||
|
"btn_start_record": "开始录像",
|
||||||
|
"btn_stop_record": "结束录像",
|
||||||
|
"show_log": "显示日志面板",
|
||||||
|
"view": "画面",
|
||||||
|
"view_pure_color": "纯色背景箭头",
|
||||||
|
"view_captured": "采集图像箭头",
|
||||||
|
"view_processed": "处理后图像",
|
||||||
|
"view_raw": "原始图像",
|
||||||
|
"sensor_type": "传感器",
|
||||||
|
"sensor_orisys": "ORY-BOX/ORY-MINI",
|
||||||
|
"sensor_dd06": "ORY-X",
|
||||||
|
"sensor_switched": "已切换传感器类型:{sensor}",
|
||||||
|
"placeholder_camera": "摄像头编号或视频路径",
|
||||||
|
"dialog_video": "选择视频文件",
|
||||||
|
"camera_found": "已发现 {count} 路摄像机:{names}",
|
||||||
|
"camera_none": "未发现可用摄像机。可点击“搜索”重扫,或通过“摄像机”选择视频文件。",
|
||||||
|
"plot_fn": "法向力 FN",
|
||||||
|
"plot_fx": "切向力 FX",
|
||||||
|
"plot_fy": "切向力 FY",
|
||||||
|
"reset_done": "追踪器已重置。",
|
||||||
|
"reset_need_start": "请先启动传感器后再重置追踪器。",
|
||||||
|
"reset_failed": "重置失败:{message}",
|
||||||
|
"force_fn_n": "法向力(N)={value:.6f}",
|
||||||
|
"recording_started": "开始录像:{name}",
|
||||||
|
"hints": "按键:Q 退出 | R 重置追踪器 | S 开始录像 | E 结束录像 | ESC 全屏",
|
||||||
|
"menu_help": "帮助",
|
||||||
|
"menu_about": "关于",
|
||||||
|
"about_title": "关于 Sensor Bench",
|
||||||
|
"about_body": "Sensor Bench 展示端\n版本:{app_version}\nOrisys:{orisys_version}\n\nOrisys-Tech",
|
||||||
|
"config_dir_tip": "已安装配置目录:{path}\n当前:{selected}",
|
||||||
|
"config_dir_missing": "未找到已安装的 Orisys 配置目录。可用“配置…”浏览本地 JSON。"
|
||||||
|
},
|
||||||
|
"workflow": {
|
||||||
|
"title": "传感器检测流程",
|
||||||
|
"current_marker": "当前",
|
||||||
|
"done_marker": "已完成",
|
||||||
|
"steps": {
|
||||||
|
"power_on": {
|
||||||
|
"title": "开机并连接",
|
||||||
|
"body": "连接传感器 USB 线缆,等待 Windows 识别设备。"
|
||||||
|
},
|
||||||
|
"select_camera": {
|
||||||
|
"title": "启动 Orisys 预览",
|
||||||
|
"body": "确认相机索引(通常为 0)和 DDJX01 配置,然后点击开始预览。格式由 Orisys 配置固定(1920x1080 @ 30)。"
|
||||||
|
},
|
||||||
|
"set_options": {
|
||||||
|
"title": "设置传感器选项",
|
||||||
|
"body": "选择与当前被测实物一致的弹性体类型、CMOS 类型和标记类型。"
|
||||||
|
},
|
||||||
|
"check_preview": {
|
||||||
|
"title": "检查原始与处理后预览",
|
||||||
|
"body": "确认原始画面约为 1920x1080,处理后 Orisys 图像约为 365x365。检查对焦、光照和画面清洁度。"
|
||||||
|
},
|
||||||
|
"record": {
|
||||||
|
"title": "录制原始数据",
|
||||||
|
"body": "选择存储文件夹,开始录制,操作传感器,然后停止录制。"
|
||||||
|
},
|
||||||
|
"review_curves": {
|
||||||
|
"title": "查看性能曲线",
|
||||||
|
"body": "观察采集/渲染 FPS 与力曲线,确认无明显掉帧或曝光不稳定后再签字确认。"
|
||||||
|
},
|
||||||
|
"sensitivity": {
|
||||||
|
"title": "灵敏度测试",
|
||||||
|
"body": "选择小压头,并加载 2g 砝码,观察力曲线是否有清晰变化。"
|
||||||
|
},
|
||||||
|
"max_load": {
|
||||||
|
"title": "最大载荷测试",
|
||||||
|
"body": "选择平板压头,并加载 10g 到 500g 的砝码观察曲线变化;MINI 使用 10g 到 500g,BOX 使用 2kg。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
@echo off
|
||||||
|
cd /d "%~dp0.."
|
||||||
|
python "%~dp0run_operator.py" %*
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Launch the private Sensor Bench operator workflow UI.
|
||||||
|
|
||||||
|
Usage (from the repository root or this folder):
|
||||||
|
python internal/run_operator.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
os.environ.setdefault("OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS", "0")
|
||||||
|
|
||||||
|
INTERNAL = Path(__file__).resolve().parent
|
||||||
|
REPO = INTERNAL.parent
|
||||||
|
PUBLIC_SRC = REPO / "src"
|
||||||
|
OPERATOR_SRC = INTERNAL / "src"
|
||||||
|
|
||||||
|
for path in (OPERATOR_SRC, PUBLIC_SRC):
|
||||||
|
if str(path) not in sys.path:
|
||||||
|
sys.path.insert(0, str(path))
|
||||||
|
|
||||||
|
# Prefer sibling orisys-sdk checkout for Sensor-owned preview.
|
||||||
|
ORISYS_SRC = REPO.parent / "orisys-sdk" / "src"
|
||||||
|
if ORISYS_SRC.is_dir() and str(ORISYS_SRC) not in sys.path:
|
||||||
|
sys.path.insert(0, str(ORISYS_SRC))
|
||||||
|
|
||||||
|
from sensor_bench_operator.__main__ import main # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
"""Private Sensor Bench operator workflow UI (internal use only)."""
|
||||||
|
|
||||||
|
from .app import MainWindow
|
||||||
|
|
||||||
|
__all__ = ["MainWindow"]
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
"""python -m sensor_bench_operator"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
os.environ.setdefault("OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS", "0")
|
||||||
|
|
||||||
|
from PyQt5.QtWidgets import QApplication
|
||||||
|
|
||||||
|
from .app import MainWindow
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
args = list(sys.argv if argv is None else argv)
|
||||||
|
qt_app = QApplication(args)
|
||||||
|
qt_app.setApplicationName("Sensor Bench Operator")
|
||||||
|
qt_app.setOrganizationName("Orisys-Tech")
|
||||||
|
window = MainWindow()
|
||||||
|
window.show()
|
||||||
|
return qt_app.exec_()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Main Sensor Bench window: SDK-owned raw/processed preview, recording, curves."""
|
"""Internal operator workflow window (private; not part of public Sensor Bench)."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -37,26 +37,28 @@ except Exception as exc: # pragma: no cover
|
|||||||
"PyQtGraph is required. Install with: pip install pyqt5 pyqtgraph"
|
"PyQtGraph is required. Install with: pip install pyqt5 pyqtgraph"
|
||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
from .acquisition import (
|
from sensor_bench.acquisition import (
|
||||||
FixedCameraMode,
|
FixedCameraMode,
|
||||||
SdkAcquisitionWorker,
|
SdkAcquisitionWorker,
|
||||||
SdkPacket,
|
SdkPacket,
|
||||||
load_fixed_mode,
|
load_fixed_mode,
|
||||||
orisys_importable,
|
orisys_importable,
|
||||||
)
|
)
|
||||||
from .config_loader import SensorOptions, WorkflowSteps, load_sensor_options, load_workflow_steps
|
from sensor_bench.config_loader import SensorOptions, WorkflowSteps, load_sensor_options, load_workflow_steps
|
||||||
from .i18n import DEFAULT_LOCALE, Translator
|
from sensor_bench.i18n import DEFAULT_LOCALE, Translator
|
||||||
from .paths import (
|
from sensor_bench.paths import (
|
||||||
|
config_display_name,
|
||||||
default_ddjx01_calibration,
|
default_ddjx01_calibration,
|
||||||
default_ddjx01_config,
|
default_ddjx01_config,
|
||||||
default_locales_dir,
|
|
||||||
default_options_path,
|
|
||||||
default_recordings_dir,
|
default_recordings_dir,
|
||||||
default_steps_path,
|
list_orisys_configs,
|
||||||
|
orisys_configs_dir,
|
||||||
project_root,
|
project_root,
|
||||||
)
|
)
|
||||||
from .plot_history import MetricHistory
|
from sensor_bench.plot_history import MetricHistory
|
||||||
from .recorder import RawRecorder
|
from sensor_bench.recorder import RawRecorder
|
||||||
|
|
||||||
|
from .paths import default_locales_dir, default_options_path, default_steps_path
|
||||||
|
|
||||||
|
|
||||||
STYLE = """
|
STYLE = """
|
||||||
@@ -362,15 +364,15 @@ class MainWindow(QMainWindow):
|
|||||||
layout.addLayout(cam_row)
|
layout.addLayout(cam_row)
|
||||||
|
|
||||||
cfg_row = QHBoxLayout()
|
cfg_row = QHBoxLayout()
|
||||||
self.config_edit = QLineEdit()
|
self.config_combo = QComboBox()
|
||||||
default_cfg = default_ddjx01_config()
|
self.config_combo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||||
self.config_edit.setText(str(default_cfg))
|
self.config_combo.currentIndexChanged.connect(self._refresh_fixed_mode)
|
||||||
self.config_edit.editingFinished.connect(self._refresh_fixed_mode)
|
cfg_row.addWidget(self.config_combo, 1)
|
||||||
cfg_row.addWidget(self.config_edit, 1)
|
|
||||||
self.btn_cfg = QPushButton()
|
self.btn_cfg = QPushButton()
|
||||||
self.btn_cfg.clicked.connect(self._on_browse_config)
|
self.btn_cfg.clicked.connect(self._on_browse_config)
|
||||||
cfg_row.addWidget(self.btn_cfg)
|
cfg_row.addWidget(self.btn_cfg)
|
||||||
layout.addLayout(cfg_row)
|
layout.addLayout(cfg_row)
|
||||||
|
self._populate_config_combo(preferred=default_ddjx01_config())
|
||||||
|
|
||||||
cal_row = QHBoxLayout()
|
cal_row = QHBoxLayout()
|
||||||
self.cal_edit = QLineEdit()
|
self.cal_edit = QLineEdit()
|
||||||
@@ -615,8 +617,9 @@ class MainWindow(QMainWindow):
|
|||||||
self.btn_revert.setText(self.t("buttons.revert"))
|
self.btn_revert.setText(self.t("buttons.revert"))
|
||||||
self.btn_reset.setText(self.t("buttons.reset"))
|
self.btn_reset.setText(self.t("buttons.reset"))
|
||||||
|
|
||||||
self.config_edit.setPlaceholderText(self.t("placeholders.config"))
|
self.config_combo.setToolTip(self.t("placeholders.config"))
|
||||||
self.cal_edit.setPlaceholderText(self.t("placeholders.cal"))
|
self.cal_edit.setPlaceholderText(self.t("placeholders.cal"))
|
||||||
|
self._update_config_tooltip()
|
||||||
if self._raw_pixmap is None:
|
if self._raw_pixmap is None:
|
||||||
self.preview.setText(self.t("labels.no_preview"))
|
self.preview.setText(self.t("labels.no_preview"))
|
||||||
if self._proc_pixmap is None:
|
if self._proc_pixmap is None:
|
||||||
@@ -708,17 +711,75 @@ class MainWindow(QMainWindow):
|
|||||||
"platform": str(self.platform_combo.currentData() or ""),
|
"platform": str(self.platform_combo.currentData() or ""),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def _selected_config(self) -> str:
|
||||||
|
data = self.config_combo.currentData()
|
||||||
|
if data:
|
||||||
|
return str(data)
|
||||||
|
return self.config_combo.currentText().strip()
|
||||||
|
|
||||||
|
def _set_config_selection(self, value: str) -> None:
|
||||||
|
value = str(value or "").strip()
|
||||||
|
if not value:
|
||||||
|
return
|
||||||
|
for index in range(self.config_combo.count()):
|
||||||
|
if str(self.config_combo.itemData(index) or "") == value:
|
||||||
|
self.config_combo.setCurrentIndex(index)
|
||||||
|
return
|
||||||
|
if Path(value).stem.lower() == "options" and Path(value).suffix.lower() == ".json":
|
||||||
|
return
|
||||||
|
self.config_combo.addItem(config_display_name(value), value)
|
||||||
|
self.config_combo.setCurrentIndex(self.config_combo.count() - 1)
|
||||||
|
|
||||||
|
def _populate_config_combo(self, *, preferred: str | None = None) -> None:
|
||||||
|
current = preferred or self._selected_config() or default_ddjx01_config()
|
||||||
|
custom_paths: list[str] = []
|
||||||
|
for index in range(self.config_combo.count()):
|
||||||
|
data = str(self.config_combo.itemData(index) or "")
|
||||||
|
if data and (Path(data).suffix.lower() == ".json" or "/" in data or "\\" in data):
|
||||||
|
if data not in custom_paths:
|
||||||
|
custom_paths.append(data)
|
||||||
|
|
||||||
|
self.config_combo.blockSignals(True)
|
||||||
|
self.config_combo.clear()
|
||||||
|
for entry in list_orisys_configs():
|
||||||
|
self.config_combo.addItem(config_display_name(entry), entry)
|
||||||
|
for path in custom_paths:
|
||||||
|
if any(str(self.config_combo.itemData(i) or "") == path for i in range(self.config_combo.count())):
|
||||||
|
continue
|
||||||
|
if Path(path).stem.lower() == "options":
|
||||||
|
continue
|
||||||
|
self.config_combo.addItem(config_display_name(path), path)
|
||||||
|
if self.config_combo.count() == 0 and current:
|
||||||
|
self.config_combo.addItem(config_display_name(current), current)
|
||||||
|
self._set_config_selection(current)
|
||||||
|
self.config_combo.blockSignals(False)
|
||||||
|
self._update_config_tooltip()
|
||||||
|
|
||||||
|
def _update_config_tooltip(self) -> None:
|
||||||
|
cfg_dir = orisys_configs_dir()
|
||||||
|
selected = self._selected_config()
|
||||||
|
if cfg_dir is not None:
|
||||||
|
tip = self.t("labels.config_dir_tip", path=cfg_dir, selected=selected)
|
||||||
|
else:
|
||||||
|
tip = self.t("labels.config_dir_missing")
|
||||||
|
self.config_combo.setToolTip(tip)
|
||||||
|
self.btn_cfg.setToolTip(tip)
|
||||||
|
|
||||||
def _refresh_orisys_status(self) -> None:
|
def _refresh_orisys_status(self) -> None:
|
||||||
ok, detail = orisys_importable()
|
ok, detail = orisys_importable()
|
||||||
if ok:
|
if ok:
|
||||||
self.sdk_status.setText(self.t("status.orisys_ok", version=detail))
|
self.sdk_status.setText(self.t("status.orisys_ok", version=detail))
|
||||||
self.btn_start.setEnabled(True)
|
self.btn_start.setEnabled(True)
|
||||||
|
self._populate_config_combo(preferred=self._selected_config() or default_ddjx01_config())
|
||||||
else:
|
else:
|
||||||
self.sdk_status.setText(self.t("status.orisys_missing"))
|
self.sdk_status.setText(self.t("status.orisys_missing"))
|
||||||
self.btn_start.setEnabled(False)
|
self.btn_start.setEnabled(False)
|
||||||
|
self._update_config_tooltip()
|
||||||
|
|
||||||
def _refresh_fixed_mode(self) -> None:
|
def _refresh_fixed_mode(self) -> None:
|
||||||
text = self.config_edit.text().strip()
|
if not hasattr(self, "mode_label"):
|
||||||
|
return
|
||||||
|
text = self._selected_config()
|
||||||
if not text:
|
if not text:
|
||||||
self._fixed_mode = None
|
self._fixed_mode = None
|
||||||
self.mode_label.setText(self.t("labels.fixed_mode_unset"))
|
self.mode_label.setText(self.t("labels.fixed_mode_unset"))
|
||||||
@@ -729,14 +790,16 @@ class MainWindow(QMainWindow):
|
|||||||
except Exception as exc: # noqa: BLE001
|
except Exception as exc: # noqa: BLE001
|
||||||
self._fixed_mode = None
|
self._fixed_mode = None
|
||||||
self.mode_label.setText(self.t("labels.fixed_mode_invalid", error=exc))
|
self.mode_label.setText(self.t("labels.fixed_mode_invalid", error=exc))
|
||||||
|
self._update_config_tooltip()
|
||||||
|
|
||||||
# -------------------------------------------------------------- capture
|
# -------------------------------------------------------------- capture
|
||||||
def _on_browse_config(self) -> None:
|
def _on_browse_config(self) -> None:
|
||||||
|
start_dir = orisys_configs_dir() or project_root()
|
||||||
path, _ = QFileDialog.getOpenFileName(
|
path, _ = QFileDialog.getOpenFileName(
|
||||||
self, self.t("dialogs.select_config"), str(project_root()), "JSON (*.json);;All (*)"
|
self, self.t("dialogs.select_config"), str(start_dir), "JSON (*.json);;All (*)"
|
||||||
)
|
)
|
||||||
if path:
|
if path:
|
||||||
self.config_edit.setText(path)
|
self._set_config_selection(path)
|
||||||
self._refresh_fixed_mode()
|
self._refresh_fixed_mode()
|
||||||
|
|
||||||
def _on_browse_cal(self) -> None:
|
def _on_browse_cal(self) -> None:
|
||||||
@@ -755,7 +818,7 @@ class MainWindow(QMainWindow):
|
|||||||
self.t("dialogs.orisys_required", detail=detail),
|
self.t("dialogs.orisys_required", detail=detail),
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
config = self.config_edit.text().strip()
|
config = self._selected_config()
|
||||||
if not config:
|
if not config:
|
||||||
QMessageBox.warning(
|
QMessageBox.warning(
|
||||||
self, self.t("dialogs.config_title"), self.t("dialogs.config_required")
|
self, self.t("dialogs.config_title"), self.t("dialogs.config_required")
|
||||||
@@ -791,7 +854,7 @@ class MainWindow(QMainWindow):
|
|||||||
self.btn_stop.setEnabled(True)
|
self.btn_stop.setEnabled(True)
|
||||||
self.btn_rec_start.setEnabled(False)
|
self.btn_rec_start.setEnabled(False)
|
||||||
self.camera_index.setEnabled(False)
|
self.camera_index.setEnabled(False)
|
||||||
self.config_edit.setEnabled(False)
|
self.config_combo.setEnabled(False)
|
||||||
self.cal_edit.setEnabled(False)
|
self.cal_edit.setEnabled(False)
|
||||||
self.backend_combo.setEnabled(False)
|
self.backend_combo.setEnabled(False)
|
||||||
self.statusBar().showMessage(self.t("status.opening", mode=mode.label()))
|
self.statusBar().showMessage(self.t("status.opening", mode=mode.label()))
|
||||||
@@ -807,7 +870,7 @@ class MainWindow(QMainWindow):
|
|||||||
self.btn_rec_start.setEnabled(False)
|
self.btn_rec_start.setEnabled(False)
|
||||||
self.btn_rec_stop.setEnabled(False)
|
self.btn_rec_stop.setEnabled(False)
|
||||||
self.camera_index.setEnabled(True)
|
self.camera_index.setEnabled(True)
|
||||||
self.config_edit.setEnabled(True)
|
self.config_combo.setEnabled(True)
|
||||||
self.cal_edit.setEnabled(True)
|
self.cal_edit.setEnabled(True)
|
||||||
self.backend_combo.setEnabled(True)
|
self.backend_combo.setEnabled(True)
|
||||||
self.statusBar().showMessage(self.t("status.preview_stopped"))
|
self.statusBar().showMessage(self.t("status.preview_stopped"))
|
||||||
@@ -839,7 +902,7 @@ class MainWindow(QMainWindow):
|
|||||||
self.btn_stop.setEnabled(False)
|
self.btn_stop.setEnabled(False)
|
||||||
self.btn_rec_start.setEnabled(False)
|
self.btn_rec_start.setEnabled(False)
|
||||||
self.camera_index.setEnabled(True)
|
self.camera_index.setEnabled(True)
|
||||||
self.config_edit.setEnabled(True)
|
self.config_combo.setEnabled(True)
|
||||||
self.cal_edit.setEnabled(True)
|
self.cal_edit.setEnabled(True)
|
||||||
self.backend_combo.setEnabled(True)
|
self.backend_combo.setEnabled(True)
|
||||||
QMessageBox.warning(self, self.t("dialogs.capture_title"), message)
|
QMessageBox.warning(self, self.t("dialogs.capture_title"), message)
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
"""Resolve internal operator resource paths (steps, options, locales)."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def is_frozen() -> bool:
|
||||||
|
return bool(getattr(sys, "frozen", False))
|
||||||
|
|
||||||
|
|
||||||
|
def app_root() -> Path:
|
||||||
|
"""Editable ``internal/`` directory, or frozen executable directory."""
|
||||||
|
if is_frozen():
|
||||||
|
return Path(sys.executable).resolve().parent
|
||||||
|
return Path(__file__).resolve().parents[2]
|
||||||
|
|
||||||
|
|
||||||
|
def resource_root() -> Path:
|
||||||
|
"""Bundled resource root for editable and PyInstaller runs."""
|
||||||
|
if is_frozen():
|
||||||
|
return Path(getattr(sys, "_MEIPASS", app_root())).resolve()
|
||||||
|
return app_root()
|
||||||
|
|
||||||
|
|
||||||
|
def operator_root() -> Path:
|
||||||
|
"""Compatibility alias for ``app_root()``."""
|
||||||
|
return app_root()
|
||||||
|
|
||||||
|
|
||||||
|
def default_options_path() -> Path:
|
||||||
|
external = app_root() / "config" / "options.json"
|
||||||
|
if external.exists():
|
||||||
|
return external
|
||||||
|
return resource_root() / "config" / "options.json"
|
||||||
|
|
||||||
|
|
||||||
|
def default_steps_path() -> Path:
|
||||||
|
external = app_root() / "steps" / "default.json"
|
||||||
|
if external.exists():
|
||||||
|
return external
|
||||||
|
return resource_root() / "steps" / "default.json"
|
||||||
|
|
||||||
|
|
||||||
|
def default_locales_dir() -> Path:
|
||||||
|
external = app_root() / "config" / "locales"
|
||||||
|
if external.exists():
|
||||||
|
return external
|
||||||
|
return resource_root() / "config" / "locales"
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
"""Smoke tests for the internal Sensor Bench operator UI."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(ROOT / "src"))
|
||||||
|
sys.path.insert(0, str(ROOT / "internal" / "src"))
|
||||||
|
|
||||||
|
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||||
|
|
||||||
|
|
||||||
|
def test_mainwindow_offscreen() -> None:
|
||||||
|
from PyQt5.QtWidgets import QApplication
|
||||||
|
from sensor_bench.acquisition import FixedCameraMode
|
||||||
|
from sensor_bench.i18n import DEFAULT_LOCALE
|
||||||
|
from sensor_bench_operator.app import MainWindow
|
||||||
|
|
||||||
|
app = QApplication.instance() or QApplication([])
|
||||||
|
win = MainWindow()
|
||||||
|
assert win.tr.locale == DEFAULT_LOCALE == "zh_CN"
|
||||||
|
assert "传感器" in win.windowTitle()
|
||||||
|
assert win.btn_start.text() == "开始预览"
|
||||||
|
assert win.camera_index.value() == 0
|
||||||
|
assert win.config_combo is not None
|
||||||
|
assert win.config_combo.count() >= 1
|
||||||
|
assert win.btn_next is not None
|
||||||
|
assert win.btn_revert is not None
|
||||||
|
assert win.btn_reset is not None
|
||||||
|
assert win.sensor_type_combo.count() == 2
|
||||||
|
assert win.indentor_combo.count() == 2
|
||||||
|
assert win.platform_combo.count() == 3
|
||||||
|
assert win.selected_sensor_options()["sensor_type"] == "BOX"
|
||||||
|
assert win.selected_sensor_options()["indentor"] == "1mm_circle"
|
||||||
|
assert win.selected_sensor_options()["platform"] == "manual"
|
||||||
|
assert not hasattr(win, "resolution_combo")
|
||||||
|
assert not hasattr(win, "btn_search")
|
||||||
|
assert win._current_step_index == 0
|
||||||
|
assert not win.btn_revert.isEnabled()
|
||||||
|
assert win.btn_next.isEnabled()
|
||||||
|
|
||||||
|
# Language switch refreshes labels without restart.
|
||||||
|
win.language_combo.setCurrentIndex(1)
|
||||||
|
assert win.tr.locale == "en_US"
|
||||||
|
assert win.windowTitle() == "Sensor Bench"
|
||||||
|
assert win.btn_start.text() == "Start Preview"
|
||||||
|
|
||||||
|
# Workflow next / revert / reset.
|
||||||
|
total = len(win.workflow.steps)
|
||||||
|
win._on_step_next()
|
||||||
|
assert win._current_step_index == 1
|
||||||
|
assert win.btn_revert.isEnabled()
|
||||||
|
win._on_step_revert()
|
||||||
|
assert win._current_step_index == 0
|
||||||
|
for _ in range(total):
|
||||||
|
win._on_step_next()
|
||||||
|
assert win._current_step_index == total
|
||||||
|
assert not win.btn_next.isEnabled()
|
||||||
|
assert "completed" in win.workflow_status.text().lower() or "完成" in win.workflow_status.text()
|
||||||
|
win._on_step_reset()
|
||||||
|
assert win._current_step_index == 0
|
||||||
|
assert win.btn_next.isEnabled()
|
||||||
|
|
||||||
|
win._fixed_mode = FixedCameraMode(
|
||||||
|
width=1920,
|
||||||
|
height=1080,
|
||||||
|
fps=30.0,
|
||||||
|
config_ref=str(ROOT / "tests" / "fixtures" / "ddjx01_min.json"),
|
||||||
|
stitch_width=365,
|
||||||
|
stitch_height=365,
|
||||||
|
)
|
||||||
|
win.mode_label.setText(win.t("labels.fixed_mode", mode=win._fixed_mode.label()))
|
||||||
|
assert "1920x1080" in win.mode_label.text()
|
||||||
|
win.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_mainwindow_offscreen()
|
||||||
|
print("ok")
|
||||||
+1
-1
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
[project]
|
[project]
|
||||||
name = "sensor-bench"
|
name = "sensor-bench"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "Operator tool for connecting sensors, viewing raw data, recording, and inspecting performance"
|
description = "Public exhibition desktop app for Orisys tactile sensor preview, recording, and force plots"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
authors = [{ name = "Orisys-Tech" }]
|
authors = [{ name = "Orisys-Tech" }]
|
||||||
|
|||||||
@@ -1,15 +1,22 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Launch Sensor Bench without installing the package.
|
"""Launch the public Sensor Bench exhibition app without installing the package.
|
||||||
|
|
||||||
Usage (from the repository root):
|
Usage (from the repository root):
|
||||||
python run.py
|
python run.py
|
||||||
|
|
||||||
|
Private operator UI:
|
||||||
|
python internal/run_operator.py
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Must be set before importing OpenCV / sensor_bench modules that import cv2.
|
||||||
|
os.environ.setdefault("OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS", "0")
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parent
|
ROOT = Path(__file__).resolve().parent
|
||||||
SRC = ROOT / "src"
|
SRC = ROOT / "src"
|
||||||
if str(SRC) not in sys.path:
|
if str(SRC) not in sys.path:
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
"""Sensor Bench: operator tooling for raw sensor inspection and recording."""
|
"""Sensor Bench: public exhibition desktop app for Orisys tactile sensors."""
|
||||||
|
|
||||||
__version__ = "0.1.0"
|
__version__ = "0.1.0"
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ from typing import Callable, Optional
|
|||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
from .force_calibration import FORCE_SAMPLE_RATE_HZ, LOWPASS_CUTOFF_HZ
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class FixedCameraMode:
|
class FixedCameraMode:
|
||||||
@@ -51,6 +53,7 @@ class SdkPacket:
|
|||||||
shear_y: float = 0.0
|
shear_y: float = 0.0
|
||||||
session_id: int = 0
|
session_id: int = 0
|
||||||
mode: Optional[FixedCameraMode] = None
|
mode: Optional[FixedCameraMode] = None
|
||||||
|
flow: Optional[np.ndarray] = None
|
||||||
|
|
||||||
|
|
||||||
def orisys_importable() -> tuple[bool, str]:
|
def orisys_importable() -> tuple[bool, str]:
|
||||||
@@ -104,6 +107,37 @@ def _ensure_bgr(img: np.ndarray) -> np.ndarray:
|
|||||||
return img
|
return img
|
||||||
|
|
||||||
|
|
||||||
|
ARROW_BG_BGR = (0x16, 0x13, 0x12) # #121316 as BGR
|
||||||
|
_ARROW_DRAW_KW = dict(
|
||||||
|
threshold=2,
|
||||||
|
grid_spacing=20,
|
||||||
|
arrow_scale=0.5,
|
||||||
|
below_threshold_color=(160, 160, 160),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def draw_arrow_display(
|
||||||
|
processed_frame: np.ndarray,
|
||||||
|
flow: Optional[np.ndarray],
|
||||||
|
*,
|
||||||
|
background: str = "pure_color",
|
||||||
|
) -> np.ndarray:
|
||||||
|
"""Build exhibition display: arrows on solid bg or over the processed frame."""
|
||||||
|
base = _ensure_bgr(processed_frame)
|
||||||
|
if flow is None:
|
||||||
|
return base
|
||||||
|
try:
|
||||||
|
import orisys
|
||||||
|
|
||||||
|
if background == "pure_color":
|
||||||
|
canvas = np.empty_like(base)
|
||||||
|
canvas[:, :] = ARROW_BG_BGR
|
||||||
|
return orisys.util.draw_arrows(canvas, flow, **_ARROW_DRAW_KW)
|
||||||
|
return orisys.util.draw_arrows(base, flow, **_ARROW_DRAW_KW)
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
return base
|
||||||
|
|
||||||
|
|
||||||
class SdkAcquisitionWorker:
|
class SdkAcquisitionWorker:
|
||||||
"""Background loop: ``Sensor.get_img()`` + deformation + contact."""
|
"""Background loop: ``Sensor.get_img()`` + deformation + contact."""
|
||||||
|
|
||||||
@@ -189,6 +223,14 @@ class SdkAcquisitionWorker:
|
|||||||
with self._lock:
|
with self._lock:
|
||||||
self._latest = None
|
self._latest = None
|
||||||
|
|
||||||
|
def reset(self) -> None:
|
||||||
|
"""Reset the live Orisys sensor tracker (baseline), if running."""
|
||||||
|
with self._lock:
|
||||||
|
sensor = self._sensor
|
||||||
|
if sensor is None:
|
||||||
|
raise RuntimeError("Sensor is not running")
|
||||||
|
sensor.reset()
|
||||||
|
|
||||||
def _close_sensor(self) -> None:
|
def _close_sensor(self) -> None:
|
||||||
sensor = self._sensor
|
sensor = self._sensor
|
||||||
self._sensor = None
|
self._sensor = None
|
||||||
@@ -220,6 +262,10 @@ class SdkAcquisitionWorker:
|
|||||||
if self._cal_path:
|
if self._cal_path:
|
||||||
kwargs["cal_path"] = self._cal_path
|
kwargs["cal_path"] = self._cal_path
|
||||||
self._sensor = orisys.Sensor(source, **kwargs)
|
self._sensor = orisys.Sensor(source, **kwargs)
|
||||||
|
self._sensor.configure(
|
||||||
|
lowpass_cutoff_hz=LOWPASS_CUTOFF_HZ,
|
||||||
|
lowpass_sample_rate_hz=FORCE_SAMPLE_RATE_HZ,
|
||||||
|
)
|
||||||
self._status(f"Streaming {mode.label()}")
|
self._status(f"Streaming {mode.label()}")
|
||||||
except Exception as exc: # noqa: BLE001
|
except Exception as exc: # noqa: BLE001
|
||||||
if self._on_error is not None and session == self._session_id:
|
if self._on_error is not None and session == self._session_id:
|
||||||
@@ -252,14 +298,23 @@ class SdkAcquisitionWorker:
|
|||||||
fps = float(len(frame_times)) if frame_times else 0.0
|
fps = float(len(frame_times)) if frame_times else 0.0
|
||||||
|
|
||||||
fn = fx = fy = 0.0
|
fn = fx = fy = 0.0
|
||||||
|
flow = None
|
||||||
try:
|
try:
|
||||||
fn, fx, fy = self._sensor.read_info(
|
fn, fx, fy, flow = self._sensor.read_info(
|
||||||
self._sensor.info.FNORMAL,
|
self._sensor.info.FNORMAL,
|
||||||
self._sensor.info.FSHEARX,
|
self._sensor.info.FSHEARX,
|
||||||
self._sensor.info.FSHEARY,
|
self._sensor.info.FSHEARY,
|
||||||
|
self._sensor.info.VRAW,
|
||||||
)
|
)
|
||||||
except Exception: # noqa: BLE001
|
except Exception: # noqa: BLE001
|
||||||
pass
|
try:
|
||||||
|
fn, fx, fy = self._sensor.read_info(
|
||||||
|
self._sensor.info.FNORMAL,
|
||||||
|
self._sensor.info.FSHEARX,
|
||||||
|
self._sensor.info.FSHEARY,
|
||||||
|
)
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
pass
|
||||||
|
|
||||||
raw_bgr = _ensure_bgr(raw)
|
raw_bgr = _ensure_bgr(raw)
|
||||||
proc_bgr = _ensure_bgr(processed)
|
proc_bgr = _ensure_bgr(processed)
|
||||||
@@ -278,6 +333,7 @@ class SdkAcquisitionWorker:
|
|||||||
shear_y=float(fy),
|
shear_y=float(fy),
|
||||||
session_id=session,
|
session_id=session,
|
||||||
mode=mode,
|
mode=mode,
|
||||||
|
flow=flow,
|
||||||
)
|
)
|
||||||
with self._lock:
|
with self._lock:
|
||||||
self._latest = packet
|
self._latest = packet
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
"""Discover USB cameras for Sensor Bench (Windows DSHOW/MSMF, Linux V4L2)."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
|
||||||
|
DEFAULT_MAX_CAMERA_INDEX = 10
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class CameraInfo:
|
||||||
|
index: int
|
||||||
|
backend_name: str
|
||||||
|
backend_api: int
|
||||||
|
width: int
|
||||||
|
height: int
|
||||||
|
|
||||||
|
@property
|
||||||
|
def source(self) -> str:
|
||||||
|
return str(self.index)
|
||||||
|
|
||||||
|
def label(self) -> str:
|
||||||
|
return f"{self.index} ({self.width}x{self.height}) [{self.backend_name}]"
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def _suppress_opencv_noise():
|
||||||
|
"""Hide OpenCV VideoCapture probe warnings (stderr + OpenCV log)."""
|
||||||
|
prev_level = None
|
||||||
|
set_level = None
|
||||||
|
try:
|
||||||
|
logging = getattr(getattr(cv2, "utils", None), "logging", None)
|
||||||
|
if logging is not None:
|
||||||
|
set_level = getattr(logging, "setLogLevel", None)
|
||||||
|
get_level = getattr(logging, "getLogLevel", None)
|
||||||
|
silent = getattr(logging, "LOG_LEVEL_SILENT", 0)
|
||||||
|
if set_level is not None:
|
||||||
|
if get_level is not None:
|
||||||
|
prev_level = get_level()
|
||||||
|
set_level(silent)
|
||||||
|
except Exception:
|
||||||
|
prev_level = None
|
||||||
|
|
||||||
|
devnull = open(os.devnull, "w")
|
||||||
|
try:
|
||||||
|
with contextlib.redirect_stderr(devnull):
|
||||||
|
stderr_fd = sys.stderr.fileno()
|
||||||
|
saved_fd = os.dup(stderr_fd)
|
||||||
|
try:
|
||||||
|
os.dup2(devnull.fileno(), stderr_fd)
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
os.dup2(saved_fd, stderr_fd)
|
||||||
|
os.close(saved_fd)
|
||||||
|
finally:
|
||||||
|
devnull.close()
|
||||||
|
if set_level is not None and prev_level is not None:
|
||||||
|
try:
|
||||||
|
set_level(prev_level)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _backend_candidates() -> list[tuple[str, int]]:
|
||||||
|
candidates: list[tuple[str, int]] = []
|
||||||
|
if sys.platform == "win32":
|
||||||
|
if hasattr(cv2, "CAP_DSHOW"):
|
||||||
|
candidates.append(("DSHOW", cv2.CAP_DSHOW))
|
||||||
|
if hasattr(cv2, "CAP_MSMF"):
|
||||||
|
candidates.append(("MSMF", cv2.CAP_MSMF))
|
||||||
|
elif sys.platform.startswith("linux"):
|
||||||
|
if hasattr(cv2, "CAP_V4L2"):
|
||||||
|
candidates.append(("V4L2", cv2.CAP_V4L2))
|
||||||
|
if not candidates:
|
||||||
|
candidates.append(("ANY", cv2.CAP_ANY))
|
||||||
|
return candidates
|
||||||
|
|
||||||
|
|
||||||
|
def _try_open(index: int, backend_name: str, backend_api: int) -> CameraInfo | None:
|
||||||
|
cap = cv2.VideoCapture(index, backend_api)
|
||||||
|
if not cap.isOpened():
|
||||||
|
cap.release()
|
||||||
|
return None
|
||||||
|
ok, frame = cap.read()
|
||||||
|
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH) or 0)
|
||||||
|
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT) or 0)
|
||||||
|
if not ok or frame is None:
|
||||||
|
if width <= 0 or height <= 0:
|
||||||
|
cap.release()
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
h, w = frame.shape[:2]
|
||||||
|
width = width or int(w)
|
||||||
|
height = height or int(h)
|
||||||
|
cap.release()
|
||||||
|
return CameraInfo(
|
||||||
|
index=index,
|
||||||
|
backend_name=backend_name,
|
||||||
|
backend_api=backend_api,
|
||||||
|
width=width,
|
||||||
|
height=height,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def list_cameras(max_index: int = DEFAULT_MAX_CAMERA_INDEX) -> list[CameraInfo]:
|
||||||
|
"""Probe indices ``0 .. max_index`` and return working cameras."""
|
||||||
|
found: list[CameraInfo] = []
|
||||||
|
seen_indices: set[int] = set()
|
||||||
|
backends = _backend_candidates()
|
||||||
|
|
||||||
|
with _suppress_opencv_noise():
|
||||||
|
for index in range(0, max_index + 1):
|
||||||
|
for backend_name, backend_api in backends:
|
||||||
|
info = _try_open(index, backend_name, backend_api)
|
||||||
|
if info is None:
|
||||||
|
continue
|
||||||
|
if index in seen_indices:
|
||||||
|
break
|
||||||
|
found.append(info)
|
||||||
|
seen_indices.add(index)
|
||||||
|
break
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
def pick_default_camera(cameras: list[CameraInfo], preferred: str | None = None) -> str | None:
|
||||||
|
if not cameras:
|
||||||
|
return None
|
||||||
|
preferred = (preferred or "").strip()
|
||||||
|
if preferred:
|
||||||
|
for cam in cameras:
|
||||||
|
if cam.source == preferred:
|
||||||
|
return cam.source
|
||||||
|
return cameras[0].source
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
"""DD06 acquisition: external capture + ROI rectify + already_processed Sensor."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Callable, Optional
|
||||||
|
|
||||||
|
from .acquisition import FixedCameraMode, SdkPacket, _ensure_bgr, load_fixed_mode, orisys_importable
|
||||||
|
from .force_calibration import FORCE_SAMPLE_RATE_HZ, LOWPASS_CUTOFF_HZ
|
||||||
|
from .roi_geometry import (
|
||||||
|
build_roi_warp,
|
||||||
|
load_json_config,
|
||||||
|
load_roi_polygon,
|
||||||
|
open_video_source,
|
||||||
|
rectified_output_size,
|
||||||
|
warp_roi_to_rectangle,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Dd06AcquisitionWorker:
|
||||||
|
"""Background loop for DD06 polygon-ROI rectification pipeline."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
on_error: Optional[Callable[[str], None]] = None,
|
||||||
|
on_status: Optional[Callable[[str], None]] = None,
|
||||||
|
):
|
||||||
|
self._on_error = on_error
|
||||||
|
self._on_status = on_status
|
||||||
|
self._thread: Optional[threading.Thread] = None
|
||||||
|
self._stop = threading.Event()
|
||||||
|
self._lock = threading.Lock()
|
||||||
|
self._latest: Optional[SdkPacket] = None
|
||||||
|
self._session_id = 0
|
||||||
|
self._sensor = None
|
||||||
|
self._cap = None
|
||||||
|
self._mode: Optional[FixedCameraMode] = None
|
||||||
|
self._video_source: Optional[str | int] = None
|
||||||
|
self._config_ref: Optional[str] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_running(self) -> bool:
|
||||||
|
return self._thread is not None and self._thread.is_alive()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def session_id(self) -> int:
|
||||||
|
return self._session_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mode(self) -> Optional[FixedCameraMode]:
|
||||||
|
return self._mode
|
||||||
|
|
||||||
|
def latest(self) -> Optional[SdkPacket]:
|
||||||
|
with self._lock:
|
||||||
|
return self._latest
|
||||||
|
|
||||||
|
def start(
|
||||||
|
self,
|
||||||
|
video_source: str | int = 0,
|
||||||
|
*,
|
||||||
|
config_path: str | Path,
|
||||||
|
) -> FixedCameraMode:
|
||||||
|
self.stop()
|
||||||
|
ok, detail = orisys_importable()
|
||||||
|
if not ok:
|
||||||
|
raise RuntimeError(
|
||||||
|
"orisys SDK is not importable. Install orisys and ensure it is available. "
|
||||||
|
f"Detail: {detail}"
|
||||||
|
)
|
||||||
|
mode = load_fixed_mode(config_path)
|
||||||
|
self._mode = mode
|
||||||
|
self._video_source = video_source
|
||||||
|
self._config_ref = str(config_path).strip()
|
||||||
|
self._latest = None
|
||||||
|
self._session_id += 1
|
||||||
|
session = self._session_id
|
||||||
|
self._stop.clear()
|
||||||
|
self._thread = threading.Thread(
|
||||||
|
target=self._run,
|
||||||
|
name=f"Dd06Acquisition-{session}",
|
||||||
|
daemon=True,
|
||||||
|
args=(session,),
|
||||||
|
)
|
||||||
|
self._thread.start()
|
||||||
|
return self._mode
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
self._stop.set()
|
||||||
|
thread = self._thread
|
||||||
|
if thread is not None and thread.is_alive():
|
||||||
|
thread.join(timeout=5.0)
|
||||||
|
self._thread = None
|
||||||
|
self._close()
|
||||||
|
with self._lock:
|
||||||
|
self._latest = None
|
||||||
|
|
||||||
|
def reset(self) -> None:
|
||||||
|
"""Reset the live Orisys sensor tracker (baseline), if running."""
|
||||||
|
with self._lock:
|
||||||
|
sensor = self._sensor
|
||||||
|
if sensor is None:
|
||||||
|
raise RuntimeError("Sensor is not running")
|
||||||
|
sensor.reset()
|
||||||
|
|
||||||
|
def _close(self) -> None:
|
||||||
|
cap = self._cap
|
||||||
|
self._cap = None
|
||||||
|
if cap is not None:
|
||||||
|
try:
|
||||||
|
cap.release()
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
pass
|
||||||
|
sensor = self._sensor
|
||||||
|
self._sensor = None
|
||||||
|
if sensor is not None:
|
||||||
|
try:
|
||||||
|
sensor.disconnect()
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _status(self, message: str) -> None:
|
||||||
|
if self._on_status is not None:
|
||||||
|
self._on_status(message)
|
||||||
|
|
||||||
|
def _run(self, session: int) -> None:
|
||||||
|
import orisys
|
||||||
|
|
||||||
|
mode = self._mode
|
||||||
|
source = self._video_source
|
||||||
|
config_ref = self._config_ref
|
||||||
|
if mode is None or source is None or config_ref is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self._status(f"Opening DD06 pipeline ({mode.width}x{mode.height})…")
|
||||||
|
config = load_json_config(config_ref)
|
||||||
|
roi_points, roi_image_size = load_roi_polygon(config)
|
||||||
|
out_size = rectified_output_size(config, roi_points)
|
||||||
|
self._sensor = orisys.Sensor("external", config_name=config_ref, verbose=False)
|
||||||
|
self._sensor.configure(
|
||||||
|
lowpass_cutoff_hz=LOWPASS_CUTOFF_HZ,
|
||||||
|
lowpass_sample_rate_hz=FORCE_SAMPLE_RATE_HZ,
|
||||||
|
)
|
||||||
|
self._cap, resolved = open_video_source(source)
|
||||||
|
warp_matrix = None
|
||||||
|
last_frame_size = None
|
||||||
|
self._status(f"Streaming DD06 ROI {out_size[0]}x{out_size[1]} from {resolved}")
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
if self._on_error is not None and session == self._session_id:
|
||||||
|
self._on_error(str(exc))
|
||||||
|
self._close()
|
||||||
|
return
|
||||||
|
|
||||||
|
frame_times: list[float] = []
|
||||||
|
while not self._stop.is_set() and session == self._session_id:
|
||||||
|
try:
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
ok, frame = self._cap.read()
|
||||||
|
if not ok or frame is None:
|
||||||
|
time.sleep(0.005)
|
||||||
|
continue
|
||||||
|
|
||||||
|
frame_h, frame_w = frame.shape[:2]
|
||||||
|
frame_size = (frame_w, frame_h)
|
||||||
|
if warp_matrix is None or last_frame_size != frame_size:
|
||||||
|
warp_matrix, _, _ = build_roi_warp(
|
||||||
|
roi_points,
|
||||||
|
roi_image_size,
|
||||||
|
frame_size,
|
||||||
|
out_size,
|
||||||
|
)
|
||||||
|
last_frame_size = frame_size
|
||||||
|
|
||||||
|
rectified = warp_roi_to_rectangle(frame, warp_matrix, out_size)
|
||||||
|
processed = self._sensor.get_img(frame=rectified, already_processed=True)
|
||||||
|
if processed is None:
|
||||||
|
processed = rectified
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._sensor.compute_deformation(decompose=True, check_motion=False)
|
||||||
|
self._sensor.compute_contact()
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
pass
|
||||||
|
|
||||||
|
t1 = time.perf_counter()
|
||||||
|
frame_times.append(t1)
|
||||||
|
cutoff = t1 - 1.0
|
||||||
|
frame_times = [t for t in frame_times if t >= cutoff]
|
||||||
|
fps = float(len(frame_times)) if frame_times else 0.0
|
||||||
|
|
||||||
|
fn = fx = fy = 0.0
|
||||||
|
flow = None
|
||||||
|
try:
|
||||||
|
fn, fx, fy, flow = self._sensor.read_info(
|
||||||
|
self._sensor.info.FNORMAL,
|
||||||
|
self._sensor.info.FSHEARX,
|
||||||
|
self._sensor.info.FSHEARY,
|
||||||
|
self._sensor.info.VRAW,
|
||||||
|
)
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
try:
|
||||||
|
fn, fx, fy = self._sensor.read_info(
|
||||||
|
self._sensor.info.FNORMAL,
|
||||||
|
self._sensor.info.FSHEARX,
|
||||||
|
self._sensor.info.FSHEARY,
|
||||||
|
)
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
pass
|
||||||
|
|
||||||
|
raw_bgr = _ensure_bgr(frame)
|
||||||
|
# Prefer sensor.img / get_img result for processed visualization.
|
||||||
|
proc_src = getattr(self._sensor, "img", None)
|
||||||
|
if proc_src is None:
|
||||||
|
proc_src = processed
|
||||||
|
proc_bgr = _ensure_bgr(proc_src)
|
||||||
|
packet = SdkPacket(
|
||||||
|
raw_frame=raw_bgr,
|
||||||
|
processed_frame=proc_bgr,
|
||||||
|
timestamp=time.time(),
|
||||||
|
fps=fps,
|
||||||
|
process_ms=(t1 - t0) * 1000.0,
|
||||||
|
width=int(raw_bgr.shape[1]),
|
||||||
|
height=int(raw_bgr.shape[0]),
|
||||||
|
processed_width=int(proc_bgr.shape[1]),
|
||||||
|
processed_height=int(proc_bgr.shape[0]),
|
||||||
|
normal_force=float(fn),
|
||||||
|
shear_x=float(fx),
|
||||||
|
shear_y=float(fy),
|
||||||
|
session_id=session,
|
||||||
|
mode=mode,
|
||||||
|
flow=flow,
|
||||||
|
)
|
||||||
|
with self._lock:
|
||||||
|
self._latest = packet
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
if self._on_error is not None and session == self._session_id:
|
||||||
|
self._on_error(str(exc))
|
||||||
|
break
|
||||||
|
|
||||||
|
self._close()
|
||||||
|
self._status("Acquisition stopped")
|
||||||
@@ -0,0 +1,910 @@
|
|||||||
|
"""Exhibition-friendly Sensor Bench window (qt_viewer_2-style layout)."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import time
|
||||||
|
from importlib.metadata import PackageNotFoundError, version as pkg_version
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, Protocol
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
from PyQt5.QtCore import Qt, QObject, QTimer, pyqtSignal
|
||||||
|
from PyQt5.QtGui import QImage, QPixmap
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QAction,
|
||||||
|
QCheckBox,
|
||||||
|
QComboBox,
|
||||||
|
QFileDialog,
|
||||||
|
QHBoxLayout,
|
||||||
|
QLabel,
|
||||||
|
QLineEdit,
|
||||||
|
QMainWindow,
|
||||||
|
QMessageBox,
|
||||||
|
QPlainTextEdit,
|
||||||
|
QPushButton,
|
||||||
|
QSizePolicy,
|
||||||
|
QSplitter,
|
||||||
|
QVBoxLayout,
|
||||||
|
QWidget,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pyqtgraph as pg
|
||||||
|
except Exception as exc: # pragma: no cover
|
||||||
|
raise ImportError(
|
||||||
|
"PyQtGraph is required. Install with: pip install pyqt5 pyqtgraph"
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
from .acquisition import (
|
||||||
|
FixedCameraMode,
|
||||||
|
SdkAcquisitionWorker,
|
||||||
|
SdkPacket,
|
||||||
|
draw_arrow_display,
|
||||||
|
load_fixed_mode,
|
||||||
|
orisys_importable,
|
||||||
|
)
|
||||||
|
from .camera_discovery import list_cameras, pick_default_camera
|
||||||
|
from .dd06_acquisition import Dd06AcquisitionWorker
|
||||||
|
from .force_calibration import (
|
||||||
|
FORCE_SAMPLE_RATE_HZ,
|
||||||
|
PLOT_BUFFER_POINTS,
|
||||||
|
PLOT_WINDOW_SEC,
|
||||||
|
Y_AXIS_MIN_SPAN,
|
||||||
|
Y_RANGE_ALPHA,
|
||||||
|
calibrate_normal_force,
|
||||||
|
)
|
||||||
|
from .i18n import DEFAULT_LOCALE, Translator
|
||||||
|
from .paths import (
|
||||||
|
config_display_name,
|
||||||
|
default_dd06_config,
|
||||||
|
default_ddjx01_config,
|
||||||
|
default_locales_dir,
|
||||||
|
default_recordings_dir,
|
||||||
|
list_orisys_configs,
|
||||||
|
orisys_configs_dir,
|
||||||
|
project_root,
|
||||||
|
)
|
||||||
|
from .plot_history import MetricHistory
|
||||||
|
from .recorder import RawRecorder
|
||||||
|
|
||||||
|
CURVE_COLORS = ("#8ab4f8", "#34a853", "#fbbc04")
|
||||||
|
APP_VERSION_FALLBACK = "0.1.0"
|
||||||
|
|
||||||
|
STYLE = """
|
||||||
|
QMainWindow, QWidget {
|
||||||
|
background-color: #1e1f22;
|
||||||
|
color: #e8eaed;
|
||||||
|
font-family: "Segoe UI", "Microsoft YaHei", "SimHei", Arial, sans-serif;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
QMenuBar {
|
||||||
|
background-color: #1e1f22;
|
||||||
|
color: #e8eaed;
|
||||||
|
}
|
||||||
|
QMenuBar::item:selected { background-color: #2d3139; }
|
||||||
|
QMenu {
|
||||||
|
background-color: #2a2d33;
|
||||||
|
color: #e8eaed;
|
||||||
|
border: 1px solid #4a505a;
|
||||||
|
}
|
||||||
|
QMenu::item:selected { background-color: #243447; }
|
||||||
|
QPushButton {
|
||||||
|
background-color: #2d3139;
|
||||||
|
border: 1px solid #4a505a;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 6px 14px;
|
||||||
|
min-height: 28px;
|
||||||
|
color: #e8eaed;
|
||||||
|
}
|
||||||
|
QPushButton:hover { background-color: #3a404c; }
|
||||||
|
QPushButton:pressed { background-color: #252930; }
|
||||||
|
QPushButton:disabled { color: #6b7280; background-color: #25262a; }
|
||||||
|
QPushButton#btnPrimary {
|
||||||
|
background-color: #1a73e8;
|
||||||
|
border-color: #1a73e8;
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
QPushButton#btnPrimary:hover { background-color: #1967d2; }
|
||||||
|
QPushButton#btnDanger {
|
||||||
|
background-color: #c5221f;
|
||||||
|
border-color: #c5221f;
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
QPushButton#btnDanger:hover { background-color: #a50e0e; }
|
||||||
|
QLineEdit, QComboBox {
|
||||||
|
background-color: #2a2d33;
|
||||||
|
border: 1px solid #4a505a;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 5px 8px;
|
||||||
|
color: #e8eaed;
|
||||||
|
}
|
||||||
|
QComboBox QAbstractItemView {
|
||||||
|
background-color: #2a2d33;
|
||||||
|
color: #e8eaed;
|
||||||
|
selection-background-color: #243447;
|
||||||
|
}
|
||||||
|
QLabel#toolbarCaption { color: #9aa0a6; }
|
||||||
|
QLabel#imagePanel {
|
||||||
|
color: #6b7280;
|
||||||
|
background-color: #121316;
|
||||||
|
border: 1px solid #3c4048;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
QPlainTextEdit {
|
||||||
|
background-color: #121316;
|
||||||
|
color: #9aa0a6;
|
||||||
|
border: 1px solid #3c4048;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-family: "Cascadia Code", "Consolas", monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
QSplitter::handle { background-color: #2a2d33; }
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class AcquisitionPipeline(Protocol):
|
||||||
|
is_running: bool
|
||||||
|
|
||||||
|
def latest(self) -> Optional[SdkPacket]: ...
|
||||||
|
|
||||||
|
def stop(self) -> None: ...
|
||||||
|
|
||||||
|
def reset(self) -> None: ...
|
||||||
|
|
||||||
|
|
||||||
|
def ndarray_to_pixmap(frame: np.ndarray) -> QPixmap:
|
||||||
|
if frame.ndim == 2 or (frame.ndim == 3 and frame.shape[2] == 1):
|
||||||
|
gray = frame if frame.ndim == 2 else frame[:, :, 0]
|
||||||
|
h, w = gray.shape[:2]
|
||||||
|
image = QImage(gray.data, w, h, w, QImage.Format_Grayscale8)
|
||||||
|
return QPixmap.fromImage(image.copy())
|
||||||
|
if frame.ndim == 3 and frame.shape[2] == 4:
|
||||||
|
rgba = cv2.cvtColor(frame, cv2.COLOR_BGRA2RGBA)
|
||||||
|
h, w = rgba.shape[:2]
|
||||||
|
image = QImage(rgba.data, w, h, w * 4, QImage.Format_RGBA8888)
|
||||||
|
return QPixmap.fromImage(image.copy())
|
||||||
|
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||||
|
h, w = rgb.shape[:2]
|
||||||
|
image = QImage(rgb.data, w, h, w * 3, QImage.Format_RGB888)
|
||||||
|
return QPixmap.fromImage(image.copy())
|
||||||
|
|
||||||
|
|
||||||
|
def app_version() -> str:
|
||||||
|
try:
|
||||||
|
return pkg_version("sensor-bench")
|
||||||
|
except PackageNotFoundError:
|
||||||
|
return APP_VERSION_FALLBACK
|
||||||
|
|
||||||
|
|
||||||
|
class _AcquisitionBridge(QObject):
|
||||||
|
error = pyqtSignal(str)
|
||||||
|
status = pyqtSignal(str)
|
||||||
|
|
||||||
|
|
||||||
|
class ExhibitionWindow(QMainWindow):
|
||||||
|
"""External exhibition UI: toolbar + image + FN/FX/FY plots + log."""
|
||||||
|
|
||||||
|
def __init__(self, *, locales_dir: Optional[Path] = None, locale: str = DEFAULT_LOCALE):
|
||||||
|
super().__init__()
|
||||||
|
self.resize(1480, 900)
|
||||||
|
self.setStyleSheet(STYLE)
|
||||||
|
|
||||||
|
self.tr = Translator(locales_dir or default_locales_dir(), locale=locale)
|
||||||
|
self.recordings_dir = default_recordings_dir()
|
||||||
|
self.force_history = MetricHistory(
|
||||||
|
npoints=PLOT_BUFFER_POINTS,
|
||||||
|
channels=3,
|
||||||
|
window_sec=PLOT_WINDOW_SEC,
|
||||||
|
sample_rate_hz=FORCE_SAMPLE_RATE_HZ,
|
||||||
|
y_axis_min_span=Y_AXIS_MIN_SPAN,
|
||||||
|
y_range_alpha=Y_RANGE_ALPHA,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._acq_bridge = _AcquisitionBridge()
|
||||||
|
self._acq_bridge.error.connect(self._on_acquisition_error)
|
||||||
|
self._acq_bridge.status.connect(self._on_acquisition_status)
|
||||||
|
self.stitch_worker = SdkAcquisitionWorker(
|
||||||
|
on_error=lambda m: self._acq_bridge.error.emit(m),
|
||||||
|
on_status=lambda m: self._acq_bridge.status.emit(m),
|
||||||
|
)
|
||||||
|
self.dd06_worker = Dd06AcquisitionWorker(
|
||||||
|
on_error=lambda m: self._acq_bridge.error.emit(m),
|
||||||
|
on_status=lambda m: self._acq_bridge.status.emit(m),
|
||||||
|
)
|
||||||
|
self.acquisition: AcquisitionPipeline = self.stitch_worker
|
||||||
|
self.recorder = RawRecorder()
|
||||||
|
|
||||||
|
self._source_pixmap: Optional[QPixmap] = None
|
||||||
|
self._last_packet: Optional[SdkPacket] = None
|
||||||
|
self._last_packet_id = 0
|
||||||
|
self._awaiting_first_frame = False
|
||||||
|
self._display_aspect = 1.0
|
||||||
|
self._view_mode = "pure_color"
|
||||||
|
self._sensor_type = "orisys"
|
||||||
|
self._log_panel_visible = False
|
||||||
|
self._fixed_mode: Optional[FixedCameraMode] = None
|
||||||
|
self._orisys_config = str(default_ddjx01_config())
|
||||||
|
self._dd06_config = str(default_dd06_config())
|
||||||
|
|
||||||
|
self._build_ui()
|
||||||
|
self._create_plots()
|
||||||
|
self._apply_language()
|
||||||
|
self._refresh_orisys_status()
|
||||||
|
self._log(self.t("exhibition.hints"))
|
||||||
|
|
||||||
|
self.timer = QTimer(self)
|
||||||
|
self.timer.timeout.connect(self._on_tick)
|
||||||
|
self.timer.start(16)
|
||||||
|
|
||||||
|
def t(self, key: str, **kwargs) -> str:
|
||||||
|
return self.tr.t(key, **kwargs)
|
||||||
|
|
||||||
|
def _log(self, message: str) -> None:
|
||||||
|
stamp = time.strftime("%H:%M:%S")
|
||||||
|
self.log_widget.appendPlainText(f"[{stamp}] {message}")
|
||||||
|
|
||||||
|
def _pipeline_running(self) -> bool:
|
||||||
|
return bool(self.stitch_worker.is_running or self.dd06_worker.is_running)
|
||||||
|
|
||||||
|
def _build_ui(self) -> None:
|
||||||
|
self._build_menu()
|
||||||
|
|
||||||
|
central = QWidget()
|
||||||
|
self.setCentralWidget(central)
|
||||||
|
root = QVBoxLayout(central)
|
||||||
|
root.setContentsMargins(8, 8, 8, 8)
|
||||||
|
root.setSpacing(8)
|
||||||
|
|
||||||
|
bar = QHBoxLayout()
|
||||||
|
bar.setSpacing(8)
|
||||||
|
|
||||||
|
self.sensor_caption = QLabel()
|
||||||
|
self.sensor_caption.setObjectName("toolbarCaption")
|
||||||
|
bar.addWidget(self.sensor_caption)
|
||||||
|
self.sensor_combo = QComboBox()
|
||||||
|
self.sensor_combo.addItem("", "orisys")
|
||||||
|
self.sensor_combo.addItem("", "dd06")
|
||||||
|
self.sensor_combo.currentIndexChanged.connect(self._on_sensor_type_changed)
|
||||||
|
bar.addWidget(self.sensor_combo)
|
||||||
|
|
||||||
|
self.btn_search = QPushButton()
|
||||||
|
self.btn_search.clicked.connect(self._on_search_cameras)
|
||||||
|
bar.addWidget(self.btn_search)
|
||||||
|
self.btn_camera = QPushButton()
|
||||||
|
self.btn_camera.clicked.connect(self._on_browse_camera)
|
||||||
|
bar.addWidget(self.btn_camera)
|
||||||
|
self.camera_edit = QLineEdit("0")
|
||||||
|
bar.addWidget(self.camera_edit, 1)
|
||||||
|
|
||||||
|
self.btn_config = QPushButton()
|
||||||
|
self.btn_config.clicked.connect(self._on_browse_config)
|
||||||
|
bar.addWidget(self.btn_config)
|
||||||
|
self.config_combo = QComboBox()
|
||||||
|
self.config_combo.setMinimumWidth(160)
|
||||||
|
self.config_combo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||||
|
bar.addWidget(self.config_combo, 1)
|
||||||
|
self._populate_config_combo(preferred=self._orisys_config)
|
||||||
|
|
||||||
|
self.btn_start = QPushButton()
|
||||||
|
self.btn_start.setObjectName("btnPrimary")
|
||||||
|
self.btn_start.clicked.connect(self._on_start)
|
||||||
|
bar.addWidget(self.btn_start)
|
||||||
|
|
||||||
|
self.btn_stop = QPushButton()
|
||||||
|
self.btn_stop.setEnabled(False)
|
||||||
|
self.btn_stop.clicked.connect(self._on_stop)
|
||||||
|
bar.addWidget(self.btn_stop)
|
||||||
|
|
||||||
|
self.btn_reset = QPushButton()
|
||||||
|
self.btn_reset.setEnabled(False)
|
||||||
|
self.btn_reset.clicked.connect(self._on_reset)
|
||||||
|
bar.addWidget(self.btn_reset)
|
||||||
|
|
||||||
|
self.btn_record = QPushButton()
|
||||||
|
self.btn_record.setObjectName("btnDanger")
|
||||||
|
self.btn_record.setEnabled(False)
|
||||||
|
self.btn_record.clicked.connect(self._on_toggle_record)
|
||||||
|
bar.addWidget(self.btn_record)
|
||||||
|
|
||||||
|
self.show_log_check = QCheckBox()
|
||||||
|
self.show_log_check.setChecked(False)
|
||||||
|
self.show_log_check.toggled.connect(self._on_show_log_changed)
|
||||||
|
bar.addWidget(self.show_log_check)
|
||||||
|
|
||||||
|
self.view_caption = QLabel()
|
||||||
|
self.view_caption.setObjectName("toolbarCaption")
|
||||||
|
bar.addWidget(self.view_caption)
|
||||||
|
self.view_combo = QComboBox()
|
||||||
|
self.view_combo.addItem("", "pure_color")
|
||||||
|
self.view_combo.addItem("", "captured")
|
||||||
|
self.view_combo.addItem("", "raw")
|
||||||
|
self.view_combo.setCurrentIndex(0)
|
||||||
|
self.view_combo.currentIndexChanged.connect(self._on_view_changed)
|
||||||
|
bar.addWidget(self.view_combo)
|
||||||
|
|
||||||
|
self.language_caption = QLabel()
|
||||||
|
self.language_caption.setObjectName("toolbarCaption")
|
||||||
|
bar.addWidget(self.language_caption)
|
||||||
|
self.language_combo = QComboBox()
|
||||||
|
self.language_combo.addItem("", "zh_CN")
|
||||||
|
self.language_combo.addItem("", "en_US")
|
||||||
|
self.language_combo.setCurrentIndex(0 if self.tr.locale == "zh_CN" else 1)
|
||||||
|
self.language_combo.currentIndexChanged.connect(self._on_language_changed)
|
||||||
|
bar.addWidget(self.language_combo)
|
||||||
|
|
||||||
|
root.addLayout(bar)
|
||||||
|
|
||||||
|
v_splitter = QSplitter(Qt.Vertical)
|
||||||
|
h_splitter = QSplitter(Qt.Horizontal)
|
||||||
|
|
||||||
|
left = QWidget()
|
||||||
|
left_layout = QVBoxLayout(left)
|
||||||
|
left_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.image_label = QLabel()
|
||||||
|
self.image_label.setObjectName("imagePanel")
|
||||||
|
self.image_label.setMinimumSize(240, 240)
|
||||||
|
self.image_label.setAlignment(Qt.AlignCenter)
|
||||||
|
self.image_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||||
|
left_layout.addWidget(self.image_label)
|
||||||
|
h_splitter.addWidget(left)
|
||||||
|
|
||||||
|
pg.setConfigOptions(antialias=True, foreground="#e8eaed", background="#121316")
|
||||||
|
self.graph_widget = pg.GraphicsLayoutWidget()
|
||||||
|
self.graph_widget.setMinimumWidth(320)
|
||||||
|
h_splitter.addWidget(self.graph_widget)
|
||||||
|
self.h_splitter = h_splitter
|
||||||
|
v_splitter.addWidget(h_splitter)
|
||||||
|
|
||||||
|
self.log_widget = QPlainTextEdit()
|
||||||
|
self.log_widget.setReadOnly(True)
|
||||||
|
self.log_widget.setMaximumBlockCount(500)
|
||||||
|
self.log_widget.setMinimumHeight(110)
|
||||||
|
self.log_widget.setVisible(self._log_panel_visible)
|
||||||
|
v_splitter.addWidget(self.log_widget)
|
||||||
|
v_splitter.setStretchFactor(0, 5)
|
||||||
|
v_splitter.setStretchFactor(1, 1)
|
||||||
|
root.addWidget(v_splitter, 1)
|
||||||
|
|
||||||
|
self.status_label = QLabel()
|
||||||
|
self.status_label.setStyleSheet("color: #9aa0a6;")
|
||||||
|
root.addWidget(self.status_label)
|
||||||
|
|
||||||
|
def _build_menu(self) -> None:
|
||||||
|
menu = self.menuBar()
|
||||||
|
self.help_menu = menu.addMenu("")
|
||||||
|
self.about_action = QAction("", self)
|
||||||
|
self.about_action.triggered.connect(self._on_about)
|
||||||
|
self.help_menu.addAction(self.about_action)
|
||||||
|
|
||||||
|
def _create_plots(self) -> None:
|
||||||
|
self.plots = []
|
||||||
|
self.curves = []
|
||||||
|
self.dot_cores = []
|
||||||
|
self.plot_titles = [
|
||||||
|
"exhibition.plot_fn",
|
||||||
|
"exhibition.plot_fx",
|
||||||
|
"exhibition.plot_fy",
|
||||||
|
]
|
||||||
|
axis_pen = pg.mkPen(color="#3c4048", width=1)
|
||||||
|
for i, title_key in enumerate(self.plot_titles):
|
||||||
|
plot = self.graph_widget.addPlot(title=self.t(title_key))
|
||||||
|
plot.showGrid(x=False, y=True, alpha=0.2)
|
||||||
|
plot.getAxis("left").setPen(axis_pen)
|
||||||
|
plot.getAxis("bottom").setPen(axis_pen)
|
||||||
|
plot.setLabel("bottom", "s")
|
||||||
|
curve = plot.plot(pen=pg.mkPen(CURVE_COLORS[i], width=2))
|
||||||
|
dot = plot.plot(
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
pen=None,
|
||||||
|
symbol="o",
|
||||||
|
symbolSize=6,
|
||||||
|
symbolBrush=CURVE_COLORS[i],
|
||||||
|
)
|
||||||
|
plot.setXRange(0, PLOT_WINDOW_SEC, padding=0)
|
||||||
|
plot.enableAutoRange(x=False, y=False)
|
||||||
|
self.plots.append(plot)
|
||||||
|
self.curves.append(curve)
|
||||||
|
self.dot_cores.append(dot)
|
||||||
|
if i < 2:
|
||||||
|
self.graph_widget.nextRow()
|
||||||
|
|
||||||
|
def _apply_language(self) -> None:
|
||||||
|
self.setWindowTitle(self.t("exhibition.title"))
|
||||||
|
self.help_menu.setTitle(self.t("exhibition.menu_help"))
|
||||||
|
self.about_action.setText(self.t("exhibition.menu_about"))
|
||||||
|
self.sensor_caption.setText(self.t("exhibition.sensor_type"))
|
||||||
|
self.btn_search.setText(self.t("exhibition.btn_search"))
|
||||||
|
self.btn_camera.setText(self.t("exhibition.btn_camera"))
|
||||||
|
self.btn_config.setText(self.t("exhibition.btn_config"))
|
||||||
|
self.btn_start.setText(self.t("exhibition.btn_start"))
|
||||||
|
self.btn_stop.setText(self.t("exhibition.btn_stop"))
|
||||||
|
self.btn_reset.setText(self.t("exhibition.btn_reset"))
|
||||||
|
self._refresh_record_button()
|
||||||
|
self.view_caption.setText(self.t("exhibition.view"))
|
||||||
|
self.show_log_check.setText(self.t("exhibition.show_log"))
|
||||||
|
self.language_caption.setText(self.t("app.language"))
|
||||||
|
self.camera_edit.setPlaceholderText(self.t("exhibition.placeholder_camera"))
|
||||||
|
self._update_config_tooltip()
|
||||||
|
|
||||||
|
self.language_combo.blockSignals(True)
|
||||||
|
self.language_combo.setItemText(0, self.t("app.lang_zh"))
|
||||||
|
self.language_combo.setItemText(1, self.t("app.lang_en"))
|
||||||
|
self.language_combo.blockSignals(False)
|
||||||
|
|
||||||
|
self.sensor_combo.blockSignals(True)
|
||||||
|
self.sensor_combo.setItemText(0, self.t("exhibition.sensor_orisys"))
|
||||||
|
self.sensor_combo.setItemText(1, self.t("exhibition.sensor_dd06"))
|
||||||
|
self.sensor_combo.blockSignals(False)
|
||||||
|
|
||||||
|
self.view_combo.blockSignals(True)
|
||||||
|
self.view_combo.setItemText(0, self.t("exhibition.view_pure_color"))
|
||||||
|
self.view_combo.setItemText(1, self.t("exhibition.view_captured"))
|
||||||
|
self.view_combo.setItemText(2, self.t("exhibition.view_raw"))
|
||||||
|
self.view_combo.blockSignals(False)
|
||||||
|
|
||||||
|
for plot, title_key in zip(self.plots, self.plot_titles):
|
||||||
|
plot.setTitle(self.t(title_key), color="#9aa0a6", size="12pt")
|
||||||
|
|
||||||
|
if self._source_pixmap is None:
|
||||||
|
self.image_label.setText(self.t("labels.no_preview"))
|
||||||
|
self.status_label.setText(self.t("status.ready"))
|
||||||
|
|
||||||
|
def _selected_config(self) -> str:
|
||||||
|
data = self.config_combo.currentData()
|
||||||
|
if data:
|
||||||
|
return str(data)
|
||||||
|
return self.config_combo.currentText().strip()
|
||||||
|
|
||||||
|
def _set_config_selection(self, value: str) -> None:
|
||||||
|
value = str(value or "").strip()
|
||||||
|
if not value:
|
||||||
|
return
|
||||||
|
for index in range(self.config_combo.count()):
|
||||||
|
if str(self.config_combo.itemData(index) or "") == value:
|
||||||
|
self.config_combo.setCurrentIndex(index)
|
||||||
|
return
|
||||||
|
# Custom path not yet in the list.
|
||||||
|
if Path(value).stem.lower() == "options" and Path(value).suffix.lower() == ".json":
|
||||||
|
return
|
||||||
|
self.config_combo.addItem(config_display_name(value), value)
|
||||||
|
self.config_combo.setCurrentIndex(self.config_combo.count() - 1)
|
||||||
|
|
||||||
|
def _populate_config_combo(self, *, preferred: str | None = None) -> None:
|
||||||
|
current = preferred or self._selected_config() or default_ddjx01_config()
|
||||||
|
custom_paths = []
|
||||||
|
for index in range(self.config_combo.count()):
|
||||||
|
data = str(self.config_combo.itemData(index) or "")
|
||||||
|
if data and (Path(data).suffix.lower() == ".json" or "/" in data or "\\" in data):
|
||||||
|
if data not in custom_paths:
|
||||||
|
custom_paths.append(data)
|
||||||
|
|
||||||
|
self.config_combo.blockSignals(True)
|
||||||
|
self.config_combo.clear()
|
||||||
|
for entry in list_orisys_configs():
|
||||||
|
self.config_combo.addItem(config_display_name(entry), entry)
|
||||||
|
for path in custom_paths:
|
||||||
|
# Avoid duplicating local entries already listed above.
|
||||||
|
if any(str(self.config_combo.itemData(i) or "") == path for i in range(self.config_combo.count())):
|
||||||
|
continue
|
||||||
|
if Path(path).stem.lower() == "options":
|
||||||
|
continue
|
||||||
|
self.config_combo.addItem(config_display_name(path), path)
|
||||||
|
if self.config_combo.count() == 0 and current:
|
||||||
|
self.config_combo.addItem(config_display_name(current), current)
|
||||||
|
self.config_combo.blockSignals(False)
|
||||||
|
self._set_config_selection(current)
|
||||||
|
self._update_config_tooltip()
|
||||||
|
|
||||||
|
def _update_config_tooltip(self) -> None:
|
||||||
|
cfg_dir = orisys_configs_dir()
|
||||||
|
selected = self._selected_config()
|
||||||
|
if cfg_dir is not None:
|
||||||
|
tip = self.t("exhibition.config_dir_tip", path=cfg_dir, selected=selected)
|
||||||
|
else:
|
||||||
|
tip = self.t("exhibition.config_dir_missing")
|
||||||
|
self.config_combo.setToolTip(tip)
|
||||||
|
self.btn_config.setToolTip(tip)
|
||||||
|
|
||||||
|
def _refresh_record_button(self) -> None:
|
||||||
|
if self.recorder.is_recording:
|
||||||
|
self.btn_record.setText(self.t("exhibition.btn_stop_record"))
|
||||||
|
else:
|
||||||
|
self.btn_record.setText(self.t("exhibition.btn_start_record"))
|
||||||
|
|
||||||
|
def _on_about(self) -> None:
|
||||||
|
ok, orisys_ver = orisys_importable()
|
||||||
|
orisys_text = orisys_ver if ok else self.t("status.orisys_missing")
|
||||||
|
QMessageBox.about(
|
||||||
|
self,
|
||||||
|
self.t("exhibition.about_title"),
|
||||||
|
self.t(
|
||||||
|
"exhibition.about_body",
|
||||||
|
app_version=app_version(),
|
||||||
|
orisys_version=orisys_text,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _on_language_changed(self, *_args) -> None:
|
||||||
|
locale = self.language_combo.currentData()
|
||||||
|
if not locale:
|
||||||
|
return
|
||||||
|
self.tr.set_locale(str(locale))
|
||||||
|
self._apply_language()
|
||||||
|
self._refresh_orisys_status()
|
||||||
|
|
||||||
|
def _on_sensor_type_changed(self, *_args) -> None:
|
||||||
|
if self._pipeline_running():
|
||||||
|
return
|
||||||
|
kind = str(self.sensor_combo.currentData() or "orisys")
|
||||||
|
if kind == self._sensor_type:
|
||||||
|
return
|
||||||
|
# Remember current config for the leaving type.
|
||||||
|
if self._sensor_type == "orisys":
|
||||||
|
self._orisys_config = self._selected_config() or self._orisys_config
|
||||||
|
else:
|
||||||
|
self._dd06_config = self._selected_config() or self._dd06_config
|
||||||
|
self._sensor_type = kind
|
||||||
|
self.acquisition = self.dd06_worker if kind == "dd06" else self.stitch_worker
|
||||||
|
preferred = self._dd06_config if kind == "dd06" else self._orisys_config
|
||||||
|
self._populate_config_combo(preferred=preferred)
|
||||||
|
self._log(self.t("exhibition.sensor_switched", sensor=kind))
|
||||||
|
|
||||||
|
def _on_view_changed(self, *_args) -> None:
|
||||||
|
mode = self.view_combo.currentData()
|
||||||
|
if mode in ("pure_color", "captured", "raw"):
|
||||||
|
self._view_mode = str(mode)
|
||||||
|
if self._last_packet is not None:
|
||||||
|
self._update_image(self._last_packet)
|
||||||
|
|
||||||
|
def _on_show_log_changed(self, visible: bool) -> None:
|
||||||
|
self._log_panel_visible = bool(visible)
|
||||||
|
self.log_widget.setVisible(self._log_panel_visible)
|
||||||
|
|
||||||
|
def _refresh_orisys_status(self) -> None:
|
||||||
|
ok, detail = orisys_importable()
|
||||||
|
if ok:
|
||||||
|
self.status_label.setText(self.t("status.orisys_ok", version=detail))
|
||||||
|
self.btn_start.setEnabled(not self._pipeline_running())
|
||||||
|
self._populate_config_combo(preferred=self._selected_config() or default_ddjx01_config())
|
||||||
|
else:
|
||||||
|
self.status_label.setText(self.t("status.orisys_missing"))
|
||||||
|
self.btn_start.setEnabled(False)
|
||||||
|
self._update_config_tooltip()
|
||||||
|
|
||||||
|
def _parse_camera_source(self) -> str | int:
|
||||||
|
text = self.camera_edit.text().strip() or "0"
|
||||||
|
if text.isdigit():
|
||||||
|
return int(text)
|
||||||
|
return text
|
||||||
|
|
||||||
|
def _on_search_cameras(self) -> None:
|
||||||
|
if self._pipeline_running():
|
||||||
|
return
|
||||||
|
preferred = self.camera_edit.text().strip()
|
||||||
|
cameras = list_cameras()
|
||||||
|
if not cameras:
|
||||||
|
self._log(self.t("exhibition.camera_none"))
|
||||||
|
return
|
||||||
|
names = ", ".join(cam.label() for cam in cameras)
|
||||||
|
self._log(self.t("exhibition.camera_found", count=len(cameras), names=names))
|
||||||
|
selected = pick_default_camera(
|
||||||
|
cameras, preferred if preferred.isdigit() else None
|
||||||
|
)
|
||||||
|
if selected is not None:
|
||||||
|
self.camera_edit.setText(selected)
|
||||||
|
|
||||||
|
def _on_browse_camera(self) -> None:
|
||||||
|
path, _ = QFileDialog.getOpenFileName(
|
||||||
|
self,
|
||||||
|
self.t("exhibition.dialog_video"),
|
||||||
|
str(project_root()),
|
||||||
|
"Video (*.mp4 *.avi *.mov);;All (*)",
|
||||||
|
)
|
||||||
|
if path:
|
||||||
|
self.camera_edit.setText(path)
|
||||||
|
|
||||||
|
def _on_browse_config(self) -> None:
|
||||||
|
start_dir = orisys_configs_dir() or project_root()
|
||||||
|
path, _ = QFileDialog.getOpenFileName(
|
||||||
|
self,
|
||||||
|
self.t("dialogs.select_config"),
|
||||||
|
str(start_dir),
|
||||||
|
"JSON (*.json);;All (*)",
|
||||||
|
)
|
||||||
|
if path:
|
||||||
|
self._set_config_selection(path)
|
||||||
|
self._update_config_tooltip()
|
||||||
|
|
||||||
|
def _on_start(self) -> None:
|
||||||
|
ok, detail = orisys_importable()
|
||||||
|
if not ok:
|
||||||
|
QMessageBox.warning(
|
||||||
|
self,
|
||||||
|
self.t("dialogs.orisys_title"),
|
||||||
|
self.t("dialogs.orisys_required", detail=detail),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
config = self._selected_config()
|
||||||
|
if not config:
|
||||||
|
QMessageBox.warning(
|
||||||
|
self, self.t("dialogs.config_title"), self.t("dialogs.config_required")
|
||||||
|
)
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
mode = load_fixed_mode(config)
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
QMessageBox.warning(
|
||||||
|
self,
|
||||||
|
self.t("dialogs.config_title"),
|
||||||
|
self.t("labels.fixed_mode_invalid", error=exc),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.force_history.reset()
|
||||||
|
self._awaiting_first_frame = True
|
||||||
|
self._source_pixmap = None
|
||||||
|
self.image_label.clear()
|
||||||
|
self.image_label.setText(self.t("labels.no_preview"))
|
||||||
|
self.btn_start.setEnabled(False)
|
||||||
|
self.btn_start.setText(self.t("exhibition.btn_starting"))
|
||||||
|
self.sensor_combo.setEnabled(False)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if self._sensor_type == "dd06":
|
||||||
|
self.acquisition = self.dd06_worker
|
||||||
|
mode = self.dd06_worker.start(
|
||||||
|
self._parse_camera_source(),
|
||||||
|
config_path=config,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.acquisition = self.stitch_worker
|
||||||
|
mode = self.stitch_worker.start(
|
||||||
|
self._parse_camera_source(),
|
||||||
|
config_path=config,
|
||||||
|
cal_path=None,
|
||||||
|
backend="auto",
|
||||||
|
)
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
self._awaiting_first_frame = False
|
||||||
|
self.btn_start.setEnabled(True)
|
||||||
|
self.btn_start.setText(self.t("exhibition.btn_start"))
|
||||||
|
self.sensor_combo.setEnabled(True)
|
||||||
|
QMessageBox.critical(self, self.t("dialogs.capture_title"), str(exc))
|
||||||
|
self._log(self.t("status.capture_error", message=exc))
|
||||||
|
return
|
||||||
|
|
||||||
|
self._fixed_mode = mode
|
||||||
|
self.btn_start.setText(self.t("exhibition.btn_start"))
|
||||||
|
self.btn_stop.setEnabled(True)
|
||||||
|
self.btn_reset.setEnabled(True)
|
||||||
|
self.btn_record.setEnabled(False)
|
||||||
|
self.btn_search.setEnabled(False)
|
||||||
|
self.btn_camera.setEnabled(False)
|
||||||
|
self.camera_edit.setEnabled(False)
|
||||||
|
self.config_combo.setEnabled(False)
|
||||||
|
self.status_label.setText(self.t("status.opening", mode=mode.label()))
|
||||||
|
self._log(self.t("status.opening", mode=mode.label()))
|
||||||
|
|
||||||
|
def _on_stop(self) -> None:
|
||||||
|
if self.recorder.is_recording:
|
||||||
|
self._stop_record()
|
||||||
|
self.stitch_worker.stop()
|
||||||
|
self.dd06_worker.stop()
|
||||||
|
self._awaiting_first_frame = False
|
||||||
|
self._last_packet = None
|
||||||
|
self._source_pixmap = None
|
||||||
|
self.image_label.clear()
|
||||||
|
self.image_label.setText(self.t("labels.no_preview"))
|
||||||
|
self.btn_start.setEnabled(orisys_importable()[0])
|
||||||
|
self.btn_stop.setEnabled(False)
|
||||||
|
self.btn_reset.setEnabled(False)
|
||||||
|
self.btn_record.setEnabled(False)
|
||||||
|
self.btn_search.setEnabled(True)
|
||||||
|
self.btn_camera.setEnabled(True)
|
||||||
|
self.camera_edit.setEnabled(True)
|
||||||
|
self.config_combo.setEnabled(True)
|
||||||
|
self.sensor_combo.setEnabled(True)
|
||||||
|
self.status_label.setText(self.t("status.preview_stopped"))
|
||||||
|
self._log(self.t("status.preview_stopped"))
|
||||||
|
|
||||||
|
def _on_reset(self) -> None:
|
||||||
|
if not self._pipeline_running():
|
||||||
|
self._log(self.t("exhibition.reset_need_start"))
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self.acquisition.reset()
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
self._log(self.t("exhibition.reset_failed", message=exc))
|
||||||
|
return
|
||||||
|
self._log(self.t("exhibition.reset_done"))
|
||||||
|
|
||||||
|
def _on_toggle_record(self) -> None:
|
||||||
|
if self.recorder.is_recording:
|
||||||
|
self._stop_record()
|
||||||
|
else:
|
||||||
|
self._start_record()
|
||||||
|
|
||||||
|
def _start_record(self) -> None:
|
||||||
|
packet = self._last_packet or self.acquisition.latest()
|
||||||
|
if packet is None:
|
||||||
|
QMessageBox.warning(
|
||||||
|
self, self.t("dialogs.recording_title"), self.t("dialogs.no_frame")
|
||||||
|
)
|
||||||
|
return
|
||||||
|
record_fps = packet.fps or (packet.mode.fps if packet.mode else 30.0)
|
||||||
|
try:
|
||||||
|
path = self.recorder.start(
|
||||||
|
self.recordings_dir, fps=float(record_fps), frame=packet.raw_frame
|
||||||
|
)
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
QMessageBox.critical(self, self.t("dialogs.recording_title"), str(exc))
|
||||||
|
return
|
||||||
|
self._refresh_record_button()
|
||||||
|
self._log(self.t("exhibition.recording_started", name=path.name))
|
||||||
|
|
||||||
|
def _stop_record(self) -> None:
|
||||||
|
path, count = self.recorder.stop()
|
||||||
|
self._refresh_record_button()
|
||||||
|
if path is None:
|
||||||
|
return
|
||||||
|
self._log(self.t("status.saved", count=count, path=path))
|
||||||
|
|
||||||
|
def _on_acquisition_error(self, message: str) -> None:
|
||||||
|
self._awaiting_first_frame = False
|
||||||
|
self.btn_start.setEnabled(orisys_importable()[0])
|
||||||
|
self.btn_stop.setEnabled(False)
|
||||||
|
self.btn_reset.setEnabled(False)
|
||||||
|
self.btn_record.setEnabled(False)
|
||||||
|
self.btn_search.setEnabled(True)
|
||||||
|
self.btn_camera.setEnabled(True)
|
||||||
|
self.camera_edit.setEnabled(True)
|
||||||
|
self.config_combo.setEnabled(True)
|
||||||
|
self.sensor_combo.setEnabled(True)
|
||||||
|
self.status_label.setText(self.t("status.capture_error", message=message))
|
||||||
|
self._log(self.t("status.capture_error", message=message))
|
||||||
|
QMessageBox.warning(self, self.t("dialogs.capture_title"), message)
|
||||||
|
|
||||||
|
def _on_acquisition_status(self, message: str) -> None:
|
||||||
|
self.status_label.setText(message)
|
||||||
|
self._log(message)
|
||||||
|
|
||||||
|
def _on_tick(self) -> None:
|
||||||
|
packet = self.acquisition.latest()
|
||||||
|
if packet is None:
|
||||||
|
return
|
||||||
|
packet_id = id(packet)
|
||||||
|
if packet_id == self._last_packet_id and not self._awaiting_first_frame:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._awaiting_first_frame:
|
||||||
|
self._awaiting_first_frame = False
|
||||||
|
self.btn_record.setEnabled(True)
|
||||||
|
info = self.t(
|
||||||
|
"status.frame_info",
|
||||||
|
raw_w=packet.width,
|
||||||
|
raw_h=packet.height,
|
||||||
|
proc_w=packet.processed_width,
|
||||||
|
proc_h=packet.processed_height,
|
||||||
|
fps=packet.fps,
|
||||||
|
)
|
||||||
|
self.status_label.setText(info)
|
||||||
|
self._log(info)
|
||||||
|
self._log(
|
||||||
|
self.t(
|
||||||
|
"exhibition.force_fn_n",
|
||||||
|
value=calibrate_normal_force(packet.normal_force),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self._last_packet = packet
|
||||||
|
self._last_packet_id = packet_id
|
||||||
|
self._update_image(packet)
|
||||||
|
self.force_history.push(
|
||||||
|
packet.timestamp,
|
||||||
|
packet.normal_force,
|
||||||
|
packet.shear_x,
|
||||||
|
packet.shear_y,
|
||||||
|
)
|
||||||
|
self._update_plots()
|
||||||
|
|
||||||
|
if self.recorder.is_recording:
|
||||||
|
self.recorder.write(packet.raw_frame)
|
||||||
|
self.status_label.setText(
|
||||||
|
self.t(
|
||||||
|
"status.recording_frames",
|
||||||
|
name=self.recorder.path.name if self.recorder.path else "",
|
||||||
|
count=self.recorder.frame_count,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _update_image(self, packet: SdkPacket) -> None:
|
||||||
|
if self._view_mode == "raw":
|
||||||
|
frame = packet.raw_frame
|
||||||
|
elif self._view_mode == "captured":
|
||||||
|
frame = draw_arrow_display(
|
||||||
|
packet.processed_frame, packet.flow, background="captured"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
frame = draw_arrow_display(
|
||||||
|
packet.processed_frame, packet.flow, background="pure_color"
|
||||||
|
)
|
||||||
|
h, w = frame.shape[:2]
|
||||||
|
aspect = float(w) / float(h) if h else 1.0
|
||||||
|
first = self._source_pixmap is None
|
||||||
|
aspect_changed = abs(aspect - self._display_aspect) > 1e-3
|
||||||
|
self._display_aspect = aspect
|
||||||
|
self._source_pixmap = ndarray_to_pixmap(frame)
|
||||||
|
if first or aspect_changed:
|
||||||
|
self._adjust_image_panel()
|
||||||
|
else:
|
||||||
|
self._render_image()
|
||||||
|
|
||||||
|
def _adjust_image_panel(self) -> None:
|
||||||
|
available = max(self.height() - 220, 200)
|
||||||
|
image_w = int(available * self._display_aspect)
|
||||||
|
image_w = max(min(image_w, max(self.width() // 2, 240)), 240)
|
||||||
|
self.h_splitter.setSizes([image_w, max(self.width() - image_w, 320)])
|
||||||
|
self._render_image()
|
||||||
|
|
||||||
|
def _render_image(self) -> None:
|
||||||
|
if self._source_pixmap is None or self._source_pixmap.isNull():
|
||||||
|
return
|
||||||
|
target = self.image_label.size()
|
||||||
|
if target.width() < 2 or target.height() < 2:
|
||||||
|
return
|
||||||
|
scaled = self._source_pixmap.scaled(
|
||||||
|
target, Qt.KeepAspectRatio, Qt.SmoothTransformation
|
||||||
|
)
|
||||||
|
self.image_label.setPixmap(scaled)
|
||||||
|
|
||||||
|
def _update_plots(self) -> None:
|
||||||
|
state = self.force_history.state()
|
||||||
|
if state is None:
|
||||||
|
return
|
||||||
|
for i, curve in enumerate(self.curves):
|
||||||
|
x = state.times
|
||||||
|
y = state.values[:, i]
|
||||||
|
curve.setData(x, y)
|
||||||
|
ymin, ymax = state.y_ranges[i]
|
||||||
|
self.plots[i].setYRange(ymin, ymax)
|
||||||
|
self.dot_cores[i].setData(x[-1:], y[-1:])
|
||||||
|
x0, x1 = state.x_range
|
||||||
|
for plot in self.plots:
|
||||||
|
plot.setXRange(x0, x1, padding=0)
|
||||||
|
|
||||||
|
def resizeEvent(self, event) -> None: # noqa: N802
|
||||||
|
super().resizeEvent(event)
|
||||||
|
self._render_image()
|
||||||
|
|
||||||
|
def keyPressEvent(self, event) -> None: # noqa: N802
|
||||||
|
key = event.key()
|
||||||
|
if key == Qt.Key_Escape:
|
||||||
|
if self.isFullScreen():
|
||||||
|
self.showNormal()
|
||||||
|
else:
|
||||||
|
self.showFullScreen()
|
||||||
|
elif key == Qt.Key_Q:
|
||||||
|
self.close()
|
||||||
|
elif key == Qt.Key_S:
|
||||||
|
if not self.recorder.is_recording and self._pipeline_running():
|
||||||
|
self._start_record()
|
||||||
|
elif key == Qt.Key_E:
|
||||||
|
if self.recorder.is_recording:
|
||||||
|
self._stop_record()
|
||||||
|
elif key == Qt.Key_R:
|
||||||
|
self._on_reset()
|
||||||
|
else:
|
||||||
|
super().keyPressEvent(event)
|
||||||
|
|
||||||
|
def closeEvent(self, event) -> None: # noqa: N802
|
||||||
|
if self.recorder.is_recording:
|
||||||
|
self.recorder.stop()
|
||||||
|
self.stitch_worker.stop()
|
||||||
|
self.dd06_worker.stop()
|
||||||
|
self.timer.stop()
|
||||||
|
super().closeEvent(event)
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
"""Force calibration coefficients and plot defaults from Orisys viewer2d."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
# Linear FN → Newtons (y = mx + c), from orisys examples/gui/sensors/adapter.py.
|
||||||
|
NORMAL_FORCE_CAL_SLOPE = 3.92685554668795e-05
|
||||||
|
NORMAL_FORCE_CAL_OFFSET = 0.0217596967278535
|
||||||
|
|
||||||
|
# PyQtGraph force-curve tuning from examples/gui/viewer2d/backend.py.
|
||||||
|
FORCE_SAMPLE_RATE_HZ = 30.0
|
||||||
|
PLOT_BUFFER_POINTS = 120
|
||||||
|
PLOT_WINDOW_SEC = PLOT_BUFFER_POINTS / FORCE_SAMPLE_RATE_HZ
|
||||||
|
Y_AXIS_MIN_SPAN = 20000.0
|
||||||
|
Y_RANGE_ALPHA = 0.5
|
||||||
|
LOWPASS_CUTOFF_HZ = 10.0
|
||||||
|
|
||||||
|
|
||||||
|
def calibrate_normal_force(raw_value: float) -> float:
|
||||||
|
"""Convert raw normal-force reading to Newtons using y = mx + c."""
|
||||||
|
return NORMAL_FORCE_CAL_SLOPE * float(raw_value) + NORMAL_FORCE_CAL_OFFSET
|
||||||
@@ -1,22 +1,30 @@
|
|||||||
"""Entry point for the Sensor Bench desktop application."""
|
"""Entry point for the public Sensor Bench exhibition application."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
# Must run before any module that imports cv2 (exhibition_app, acquisition, etc.).
|
||||||
|
os.environ.setdefault("OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS", "0")
|
||||||
|
|
||||||
from PyQt5.QtWidgets import QApplication
|
from PyQt5.QtWidgets import QApplication
|
||||||
|
|
||||||
from .app import MainWindow
|
|
||||||
|
|
||||||
|
|
||||||
def main(argv: list[str] | None = None) -> int:
|
def main(argv: list[str] | None = None) -> int:
|
||||||
args = list(sys.argv if argv is None else argv)
|
parser_args = list(sys.argv[1:] if argv is None else argv[1:])
|
||||||
qt_app = QApplication(args)
|
# Accept unknown Qt args (e.g. -platform offscreen) without advertising
|
||||||
|
# internal-only flags on the public CLI.
|
||||||
|
qt_args = parser_args
|
||||||
|
|
||||||
|
qt_app = QApplication([sys.argv[0], *qt_args] if argv is None else [argv[0], *qt_args])
|
||||||
qt_app.setApplicationName("Sensor Bench")
|
qt_app.setApplicationName("Sensor Bench")
|
||||||
qt_app.setOrganizationName("Orisys-Tech")
|
qt_app.setOrganizationName("Orisys-Tech")
|
||||||
|
|
||||||
window = MainWindow()
|
from .exhibition_app import ExhibitionWindow
|
||||||
window.show()
|
|
||||||
|
window = ExhibitionWindow()
|
||||||
|
window.showMaximized()
|
||||||
return qt_app.exec_()
|
return qt_app.exec_()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+120
-20
@@ -6,6 +6,9 @@ import sys
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
NON_SENSOR_CONFIG_STEMS = {"options", "bench_theme"}
|
||||||
|
|
||||||
|
|
||||||
def is_frozen() -> bool:
|
def is_frozen() -> bool:
|
||||||
return bool(getattr(sys, "frozen", False))
|
return bool(getattr(sys, "frozen", False))
|
||||||
|
|
||||||
@@ -29,20 +32,6 @@ def project_root() -> Path:
|
|||||||
return app_root()
|
return app_root()
|
||||||
|
|
||||||
|
|
||||||
def default_options_path() -> Path:
|
|
||||||
external = app_root() / "config" / "options.json"
|
|
||||||
if external.exists():
|
|
||||||
return external
|
|
||||||
return resource_root() / "config" / "options.json"
|
|
||||||
|
|
||||||
|
|
||||||
def default_steps_path() -> Path:
|
|
||||||
external = app_root() / "steps" / "default.json"
|
|
||||||
if external.exists():
|
|
||||||
return external
|
|
||||||
return resource_root() / "steps" / "default.json"
|
|
||||||
|
|
||||||
|
|
||||||
def default_locales_dir() -> Path:
|
def default_locales_dir() -> Path:
|
||||||
external = app_root() / "config" / "locales"
|
external = app_root() / "config" / "locales"
|
||||||
if external.exists():
|
if external.exists():
|
||||||
@@ -64,17 +53,128 @@ def default_orisys_sdk_root() -> Path:
|
|||||||
return Path(r"D:\repo\orisys-sdk")
|
return Path(r"D:\repo\orisys-sdk")
|
||||||
|
|
||||||
|
|
||||||
def default_ddjx01_config() -> str:
|
def local_sensor_configs_dir() -> Path:
|
||||||
if is_frozen():
|
"""Editable sensor configs next to the app (or repo) root."""
|
||||||
return "ddjx01"
|
return app_root() / "config"
|
||||||
path = default_orisys_sdk_root() / "config" / "ddjx01.json"
|
|
||||||
if path.exists():
|
|
||||||
return str(path)
|
def _is_sensor_config_json(path: Path) -> bool:
|
||||||
|
if path.suffix.lower() != ".json":
|
||||||
|
return False
|
||||||
|
if path.parent.name.lower() == "locales":
|
||||||
|
return False
|
||||||
|
if path.stem.lower() in NON_SENSOR_CONFIG_STEMS:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def list_local_sensor_config_files() -> list[Path]:
|
||||||
|
"""Return local editable sensor config JSON paths (excludes options/locales)."""
|
||||||
|
root = local_sensor_configs_dir()
|
||||||
|
if not root.exists():
|
||||||
|
return []
|
||||||
|
return sorted(p for p in root.glob("*.json") if _is_sensor_config_json(p))
|
||||||
|
|
||||||
|
|
||||||
|
def orisys_configs_dir() -> Path | None:
|
||||||
|
"""Prefer local sensor configs dir when present; else installed package dir."""
|
||||||
|
local = local_sensor_configs_dir()
|
||||||
|
if list_local_sensor_config_files():
|
||||||
|
return local
|
||||||
|
try:
|
||||||
|
import orisys
|
||||||
|
|
||||||
|
path = Path(orisys.configs.CONFIG_DIR)
|
||||||
|
return path if path.exists() else None
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def list_orisys_configs() -> list[str]:
|
||||||
|
"""Return selectable sensor configs.
|
||||||
|
|
||||||
|
Local ``config/*.json`` files (excluding non-sensor stems such as
|
||||||
|
``bench_theme``) are listed first as
|
||||||
|
absolute paths so Start uses the editable copy beside the EXE. Package stems
|
||||||
|
from the installed Orisys wheel are appended when not already covered.
|
||||||
|
"""
|
||||||
|
entries: list[str] = []
|
||||||
|
seen_stems: set[str] = set()
|
||||||
|
|
||||||
|
for path in list_local_sensor_config_files():
|
||||||
|
stem = path.stem.lower()
|
||||||
|
if stem in seen_stems:
|
||||||
|
continue
|
||||||
|
entries.append(str(path.resolve()))
|
||||||
|
seen_stems.add(stem)
|
||||||
|
|
||||||
|
try:
|
||||||
|
import orisys
|
||||||
|
|
||||||
|
for stem in orisys.configs.list_config():
|
||||||
|
key = str(stem).strip()
|
||||||
|
if not key:
|
||||||
|
continue
|
||||||
|
if key.lower() in NON_SENSOR_CONFIG_STEMS:
|
||||||
|
continue
|
||||||
|
if key.lower() in seen_stems:
|
||||||
|
continue
|
||||||
|
entries.append(key)
|
||||||
|
seen_stems.add(key.lower())
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
pass
|
||||||
|
|
||||||
|
return entries
|
||||||
|
|
||||||
|
|
||||||
|
def config_display_name(value: str) -> str:
|
||||||
|
"""Short label for a config path or package stem."""
|
||||||
|
text = str(value or "").strip()
|
||||||
|
if not text:
|
||||||
|
return ""
|
||||||
|
path = Path(text)
|
||||||
|
if path.suffix.lower() == ".json" or "/" in text or "\\" in text:
|
||||||
|
return path.stem
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def default_config_name(sensor_type: str = "orisys") -> str:
|
||||||
|
"""Preferred package config name for a sensor pipeline."""
|
||||||
|
kind = str(sensor_type or "orisys").strip().lower()
|
||||||
|
if kind in ("dd06", "dd06-ov", "roi", "polygon_roi"):
|
||||||
|
return "dd06-ov"
|
||||||
return "ddjx01"
|
return "ddjx01"
|
||||||
|
|
||||||
|
|
||||||
|
def _pick_default_config(preferred_stem: str) -> str:
|
||||||
|
entries = list_orisys_configs()
|
||||||
|
for entry in entries:
|
||||||
|
if config_display_name(entry).lower() == preferred_stem.lower():
|
||||||
|
return entry
|
||||||
|
if entries:
|
||||||
|
return entries[0]
|
||||||
|
return preferred_stem
|
||||||
|
|
||||||
|
|
||||||
|
def default_ddjx01_config() -> str:
|
||||||
|
"""Prefer local editable config path, else package stem."""
|
||||||
|
return _pick_default_config(default_config_name("orisys"))
|
||||||
|
|
||||||
|
|
||||||
def default_ddjx01_calibration() -> Path:
|
def default_ddjx01_calibration() -> Path:
|
||||||
external = app_root() / "config" / "ddjx01.npy"
|
external = app_root() / "config" / "ddjx01.npy"
|
||||||
if external.exists():
|
if external.exists():
|
||||||
return external
|
return external
|
||||||
return default_orisys_sdk_root() / "config" / "ddjx01.npy"
|
return default_orisys_sdk_root() / "config" / "ddjx01.npy"
|
||||||
|
|
||||||
|
|
||||||
|
def default_dd06_config() -> str:
|
||||||
|
preferred = default_config_name("dd06")
|
||||||
|
entries = list_orisys_configs()
|
||||||
|
for stem in (preferred, "dd06"):
|
||||||
|
for entry in entries:
|
||||||
|
if config_display_name(entry).lower() == stem.lower():
|
||||||
|
return entry
|
||||||
|
if entries:
|
||||||
|
return preferred
|
||||||
|
return preferred
|
||||||
|
|||||||
@@ -16,19 +16,70 @@ class PlotState:
|
|||||||
y_ranges: tuple[tuple[float, float], ...]
|
y_ranges: tuple[tuple[float, float], ...]
|
||||||
|
|
||||||
|
|
||||||
class MetricHistory:
|
def lowpass_filter(
|
||||||
"""Ring-style buffer for FPS / brightness (or other scalar metrics)."""
|
data,
|
||||||
|
cutoff_hz: float,
|
||||||
|
sample_rate_hz: float,
|
||||||
|
order: int = 2,
|
||||||
|
) -> np.ndarray:
|
||||||
|
"""Zero-phase Butterworth low-pass for uniformly sampled 1-D data."""
|
||||||
|
y = np.asarray(data, dtype=np.float64)
|
||||||
|
if cutoff_hz == 0:
|
||||||
|
return y.copy()
|
||||||
|
if y.size == 0:
|
||||||
|
return y
|
||||||
|
|
||||||
def __init__(self, npoints: int = 120, *, channels: int = 2, window_sec: float = 4.0):
|
nyquist = 0.5 * sample_rate_hz
|
||||||
|
if cutoff_hz < 0 or cutoff_hz >= nyquist:
|
||||||
|
raise ValueError(
|
||||||
|
f"cutoff_hz must be in (0, {nyquist}) for sample_rate_hz={sample_rate_hz}"
|
||||||
|
)
|
||||||
|
|
||||||
|
from scipy.signal import butter, filtfilt
|
||||||
|
|
||||||
|
b, a = butter(order, cutoff_hz / nyquist, btype="low")
|
||||||
|
padlen = 3 * (max(len(b), len(a)) - 1)
|
||||||
|
if y.size <= padlen:
|
||||||
|
return y.copy()
|
||||||
|
return filtfilt(b, a, y)
|
||||||
|
|
||||||
|
|
||||||
|
class MetricHistory:
|
||||||
|
"""Ring-style buffer for force / metric curves (viewer2d-compatible options)."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
npoints: int = 120,
|
||||||
|
*,
|
||||||
|
channels: int = 2,
|
||||||
|
window_sec: float = 4.0,
|
||||||
|
sample_rate_hz: float | None = None,
|
||||||
|
lowpass_cutoff_hz: float = 0.0,
|
||||||
|
y_axis_min_span: float | None = None,
|
||||||
|
y_range_alpha: float = 0.5,
|
||||||
|
):
|
||||||
self.npoints = int(npoints)
|
self.npoints = int(npoints)
|
||||||
self.channels = int(channels)
|
self.channels = int(channels)
|
||||||
self.window_sec = float(window_sec)
|
self.window_sec = float(window_sec)
|
||||||
|
self.sample_rate_hz = (
|
||||||
|
float(sample_rate_hz)
|
||||||
|
if sample_rate_hz is not None
|
||||||
|
else (self.npoints / self.window_sec if self.window_sec > 0 else 30.0)
|
||||||
|
)
|
||||||
|
self.lowpass_cutoff_hz = float(lowpass_cutoff_hz)
|
||||||
|
self.y_axis_min_span = (
|
||||||
|
None if y_axis_min_span is None else float(y_axis_min_span)
|
||||||
|
)
|
||||||
|
self.y_range_alpha = float(y_range_alpha)
|
||||||
self.reset()
|
self.reset()
|
||||||
|
|
||||||
def reset(self) -> None:
|
def reset(self) -> None:
|
||||||
self._data = np.full((self.npoints, 1 + self.channels), np.nan, dtype=np.float64)
|
self._data = np.full((self.npoints, 1 + self.channels), np.nan, dtype=np.float64)
|
||||||
self.sample_count = 0
|
self.sample_count = 0
|
||||||
self._t0: Optional[float] = None
|
self._t0: Optional[float] = None
|
||||||
|
self._y_range_smooth: list[Optional[tuple[float, float]]] = [
|
||||||
|
None for _ in range(self.channels)
|
||||||
|
]
|
||||||
|
|
||||||
def push(self, timestamp: float, *values: float) -> None:
|
def push(self, timestamp: float, *values: float) -> None:
|
||||||
if len(values) != self.channels:
|
if len(values) != self.channels:
|
||||||
@@ -44,6 +95,37 @@ class MetricHistory:
|
|||||||
self._data[:-1, :] = self._data[1:, :]
|
self._data[:-1, :] = self._data[1:, :]
|
||||||
self._data[-1, :] = row
|
self._data[-1, :] = row
|
||||||
|
|
||||||
|
def _smooth_y_range(self, channel: int, ymin: float, ymax: float) -> tuple[float, float]:
|
||||||
|
if self.y_axis_min_span is None:
|
||||||
|
if abs(ymax - ymin) < 1e-6:
|
||||||
|
pad = 1.0 if abs(ymin) < 1e-6 else abs(ymin) * 0.1
|
||||||
|
ymin -= pad
|
||||||
|
ymax += pad
|
||||||
|
else:
|
||||||
|
pad = (ymax - ymin) * 0.12
|
||||||
|
ymin -= pad
|
||||||
|
ymax += pad
|
||||||
|
return ymin, ymax
|
||||||
|
|
||||||
|
if self._y_range_smooth[channel] is None:
|
||||||
|
smoothed = (ymin, ymax)
|
||||||
|
else:
|
||||||
|
prev_min, prev_max = self._y_range_smooth[channel]
|
||||||
|
alpha = self.y_range_alpha
|
||||||
|
ymin = alpha * ymin + (1.0 - alpha) * prev_min
|
||||||
|
ymax = alpha * ymax + (1.0 - alpha) * prev_max
|
||||||
|
smoothed = (ymin, ymax)
|
||||||
|
|
||||||
|
span = smoothed[1] - smoothed[0]
|
||||||
|
if span < self.y_axis_min_span:
|
||||||
|
mid = 0.5 * (smoothed[0] + smoothed[1])
|
||||||
|
half = self.y_axis_min_span / 2.0
|
||||||
|
smoothed = (mid - half, mid + half)
|
||||||
|
|
||||||
|
self._y_range_smooth[channel] = smoothed
|
||||||
|
pad = max((smoothed[1] - smoothed[0]) * 0.12, 0.01)
|
||||||
|
return smoothed[0] - pad, smoothed[1] + pad
|
||||||
|
|
||||||
def state(self) -> Optional[PlotState]:
|
def state(self) -> Optional[PlotState]:
|
||||||
n = self.sample_count
|
n = self.sample_count
|
||||||
if n == 0:
|
if n == 0:
|
||||||
@@ -51,6 +133,14 @@ class MetricHistory:
|
|||||||
block = self._data[:n]
|
block = self._data[:n]
|
||||||
times = block[:, 0].copy()
|
times = block[:, 0].copy()
|
||||||
values = block[:, 1:].copy()
|
values = block[:, 1:].copy()
|
||||||
|
if self.lowpass_cutoff_hz > 0:
|
||||||
|
for i in range(self.channels):
|
||||||
|
values[:, i] = lowpass_filter(
|
||||||
|
values[:, i],
|
||||||
|
self.lowpass_cutoff_hz,
|
||||||
|
sample_rate_hz=self.sample_rate_hz,
|
||||||
|
)
|
||||||
|
|
||||||
if n < self.npoints:
|
if n < self.npoints:
|
||||||
x_range = (0.0, self.window_sec)
|
x_range = (0.0, self.window_sec)
|
||||||
else:
|
else:
|
||||||
@@ -67,15 +157,7 @@ class MetricHistory:
|
|||||||
ymax = float(np.nanmax(col))
|
ymax = float(np.nanmax(col))
|
||||||
if not np.isfinite(ymin) or not np.isfinite(ymax):
|
if not np.isfinite(ymin) or not np.isfinite(ymax):
|
||||||
ymin, ymax = 0.0, 1.0
|
ymin, ymax = 0.0, 1.0
|
||||||
if abs(ymax - ymin) < 1e-6:
|
y_ranges.append(self._smooth_y_range(i, ymin, ymax))
|
||||||
pad = 1.0 if abs(ymin) < 1e-6 else abs(ymin) * 0.1
|
|
||||||
ymin -= pad
|
|
||||||
ymax += pad
|
|
||||||
else:
|
|
||||||
pad = (ymax - ymin) * 0.12
|
|
||||||
ymin -= pad
|
|
||||||
ymax += pad
|
|
||||||
y_ranges.append((ymin, ymax))
|
|
||||||
|
|
||||||
return PlotState(
|
return PlotState(
|
||||||
times=times,
|
times=times,
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
"""Polygon ROI geometry helpers for DD06-style rectification."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
def load_json_config(path: str | Path) -> dict[str, Any]:
|
||||||
|
path = Path(path)
|
||||||
|
if path.exists():
|
||||||
|
with path.open("r", encoding="utf-8") as f:
|
||||||
|
return json.load(f)
|
||||||
|
try:
|
||||||
|
import orisys
|
||||||
|
|
||||||
|
return orisys.configs.load_config(str(path))
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
raise FileNotFoundError(f"Config not found as file or bundled name: {path}") from exc
|
||||||
|
|
||||||
|
|
||||||
|
def load_roi_polygon(config: dict[str, Any]):
|
||||||
|
"""Return (points Nx2 float32, image_size [w,h]) from nested or legacy ROI fields."""
|
||||||
|
roi = config.get("roi") if isinstance(config.get("roi"), dict) else config
|
||||||
|
if not isinstance(roi, dict):
|
||||||
|
raise ValueError("Config has no usable ROI section")
|
||||||
|
|
||||||
|
image_size = roi.get("image_size") or config.get("camera", {}).get("resolution_raw")
|
||||||
|
if image_size is None:
|
||||||
|
raise ValueError("ROI image_size is missing")
|
||||||
|
|
||||||
|
points = roi.get("points") or roi.get("polygon") or roi.get("roi_polygon")
|
||||||
|
if points is None and roi.get("xyxy") is not None:
|
||||||
|
x1, y1, x2, y2 = [int(v) for v in roi["xyxy"]]
|
||||||
|
points = [[x1, y1], [x2, y1], [x2, y2], [x1, y2]]
|
||||||
|
if not points:
|
||||||
|
raise ValueError("ROI points are missing")
|
||||||
|
|
||||||
|
pts = np.asarray(points, dtype=np.float32).reshape(-1, 2)
|
||||||
|
if pts.shape[0] < 4:
|
||||||
|
raise ValueError(f"ROI needs at least 4 points, got {pts.shape[0]}")
|
||||||
|
return pts, [int(image_size[0]), int(image_size[1])]
|
||||||
|
|
||||||
|
|
||||||
|
def scale_points(points, source_size, target_size):
|
||||||
|
source_w, source_h = float(source_size[0]), float(source_size[1])
|
||||||
|
target_w, target_h = float(target_size[0]), float(target_size[1])
|
||||||
|
pts = np.asarray(points, dtype=np.float32).reshape(-1, 2).copy()
|
||||||
|
if source_w > 0 and source_h > 0 and (source_w, source_h) != (target_w, target_h):
|
||||||
|
pts[:, 0] *= target_w / source_w
|
||||||
|
pts[:, 1] *= target_h / source_h
|
||||||
|
return pts
|
||||||
|
|
||||||
|
|
||||||
|
def order_quad_points(points):
|
||||||
|
"""Order 4 points as TL, TR, BR, BL for perspective transforms."""
|
||||||
|
pts = np.asarray(points, dtype=np.float32).reshape(-1, 2)
|
||||||
|
if pts.shape[0] != 4:
|
||||||
|
hull = cv2.convexHull(pts.astype(np.float32))
|
||||||
|
rect = cv2.minAreaRect(hull)
|
||||||
|
pts = cv2.boxPoints(rect).astype(np.float32)
|
||||||
|
|
||||||
|
s = pts.sum(axis=1)
|
||||||
|
diff = np.diff(pts, axis=1).reshape(-1)
|
||||||
|
ordered = np.zeros((4, 2), dtype=np.float32)
|
||||||
|
ordered[0] = pts[np.argmin(s)]
|
||||||
|
ordered[2] = pts[np.argmax(s)]
|
||||||
|
ordered[1] = pts[np.argmin(diff)]
|
||||||
|
ordered[3] = pts[np.argmax(diff)]
|
||||||
|
return ordered
|
||||||
|
|
||||||
|
|
||||||
|
def rectified_output_size(config, points=None):
|
||||||
|
"""Prefer camera.resolution_stitch; fall back to ROI bounding box size."""
|
||||||
|
stitch = config.get("camera", {}).get("resolution_stitch")
|
||||||
|
if stitch is not None and len(stitch) >= 2:
|
||||||
|
return int(stitch[0]), int(stitch[1])
|
||||||
|
if points is not None:
|
||||||
|
pts = np.asarray(points, dtype=np.float32).reshape(-1, 2)
|
||||||
|
width = max(
|
||||||
|
int(np.linalg.norm(pts[1] - pts[0])),
|
||||||
|
int(np.linalg.norm(pts[2] - pts[3])),
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
height = max(
|
||||||
|
int(np.linalg.norm(pts[3] - pts[0])),
|
||||||
|
int(np.linalg.norm(pts[2] - pts[1])),
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
return width, height
|
||||||
|
raise ValueError("Cannot determine rectified output size")
|
||||||
|
|
||||||
|
|
||||||
|
def build_roi_warp(points, source_image_size, frame_size, output_size):
|
||||||
|
frame_w, frame_h = int(frame_size[0]), int(frame_size[1])
|
||||||
|
scaled = scale_points(points, source_image_size, (frame_w, frame_h))
|
||||||
|
src = order_quad_points(scaled)
|
||||||
|
out_w, out_h = int(output_size[0]), int(output_size[1])
|
||||||
|
dst = np.array(
|
||||||
|
[
|
||||||
|
[0.0, 0.0],
|
||||||
|
[out_w - 1.0, 0.0],
|
||||||
|
[out_w - 1.0, out_h - 1.0],
|
||||||
|
[0.0, out_h - 1.0],
|
||||||
|
],
|
||||||
|
dtype=np.float32,
|
||||||
|
)
|
||||||
|
matrix = cv2.getPerspectiveTransform(src, dst)
|
||||||
|
return matrix, src, (out_w, out_h)
|
||||||
|
|
||||||
|
|
||||||
|
def warp_roi_to_rectangle(frame, matrix, output_size):
|
||||||
|
out_w, out_h = int(output_size[0]), int(output_size[1])
|
||||||
|
return cv2.warpPerspective(
|
||||||
|
frame,
|
||||||
|
matrix,
|
||||||
|
(out_w, out_h),
|
||||||
|
flags=cv2.INTER_LINEAR,
|
||||||
|
borderMode=cv2.BORDER_REPLICATE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def open_video_source(video):
|
||||||
|
"""Open a camera index or video file. Returns (cap, resolved_source)."""
|
||||||
|
text = str(video).strip()
|
||||||
|
if text.isdigit():
|
||||||
|
source = int(text)
|
||||||
|
cap = cv2.VideoCapture(source, cv2.CAP_DSHOW)
|
||||||
|
if not cap.isOpened():
|
||||||
|
cap.release()
|
||||||
|
cap = cv2.VideoCapture(source)
|
||||||
|
else:
|
||||||
|
path = Path(text)
|
||||||
|
if not path.exists():
|
||||||
|
raise FileNotFoundError(f"Video not found: {path}")
|
||||||
|
source = str(path)
|
||||||
|
cap = cv2.VideoCapture(source)
|
||||||
|
if not cap.isOpened():
|
||||||
|
raise RuntimeError(f"Failed to open video source: {video}")
|
||||||
|
return cap, source
|
||||||
+123
-52
@@ -35,10 +35,13 @@ def test_translator_chinese_default() -> None:
|
|||||||
assert DEFAULT_LOCALE == "zh_CN"
|
assert DEFAULT_LOCALE == "zh_CN"
|
||||||
tr = Translator(default_locales_dir())
|
tr = Translator(default_locales_dir())
|
||||||
assert tr.locale == "zh_CN"
|
assert tr.locale == "zh_CN"
|
||||||
assert "传感器" in tr.t("app.title")
|
assert "传感器" in tr.t("app.title") or "展示" in tr.t("exhibition.title")
|
||||||
tr.set_locale("en_US")
|
tr.set_locale("en_US")
|
||||||
assert tr.t("app.title") == "Sensor Bench"
|
assert tr.t("app.title") == "Sensor Bench"
|
||||||
assert "Power on" in tr.step_title("power_on")
|
assert "Exhibition" in tr.t("exhibition.title")
|
||||||
|
# Public locales must not ship internal inspection workflow copy.
|
||||||
|
assert "workflow" not in (tr._catalogs.get("en_US") or {})
|
||||||
|
assert tr._lookup("en_US", "workflow.title") is None
|
||||||
|
|
||||||
|
|
||||||
def test_sdk_worker_packet_mapping() -> None:
|
def test_sdk_worker_packet_mapping() -> None:
|
||||||
@@ -65,6 +68,9 @@ def test_sdk_worker_packet_mapping() -> None:
|
|||||||
def read_info(self, *args):
|
def read_info(self, *args):
|
||||||
return 1.0, 2.0, 3.0
|
return 1.0, 2.0, 3.0
|
||||||
|
|
||||||
|
def configure(self, **kwargs):
|
||||||
|
return self
|
||||||
|
|
||||||
def disconnect(self):
|
def disconnect(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -102,65 +108,127 @@ def test_sdk_worker_packet_mapping() -> None:
|
|||||||
assert packet.processed_frame.shape[:2] == (365, 365)
|
assert packet.processed_frame.shape[:2] == (365, 365)
|
||||||
|
|
||||||
|
|
||||||
def test_mainwindow_offscreen() -> None:
|
def test_force_calibration_and_plot_min_span() -> None:
|
||||||
|
from sensor_bench.force_calibration import (
|
||||||
|
NORMAL_FORCE_CAL_OFFSET,
|
||||||
|
Y_AXIS_MIN_SPAN,
|
||||||
|
calibrate_normal_force,
|
||||||
|
)
|
||||||
|
from sensor_bench.plot_history import MetricHistory
|
||||||
|
|
||||||
|
assert abs(calibrate_normal_force(0.0) - NORMAL_FORCE_CAL_OFFSET) < 1e-12
|
||||||
|
hist = MetricHistory(
|
||||||
|
npoints=8,
|
||||||
|
channels=3,
|
||||||
|
window_sec=1.0,
|
||||||
|
sample_rate_hz=30.0,
|
||||||
|
lowpass_cutoff_hz=0.0,
|
||||||
|
y_axis_min_span=Y_AXIS_MIN_SPAN,
|
||||||
|
y_range_alpha=0.5,
|
||||||
|
)
|
||||||
|
for i in range(5):
|
||||||
|
hist.push(float(i) / 30.0, 100.0, 100.0, 100.0)
|
||||||
|
state = hist.state()
|
||||||
|
assert state is not None
|
||||||
|
for ymin, ymax in state.y_ranges:
|
||||||
|
assert (ymax - ymin) >= Y_AXIS_MIN_SPAN * 0.99
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_orisys_configs_excludes_options() -> None:
|
||||||
|
from sensor_bench.paths import (
|
||||||
|
config_display_name,
|
||||||
|
list_local_sensor_config_files,
|
||||||
|
list_orisys_configs,
|
||||||
|
local_sensor_configs_dir,
|
||||||
|
)
|
||||||
|
|
||||||
|
entries = list_orisys_configs()
|
||||||
|
labels = [config_display_name(e).lower() for e in entries]
|
||||||
|
assert "options" not in labels
|
||||||
|
assert all(Path(e).stem.lower() != "options" for e in entries if Path(e).suffix.lower() == ".json")
|
||||||
|
|
||||||
|
local_ddjx = local_sensor_configs_dir() / "ddjx01.json"
|
||||||
|
if local_ddjx.exists():
|
||||||
|
local_files = list_local_sensor_config_files()
|
||||||
|
assert any(p.name == "ddjx01.json" for p in local_files)
|
||||||
|
assert any(config_display_name(e).lower() == "ddjx01" for e in entries)
|
||||||
|
|
||||||
|
|
||||||
|
def test_roi_geometry_helpers() -> None:
|
||||||
|
from sensor_bench.roi_geometry import build_roi_warp, order_quad_points, warp_roi_to_rectangle
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
points = np.array([[10, 10], [100, 12], [98, 90], [12, 88]], dtype=np.float32)
|
||||||
|
ordered = order_quad_points(points)
|
||||||
|
assert ordered.shape == (4, 2)
|
||||||
|
matrix, src, size = build_roi_warp(points, (120, 100), (120, 100), (64, 64))
|
||||||
|
assert size == (64, 64)
|
||||||
|
assert src.shape == (4, 2)
|
||||||
|
frame = np.zeros((100, 120, 3), dtype=np.uint8)
|
||||||
|
out = warp_roi_to_rectangle(frame, matrix, size)
|
||||||
|
assert out.shape == (64, 64, 3)
|
||||||
|
|
||||||
|
|
||||||
|
def test_exhibition_window_offscreen() -> None:
|
||||||
from PyQt5.QtWidgets import QApplication
|
from PyQt5.QtWidgets import QApplication
|
||||||
from sensor_bench.app import MainWindow
|
from sensor_bench.exhibition_app import ExhibitionWindow
|
||||||
from sensor_bench.acquisition import FixedCameraMode
|
|
||||||
from sensor_bench.i18n import DEFAULT_LOCALE
|
from sensor_bench.i18n import DEFAULT_LOCALE
|
||||||
|
|
||||||
app = QApplication.instance() or QApplication([])
|
app = QApplication.instance() or QApplication([])
|
||||||
win = MainWindow()
|
win = ExhibitionWindow()
|
||||||
assert win.tr.locale == DEFAULT_LOCALE == "zh_CN"
|
assert win.tr.locale == DEFAULT_LOCALE == "zh_CN"
|
||||||
assert "传感器" in win.windowTitle()
|
assert "展示" in win.windowTitle() or "Sensor Bench" in win.windowTitle()
|
||||||
assert win.btn_start.text() == "开始预览"
|
assert win.btn_start is not None
|
||||||
assert win.camera_index.value() == 0
|
assert win.btn_stop is not None
|
||||||
assert win.btn_next is not None
|
|
||||||
assert win.btn_revert is not None
|
|
||||||
assert win.btn_reset is not None
|
assert win.btn_reset is not None
|
||||||
assert win.sensor_type_combo.count() == 2
|
assert win.btn_record is not None
|
||||||
assert win.indentor_combo.count() == 2
|
assert win.btn_search is not None
|
||||||
assert win.platform_combo.count() == 3
|
assert win.btn_camera is not None
|
||||||
assert win.selected_sensor_options()["sensor_type"] == "BOX"
|
assert not hasattr(win, "btn_cal")
|
||||||
assert win.selected_sensor_options()["indentor"] == "1mm_circle"
|
assert not hasattr(win, "cal_edit")
|
||||||
assert win.selected_sensor_options()["platform"] == "manual"
|
assert win.btn_reset.text() == "重置"
|
||||||
assert not hasattr(win, "resolution_combo")
|
assert win.image_label is not None
|
||||||
assert not hasattr(win, "btn_search")
|
assert win.log_widget is not None
|
||||||
assert win._current_step_index == 0
|
assert not win.show_log_check.isChecked()
|
||||||
assert not win.btn_revert.isEnabled()
|
assert win.log_widget.isHidden()
|
||||||
assert win.btn_next.isEnabled()
|
assert win.show_log_check.text() == "显示日志面板"
|
||||||
|
win.show_log_check.setChecked(True)
|
||||||
|
assert win._log_panel_visible
|
||||||
|
assert not win.log_widget.isHidden()
|
||||||
|
win.show_log_check.setChecked(False)
|
||||||
|
assert win.log_widget.isHidden()
|
||||||
|
assert not win._log_panel_visible
|
||||||
|
assert len(win.plots) == 3
|
||||||
|
assert len(win.curves) == 3
|
||||||
|
assert win.btn_start.text() == "启动"
|
||||||
|
assert not win.btn_stop.isEnabled()
|
||||||
|
assert win._view_mode == "pure_color"
|
||||||
|
assert win.view_combo.currentData() == "pure_color"
|
||||||
|
assert win.about_action is not None
|
||||||
|
assert win.help_menu is not None
|
||||||
|
assert win.sensor_combo.count() == 2
|
||||||
|
assert win.config_combo is not None
|
||||||
|
assert win.config_combo.count() >= 1
|
||||||
|
from sensor_bench.paths import list_orisys_configs
|
||||||
|
|
||||||
|
installed = list_orisys_configs()
|
||||||
|
if "ddjx01" in installed:
|
||||||
|
assert win.config_combo.currentData() == "ddjx01" or win._selected_config() == "ddjx01"
|
||||||
|
|
||||||
# Language switch refreshes labels without restart.
|
|
||||||
win.language_combo.setCurrentIndex(1)
|
win.language_combo.setCurrentIndex(1)
|
||||||
assert win.tr.locale == "en_US"
|
assert win.tr.locale == "en_US"
|
||||||
assert win.windowTitle() == "Sensor Bench"
|
assert "Exhibition" in win.windowTitle()
|
||||||
assert win.btn_start.text() == "Start Preview"
|
assert win.btn_start.text() == "Start"
|
||||||
|
assert win.btn_reset.text() == "Reset"
|
||||||
|
assert win.show_log_check.text() == "Show log panel"
|
||||||
|
assert "About" in win.about_action.text()
|
||||||
|
|
||||||
# Workflow next / revert / reset.
|
win.view_combo.setCurrentIndex(1)
|
||||||
total = len(win.workflow.steps)
|
assert win._view_mode == "captured"
|
||||||
win._on_step_next()
|
win.sensor_combo.setCurrentIndex(1)
|
||||||
assert win._current_step_index == 1
|
assert win._sensor_type == "dd06"
|
||||||
assert win.btn_revert.isEnabled()
|
selected = str(win._selected_config()).lower()
|
||||||
win._on_step_revert()
|
assert "dd06" in selected or selected.endswith(".json")
|
||||||
assert win._current_step_index == 0
|
|
||||||
for _ in range(total):
|
|
||||||
win._on_step_next()
|
|
||||||
assert win._current_step_index == total
|
|
||||||
assert not win.btn_next.isEnabled()
|
|
||||||
assert "completed" in win.workflow_status.text().lower() or "完成" in win.workflow_status.text()
|
|
||||||
win._on_step_reset()
|
|
||||||
assert win._current_step_index == 0
|
|
||||||
assert win.btn_next.isEnabled()
|
|
||||||
|
|
||||||
win._fixed_mode = FixedCameraMode(
|
|
||||||
width=1920,
|
|
||||||
height=1080,
|
|
||||||
fps=30.0,
|
|
||||||
config_ref=str(ROOT / "tests" / "fixtures" / "ddjx01_min.json"),
|
|
||||||
stitch_width=365,
|
|
||||||
stitch_height=365,
|
|
||||||
)
|
|
||||||
win.mode_label.setText(win.t("labels.fixed_mode", mode=win._fixed_mode.label()))
|
|
||||||
assert "1920x1080" in win.mode_label.text()
|
|
||||||
win.close()
|
win.close()
|
||||||
|
|
||||||
|
|
||||||
@@ -168,5 +236,8 @@ if __name__ == "__main__":
|
|||||||
test_load_fixed_mode()
|
test_load_fixed_mode()
|
||||||
test_translator_chinese_default()
|
test_translator_chinese_default()
|
||||||
test_sdk_worker_packet_mapping()
|
test_sdk_worker_packet_mapping()
|
||||||
test_mainwindow_offscreen()
|
test_force_calibration_and_plot_min_span()
|
||||||
|
test_list_orisys_configs_excludes_options()
|
||||||
|
test_roi_geometry_helpers()
|
||||||
|
test_exhibition_window_offscreen()
|
||||||
print("ok")
|
print("ok")
|
||||||
|
|||||||
Reference in New Issue
Block a user