Point cloud and camera estimation works well

This commit is contained in:
Atakan Topaloglu
2025-03-20 20:08:51 +03:00
parent 61020e2751
commit 4b14330514
6 changed files with 723 additions and 79 deletions
+1
View File
@@ -11,6 +11,7 @@ reconstructions/
# C extensions
*.so
output_colmap*
# Distribution / packaging
.Python
build/
+277
View File
@@ -0,0 +1,277 @@
import numpy as np
import os
import torch
from scipy.spatial.transform import Rotation as R
import glob
import argparse
from PIL import Image
def rotation_matrix_to_quaternion(rot_matrix):
"""Convert rotation matrix to quaternion (w, x, y, z)"""
rotation = R.from_matrix(rot_matrix)
return rotation.as_quat()[[3, 0, 1, 2]] # Reorder from (x, y, z, w) to (w, x, y, z)
def write_cameras_txt(intrinsics, image_shapes, output_dir):
"""Write cameras.txt file"""
os.makedirs(output_dir, exist_ok=True)
with open(os.path.join(output_dir, "cameras.txt"), "w") as f:
f.write("# Camera list with one line of data per camera:\n")
f.write("# CAMERA_ID, MODEL, WIDTH, HEIGHT, PARAMS[]\n")
f.write(f"# Number of cameras: {len(intrinsics)}\n")
for i, (intrin, shape) in enumerate(zip(intrinsics, image_shapes)):
# Extract image dimensions
height, width = shape
# Extract focal length and principal point
fx, fy = intrin[0, 0], intrin[1, 1]
cx, cy = intrin[0, 2], intrin[1, 2]
# Using PINHOLE model (fx, fy, cx, cy)
camera_id = i + 1
f.write(f"{camera_id} PINHOLE {width} {height} {fx} {fy} {cx} {cy}\n")
def extract_sparse_point_cloud(point_maps, images, sample_step=20):
"""Extract a sparse point cloud from point maps with colors from images"""
points = []
point_colors = [] # Will store actual colors from images
point_origins = [] # Track which image/pixel each point came from
for img_idx, (point_map, img) in enumerate(zip(point_maps, images)):
h, w, _ = point_map.shape
for y in range(0, h, sample_step):
for x in range(0, w, sample_step):
# Get 3D point coordinates
point3d = point_map[y, x]
# Skip invalid points
if np.linalg.norm(point3d) < 0.001 or np.linalg.norm(point3d) > 1000:
continue
# Extract the color from the image
color = img[y, x]
# Add point, color, and origin info
points.append(point3d)
point_colors.append(color)
point_origins.append((img_idx, y, x))
return np.array(points), np.array(point_colors), point_origins
def filter_points_by_distance(points, colors, origins, min_distance=0.05):
"""Filter points that are too close to each other"""
# if len(points) == 0:
return points, colors, origins
def compute_point_visibility(points3d, extrinsics, intrinsics, image_shapes):
"""Compute which images can see each 3D point"""
num_points = len(points3d)
num_images = len(extrinsics)
# For each image, store visible 2D projections
image_points = [[] for _ in range(num_images)]
# For each 3D point, store which images see it
point_tracks = [[] for _ in range(num_points)]
for point_idx, point3d in enumerate(points3d):
for img_idx, (extrinsic, intrinsic, shape) in enumerate(zip(extrinsics, intrinsics, image_shapes)):
# Get image dimensions
height, width = shape
# Transform point to camera coordinates
point_cam = np.dot(extrinsic[:3, :3], point3d) + extrinsic[:3, 3]
# Check if point is in front of camera
if point_cam[2] <= 0:
continue
# Project to image plane
point_homo = np.dot(intrinsic, point_cam / point_cam[2])
x, y = point_homo[0], point_homo[1]
# Check if within image boundaries
if 0 <= x < width and 0 <= y < height:
image_points[img_idx].append((x, y, point_idx))
point_tracks[point_idx].append((img_idx + 1, len(image_points[img_idx]) - 1))
return image_points, point_tracks
def write_images_txt(extrinsics, image_points, image_names, output_dir):
"""Write images.txt file"""
with open(os.path.join(output_dir, "images.txt"), "w") as f:
f.write("# Image list with two lines of data per image:\n")
f.write("# IMAGE_ID, QW, QX, QY, QZ, TX, TY, TZ, CAMERA_ID, NAME\n")
f.write("# POINTS2D[] as (X, Y, POINT3D_ID)\n")
f.write(f"# Number of images: {len(extrinsics)}\n")
for img_idx, (extrinsic, points) in enumerate(zip(extrinsics, image_points)):
# Extract rotation matrix and translation vector
rot_matrix = extrinsic[:3, :3]
trans_vector = extrinsic[:3, 3]
# Convert to camera-to-world transformation
rot_c2w = rot_matrix.T # Transpose for inverse rotation
trans_c2w = -np.dot(rot_c2w, trans_vector) # -R^T * t
quat_c2w = rotation_matrix_to_quaternion(rot_c2w)
image_id = img_idx + 1
camera_id = img_idx + 1 # Assuming one camera per image
image_name = os.path.basename(image_names[img_idx])
# Write image pose
f.write(f"{image_id} {quat_c2w[0]:.9f} {quat_c2w[1]:.9f} {quat_c2w[2]:.9f} {quat_c2w[3]:.9f} {trans_c2w[0]:.9f} {trans_c2w[1]:.9f} {trans_c2w[2]:.9f} {camera_id} {image_name}\n")
# Write 2D point observations
points2d_line = " ".join([f"{x:.6f} {y:.6f} {point3d_id}" for x, y, point3d_id in points])
f.write(f"{points2d_line}\n")
def write_points3d_txt(points3d, point_colors, point_tracks, output_dir):
"""Write points3D.txt file"""
with open(os.path.join(output_dir, "points3D.txt"), "w") as f:
f.write("# 3D point list with one line of data per point:\n")
f.write("# POINT3D_ID, X, Y, Z, R, G, B, ERROR, TRACK[] as (IMAGE_ID, POINT2D_IDX)\n")
f.write(f"# Number of points: {len(points3d)}\n")
valid_points = 0
for point_idx, (point3d, color, track) in enumerate(zip(points3d, point_colors, point_tracks)):
# Skip points that aren't visible in any image
if not track:
continue
# Convert color to integers
r, g, b = [int(c * 255) for c in color]
error = 1.0 # Placeholder for reprojection error
# Format track information
track_str = " ".join([f"{img_id} {point2d_idx}" for img_id, point2d_idx in track])
# Write point information
f.write(f"{point_idx} {point3d[0]:.9f} {point3d[1]:.9f} {point3d[2]:.9f} {r} {g} {b} {error:.9f} {track_str}\n")
valid_points += 1
print(f"Written {valid_points} valid points (visible in at least one image)")
def convert_to_colmap_format(extrinsics, intrinsics, point_maps, image_paths, output_dir, image_shapes, images):
"""Convert model estimates to COLMAP format files"""
os.makedirs(output_dir, exist_ok=True)
# Extract sparse point cloud with colors from images
print("Extracting colored sparse point cloud...")
points3d, point_colors, point_origins = extract_sparse_point_cloud(point_maps, images, sample_step=20)
print(f"Extracted {len(points3d)} colored points")
# Compute point visibility in each image
print("Computing point visibility...")
image_points, point_tracks = compute_point_visibility(
points3d, extrinsics, intrinsics, image_shapes
)
# Write COLMAP files
print("Writing COLMAP format files...")
write_cameras_txt(intrinsics, image_shapes, output_dir)
write_images_txt(extrinsics, image_points, image_paths, output_dir)
write_points3d_txt(points3d, point_colors, point_tracks, output_dir)
print(f"COLMAP format files written to {output_dir}")
def main():
parser = argparse.ArgumentParser(description="Convert VGGT estimates to COLMAP format")
parser.add_argument("image_dir", help="Directory containing input images")
parser.add_argument("output_dir", help="Directory to write COLMAP files")
parser.add_argument("--sample-step", type=int, default=10, help="Point sampling step (lower = denser point cloud)")
args = parser.parse_args()
# Get list of images
image_paths = sorted(glob.glob(os.path.join(args.image_dir, "*.jpg"))) + \
sorted(glob.glob(os.path.join(args.image_dir, "*.png")))
if not image_paths:
raise ValueError(f"No images found in {args.image_dir}")
print(f"Found {len(image_paths)} images")
# Process images with VGGT model
device = "cuda" if torch.cuda.is_available() else "cpu"
dtype = torch.bfloat16 if torch.cuda.is_available() else torch.float32
print(f"Using device: {device}")
print("Loading VGGT model...")
from vggt.models.vggt import VGGT
from vggt.utils.load_fn import load_and_preprocess_images
from vggt.utils.pose_enc import pose_encoding_to_extri_intri
from vggt.utils.geometry import unproject_depth_map_to_point_map
model = VGGT.from_pretrained("facebook/VGGT-1B").to(device)
print("Processing images...")
images_tensor = load_and_preprocess_images(image_paths).to(device)
# Also load images in RGB format for coloring
original_images = []
image_shapes = []
for img_path in image_paths:
with Image.open(img_path) as img:
# Convert to RGB and numpy array for coloring purposes
np_img = np.array(img.convert('RGB')) / 255.0 # Normalize to 0-1
original_images.append(np_img)
# Store image shape
image_shapes.append((img.height, img.width))
with torch.no_grad():
with torch.cuda.amp.autocast(dtype=dtype):
images_batch = images_tensor[None] # add batch dimension
aggregated_tokens_list, ps_idx = model.aggregator(images_batch)
print("Estimating camera parameters...")
pose_enc = model.camera_head(aggregated_tokens_list)[-1]
extrinsic, intrinsic = pose_encoding_to_extri_intri(pose_enc, images_batch.shape[-2:])
print("Estimating depth maps...")
depth_map, _ = model.depth_head(aggregated_tokens_list, images_batch, ps_idx)
print("Unprojecting to 3D points...")
point_map = unproject_depth_map_to_point_map(
depth_map.squeeze(0), extrinsic.squeeze(0), intrinsic.squeeze(0)
)
# Convert to numpy arrays if they're not already
if isinstance(extrinsic, torch.Tensor):
extrinsic_np = extrinsic.squeeze(0).cpu().numpy()
else:
extrinsic_np = extrinsic
if isinstance(intrinsic, torch.Tensor):
intrinsic_np = intrinsic.squeeze(0).cpu().numpy()
else:
intrinsic_np = intrinsic
if isinstance(point_map, torch.Tensor):
point_map_np = point_map.cpu().numpy()
else:
point_map_np = point_map
# Resize original images to match point map if needed
resized_images = []
for i, img in enumerate(original_images):
h, w, _ = point_map_np[i].shape
if img.shape[0] != h or img.shape[1] != w:
# Use simple resizing - could be improved with better interpolation
from skimage.transform import resize
resized_img = resize(img, (h, w, 3), anti_aliasing=True)
resized_images.append(resized_img)
else:
resized_images.append(img)
# Convert to COLMAP format
convert_to_colmap_format(
extrinsic_np, intrinsic_np, point_map_np, image_paths,
args.output_dir, image_shapes, resized_images
)
if __name__ == "__main__":
main()
+127
View File
@@ -0,0 +1,127 @@
#!/usr/bin/env python
import os
import numpy as np
import json
import csv
import glob
import argparse
def extract_camera_transforms(extrinsics):
"""Extract camera positions and orientations from extrinsic matrices"""
cameras = []
for ext in extrinsics:
# Create 4x4 transformation matrix
T = np.eye(4)
T[:3, :4] = ext
# Extract rotation matrix and position
R = T[:3, :3]
t = T[:3, 3]
# Get camera center in world coordinates (-R^T * t)
center = -np.dot(R.T, t)
# Store camera data
camera_data = {
'position': center.tolist(),
'rotation_matrix': R.tolist(),
'transform_matrix': T.tolist()
}
cameras.append(camera_data)
return cameras
def export_cameras_csv(cameras, output_path):
"""Export camera data to CSV format"""
with open(output_path, 'w', newline='') as f:
writer = csv.writer(f)
# Write header
writer.writerow(['frame', 'pos_x', 'pos_y', 'pos_z',
'r11', 'r12', 'r13',
'r21', 'r22', 'r23',
'r31', 'r32', 'r33'])
# Write data for each camera
for i, cam in enumerate(cameras):
pos = cam['position']
rot = cam['rotation_matrix']
row = [i,
pos[0], pos[1], pos[2],
rot[0][0], rot[0][1], rot[0][2],
rot[1][0], rot[1][1], rot[1][2],
rot[2][0], rot[2][1], rot[2][2]]
writer.writerow(row)
def export_cameras_json(cameras, output_path):
"""Export camera data to JSON format"""
camera_data = {
'cameras': [
{
'frame': i,
'position': cam['position'],
'rotation_matrix': cam['rotation_matrix'],
'transform_matrix': cam['transform_matrix']
}
for i, cam in enumerate(cameras)
]
}
with open(output_path, 'w') as f:
json.dump(camera_data, f, indent=2)
def main():
parser = argparse.ArgumentParser(
description="Export camera poses from inference results to CSV and JSON"
)
parser.add_argument(
"--npz_files", type=str, required=True,
help="Path to directory containing NPZ files or glob pattern to match them"
)
parser.add_argument(
"--output_prefix", type=str, default="camera_poses0",
help="Prefix for output files (will create .csv and .json files)"
)
args = parser.parse_args()
# Get all NPZ files
if os.path.isdir(args.npz_files):
npz_paths = glob.glob(os.path.join(args.npz_files, "**", "*.npz"), recursive=True)
else:
npz_paths = glob.glob(args.npz_files)
if not npz_paths:
raise ValueError(f"No NPZ files found at {args.npz_files}")
print(f"Found {len(npz_paths)} NPZ files")
# Collect all camera transforms
all_cameras = []
for npz_path in npz_paths:
print(f"Processing {npz_path}")
predictions = dict(np.load(npz_path, allow_pickle=True))
if "extrinsic" in predictions:
extrinsics = predictions["extrinsic"]
cameras = extract_camera_transforms(extrinsics)
all_cameras.extend(cameras)
if not all_cameras:
raise ValueError("No camera transforms found in the provided NPZ files")
print(f"Extracted {len(all_cameras)} camera transforms")
# Export to CSV and JSON
csv_path = f"{args.output_prefix}.csv"
json_path = f"{args.output_prefix}.json"
export_cameras_csv(all_cameras, csv_path)
export_cameras_json(all_cameras, json_path)
print(f"Exports complete!")
print(f"CSV file saved to: {csv_path}")
print(f"JSON file saved to: {json_path}")
if __name__ == "__main__":
main()
Binary file not shown.
+296 -79
View File
@@ -29,6 +29,247 @@ from vggt.utils.geometry import unproject_depth_map_to_point_map
# -------------------------------------------------------------------------
# Overlapping Batch Processing Functions
# -------------------------------------------------------------------------
def improve_camera_based_initialization(source_cameras, target_cameras, source_images, target_images):
"""
Create a better initial transformation using camera poses to prevent bottom-to-top matching issues
"""
print("Finding optimal camera alignment...")
# Find overlapping camera frames if any
source_filenames = [os.path.splitext(os.path.basename(path))[0] for path in source_images]
target_filenames = [os.path.splitext(os.path.basename(path))[0] for path in target_images]
common_files = set(source_filenames) & set(target_filenames)
# Get indices in each batch
source_overlap_idx = [source_filenames.index(f) for f in common_files if f in source_filenames]
target_overlap_idx = [target_filenames.index(f) for f in common_files if f in target_filenames]
# Try multiple alignment strategies and choose the best one
alignment_options = []
# Option 1: Use overlapping cameras if available
if len(source_overlap_idx) >= 3:
print(f"Strategy 1: Using {len(source_overlap_idx)} explicitly overlapping cameras")
source_pts = source_cameras[source_overlap_idx]
target_pts = target_cameras[target_overlap_idx]
# Calculate transformation
transform, error = compute_rigid_transform(source_pts, target_pts)
alignment_options.append(("Overlapping Cameras", transform, error, source_overlap_idx, target_overlap_idx))
# Option 2: Try start-to-end alignment (last cameras of target to first cameras of source)
num_match = min(8, min(len(source_cameras), len(target_cameras)))
source_start_idx = list(range(num_match))
target_end_idx = list(range(len(target_cameras) - num_match, len(target_cameras)))
print(f"Strategy 2: Matching last {num_match} cameras of target to first {num_match} cameras of source")
source_pts = source_cameras[:num_match]
target_pts = target_cameras[-num_match:]
transform, error = compute_rigid_transform(source_pts, target_pts)
alignment_options.append(("Start-to-End", transform, error, source_start_idx, target_end_idx))
# Option 3: Try end-to-start alignment (first cameras of target to last cameras of source)
source_end_idx = list(range(len(source_cameras) - num_match, len(source_cameras)))
target_start_idx = list(range(num_match))
print(f"Strategy 3: Matching first {num_match} cameras of target to last {num_match} cameras of source")
source_pts = source_cameras[-num_match:]
target_pts = target_cameras[:num_match]
transform, error = compute_rigid_transform(source_pts, target_pts)
alignment_options.append(("End-to-Start", transform, error, source_end_idx, target_start_idx))
# Choose the best alignment based on error
alignment_options.sort(key=lambda x: x[2])
best_alignment = alignment_options[0]
print(f"Selected alignment strategy: {best_alignment[0]} with error: {best_alignment[2]:.6f}")
return best_alignment[1], best_alignment[3], best_alignment[4]
def compute_rigid_transform(source_pts, target_pts):
"""Calculate optimal rigid transform between point sets and return error metric"""
# Calculate centroids
source_centroid = np.mean(source_pts, axis=0)
target_centroid = np.mean(target_pts, axis=0)
# Center the points
source_centered = source_pts - source_centroid
target_centered = target_pts - target_centroid
# Compute rotation using SVD
H = np.dot(source_centered.T, target_centered)
U, _, Vt = np.linalg.svd(H)
R = np.dot(Vt.T, U.T)
# Ensure proper rotation matrix (det=1)
if np.linalg.det(R) < 0:
Vt[-1, :] *= -1
R = np.dot(Vt.T, U.T)
# Translation
t = target_centroid - np.dot(R, source_centroid)
# Create transformation matrix
transformation = np.eye(4)
transformation[:3, :3] = R
transformation[:3, 3] = t
# Calculate error metric (average distance after transform)
transformed_source = np.dot(source_centered, R.T) + target_centroid
error = np.mean(np.linalg.norm(transformed_source - target_pts, axis=1))
return transformation, error
def visualize_registration_overlap(source_pcd, target_pcd, source_cameras, target_cameras,
source_overlap_idx, target_overlap_idx, transformation, output_path):
"""
Create a visualization GLB showing both point clouds and their camera positions
"""
# Transform source to align with target
source_transformed = copy.deepcopy(source_pcd)
source_transformed.transform(transformation)
# Color the point clouds differently
source_points = np.asarray(source_transformed.points)
source_colors = np.ones((len(source_points), 3)) * [1.0, 0.7, 0.0] # Yellow for source
target_points = np.asarray(target_pcd.points)
target_colors = np.ones((len(target_points), 3)) * [0.0, 0.65, 0.93] # Blue for target
# Combine the point clouds
all_points = np.vstack((source_points, target_points))
all_colors = np.vstack((source_colors, target_colors))
all_colors = (all_colors * 255).astype(np.uint8)
# Create trimesh scene
scene = trimesh.Scene()
# Add the combined point cloud
cloud = trimesh.PointCloud(vertices=all_points, colors=all_colors)
scene.add_geometry(cloud)
# Transform source cameras
transformed_source_cameras = []
for cam in source_cameras:
# Convert to homogeneous coordinates
cam_h = np.ones(4)
cam_h[:3] = cam
# Transform
cam_transformed = transformation @ cam_h
transformed_source_cameras.append(cam_transformed[:3])
# Add camera positions as spheres
for i, cam_pos in enumerate(transformed_source_cameras):
# Red for overlapping source cameras, orange for others
color = [255, 0, 0, 255] if i in source_overlap_idx else [255, 165, 0, 255]
sphere = trimesh.primitives.Sphere(radius=0.05, center=cam_pos)
sphere.visual.face_colors = color
scene.add_geometry(sphere)
for i, cam_pos in enumerate(target_cameras):
# Green for overlapping target cameras, cyan for others
color = [0, 255, 0, 255] if i in target_overlap_idx else [0, 255, 255, 255]
sphere = trimesh.primitives.Sphere(radius=0.05, center=cam_pos)
sphere.visual.face_colors = color
scene.add_geometry(sphere)
# Add camera trajectory lines using cylinders instead of paths
# Source trajectory (red)
if len(transformed_source_cameras) >= 2:
for i in range(len(transformed_source_cameras) - 1):
p1 = transformed_source_cameras[i]
p2 = transformed_source_cameras[i+1]
# Create a cylinder between consecutive camera positions
direction = p2 - p1
length = np.linalg.norm(direction)
if length > 0: # Avoid zero-length cylinders
# Create a cylinder
radius = 0.01 # Cylinder radius
center = (p1 + p2) / 2
# Calculate rotation to align cylinder with direction
direction_norm = direction / length
z_axis = np.array([0, 0, 1])
if np.allclose(direction_norm, z_axis) or np.allclose(direction_norm, -z_axis):
rotation = np.eye(3)
else:
# Find rotation from z-axis to direction
cross = np.cross(z_axis, direction_norm)
sin_angle = np.linalg.norm(cross)
cos_angle = np.dot(z_axis, direction_norm)
# Skew-symmetric cross-product matrix
K = np.array([[0, -cross[2], cross[1]],
[cross[2], 0, -cross[0]],
[-cross[1], cross[0], 0]])
# Rodrigues formula
if sin_angle > 0:
rotation = np.eye(3) + K + K @ K * (1 - cos_angle) / (sin_angle ** 2)
else:
rotation = np.eye(3)
# Create cylinder
transform = np.eye(4)
transform[:3, :3] = rotation
transform[:3, 3] = center
cylinder = trimesh.creation.cylinder(radius=radius, height=length)
cylinder.apply_transform(transform)
cylinder.visual.face_colors = [255, 0, 0, 100] # Red, semi-transparent
scene.add_geometry(cylinder)
# Target trajectory (green)
if len(target_cameras) >= 2:
for i in range(len(target_cameras) - 1):
p1 = target_cameras[i]
p2 = target_cameras[i+1]
direction = p2 - p1
length = np.linalg.norm(direction)
if length > 0:
radius = 0.01
center = (p1 + p2) / 2
direction_norm = direction / length
z_axis = np.array([0, 0, 1])
if np.allclose(direction_norm, z_axis) or np.allclose(direction_norm, -z_axis):
rotation = np.eye(3)
else:
cross = np.cross(z_axis, direction_norm)
sin_angle = np.linalg.norm(cross)
cos_angle = np.dot(z_axis, direction_norm)
K = np.array([[0, -cross[2], cross[1]],
[cross[2], 0, -cross[0]],
[-cross[1], cross[0], 0]])
if sin_angle > 0:
rotation = np.eye(3) + K + K @ K * (1 - cos_angle) / (sin_angle ** 2)
else:
rotation = np.eye(3)
transform = np.eye(4)
transform[:3, :3] = rotation
transform[:3, 3] = center
cylinder = trimesh.creation.cylinder(radius=radius, height=length)
cylinder.apply_transform(transform)
cylinder.visual.face_colors = [0, 255, 0, 100] # Green, semi-transparent
scene.add_geometry(cylinder)
# Export the scene
scene.export(output_path)
print(f"Registration visualization saved to {output_path}")
return scene
def create_overlapping_batches(image_paths, batch_size=16, overlap_size=8):
"""
Create overlapping batches of sequential images
@@ -142,7 +383,7 @@ def process_image_batch(image_batch, model, batch_id, output_dir):
glb_path = os.path.join(batch_dir, "scene.glb")
glbscene = predictions_to_glb(
predictions,
conf_thres=90.0,
conf_thres=50.0,
filter_by_frames="All",
mask_black_bg=True,
mask_white_bg=True,
@@ -268,92 +509,68 @@ def find_overlapping_cameras(batch1_cameras, batch2_cameras, batch1_images, batc
def register_consecutive_point_clouds(source_pcd, target_pcd, source_cameras, target_cameras,
source_images, target_images):
source_images, target_images, output_dir=None, batch_id=None):
"""
Register two point clouds from consecutive batches using both overlapping cameras
and point cloud features
Returns:
Transformation matrix to align source to target
Register two point clouds from consecutive batches with improved initialization
"""
print("Registering consecutive point clouds...")
# 1. Find overlapping cameras
source_overlap_idx, target_overlap_idx = find_overlapping_cameras(
# 1. Better initialization using camera poses
initial_transformation, source_overlap_idx, target_overlap_idx = improve_camera_based_initialization(
source_cameras, target_cameras, source_images, target_images
)
if len(source_overlap_idx) < 2:
print("Warning: Less than 2 overlapping cameras found, alignment may be inaccurate")
# Fall back to using all cameras if not enough overlap
source_pts = source_cameras
target_pts = target_cameras[:len(source_pts)] if len(target_cameras) > len(source_pts) else target_cameras
else:
# Use overlapping cameras for initial alignment
source_pts = source_cameras[source_overlap_idx]
target_pts = target_cameras[target_overlap_idx]
# 2. Visualize pre-registration alignment
if output_dir is not None and batch_id is not None:
pre_reg_path = os.path.join(output_dir, f"pre_registration_batch_{batch_id}.glb")
visualize_registration_overlap(
source_pcd, target_pcd, source_cameras, target_cameras,
source_overlap_idx, target_overlap_idx, initial_transformation, pre_reg_path
)
print(f"Found {len(source_overlap_idx)} overlapping cameras for alignment")
# 2. Initial alignment using camera positions
# Calculate centroids
source_centroid = np.mean(source_pts, axis=0)
target_centroid = np.mean(target_pts, axis=0)
# Center the points
source_centered = source_pts - source_centroid
target_centered = target_pts - target_centroid
# Compute rotation using SVD
H = np.dot(source_centered.T, target_centered)
U, _, Vt = np.linalg.svd(H)
R = np.dot(Vt.T, U.T)
# Ensure proper rotation matrix (det=1)
if np.linalg.det(R) < 0:
Vt[-1, :] *= -1
R = np.dot(Vt.T, U.T)
# Translation
t = target_centroid - np.dot(R, source_centroid)
# Initial transformation matrix
initial_transformation = np.eye(4)
initial_transformation[:3, :3] = R
initial_transformation[:3, 3] = t
# 3. Apply initial transformation to source point cloud
# 3. Apply initial transformation and proceed with ICP refinement
source_aligned = copy.deepcopy(source_pcd)
source_aligned.transform(initial_transformation)
# 4. Refine alignment with Colored ICP
# Prepare for colored ICP
# Prepare for ICP
source_aligned.estimate_normals()
target_pcd.estimate_normals()
# Run Colored ICP
print("Running colored ICP for fine alignment...")
reg_p2p = o3d.pipelines.registration.registration_colored_icp(
source_aligned, target_pcd, 0.05,
initial_transformation,
o3d.pipelines.registration.TransformationEstimationForColoredICP(),
o3d.pipelines.registration.ICPConvergenceCriteria(max_iteration=50)
)
# Try different refinement methods
try:
print("Running colored ICP for fine alignment...")
reg_p2p = o3d.pipelines.registration.registration_colored_icp(
source_aligned, target_pcd, 0.05,
np.eye(4), # Start from current alignment
o3d.pipelines.registration.TransformationEstimationForColoredICP(),
o3d.pipelines.registration.ICPConvergenceCriteria(max_iteration=50)
)
print(f"ICP fitness: {reg_p2p.fitness}, RMSE: {reg_p2p.inlier_rmse}")
# Only use ICP refinement if it significantly improves the alignment
if reg_p2p.fitness > 0.3: # Adjust threshold as needed
final_transformation = reg_p2p.transformation @ initial_transformation
else:
print("ICP did not improve alignment significantly, using initial transformation")
final_transformation = initial_transformation
except Exception as e:
print(f"ICP refinement failed: {str(e)}")
final_transformation = initial_transformation
# Final transformation matrix (combines initial and refinement)
final_transformation = reg_p2p.transformation @ initial_transformation
print(f"Registration complete with fitness: {reg_p2p.fitness}, RMSE: {reg_p2p.inlier_rmse}")
# 4. Visualize post-registration alignment
if output_dir is not None and batch_id is not None:
post_reg_path = os.path.join(output_dir, f"post_registration_batch_{batch_id}.glb")
visualize_registration_overlap(
source_pcd, target_pcd, source_cameras, target_cameras,
source_overlap_idx, target_overlap_idx, final_transformation, post_reg_path
)
return final_transformation
def register_all_point_clouds(batch_results, voxel_size=0.02):
def register_all_point_clouds(batch_results, output_dir, voxel_size=0.02):
"""
Register all point clouds from overlapping batches
Returns:
List of transformation matrices and list of point clouds
Register all point clouds with better initialization and visualization
"""
point_clouds = []
camera_positions = []
@@ -363,7 +580,7 @@ def register_all_point_clouds(batch_results, voxel_size=0.02):
for batch_result in batch_results:
predictions = batch_result["predictions"]
pcd, cameras = extract_point_cloud_from_predictions(
predictions, conf_threshold=50.0, voxel_size=voxel_size, mask_bg=True
predictions, conf_threshold=30.0, voxel_size=voxel_size, mask_bg=True
)
point_clouds.append(pcd)
camera_positions.append(cameras)
@@ -377,25 +594,24 @@ def register_all_point_clouds(batch_results, voxel_size=0.02):
source_images = batch_results[i]["image_paths"]
target_images = batch_results[i-1]["image_paths"]
# Pass both output_dir and batch_id
transformation = register_consecutive_point_clouds(
source_pcd, target_pcd, source_cameras, target_cameras,
source_images, target_images
source_images, target_images, output_dir, i
)
transformations.append(transformation)
# Apply transformations to point clouds
# Apply transformations to align all point clouds
aligned_point_clouds = [point_clouds[0]] # First cloud is reference
# Forward transformations - transform each cloud to first cloud's reference frame
for i in range(1, len(point_clouds)):
# Initialize with identity transformation
# Calculate cumulative transformation
cumulative_transform = np.eye(4)
# Compose transformations from current cloud back to the first cloud
for j in range(i-1, -1, -1):
cumulative_transform = transformations[j] @ cumulative_transform
# Apply transformation
transformed_pcd = copy.deepcopy(point_clouds[i])
transformed_pcd.transform(cumulative_transform)
aligned_point_clouds.append(transformed_pcd)
@@ -487,8 +703,9 @@ def reconstruct_from_images_with_overlap(image_dir, output_path, batch_size=8, o
predictions = dict(np.load(result["npz_path"], allow_pickle=True))
batch_results[i]["predictions"] = predictions
# Register point clouds
aligned_point_clouds, transformations = register_all_point_clouds(batch_results, voxel_size)
# Register point clouds - FIXED LINE BELOW
aligned_point_clouds, transformations = register_all_point_clouds(batch_results, output_dir, voxel_size)
# Register point cloud
# Merge point clouds
merged_cloud = merge_aligned_point_clouds(aligned_point_clouds, voxel_size)
@@ -540,15 +757,15 @@ def main():
help="Output GLB file path for the merged scene"
)
parser.add_argument(
"--batch_size", type=int, default=30,
"--batch_size", type=int, default=28,
help="Number of images per batch"
)
parser.add_argument(
"--overlap_size", type=int, default=20,
"--overlap_size", type=int, default=16,
help="Number of overlapping images between consecutive batches"
)
parser.add_argument(
"--conf_threshold", type=float, default=90.0,
"--conf_threshold", type=float, default=20.0,
help="Confidence threshold percentage for filtering points (0-100)"
)
parser.add_argument(
+22
View File
@@ -0,0 +1,22 @@
import pandas as pd
def subtract_consecutive_rows(csv_file, output_file):
# Read the csv file into a DataFrame
df = pd.read_csv(csv_file)
# Check if the DataFrame has at least two rows
if len(df) < 2:
print("The CSV file must have at least two rows.")
return
# Subtract consecutive rows
result_df = df.diff().iloc[1:]
# Save the result to a new CSV file
result_df.to_csv(output_file, index=False)
print(f"Result saved to {output_file}")
# Example usage
csv_file_path = 'camera_poses2.csv'
output_file_path = 'camera_poses_sub2.csv'
subtract_consecutive_rows(csv_file_path, output_file_path)