设置目录选择

This commit is contained in:
i
2026-04-22 10:49:07 +08:00
parent 2996ba96e7
commit e355757b43
+237 -75
View File
@@ -53,7 +53,6 @@ from PyQt5.QtGui import QImage, QPixmap, QFont
from d2c_batch import batch_convert from d2c_batch import batch_convert
DATA_DIR = Path("data")
PARAMS_FILE = "camera_params.json" PARAMS_FILE = "camera_params.json"
MIN_DEPTH_MM, MAX_DEPTH_MM = 200, 5000 MIN_DEPTH_MM, MAX_DEPTH_MM = 200, 5000
_DISCONNECT_TIMEOUTS = 6 # 6 × 500 ms = 3 s 无帧 → 断线 _DISCONNECT_TIMEOUTS = 6 # 6 × 500 ms = 3 s 无帧 → 断线
@@ -81,16 +80,32 @@ class AppSettings:
FONT_SCALE_AUTO = "auto" FONT_SCALE_AUTO = "auto"
def __init__(self): def __init__(self):
self.font_scale: str = self.FONT_SCALE_AUTO self.font_scale: str = self.FONT_SCALE_AUTO
self.splat_radius: int = 1 # 0=关闭, 1=3×3, 2=5×5, 3=7×7 self.splat_radius: int = 1 # 0=关闭, 1=3×3, 2=5×5, 3=7×7
self.save_base_dir: str = "" # 空 = 当前目录
self.capture_rgb: bool = True
self.capture_depth: bool = True
self.capture_d2c: bool = True
self.capture_irl: bool = False
self.capture_irr: bool = False
self.capture_pc: bool = False
self.capture_pc_color:bool = False
self._load() self._load()
def _load(self): def _load(self):
if self._CONFIG_PATH.is_file(): if self._CONFIG_PATH.is_file():
try: try:
data = json.loads(self._CONFIG_PATH.read_text(encoding="utf-8")) d = json.loads(self._CONFIG_PATH.read_text(encoding="utf-8"))
self.font_scale = data.get("font_scale", self.FONT_SCALE_AUTO) self.font_scale = d.get("font_scale", self.FONT_SCALE_AUTO)
self.splat_radius = int(data.get("splat_radius", 1)) self.splat_radius = int(d.get("splat_radius", 1))
self.save_base_dir = d.get("save_base_dir", "")
self.capture_rgb = bool(d.get("capture_rgb", True))
self.capture_depth = bool(d.get("capture_depth", True))
self.capture_d2c = bool(d.get("capture_d2c", True))
self.capture_irl = bool(d.get("capture_irl", False))
self.capture_irr = bool(d.get("capture_irr", False))
self.capture_pc = bool(d.get("capture_pc", False))
self.capture_pc_color= bool(d.get("capture_pc_color", False))
except Exception: except Exception:
pass pass
@@ -98,7 +113,18 @@ class AppSettings:
self._APP_DIR.mkdir(parents=True, exist_ok=True) self._APP_DIR.mkdir(parents=True, exist_ok=True)
self._CONFIG_PATH.write_text( self._CONFIG_PATH.write_text(
json.dumps( json.dumps(
{"font_scale": self.font_scale, "splat_radius": self.splat_radius}, {
"font_scale": self.font_scale,
"splat_radius": self.splat_radius,
"save_base_dir": self.save_base_dir,
"capture_rgb": self.capture_rgb,
"capture_depth": self.capture_depth,
"capture_d2c": self.capture_d2c,
"capture_irl": self.capture_irl,
"capture_irr": self.capture_irr,
"capture_pc": self.capture_pc,
"capture_pc_color": self.capture_pc_color,
},
ensure_ascii=False, indent=2, ensure_ascii=False, indent=2,
), ),
encoding="utf-8", encoding="utf-8",
@@ -129,7 +155,7 @@ class SettingsDialog(QDialog):
def __init__(self, settings: "AppSettings", parent=None): def __init__(self, settings: "AppSettings", parent=None):
super().__init__(parent) super().__init__(parent)
self.setWindowTitle("程序设置") self.setWindowTitle("程序设置")
self.setFixedSize(_sz(440), _sz(210)) self.setFixedSize(_sz(520), _sz(360))
self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint)
self._settings = settings self._settings = settings
self._build_ui() self._build_ui()
@@ -187,6 +213,60 @@ class SettingsDialog(QDialog):
splat_info.setStyleSheet(f"color:#888; font-size:{_fs(11)}px;") splat_info.setStyleSheet(f"color:#888; font-size:{_fs(11)}px;")
layout.addWidget(splat_info) layout.addWidget(splat_info)
# ── 分隔 ──
sep = QLabel("── 采集设置 ──")
sep.setAlignment(Qt.AlignCenter)
sep.setStyleSheet(f"color:#666; font-size:{_fs(11)}px; margin-top:{_sz(4)}px;")
layout.addWidget(sep)
# 保存根目录
dir_row = QHBoxLayout()
dir_lbl = QLabel("根目录:")
dir_lbl.setMinimumWidth(_sz(80))
dir_row.addWidget(dir_lbl)
self._base_dir_edit = QLineEdit(self._settings.save_base_dir)
self._base_dir_edit.setPlaceholderText("留空 = 当前工作目录")
dir_row.addWidget(self._base_dir_edit)
browse_btn = QPushButton("浏览…")
browse_btn.setFixedWidth(_sz(60))
browse_btn.clicked.connect(self._browse_save_dir)
dir_row.addWidget(browse_btn)
layout.addLayout(dir_row)
# 默认采集内容
cap_lbl = QLabel("默认采集内容:")
layout.addWidget(cap_lbl)
self._cap_checks: dict = {}
cap_items = [
("capture_rgb", "RGB"),
("capture_depth", "Depth"),
("capture_d2c", "Depth D2C"),
("capture_irl", "IR-L"),
("capture_irr", "IR-R"),
("capture_pc", "点云"),
("capture_pc_color", "颜色点云"),
]
cap_row1 = QHBoxLayout()
cap_row1.setSpacing(_sz(8))
for attr, label in cap_items[:5]:
cb = QCheckBox(label)
cb.setChecked(getattr(self._settings, attr, True))
cap_row1.addWidget(cb)
self._cap_checks[attr] = cb
cap_row1.addStretch()
layout.addLayout(cap_row1)
cap_row2 = QHBoxLayout()
cap_row2.setSpacing(_sz(8))
for attr, label in cap_items[5:]:
cb = QCheckBox(label)
cb.setChecked(getattr(self._settings, attr, False))
cap_row2.addWidget(cb)
self._cap_checks[attr] = cb
cap_row2.addStretch()
layout.addLayout(cap_row2)
layout.addStretch() layout.addStretch()
btn_row = QHBoxLayout() btn_row = QHBoxLayout()
@@ -201,9 +281,19 @@ class SettingsDialog(QDialog):
btn_row.addWidget(cancel_btn) btn_row.addWidget(cancel_btn)
layout.addLayout(btn_row) layout.addLayout(btn_row)
def _browse_save_dir(self):
d = QFileDialog.getExistingDirectory(
self, "选择数据保存根目录",
self._base_dir_edit.text() or str(Path.cwd()))
if d:
self._base_dir_edit.setText(d)
def _save_and_close(self): def _save_and_close(self):
self._settings.font_scale = self._scale_combo.currentData() self._settings.font_scale = self._scale_combo.currentData()
self._settings.splat_radius = self._splat_combo.currentData() self._settings.splat_radius = self._splat_combo.currentData()
self._settings.save_base_dir = self._base_dir_edit.text().strip()
for attr, cb in self._cap_checks.items():
setattr(self._settings, attr, cb.isChecked())
self._settings.save() self._settings.save()
self.accept() self.accept()
@@ -345,6 +435,7 @@ class CameraWorker(QThread):
self._disconnect_emitted = False self._disconnect_emitted = False
self._sync_wait_count = 0 # 等待同步帧的计数器 self._sync_wait_count = 0 # 等待同步帧的计数器
self._splat_radius = 0 self._splat_radius = 0
self._capture_opts: dict = {}
def set_d2c(self, enabled: bool): def set_d2c(self, enabled: bool):
with self._lock: with self._lock:
@@ -354,10 +445,11 @@ class CameraWorker(QThread):
with self._lock: with self._lock:
self._splat_radius = radius self._splat_radius = radius
def trigger_capture(self, save_dir: str): def trigger_capture(self, save_dir: str, capture_opts: dict | None = None):
with self._lock: with self._lock:
self._capture_dir = save_dir self._capture_dir = save_dir
self._do_capture = True self._capture_opts = capture_opts or {}
self._do_capture = True
def stop(self): def stop(self):
self._running = False self._running = False
@@ -498,6 +590,7 @@ class CameraWorker(QThread):
do_cap = self._do_capture do_cap = self._do_capture
cap_dir = self._capture_dir cap_dir = self._capture_dir
splat_r = self._splat_radius splat_r = self._splat_radius
cap_opts = self._capture_opts
# 首帧提取相机参数 # 首帧提取相机参数
if self._camera_params is None: if self._camera_params is None:
@@ -530,7 +623,7 @@ class CameraWorker(QThread):
self.log_msg.emit(f"帧同步良好(RGB-D 差 {ts_diff}ms),开始拍摄") self.log_msg.emit(f"帧同步良好(RGB-D 差 {ts_diff}ms),开始拍摄")
try: try:
self._run_capture( self._run_capture(
frames, cap_dir, frames, cap_dir, cap_opts,
align_filter, point_cloud_filter, align_filter, point_cloud_filter,
frame_to_bgr_image, save_point_cloud_to_ply, frame_to_bgr_image, save_point_cloud_to_ply,
OBFrameType, OBFormat, OBFrameType, OBFormat,
@@ -586,7 +679,7 @@ class CameraWorker(QThread):
# ── Capture ── # ── Capture ──
def _run_capture(self, frames, save_dir, def _run_capture(self, frames, save_dir, capture_opts,
align_filter, point_cloud_filter, align_filter, point_cloud_filter,
frame_to_bgr_image, save_point_cloud_to_ply, frame_to_bgr_image, save_point_cloud_to_ply,
OBFrameType, OBFormat): OBFrameType, OBFormat):
@@ -596,57 +689,66 @@ class CameraWorker(QThread):
cf = frames.get_color_frame() cf = frames.get_color_frame()
df = frames.get_depth_frame() df = frames.get_depth_frame()
if cf: if capture_opts.get("rgb", True) and cf:
bgr = frame_to_bgr_image(cf) bgr = frame_to_bgr_image(cf)
if bgr is not None: if bgr is not None:
imwrite_utf8(str(base / "color.png"), bgr) imwrite_utf8(str(base / "color.png"), bgr)
if self._ir_enabled: if self._ir_enabled:
if self._has_dual_ir: if self._has_dual_ir:
img = process_ir_frame(frames.get_frame(OBFrameType.LEFT_IR_FRAME)) if capture_opts.get("irl", True):
if img is not None: img = process_ir_frame(frames.get_frame(OBFrameType.LEFT_IR_FRAME))
imwrite_utf8(str(base / "ir_left.png"), img) if img is not None:
img = process_ir_frame(frames.get_frame(OBFrameType.RIGHT_IR_FRAME)) imwrite_utf8(str(base / "ir_left.png"), img)
if img is not None: if capture_opts.get("irr", True):
imwrite_utf8(str(base / "ir_right.png"), img) img = process_ir_frame(frames.get_frame(OBFrameType.RIGHT_IR_FRAME))
if img is not None:
imwrite_utf8(str(base / "ir_right.png"), img)
else: else:
img = process_ir_frame(frames.get_frame(OBFrameType.IR_FRAME)) if capture_opts.get("irl", True):
if img is not None: img = process_ir_frame(frames.get_frame(OBFrameType.IR_FRAME))
imwrite_utf8(str(base / "ir.png"), img) if img is not None:
imwrite_utf8(str(base / "ir.png"), img)
if df: if capture_opts.get("depth", True) and df:
raw = np.frombuffer(df.get_data(), dtype=np.uint16).reshape( raw = np.frombuffer(df.get_data(), dtype=np.uint16).reshape(
df.get_height(), df.get_width()) df.get_height(), df.get_width())
imwrite_utf8(str(base / "depth.png"), raw) imwrite_utf8(str(base / "depth.png"), raw)
aligned = align_filter.process(frames) need_aligned = (capture_opts.get("d2c", True) or
if aligned: capture_opts.get("pc", False) or
aset = aligned.as_frame_set() capture_opts.get("pc_color", False))
adf = aset.get_depth_frame() if need_aligned:
if adf: aligned = align_filter.process(frames)
ad = np.frombuffer(adf.get_data(), dtype=np.uint16).reshape( if aligned:
adf.get_height(), adf.get_width()) aset = aligned.as_frame_set()
imwrite_utf8(str(base / "depth_d2c.png"), ad) adf = aset.get_depth_frame()
imwrite_utf8(str(base / "depth_d2c_vis.png"), if adf and capture_opts.get("d2c", True):
depth_to_vis(ad, adf.get_depth_scale())) ad = np.frombuffer(adf.get_data(), dtype=np.uint16).reshape(
adf.get_height(), adf.get_width())
imwrite_utf8(str(base / "depth_d2c.png"), ad)
imwrite_utf8(str(base / "depth_d2c_vis.png"),
depth_to_vis(ad, adf.get_depth_scale()))
try: if capture_opts.get("pc", False):
point_cloud_filter.set_create_point_format(OBFormat.POINT) try:
pc = point_cloud_filter.process(aligned) point_cloud_filter.set_create_point_format(OBFormat.POINT)
if pc: pc = point_cloud_filter.process(aligned)
save_ply_utf8(str(base / "point_cloud.ply"), pc, if pc:
save_point_cloud_to_ply) save_ply_utf8(str(base / "point_cloud.ply"), pc,
except Exception as exc: save_point_cloud_to_ply)
self.log_msg.emit(f"点云警告:{exc}") except Exception as exc:
self.log_msg.emit(f"点云警告:{exc}")
try: if capture_opts.get("pc_color", False):
point_cloud_filter.set_create_point_format(OBFormat.RGB_POINT) try:
pc = point_cloud_filter.process(aligned) point_cloud_filter.set_create_point_format(OBFormat.RGB_POINT)
if pc: pc = point_cloud_filter.process(aligned)
save_ply_utf8(str(base / "point_cloud_color.ply"), pc, if pc:
save_point_cloud_to_ply) save_ply_utf8(str(base / "point_cloud_color.ply"), pc,
except Exception as exc: save_point_cloud_to_ply)
self.log_msg.emit(f"彩色点云警告:{exc}") except Exception as exc:
self.log_msg.emit(f"彩色点云警告:{exc}")
self.capture_done.emit(save_dir) self.capture_done.emit(save_dir)
@@ -932,12 +1034,27 @@ class MainWindow(QMainWindow):
tabs.addTab(self._build_d2c_tab(), " Orbbec Viewer 深度图 D2C ") tabs.addTab(self._build_d2c_tab(), " Orbbec Viewer 深度图 D2C ")
root.addWidget(tabs) root.addWidget(tabs)
def _browse_base_dir(self):
d = QFileDialog.getExistingDirectory(
self, "选择数据保存根目录",
self.base_dir_edit.text() or str(Path.cwd()))
if d:
self.base_dir_edit.setText(d)
self._app_settings.save_base_dir = d
self._app_settings.save()
def _show_settings(self): def _show_settings(self):
dlg = SettingsDialog(self._app_settings, self) dlg = SettingsDialog(self._app_settings, self)
if dlg.exec_() == QDialog.Accepted: if dlg.exec_() == QDialog.Accepted:
# 立即更新当前流的 splat 半径 # 立即更新当前流的 splat 半径
if self._worker and self.capture_splat_check.isChecked(): if self._worker and self.capture_splat_check.isChecked():
self._worker.set_splat(max(1, self._app_settings.splat_radius)) self._worker.set_splat(max(1, self._app_settings.splat_radius))
# 同步根目录显示
self.base_dir_edit.setText(
self._app_settings.save_base_dir or str(Path.cwd()))
# 同步采集内容复选框为新默认值
for key, cb in self._capture_checks.items():
cb.setChecked(getattr(self._app_settings, f"capture_{key}", cb.isChecked()))
def _on_capture_splat_changed(self, state: int): def _on_capture_splat_changed(self, state: int):
radius = max(1, self._app_settings.splat_radius) if state == Qt.Checked else 0 radius = max(1, self._app_settings.splat_radius) if state == Qt.Checked else 0
@@ -1023,40 +1140,83 @@ class MainWindow(QMainWindow):
g2.addWidget(self.stream_btn) g2.addWidget(self.stream_btn)
layout.addWidget(grp2) layout.addWidget(grp2)
# ── 第 3 步:保存目录 ── # ── 第 3 步:保存目录 & 采集内容 ──
grp3 = self._step_group(3, "保存目录") grp3 = self._step_group(3, "保存目录 & 采集内容")
g3 = QHBoxLayout(grp3) vg3 = QVBoxLayout(grp3)
g3.setContentsMargins(8, 4, 8, 6) vg3.setContentsMargins(8, 4, 8, 8)
g3.setSpacing(8) vg3.setSpacing(5)
# 行 1:根目录选择
row_base = QHBoxLayout()
row_base.setSpacing(6)
lbl_base = QLabel("根目录:")
lbl_base.setFixedWidth(_sz(58))
row_base.addWidget(lbl_base)
self.base_dir_edit = QLineEdit(
self._app_settings.save_base_dir or str(Path.cwd()))
row_base.addWidget(self.base_dir_edit)
browse_base_btn = QPushButton("浏览…")
browse_base_btn.setFixedWidth(_sz(58))
browse_base_btn.setFixedHeight(_sz(26))
browse_base_btn.clicked.connect(self._browse_base_dir)
row_base.addWidget(browse_base_btn)
vg3.addLayout(row_base)
# 行 2:子目录 + 状态 + D2C + Splat
row_ses = QHBoxLayout()
row_ses.setSpacing(8)
lbl_ses = QLabel("子目录:")
lbl_ses.setFixedWidth(_sz(58))
row_ses.addWidget(lbl_ses)
today = datetime.now() today = datetime.now()
self.session_edit = QLineEdit(f"{today.year}年{today.month}月{today.day}日") self.session_edit = QLineEdit(f"{today.year}年{today.month}月{today.day}日")
self.session_edit.setMinimumWidth(_sz(160)) self.session_edit.setMinimumWidth(_sz(140))
g3.addWidget(self.session_edit) row_ses.addWidget(self.session_edit)
create_btn = QPushButton("创建 / 载入") create_btn = QPushButton("创建 / 载入")
create_btn.setFixedHeight(_sz(28)) create_btn.setFixedHeight(_sz(26))
create_btn.clicked.connect(self._create_session) create_btn.clicked.connect(self._create_session)
g3.addWidget(create_btn) row_ses.addWidget(create_btn)
self.session_status = QLabel("(未创建)") self.session_status = QLabel("(未创建)")
self.session_status.setStyleSheet(f"color:gray; font-size:{_fs(12)}px;") self.session_status.setStyleSheet(f"color:gray; font-size:{_fs(12)}px;")
g3.addWidget(self.session_status) row_ses.addWidget(self.session_status)
g3.addStretch() row_ses.addStretch()
self.d2c_check = QCheckBox("D2C 预览")
self.d2c_check = QCheckBox("启用 D2C 对齐预览") self.d2c_check.setToolTip("勾选后预览使用 D2C 对齐;拍照时始终保存全部选中的数据")
self.d2c_check.setToolTip("勾选后预览使用 D2C 对齐;拍照时始终保存全部数据")
self.d2c_check.stateChanged.connect( self.d2c_check.stateChanged.connect(
lambda s: self._worker and self._worker.set_d2c(s == Qt.Checked)) lambda s: self._worker and self._worker.set_d2c(s == Qt.Checked))
g3.addWidget(self.d2c_check) row_ses.addWidget(self.d2c_check)
row_ses.addSpacing(6)
g3.addSpacing(8)
self.capture_splat_check = QCheckBox("Splat 填充") self.capture_splat_check = QCheckBox("Splat 填充")
self.capture_splat_check.setToolTip( self.capture_splat_check.setToolTip(
"对深度图应用 Splat 扩散填充,减少 D2C 空洞(大小在「设置→程序设置」中配置)") "对深度图应用 Splat 扩散填充,减少 D2C 空洞(大小在「设置→程序设置」中配置)")
self.capture_splat_check.setChecked(self._app_settings.splat_radius > 0) self.capture_splat_check.setChecked(self._app_settings.splat_radius > 0)
self.capture_splat_check.stateChanged.connect(self._on_capture_splat_changed) self.capture_splat_check.stateChanged.connect(self._on_capture_splat_changed)
g3.addWidget(self.capture_splat_check) row_ses.addWidget(self.capture_splat_check)
vg3.addLayout(row_ses)
# 行 3:采集内容复选框
row_cap = QHBoxLayout()
row_cap.setSpacing(_sz(8))
lbl_cap = QLabel("采集内容:")
lbl_cap.setFixedWidth(_sz(58))
row_cap.addWidget(lbl_cap)
self._capture_checks: dict = {}
for key, label, default in [
("rgb", "RGB", self._app_settings.capture_rgb),
("depth", "Depth", self._app_settings.capture_depth),
("d2c", "Depth D2C", self._app_settings.capture_d2c),
("irl", "IR-L", self._app_settings.capture_irl),
("irr", "IR-R", self._app_settings.capture_irr),
("pc", "点云", self._app_settings.capture_pc),
("pc_color", "颜色点云", self._app_settings.capture_pc_color),
]:
cb = QCheckBox(label)
cb.setChecked(default)
row_cap.addWidget(cb)
self._capture_checks[key] = cb
row_cap.addStretch()
vg3.addLayout(row_cap)
layout.addWidget(grp3) layout.addWidget(grp3)
# ── 预览区 ── # ── 预览区 ──
@@ -1406,9 +1566,10 @@ class MainWindow(QMainWindow):
def _create_session(self): def _create_session(self):
name = self.session_edit.text().strip() name = self.session_edit.text().strip()
if not name: if not name:
QMessageBox.warning(self, "提示", "请输入目录名称。") QMessageBox.warning(self, "提示", "请输入子目录名称。")
return return
session_dir = DATA_DIR / name base = Path(self.base_dir_edit.text().strip() or ".")
session_dir = base / name
session_dir.mkdir(parents=True, exist_ok=True) session_dir.mkdir(parents=True, exist_ok=True)
self._session_dir = session_dir self._session_dir = session_dir
if self._worker and self._worker._camera_params: if self._worker and self._worker._camera_params:
@@ -1441,7 +1602,8 @@ class MainWindow(QMainWindow):
ts = datetime.now().strftime("%Y%m%d%H%M%S") ts = datetime.now().strftime("%Y%m%d%H%M%S")
self.capture_btn.setEnabled(False) self.capture_btn.setEnabled(False)
self.capture_btn.setText("拍摄中…") self.capture_btn.setText("拍摄中…")
self._worker.trigger_capture(str(self._session_dir / ts)) capture_opts = {k: cb.isChecked() for k, cb in self._capture_checks.items()}
self._worker.trigger_capture(str(self._session_dir / ts), capture_opts)
def _on_capture_done(self, save_dir: str): def _on_capture_done(self, save_dir: str):
self.capture_btn.setEnabled(True) self.capture_btn.setEnabled(True)