dasd
This commit is contained in:
parent
0afaec558f
commit
973c02a043
|
|
@ -4,6 +4,7 @@ __pycache__/
|
||||||
.venv/
|
.venv/
|
||||||
.build-venv/
|
.build-venv/
|
||||||
venv/
|
venv/
|
||||||
|
client/
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
dist/
|
dist/
|
||||||
build/
|
build/
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ python main.py static/test.mp4
|
||||||
1. **文件 → 打开视频**
|
1. **文件 → 打开视频**
|
||||||
2. 在首帧依次点击 **O**(圆心)、**M**(初始尾端参考)
|
2. 在首帧依次点击 **O**(圆心)、**M**(初始尾端参考)
|
||||||
3. 拖动滑块到目标帧,点击 **M2**(当前尾端),右侧显示角度
|
3. 拖动滑块到目标帧,点击 **M2**(当前尾端),右侧显示角度
|
||||||
|
4. 在画面上按 **A** / **D** 可按“跳帧数”向前 / 向后跳转
|
||||||
|
|
||||||
### 测量模式
|
### 测量模式
|
||||||
|
|
||||||
|
|
@ -34,6 +35,7 @@ python main.py static/test.mp4
|
||||||
3. 逐帧点击 M2,每点完一帧自动进下一帧
|
3. 逐帧点击 M2,每点完一帧自动进下一帧
|
||||||
4. 点击 **「停止测量」** 或跑完全部帧后自动停止
|
4. 点击 **「停止测量」** 或跑完全部帧后自动停止
|
||||||
5. **「导出测量数据 (CSV)」** 保存角度 + 每帧标注截图
|
5. **「导出测量数据 (CSV)」** 保存角度 + 每帧标注截图
|
||||||
|
6. 测量中按 **A** / **D** 按“跳帧数”跳转,按 **→** 跳过当前帧并记录 0
|
||||||
|
|
||||||
### 方向矫正
|
### 方向矫正
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
# Actuator Agent Guide
|
||||||
|
|
||||||
|
## Project
|
||||||
|
|
||||||
|
- Python/PyQt6 desktop tool for reading actuator angles from video frames.
|
||||||
|
- Application entry point: `main.py`.
|
||||||
|
- Core modules: `actuator/angle_calc.py`, `actuator/video_reader.py`, and `actuator/automation.py`.
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
|
||||||
|
- Use the project-local `uv` virtual environment: `client/`.
|
||||||
|
- Python version: 3.10.
|
||||||
|
- Install or refresh dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv pip install --python client/bin/python -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run And Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
client/bin/python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
For Wayland:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
QT_QPA_PLATFORM=xcb client/bin/python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Smoke-test GUI initialization without a display:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
QT_QPA_PLATFORM=offscreen timeout 5s client/bin/python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
An exit status caused by `timeout` is expected when the GUI initializes and remains open.
|
||||||
|
|
||||||
|
## Frame Navigation
|
||||||
|
|
||||||
|
- Set the "跳帧数" control to choose a frame-jump step (1-10000).
|
||||||
|
- `A`: move backward by the selected step.
|
||||||
|
- `D`: move forward by the selected step.
|
||||||
|
- During measurement, `A`/`D` only navigate and do not create measurements for skipped frames.
|
||||||
|
- During measurement, `Right` skips the current frame and records angle `0`; if M2 is selected, it records the measurement and advances one frame.
|
||||||
|
|
||||||
|
## Change Guidelines
|
||||||
|
|
||||||
|
- Keep GUI text in Chinese to match the existing application.
|
||||||
|
- Run `client/bin/python -m py_compile main.py actuator/*.py` after Python changes.
|
||||||
|
- Do not commit the local `client/` environment; it is ignored by Git.
|
||||||
46
main.py
46
main.py
|
|
@ -24,7 +24,7 @@ from PyQt6.QtWidgets import (
|
||||||
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
||||||
QLabel, QPushButton, QSlider, QFileDialog, QGroupBox,
|
QLabel, QPushButton, QSlider, QFileDialog, QGroupBox,
|
||||||
QDoubleSpinBox, QFormLayout, QMessageBox, QSplitter, QCheckBox,
|
QDoubleSpinBox, QFormLayout, QMessageBox, QSplitter, QCheckBox,
|
||||||
QSizePolicy,
|
QSizePolicy, QSpinBox,
|
||||||
)
|
)
|
||||||
|
|
||||||
from actuator import VideoReader, signed_angle, curvature_from_three_points
|
from actuator import VideoReader, signed_angle, curvature_from_three_points
|
||||||
|
|
@ -55,6 +55,7 @@ class FrameView(QLabel):
|
||||||
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
self.setStyleSheet("background: #1a1a1a; border: 1px solid #333;")
|
self.setStyleSheet("background: #1a1a1a; border: 1px solid #333;")
|
||||||
self.setMouseTracking(True)
|
self.setMouseTracking(True)
|
||||||
|
self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||||
self.setAcceptDrops(True)
|
self.setAcceptDrops(True)
|
||||||
|
|
||||||
self._frame: Optional[np.ndarray] = None
|
self._frame: Optional[np.ndarray] = None
|
||||||
|
|
@ -375,10 +376,19 @@ class MainWindow(QMainWindow):
|
||||||
self.slider.setMaximum(0)
|
self.slider.setMaximum(0)
|
||||||
self.slider.valueChanged.connect(self._on_slider)
|
self.slider.valueChanged.connect(self._on_slider)
|
||||||
nl.addWidget(self.slider)
|
nl.addWidget(self.slider)
|
||||||
|
step_row = QHBoxLayout()
|
||||||
|
step_row.addWidget(QLabel("跳帧数:"))
|
||||||
|
self.spin_frame_step = QSpinBox()
|
||||||
|
self.spin_frame_step.setRange(1, 10000)
|
||||||
|
self.spin_frame_step.setValue(1)
|
||||||
|
self.spin_frame_step.setSuffix(" 帧")
|
||||||
|
self.spin_frame_step.setToolTip("A/D 快捷键和导航按钮的跳转步长")
|
||||||
|
step_row.addWidget(self.spin_frame_step)
|
||||||
|
nl.addLayout(step_row)
|
||||||
bn = QHBoxLayout()
|
bn = QHBoxLayout()
|
||||||
self.btn_prev = QPushButton("◀")
|
self.btn_prev = QPushButton("A ◀")
|
||||||
self.btn_prev.clicked.connect(self._prev_frame)
|
self.btn_prev.clicked.connect(self._prev_frame)
|
||||||
self.btn_next = QPushButton("▶")
|
self.btn_next = QPushButton("D ▶")
|
||||||
self.btn_next.clicked.connect(self._next_frame)
|
self.btn_next.clicked.connect(self._next_frame)
|
||||||
bn.addWidget(self.btn_prev)
|
bn.addWidget(self.btn_prev)
|
||||||
bn.addWidget(self.btn_next)
|
bn.addWidget(self.btn_next)
|
||||||
|
|
@ -486,13 +496,23 @@ class MainWindow(QMainWindow):
|
||||||
self._show_frame(value)
|
self._show_frame(value)
|
||||||
|
|
||||||
def _prev_frame(self) -> None:
|
def _prev_frame(self) -> None:
|
||||||
if self._video and not self._measuring:
|
self._jump_frames(-self.spin_frame_step.value())
|
||||||
self._show_frame(max(0, self._current_frame_idx - 1))
|
|
||||||
|
|
||||||
def _next_frame(self) -> None:
|
def _next_frame(self) -> None:
|
||||||
if self._video and not self._measuring:
|
self._jump_frames(self.spin_frame_step.value())
|
||||||
self._show_frame(min(self._video.frame_count - 1,
|
|
||||||
self._current_frame_idx + 1))
|
def _jump_frames(self, offset: int) -> None:
|
||||||
|
"""按偏移量跳转,测量时不为未测帧创建记录。"""
|
||||||
|
if self._video is None:
|
||||||
|
return
|
||||||
|
target = max(0, min(self._video.frame_count - 1,
|
||||||
|
self._current_frame_idx + offset))
|
||||||
|
if target == self._current_frame_idx:
|
||||||
|
return
|
||||||
|
if self._measuring:
|
||||||
|
self._go_to_frame(target)
|
||||||
|
else:
|
||||||
|
self._show_frame(target)
|
||||||
|
|
||||||
# ══════════════ 测量模式 ═════════════════════════════
|
# ══════════════ 测量模式 ═════════════════════════════
|
||||||
|
|
||||||
|
|
@ -527,6 +547,12 @@ class MainWindow(QMainWindow):
|
||||||
self._update_display()
|
self._update_display()
|
||||||
|
|
||||||
def _on_key_pressed(self, key: int) -> None:
|
def _on_key_pressed(self, key: int) -> None:
|
||||||
|
if key == Qt.Key.Key_A:
|
||||||
|
self._prev_frame()
|
||||||
|
return
|
||||||
|
if key == Qt.Key.Key_D:
|
||||||
|
self._next_frame()
|
||||||
|
return
|
||||||
if not self._measuring:
|
if not self._measuring:
|
||||||
return
|
return
|
||||||
if key == Qt.Key.Key_Right:
|
if key == Qt.Key.Key_Right:
|
||||||
|
|
@ -535,9 +561,7 @@ class MainWindow(QMainWindow):
|
||||||
else:
|
else:
|
||||||
self._record_skip()
|
self._record_skip()
|
||||||
elif key == Qt.Key.Key_Left:
|
elif key == Qt.Key.Key_Left:
|
||||||
if self._current_frame_idx > 0:
|
self._jump_frames(-1)
|
||||||
self._current_frame_idx -= 1
|
|
||||||
self._show_frame(self._current_frame_idx)
|
|
||||||
|
|
||||||
def _record_skip(self) -> None:
|
def _record_skip(self) -> None:
|
||||||
"""跳过当前帧,记录 0"""
|
"""跳过当前帧,记录 0"""
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue