This commit is contained in:
QQ 2026-07-30 15:55:33 +08:00
parent 0afaec558f
commit 973c02a043
4 changed files with 89 additions and 11 deletions

1
.gitignore vendored
View File

@ -4,6 +4,7 @@ __pycache__/
.venv/ .venv/
.build-venv/ .build-venv/
venv/ venv/
client/
*.egg-info/ *.egg-info/
dist/ dist/
build/ build/

View File

@ -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
### 方向矫正 ### 方向矫正

51
agent.md Normal file
View File

@ -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
View File

@ -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"""