2026-04-21 15:39:05 +08:00
|
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
"""
|
|
|
|
|
|
Orbbec D2C Batch Converter (Software)
|
|
|
|
|
|
|
|
|
|
|
|
Usage:
|
|
|
|
|
|
python d2c_batch.py # Interactive: select profiles, fetch params, convert
|
|
|
|
|
|
python d2c_batch.py --params # Only fetch/update camera params (no conversion)
|
|
|
|
|
|
python d2c_batch.py --convert # Only convert using saved camera_params.json
|
|
|
|
|
|
|
|
|
|
|
|
Workflow:
|
|
|
|
|
|
1. Select RGB profile (resolution / fps / format)
|
|
|
|
|
|
2. Select Depth profile (resolution / fps / format)
|
|
|
|
|
|
3. Camera intrinsics + extrinsics are fetched and saved to camera_params.json
|
|
|
|
|
|
4. Enter depth image directory
|
|
|
|
|
|
5. Batch-convert all PNG depth images via software D2C
|
|
|
|
|
|
-> Output saved to <input_dir>_d2c/
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
import os
|
|
|
|
|
|
import sys
|
|
|
|
|
|
import json
|
|
|
|
|
|
import time
|
|
|
|
|
|
import numpy as np
|
|
|
|
|
|
import cv2
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
2026-04-21 20:26:21 +08:00
|
|
|
|
try:
|
|
|
|
|
|
from PIL import Image as _PIL_Image
|
|
|
|
|
|
_HAS_PIL = True
|
|
|
|
|
|
except ImportError:
|
|
|
|
|
|
_HAS_PIL = False
|
|
|
|
|
|
|
2026-04-21 15:39:05 +08:00
|
|
|
|
PARAMS_FILE = "camera_params.json"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Camera parameter acquisition
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
def _list_video_profiles(profile_list):
|
|
|
|
|
|
"""Return list of (index, VideoStreamProfile) for all video profiles."""
|
|
|
|
|
|
results = []
|
|
|
|
|
|
count = profile_list.get_count()
|
|
|
|
|
|
for i in range(count):
|
|
|
|
|
|
p = profile_list.get_stream_profile_by_index(i)
|
|
|
|
|
|
vp = p.as_video_stream_profile()
|
|
|
|
|
|
if vp is None:
|
|
|
|
|
|
continue
|
|
|
|
|
|
results.append((i, vp))
|
|
|
|
|
|
return results
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _print_profiles(profiles, sensor_name):
|
|
|
|
|
|
print(f"\nAvailable {sensor_name} profiles:")
|
|
|
|
|
|
print(f" {'#':<4} {'Resolution':<14} {'FPS':<6} {'Format'}")
|
|
|
|
|
|
print(f" {'-'*40}")
|
|
|
|
|
|
for idx, vp in profiles:
|
|
|
|
|
|
fmt = str(vp.get_format()).split(".")[-1]
|
|
|
|
|
|
print(f" [{idx:<2}] {vp.get_width()}x{vp.get_height():<8} {vp.get_fps():<6} {fmt}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _select_profile(profile_list, sensor_name):
|
|
|
|
|
|
"""Interactive profile selection. Returns the selected VideoStreamProfile."""
|
|
|
|
|
|
profiles = _list_video_profiles(profile_list)
|
|
|
|
|
|
if not profiles:
|
|
|
|
|
|
print(f" No {sensor_name} profiles found!")
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
_print_profiles(profiles, sensor_name)
|
|
|
|
|
|
indices = [i for i, _ in profiles]
|
|
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
|
raw = input(f"Select {sensor_name} profile index: ").strip()
|
|
|
|
|
|
try:
|
|
|
|
|
|
choice = int(raw)
|
|
|
|
|
|
if choice in indices:
|
|
|
|
|
|
selected = next(vp for i, vp in profiles if i == choice)
|
|
|
|
|
|
fmt = str(selected.get_format()).split(".")[-1]
|
|
|
|
|
|
print(f" -> Selected: {selected.get_width()}x{selected.get_height()} @ {selected.get_fps()}fps {fmt}")
|
|
|
|
|
|
return selected
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
print(f" Invalid. Choose from: {indices}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _intrinsic_to_dict(intr):
|
|
|
|
|
|
return {
|
|
|
|
|
|
"fx": float(intr.fx), "fy": float(intr.fy),
|
|
|
|
|
|
"cx": float(intr.cx), "cy": float(intr.cy),
|
|
|
|
|
|
"width": int(intr.width), "height": int(intr.height),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _distortion_to_dict(dist):
|
|
|
|
|
|
return {
|
|
|
|
|
|
"k1": float(dist.k1), "k2": float(dist.k2),
|
|
|
|
|
|
"k3": float(dist.k3), "k4": float(dist.k4),
|
|
|
|
|
|
"k5": float(dist.k5), "k6": float(dist.k6),
|
|
|
|
|
|
"p1": float(dist.p1), "p2": float(dist.p2),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def fetch_camera_params():
|
|
|
|
|
|
"""
|
|
|
|
|
|
Interactively select color + depth profiles, start the pipeline briefly to
|
|
|
|
|
|
capture one frameset, extract intrinsics/extrinsics, and return as a dict.
|
|
|
|
|
|
"""
|
|
|
|
|
|
from pyorbbecsdk import Pipeline, Config, OBSensorType
|
|
|
|
|
|
|
|
|
|
|
|
print("\n=== Phase 1: Camera Parameter Acquisition ===")
|
|
|
|
|
|
pipeline = Pipeline()
|
|
|
|
|
|
config = Config()
|
|
|
|
|
|
|
|
|
|
|
|
# --- Color profile ---
|
|
|
|
|
|
try:
|
|
|
|
|
|
color_list = pipeline.get_stream_profile_list(OBSensorType.COLOR_SENSOR)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f" Cannot get color profiles: {e}")
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
color_profile = _select_profile(color_list, "RGB (Color)")
|
|
|
|
|
|
if color_profile is None:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
# --- Depth profile ---
|
|
|
|
|
|
try:
|
|
|
|
|
|
depth_list = pipeline.get_stream_profile_list(OBSensorType.DEPTH_SENSOR)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f" Cannot get depth profiles: {e}")
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
depth_profile = _select_profile(depth_list, "Depth")
|
|
|
|
|
|
if depth_profile is None:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
config.enable_stream(color_profile)
|
|
|
|
|
|
config.enable_stream(depth_profile)
|
|
|
|
|
|
|
|
|
|
|
|
print("\n Starting pipeline...")
|
|
|
|
|
|
try:
|
|
|
|
|
|
pipeline.start(config)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f" Failed to start pipeline: {e}")
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
# --- Wait for first valid frameset ---
|
|
|
|
|
|
frames = None
|
|
|
|
|
|
deadline = time.time() + 10.0
|
|
|
|
|
|
while time.time() < deadline:
|
|
|
|
|
|
f = pipeline.wait_for_frames(200)
|
|
|
|
|
|
if f and f.get_color_frame() and f.get_depth_frame():
|
|
|
|
|
|
frames = f
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
if frames is None:
|
|
|
|
|
|
print(" Timeout: no frameset received within 10 s.")
|
|
|
|
|
|
pipeline.stop()
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
color_frame = frames.get_color_frame()
|
|
|
|
|
|
depth_frame = frames.get_depth_frame()
|
|
|
|
|
|
|
|
|
|
|
|
# --- Extract profile-specific intrinsics from actual frames ---
|
|
|
|
|
|
color_vp = color_frame.get_stream_profile().as_video_stream_profile()
|
|
|
|
|
|
depth_vp = depth_frame.get_stream_profile().as_video_stream_profile()
|
|
|
|
|
|
|
|
|
|
|
|
color_intr = color_vp.get_intrinsic()
|
|
|
|
|
|
color_dist = color_vp.get_distortion()
|
|
|
|
|
|
depth_intr = depth_vp.get_intrinsic()
|
|
|
|
|
|
depth_dist = depth_vp.get_distortion()
|
|
|
|
|
|
|
|
|
|
|
|
# Extrinsic: depth camera -> color camera
|
|
|
|
|
|
extrinsic = depth_vp.get_extrinsic_to(color_vp)
|
|
|
|
|
|
|
|
|
|
|
|
depth_scale = depth_frame.get_depth_scale() # mm per raw unit
|
|
|
|
|
|
|
|
|
|
|
|
pipeline.stop()
|
|
|
|
|
|
print(" Pipeline stopped.")
|
|
|
|
|
|
|
|
|
|
|
|
params = {
|
|
|
|
|
|
"color": {
|
|
|
|
|
|
"width": color_frame.get_width(),
|
|
|
|
|
|
"height": color_frame.get_height(),
|
|
|
|
|
|
"fps": color_profile.get_fps(),
|
|
|
|
|
|
"format": str(color_profile.get_format()).split(".")[-1],
|
|
|
|
|
|
"intrinsic": _intrinsic_to_dict(color_intr),
|
|
|
|
|
|
"distortion": _distortion_to_dict(color_dist),
|
|
|
|
|
|
},
|
|
|
|
|
|
"depth": {
|
|
|
|
|
|
"width": depth_frame.get_width(),
|
|
|
|
|
|
"height": depth_frame.get_height(),
|
|
|
|
|
|
"fps": depth_profile.get_fps(),
|
|
|
|
|
|
"format": str(depth_profile.get_format()).split(".")[-1],
|
|
|
|
|
|
"scale": float(depth_scale),
|
|
|
|
|
|
"intrinsic": _intrinsic_to_dict(depth_intr),
|
|
|
|
|
|
"distortion": _distortion_to_dict(depth_dist),
|
|
|
|
|
|
},
|
|
|
|
|
|
# rot: 9-element flat array (row-major 3x3), transform: 3-element translation (mm)
|
|
|
|
|
|
"extrinsic": {
|
|
|
|
|
|
"rot": extrinsic.rot.tolist(),
|
|
|
|
|
|
"transform": extrinsic.transform.tolist(),
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
return params
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Software D2C transformation
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
def _undistort_points(u, v, intr, dist):
|
|
|
|
|
|
"""Brown-Conrady lens undistortion (returns undistorted normalised coords)."""
|
|
|
|
|
|
fx, fy = intr["fx"], intr["fy"]
|
|
|
|
|
|
cx, cy = intr["cx"], intr["cy"]
|
|
|
|
|
|
k1, k2, k3 = dist["k1"], dist["k2"], dist["k3"]
|
|
|
|
|
|
p1, p2 = dist["p1"], dist["p2"]
|
|
|
|
|
|
|
|
|
|
|
|
x = (u - cx) / fx
|
|
|
|
|
|
y = (v - cy) / fy
|
|
|
|
|
|
|
|
|
|
|
|
r2 = x * x + y * y
|
|
|
|
|
|
radial = 1.0 + k1 * r2 + k2 * r2**2 + k3 * r2**3
|
|
|
|
|
|
x_u = x * radial + 2.0 * p1 * x * y + p2 * (r2 + 2.0 * x * x)
|
|
|
|
|
|
y_u = y * radial + p1 * (r2 + 2.0 * y * y) + 2.0 * p2 * x * y
|
|
|
|
|
|
return x_u, y_u
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-04-21 20:26:21 +08:00
|
|
|
|
def software_d2c(depth_img, params, splat=True):
|
2026-04-21 15:39:05 +08:00
|
|
|
|
"""
|
|
|
|
|
|
Transform a uint16 depth image (depth camera space) to an aligned uint16
|
|
|
|
|
|
depth image in color camera space.
|
|
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
|
|
|
|
|
depth_img : np.ndarray (H_d x W_d, uint16) raw depth in sensor units
|
|
|
|
|
|
params : dict camera_params.json content
|
|
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
|
-------
|
|
|
|
|
|
aligned : np.ndarray (H_c x W_c, uint16) aligned depth in sensor units
|
|
|
|
|
|
"""
|
|
|
|
|
|
depth_intr = params["depth"]["intrinsic"]
|
|
|
|
|
|
depth_dist = params["depth"]["distortion"]
|
|
|
|
|
|
color_intr = params["color"]["intrinsic"]
|
|
|
|
|
|
ext = params["extrinsic"]
|
|
|
|
|
|
|
|
|
|
|
|
color_w = color_intr["width"]
|
|
|
|
|
|
color_h = color_intr["height"]
|
|
|
|
|
|
fx_c, fy_c = color_intr["fx"], color_intr["fy"]
|
|
|
|
|
|
cx_c, cy_c = color_intr["cx"], color_intr["cy"]
|
|
|
|
|
|
|
|
|
|
|
|
dh, dw = depth_img.shape
|
|
|
|
|
|
depth_scale = params["depth"]["scale"]
|
|
|
|
|
|
|
|
|
|
|
|
# Pixel grid
|
|
|
|
|
|
u_d = np.arange(dw, dtype=np.float32)
|
|
|
|
|
|
v_d = np.arange(dh, dtype=np.float32)
|
|
|
|
|
|
u_d, v_d = np.meshgrid(u_d, v_d) # (dh, dw)
|
|
|
|
|
|
|
|
|
|
|
|
# Depth in mm (float)
|
|
|
|
|
|
Z = depth_img.astype(np.float32) * depth_scale
|
|
|
|
|
|
valid = Z > 0
|
|
|
|
|
|
|
|
|
|
|
|
# Undistort + unproject to 3-D (depth camera space, mm)
|
|
|
|
|
|
x_u, y_u = _undistort_points(u_d, v_d, depth_intr, depth_dist)
|
|
|
|
|
|
X = x_u * Z # (dh, dw)
|
|
|
|
|
|
Y = y_u * Z
|
|
|
|
|
|
|
|
|
|
|
|
# Extrinsic: R (3x3) and t (3,) in mm
|
|
|
|
|
|
R = np.asarray(ext["rot"], dtype=np.float64).reshape(3, 3)
|
|
|
|
|
|
t = np.asarray(ext["transform"], dtype=np.float64)
|
|
|
|
|
|
|
|
|
|
|
|
# Vectorised transform [X_c, Y_c, Z_c] = R @ [X, Y, Z]^T + t
|
|
|
|
|
|
pts = np.stack([X.ravel(), Y.ravel(), Z.ravel()], axis=0).astype(np.float64) # (3, N)
|
|
|
|
|
|
pts_c = R @ pts + t[:, np.newaxis] # (3, N)
|
|
|
|
|
|
|
|
|
|
|
|
Xc = pts_c[0].reshape(dh, dw).astype(np.float32)
|
|
|
|
|
|
Yc = pts_c[1].reshape(dh, dw).astype(np.float32)
|
|
|
|
|
|
Zc = pts_c[2].reshape(dh, dw).astype(np.float32)
|
|
|
|
|
|
|
|
|
|
|
|
# Project onto color image plane
|
|
|
|
|
|
valid_c = valid & (Zc > 0)
|
|
|
|
|
|
u_c = np.where(valid_c, fx_c * Xc / Zc + cx_c, -1.0).astype(np.float32)
|
|
|
|
|
|
v_c = np.where(valid_c, fy_c * Yc / Zc + cy_c, -1.0).astype(np.float32)
|
|
|
|
|
|
|
|
|
|
|
|
u_ci = np.round(u_c).astype(np.int32)
|
|
|
|
|
|
v_ci = np.round(v_c).astype(np.int32)
|
|
|
|
|
|
|
|
|
|
|
|
in_bounds = (u_ci >= 0) & (u_ci < color_w) & (v_ci >= 0) & (v_ci < color_h)
|
|
|
|
|
|
mask = valid_c & in_bounds
|
|
|
|
|
|
|
|
|
|
|
|
u_vals = u_ci[mask]
|
|
|
|
|
|
v_vals = v_ci[mask]
|
|
|
|
|
|
z_vals = depth_img[mask] # keep original uint16 sensor units
|
|
|
|
|
|
|
|
|
|
|
|
# Write far pixels first so nearer pixels overwrite (z small = close)
|
|
|
|
|
|
order = np.argsort(z_vals)[::-1]
|
|
|
|
|
|
u_vals = u_vals[order]
|
|
|
|
|
|
v_vals = v_vals[order]
|
|
|
|
|
|
z_vals = z_vals[order]
|
|
|
|
|
|
|
|
|
|
|
|
aligned = np.zeros((color_h, color_w), dtype=np.uint16)
|
2026-04-21 20:26:21 +08:00
|
|
|
|
|
|
|
|
|
|
if splat:
|
|
|
|
|
|
# Splat each depth pixel to a 3×3 neighbourhood to fill sub-pixel gaps
|
|
|
|
|
|
# that arise from forward projection. Far→near order is preserved so
|
|
|
|
|
|
# the nearest surface always wins when splats overlap.
|
|
|
|
|
|
for dv in range(-1, 2):
|
|
|
|
|
|
for du in range(-1, 2):
|
|
|
|
|
|
us = np.clip(u_vals + du, 0, color_w - 1)
|
|
|
|
|
|
vs = np.clip(v_vals + dv, 0, color_h - 1)
|
|
|
|
|
|
aligned[vs, us] = z_vals
|
|
|
|
|
|
else:
|
|
|
|
|
|
aligned[v_vals, u_vals] = z_vals
|
|
|
|
|
|
|
2026-04-21 15:39:05 +08:00
|
|
|
|
return aligned
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Batch conversion
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
def depth_to_colormap(aligned, min_depth_mm=200, max_depth_mm=5000, depth_scale=1.0):
|
|
|
|
|
|
"""Convert uint16 aligned depth to a JET pseudo-color BGR image."""
|
|
|
|
|
|
depth_mm = aligned.astype(np.float32) * depth_scale
|
|
|
|
|
|
valid = (depth_mm > min_depth_mm) & (depth_mm < max_depth_mm)
|
|
|
|
|
|
norm = np.zeros_like(depth_mm)
|
|
|
|
|
|
norm[valid] = (depth_mm[valid] - min_depth_mm) / (max_depth_mm - min_depth_mm)
|
|
|
|
|
|
norm = np.clip(norm, 0.0, 1.0)
|
|
|
|
|
|
gray = (norm * 255).astype(np.uint8)
|
|
|
|
|
|
colormap = cv2.applyColorMap(gray, cv2.COLORMAP_JET)
|
|
|
|
|
|
colormap[~valid] = 0 # black for invalid pixels
|
|
|
|
|
|
return colormap
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-04-21 20:26:21 +08:00
|
|
|
|
def batch_convert(params, depth_dir_str, splat=True):
|
2026-04-21 15:39:05 +08:00
|
|
|
|
depth_dir = Path(depth_dir_str.strip().strip('"\''))
|
|
|
|
|
|
if not depth_dir.exists():
|
|
|
|
|
|
print(f" Directory not found: {depth_dir}")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
png_files = sorted(depth_dir.glob("*.png")) + sorted(depth_dir.glob("*.PNG"))
|
|
|
|
|
|
png_files = sorted(set(png_files))
|
|
|
|
|
|
if not png_files:
|
|
|
|
|
|
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.mkdir(exist_ok=True)
|
|
|
|
|
|
vis_dir.mkdir(exist_ok=True)
|
|
|
|
|
|
|
|
|
|
|
|
color_w = params["color"]["intrinsic"]["width"]
|
|
|
|
|
|
color_h = params["color"]["intrinsic"]["height"]
|
|
|
|
|
|
depth_scale = params["depth"]["scale"]
|
|
|
|
|
|
|
|
|
|
|
|
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" Files : {len(png_files)}")
|
|
|
|
|
|
print(f" Depth scale : {depth_scale:.6f} mm/unit")
|
|
|
|
|
|
print(f" Output size : {color_w}x{color_h} (color resolution)")
|
2026-04-21 20:26:21 +08:00
|
|
|
|
print(f" Splat (3×3) : {'on' if splat else 'off'}")
|
2026-04-21 15:39:05 +08:00
|
|
|
|
print()
|
|
|
|
|
|
|
|
|
|
|
|
t0 = time.time()
|
|
|
|
|
|
ok = 0
|
|
|
|
|
|
for i, fpath in enumerate(png_files, 1):
|
2026-04-21 20:26:21 +08:00
|
|
|
|
depth_img = None
|
|
|
|
|
|
if _HAS_PIL:
|
|
|
|
|
|
try:
|
|
|
|
|
|
depth_img = np.array(_PIL_Image.open(str(fpath)))
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
depth_img = None
|
|
|
|
|
|
if depth_img is None:
|
|
|
|
|
|
depth_img = cv2.imread(str(fpath), cv2.IMREAD_UNCHANGED)
|
2026-04-21 15:39:05 +08:00
|
|
|
|
if depth_img is None:
|
|
|
|
|
|
print(f" [{i:>4}/{len(png_files)}] SKIP (unreadable): {fpath.name}")
|
|
|
|
|
|
continue
|
|
|
|
|
|
if depth_img.ndim != 2:
|
|
|
|
|
|
depth_img = depth_img[:, :, 0]
|
|
|
|
|
|
depth_img = depth_img.astype(np.uint16)
|
|
|
|
|
|
|
2026-04-21 20:26:21 +08:00
|
|
|
|
aligned = software_d2c(depth_img, params, splat=splat)
|
2026-04-21 15:39:05 +08:00
|
|
|
|
|
|
|
|
|
|
# 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)
|
|
|
|
|
|
|
|
|
|
|
|
ok += 1
|
|
|
|
|
|
print(f" [{i:>4}/{len(png_files)}] {fpath.name}")
|
|
|
|
|
|
|
|
|
|
|
|
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}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Parameter display
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
def print_params_summary(params):
|
|
|
|
|
|
c = params["color"]
|
|
|
|
|
|
d = params["depth"]
|
|
|
|
|
|
ci = c["intrinsic"]
|
|
|
|
|
|
di = d["intrinsic"]
|
|
|
|
|
|
ext = params["extrinsic"]
|
|
|
|
|
|
R = np.array(ext["rot"]).reshape(3, 3)
|
|
|
|
|
|
t = np.array(ext["transform"])
|
|
|
|
|
|
|
|
|
|
|
|
print("\n--- Camera Parameters Summary ---")
|
|
|
|
|
|
print(f" RGB : {c['width']}x{c['height']} @ {c['fps']}fps {c['format']}")
|
|
|
|
|
|
print(f" fx={ci['fx']:.4f} fy={ci['fy']:.4f} cx={ci['cx']:.4f} cy={ci['cy']:.4f}")
|
|
|
|
|
|
print(f" Depth : {d['width']}x{d['height']} @ {d['fps']}fps {d['format']}")
|
|
|
|
|
|
print(f" fx={di['fx']:.4f} fy={di['fy']:.4f} cx={di['cx']:.4f} cy={di['cy']:.4f}")
|
|
|
|
|
|
print(f" scale={d['scale']:.6f} mm/unit")
|
|
|
|
|
|
print(f" Extrinsic R :\n {R[0]}\n {R[1]}\n {R[2]}")
|
|
|
|
|
|
print(f" Extrinsic t : {t} (mm)")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Entry point
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
|
|
only_params = "--params" in sys.argv
|
|
|
|
|
|
only_convert = "--convert" in sys.argv
|
2026-04-21 20:26:21 +08:00
|
|
|
|
no_splat = "--no-splat" in sys.argv
|
2026-04-21 15:39:05 +08:00
|
|
|
|
|
|
|
|
|
|
params = None
|
|
|
|
|
|
|
|
|
|
|
|
# ---- Load or fetch camera parameters ----
|
|
|
|
|
|
if not only_convert:
|
|
|
|
|
|
if os.path.exists(PARAMS_FILE) and not only_params:
|
|
|
|
|
|
print(f"Found saved camera params: {PARAMS_FILE}")
|
|
|
|
|
|
ans = input("Use existing params? [y=use existing / n=re-fetch from camera]: ").strip().lower()
|
|
|
|
|
|
if ans == "y":
|
|
|
|
|
|
with open(PARAMS_FILE) as f:
|
|
|
|
|
|
params = json.load(f)
|
|
|
|
|
|
print(" Loaded existing params.")
|
|
|
|
|
|
|
|
|
|
|
|
if params is None:
|
|
|
|
|
|
params = fetch_camera_params()
|
|
|
|
|
|
if params is None:
|
|
|
|
|
|
print("Failed to fetch camera parameters. Exiting.")
|
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
with open(PARAMS_FILE, "w") as f:
|
|
|
|
|
|
json.dump(params, f, indent=2)
|
|
|
|
|
|
print(f"\n Camera params saved to: {PARAMS_FILE}")
|
|
|
|
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
# --convert: must have existing params file
|
|
|
|
|
|
if not os.path.exists(PARAMS_FILE):
|
|
|
|
|
|
print(f"No {PARAMS_FILE} found. Run without --convert first to fetch camera params.")
|
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
with open(PARAMS_FILE) as f:
|
|
|
|
|
|
params = json.load(f)
|
|
|
|
|
|
print(f"Loaded camera params from: {PARAMS_FILE}")
|
|
|
|
|
|
|
|
|
|
|
|
print_params_summary(params)
|
|
|
|
|
|
|
|
|
|
|
|
if only_params:
|
|
|
|
|
|
print("\n--params mode: done.")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# ---- Batch conversion ----
|
|
|
|
|
|
depth_dir = input("\nEnter depth images directory: ").strip()
|
|
|
|
|
|
if not depth_dir:
|
|
|
|
|
|
print("No directory entered. Exiting.")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
2026-04-21 20:26:21 +08:00
|
|
|
|
batch_convert(params, depth_dir, splat=not no_splat)
|
2026-04-21 15:39:05 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
main()
|