测量模式:从任意帧开始 + 左右键导航 + 跳过帧记录0

This commit is contained in:
QQ 2026-07-06 15:05:15 +08:00
parent 591fc790bf
commit 426dbe191d
1 changed files with 59 additions and 72 deletions

131
main.py
View File

@ -71,6 +71,7 @@ class FrameView(QLabel):
self._block_clicks: bool = False
self.on_point_selected = None
self.on_key_pressed = None # 键盘回调
@property
def reference_ready(self) -> bool:
@ -130,6 +131,11 @@ class FrameView(QLabel):
if self.on_point_selected:
self.on_point_selected(self._select_mode, pt)
def keyPressEvent(self, event) -> None:
if self.on_key_pressed:
self.on_key_pressed(event.key())
super().keyPressEvent(event)
def paintEvent(self, event) -> None:
super().paintEvent(event)
painter = QPainter(self)
@ -235,6 +241,7 @@ class MainWindow(QMainWindow):
self.frame_view = FrameView()
self.frame_view.on_point_selected = self._on_point_changed
self.frame_view.on_key_pressed = self._on_key_pressed
# 右侧面板(弹性宽度)
panel = QWidget()
@ -418,7 +425,7 @@ class MainWindow(QMainWindow):
f"{self._video.fps:.1f} fps")
def _show_frame(self, idx: int) -> None:
"""随机访问显示帧(手动导航用)"""
"""显示指定"""
if self._video is None:
return
frame = self._video.read_frame(idx)
@ -431,20 +438,6 @@ class MainWindow(QMainWindow):
self.slider.blockSignals(False)
self._update_display()
def _show_frame_seq(self, idx: int) -> None:
"""顺序读取显示帧(测量模式用,不 seek"""
if self._video is None:
return
frame = self._video.read_next()
if frame is None:
return
self._current_frame_idx = idx
self.frame_view.set_frame(frame)
self.slider.blockSignals(True)
self.slider.setValue(idx)
self.slider.blockSignals(False)
self._update_display()
def _update_display(self) -> None:
self.lbl_frame.setText(str(self._current_frame_idx))
o, m, m2 = self.frame_view.point_o, self.frame_view.point_m, self.frame_view.point_m2
@ -498,12 +491,12 @@ class MainWindow(QMainWindow):
QMessageBox.warning(self, "警告", "请先在首帧选定 O 和 M")
return
# 重置测量
self._measurements.clear()
self._prev_angle = None
self._measuring = True
self.frame_view._select_mode = "m2"
self.frame_view._block_clicks = False
self.frame_view.setFocus() # 接收键盘事件
# 禁用无关控件
self.btn_start.setEnabled(False)
@ -514,51 +507,38 @@ class MainWindow(QMainWindow):
self.btn_reset_all.setEnabled(False)
self.btn_reset_m2.setEnabled(False)
# 重开视频,从第 0 帧开始顺序读取
self._video.reset()
self.frame_view.reset_m2()
self._show_frame_seq(0)
self.lbl_hint.setText("⏳ 点击 M2 → 自动进入下一帧")
# 从当前帧开始
self._show_frame(self._current_frame_idx)
self.lbl_hint.setText("⏳ 点击 M2 或按 → 跳过")
self.lbl_hint.setStyleSheet(
"color: #0af; font-size: 12px; font-weight: bold;")
self._update_display()
def _stop_measuring(self) -> None:
self._measuring = False
self.frame_view._block_clicks = False
self.btn_start.setEnabled(True)
self.btn_stop.setEnabled(False)
self.slider.setEnabled(True)
self.btn_prev.setEnabled(True)
self.btn_next.setEnabled(True)
self.btn_reset_all.setEnabled(True)
self.btn_reset_m2.setEnabled(True)
self.lbl_hint.setText(
f"已停止 · 共 {len(self._measurements)} 帧数据")
self.lbl_hint.setStyleSheet("color: #888; font-size: 11px;")
self._update_display()
def _go_to_frame(self, idx: int) -> None:
"""测量模式中顺序前进并清空 M2"""
self.frame_view.reset_m2()
self._show_frame_seq(idx)
# ══════════════ 选点回调 ═════════════════════════════
def _on_point_changed(self, mode: str, pt: tuple) -> None:
x, y = int(round(pt[0])), int(round(pt[1]))
if mode == "o":
self.lbl_o.setText(f"({x}, {y})")
self.lbl_hint.setText("点击画面选 M")
elif mode == "m":
self.lbl_m.setText(f"({x}, {y})")
if not self._measuring:
self.lbl_hint.setText("点击画面选 M2或点「开始测量」")
elif mode == "m2":
self.lbl_m2.setText(f"({x}, {y})")
self._update_display()
if self._measuring:
def _on_key_pressed(self, key: int) -> None:
if not self._measuring:
return
if key == Qt.Key.Key_Right:
if self.frame_view.point_m2 is not None:
self._record_and_advance()
else:
self._record_skip()
elif key == Qt.Key.Key_Left:
if self._current_frame_idx > 0:
self._current_frame_idx -= 1
self._show_frame(self._current_frame_idx)
def _record_skip(self) -> None:
"""跳过当前帧,记录 0"""
o = self.frame_view.point_o
m = self.frame_view.point_m
if o is None or m is None:
return
self._measurements.append((
self._current_frame_idx, 0.0, o[0], o[1],
))
self._prev_angle = 0.0
QApplication.processEvents()
QTimer.singleShot(50, self._advance_frame)
def _record_and_advance(self) -> None:
o = self.frame_view.point_o
@ -574,8 +554,6 @@ class MainWindow(QMainWindow):
self._measurements.append((
self._current_frame_idx, angle, m2[0], m2[1],
))
# 先保存当前角度为上帧,再进下一帧
self._prev_angle = angle
QApplication.processEvents()
QTimer.singleShot(150, self._advance_frame)
@ -592,6 +570,26 @@ class MainWindow(QMainWindow):
return
self._go_to_frame(next_idx)
def _go_to_frame(self, idx: int) -> None:
"""测量模式中跳转并清空 M2"""
self.frame_view.reset_m2()
self._show_frame(idx)
def _stop_measuring(self) -> None:
self._measuring = False
self.frame_view._block_clicks = False
self.btn_start.setEnabled(True)
self.btn_stop.setEnabled(False)
self.slider.setEnabled(True)
self.btn_prev.setEnabled(True)
self.btn_next.setEnabled(True)
self.btn_reset_all.setEnabled(True)
self.btn_reset_m2.setEnabled(True)
self.lbl_hint.setText(
f"已停止 · 共 {len(self._measurements)} 帧数据")
self.lbl_hint.setStyleSheet("color: #888; font-size: 11px;")
self._update_display()
# ══════════════ 方向矫正 ═════════════════════════════
def _on_flip_toggled(self, checked: bool) -> None:
@ -642,11 +640,6 @@ class MainWindow(QMainWindow):
frames_dir = path.parent / f"{path.stem}_frames"
frames_dir.mkdir(exist_ok=True)
# 用顺序读取导出(避免 seek 卡死)
self._video.reset()
frame_buf = self._video.read_next()
buf_idx = 0
with open(path, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow([
@ -656,18 +649,12 @@ class MainWindow(QMainWindow):
"screenshot",
])
for frame_idx, angle, m2x, m2y in self._measurements:
# 跳过不匹配的帧(理论不会发生)
while buf_idx < frame_idx and frame_buf is not None:
frame_buf = self._video.read_next()
buf_idx += 1
if frame_buf is not None and buf_idx == frame_idx:
frame = self._video.read_frame(frame_idx)
if frame is not None:
annotated = self._draw_annotations(
frame_buf, o, m, (m2x, m2y))
frame, o, m, (m2x, m2y))
img_name = f"frame_{frame_idx:04d}.png"
cv2.imwrite(str(frames_dir / img_name), annotated)
frame_buf = self._video.read_next()
buf_idx += 1
else:
img_name = ""