Move of the utils and visualiztion functions to a separate files
Browse files- __pycache__/utils.cpython-310.pyc +0 -0
- __pycache__/visu.cpython-310.pyc +0 -0
- test.py +3 -377
- utils.py +11 -0
- visu.py +373 -0
__pycache__/utils.cpython-310.pyc
ADDED
|
Binary file (541 Bytes). View file
|
|
|
__pycache__/visu.cpython-310.pyc
ADDED
|
Binary file (8.86 kB). View file
|
|
|
test.py
CHANGED
|
@@ -6,382 +6,10 @@ import tempfile,zipfile
|
|
| 6 |
import io
|
| 7 |
import open3d as o3d
|
| 8 |
|
| 9 |
-
from
|
| 10 |
-
|
| 11 |
-
def read_colmap_rec(colmap_data):
|
| 12 |
-
with tempfile.TemporaryDirectory() as tmpdir:
|
| 13 |
-
with zipfile.ZipFile(io.BytesIO(colmap_data), "r") as zf:
|
| 14 |
-
zf.extractall(tmpdir) # unpacks cameras.txt, images.txt, etc. to tmpdir
|
| 15 |
-
# Now parse with pycolmap
|
| 16 |
-
rec = pycolmap.Reconstruction(tmpdir)
|
| 17 |
-
return rec
|
| 18 |
-
|
| 19 |
-
def _plotly_rgb_to_normalized_o3d_color(color_val) -> list[float]:
|
| 20 |
-
"""
|
| 21 |
-
Converts Plotly-style color (str 'rgb(r,g,b)' or tuple (r,g,b))
|
| 22 |
-
to normalized [r/255, g/255, b/255] for Open3D.
|
| 23 |
-
"""
|
| 24 |
-
if isinstance(color_val, str):
|
| 25 |
-
if color_val.startswith('rgba'): # e.g. 'rgba(255,0,0,0.5)'
|
| 26 |
-
parts = color_val[5:-1].split(',')
|
| 27 |
-
return [int(p.strip())/255.0 for p in parts[:3]] # Ignore alpha
|
| 28 |
-
elif color_val.startswith('rgb'): # e.g. 'rgb(255,0,0)'
|
| 29 |
-
parts = color_val[4:-1].split(',')
|
| 30 |
-
return [int(p.strip())/255.0 for p in parts]
|
| 31 |
-
else:
|
| 32 |
-
# Basic color names are not directly supported by this helper for Open3D.
|
| 33 |
-
# Plotly might resolve them, but Open3D needs explicit RGB.
|
| 34 |
-
# Consider adding a name-to-RGB mapping if needed.
|
| 35 |
-
raise ValueError(f"Unsupported color string format for Open3D conversion: {color_val}. Expected 'rgb(...)' or 'rgba(...)'.")
|
| 36 |
-
elif isinstance(color_val, (list, tuple)) and len(color_val) == 3:
|
| 37 |
-
# Assuming input tuple/list is in 0-255 range (e.g., from edge_color_mapping)
|
| 38 |
-
return [c/255.0 for c in color_val]
|
| 39 |
-
raise ValueError(f"Unsupported color type for Open3D conversion: {type(color_val)}. Expected string or 3-element tuple/list.")
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
def plot_reconstruction_local(
|
| 43 |
-
fig: go.Figure,
|
| 44 |
-
rec: pycolmap.Reconstruction, # Added type hint
|
| 45 |
-
color: str = 'rgb(0, 0, 255)',
|
| 46 |
-
name: Optional[str] = None,
|
| 47 |
-
points: bool = True,
|
| 48 |
-
cameras: bool = True,
|
| 49 |
-
cs: float = 1.0,
|
| 50 |
-
single_color_points=False,
|
| 51 |
-
camera_color='rgba(0, 255, 0, 0.5)',
|
| 52 |
-
crop_outliers: bool = False):
|
| 53 |
-
# rec is a pycolmap.Reconstruction object
|
| 54 |
-
# Filter outliers
|
| 55 |
-
xyzs = []
|
| 56 |
-
rgbs = []
|
| 57 |
-
# Iterate over rec.points3D
|
| 58 |
-
for k, p3D in rec.points3D.items():
|
| 59 |
-
#print (p3D)
|
| 60 |
-
xyzs.append(p3D.xyz)
|
| 61 |
-
rgbs.append(p3D.color)
|
| 62 |
-
|
| 63 |
-
xyzs = np.array(xyzs)
|
| 64 |
-
rgbs = np.array(rgbs)
|
| 65 |
-
|
| 66 |
-
# Crop outliers if requested
|
| 67 |
-
if crop_outliers and len(xyzs) > 0:
|
| 68 |
-
# Calculate distances from origin
|
| 69 |
-
distances = np.linalg.norm(xyzs, axis=1)
|
| 70 |
-
# Find threshold at 98th percentile (removing 2% furthest points)
|
| 71 |
-
threshold = np.percentile(distances, 98)
|
| 72 |
-
# Filter points
|
| 73 |
-
mask = distances <= threshold
|
| 74 |
-
xyzs = xyzs[mask]
|
| 75 |
-
rgbs = rgbs[mask]
|
| 76 |
-
print(f"Cropped outliers: removed {np.sum(~mask)} out of {len(mask)} points ({np.sum(~mask)/len(mask)*100:.2f}%)")
|
| 77 |
-
|
| 78 |
-
if points and len(xyzs) > 0:
|
| 79 |
-
pcd = o3d.geometry.PointCloud()
|
| 80 |
-
pcd.points = o3d.utility.Vector3dVector(xyzs)
|
| 81 |
-
|
| 82 |
-
# Normalize RGB colors from [0, 255] to [0, 1] for Open3D
|
| 83 |
-
# and ensure rgbs is not empty before normalization
|
| 84 |
-
if rgbs.size > 0:
|
| 85 |
-
normalized_rgbs = rgbs / 255.0
|
| 86 |
-
pcd.colors = o3d.utility.Vector3dVector(normalized_rgbs)
|
| 87 |
-
|
| 88 |
-
# Original Plotly plot_points call is replaced by Open3D visualization:
|
| 89 |
-
# plot_points(fig, xyzs, color=color if single_color_points else rgbs, ps=1, name=name)
|
| 90 |
-
|
| 91 |
-
# This code assumes it's placed within the plot_reconstruction_local function,
|
| 92 |
-
# after point cloud processing, and that a list `geometries` (List[o3d.geometry.Geometry])
|
| 93 |
-
# is defined in the function's scope to collect all geometries.
|
| 94 |
-
# It uses arguments `cameras`, `rec`, `cs`, `camera_color` from the function signature.
|
| 95 |
-
# The helper `_plotly_rgb_to_normalized_o3d_color` is assumed to be available.
|
| 96 |
-
|
| 97 |
-
if cameras: # Check if camera visualization is enabled
|
| 98 |
-
try:
|
| 99 |
-
# Convert Plotly-style camera_color string to normalized RGB for Open3D
|
| 100 |
-
cam_color_normalized = _plotly_rgb_to_normalized_o3d_color(camera_color)
|
| 101 |
-
except ValueError as e:
|
| 102 |
-
print(f"Warning: Invalid camera_color '{camera_color}'. Using default green. Error: {e}")
|
| 103 |
-
cam_color_normalized = [0.0, 1.0, 0.0] # Default to green
|
| 104 |
-
|
| 105 |
-
geometries = []
|
| 106 |
-
|
| 107 |
-
for image_id, image_data in rec.images.items():
|
| 108 |
-
# Get camera object and its intrinsic matrix K
|
| 109 |
-
cam = rec.cameras[image_data.camera_id]
|
| 110 |
-
K = cam.calibration_matrix()
|
| 111 |
-
|
| 112 |
-
# Validate intrinsics (e.g., focal length check from original code)
|
| 113 |
-
if K[0, 0] > 5000 or K[1, 1] > 5000:
|
| 114 |
-
print(f"Skipping camera for image {image_id} due to large focal length (fx={K[0,0]}, fy={K[1,1]}).")
|
| 115 |
-
continue
|
| 116 |
-
|
| 117 |
-
# Get camera pose (T_world_cam = T_cam_world.inverse())
|
| 118 |
-
# image_data.cam_from_world is T_cam_world (camera coordinates from world coordinates)
|
| 119 |
-
T_world_cam = image_data.cam_from_world.inverse()
|
| 120 |
-
R_wc = T_world_cam.rotation.matrix() # Rotation: camera frame to world frame
|
| 121 |
-
t_wc = T_world_cam.translation # Origin of camera frame in world coordinates (pyramid apex)
|
| 122 |
-
|
| 123 |
-
W, H = float(cam.width), float(cam.height)
|
| 124 |
-
|
| 125 |
-
# Skip if camera dimensions are invalid
|
| 126 |
-
if W <= 0 or H <= 0:
|
| 127 |
-
print(f"Skipping camera for image {image_id} due to invalid dimensions (W={W}, H={H}).")
|
| 128 |
-
continue
|
| 129 |
-
|
| 130 |
-
# Define image plane corners in pixel coordinates (top-left, top-right, bottom-right, bottom-left)
|
| 131 |
-
img_corners_px = np.array([
|
| 132 |
-
[0, 0], [W, 0], [W, H], [0, H]
|
| 133 |
-
], dtype=float)
|
| 134 |
-
|
| 135 |
-
# Convert pixel corners to homogeneous coordinates
|
| 136 |
-
img_corners_h = np.hstack([img_corners_px, np.ones((4, 1))])
|
| 137 |
-
|
| 138 |
-
try:
|
| 139 |
-
K_inv = np.linalg.inv(K)
|
| 140 |
-
except np.linalg.LinAlgError:
|
| 141 |
-
print(f"Skipping camera for image {image_id} due to non-invertible K matrix.")
|
| 142 |
-
continue
|
| 143 |
-
|
| 144 |
-
# Unproject pixel corners to normalized camera coordinates (points on z=1 plane in camera frame)
|
| 145 |
-
cam_coords_norm = (K_inv @ img_corners_h.T).T
|
| 146 |
-
|
| 147 |
-
# Scale these points by 'cs' (camera scale factor, determines frustum size)
|
| 148 |
-
# These points are ( (x/z)*cs, (y/z)*cs, cs ) in the camera's coordinate system.
|
| 149 |
-
cam_coords_scaled = cam_coords_norm * cs
|
| 150 |
-
|
| 151 |
-
# Transform scaled base corners from camera coordinates to world coordinates
|
| 152 |
-
world_coords_base = (R_wc @ cam_coords_scaled.T).T + t_wc
|
| 153 |
-
|
| 154 |
-
# Define vertices for the camera pyramid visualization
|
| 155 |
-
# Vertex 0 is the apex (camera center), vertices 1-4 are the base corners
|
| 156 |
-
pyramid_vertices = np.vstack((t_wc, world_coords_base))
|
| 157 |
-
if not pyramid_vertices.flags['C_CONTIGUOUS']:
|
| 158 |
-
pyramid_vertices = np.ascontiguousarray(pyramid_vertices, dtype=np.float64)
|
| 159 |
-
elif pyramid_vertices.dtype != np.float64:
|
| 160 |
-
pyramid_vertices = np.asarray(pyramid_vertices, dtype=np.float64)
|
| 161 |
-
|
| 162 |
-
# Define lines for the pyramid: 4 from apex to base, 4 for the base rectangle
|
| 163 |
-
lines = np.array([
|
| 164 |
-
[0, 1], [0, 2], [0, 3], [0, 4], # Apex to base corners
|
| 165 |
-
[1, 2], [2, 3], [3, 4], [4, 1] # Base rectangle
|
| 166 |
-
])
|
| 167 |
-
|
| 168 |
-
if not lines.flags['C_CONTIGUOUS']:
|
| 169 |
-
lines = np.ascontiguousarray(lines, dtype=np.int32)
|
| 170 |
-
elif lines.dtype != np.int32:
|
| 171 |
-
lines = np.asarray(lines, dtype=np.int32)
|
| 172 |
-
|
| 173 |
-
# Create Open3D LineSet object for the camera pyramid
|
| 174 |
-
camera_pyramid_lineset = o3d.geometry.LineSet()
|
| 175 |
-
camera_pyramid_lineset.points = o3d.utility.Vector3dVector(pyramid_vertices)
|
| 176 |
-
camera_pyramid_lineset.lines = o3d.utility.Vector2iVector(lines)
|
| 177 |
-
|
| 178 |
-
# Add the camera visualization to the list of geometries to be rendered
|
| 179 |
-
geometries.append(camera_pyramid_lineset)
|
| 180 |
-
|
| 181 |
-
else:
|
| 182 |
-
geometries = []
|
| 183 |
-
|
| 184 |
-
return pcd, geometries
|
| 185 |
-
|
| 186 |
-
def plot_wireframe_local(
|
| 187 |
-
fig: go.Figure, # This argument is no longer used for plotting by this function.
|
| 188 |
-
vertices: np.ndarray,
|
| 189 |
-
edges: np.ndarray,
|
| 190 |
-
classifications: np.ndarray = None,
|
| 191 |
-
color: str = 'rgb(0, 0, 255)', # Default color for vertices and unclassified/default edges.
|
| 192 |
-
name: Optional[str] = None, # No direct equivalent for Open3D geometry list's name/legend.
|
| 193 |
-
**kwargs) -> list: # Returns a list of o3d.geometry.Geometry objects.
|
| 194 |
-
"""
|
| 195 |
-
Generates Open3D geometries for a wireframe: a PointCloud for vertices
|
| 196 |
-
and a LineSet for edges.
|
| 197 |
-
|
| 198 |
-
Args:
|
| 199 |
-
fig: Plotly figure object (no longer used for plotting by this function).
|
| 200 |
-
vertices: Numpy array of vertex coordinates (N, 3).
|
| 201 |
-
edges: Numpy array of edge connections (M, 2), indices into vertices.
|
| 202 |
-
classifications: Optional numpy array of classifications for edges.
|
| 203 |
-
color: Default color string (e.g., 'rgb(0,0,255)') for vertices and
|
| 204 |
-
for edges if classifications are not provided or don't match.
|
| 205 |
-
name: Optional name (unused in Open3D context here).
|
| 206 |
-
**kwargs: Additional keyword arguments (unused).
|
| 207 |
-
|
| 208 |
-
Returns:
|
| 209 |
-
A list of Open3D geometry objects. Empty if no vertices.
|
| 210 |
-
Note: Plotly-specific 'ps' (point size) and line width are not
|
| 211 |
-
directly translated. These are typically visualizer render options in Open3D.
|
| 212 |
-
"""
|
| 213 |
-
open3d_geometries = []
|
| 214 |
-
|
| 215 |
-
# Ensure gt_vertices is numpy array, C-contiguous, and float64
|
| 216 |
-
# np.asarray avoids a copy if 'vertices' is already a suitable ndarray.
|
| 217 |
-
gt_vertices = np.asarray(vertices)
|
| 218 |
-
if not gt_vertices.flags['C_CONTIGUOUS'] or gt_vertices.dtype != np.float64:
|
| 219 |
-
gt_vertices = np.ascontiguousarray(gt_vertices, dtype=np.float64)
|
| 220 |
-
|
| 221 |
-
# Ensure gt_connections is numpy array, C-contiguous, and int32
|
| 222 |
-
gt_connections = np.asarray(edges)
|
| 223 |
-
if not gt_connections.flags['C_CONTIGUOUS'] or gt_connections.dtype != np.int32:
|
| 224 |
-
gt_connections = np.ascontiguousarray(gt_connections, dtype=np.int32)
|
| 225 |
-
|
| 226 |
-
if gt_vertices.size == 0:
|
| 227 |
-
return open3d_geometries
|
| 228 |
-
|
| 229 |
-
# 1. Create PointCloud for Vertices
|
| 230 |
-
try:
|
| 231 |
-
vertex_rgb_normalized = _plotly_rgb_to_normalized_o3d_color(color)
|
| 232 |
-
except ValueError as e:
|
| 233 |
-
print(f"Warning: Could not parse vertex color '{color}'. Using default black. Error: {e}")
|
| 234 |
-
vertex_rgb_normalized = [0.0, 0.0, 0.0] # Default to black on error
|
| 235 |
-
|
| 236 |
-
vertex_pcd = o3d.geometry.PointCloud()
|
| 237 |
-
# gt_vertices is now C-contiguous and float64
|
| 238 |
-
vertex_pcd.points = o3d.utility.Vector3dVector(gt_vertices)
|
| 239 |
-
|
| 240 |
-
num_vertices = len(gt_vertices)
|
| 241 |
-
if num_vertices > 0:
|
| 242 |
-
# Create vertex_colors_np with correct dtype
|
| 243 |
-
vertex_colors_np = np.array([vertex_rgb_normalized] * num_vertices, dtype=np.float64)
|
| 244 |
-
# Ensure C-contiguity (dtype is already float64 from np.array construction)
|
| 245 |
-
# This check is a safeguard, as np.array from a list of lists with specified dtype is usually contiguous.
|
| 246 |
-
if not vertex_colors_np.flags['C_CONTIGUOUS']:
|
| 247 |
-
vertex_colors_np = np.ascontiguousarray(vertex_colors_np) # Preserves dtype
|
| 248 |
-
vertex_pcd.colors = o3d.utility.Vector3dVector(vertex_colors_np)
|
| 249 |
-
open3d_geometries.append(vertex_pcd)
|
| 250 |
-
|
| 251 |
-
# 2. Create LineSet for Edges
|
| 252 |
-
if gt_connections.size > 0 and num_vertices > 0: # Edges need vertices
|
| 253 |
-
line_set = o3d.geometry.LineSet()
|
| 254 |
-
# gt_vertices is already C-contiguous and float64
|
| 255 |
-
line_set.points = o3d.utility.Vector3dVector(gt_vertices)
|
| 256 |
-
# gt_connections is already C-contiguous and int32
|
| 257 |
-
line_set.lines = o3d.utility.Vector2iVector(gt_connections)
|
| 258 |
-
|
| 259 |
-
line_colors_list_normalized = []
|
| 260 |
-
if classifications is not None and len(classifications) == len(gt_connections):
|
| 261 |
-
# Assuming EDGE_CLASSES_BY_ID and edge_color_mapping are defined in the global scope
|
| 262 |
-
# or imported, as implied by the original code structure.
|
| 263 |
-
for c_idx in classifications:
|
| 264 |
-
try:
|
| 265 |
-
color_tuple_255 = edge_color_mapping[EDGE_CLASSES_BY_ID[c_idx]]
|
| 266 |
-
line_colors_list_normalized.append(_plotly_rgb_to_normalized_o3d_color(color_tuple_255))
|
| 267 |
-
except KeyError: # Handle cases where classification ID or mapping is not found
|
| 268 |
-
print(f"Warning: Classification ID {c_idx} or its mapping not found. Using default color.")
|
| 269 |
-
line_colors_list_normalized.append(vertex_rgb_normalized) # Fallback to default vertex color
|
| 270 |
-
except Exception as e:
|
| 271 |
-
print(f"Warning: Error processing classification color for index {c_idx}. Using default. Error: {e}")
|
| 272 |
-
line_colors_list_normalized.append(vertex_rgb_normalized) # Fallback
|
| 273 |
-
else:
|
| 274 |
-
# Use the default 'color' for all lines if no classifications or mismatch
|
| 275 |
-
default_line_rgb_normalized = vertex_rgb_normalized # Same as vertex color by default
|
| 276 |
-
for _ in range(len(gt_connections)):
|
| 277 |
-
line_colors_list_normalized.append(default_line_rgb_normalized)
|
| 278 |
-
|
| 279 |
-
if line_colors_list_normalized: # Check if list is not empty
|
| 280 |
-
# Create line_colors_np with correct dtype
|
| 281 |
-
line_colors_np = np.array(line_colors_list_normalized, dtype=np.float64)
|
| 282 |
-
# Ensure C-contiguity (dtype is already float64)
|
| 283 |
-
# Safeguard, similar to vertex_colors_np.
|
| 284 |
-
if not line_colors_np.flags['C_CONTIGUOUS']:
|
| 285 |
-
line_colors_np = np.ascontiguousarray(line_colors_np) # Preserves dtype
|
| 286 |
-
line_set.colors = o3d.utility.Vector3dVector(line_colors_np)
|
| 287 |
-
|
| 288 |
-
open3d_geometries.append(line_set)
|
| 289 |
-
|
| 290 |
-
return open3d_geometries
|
| 291 |
-
|
| 292 |
-
def plot_bpo_cameras_from_entry_local(fig: go.Figure, entry: dict, idx = None, camera_scale_factor: float = 1.0):
|
| 293 |
-
def cam2world_to_world2cam(R, t):
|
| 294 |
-
rt = np.eye(4)
|
| 295 |
-
rt[:3,:3] = R
|
| 296 |
-
rt[:3,3] = t.reshape(-1)
|
| 297 |
-
rt = np.linalg.inv(rt)
|
| 298 |
-
return rt[:3,:3], rt[:3,3]
|
| 299 |
-
geometries = []
|
| 300 |
-
for i in range(len(entry['R'])):
|
| 301 |
-
if idx is not None and i != idx:
|
| 302 |
-
continue
|
| 303 |
-
|
| 304 |
-
# Parameters for this camera visualization
|
| 305 |
-
# current_cam_size = 1.0 # Original 'size = 1.' - Replaced by camera_scale_factor
|
| 306 |
-
current_cam_color_str = 'rgb(0, 0, 255)' # Original 'color = 'rgb(0, 0, 255)''
|
| 307 |
-
|
| 308 |
-
# Load camera parameters from entry
|
| 309 |
-
K_matrix = np.array(entry['K'][i])
|
| 310 |
-
R_orig = np.array(entry['R'][i])
|
| 311 |
-
t_orig = np.array(entry['t'][i])
|
| 312 |
-
|
| 313 |
-
# Apply cam2world_to_world2cam transformation as in original snippet
|
| 314 |
-
# This R_transformed, t_transformed will be used to place the camera geometry
|
| 315 |
-
R_transformed, t_transformed = cam2world_to_world2cam(R_orig, t_orig)
|
| 316 |
-
|
| 317 |
-
# Image dimensions from K matrix (cx, cy are K[0,2], K[1,2])
|
| 318 |
-
# Ensure W_img and H_img are derived correctly. Assuming K[0,2] and K[1,2] are principal points cx, cy.
|
| 319 |
-
# If K is [fx, 0, cx; 0, fy, cy; 0, 0, 1], then W_img and H_img might need to come from elsewhere
|
| 320 |
-
# or be estimated if not directly available. The original code used K[0,2]*2, K[1,2]*2.
|
| 321 |
-
# This implies cx = W/2, cy = H/2.
|
| 322 |
-
W_img, H_img = K_matrix[0, 2] * 2, K_matrix[1, 2] * 2
|
| 323 |
-
if W_img <= 0 or H_img <= 0:
|
| 324 |
-
# Attempt to get W, H from cam.width, cam.height if available in entry, like in colmap
|
| 325 |
-
# This part depends on the structure of 'entry'. For now, stick to original logic.
|
| 326 |
-
print(f"Warning: Camera {i} has invalid dimensions (W={W_img}, H={H_img}) based on K. Skipping.")
|
| 327 |
-
continue
|
| 328 |
-
|
| 329 |
-
# Define image plane corners in pixel coordinates (top-left, top-right, bottom-right, bottom-left)
|
| 330 |
-
corners_px = np.array([[0, 0], [W_img, 0], [W_img, H_img], [0, H_img]], dtype=float)
|
| 331 |
-
|
| 332 |
-
# Removed scale_val, image_extent, world_extent calculations.
|
| 333 |
-
# The scaling is now directly controlled by camera_scale_factor.
|
| 334 |
-
|
| 335 |
-
try:
|
| 336 |
-
K_inv = np.linalg.inv(K_matrix)
|
| 337 |
-
except np.linalg.LinAlgError:
|
| 338 |
-
print(f"Warning: K matrix for camera {i} is singular. Skipping this camera.")
|
| 339 |
-
continue
|
| 340 |
-
|
| 341 |
-
# Unproject pixel corners to homogeneous camera coordinates.
|
| 342 |
-
# Assuming to_homogeneous converts (N,2) pixel coords to (N,3) homogeneous coords [px, py, 1].
|
| 343 |
-
# These points are on the z=1 plane in camera coordinates.
|
| 344 |
-
corners_cam_homog = to_homogeneous(corners_px) @ K_inv.T
|
| 345 |
-
|
| 346 |
-
# Scale these points by camera_scale_factor.
|
| 347 |
-
# This makes the frustum base at z=camera_scale_factor in camera coordinates.
|
| 348 |
-
scaled_cam_points = corners_cam_homog * camera_scale_factor
|
| 349 |
-
|
| 350 |
-
# Transform scaled camera points to world coordinates using R_transformed, t_transformed
|
| 351 |
-
world_coords_base = scaled_cam_points @ R_transformed.T + t_transformed
|
| 352 |
-
|
| 353 |
-
# Apex of the pyramid is t_transformed
|
| 354 |
-
apex_world = t_transformed.reshape(1, 3)
|
| 355 |
-
|
| 356 |
-
# Vertices for Open3D LineSet: apex (vertex 0) + 4 base corners (vertices 1-4)
|
| 357 |
-
pyramid_vertices_np = np.vstack((apex_world, world_coords_base))
|
| 358 |
-
if not pyramid_vertices_np.flags['C_CONTIGUOUS'] or pyramid_vertices_np.dtype != np.float64:
|
| 359 |
-
pyramid_vertices_np = np.ascontiguousarray(pyramid_vertices_np, dtype=np.float64)
|
| 360 |
-
|
| 361 |
-
# Lines for the pyramid: 4 from apex to base, 4 for the base rectangle
|
| 362 |
-
lines_np = np.array([
|
| 363 |
-
[0, 1], [0, 2], [0, 3], [0, 4], # Apex to base corners
|
| 364 |
-
[1, 2], [2, 3], [3, 4], [4, 1] # Base rectangle (closed loop)
|
| 365 |
-
], dtype=np.int32)
|
| 366 |
-
|
| 367 |
-
# Create Open3D LineSet object for the camera pyramid
|
| 368 |
-
camera_lineset = o3d.geometry.LineSet()
|
| 369 |
-
camera_lineset.points = o3d.utility.Vector3dVector(pyramid_vertices_np)
|
| 370 |
-
lines_np = np.ascontiguousarray(lines_np, dtype=np.int32)
|
| 371 |
-
camera_lineset.lines = o3d.utility.Vector2iVector(lines_np)
|
| 372 |
-
|
| 373 |
-
# Color the LineSet
|
| 374 |
-
try:
|
| 375 |
-
o3d_color = _plotly_rgb_to_normalized_o3d_color(current_cam_color_str)
|
| 376 |
-
except ValueError as e:
|
| 377 |
-
print(f"Warning: Invalid camera color string '{current_cam_color_str}' for camera {i}. Using default blue. Error: {e}")
|
| 378 |
-
o3d_color = [0.0, 0.0, 1.0] # Default to blue
|
| 379 |
-
camera_lineset.colors = o3d.utility.Vector3dVector(np.array([o3d_color] * len(lines_np), dtype=np.float64))
|
| 380 |
-
|
| 381 |
-
geometries.append(camera_lineset)
|
| 382 |
-
|
| 383 |
-
return geometries
|
| 384 |
|
|
|
|
| 385 |
|
| 386 |
ds = load_dataset("usm3d/hoho25k", streaming=True, trust_remote_code=True)
|
| 387 |
for a in ds['train']:
|
|
@@ -394,8 +22,6 @@ for a in ds['train']:
|
|
| 394 |
wireframe2 = plot_wireframe_local(None, pred_vertices, pred_edges, None, color='rgb(255, 0, 0)')
|
| 395 |
bpo_cams = plot_bpo_cameras_from_entry_local(None, a)
|
| 396 |
|
| 397 |
-
print(len(geometries), len(bpo_cams))
|
| 398 |
-
|
| 399 |
visu_all = [pcd] + geometries + wireframe + bpo_cams + wireframe2
|
| 400 |
o3d.visualization.draw_geometries(visu_all, window_name="3D Reconstruction")
|
| 401 |
|
|
|
|
| 6 |
import io
|
| 7 |
import open3d as o3d
|
| 8 |
|
| 9 |
+
from visu import plot_reconstruction_local, plot_wireframe_local, plot_bpo_cameras_from_entry_local, _plotly_rgb_to_normalized_o3d_color
|
| 10 |
+
from utils import read_colmap_rec
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
+
from hoho2025.example_solutions import predict_wireframe
|
| 13 |
|
| 14 |
ds = load_dataset("usm3d/hoho25k", streaming=True, trust_remote_code=True)
|
| 15 |
for a in ds['train']:
|
|
|
|
| 22 |
wireframe2 = plot_wireframe_local(None, pred_vertices, pred_edges, None, color='rgb(255, 0, 0)')
|
| 23 |
bpo_cams = plot_bpo_cameras_from_entry_local(None, a)
|
| 24 |
|
|
|
|
|
|
|
| 25 |
visu_all = [pcd] + geometries + wireframe + bpo_cams + wireframe2
|
| 26 |
o3d.visualization.draw_geometries(visu_all, window_name="3D Reconstruction")
|
| 27 |
|
utils.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pycolmap
|
| 2 |
+
import tempfile,zipfile
|
| 3 |
+
import io
|
| 4 |
+
|
| 5 |
+
def read_colmap_rec(colmap_data):
|
| 6 |
+
with tempfile.TemporaryDirectory() as tmpdir:
|
| 7 |
+
with zipfile.ZipFile(io.BytesIO(colmap_data), "r") as zf:
|
| 8 |
+
zf.extractall(tmpdir) # unpacks cameras.txt, images.txt, etc. to tmpdir
|
| 9 |
+
# Now parse with pycolmap
|
| 10 |
+
rec = pycolmap.Reconstruction(tmpdir)
|
| 11 |
+
return rec
|
visu.py
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from datasets import load_dataset
|
| 2 |
+
from hoho2025.vis import plot_all_modalities
|
| 3 |
+
from hoho2025.viz3d import *
|
| 4 |
+
import pycolmap
|
| 5 |
+
import tempfile,zipfile
|
| 6 |
+
import io
|
| 7 |
+
import open3d as o3d
|
| 8 |
+
|
| 9 |
+
def _plotly_rgb_to_normalized_o3d_color(color_val) -> list[float]:
|
| 10 |
+
"""
|
| 11 |
+
Converts Plotly-style color (str 'rgb(r,g,b)' or tuple (r,g,b))
|
| 12 |
+
to normalized [r/255, g/255, b/255] for Open3D.
|
| 13 |
+
"""
|
| 14 |
+
if isinstance(color_val, str):
|
| 15 |
+
if color_val.startswith('rgba'): # e.g. 'rgba(255,0,0,0.5)'
|
| 16 |
+
parts = color_val[5:-1].split(',')
|
| 17 |
+
return [int(p.strip())/255.0 for p in parts[:3]] # Ignore alpha
|
| 18 |
+
elif color_val.startswith('rgb'): # e.g. 'rgb(255,0,0)'
|
| 19 |
+
parts = color_val[4:-1].split(',')
|
| 20 |
+
return [int(p.strip())/255.0 for p in parts]
|
| 21 |
+
else:
|
| 22 |
+
# Basic color names are not directly supported by this helper for Open3D.
|
| 23 |
+
# Plotly might resolve them, but Open3D needs explicit RGB.
|
| 24 |
+
# Consider adding a name-to-RGB mapping if needed.
|
| 25 |
+
raise ValueError(f"Unsupported color string format for Open3D conversion: {color_val}. Expected 'rgb(...)' or 'rgba(...)'.")
|
| 26 |
+
elif isinstance(color_val, (list, tuple)) and len(color_val) == 3:
|
| 27 |
+
# Assuming input tuple/list is in 0-255 range (e.g., from edge_color_mapping)
|
| 28 |
+
return [c/255.0 for c in color_val]
|
| 29 |
+
raise ValueError(f"Unsupported color type for Open3D conversion: {type(color_val)}. Expected string or 3-element tuple/list.")
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def plot_reconstruction_local(
|
| 33 |
+
fig: go.Figure,
|
| 34 |
+
rec: pycolmap.Reconstruction, # Added type hint
|
| 35 |
+
color: str = 'rgb(0, 0, 255)',
|
| 36 |
+
name: Optional[str] = None,
|
| 37 |
+
points: bool = True,
|
| 38 |
+
cameras: bool = True,
|
| 39 |
+
cs: float = 1.0,
|
| 40 |
+
single_color_points=False,
|
| 41 |
+
camera_color='rgba(0, 255, 0, 0.5)',
|
| 42 |
+
crop_outliers: bool = False):
|
| 43 |
+
# rec is a pycolmap.Reconstruction object
|
| 44 |
+
# Filter outliers
|
| 45 |
+
xyzs = []
|
| 46 |
+
rgbs = []
|
| 47 |
+
# Iterate over rec.points3D
|
| 48 |
+
for k, p3D in rec.points3D.items():
|
| 49 |
+
#print (p3D)
|
| 50 |
+
xyzs.append(p3D.xyz)
|
| 51 |
+
rgbs.append(p3D.color)
|
| 52 |
+
|
| 53 |
+
xyzs = np.array(xyzs)
|
| 54 |
+
rgbs = np.array(rgbs)
|
| 55 |
+
|
| 56 |
+
# Crop outliers if requested
|
| 57 |
+
if crop_outliers and len(xyzs) > 0:
|
| 58 |
+
# Calculate distances from origin
|
| 59 |
+
distances = np.linalg.norm(xyzs, axis=1)
|
| 60 |
+
# Find threshold at 98th percentile (removing 2% furthest points)
|
| 61 |
+
threshold = np.percentile(distances, 98)
|
| 62 |
+
# Filter points
|
| 63 |
+
mask = distances <= threshold
|
| 64 |
+
xyzs = xyzs[mask]
|
| 65 |
+
rgbs = rgbs[mask]
|
| 66 |
+
print(f"Cropped outliers: removed {np.sum(~mask)} out of {len(mask)} points ({np.sum(~mask)/len(mask)*100:.2f}%)")
|
| 67 |
+
|
| 68 |
+
if points and len(xyzs) > 0:
|
| 69 |
+
pcd = o3d.geometry.PointCloud()
|
| 70 |
+
pcd.points = o3d.utility.Vector3dVector(xyzs)
|
| 71 |
+
|
| 72 |
+
# Normalize RGB colors from [0, 255] to [0, 1] for Open3D
|
| 73 |
+
# and ensure rgbs is not empty before normalization
|
| 74 |
+
if rgbs.size > 0:
|
| 75 |
+
normalized_rgbs = rgbs / 255.0
|
| 76 |
+
pcd.colors = o3d.utility.Vector3dVector(normalized_rgbs)
|
| 77 |
+
|
| 78 |
+
# Original Plotly plot_points call is replaced by Open3D visualization:
|
| 79 |
+
# plot_points(fig, xyzs, color=color if single_color_points else rgbs, ps=1, name=name)
|
| 80 |
+
|
| 81 |
+
# This code assumes it's placed within the plot_reconstruction_local function,
|
| 82 |
+
# after point cloud processing, and that a list `geometries` (List[o3d.geometry.Geometry])
|
| 83 |
+
# is defined in the function's scope to collect all geometries.
|
| 84 |
+
# It uses arguments `cameras`, `rec`, `cs`, `camera_color` from the function signature.
|
| 85 |
+
# The helper `_plotly_rgb_to_normalized_o3d_color` is assumed to be available.
|
| 86 |
+
|
| 87 |
+
if cameras: # Check if camera visualization is enabled
|
| 88 |
+
try:
|
| 89 |
+
# Convert Plotly-style camera_color string to normalized RGB for Open3D
|
| 90 |
+
cam_color_normalized = _plotly_rgb_to_normalized_o3d_color(camera_color)
|
| 91 |
+
except ValueError as e:
|
| 92 |
+
print(f"Warning: Invalid camera_color '{camera_color}'. Using default green. Error: {e}")
|
| 93 |
+
cam_color_normalized = [0.0, 1.0, 0.0] # Default to green
|
| 94 |
+
|
| 95 |
+
geometries = []
|
| 96 |
+
|
| 97 |
+
for image_id, image_data in rec.images.items():
|
| 98 |
+
# Get camera object and its intrinsic matrix K
|
| 99 |
+
cam = rec.cameras[image_data.camera_id]
|
| 100 |
+
K = cam.calibration_matrix()
|
| 101 |
+
|
| 102 |
+
# Validate intrinsics (e.g., focal length check from original code)
|
| 103 |
+
if K[0, 0] > 5000 or K[1, 1] > 5000:
|
| 104 |
+
print(f"Skipping camera for image {image_id} due to large focal length (fx={K[0,0]}, fy={K[1,1]}).")
|
| 105 |
+
continue
|
| 106 |
+
|
| 107 |
+
# Get camera pose (T_world_cam = T_cam_world.inverse())
|
| 108 |
+
# image_data.cam_from_world is T_cam_world (camera coordinates from world coordinates)
|
| 109 |
+
T_world_cam = image_data.cam_from_world.inverse()
|
| 110 |
+
R_wc = T_world_cam.rotation.matrix() # Rotation: camera frame to world frame
|
| 111 |
+
t_wc = T_world_cam.translation # Origin of camera frame in world coordinates (pyramid apex)
|
| 112 |
+
|
| 113 |
+
W, H = float(cam.width), float(cam.height)
|
| 114 |
+
|
| 115 |
+
# Skip if camera dimensions are invalid
|
| 116 |
+
if W <= 0 or H <= 0:
|
| 117 |
+
print(f"Skipping camera for image {image_id} due to invalid dimensions (W={W}, H={H}).")
|
| 118 |
+
continue
|
| 119 |
+
|
| 120 |
+
# Define image plane corners in pixel coordinates (top-left, top-right, bottom-right, bottom-left)
|
| 121 |
+
img_corners_px = np.array([
|
| 122 |
+
[0, 0], [W, 0], [W, H], [0, H]
|
| 123 |
+
], dtype=float)
|
| 124 |
+
|
| 125 |
+
# Convert pixel corners to homogeneous coordinates
|
| 126 |
+
img_corners_h = np.hstack([img_corners_px, np.ones((4, 1))])
|
| 127 |
+
|
| 128 |
+
try:
|
| 129 |
+
K_inv = np.linalg.inv(K)
|
| 130 |
+
except np.linalg.LinAlgError:
|
| 131 |
+
print(f"Skipping camera for image {image_id} due to non-invertible K matrix.")
|
| 132 |
+
continue
|
| 133 |
+
|
| 134 |
+
# Unproject pixel corners to normalized camera coordinates (points on z=1 plane in camera frame)
|
| 135 |
+
cam_coords_norm = (K_inv @ img_corners_h.T).T
|
| 136 |
+
|
| 137 |
+
# Scale these points by 'cs' (camera scale factor, determines frustum size)
|
| 138 |
+
# These points are ( (x/z)*cs, (y/z)*cs, cs ) in the camera's coordinate system.
|
| 139 |
+
cam_coords_scaled = cam_coords_norm * cs
|
| 140 |
+
|
| 141 |
+
# Transform scaled base corners from camera coordinates to world coordinates
|
| 142 |
+
world_coords_base = (R_wc @ cam_coords_scaled.T).T + t_wc
|
| 143 |
+
|
| 144 |
+
# Define vertices for the camera pyramid visualization
|
| 145 |
+
# Vertex 0 is the apex (camera center), vertices 1-4 are the base corners
|
| 146 |
+
pyramid_vertices = np.vstack((t_wc, world_coords_base))
|
| 147 |
+
if not pyramid_vertices.flags['C_CONTIGUOUS']:
|
| 148 |
+
pyramid_vertices = np.ascontiguousarray(pyramid_vertices, dtype=np.float64)
|
| 149 |
+
elif pyramid_vertices.dtype != np.float64:
|
| 150 |
+
pyramid_vertices = np.asarray(pyramid_vertices, dtype=np.float64)
|
| 151 |
+
|
| 152 |
+
# Define lines for the pyramid: 4 from apex to base, 4 for the base rectangle
|
| 153 |
+
lines = np.array([
|
| 154 |
+
[0, 1], [0, 2], [0, 3], [0, 4], # Apex to base corners
|
| 155 |
+
[1, 2], [2, 3], [3, 4], [4, 1] # Base rectangle
|
| 156 |
+
])
|
| 157 |
+
|
| 158 |
+
if not lines.flags['C_CONTIGUOUS']:
|
| 159 |
+
lines = np.ascontiguousarray(lines, dtype=np.int32)
|
| 160 |
+
elif lines.dtype != np.int32:
|
| 161 |
+
lines = np.asarray(lines, dtype=np.int32)
|
| 162 |
+
|
| 163 |
+
# Create Open3D LineSet object for the camera pyramid
|
| 164 |
+
camera_pyramid_lineset = o3d.geometry.LineSet()
|
| 165 |
+
camera_pyramid_lineset.points = o3d.utility.Vector3dVector(pyramid_vertices)
|
| 166 |
+
camera_pyramid_lineset.lines = o3d.utility.Vector2iVector(lines)
|
| 167 |
+
|
| 168 |
+
# Add the camera visualization to the list of geometries to be rendered
|
| 169 |
+
geometries.append(camera_pyramid_lineset)
|
| 170 |
+
|
| 171 |
+
else:
|
| 172 |
+
geometries = []
|
| 173 |
+
|
| 174 |
+
return pcd, geometries
|
| 175 |
+
|
| 176 |
+
def plot_wireframe_local(
|
| 177 |
+
fig: go.Figure, # This argument is no longer used for plotting by this function.
|
| 178 |
+
vertices: np.ndarray,
|
| 179 |
+
edges: np.ndarray,
|
| 180 |
+
classifications: np.ndarray = None,
|
| 181 |
+
color: str = 'rgb(0, 0, 255)', # Default color for vertices and unclassified/default edges.
|
| 182 |
+
name: Optional[str] = None, # No direct equivalent for Open3D geometry list's name/legend.
|
| 183 |
+
**kwargs) -> list: # Returns a list of o3d.geometry.Geometry objects.
|
| 184 |
+
"""
|
| 185 |
+
Generates Open3D geometries for a wireframe: a PointCloud for vertices
|
| 186 |
+
and a LineSet for edges.
|
| 187 |
+
|
| 188 |
+
Args:
|
| 189 |
+
fig: Plotly figure object (no longer used for plotting by this function).
|
| 190 |
+
vertices: Numpy array of vertex coordinates (N, 3).
|
| 191 |
+
edges: Numpy array of edge connections (M, 2), indices into vertices.
|
| 192 |
+
classifications: Optional numpy array of classifications for edges.
|
| 193 |
+
color: Default color string (e.g., 'rgb(0,0,255)') for vertices and
|
| 194 |
+
for edges if classifications are not provided or don't match.
|
| 195 |
+
name: Optional name (unused in Open3D context here).
|
| 196 |
+
**kwargs: Additional keyword arguments (unused).
|
| 197 |
+
|
| 198 |
+
Returns:
|
| 199 |
+
A list of Open3D geometry objects. Empty if no vertices.
|
| 200 |
+
Note: Plotly-specific 'ps' (point size) and line width are not
|
| 201 |
+
directly translated. These are typically visualizer render options in Open3D.
|
| 202 |
+
"""
|
| 203 |
+
open3d_geometries = []
|
| 204 |
+
|
| 205 |
+
# Ensure gt_vertices is numpy array, C-contiguous, and float64
|
| 206 |
+
# np.asarray avoids a copy if 'vertices' is already a suitable ndarray.
|
| 207 |
+
gt_vertices = np.asarray(vertices)
|
| 208 |
+
if not gt_vertices.flags['C_CONTIGUOUS'] or gt_vertices.dtype != np.float64:
|
| 209 |
+
gt_vertices = np.ascontiguousarray(gt_vertices, dtype=np.float64)
|
| 210 |
+
|
| 211 |
+
# Ensure gt_connections is numpy array, C-contiguous, and int32
|
| 212 |
+
gt_connections = np.asarray(edges)
|
| 213 |
+
if not gt_connections.flags['C_CONTIGUOUS'] or gt_connections.dtype != np.int32:
|
| 214 |
+
gt_connections = np.ascontiguousarray(gt_connections, dtype=np.int32)
|
| 215 |
+
|
| 216 |
+
if gt_vertices.size == 0:
|
| 217 |
+
return open3d_geometries
|
| 218 |
+
|
| 219 |
+
# 1. Create PointCloud for Vertices
|
| 220 |
+
try:
|
| 221 |
+
vertex_rgb_normalized = _plotly_rgb_to_normalized_o3d_color(color)
|
| 222 |
+
except ValueError as e:
|
| 223 |
+
print(f"Warning: Could not parse vertex color '{color}'. Using default black. Error: {e}")
|
| 224 |
+
vertex_rgb_normalized = [0.0, 0.0, 0.0] # Default to black on error
|
| 225 |
+
|
| 226 |
+
vertex_pcd = o3d.geometry.PointCloud()
|
| 227 |
+
# gt_vertices is now C-contiguous and float64
|
| 228 |
+
vertex_pcd.points = o3d.utility.Vector3dVector(gt_vertices)
|
| 229 |
+
|
| 230 |
+
num_vertices = len(gt_vertices)
|
| 231 |
+
if num_vertices > 0:
|
| 232 |
+
# Create vertex_colors_np with correct dtype
|
| 233 |
+
vertex_colors_np = np.array([vertex_rgb_normalized] * num_vertices, dtype=np.float64)
|
| 234 |
+
# Ensure C-contiguity (dtype is already float64 from np.array construction)
|
| 235 |
+
# This check is a safeguard, as np.array from a list of lists with specified dtype is usually contiguous.
|
| 236 |
+
if not vertex_colors_np.flags['C_CONTIGUOUS']:
|
| 237 |
+
vertex_colors_np = np.ascontiguousarray(vertex_colors_np) # Preserves dtype
|
| 238 |
+
vertex_pcd.colors = o3d.utility.Vector3dVector(vertex_colors_np)
|
| 239 |
+
open3d_geometries.append(vertex_pcd)
|
| 240 |
+
|
| 241 |
+
# 2. Create LineSet for Edges
|
| 242 |
+
if gt_connections.size > 0 and num_vertices > 0: # Edges need vertices
|
| 243 |
+
line_set = o3d.geometry.LineSet()
|
| 244 |
+
# gt_vertices is already C-contiguous and float64
|
| 245 |
+
line_set.points = o3d.utility.Vector3dVector(gt_vertices)
|
| 246 |
+
# gt_connections is already C-contiguous and int32
|
| 247 |
+
line_set.lines = o3d.utility.Vector2iVector(gt_connections)
|
| 248 |
+
|
| 249 |
+
line_colors_list_normalized = []
|
| 250 |
+
if classifications is not None and len(classifications) == len(gt_connections):
|
| 251 |
+
# Assuming EDGE_CLASSES_BY_ID and edge_color_mapping are defined in the global scope
|
| 252 |
+
# or imported, as implied by the original code structure.
|
| 253 |
+
for c_idx in classifications:
|
| 254 |
+
try:
|
| 255 |
+
color_tuple_255 = edge_color_mapping[EDGE_CLASSES_BY_ID[c_idx]]
|
| 256 |
+
line_colors_list_normalized.append(_plotly_rgb_to_normalized_o3d_color(color_tuple_255))
|
| 257 |
+
except KeyError: # Handle cases where classification ID or mapping is not found
|
| 258 |
+
print(f"Warning: Classification ID {c_idx} or its mapping not found. Using default color.")
|
| 259 |
+
line_colors_list_normalized.append(vertex_rgb_normalized) # Fallback to default vertex color
|
| 260 |
+
except Exception as e:
|
| 261 |
+
print(f"Warning: Error processing classification color for index {c_idx}. Using default. Error: {e}")
|
| 262 |
+
line_colors_list_normalized.append(vertex_rgb_normalized) # Fallback
|
| 263 |
+
else:
|
| 264 |
+
# Use the default 'color' for all lines if no classifications or mismatch
|
| 265 |
+
default_line_rgb_normalized = vertex_rgb_normalized # Same as vertex color by default
|
| 266 |
+
for _ in range(len(gt_connections)):
|
| 267 |
+
line_colors_list_normalized.append(default_line_rgb_normalized)
|
| 268 |
+
|
| 269 |
+
if line_colors_list_normalized: # Check if list is not empty
|
| 270 |
+
# Create line_colors_np with correct dtype
|
| 271 |
+
line_colors_np = np.array(line_colors_list_normalized, dtype=np.float64)
|
| 272 |
+
# Ensure C-contiguity (dtype is already float64)
|
| 273 |
+
# Safeguard, similar to vertex_colors_np.
|
| 274 |
+
if not line_colors_np.flags['C_CONTIGUOUS']:
|
| 275 |
+
line_colors_np = np.ascontiguousarray(line_colors_np) # Preserves dtype
|
| 276 |
+
line_set.colors = o3d.utility.Vector3dVector(line_colors_np)
|
| 277 |
+
|
| 278 |
+
open3d_geometries.append(line_set)
|
| 279 |
+
|
| 280 |
+
return open3d_geometries
|
| 281 |
+
|
| 282 |
+
def plot_bpo_cameras_from_entry_local(fig: go.Figure, entry: dict, idx = None, camera_scale_factor: float = 1.0):
|
| 283 |
+
def cam2world_to_world2cam(R, t):
|
| 284 |
+
rt = np.eye(4)
|
| 285 |
+
rt[:3,:3] = R
|
| 286 |
+
rt[:3,3] = t.reshape(-1)
|
| 287 |
+
rt = np.linalg.inv(rt)
|
| 288 |
+
return rt[:3,:3], rt[:3,3]
|
| 289 |
+
geometries = []
|
| 290 |
+
for i in range(len(entry['R'])):
|
| 291 |
+
if idx is not None and i != idx:
|
| 292 |
+
continue
|
| 293 |
+
|
| 294 |
+
# Parameters for this camera visualization
|
| 295 |
+
# current_cam_size = 1.0 # Original 'size = 1.' - Replaced by camera_scale_factor
|
| 296 |
+
current_cam_color_str = 'rgb(0, 0, 255)' # Original 'color = 'rgb(0, 0, 255)''
|
| 297 |
+
|
| 298 |
+
# Load camera parameters from entry
|
| 299 |
+
K_matrix = np.array(entry['K'][i])
|
| 300 |
+
R_orig = np.array(entry['R'][i])
|
| 301 |
+
t_orig = np.array(entry['t'][i])
|
| 302 |
+
|
| 303 |
+
# Apply cam2world_to_world2cam transformation as in original snippet
|
| 304 |
+
# This R_transformed, t_transformed will be used to place the camera geometry
|
| 305 |
+
R_transformed, t_transformed = cam2world_to_world2cam(R_orig, t_orig)
|
| 306 |
+
|
| 307 |
+
# Image dimensions from K matrix (cx, cy are K[0,2], K[1,2])
|
| 308 |
+
# Ensure W_img and H_img are derived correctly. Assuming K[0,2] and K[1,2] are principal points cx, cy.
|
| 309 |
+
# If K is [fx, 0, cx; 0, fy, cy; 0, 0, 1], then W_img and H_img might need to come from elsewhere
|
| 310 |
+
# or be estimated if not directly available. The original code used K[0,2]*2, K[1,2]*2.
|
| 311 |
+
# This implies cx = W/2, cy = H/2.
|
| 312 |
+
W_img, H_img = K_matrix[0, 2] * 2, K_matrix[1, 2] * 2
|
| 313 |
+
if W_img <= 0 or H_img <= 0:
|
| 314 |
+
# Attempt to get W, H from cam.width, cam.height if available in entry, like in colmap
|
| 315 |
+
# This part depends on the structure of 'entry'. For now, stick to original logic.
|
| 316 |
+
print(f"Warning: Camera {i} has invalid dimensions (W={W_img}, H={H_img}) based on K. Skipping.")
|
| 317 |
+
continue
|
| 318 |
+
|
| 319 |
+
# Define image plane corners in pixel coordinates (top-left, top-right, bottom-right, bottom-left)
|
| 320 |
+
corners_px = np.array([[0, 0], [W_img, 0], [W_img, H_img], [0, H_img]], dtype=float)
|
| 321 |
+
|
| 322 |
+
# Removed scale_val, image_extent, world_extent calculations.
|
| 323 |
+
# The scaling is now directly controlled by camera_scale_factor.
|
| 324 |
+
|
| 325 |
+
try:
|
| 326 |
+
K_inv = np.linalg.inv(K_matrix)
|
| 327 |
+
except np.linalg.LinAlgError:
|
| 328 |
+
print(f"Warning: K matrix for camera {i} is singular. Skipping this camera.")
|
| 329 |
+
continue
|
| 330 |
+
|
| 331 |
+
# Unproject pixel corners to homogeneous camera coordinates.
|
| 332 |
+
# Assuming to_homogeneous converts (N,2) pixel coords to (N,3) homogeneous coords [px, py, 1].
|
| 333 |
+
# These points are on the z=1 plane in camera coordinates.
|
| 334 |
+
corners_cam_homog = to_homogeneous(corners_px) @ K_inv.T
|
| 335 |
+
|
| 336 |
+
# Scale these points by camera_scale_factor.
|
| 337 |
+
# This makes the frustum base at z=camera_scale_factor in camera coordinates.
|
| 338 |
+
scaled_cam_points = corners_cam_homog * camera_scale_factor
|
| 339 |
+
|
| 340 |
+
# Transform scaled camera points to world coordinates using R_transformed, t_transformed
|
| 341 |
+
world_coords_base = scaled_cam_points @ R_transformed.T + t_transformed
|
| 342 |
+
|
| 343 |
+
# Apex of the pyramid is t_transformed
|
| 344 |
+
apex_world = t_transformed.reshape(1, 3)
|
| 345 |
+
|
| 346 |
+
# Vertices for Open3D LineSet: apex (vertex 0) + 4 base corners (vertices 1-4)
|
| 347 |
+
pyramid_vertices_np = np.vstack((apex_world, world_coords_base))
|
| 348 |
+
if not pyramid_vertices_np.flags['C_CONTIGUOUS'] or pyramid_vertices_np.dtype != np.float64:
|
| 349 |
+
pyramid_vertices_np = np.ascontiguousarray(pyramid_vertices_np, dtype=np.float64)
|
| 350 |
+
|
| 351 |
+
# Lines for the pyramid: 4 from apex to base, 4 for the base rectangle
|
| 352 |
+
lines_np = np.array([
|
| 353 |
+
[0, 1], [0, 2], [0, 3], [0, 4], # Apex to base corners
|
| 354 |
+
[1, 2], [2, 3], [3, 4], [4, 1] # Base rectangle (closed loop)
|
| 355 |
+
], dtype=np.int32)
|
| 356 |
+
|
| 357 |
+
# Create Open3D LineSet object for the camera pyramid
|
| 358 |
+
camera_lineset = o3d.geometry.LineSet()
|
| 359 |
+
camera_lineset.points = o3d.utility.Vector3dVector(pyramid_vertices_np)
|
| 360 |
+
lines_np = np.ascontiguousarray(lines_np, dtype=np.int32)
|
| 361 |
+
camera_lineset.lines = o3d.utility.Vector2iVector(lines_np)
|
| 362 |
+
|
| 363 |
+
# Color the LineSet
|
| 364 |
+
try:
|
| 365 |
+
o3d_color = _plotly_rgb_to_normalized_o3d_color(current_cam_color_str)
|
| 366 |
+
except ValueError as e:
|
| 367 |
+
print(f"Warning: Invalid camera color string '{current_cam_color_str}' for camera {i}. Using default blue. Error: {e}")
|
| 368 |
+
o3d_color = [0.0, 0.0, 1.0] # Default to blue
|
| 369 |
+
camera_lineset.colors = o3d.utility.Vector3dVector(np.array([o3d_color] * len(lines_np), dtype=np.float64))
|
| 370 |
+
|
| 371 |
+
geometries.append(camera_lineset)
|
| 372 |
+
|
| 373 |
+
return geometries
|