a
This commit is contained in:
+130
-13
@@ -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__":
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user