改了设置

This commit is contained in:
i
2026-04-22 10:45:09 +08:00
parent e06297f06b
commit 2996ba96e7
2 changed files with 102 additions and 37 deletions
+9 -8
View File
@@ -225,7 +225,7 @@ def _undistort_points(u, v, intr, dist):
return x_u, y_u return x_u, y_u
def software_d2c(depth_img, params, splat=True): def software_d2c(depth_img, params, splat_radius=1):
""" """
Transform a uint16 depth image (depth camera space) to an aligned uint16 Transform a uint16 depth image (depth camera space) to an aligned uint16
depth image in color camera space. depth image in color camera space.
@@ -301,12 +301,12 @@ def software_d2c(depth_img, params, splat=True):
aligned = np.zeros((color_h, color_w), dtype=np.uint16) aligned = np.zeros((color_h, color_w), dtype=np.uint16)
if splat: if splat_radius > 0:
# Splat each depth pixel to a 3×3 neighbourhood to fill sub-pixel gaps # Splat each depth pixel to an N×N neighbourhood to fill sub-pixel gaps
# that arise from forward projection. Far→near order is preserved so # that arise from forward projection. Far→near order is preserved so
# the nearest surface always wins when splats overlap. # the nearest surface always wins when splats overlap.
for dv in range(-1, 2): for dv in range(-splat_radius, splat_radius + 1):
for du in range(-1, 2): for du in range(-splat_radius, splat_radius + 1):
us = np.clip(u_vals + du, 0, color_w - 1) us = np.clip(u_vals + du, 0, color_w - 1)
vs = np.clip(v_vals + dv, 0, color_h - 1) vs = np.clip(v_vals + dv, 0, color_h - 1)
aligned[vs, us] = z_vals aligned[vs, us] = z_vals
@@ -333,7 +333,7 @@ def depth_to_colormap(aligned, min_depth_mm=200, max_depth_mm=5000, depth_scale=
return colormap return colormap
def batch_convert(params, depth_dir_str, splat=True): def batch_convert(params, depth_dir_str, splat_radius=1):
depth_dir = Path(depth_dir_str.strip().strip('"\'')) depth_dir = Path(depth_dir_str.strip().strip('"\''))
if not depth_dir.exists(): if not depth_dir.exists():
print(f" Directory not found: {depth_dir}") print(f" Directory not found: {depth_dir}")
@@ -361,7 +361,8 @@ def batch_convert(params, depth_dir_str, splat=True):
print(f" Files : {len(png_files)}") print(f" Files : {len(png_files)}")
print(f" Depth scale : {depth_scale:.6f} mm/unit") print(f" Depth scale : {depth_scale:.6f} mm/unit")
print(f" Output size : {color_w}x{color_h} (color resolution)") print(f" Output size : {color_w}x{color_h} (color resolution)")
print(f" Splat (3×3) : {'on' if splat else 'off'}") sz = 2 * splat_radius + 1
print(f" Splat : {f'{sz}×{sz}' if splat_radius > 0 else 'off'}")
print() print()
t0 = time.time() t0 = time.time()
@@ -382,7 +383,7 @@ def batch_convert(params, depth_dir_str, splat=True):
depth_img = depth_img[:, :, 0] depth_img = depth_img[:, :, 0]
depth_img = depth_img.astype(np.uint16) depth_img = depth_img.astype(np.uint16)
aligned = software_d2c(depth_img, params, splat=splat) aligned = software_d2c(depth_img, params, splat_radius=splat_radius)
# Save aligned uint16 depth # Save aligned uint16 depth
cv2.imwrite(str(out_dir / fpath.name), aligned) cv2.imwrite(str(out_dir / fpath.name), aligned)
+93 -29
View File
@@ -76,24 +76,31 @@ def _sz(n: float) -> int:
# ───────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────
class AppSettings: class AppSettings:
_CONFIG_PATH = Path(__file__).parent / "app_settings.json" _APP_DIR = Path(os.environ.get("APPDATA", Path.home())) / "OrbbecD2C"
_CONFIG_PATH = _APP_DIR / "app_settings.json"
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._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")) data = json.loads(self._CONFIG_PATH.read_text(encoding="utf-8"))
self.font_scale = data.get("font_scale", self.FONT_SCALE_AUTO) self.font_scale = data.get("font_scale", self.FONT_SCALE_AUTO)
self.splat_radius = int(data.get("splat_radius", 1))
except Exception: except Exception:
pass pass
def save(self): def save(self):
self._APP_DIR.mkdir(parents=True, exist_ok=True)
self._CONFIG_PATH.write_text( self._CONFIG_PATH.write_text(
json.dumps({"font_scale": self.font_scale}, ensure_ascii=False, indent=2), json.dumps(
{"font_scale": self.font_scale, "splat_radius": self.splat_radius},
ensure_ascii=False, indent=2,
),
encoding="utf-8", encoding="utf-8",
) )
@@ -122,7 +129,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(420), _sz(160)) self.setFixedSize(_sz(440), _sz(210))
self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint)
self._settings = settings self._settings = settings
self._build_ui() self._build_ui()
@@ -161,6 +168,25 @@ class SettingsDialog(QDialog):
info.setStyleSheet(f"color:#888; font-size:{_fs(11)}px;") info.setStyleSheet(f"color:#888; font-size:{_fs(11)}px;")
layout.addWidget(info) layout.addWidget(info)
splat_row = QHBoxLayout()
splat_lbl = QLabel("Splat 填充大小:")
splat_lbl.setMinimumWidth(_sz(110))
splat_row.addWidget(splat_lbl)
self._splat_combo = QComboBox()
self._splat_combo.setSizeAdjustPolicy(QComboBox.AdjustToContents)
for val, label in [(0, "关闭"), (1, "3×3"), (2, "5×5"), (3, "7×7")]:
self._splat_combo.addItem(label, val)
idx = self._splat_combo.findData(self._settings.splat_radius)
if idx >= 0:
self._splat_combo.setCurrentIndex(idx)
splat_row.addWidget(self._splat_combo)
splat_row.addStretch()
layout.addLayout(splat_row)
splat_info = QLabel("Splat 填充可减少深度图 D2C 后的空洞(修改立即对当前会话生效)")
splat_info.setStyleSheet(f"color:#888; font-size:{_fs(11)}px;")
layout.addWidget(splat_info)
layout.addStretch() layout.addStretch()
btn_row = QHBoxLayout() btn_row = QHBoxLayout()
@@ -176,7 +202,8 @@ class SettingsDialog(QDialog):
layout.addLayout(btn_row) layout.addLayout(btn_row)
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.save() self._settings.save()
self.accept() self.accept()
@@ -219,6 +246,17 @@ def ndarray_to_pixmap(img_bgr: np.ndarray, max_w: int, max_h: int) -> QPixmap:
return QPixmap.fromImage(qimg) return QPixmap.fromImage(qimg)
def _splat_depth(depth: np.ndarray, radius: int) -> np.ndarray:
"""Dilate non-zero depth pixels outward to fill projection gaps, then restore originals."""
if radius <= 0:
return depth
kernel = np.ones((2 * radius + 1, 2 * radius + 1), dtype=np.uint8)
dilated = cv2.dilate(depth, kernel)
valid = depth > 0
dilated[valid] = depth[valid]
return dilated
def depth_to_vis(depth_uint16: np.ndarray, scale: float) -> np.ndarray: def depth_to_vis(depth_uint16: np.ndarray, scale: float) -> np.ndarray:
depth_mm = depth_uint16.astype(np.float32) * scale depth_mm = depth_uint16.astype(np.float32) * scale
norm = np.clip((depth_mm - MIN_DEPTH_MM) / (MAX_DEPTH_MM - MIN_DEPTH_MM), 0.0, 1.0) norm = np.clip((depth_mm - MIN_DEPTH_MM) / (MAX_DEPTH_MM - MIN_DEPTH_MM), 0.0, 1.0)
@@ -306,11 +344,16 @@ class CameraWorker(QThread):
self._consec_timeouts = 0 self._consec_timeouts = 0
self._disconnect_emitted = False self._disconnect_emitted = False
self._sync_wait_count = 0 # 等待同步帧的计数器 self._sync_wait_count = 0 # 等待同步帧的计数器
self._splat_radius = 0
def set_d2c(self, enabled: bool): def set_d2c(self, enabled: bool):
with self._lock: with self._lock:
self._d2c = enabled self._d2c = enabled
def set_splat(self, radius: int):
with self._lock:
self._splat_radius = radius
def trigger_capture(self, save_dir: str): def trigger_capture(self, save_dir: str):
with self._lock: with self._lock:
self._capture_dir = save_dir self._capture_dir = save_dir
@@ -451,9 +494,10 @@ class CameraWorker(QThread):
self._consec_timeouts = 0 self._consec_timeouts = 0
with self._lock: with self._lock:
d2c = self._d2c d2c = self._d2c
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
# 首帧提取相机参数 # 首帧提取相机参数
if self._camera_params is None: if self._camera_params is None:
@@ -516,6 +560,8 @@ class CameraWorker(QThread):
depth_data = np.frombuffer(df.get_data(), dtype=np.uint16).reshape( depth_data = np.frombuffer(df.get_data(), dtype=np.uint16).reshape(
df.get_height(), df.get_width()) df.get_height(), df.get_width())
if splat_r > 0:
depth_data = _splat_depth(depth_data, splat_r)
depth_vis = depth_to_vis(depth_data, df.get_depth_scale()) depth_vis = depth_to_vis(depth_data, df.get_depth_scale())
now = time.monotonic() now = time.monotonic()
@@ -763,11 +809,11 @@ class BatchD2CWorker(QThread):
log = pyqtSignal(str) log = pyqtSignal(str)
done = pyqtSignal() done = pyqtSignal()
def __init__(self, params: dict, depth_dir: str, splat: bool = True): def __init__(self, params: dict, depth_dir: str, splat_radius: int = 1):
super().__init__() super().__init__()
self._params = params self._params = params
self._depth_dir = depth_dir self._depth_dir = depth_dir
self._splat = splat self._splat_radius = splat_radius
def run(self): def run(self):
class _Cap(io.StringIO): class _Cap(io.StringIO):
@@ -781,7 +827,7 @@ class BatchD2CWorker(QThread):
with contextlib.redirect_stdout(_Cap(self.log)): with contextlib.redirect_stdout(_Cap(self.log)):
try: try:
batch_convert(self._params, self._depth_dir, splat=self._splat) batch_convert(self._params, self._depth_dir, splat_radius=self._splat_radius)
except Exception as exc: except Exception as exc:
self.log.emit(f"Error: {exc}") self.log.emit(f"Error: {exc}")
self.done.emit() self.done.emit()
@@ -888,7 +934,15 @@ class MainWindow(QMainWindow):
def _show_settings(self): def _show_settings(self):
dlg = SettingsDialog(self._app_settings, self) dlg = SettingsDialog(self._app_settings, self)
dlg.exec_() if dlg.exec_() == QDialog.Accepted:
# 立即更新当前流的 splat 半径
if self._worker and self.capture_splat_check.isChecked():
self._worker.set_splat(max(1, self._app_settings.splat_radius))
def _on_capture_splat_changed(self, state: int):
radius = max(1, self._app_settings.splat_radius) if state == Qt.Checked else 0
if self._worker:
self._worker.set_splat(radius)
@staticmethod @staticmethod
def _step_group(num: int, title: str) -> QGroupBox: def _step_group(num: int, title: str) -> QGroupBox:
@@ -995,6 +1049,14 @@ class MainWindow(QMainWindow):
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) g3.addWidget(self.d2c_check)
g3.addSpacing(8)
self.capture_splat_check = QCheckBox("Splat 填充")
self.capture_splat_check.setToolTip(
"对深度图应用 Splat 扩散填充,减少 D2C 空洞(大小在「设置→程序设置」中配置)")
self.capture_splat_check.setChecked(self._app_settings.splat_radius > 0)
self.capture_splat_check.stateChanged.connect(self._on_capture_splat_changed)
g3.addWidget(self.capture_splat_check)
layout.addWidget(grp3) layout.addWidget(grp3)
# ── 预览区 ── # ── 预览区 ──
@@ -1142,7 +1204,7 @@ class MainWindow(QMainWindow):
step4_lbl = QLabel("第 4 步 · 开始转换") step4_lbl = QLabel("第 4 步 · 开始转换")
step4_lbl.setStyleSheet(f"color:#8ec8ff; font-weight:bold; font-size:{_fs(12)}px;") step4_lbl.setStyleSheet(f"color:#8ec8ff; font-weight:bold; font-size:{_fs(12)}px;")
step4_row.addWidget(step4_lbl) step4_row.addWidget(step4_lbl)
self.splat_check = QCheckBox("Splat 3×3 填充(减少 D2C 空洞,默认开启)") self.splat_check = QCheckBox("Splat 填充(减少 D2C 空洞,默认开启)")
self.splat_check.setChecked(True) self.splat_check.setChecked(True)
step4_row.addWidget(self.splat_check) step4_row.addWidget(self.splat_check)
step4_row.addStretch() step4_row.addStretch()
@@ -1244,16 +1306,16 @@ class MainWindow(QMainWindow):
nc, nd = len(self._color_profiles_data), len(self._depth_profiles_data) nc, nd = len(self._color_profiles_data), len(self._depth_profiles_data)
msg = f"已连接 SN: {sn} 彩色 {nc} 项 / 深度 {nd} 项" msg = f"已连接 SN: {sn} 彩色 {nc} 项 / 深度 {nd} 项"
self.device_status.setText(msg) self.device_status.setText(msg)
self.device_status.setStyleSheet("color:#4aaa4a; font-size:12px;") self.device_status.setStyleSheet(f"color:#4aaa4a; font-size:{_fs(12)}px;")
self.d2c_device_status.setText(msg) self.d2c_device_status.setText(msg)
self.d2c_device_status.setStyleSheet("color:#4aaa4a; font-size:12px;") self.d2c_device_status.setStyleSheet(f"color:#4aaa4a; font-size:{_fs(12)}px;")
except Exception as exc: except Exception as exc:
msg = f"未找到设备:{exc}" msg = f"未找到设备:{exc}"
self.device_status.setText(msg) self.device_status.setText(msg)
self.device_status.setStyleSheet("color:#cc6666; font-size:12px;") self.device_status.setStyleSheet(f"color:#cc6666; font-size:{_fs(12)}px;")
self.d2c_device_status.setText(msg) self.d2c_device_status.setText(msg)
self.d2c_device_status.setStyleSheet("color:#cc6666; font-size:12px;") self.d2c_device_status.setStyleSheet(f"color:#cc6666; font-size:{_fs(12)}px;")
# ───────────────────────────────── # ─────────────────────────────────
# Stream Toggle # Stream Toggle
@@ -1279,6 +1341,8 @@ class MainWindow(QMainWindow):
self._stop_stream() # 确保旧 worker 已停止 self._stop_stream() # 确保旧 worker 已停止
preset = self.preset_combo.currentData() # None 表示不切换 preset = self.preset_combo.currentData() # None 表示不切换
self._worker = CameraWorker(color_cfg, depth_cfg, preset=preset) self._worker = CameraWorker(color_cfg, depth_cfg, preset=preset)
if self.capture_splat_check.isChecked():
self._worker.set_splat(max(1, self._app_settings.splat_radius))
self._worker.preview_ready.connect(self._update_preview) self._worker.preview_ready.connect(self._update_preview)
self._worker.capture_done.connect(self._on_capture_done) self._worker.capture_done.connect(self._on_capture_done)
self._worker.params_ready.connect(self._on_params_ready) self._worker.params_ready.connect(self._on_params_ready)
@@ -1323,7 +1387,7 @@ class MainWindow(QMainWindow):
lbl.setText("设备已断开,请刷新后重新开启") lbl.setText("设备已断开,请刷新后重新开启")
lbl.setPixmap(QPixmap()) lbl.setPixmap(QPixmap())
self.device_status.setText("设备已断开连接,请重新插入后点击「刷新设备」") self.device_status.setText("设备已断开连接,请重新插入后点击「刷新设备」")
self.device_status.setStyleSheet("color:#cc8844; font-size:12px;") self.device_status.setStyleSheet(f"color:#cc8844; font-size:{_fs(12)}px;")
# ───────────────────────────────── # ─────────────────────────────────
# Preview # Preview
@@ -1350,7 +1414,7 @@ class MainWindow(QMainWindow):
if self._worker and self._worker._camera_params: if self._worker and self._worker._camera_params:
self._save_params_to_session(self._worker._camera_params) self._save_params_to_session(self._worker._camera_params)
self.session_status.setText(f"✓ {session_dir}") self.session_status.setText(f"✓ {session_dir}")
self.session_status.setStyleSheet("color:#2a9a2a; font-size:12px;") self.session_status.setStyleSheet(f"color:#2a9a2a; font-size:{_fs(12)}px;")
def _on_params_ready(self, params: dict): def _on_params_ready(self, params: dict):
sn = params.get("device", {}).get("serial_number", "unknown") sn = params.get("device", {}).get("serial_number", "unknown")
@@ -1394,18 +1458,18 @@ class MainWindow(QMainWindow):
di = self.d2c_depth_combo.currentIndex() di = self.d2c_depth_combo.currentIndex()
if ci < 0 or di < 0: if ci < 0 or di < 0:
self.params_selected_lbl.setText("请先刷新设备并选择分辨率") self.params_selected_lbl.setText("请先刷新设备并选择分辨率")
self.params_selected_lbl.setStyleSheet("color:#cc6666; font-size:12px;") self.params_selected_lbl.setStyleSheet(f"color:#cc6666; font-size:{_fs(12)}px;")
return return
cc = self.d2c_color_combo.itemData(ci) cc = self.d2c_color_combo.itemData(ci)
dc = self.d2c_depth_combo.itemData(di) dc = self.d2c_depth_combo.itemData(di)
if not cc or not dc: if not cc or not dc:
self.params_selected_lbl.setText("无配置数据,请先刷新设备") self.params_selected_lbl.setText("无配置数据,请先刷新设备")
self.params_selected_lbl.setStyleSheet("color:#cc6666; font-size:12px;") self.params_selected_lbl.setStyleSheet(f"color:#cc6666; font-size:{_fs(12)}px;")
return return
self.params_selected_lbl.setText("正在从设备获取参数…") self.params_selected_lbl.setText("正在从设备获取参数…")
self.params_selected_lbl.setStyleSheet("color:#888; font-size:12px;") self.params_selected_lbl.setStyleSheet(f"color:#888; font-size:{_fs(12)}px;")
self._fetch_worker = FetchParamsWorker(cc, dc) self._fetch_worker = FetchParamsWorker(cc, dc)
self._fetch_worker.log.connect(self._append_d2c_log) self._fetch_worker.log.connect(self._append_d2c_log)
self._fetch_worker.params_ready.connect(self._on_fetch_params_ready) self._fetch_worker.params_ready.connect(self._on_fetch_params_ready)
@@ -1414,7 +1478,7 @@ class MainWindow(QMainWindow):
def _on_fetch_params_ready(self, params: dict, path: str): def _on_fetch_params_ready(self, params: dict, path: str):
self.params_path_edit.setText(path) self.params_path_edit.setText(path)
self.params_selected_lbl.setText(f"✓ 已获取并缓存:{path}") self.params_selected_lbl.setText(f"✓ 已获取并缓存:{path}")
self.params_selected_lbl.setStyleSheet("color:#4aaa4a; font-size:12px;") self.params_selected_lbl.setStyleSheet(f"color:#4aaa4a; font-size:{_fs(12)}px;")
self._append_d2c_log( self._append_d2c_log(
f"[参数获取] RGB {params['color']['width']}x{params['color']['height']} " f"[参数获取] RGB {params['color']['width']}x{params['color']['height']} "
f"/ Depth {params['depth']['width']}x{params['depth']['height']}" f"/ Depth {params['depth']['width']}x{params['depth']['height']}"
@@ -1476,7 +1540,7 @@ class MainWindow(QMainWindow):
if f: if f:
edit.setText(f) edit.setText(f)
self.params_selected_lbl.setText(f"✓ {f}") self.params_selected_lbl.setText(f"✓ {f}")
self.params_selected_lbl.setStyleSheet("color:#4aaa4a; font-size:12px;") self.params_selected_lbl.setStyleSheet(f"color:#4aaa4a; font-size:{_fs(12)}px;")
def _run_batch_d2c(self): def _run_batch_d2c(self):
depth_dir = self.d2c_dir_edit.text().strip() depth_dir = self.d2c_dir_edit.text().strip()
@@ -1496,8 +1560,8 @@ class MainWindow(QMainWindow):
self.d2c_log.append(f"[参数文件] {params_file}") self.d2c_log.append(f"[参数文件] {params_file}")
self.d2c_log.append(f"[深度图目录] {depth_dir}\n") self.d2c_log.append(f"[深度图目录] {depth_dir}\n")
self.run_d2c_btn.setEnabled(False) self.run_d2c_btn.setEnabled(False)
self._batch_worker = BatchD2CWorker(params, depth_dir, splat_r = self._app_settings.splat_radius if self.splat_check.isChecked() else 0
splat=self.splat_check.isChecked()) self._batch_worker = BatchD2CWorker(params, depth_dir, splat_radius=splat_r)
self._batch_worker.log.connect(self.d2c_log.append) self._batch_worker.log.connect(self.d2c_log.append)
self._batch_worker.done.connect(self._on_batch_done) self._batch_worker.done.connect(self._on_batch_done)
self._batch_worker.start() self._batch_worker.start()