diff --git a/.gitignore b/.gitignore index ffe2997..9d318b5 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ Log/ .vscode/ camera_params.json **.pyc** -data/ \ No newline at end of file +data/ +.env \ No newline at end of file diff --git a/main.py b/main.py index 4380108..5eaad9b 100644 --- a/main.py +++ b/main.py @@ -180,6 +180,8 @@ class CameraWorker(QThread): self._has_dual_ir = False self._ir_enabled = False self._last_emit_ts = 0.0 + self._last_frames = None # 最近一帧,供"按键即拍"使用 + self._capture_frames = None # 触发拍照时快照的帧 self._consec_timeouts = 0 self._disconnect_emitted = False @@ -189,8 +191,9 @@ class CameraWorker(QThread): def trigger_capture(self, save_dir: str): with self._lock: - self._capture_dir = save_dir - self._do_capture = True + self._capture_dir = save_dir + self._capture_frames = self._last_frames # 快照当前帧 + self._do_capture = True def stop(self): self._running = False @@ -309,11 +312,14 @@ class CameraWorker(QThread): self._consec_timeouts = 0 with self._lock: - d2c = self._d2c - do_cap = self._do_capture - cap_dir = self._capture_dir + self._last_frames = frames # 始终保存最新帧供快照 + d2c = self._d2c + do_cap = self._do_capture + cap_dir = self._capture_dir + cap_frames = self._capture_frames if do_cap: - self._do_capture = False + self._do_capture = False + self._capture_frames = None # 首帧提取相机参数 if self._camera_params is None: @@ -332,7 +338,7 @@ class CameraWorker(QThread): if do_cap and cap_dir: try: self._run_capture( - frames, cap_dir, + cap_frames or frames, cap_dir, align_filter, point_cloud_filter, frame_to_bgr_image, save_point_cloud_to_ply, OBFrameType, OBFormat, @@ -521,6 +527,44 @@ class BatchD2CWorker(QThread): self.done.emit() +# ───────────────────────────────────────────────────────────────── +# Toast Overlay(不阻塞鼠标,自动消失) +# ───────────────────────────────────────────────────────────────── + +class _Toast(QLabel): + def __init__(self, parent: QWidget): + super().__init__(parent) + self.setAlignment(Qt.AlignCenter) + self.setAttribute(Qt.WA_TransparentForMouseEvents, True) + self.setStyleSheet( + "background: rgba(30,140,30,210);" + "color: white;" + "border-radius: 10px;" + "font-size: 15px;" + "padding: 10px 28px;" + ) + self._timer = QTimer(self) + self._timer.setSingleShot(True) + self._timer.timeout.connect(self.hide) + self.hide() + + def show_msg(self, text: str, ms: int = 1000): + self.setText(text) + self.adjustSize() + p = self.parent() + self.move((p.width() - self.width()) // 2, + p.height() - self.height() - 70) + self.raise_() + self.show() + self._timer.start(ms) + + def reposition(self): + if self.isVisible(): + p = self.parent() + self.move((p.width() - self.width()) // 2, + p.height() - self.height() - 70) + + # ───────────────────────────────────────────────────────────────── # Main Window # ───────────────────────────────────────────────────────────────── @@ -542,6 +586,8 @@ class MainWindow(QMainWindow): self._depth_profiles_data: list[dict] = [] self._build_ui() + # toast 覆盖在 central widget 上 + self._toast = _Toast(self.centralWidget()) # ───────────────────────────────── # UI Construction @@ -572,86 +618,100 @@ class MainWindow(QMainWindow): tabs.addTab(self._build_d2c_tab(), " Orbbec Viewer 深度图 D2C ") root.addWidget(tabs) + @staticmethod + def _step_group(num: int, title: str) -> QGroupBox: + grp = QGroupBox(f"第 {num} 步 · {title}") + grp.setStyleSheet( + "QGroupBox { font-weight:bold; font-size:12px; color:#8ec8ff;" + " border:1px solid #444; border-radius:5px; margin-top:6px; padding-top:4px; }" + "QGroupBox::title { subcontrol-origin:margin; left:10px; padding:0 4px; }" + ) + return grp + def _build_capture_tab(self) -> QWidget: w = QWidget() layout = QVBoxLayout(w) - layout.setContentsMargins(12, 10, 12, 10) - layout.setSpacing(8) + layout.setContentsMargins(12, 8, 12, 8) + layout.setSpacing(6) - # ── 设备刷新行 ── - dev_row = QHBoxLayout() + COMBO_CSS = "QComboBox { combobox-popup: 0; }" + + # ── 第 1 步:刷新设备 ── + grp1 = self._step_group(1, "刷新设备") + g1 = QHBoxLayout(grp1) + g1.setContentsMargins(8, 4, 8, 6) refresh_btn = QPushButton("刷新设备") - refresh_btn.setFixedHeight(30) + refresh_btn.setFixedHeight(28) refresh_btn.clicked.connect(self._refresh_devices) - dev_row.addWidget(refresh_btn) - + g1.addWidget(refresh_btn) self.device_status = QLabel("未连接设备,请点击「刷新设备」") - self.device_status.setStyleSheet("color: #aaa; font-size: 12px;") - dev_row.addWidget(self.device_status) - dev_row.addStretch() - layout.addLayout(dev_row) + self.device_status.setStyleSheet("color:#aaa; font-size:12px;") + g1.addWidget(self.device_status) + g1.addStretch() + layout.addWidget(grp1) - # ── 流配置行 ── - stream_grp = QGroupBox("流配置") - stream_layout = QHBoxLayout(stream_grp) - stream_layout.setSpacing(12) + # ── 第 2 步:流配置 ── + grp2 = self._step_group(2, "流配置") + g2 = QHBoxLayout(grp2) + g2.setContentsMargins(8, 4, 8, 6) + g2.setSpacing(10) - stream_layout.addWidget(QLabel("RGB:")) + g2.addWidget(QLabel("RGB:")) self.color_combo = QComboBox() - self.color_combo.setMinimumWidth(260) + self.color_combo.setMinimumWidth(250) self.color_combo.setMaxVisibleItems(12) - self.color_combo.setStyleSheet("QComboBox { combobox-popup: 0; }") - stream_layout.addWidget(self.color_combo) + self.color_combo.setStyleSheet(COMBO_CSS) + g2.addWidget(self.color_combo) - stream_layout.addSpacing(16) - stream_layout.addWidget(QLabel("深度:")) + g2.addSpacing(12) + g2.addWidget(QLabel("深度:")) self.depth_combo = QComboBox() - self.depth_combo.setMinimumWidth(260) + self.depth_combo.setMinimumWidth(250) self.depth_combo.setMaxVisibleItems(12) - self.depth_combo.setStyleSheet("QComboBox { combobox-popup: 0; }") - stream_layout.addWidget(self.depth_combo) - - stream_layout.addStretch() + self.depth_combo.setStyleSheet(COMBO_CSS) + g2.addWidget(self.depth_combo) + g2.addStretch() self.stream_btn = QPushButton("开启") self.stream_btn.setCheckable(True) - self.stream_btn.setFixedSize(88, 32) + self.stream_btn.setFixedSize(88, 30) self.stream_btn.setStyleSheet( - "QPushButton{ border-radius:6px; border:none; font-weight:bold; }" + "QPushButton{ border-radius:5px; border:none; font-weight:bold; }" "QPushButton:!checked{ background:#555; color:#ccc; }" "QPushButton:checked{ background:#2a8a2a; color:white; }" "QPushButton:disabled{ background:#444; color:#777; }" ) self.stream_btn.toggled.connect(self._on_stream_toggle) - stream_layout.addWidget(self.stream_btn) + g2.addWidget(self.stream_btn) + layout.addWidget(grp2) - layout.addWidget(stream_grp) + # ── 第 3 步:保存目录 ── + grp3 = self._step_group(3, "保存目录") + g3 = QHBoxLayout(grp3) + g3.setContentsMargins(8, 4, 8, 6) + g3.setSpacing(8) - # ── 目录 + D2C 控制行 ── - ctrl = QHBoxLayout() - ctrl.addWidget(QLabel("保存目录:")) today = datetime.now() self.session_edit = QLineEdit(f"{today.year}年{today.month}月{today.day}日") - self.session_edit.setMinimumWidth(170) - ctrl.addWidget(self.session_edit) + self.session_edit.setMinimumWidth(160) + g3.addWidget(self.session_edit) create_btn = QPushButton("创建 / 载入") create_btn.setFixedHeight(28) create_btn.clicked.connect(self._create_session) - ctrl.addWidget(create_btn) + g3.addWidget(create_btn) self.session_status = QLabel("(未创建)") self.session_status.setStyleSheet("color:gray; font-size:12px;") - ctrl.addWidget(self.session_status) - ctrl.addStretch() + g3.addWidget(self.session_status) + g3.addStretch() self.d2c_check = QCheckBox("启用 D2C 对齐预览") self.d2c_check.setToolTip("勾选后预览使用 D2C 对齐;拍照时始终保存全部数据") self.d2c_check.stateChanged.connect( lambda s: self._worker and self._worker.set_d2c(s == Qt.Checked)) - ctrl.addWidget(self.d2c_check) - - layout.addLayout(ctrl) + g3.addWidget(self.d2c_check) + layout.addWidget(grp3) # ── 预览区 ── preview = QFrame() @@ -669,7 +729,7 @@ class MainWindow(QMainWindow): box.addWidget(hdr) lbl = QLabel("等待画面…") lbl.setAlignment(Qt.AlignCenter) - lbl.setMinimumSize(540, 380) + lbl.setMinimumSize(540, 360) lbl.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) lbl.setStyleSheet("background:#111; border:1px solid #555; color:#666;") setattr(self, attr, lbl) @@ -678,13 +738,22 @@ class MainWindow(QMainWindow): layout.addWidget(preview, stretch=1) - # ── 拍照按钮 ── - btn_row = QHBoxLayout() - btn_row.addStretch() + # ── 第 4 步:拍照 ── + step4_row = QHBoxLayout() + step4_lbl = QLabel("第 4 步 · 拍照") + step4_lbl.setStyleSheet( + "color:#8ec8ff; font-weight:bold; font-size:12px;") + step4_row.addWidget(step4_lbl) + step4_row.addStretch() + + hint = QLabel("快捷键 S") + hint.setStyleSheet("color:#666; font-size:11px;") + step4_row.addWidget(hint) + self.capture_btn = QPushButton("拍照") - self.capture_btn.setFixedSize(160, 48) + self.capture_btn.setFixedSize(160, 44) self.capture_btn.setEnabled(False) - f = QFont(); f.setPointSize(14); f.setBold(True) + f = QFont(); f.setPointSize(13); f.setBold(True) self.capture_btn.setFont(f) self.capture_btn.setStyleSheet( "QPushButton{ background:#2a6ebb; color:white; border-radius:8px; border:none; }" @@ -693,9 +762,8 @@ class MainWindow(QMainWindow): "QPushButton:disabled{ background:#444; color:#777; }" ) self.capture_btn.clicked.connect(self._on_capture) - btn_row.addWidget(self.capture_btn) - btn_row.addStretch() - layout.addLayout(btn_row) + step4_row.addWidget(self.capture_btn) + layout.addLayout(step4_row) return w @@ -983,11 +1051,7 @@ class MainWindow(QMainWindow): def _on_capture_done(self, save_dir: str): self.capture_btn.setEnabled(True) self.capture_btn.setText("拍照") - msg = QMessageBox(QMessageBox.Information, "拍摄完成", - f"已保存至:\n{save_dir}", QMessageBox.Ok, self) - msg.setWindowModality(Qt.NonModal) - msg.show() - QTimer.singleShot(3000, msg.accept) + self._toast.show_msg("拍摄完成!", 1000) # ───────────────────────────────── # Batch D2C Tab @@ -1088,6 +1152,22 @@ class MainWindow(QMainWindow): def _append_d2c_log(self, msg: str): self.d2c_log.append(msg) + # ───────────────────────────────── + # Keyboard & Resize + # ───────────────────────────────── + + def keyPressEvent(self, event): + if event.key() == Qt.Key_S: + focused = QApplication.focusWidget() + if not isinstance(focused, (QLineEdit, QTextEdit)) and self.capture_btn.isEnabled(): + self._on_capture() + return + super().keyPressEvent(event) + + def resizeEvent(self, event): + super().resizeEvent(event) + self._toast.reposition() + # ───────────────────────────────── # Close # ─────────────────────────────────