This commit is contained in:
i
2026-04-22 11:35:36 +08:00
parent e355757b43
commit c204293162
2 changed files with 147 additions and 20 deletions
+130 -13
View File
@@ -316,6 +316,96 @@ def software_d2c(depth_img, params, splat_radius=1):
return aligned
# ---------------------------------------------------------------------------
# Inverse-projection Software D2C (no gaps)
# ---------------------------------------------------------------------------
def software_d2c_inv(depth_img, params):
"""
Inverse-projection D2C: for each output (color) pixel, back-project through
the color camera into depth-camera space and sample the depth image.
Produces a dense aligned map with no forward-projection holes.
Two-pass approach:
Pass 0 – rotation-only to obtain a coarse depth estimate.
Pass 1 – full R/t transform with that estimate for a refined map.
Parameters
----------
depth_img : np.ndarray (H_d × W_d, uint16)
params : dict camera_params.json content
Returns
-------
aligned : np.ndarray (H_c × W_c, uint16)
"""
depth_intr = params["depth"]["intrinsic"]
color_intr = params["color"]["intrinsic"]
ext = params["extrinsic"]
depth_scale = params["depth"]["scale"] # mm / sensor-unit
color_w = color_intr["width"]
color_h = color_intr["height"]
fx_d, fy_d = depth_intr["fx"], depth_intr["fy"]
cx_d, cy_d = depth_intr["cx"], depth_intr["cy"]
fx_c, fy_c = color_intr["fx"], color_intr["fy"]
cx_c, cy_c = color_intr["cx"], color_intr["cy"]
R = np.array(ext["rot"], dtype=np.float64).reshape(3, 3)
t = np.array(ext["transform"], dtype=np.float64) # mm
R_inv = R.T # R^{-1} = R^T for rotation matrices
b = R_inv @ t # translation in depth-camera frame (mm)
# Color pixel grid
uc, vc = np.meshgrid(np.arange(color_w, dtype=np.float32),
np.arange(color_h, dtype=np.float32))
xn_c = (uc - cx_c) / fx_c
yn_c = (vc - cy_c) / fy_c
# Ray directions in depth-camera space: [ax, ay, az] = R^T @ [xn, yn, 1]
N = color_w * color_h
dirs = np.stack([xn_c.ravel(), yn_c.ravel(), np.ones(N, np.float32)], axis=0)
dirs_d = (R_inv @ dirs.astype(np.float64)).astype(np.float32)
ax = dirs_d[0].reshape(color_h, color_w)
ay = dirs_d[1].reshape(color_h, color_w)
az = dirs_d[2].reshape(color_h, color_w)
depth_f32 = depth_img.astype(np.float32)
az_safe = np.where(az > 1e-6, az, 1.0).astype(np.float32)
# Pass 0: rotation-only estimate of depth-image coordinates
m0_u = (fx_d * ax / az_safe + cx_d).astype(np.float32)
m0_v = (fy_d * ay / az_safe + cy_d).astype(np.float32)
Z_d0 = cv2.remap(depth_f32, m0_u, m0_v,
cv2.INTER_NEAREST,
borderMode=cv2.BORDER_CONSTANT, borderValue=0)
# Estimate Z_c (mm): Z_d_mm = Z_c*az - b[2] → Z_c = (Z_d_mm + b[2]) / az
bx, by, bz = float(b[0]), float(b[1]), float(b[2])
Z_c_mm = np.where(az_safe > 1e-6,
(Z_d0 * depth_scale + bz) / az_safe,
0.0).astype(np.float32)
# Pass 1: full transform using Z_c estimate
X_d = Z_c_mm * ax - bx
Y_d = Z_c_mm * ay - by
Z_d = Z_c_mm * az - bz
Z_d_safe = np.where(Z_d > 1e-6, Z_d, 1.0).astype(np.float32)
m1_u = (fx_d * X_d / Z_d_safe + cx_d).astype(np.float32)
m1_v = (fy_d * Y_d / Z_d_safe + cy_d).astype(np.float32)
valid = (Z_d0 > 0) & (az > 1e-6) & (Z_d > 1e-6)
m1_u = np.where(valid, m1_u, -1.0).astype(np.float32)
m1_v = np.where(valid, m1_v, -1.0).astype(np.float32)
aligned = cv2.remap(depth_f32, m1_u, m1_v,
cv2.INTER_NEAREST,
borderMode=cv2.BORDER_CONSTANT, borderValue=0)
return aligned.astype(np.uint16)
# ---------------------------------------------------------------------------
# Batch conversion
# ---------------------------------------------------------------------------
@@ -333,7 +423,16 @@ def depth_to_colormap(aligned, min_depth_mm=200, max_depth_mm=5000, depth_scale=
return colormap
def batch_convert(params, depth_dir_str, splat_radius=1):
def batch_convert(params, depth_dir_str, method="inverse", splat_radius=1):
"""
Batch D2C conversion.
Parameters
----------
method : "inverse" (default) — inverse-projection, no gaps
"forward" — forward-projection with Splat fill
splat_radius : int — Splat kernel half-size (only used when method="forward")
"""
depth_dir = Path(depth_dir_str.strip().strip('"\''))
if not depth_dir.exists():
print(f" Directory not found: {depth_dir}")
@@ -345,24 +444,32 @@ def batch_convert(params, depth_dir_str, splat_radius=1):
print(f" No PNG files found in: {depth_dir}")
return
out_dir = depth_dir.parent / (depth_dir.name + "_d2c")
vis_dir = depth_dir.parent / (depth_dir.name + "_d2c_vis")
out_dir = depth_dir.parent / (depth_dir.name + "_d2c")
vis_dir = depth_dir.parent / (depth_dir.name + "_d2c_vis")
orig_vis_dir = depth_dir.parent / (depth_dir.name + "_depth_vis")
out_dir.mkdir(exist_ok=True)
vis_dir.mkdir(exist_ok=True)
orig_vis_dir.mkdir(exist_ok=True)
color_w = params["color"]["intrinsic"]["width"]
color_h = params["color"]["intrinsic"]["height"]
depth_scale = params["depth"]["scale"]
if method == "forward":
sz = 2 * splat_radius + 1
method_str = f"正向投影 + Splat {sz}×{sz}" if splat_radius > 0 else "正向投影(无 Splat)"
else:
method_str = "逆向投影(SDK 同等)"
print(f"\n=== Phase 2: Batch D2C Conversion ===")
print(f" Input dir : {depth_dir}")
print(f" Aligned depth: {out_dir}")
print(f" Pseudo-color : {vis_dir}")
print(f" D2C vis : {vis_dir}")
print(f" Orig depth : {orig_vis_dir}")
print(f" Files : {len(png_files)}")
print(f" Depth scale : {depth_scale:.6f} mm/unit")
print(f" Output size : {color_w}x{color_h} (color resolution)")
sz = 2 * splat_radius + 1
print(f" Splat : {f'{sz}×{sz}' if splat_radius > 0 else 'off'}")
print(f" Method : {method_str}")
print()
t0 = time.time()
@@ -383,15 +490,23 @@ def batch_convert(params, depth_dir_str, splat_radius=1):
depth_img = depth_img[:, :, 0]
depth_img = depth_img.astype(np.uint16)
aligned = software_d2c(depth_img, params, splat_radius=splat_radius)
# Save original depth pseudo-color
vis_name = fpath.stem + "_vis.png"
cv2.imwrite(str(orig_vis_dir / vis_name),
depth_to_colormap(depth_img, depth_scale=depth_scale))
# D2C alignment
if method == "forward":
aligned = software_d2c(depth_img, params, splat_radius=splat_radius)
else:
aligned = software_d2c_inv(depth_img, params)
# Save aligned uint16 depth
cv2.imwrite(str(out_dir / fpath.name), aligned)
# Save pseudo-color visualization
colormap = depth_to_colormap(aligned, depth_scale=depth_scale)
vis_name = fpath.stem + "_vis.png"
cv2.imwrite(str(vis_dir / vis_name), colormap)
# Save aligned depth pseudo-color
cv2.imwrite(str(vis_dir / vis_name),
depth_to_colormap(aligned, depth_scale=depth_scale))
ok += 1
print(f" [{i:>4}/{len(png_files)}] {fpath.name}")
@@ -399,7 +514,8 @@ def batch_convert(params, depth_dir_str, splat_radius=1):
elapsed = time.time() - t0
print(f"\n Done. {ok}/{len(png_files)} files converted in {elapsed:.1f}s")
print(f" Aligned depth : {out_dir}")
print(f" Pseudo-color : {vis_dir}")
print(f" D2C vis : {vis_dir}")
print(f" Orig depth : {orig_vis_dir}")
# ---------------------------------------------------------------------------
@@ -476,7 +592,8 @@ def main():
print("No directory entered. Exiting.")
return
batch_convert(params, depth_dir, splat=not no_splat)
method = "forward" if no_splat else "inverse"
batch_convert(params, depth_dir, method=method)
if __name__ == "__main__":
+17 -7
View File
@@ -714,6 +714,8 @@ class CameraWorker(QThread):
raw = np.frombuffer(df.get_data(), dtype=np.uint16).reshape(
df.get_height(), df.get_width())
imwrite_utf8(str(base / "depth.png"), raw)
imwrite_utf8(str(base / "depth_vis.png"),
depth_to_vis(raw, df.get_depth_scale()))
need_aligned = (capture_opts.get("d2c", True) or
capture_opts.get("pc", False) or
@@ -911,10 +913,12 @@ class BatchD2CWorker(QThread):
log = pyqtSignal(str)
done = pyqtSignal()
def __init__(self, params: dict, depth_dir: str, splat_radius: int = 1):
def __init__(self, params: dict, depth_dir: str,
method: str = "inverse", splat_radius: int = 1):
super().__init__()
self._params = params
self._depth_dir = depth_dir
self._method = method
self._splat_radius = splat_radius
def run(self):
@@ -929,7 +933,9 @@ class BatchD2CWorker(QThread):
with contextlib.redirect_stdout(_Cap(self.log)):
try:
batch_convert(self._params, self._depth_dir, splat_radius=self._splat_radius)
batch_convert(self._params, self._depth_dir,
method=self._method,
splat_radius=self._splat_radius)
except Exception as exc:
self.log.emit(f"Error: {exc}")
self.done.emit()
@@ -1364,9 +1370,11 @@ class MainWindow(QMainWindow):
step4_lbl = QLabel("第 4 步 · 开始转换")
step4_lbl.setStyleSheet(f"color:#8ec8ff; font-weight:bold; font-size:{_fs(12)}px;")
step4_row.addWidget(step4_lbl)
self.splat_check = QCheckBox("Splat 填充(减少 D2C 空洞,默认开启)")
self.splat_check.setChecked(True)
step4_row.addWidget(self.splat_check)
self.d2c_method_combo = QComboBox()
self.d2c_method_combo.setSizeAdjustPolicy(QComboBox.AdjustToContents)
self.d2c_method_combo.addItem("逆向投影(SDK 同等,推荐)", "inverse")
self.d2c_method_combo.addItem("正向投影 + Splat 填充", "forward")
step4_row.addWidget(self.d2c_method_combo)
step4_row.addStretch()
self.run_d2c_btn = QPushButton("▶ 开始批量 D2C 转换")
self.run_d2c_btn.setFixedHeight(_sz(36))
@@ -1722,8 +1730,10 @@ class MainWindow(QMainWindow):
self.d2c_log.append(f"[参数文件] {params_file}")
self.d2c_log.append(f"[深度图目录] {depth_dir}\n")
self.run_d2c_btn.setEnabled(False)
splat_r = self._app_settings.splat_radius if self.splat_check.isChecked() else 0
self._batch_worker = BatchD2CWorker(params, depth_dir, splat_radius=splat_r)
method = self.d2c_method_combo.currentData()
splat_r = self._app_settings.splat_radius if method == "forward" else 1
self._batch_worker = BatchD2CWorker(params, depth_dir,
method=method, splat_radius=splat_r)
self._batch_worker.log.connect(self.d2c_log.append)
self._batch_worker.done.connect(self._on_batch_done)
self._batch_worker.start()