added camera search and select, auto preference saving and version tag to 0.4.2 #8

Merged
zhangyijian merged 1 commits from feature/usbcam_search into dev 2026-07-15 15:11:00 +08:00
9 changed files with 525 additions and 18 deletions
Showing only changes of commit 11a747a771 - Show all commits
+1 -1
View File
@@ -25,7 +25,7 @@ pip install orisys-{version}-{python-version}-{python-version}-{os}_{machine}.wh
```
如
```bash
pip install orisys-0.4.1-cp310-cp310-win_amd64.whl
pip install orisys-0.4.2-cp310-cp310-win_amd64.whl
```
3. **安装 CuPy(根据您的 CUDA 版本选择):**
+5 -5
View File
@@ -1,5 +1,5 @@
叠动科技软件使用手册
版本: 0.4.1
版本: 0.4.2
更新日期: 2026年7月13日
# 简介
@@ -57,9 +57,9 @@ orisys SDK 以 .whl 文件形式提供,您需要根据操作系统、Python
## 下载正确的 .whl 文件
从官方渠道获取 .whl 文件,文件名格式通常为:
orisys-{版本}-{Python版本}-{Python版本}-{操作系统}_{硬件架构}.whl
例如:orisys-0.4.1-cp310-cp310-win_amd64.whl 表示:
例如:orisys-0.4.2-cp310-cp310-win_amd64.whl 表示:
版本:0.4.1
版本:0.4.2
Python 版本:3.10(cp310)
操作系统:Windows(win)
硬件架构:64位(amd64)
@@ -71,7 +71,7 @@ Python 版本:3.10(cp310)
``` bash
# 请将文件名替换为您实际下载的文件名
pip install orisys-0.4.1-cp310-cp310-win_amd64.whl
pip install orisys-0.4.2-cp310-cp310-win_amd64.whl
```
## 安装 CuPy(可选)
@@ -108,7 +108,7 @@ python
import orisys
print(orisys.__version__)
```
如果输出版本号(例如 0.4.1),则说明安装成功。
如果输出版本号(例如 0.4.2),则说明安装成功。
# 使用方法
## 运行示例程序
+139
View File
@@ -0,0 +1,139 @@
"""Discover USB cameras for the dashboard (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"):
# Prefer v4l2 for USB camera indices on 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
+129 -3
View File
@@ -22,6 +22,8 @@ from pipeline.csv_recorder import CsvRecorder
from pipeline.service import FingerPipeline
from .app_paths import resource_path
from .camera_discovery import list_cameras, pick_default_camera
from .preferences import get_preferred_video_source, load_preferences, save_preferences
from .layout import (
DashboardLayout,
LAYOUT,
@@ -102,26 +104,45 @@ class FingerDashboardWindow(QMainWindow):
mesh_state=self.mesh_state,
show_log=self._log_panel_visible,
)
# CLI / pipeline defaults first, then overlay saved user preferences.
self.settings_panel.set_video_source(str(self.config.video))
self.settings_panel.set_config_path(self.config.config)
self.settings_panel.set_calibration_path(self.config.calib or "")
self.settings_panel.set_finger_export_dir(self.config.export_dir)
self.settings_panel.set_csv_save_dir(self.config.csv_save_dir)
self.settings_panel.log_visibility_changed.connect(self._set_log_panel_visible)
self.settings_panel.free_rotation_changed.connect(self.mesh_viewport.set_free_rotation)
self._preferences = load_preferences()
self.settings_panel.apply_preference_state(self._preferences)
self._log_panel_visible = bool(self.settings_panel.show_log_check.isChecked())
self.mesh_viewport.set_free_rotation(bool(self.settings_panel.free_rotate_check.isChecked()))
if self.arrow_panel is not None:
self.settings_panel.arrow_background_changed.connect(self.arrow_panel.set_background)
self.arrow_panel.set_background(self.settings_panel.arrow_background())
# Sync pipeline config fields from restored panel values.
self.config.video = self.settings_panel.video_source() or self.config.video
self.config.config = self.settings_panel.config_path() or self.config.config
self.config.export_dir = self.settings_panel.finger_export_dir() or self.config.export_dir
self.config.csv_save_dir = self.settings_panel.csv_save_dir() or self.config.csv_save_dir
self.settings_panel.log_visibility_changed.connect(self._on_log_visibility_changed)
self.settings_panel.free_rotation_changed.connect(self._on_free_rotation_changed)
if self.arrow_panel is not None:
self.settings_panel.arrow_background_changed.connect(self._on_arrow_background_changed)
else:
self.settings_panel.arrow_background_changed.connect(lambda _v: self._save_preferences())
self.settings_panel.view_reset_requested.connect(self.mesh_viewport.reset_view)
self.settings_panel.browse_video_requested.connect(self._select_camera)
self.settings_panel.camera_refresh_requested.connect(self._refresh_cameras)
self.settings_panel.camera_selection_changed.connect(self._on_camera_selection_changed)
self.settings_panel.browse_config_requested.connect(self._select_config)
self.settings_panel.browse_finger_assets_requested.connect(self._select_finger_assets)
self.settings_panel.browse_csv_dir_requested.connect(self._select_csv_dir)
self.settings_panel.csv_record_toggled.connect(self._on_csv_record_toggled)
self.settings_panel.show_flow_arrows_check.toggled.connect(lambda _v: self._save_preferences())
self.settings_panel.btn_play_pause.clicked.connect(self._on_play_pause_clicked)
self.settings_panel.btn_reset_tracker.clicked.connect(self._on_reset)
self.settings_panel.set_play_pause_state("run")
self.settings_panel.set_csv_controls_for_pipeline(False)
self._refresh_cameras(announce=True)
self._panel_widgets: dict[str, QWidget] = {}
self._panel_widgets["mesh"] = self.mesh_viewport
@@ -162,6 +183,37 @@ class FingerDashboardWindow(QMainWindow):
self.log_widget.setVisible(self._log_panel_visible)
QTimer.singleShot(0, self._apply_panel_layout)
def _on_log_visibility_changed(self, visible: bool) -> None:
self._set_log_panel_visible(visible)
self._save_preferences()
def _on_free_rotation_changed(self, enabled: bool) -> None:
self.mesh_viewport.set_free_rotation(enabled)
self._save_preferences()
def _on_arrow_background_changed(self, value: str) -> None:
if self.arrow_panel is not None:
self.arrow_panel.set_background(value)
self._save_preferences()
def _save_preferences(self) -> None:
prefs = self.settings_panel.preference_state()
self._preferences = prefs
save_preferences(prefs)
# Keep in-memory pipeline config aligned with panel values.
video = prefs.get("video_source")
if isinstance(video, str) and video.strip():
self.config.video = video.strip()
config_path = prefs.get("config_path")
if isinstance(config_path, str) and config_path.strip():
self.config.config = config_path.strip()
assets = prefs.get("finger_assets_dir")
if isinstance(assets, str) and assets.strip():
self.config.export_dir = assets.strip()
csv_dir = prefs.get("csv_save_dir")
if isinstance(csv_dir, str) and csv_dir.strip():
self.config.csv_save_dir = csv_dir.strip()
def _apply_panel_layout(self) -> None:
setup = self.layout.panel_setup
scale = self._layout_scale_factor()
@@ -323,6 +375,14 @@ class FingerDashboardWindow(QMainWindow):
self.config.export_dir = self.settings_panel.finger_export_dir()
self.config.csv_save_dir = self.settings_panel.csv_save_dir() or "./outputs/csv"
if not self.config.video:
self._set_play_pause_button("run")
self.settings_panel.set_finger_export_dir_enabled(True)
print("请先选择摄像机或视频文件。")
return
self._save_preferences()
try:
self.pipeline = FingerPipeline(self.config)
self.pipeline.open()
@@ -342,6 +402,7 @@ class FingerDashboardWindow(QMainWindow):
self.timer.start(self.layout.timer_interval_ms)
self._set_play_pause_button("pause")
self.settings_panel.btn_reset_tracker.setEnabled(True)
self.settings_panel.set_camera_controls_enabled(False)
self.settings_panel.set_csv_controls_for_pipeline(True)
print(
f"▶ 启动 — 摄像机={self.config.video}, 配置={self.config.config}, "
@@ -364,7 +425,9 @@ class FingerDashboardWindow(QMainWindow):
self._set_play_pause_button("run")
self.settings_panel.btn_reset_tracker.setEnabled(False)
self.settings_panel.set_finger_export_dir_enabled(True)
self.settings_panel.set_camera_controls_enabled(True)
self.settings_panel.set_csv_controls_for_pipeline(False)
self._update_play_enabled_for_source()
def _on_reset(self) -> None:
if not self._running:
@@ -459,6 +522,60 @@ class FingerDashboardWindow(QMainWindow):
)
)
def _refresh_cameras(self, announce: bool = True) -> None:
"""Scan cameras and populate the settings dropdown."""
keep_file = self.settings_panel.is_file_source()
existing_file = self.settings_panel.video_source() if keep_file else ""
preferred = None
current = self.settings_panel.selected_camera_source()
saved = get_preferred_video_source(self._preferences)
cli_video = str(self.config.video).strip()
if current.isdigit():
preferred = current
elif saved and str(saved).isdigit():
preferred = str(saved)
elif cli_video.isdigit():
preferred = cli_video
cameras = list_cameras()
selected = pick_default_camera(cameras, preferred)
self.settings_panel.set_camera_options(cameras, selected)
if keep_file and existing_file:
self.settings_panel.set_video_source(existing_file)
elif selected:
self.settings_panel.set_video_source(selected)
self.config.video = selected
elif not keep_file:
self.config.video = ""
self._update_play_enabled_for_source()
self._save_preferences()
if announce:
if cameras:
names = ", ".join(cam.label() for cam in cameras)
print(f"已发现 {len(cameras)} 路摄像机:{names}")
if selected and not keep_file:
print(f"当前摄像机:{selected}")
else:
print("未发现可用摄像机。可点击 Refresh 重扫,或通过 … 选择视频文件。")
def _update_play_enabled_for_source(self) -> None:
if self._running:
return
usable = self.settings_panel.has_usable_source()
self.settings_panel.set_play_pause_state("run")
self.settings_panel.btn_play_pause.setEnabled(usable)
def _on_camera_selection_changed(self, camera_id: str) -> None:
camera_id = str(camera_id).strip()
if not camera_id:
return
self.config.video = camera_id
self._save_preferences()
self._update_play_enabled_for_source()
def _select_camera(self) -> None:
path, _ = QFileDialog.getOpenFileName(
self,
@@ -468,6 +585,9 @@ class FingerDashboardWindow(QMainWindow):
)
if path:
self.settings_panel.set_video_source(path)
self.config.video = path
self._save_preferences()
self._update_play_enabled_for_source()
def _select_config(self) -> None:
path, _ = QFileDialog.getOpenFileName(
@@ -478,6 +598,8 @@ class FingerDashboardWindow(QMainWindow):
)
if path:
self.settings_panel.set_config_path(path)
self.config.config = path
self._save_preferences()
def _select_finger_assets(self) -> None:
path = QFileDialog.getExistingDirectory(
@@ -487,6 +609,8 @@ class FingerDashboardWindow(QMainWindow):
)
if path:
self.settings_panel.set_finger_export_dir(path)
self.config.export_dir = path
self._save_preferences()
def _select_csv_dir(self) -> None:
path = QFileDialog.getExistingDirectory(
@@ -497,6 +621,7 @@ class FingerDashboardWindow(QMainWindow):
if path:
self.settings_panel.set_csv_save_dir(path)
self.config.csv_save_dir = path
self._save_preferences()
def _on_csv_record_toggled(self, start: bool) -> None:
if start:
@@ -556,6 +681,7 @@ class FingerDashboardWindow(QMainWindow):
def closeEvent(self, event):
self.timer.stop()
self._stop_csv_recording(announce=True)
self._save_preferences()
self.mesh_viewport.close_renderer()
self.pipeline.close()
event.accept()
+10
View File
@@ -0,0 +1,10 @@
{
"video_source": "0",
"config_path": "./config/dd02-ov.json",
"finger_assets_dir": "D:/repo/orisys-sdk/assets/finger/30fps-0703",
"csv_save_dir": "./outputs/csv",
"show_log": true,
"show_flow_arrows": false,
"arrow_background": "pure_color",
"free_rotate": true
}
+64
View File
@@ -0,0 +1,64 @@
"""Load/save dashboard user preferences near this module."""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
from .app_paths import resource_path
PREFERENCE_FILE = "preference.json"
LEGACY_CAMERA_SELECTION_FILE = "camera_selection.json"
_GUI_DIR = Path(__file__).resolve().parent
def preference_path() -> Path:
# Keep preference file beside this module: examples/gui/preference.json
return _GUI_DIR / PREFERENCE_FILE
def legacy_camera_selection_path() -> Path:
return resource_path(LEGACY_CAMERA_SELECTION_FILE)
def _read_json(path: Path) -> dict[str, Any]:
if not path.is_file():
return {}
try:
data = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return {}
return data if isinstance(data, dict) else {}
def load_preferences() -> dict[str, Any]:
"""Load preferences, migrating camera_id from legacy file when needed."""
prefs = _read_json(preference_path())
if "video_source" not in prefs:
legacy = _read_json(legacy_camera_selection_path())
camera_id = legacy.get("camera_id")
if camera_id is not None and str(camera_id).strip():
prefs["video_source"] = str(camera_id).strip()
return prefs
def save_preferences(prefs: dict[str, Any]) -> None:
"""Write preferences atomically enough for dashboard use."""
if not isinstance(prefs, dict):
return
path = preference_path()
try:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(prefs, indent=2) + "\n", encoding="utf-8")
except OSError:
pass
def get_preferred_video_source(prefs: dict[str, Any] | None = None) -> str | None:
data = prefs if prefs is not None else load_preferences()
value = data.get("video_source")
if value is None:
return None
text = str(value).strip()
return text or None
+175 -7
View File
@@ -43,6 +43,8 @@ class SettingsPanelWidget(QWidget):
browse_finger_assets_requested = pyqtSignal()
browse_csv_dir_requested = pyqtSignal()
csv_record_toggled = pyqtSignal(bool)
camera_refresh_requested = pyqtSignal()
camera_selection_changed = pyqtSignal(str)
def __init__(
self,
@@ -105,9 +107,16 @@ class SettingsPanelWidget(QWidget):
root.addWidget(self._caption("Video source"))
video_row = QHBoxLayout()
video_row.setSpacing(6)
self.video_edit = QLineEdit("0")
self.video_edit.setPlaceholderText("Camera ID or video path")
video_row.addWidget(self.video_edit, 1)
self.camera_combo = QComboBox()
self.camera_combo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.camera_combo.setToolTip("Select an available camera")
self.camera_combo.currentIndexChanged.connect(self._on_camera_combo_changed)
video_row.addWidget(self.camera_combo, 1)
self.btn_refresh_cameras = QPushButton("Refresh")
self.btn_refresh_cameras.setFixedWidth(64)
self.btn_refresh_cameras.setToolTip("Re-scan available cameras")
self.btn_refresh_cameras.clicked.connect(self.camera_refresh_requested.emit)
video_row.addWidget(self.btn_refresh_cameras)
self.btn_browse_video = QPushButton("…")
self.btn_browse_video.setFixedWidth(32)
self.btn_browse_video.setToolTip("Browse video file")
@@ -115,6 +124,16 @@ class SettingsPanelWidget(QWidget):
video_row.addWidget(self.btn_browse_video)
root.addLayout(video_row)
self.video_file_label = QLabel("")
self.video_file_label.setObjectName("settingsCaption")
self.video_file_label.setWordWrap(True)
self.video_file_label.hide()
root.addWidget(self.video_file_label)
self._source_mode = "camera" # "camera" | "file"
self._video_file_path = ""
self._updating_camera_options = False
root.addWidget(self._caption("Config"))
config_row = QHBoxLayout()
config_row.setSpacing(6)
@@ -218,8 +237,103 @@ class SettingsPanelWidget(QWidget):
style.polish(btn)
btn.update()
def set_camera_options(self, cameras, selected_value: str | None = None) -> None:
"""Populate camera combo from discovered cameras.
``cameras`` items may be objects with ``.source`` / ``.label()`` or
``(source, label)`` tuples.
"""
self._updating_camera_options = True
self.camera_combo.blockSignals(True)
self.camera_combo.clear()
sources: list[str] = []
for cam in cameras:
if hasattr(cam, "source") and callable(getattr(cam, "label", None)):
source = str(cam.source)
label = str(cam.label())
else:
source, label = cam
source = str(source)
label = str(label)
self.camera_combo.addItem(label, source)
sources.append(source)
preferred = (selected_value or "").strip()
idx = self.camera_combo.findData(preferred) if preferred else -1
if idx < 0 and self.camera_combo.count() > 0:
idx = 0
if idx >= 0:
self.camera_combo.setCurrentIndex(idx)
# File mode is cleared only when we intentionally select a camera.
if self._source_mode != "file":
self._source_mode = "camera"
self._video_file_path = ""
self.video_file_label.hide()
self.camera_combo.setEnabled(self.camera_combo.count() > 0)
self.camera_combo.blockSignals(False)
self._updating_camera_options = False
def set_video_source(self, value: str) -> None:
self.video_edit.setText(value)
"""Set current video source (camera index string or file path)."""
value = (value or "").strip()
if not value:
return
# Numeric camera index → select in combo if present; else keep as preferred text via adding.
if value.isdigit() or (value.startswith("-") and value[1:].isdigit()):
idx = self.camera_combo.findData(value)
if idx >= 0:
self._source_mode = "camera"
self._video_file_path = ""
self.video_file_label.hide()
self.camera_combo.blockSignals(True)
self.camera_combo.setCurrentIndex(idx)
self.camera_combo.blockSignals(False)
else:
# Not yet scanned / not present: remember as pending camera selection.
self._source_mode = "camera"
self._video_file_path = ""
self.video_file_label.hide()
return
# Treat as video file path.
self._source_mode = "file"
self._video_file_path = value
self.video_file_label.setText(f"File: {value}")
self.video_file_label.show()
def selected_camera_source(self) -> str:
data = self.camera_combo.currentData()
return str(data) if data is not None else ""
def video_source(self) -> str:
if self._source_mode == "file" and self._video_file_path:
return self._video_file_path
return self.selected_camera_source()
def has_usable_source(self) -> bool:
if self._source_mode == "file" and self._video_file_path:
return True
return bool(self.selected_camera_source())
def is_file_source(self) -> bool:
return self._source_mode == "file" and bool(self._video_file_path)
def set_camera_controls_enabled(self, enabled: bool) -> None:
self.btn_refresh_cameras.setEnabled(enabled)
self.btn_browse_video.setEnabled(enabled)
self.camera_combo.setEnabled(enabled and self.camera_combo.count() > 0)
def _on_camera_combo_changed(self, _index: int) -> None:
if self._updating_camera_options:
return
source = self.selected_camera_source()
if not source:
return
self._source_mode = "camera"
self._video_file_path = ""
self.video_file_label.hide()
self.camera_selection_changed.emit(source)
def set_config_path(self, value: str) -> None:
self.config_edit.setText(value)
@@ -290,9 +404,6 @@ class SettingsPanelWidget(QWidget):
value = self.arrow_background_combo.currentData()
return value if value else "unrolled"
def video_source(self) -> str:
return self.video_edit.text().strip()
def config_path(self) -> str:
return self.config_edit.text().strip()
@@ -304,3 +415,60 @@ class SettingsPanelWidget(QWidget):
def csv_save_dir(self) -> str:
return self.csv_dir_edit.text().strip()
def preference_state(self) -> dict:
"""Serialize user-facing settings suitable for preference.json."""
return {
"video_source": self.video_source(),
"config_path": self.config_path(),
"finger_assets_dir": self.finger_export_dir(),
"csv_save_dir": self.csv_save_dir(),
"show_log": bool(self.show_log_check.isChecked()),
"show_flow_arrows": bool(self.show_flow_arrows_check.isChecked()),
"arrow_background": self.arrow_background(),
"free_rotate": bool(self.free_rotate_check.isChecked()),
}
def apply_preference_state(self, state: dict | None) -> None:
"""Restore user-facing settings from preference.json."""
if not isinstance(state, dict) or not state:
return
show_log = state.get("show_log")
if isinstance(show_log, bool):
self.show_log_check.blockSignals(True)
self.show_log_check.setChecked(show_log)
self.show_log_check.blockSignals(False)
show_flow = state.get("show_flow_arrows")
if isinstance(show_flow, bool):
self.show_flow_arrows_check.blockSignals(True)
self.show_flow_arrows_check.setChecked(show_flow)
self.show_flow_arrows_check.blockSignals(False)
self._mesh_state.set_flow_arrows_enabled(show_flow)
arrow_bg = state.get("arrow_background")
if isinstance(arrow_bg, str) and arrow_bg:
self.set_arrow_background(arrow_bg) # type: ignore[arg-type]
free_rotate = state.get("free_rotate")
if isinstance(free_rotate, bool):
self.free_rotate_check.blockSignals(True)
self.free_rotate_check.setChecked(free_rotate)
self.free_rotate_check.blockSignals(False)
config_path = state.get("config_path")
if isinstance(config_path, str) and config_path.strip():
self.set_config_path(config_path.strip())
assets = state.get("finger_assets_dir")
if isinstance(assets, str) and assets.strip():
self.set_finger_export_dir(assets.strip())
csv_dir = state.get("csv_save_dir")
if isinstance(csv_dir, str) and csv_dir.strip():
self.set_csv_save_dir(csv_dir.strip())
video = state.get("video_source")
if isinstance(video, str) and video.strip():
self.set_video_source(video.strip())
+1 -1
View File
@@ -5,7 +5,7 @@ A Python library for processing and analyzing optical tactile sensor data,
including image stitching, optical flow tracking, and deformation decomposition.
"""
__version__ = "0.4.1"
__version__ = "0.4.2"
__author__ = "Orisys"
from .sensor import Sensor
+1 -1
View File
@@ -35,7 +35,7 @@ def test_pyproject_dynamic_version(packaging_utils):
sys.path.insert(0, str(build_dir))
from pyproject_utils import get_version
assert get_version(REPO_ROOT) == "0.4.1"
assert get_version(REPO_ROOT) == "0.4.2"
def test_pyproject_bundle_manifest(packaging_utils):