测量模式:从任意帧开始 + 左右键导航 + 跳过帧记录0
This commit is contained in:
parent
591fc790bf
commit
426dbe191d
131
main.py
131
main.py
|
|
@ -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 = ""
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue