Initial commit: Speckle-Scanner 3D pipeline with setup README
This commit is contained in:
@@ -0,0 +1,626 @@
|
||||
import os
|
||||
import argparse
|
||||
import numpy as np
|
||||
import cv2
|
||||
import subprocess
|
||||
import glob
|
||||
import re
|
||||
import open3d as o3d
|
||||
import matplotlib.pyplot as plt
|
||||
import time
|
||||
from scipy.ndimage import median_filter
|
||||
from numba import njit, prange
|
||||
|
||||
# Save the point cloud in PLY format.
|
||||
def save_ply(filename, points):
|
||||
with open(filename, 'w') as f:
|
||||
f.write("ply\n")
|
||||
f.write("format ascii 1.0\n")
|
||||
f.write(f"element vertex {points.shape[0]}\n")
|
||||
f.write("property float x\n")
|
||||
f.write("property float y\n")
|
||||
f.write("property float z\n")
|
||||
f.write("property uchar red\n")
|
||||
f.write("property uchar green\n")
|
||||
f.write("property uchar blue\n")
|
||||
f.write("end_header\n")
|
||||
|
||||
for point in points:
|
||||
x, y, z, r, g, b = point
|
||||
f.write(f"{x} {y} {z} {int(r * 255)} {int(g * 255)} {int(b * 255)}\n")
|
||||
|
||||
def save_ascii_point_cloud(filename, points):
|
||||
with open(filename, 'w') as f:
|
||||
for point in points:
|
||||
x, y, z, r, g, b = point
|
||||
f.write(f"{x} {y} {z} {int(r * 255)} {int(g * 255)} {int(b * 255)}\n")
|
||||
|
||||
def process_disparity(disp_left, disp_right, tolerance=1.0, filter_size=10):
|
||||
disp_right_aligned = -disp_right # Align disparities
|
||||
height, width = disp_left.shape
|
||||
invalid_value = np.min(disp_left)
|
||||
valid_disp = np.full_like(disp_left, invalid_value)
|
||||
|
||||
for y in range(height):
|
||||
for x in range(width):
|
||||
d_left = disp_left[y, x]
|
||||
if d_left == invalid_value:
|
||||
continue
|
||||
x_right = int(x - d_left)
|
||||
if x_right < 0 or x_right >= width:
|
||||
continue
|
||||
d_right = disp_right_aligned[y, x_right]
|
||||
if abs(d_left - d_right) <= tolerance:
|
||||
valid_disp[y, x] = d_left
|
||||
|
||||
return median_filter(valid_disp, size=filter_size)
|
||||
|
||||
def save_with_colorbar(matrix, output_path, cmap="jet"):
|
||||
plt.figure(figsize=(10, 6))
|
||||
plt.imshow(matrix, cmap=cmap, aspect="auto")
|
||||
plt.colorbar()
|
||||
plt.title("Matrix Visualization with Colorbar")
|
||||
plt.savefig(output_path, bbox_inches="tight")
|
||||
plt.close()
|
||||
|
||||
def load_q_matrix(q_file):
|
||||
fs = cv2.FileStorage(q_file, cv2.FILE_STORAGE_READ)
|
||||
Q = fs.getNode("Q").mat()
|
||||
fs.release()
|
||||
if Q is None:
|
||||
raise ValueError("Failed to load Q matrix from the provided file.")
|
||||
return Q
|
||||
|
||||
def extract_ts_token(filename, prefix):
|
||||
"""Extract ts token from e.g. lc_ts1634840093_ck....png -> ('ts1634840093', 1634840093)."""
|
||||
name = os.path.basename(filename)
|
||||
m = re.search(rf"^{re.escape(prefix)}(ts\d+)", name, re.IGNORECASE)
|
||||
if not m:
|
||||
return None, None
|
||||
ts_token = m.group(1).lower()
|
||||
ts_int = int(re.search(r"\d+", ts_token).group())
|
||||
return ts_token, ts_int
|
||||
|
||||
|
||||
def discover_stereo_pairs(left_dir, right_dir, left_prefix="", right_prefix="", extensions=(".png", ".bmp")):
|
||||
"""
|
||||
Match lc/rc files by shared ts token (ck* suffix ignored).
|
||||
Returns list of (left_path, right_path, ts_int) sorted by timestamp ascending.
|
||||
"""
|
||||
best_pairs = []
|
||||
best_ext = None
|
||||
|
||||
for ext in extensions:
|
||||
pairs_by_ts = {}
|
||||
for lc_path in glob.glob(os.path.join(left_dir, f"{left_prefix}*{ext}")):
|
||||
ts_token, ts_int = extract_ts_token(lc_path, left_prefix)
|
||||
if ts_token is None:
|
||||
continue
|
||||
|
||||
rc_pattern = os.path.join(right_dir, f"{right_prefix}{ts_token}_*{ext}")
|
||||
rc_matches = sorted(glob.glob(rc_pattern))
|
||||
if not rc_matches:
|
||||
rc_pattern = os.path.join(right_dir, f"{right_prefix}{ts_token}*{ext}")
|
||||
rc_matches = sorted(glob.glob(rc_pattern))
|
||||
if not rc_matches:
|
||||
continue
|
||||
|
||||
pairs_by_ts[ts_int] = (lc_path, rc_matches[0], ts_int)
|
||||
|
||||
pairs = [pairs_by_ts[k] for k in sorted(pairs_by_ts)]
|
||||
if len(pairs) > len(best_pairs):
|
||||
best_pairs = pairs
|
||||
best_ext = ext.lstrip(".")
|
||||
|
||||
return best_pairs, best_ext
|
||||
|
||||
|
||||
def load_stereo_stacks(left_dir, right_dir, num_images=None, left_prefix="", right_prefix=""):
|
||||
"""
|
||||
Load stereo image stacks matched by ts token.
|
||||
|
||||
num_images:
|
||||
None or <= 0 -> use all matched pairs in the folder
|
||||
N < available -> use the last N pairs (highest timestamps)
|
||||
N >= available -> use all available pairs
|
||||
"""
|
||||
pairs, ext = discover_stereo_pairs(left_dir, right_dir, left_prefix, right_prefix)
|
||||
if not pairs:
|
||||
raise ValueError(
|
||||
"No matching stereo image pairs (.png or .bmp) found. "
|
||||
f"Expected {left_prefix}ts<timestamp>_*.{{png,bmp}} paired with "
|
||||
f"{right_prefix}ts<same_timestamp>_*.{{png,bmp}}."
|
||||
)
|
||||
|
||||
total = len(pairs)
|
||||
if num_images is None or num_images <= 0:
|
||||
selected = pairs
|
||||
elif num_images >= total:
|
||||
if num_images > total:
|
||||
print(f"Warning: Only {total} pair(s) available (requested {num_images}). Using all.")
|
||||
selected = pairs
|
||||
else:
|
||||
selected = pairs[-num_images:]
|
||||
|
||||
ts_first = selected[0][2]
|
||||
ts_last = selected[-1][2]
|
||||
print(
|
||||
f"Using {len(selected)}/{total} {ext.upper()} pair(s) "
|
||||
f"(timestamps {ts_first} … {ts_last}; matched on ts, ck suffix ignored)."
|
||||
)
|
||||
|
||||
left_images = []
|
||||
right_images = []
|
||||
for lc_path, rc_path, _ in selected:
|
||||
left_img = cv2.imread(lc_path, cv2.IMREAD_GRAYSCALE)
|
||||
right_img = cv2.imread(rc_path, cv2.IMREAD_GRAYSCALE)
|
||||
if left_img is None or right_img is None:
|
||||
raise ValueError(f"Failed to load pair: {lc_path} / {rc_path}")
|
||||
if left_img.shape != right_img.shape:
|
||||
raise ValueError(
|
||||
f"Shape mismatch for ts pair: {os.path.basename(lc_path)} "
|
||||
f"{left_img.shape} vs {os.path.basename(rc_path)} {right_img.shape}"
|
||||
)
|
||||
left_images.append(left_img)
|
||||
right_images.append(right_img)
|
||||
|
||||
return np.stack(left_images), np.stack(right_images)
|
||||
|
||||
def is_gpu_available():
|
||||
try:
|
||||
output = subprocess.check_output("nvidia-smi", stderr=subprocess.STDOUT, shell=True)
|
||||
return True
|
||||
except subprocess.CalledProcessError:
|
||||
return False
|
||||
|
||||
if is_gpu_available():
|
||||
print("GPU is available!")
|
||||
import cupy as cp
|
||||
############# GPU Code Starts #############
|
||||
|
||||
cuda_kernel = cp.RawKernel(r'''
|
||||
extern "C" __global__
|
||||
void disparity_kernel(
|
||||
const float* left_stack, const float* right_stack, float* disp_matrix, int* vert_disp_matrix, float* corr_matrix,
|
||||
int num_pairs, int height, int width, int window_size, int negative_range, int positive_range,
|
||||
int v_neg_range, int v_pos_range, float zncc_threshold, bool interpolation, int interpolation_method
|
||||
) {
|
||||
int row = blockIdx.y * blockDim.y + threadIdx.y;
|
||||
int col1 = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
|
||||
int half_window = window_size / 2;
|
||||
int patch_size = window_size * window_size;
|
||||
|
||||
// Check if the current pixel is within valid bounds
|
||||
if (row < half_window || row >= height - half_window || col1 < half_window || col1 >= width - half_window) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Local array to store correlation values for each disparity
|
||||
const int max_disparity = positive_range - negative_range + 1; // Total disparities: 54 - (-160) + 1 = 215
|
||||
float corrs[4000]; // Adjust size to match max_disparity
|
||||
int shifts[4000]; // Store vertical shifts for each disparity
|
||||
|
||||
// Initialize correlation values to -1
|
||||
for (int i = 0; i < max_disparity; i++) {
|
||||
corrs[i] = -1.0f;
|
||||
shifts[i] = 0;
|
||||
}
|
||||
|
||||
// Iterate over possible horizontal disparities
|
||||
for (int col2 = max(half_window, col1 - positive_range); col2 <= min(width - half_window - 1, col1 - negative_range); col2++) {
|
||||
int disparity = col1 - col2;
|
||||
|
||||
// Check if disparity is within the valid range [-160, 54]
|
||||
if (disparity < negative_range || disparity > positive_range) {
|
||||
continue; // Skip invalid disparities
|
||||
}
|
||||
|
||||
// Map disparity to index in corrs array
|
||||
int disparity_idx = disparity - negative_range;
|
||||
|
||||
// Iterate over possible vertical disparities
|
||||
for (int v_shift = v_neg_range; v_shift <= v_pos_range; v_shift++) {
|
||||
int row_shifted = row + v_shift;
|
||||
if (row_shifted < half_window || row_shifted >= height - half_window) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Compute mean of the patches in the left and right images
|
||||
float mean1 = 0.0f, mean2 = 0.0f;
|
||||
for (int pair = 0; pair < num_pairs; pair++) {
|
||||
for (int i = 0; i < patch_size; i++) {
|
||||
int patch_row = i / window_size;
|
||||
int patch_col = i % window_size;
|
||||
mean1 += left_stack[(pair * height + row - half_window + patch_row) * width + (col1 - half_window + patch_col)];
|
||||
mean2 += right_stack[(pair * height + row_shifted - half_window + patch_row) * width + (col2 - half_window + patch_col)];
|
||||
}
|
||||
}
|
||||
mean1 /= (num_pairs * patch_size);
|
||||
mean2 /= (num_pairs * patch_size);
|
||||
|
||||
// Compute standard deviation of the patches
|
||||
float std1 = 0.0f, std2 = 0.0f;
|
||||
for (int pair = 0; pair < num_pairs; pair++) {
|
||||
for (int i = 0; i < patch_size; i++) {
|
||||
int patch_row = i / window_size;
|
||||
int patch_col = i % window_size;
|
||||
float val1 = left_stack[(pair * height + row - half_window + patch_row) * width + (col1 - half_window + patch_col)] - mean1;
|
||||
float val2 = right_stack[(pair * height + row_shifted - half_window + patch_row) * width + (col2 - half_window + patch_col)] - mean2;
|
||||
std1 += val1 * val1;
|
||||
std2 += val2 * val2;
|
||||
}
|
||||
}
|
||||
std1 = sqrt(std1 / (num_pairs * patch_size));
|
||||
std2 = sqrt(std2 / (num_pairs * patch_size));
|
||||
|
||||
if (std1 == 0 || std2 == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Compute ZNCC (Zero-Normalized Cross-Correlation)
|
||||
float sum = 0.0f;
|
||||
for (int pair = 0; pair < num_pairs; pair++) {
|
||||
for (int i = 0; i < patch_size; i++) {
|
||||
int patch_row = i / window_size;
|
||||
int patch_col = i % window_size;
|
||||
float val1 = left_stack[(pair * height + row - half_window + patch_row) * width + (col1 - half_window + patch_col)] - mean1;
|
||||
float val2 = right_stack[(pair * height + row_shifted - half_window + patch_row) * width + (col2 - half_window + patch_col)] - mean2;
|
||||
sum += val1 * val2;
|
||||
}
|
||||
}
|
||||
float rho = sum / (std1 * std2 * num_pairs * patch_size);
|
||||
|
||||
// Store the maximum correlation for this disparity
|
||||
if (rho > corrs[disparity_idx]) {
|
||||
corrs[disparity_idx] = rho;
|
||||
shifts[disparity_idx] = v_shift;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find the disparity with the maximum correlation
|
||||
int max_corr_idx = 0;
|
||||
for (int i = 1; i < max_disparity; i++) {
|
||||
if (corrs[i] > corrs[max_corr_idx]) {
|
||||
max_corr_idx = i;
|
||||
}
|
||||
}
|
||||
|
||||
// Perform interpolation if enabled
|
||||
if (corrs[max_corr_idx] > zncc_threshold) {
|
||||
float d_subpixel = (float)(max_corr_idx + negative_range);
|
||||
|
||||
if (interpolation && max_corr_idx > 0 && max_corr_idx < max_disparity - 1) {
|
||||
float corr_left = corrs[max_corr_idx - 1];
|
||||
float corr_best = corrs[max_corr_idx];
|
||||
float corr_right = corrs[max_corr_idx + 1];
|
||||
|
||||
if (interpolation_method == 1) {
|
||||
// Parabolic interpolation
|
||||
float denom = (corr_left - 2 * corr_best + corr_right);
|
||||
if (denom != 0.0f) {
|
||||
d_subpixel += (corr_left - corr_right) / (2 * denom);
|
||||
}
|
||||
} else if (interpolation_method == 2) {
|
||||
// Gaussian interpolation
|
||||
float log_minus_1 = logf(max(corr_left, 1e-10f));
|
||||
float log_0 = logf(max(corr_best, 1e-10f));
|
||||
float log_plus_1 = logf(max(corr_right, 1e-10f));
|
||||
|
||||
float a = (log_plus_1 - 2 * log_0 + log_minus_1) / 2.0f;
|
||||
float b = (log_plus_1 - log_minus_1) / 2.0f;
|
||||
|
||||
if (a != 0.0f) {
|
||||
d_subpixel += -b / (2 * a);
|
||||
}
|
||||
} else if (interpolation_method == 3) {
|
||||
// Angular interpolation
|
||||
float angle_minus_1 = atan2f(corr_left, 1.0f);
|
||||
float angle_0 = atan2f(corr_best, 1.0f);
|
||||
float angle_plus_1 = atan2f(corr_right, 1.0f);
|
||||
|
||||
float mean_angle = (angle_plus_1 - angle_minus_1) / 2.0f;
|
||||
d_subpixel -= tanf(mean_angle);
|
||||
}
|
||||
}
|
||||
|
||||
// Store the results
|
||||
int output_row = row - half_window;
|
||||
int output_col = col1 - half_window;
|
||||
disp_matrix[output_row * (width - window_size) + output_col] = d_subpixel;
|
||||
vert_disp_matrix[output_row * (width - window_size) + output_col] = shifts[max_corr_idx];
|
||||
corr_matrix[output_row * (width - window_size) + output_col] = corrs[max_corr_idx];
|
||||
}
|
||||
}
|
||||
''', 'disparity_kernel')
|
||||
|
||||
def compute_disparity_map_with_subpixel_GPU(
|
||||
left_stack, right_stack, window_size, negative_range, positive_range, v_neg_range, v_pos_range, zncc_threshold,
|
||||
interpolation, method
|
||||
):
|
||||
# Map method strings to numeric values
|
||||
methods = {
|
||||
"parabolic": 1,
|
||||
"gaussian": 2,
|
||||
"equiangular": 3
|
||||
}
|
||||
|
||||
# Validate method
|
||||
if method not in methods:
|
||||
raise ValueError(f"Invalid method: {method}. Must be one of {list(methods.keys())}")
|
||||
|
||||
# Get the numeric value for the method
|
||||
method_code = methods[method]
|
||||
|
||||
num_pairs, height, width = left_stack.shape
|
||||
disp_matrix = cp.full((height - window_size, width - window_size), negative_range, dtype=cp.float32)
|
||||
vert_disp_matrix = cp.zeros((height - window_size, width - window_size), dtype=cp.int32)
|
||||
corr_matrix = cp.zeros((height - window_size, width - window_size), dtype=cp.float32)
|
||||
|
||||
# Transfer data to GPU
|
||||
left_stack_gpu = cp.asarray(left_stack, dtype=cp.float32)
|
||||
right_stack_gpu = cp.asarray(right_stack, dtype=cp.float32)
|
||||
|
||||
# Define block and grid sizes
|
||||
block_size = (16, 16)
|
||||
grid_size = (
|
||||
(width - window_size + block_size[0] - 1) // block_size[0],
|
||||
(height - window_size + block_size[1] - 1) // block_size[1]
|
||||
)
|
||||
|
||||
# Launch the CUDA kernel
|
||||
cuda_kernel(
|
||||
grid_size, block_size,
|
||||
(left_stack_gpu, right_stack_gpu, disp_matrix, vert_disp_matrix, corr_matrix,
|
||||
num_pairs, height, width, window_size, negative_range, positive_range,
|
||||
v_neg_range, v_pos_range, zncc_threshold, interpolation, method_code)
|
||||
)
|
||||
|
||||
disp_matrix_np = disp_matrix.get()
|
||||
vert_disp_matrix_np = vert_disp_matrix.get()
|
||||
corr_matrix_np = corr_matrix.get()
|
||||
|
||||
return disp_matrix_np, vert_disp_matrix_np, corr_matrix_np
|
||||
|
||||
############# GPU Code Ends #############
|
||||
|
||||
|
||||
|
||||
############# CPU Code Starts #############
|
||||
|
||||
# Subpixel interpolation methods
|
||||
@njit
|
||||
def parabolic_interpolation(corrs, idx):
|
||||
zncc_d_minus_1 = corrs[idx - 1]
|
||||
zncc_d = corrs[idx]
|
||||
zncc_d_plus_1 = corrs[idx + 1]
|
||||
|
||||
a = (zncc_d_plus_1 - 2 * zncc_d + zncc_d_minus_1) / 2.0
|
||||
b = (zncc_d_plus_1 - zncc_d_minus_1) / 2.0
|
||||
|
||||
if a != 0.0:
|
||||
return -b / (2 * a)
|
||||
return 0.0
|
||||
|
||||
@njit
|
||||
def gaussian_interpolation(corrs, idx):
|
||||
zncc_d_minus_1 = corrs[idx - 1]
|
||||
zncc_d = corrs[idx]
|
||||
zncc_d_plus_1 = corrs[idx + 1]
|
||||
|
||||
log_minus_1 = np.log(max(zncc_d_minus_1, 1e-10))
|
||||
log_0 = np.log(max(zncc_d, 1e-10))
|
||||
log_plus_1 = np.log(max(zncc_d_plus_1, 1e-10))
|
||||
|
||||
a = (log_plus_1 - 2 * log_0 + log_minus_1) / 2.0
|
||||
b = (log_plus_1 - log_minus_1) / 2.0
|
||||
|
||||
if a != 0.0:
|
||||
return -b / (2 * a)
|
||||
return 0.0
|
||||
|
||||
@njit
|
||||
def equiangular_interpolation(corrs, idx):
|
||||
zncc_d_minus_1 = corrs[idx - 1]
|
||||
zncc_d = corrs[idx]
|
||||
zncc_d_plus_1 = corrs[idx + 1]
|
||||
|
||||
# Convert to angles (avoid division by zero)
|
||||
angle_minus_1 = np.arctan2(zncc_d_minus_1, 1.0)
|
||||
angle_0 = np.arctan2(zncc_d, 1.0)
|
||||
angle_plus_1 = np.arctan2(zncc_d_plus_1, 1.0)
|
||||
|
||||
mean_angle = (angle_plus_1 - angle_minus_1) / 2.0
|
||||
|
||||
return np.tan(mean_angle)
|
||||
@njit
|
||||
def subpixel_interpolation(corrs, max_corr_idx, method):
|
||||
if max_corr_idx <= 0 or max_corr_idx >= len(corrs) - 1:
|
||||
return 0.0 # Boundary condition: cannot interpolate
|
||||
|
||||
if method == "parabolic":
|
||||
return parabolic_interpolation(corrs, max_corr_idx)
|
||||
elif method == "gaussian":
|
||||
return gaussian_interpolation(corrs, max_corr_idx)
|
||||
elif method == "equiangular":
|
||||
return equiangular_interpolation(corrs, max_corr_idx)
|
||||
else:
|
||||
raise ValueError(f"Unknown interpolation method: {method}")
|
||||
|
||||
@njit
|
||||
def zncc_patch(patch1, patch2):
|
||||
mean1 = np.mean(patch1)
|
||||
mean2 = np.mean(patch2)
|
||||
std1 = np.std(patch1)
|
||||
std2 = np.std(patch2)
|
||||
|
||||
if std1 == 0 or std2 == 0:
|
||||
return -1.0
|
||||
|
||||
return np.sum((patch1 - mean1) * (patch2 - mean2)) / (std1 * std2 * patch1.size)
|
||||
|
||||
@njit(parallel=True)
|
||||
def compute_disparity_map_with_subpixel_CPU(
|
||||
left_stack, right_stack, window_size, negative_range, positive_range, v_neg_range, v_pos_range, zncc_threshold, interpolation, method
|
||||
):
|
||||
num_pairs, height, width = left_stack.shape
|
||||
disp_matrix = np.full((height - window_size, width - window_size), negative_range, dtype=np.float32)
|
||||
vert_disp_matrix = np.zeros_like(disp_matrix, dtype=np.int32)
|
||||
corr_matrix = np.zeros_like(disp_matrix, dtype=np.float32)
|
||||
|
||||
half_window = window_size // 2
|
||||
|
||||
for row in prange(half_window, height - half_window):
|
||||
for col1 in range(half_window, width - half_window):
|
||||
win1_stack = left_stack[:, row - half_window : row + half_window + 1, col1 - half_window : col1 + half_window + 1]
|
||||
corrs = np.full((positive_range - negative_range), -1.0, dtype=np.float32)
|
||||
shifts = np.full((positive_range - negative_range, 2), 0, dtype=np.int32)
|
||||
|
||||
for idx, col2 in enumerate(range(max(half_window, col1 - positive_range), min(width - half_window, col1 - negative_range))):
|
||||
max_corr = -1.0
|
||||
best_disp_v = 0
|
||||
for v_shift in range(v_neg_range, v_pos_range + 1):
|
||||
row_shifted = row + v_shift
|
||||
if not (half_window <= row_shifted < height - half_window):
|
||||
continue
|
||||
|
||||
win2_stack = right_stack[:, row_shifted - half_window : row_shifted + half_window + 1,
|
||||
col2 - half_window : col2 + half_window + 1]
|
||||
|
||||
rho = zncc_patch(win1_stack.ravel(), win2_stack.ravel())
|
||||
|
||||
if rho > max_corr:
|
||||
max_corr = rho
|
||||
best_disp_v = v_shift
|
||||
corrs[idx] = max_corr
|
||||
shifts[idx, :] = np.array([col1 - col2, best_disp_v])
|
||||
|
||||
max_corr_idx = np.argmax(corrs)
|
||||
max_corr_value = corrs[max_corr_idx]
|
||||
|
||||
if max_corr_value > zncc_threshold:
|
||||
disparity_h, disparity_v = shifts[max_corr_idx]
|
||||
|
||||
if interpolation:
|
||||
subpixel_offset = subpixel_interpolation(corrs, max_corr_idx, method)
|
||||
disparity_h = disparity_h - subpixel_offset
|
||||
disp_matrix[row - half_window, col1 - half_window] = disparity_h
|
||||
vert_disp_matrix[row - half_window, col1 - half_window] = disparity_v
|
||||
corr_matrix[row - half_window, col1 - half_window] = max_corr_value
|
||||
|
||||
return disp_matrix, vert_disp_matrix, corr_matrix
|
||||
|
||||
############# CPU Code Ends #############
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Stereo Disparity Map Generator")
|
||||
parser.add_argument("--window_size", type=int, default=5, help="Window size for block matching")
|
||||
parser.add_argument("--H_neg_range", type=int, default=0, help="Horizontal negative range for disparity")
|
||||
parser.add_argument("--H_pos_range", type=int, default=65, help="Horizontal positive range for disparity")
|
||||
parser.add_argument("--v_neg_range", type=int, default=-4, help="Vertical negative range")
|
||||
parser.add_argument("--v_pos_range", type=int, default=2, help="Vertical positive range")
|
||||
parser.add_argument("--zncc_threshold", type=float, default=0.4, help="ZNCC threshold")
|
||||
parser.add_argument(
|
||||
"--num_images",
|
||||
type=int,
|
||||
default=None,
|
||||
help="Number of stereo pairs to use. Default: all matched pairs in the folder. "
|
||||
"If fewer than available, the last N pairs (highest timestamps) are used.",
|
||||
)
|
||||
parser.add_argument("--noise_filter", type=str, default="open3d", choices=["median", "open3d", None], help="Post-processing noise filter method: 'median' or 'open3d'")
|
||||
parser.add_argument("--interpolation", type=bool, default=True, help="Use sub-pixel interpolation")
|
||||
parser.add_argument("--method", type=str, default="parabolic", help="Interpolation method (parabolic, gaussian, equiangular)")
|
||||
parser.add_argument("--q_file", type=str, default="./data1/Q.cvstore", help="Path to Q matrix file")
|
||||
parser.add_argument("--left_dir", type=str, default="./data1/left", help="Path to left images")
|
||||
parser.add_argument("--right_dir", type=str, default="./data1/right", help="Path to right images")
|
||||
parser.add_argument("--output_dir", type=str, default="./results", help="Output directory (fallback when disp/pcl dirs not set)")
|
||||
parser.add_argument("--noise_remove", type=bool, default=True, help="Enable noise removal using left-right consistency check")
|
||||
parser.add_argument("--left_prefix", type=str, default="", help="Filename prefix for left images (e.g. 'lc_') when both cameras share a folder")
|
||||
parser.add_argument("--right_prefix", type=str, default="", help="Filename prefix for right images (e.g. 'rc_') when both cameras share a folder")
|
||||
parser.add_argument("--disp_output_dir", type=str, default=None, help="Output dir for disparity/map files; falls back to --output_dir")
|
||||
parser.add_argument("--pcl_output_dir", type=str, default=None, help="Output dir for point cloud files; falls back to --output_dir")
|
||||
parser.add_argument("--troubleshooting", action="store_true", help="Save all outputs (vertical shift, correlation maps, point cloud); by default only disparity map is saved")
|
||||
args = parser.parse_args()
|
||||
|
||||
start_time = time.time()
|
||||
disp_out = args.disp_output_dir or args.output_dir
|
||||
pcl_out = args.pcl_output_dir or args.output_dir
|
||||
os.makedirs(disp_out, exist_ok=True)
|
||||
os.makedirs(pcl_out, exist_ok=True)
|
||||
|
||||
left_stack, right_stack = load_stereo_stacks(
|
||||
args.left_dir, args.right_dir, args.num_images,
|
||||
args.left_prefix, args.right_prefix
|
||||
)
|
||||
left_shap = left_stack.shape
|
||||
print(f"{left_shap[0]} image pairs loaded and their shapes: {left_shap}, {right_stack.shape}")
|
||||
Q = load_q_matrix(args.q_file)
|
||||
print("Q matrix loaded successfully.")
|
||||
if is_gpu_available():
|
||||
print("Running on GPU...")
|
||||
disp_matrix_LR, vert_disp_matrix, corr_matrix = compute_disparity_map_with_subpixel_GPU(
|
||||
left_stack, right_stack, args.window_size, args.H_neg_range, args.H_pos_range, args.v_neg_range, args.v_pos_range, np.float32(args.zncc_threshold), args.interpolation, args.method
|
||||
)
|
||||
if args.noise_remove:
|
||||
disp_matrix_RL, _, _ = compute_disparity_map_with_subpixel_GPU(
|
||||
right_stack, left_stack, args.window_size, -args.H_pos_range, -args.H_neg_range, -args.v_pos_range, -args.v_neg_range, np.float32(args.zncc_threshold), args.interpolation, args.method
|
||||
)
|
||||
disp_matrix = process_disparity(disp_matrix_LR, disp_matrix_RL)
|
||||
else:
|
||||
disp_matrix = disp_matrix_LR
|
||||
|
||||
else:
|
||||
print("Running on CPU...")
|
||||
disp_matrix_LR, vert_disp_matrix, corr_matrix = compute_disparity_map_with_subpixel_CPU(
|
||||
left_stack, right_stack, args.window_size, args.H_neg_range, args.H_pos_range, args.v_neg_range, args.v_pos_range, args.zncc_threshold, args.interpolation, args.method
|
||||
)
|
||||
if args.noise_remove:
|
||||
disp_matrix_RL, _, _ = compute_disparity_map_with_subpixel_CPU(
|
||||
right_stack, left_stack, args.window_size, -args.H_pos_range, -args.H_neg_range, -args.v_pos_range, -args.v_neg_range, args.zncc_threshold, args.interpolation, args.method
|
||||
)
|
||||
disp_matrix = process_disparity(disp_matrix_LR, disp_matrix_RL)
|
||||
else:
|
||||
disp_matrix = disp_matrix_LR
|
||||
|
||||
# Always save disparity map
|
||||
np.save(f"{disp_out}/disparity.npy", disp_matrix)
|
||||
save_with_colorbar(disp_matrix, f"{disp_out}/Disparity_map_colorbar.png")
|
||||
print(f"Disparity map saved to {disp_out}")
|
||||
|
||||
if args.troubleshooting:
|
||||
np.save(f"{disp_out}/vertical_shift_map.npy", vert_disp_matrix)
|
||||
np.save(f"{disp_out}/correlation_map.npy", corr_matrix)
|
||||
save_with_colorbar(vert_disp_matrix, f"{disp_out}/vertical_shift_map_colorbar.png")
|
||||
save_with_colorbar(corr_matrix, f"{disp_out}/correlation_map_colorbar.png")
|
||||
|
||||
if args.noise_filter == "median":
|
||||
disp_matrix = median_filter(disp_matrix, size=15)
|
||||
|
||||
disp_map_loaded = disp_matrix.astype(np.float32)
|
||||
point_cloud = cv2.reprojectImageTo3D(disp_map_loaded, Q)
|
||||
mask_map = disp_map_loaded > disp_map_loaded.min()
|
||||
point_cloud = point_cloud[mask_map]
|
||||
|
||||
depth_values = point_cloud[:, 2]
|
||||
depth_normalized = (depth_values - depth_values.min()) / (depth_values.max() - depth_values.min())
|
||||
colormap = plt.get_cmap('jet')
|
||||
colors = colormap(depth_normalized)[:, :3]
|
||||
|
||||
if args.noise_filter == "open3d":
|
||||
pcd = o3d.geometry.PointCloud()
|
||||
pcd.points = o3d.utility.Vector3dVector(point_cloud)
|
||||
pcd.colors = o3d.utility.Vector3dVector(colors)
|
||||
pcd_clean, ind = pcd.remove_statistical_outlier(nb_neighbors=50, std_ratio=0.004)
|
||||
point_cloud = np.asarray(pcd_clean.points)
|
||||
colors = np.asarray(pcd_clean.colors)
|
||||
|
||||
point_cloud_with_colors = np.hstack((point_cloud, colors))
|
||||
output_file_ply = f"{pcl_out}/Point_cloud.ply"
|
||||
output_file_txt = f"{pcl_out}/Point_cloud.txt"
|
||||
save_ply(output_file_ply, point_cloud_with_colors)
|
||||
save_ascii_point_cloud(output_file_txt, point_cloud_with_colors)
|
||||
print(f"Vertical shift, correlation maps and point cloud saved (troubleshooting mode)")
|
||||
print(f"Point cloud saved to {pcl_out}")
|
||||
|
||||
print(f"Execution time: {time.time() - start_time:.2f} seconds")
|
||||
Reference in New Issue
Block a user