| |
| |
| |
| |
| |
| from datasets import load_dataset |
| from hoho2025.vis import plot_all_modalities |
| from hoho2025.viz3d import * |
| import pycolmap |
| import tempfile,zipfile |
| import io |
| import open3d as o3d |
| from PIL import Image, ImageDraw |
|
|
| def _plotly_rgb_to_normalized_o3d_color(color_val) -> list[float]: |
| """ |
| Converts Plotly-style color (str 'rgb(r,g,b)' or tuple (r,g,b)) |
| to normalized [r/255, g/255, b/255] for Open3D. |
| """ |
| if isinstance(color_val, str): |
| if color_val.startswith('rgba'): |
| parts = color_val[5:-1].split(',') |
| return [int(p.strip())/255.0 for p in parts[:3]] |
| elif color_val.startswith('rgb'): |
| parts = color_val[4:-1].split(',') |
| return [int(p.strip())/255.0 for p in parts] |
| else: |
| |
| |
| |
| raise ValueError(f"Unsupported color string format for Open3D conversion: {color_val}. Expected 'rgb(...)' or 'rgba(...)'.") |
| elif isinstance(color_val, (list, tuple)) and len(color_val) == 3: |
| |
| return [c/255.0 for c in color_val] |
| raise ValueError(f"Unsupported color type for Open3D conversion: {type(color_val)}. Expected string or 3-element tuple/list.") |
|
|
| def draw_crosses_on_image(image_pil, vertices_data, output_file_path, size=5, color=(0, 0, 0)): |
| """ |
| Draws crosses on a PIL Image at specified vertex locations and saves it. |
| Args: |
| image_pil (PIL.Image.Image): The image to draw on. |
| vertices_data (list): List of dictionaries, each with an 'xy' key |
| holding [x, y] coordinates. |
| output_file_path (str): Path to save the modified image. |
| size (int): Size of the cross arms. |
| color (tuple): RGB color for the cross. |
| """ |
| |
| img_to_draw_on = image_pil.copy() |
| drawer = ImageDraw.Draw(img_to_draw_on) |
|
|
| for vert_info in vertices_data: |
| if 'xy' in vert_info: |
| x, y = vert_info['xy'] |
| |
| x_int, y_int = int(round(x)), int(round(y)) |
| |
| |
| drawer.line([(x_int - size, y_int), (x_int + size, y_int)], fill=color, width=1) |
| |
| drawer.line([(x_int, y_int - size), (x_int, y_int + size)], fill=color, width=1) |
| |
| img_to_draw_on.save(output_file_path) |
|
|
| def save_gestalt_with_proj(gest_seg_np, gt_verts, img_id): |
| |
| |
| if gest_seg_np.ndim == 2: |
| img_to_draw_on = Image.fromarray(gest_seg_np, mode='L') |
| elif gest_seg_np.ndim == 3 and gest_seg_np.shape[2] == 3: |
| img_to_draw_on = Image.fromarray(gest_seg_np, mode='RGB') |
| else: |
| |
| |
| img_to_draw_on = Image.fromarray(gest_seg_np.astype(np.uint8)) |
|
|
| |
| if img_to_draw_on.mode == 'L': |
| img_to_draw_on = img_to_draw_on.convert('RGB') |
| |
| draw = ImageDraw.Draw(img_to_draw_on) |
| cross_size = 5 |
| cross_color = (0, 0, 0) |
|
|
| for vert_dict in gt_verts: |
| x, y = vert_dict['xy'] |
| |
| draw.line([(x - cross_size, y), (x + cross_size, y)], fill=cross_color, width=1) |
| |
| draw.line([(x, y - cross_size), (x, y + cross_size)], fill=cross_color, width=1) |
| |
| |
| |
| img_to_draw_on.save(f'gestalt_cross/{img_id}.png') |
| |
| def plot_reconstruction_local( |
| fig: go.Figure, |
| rec: pycolmap.Reconstruction, |
| color: str = 'rgb(0, 0, 255)', |
| name: Optional[str] = None, |
| points: bool = True, |
| cameras: bool = True, |
| cs: float = 1.0, |
| single_color_points=False, |
| camera_color='rgba(0, 255, 0, 0.5)', |
| crop_outliers: bool = False): |
| |
| |
| xyzs = [] |
| rgbs = [] |
| |
| for k, p3D in rec.points3D.items(): |
| |
| xyzs.append(p3D.xyz) |
| rgbs.append(p3D.color) |
| |
| xyzs = np.array(xyzs) |
| rgbs = np.array(rgbs) |
| |
| |
| if crop_outliers and len(xyzs) > 0: |
| |
| distances = np.linalg.norm(xyzs, axis=1) |
| |
| threshold = np.percentile(distances, 98) |
| |
| mask = distances <= threshold |
| xyzs = xyzs[mask] |
| rgbs = rgbs[mask] |
| print(f"Cropped outliers: removed {np.sum(~mask)} out of {len(mask)} points ({np.sum(~mask)/len(mask)*100:.2f}%)") |
|
|
| if points and len(xyzs) > 0: |
| pcd = o3d.geometry.PointCloud() |
| pcd.points = o3d.utility.Vector3dVector(xyzs) |
| |
| |
| |
| if rgbs.size > 0: |
| normalized_rgbs = rgbs / 255.0 |
| pcd.colors = o3d.utility.Vector3dVector(normalized_rgbs) |
| |
| |
| |
|
|
| |
| |
| |
| |
| |
|
|
| if cameras: |
| try: |
| |
| cam_color_normalized = _plotly_rgb_to_normalized_o3d_color(camera_color) |
| except ValueError as e: |
| print(f"Warning: Invalid camera_color '{camera_color}'. Using default green. Error: {e}") |
| cam_color_normalized = [0.0, 1.0, 0.0] |
|
|
| geometries = [] |
|
|
| for image_id, image_data in rec.images.items(): |
| |
| cam = rec.cameras[image_data.camera_id] |
| K = cam.calibration_matrix() |
|
|
| |
| if K[0, 0] > 5000 or K[1, 1] > 5000: |
| print(f"Skipping camera for image {image_id} due to large focal length (fx={K[0,0]}, fy={K[1,1]}).") |
| continue |
|
|
| |
| |
| T_world_cam = image_data.cam_from_world.inverse() |
| R_wc = T_world_cam.rotation.matrix() |
| t_wc = T_world_cam.translation |
|
|
| W, H = float(cam.width), float(cam.height) |
| |
| |
| if W <= 0 or H <= 0: |
| print(f"Skipping camera for image {image_id} due to invalid dimensions (W={W}, H={H}).") |
| continue |
|
|
| |
| img_corners_px = np.array([ |
| [0, 0], [W, 0], [W, H], [0, H] |
| ], dtype=float) |
|
|
| |
| img_corners_h = np.hstack([img_corners_px, np.ones((4, 1))]) |
|
|
| try: |
| K_inv = np.linalg.inv(K) |
| except np.linalg.LinAlgError: |
| print(f"Skipping camera for image {image_id} due to non-invertible K matrix.") |
| continue |
| |
| |
| cam_coords_norm = (K_inv @ img_corners_h.T).T |
| |
| |
| |
| cam_coords_scaled = cam_coords_norm * cs |
|
|
| |
| world_coords_base = (R_wc @ cam_coords_scaled.T).T + t_wc |
|
|
| |
| |
| pyramid_vertices = np.vstack((t_wc, world_coords_base)) |
| if not pyramid_vertices.flags['C_CONTIGUOUS']: |
| pyramid_vertices = np.ascontiguousarray(pyramid_vertices, dtype=np.float64) |
| elif pyramid_vertices.dtype != np.float64: |
| pyramid_vertices = np.asarray(pyramid_vertices, dtype=np.float64) |
|
|
| |
| lines = np.array([ |
| [0, 1], [0, 2], [0, 3], [0, 4], |
| [1, 2], [2, 3], [3, 4], [4, 1] |
| ]) |
|
|
| if not lines.flags['C_CONTIGUOUS']: |
| lines = np.ascontiguousarray(lines, dtype=np.int32) |
| elif lines.dtype != np.int32: |
| lines = np.asarray(lines, dtype=np.int32) |
|
|
| |
| camera_pyramid_lineset = o3d.geometry.LineSet() |
| camera_pyramid_lineset.points = o3d.utility.Vector3dVector(pyramid_vertices) |
| camera_pyramid_lineset.lines = o3d.utility.Vector2iVector(lines) |
| |
| |
| geometries.append(camera_pyramid_lineset) |
|
|
| else: |
| geometries = [] |
|
|
| return pcd, geometries |
|
|
| def plot_wireframe_local( |
| fig: go.Figure, |
| vertices: np.ndarray, |
| edges: np.ndarray, |
| classifications: np.ndarray = None, |
| color: str = 'rgb(0, 0, 255)', |
| name: Optional[str] = None, |
| **kwargs) -> list: |
| """ |
| Generates Open3D geometries for a wireframe: a PointCloud for vertices |
| and a LineSet for edges. |
| |
| Args: |
| fig: Plotly figure object (no longer used for plotting by this function). |
| vertices: Numpy array of vertex coordinates (N, 3). |
| edges: Numpy array of edge connections (M, 2), indices into vertices. |
| classifications: Optional numpy array of classifications for edges. |
| color: Default color string (e.g., 'rgb(0,0,255)') for vertices and |
| for edges if classifications are not provided or don't match. |
| name: Optional name (unused in Open3D context here). |
| **kwargs: Additional keyword arguments (unused). |
| |
| Returns: |
| A list of Open3D geometry objects. Empty if no vertices. |
| Note: Plotly-specific 'ps' (point size) and line width are not |
| directly translated. These are typically visualizer render options in Open3D. |
| """ |
| open3d_geometries = [] |
| |
| |
| |
| gt_vertices = np.asarray(vertices) |
| if not gt_vertices.flags['C_CONTIGUOUS'] or gt_vertices.dtype != np.float64: |
| gt_vertices = np.ascontiguousarray(gt_vertices, dtype=np.float64) |
|
|
| |
| gt_connections = np.asarray(edges) |
| if not gt_connections.flags['C_CONTIGUOUS'] or gt_connections.dtype != np.int32: |
| gt_connections = np.ascontiguousarray(gt_connections, dtype=np.int32) |
|
|
| if gt_vertices.size == 0: |
| return open3d_geometries |
|
|
| |
| try: |
| vertex_rgb_normalized = _plotly_rgb_to_normalized_o3d_color(color) |
| except ValueError as e: |
| print(f"Warning: Could not parse vertex color '{color}'. Using default black. Error: {e}") |
| vertex_rgb_normalized = [0.0, 0.0, 0.0] |
|
|
| vertex_pcd = o3d.geometry.PointCloud() |
| |
| vertex_pcd.points = o3d.utility.Vector3dVector(gt_vertices) |
| |
| num_vertices = len(gt_vertices) |
| if num_vertices > 0: |
| |
| vertex_colors_np = np.array([vertex_rgb_normalized] * num_vertices, dtype=np.float64) |
| |
| |
| if not vertex_colors_np.flags['C_CONTIGUOUS']: |
| vertex_colors_np = np.ascontiguousarray(vertex_colors_np) |
| vertex_pcd.colors = o3d.utility.Vector3dVector(vertex_colors_np) |
| open3d_geometries.append(vertex_pcd) |
|
|
| |
| if gt_connections.size > 0 and num_vertices > 0: |
| line_set = o3d.geometry.LineSet() |
| |
| line_set.points = o3d.utility.Vector3dVector(gt_vertices) |
| |
| line_set.lines = o3d.utility.Vector2iVector(gt_connections) |
|
|
| line_colors_list_normalized = [] |
| if classifications is not None and len(classifications) == len(gt_connections): |
| |
| |
| for c_idx in classifications: |
| try: |
| color_tuple_255 = edge_color_mapping[EDGE_CLASSES_BY_ID[c_idx]] |
| line_colors_list_normalized.append(_plotly_rgb_to_normalized_o3d_color(color_tuple_255)) |
| except KeyError: |
| print(f"Warning: Classification ID {c_idx} or its mapping not found. Using default color.") |
| line_colors_list_normalized.append(vertex_rgb_normalized) |
| except Exception as e: |
| print(f"Warning: Error processing classification color for index {c_idx}. Using default. Error: {e}") |
| line_colors_list_normalized.append(vertex_rgb_normalized) |
| else: |
| |
| default_line_rgb_normalized = vertex_rgb_normalized |
| for _ in range(len(gt_connections)): |
| line_colors_list_normalized.append(default_line_rgb_normalized) |
| |
| if line_colors_list_normalized: |
| |
| line_colors_np = np.array(line_colors_list_normalized, dtype=np.float64) |
| |
| |
| if not line_colors_np.flags['C_CONTIGUOUS']: |
| line_colors_np = np.ascontiguousarray(line_colors_np) |
| line_set.colors = o3d.utility.Vector3dVector(line_colors_np) |
| |
| open3d_geometries.append(line_set) |
| |
| return open3d_geometries |
| |
| def plot_bpo_cameras_from_entry_local(fig: go.Figure, entry: dict, idx = None, camera_scale_factor: float = 1.0): |
| def cam2world_to_world2cam(R, t): |
| rt = np.eye(4) |
| rt[:3,:3] = R |
| rt[:3,3] = t.reshape(-1) |
| rt = np.linalg.inv(rt) |
| return rt[:3,:3], rt[:3,3] |
| geometries = [] |
| for i in range(len(entry['R'])): |
| if idx is not None and i != idx: |
| continue |
| |
| |
| |
| current_cam_color_str = 'rgb(0, 0, 255)' |
|
|
| |
| K_matrix = np.array(entry['K'][i]) |
| R_orig = np.array(entry['R'][i]) |
| t_orig = np.array(entry['t'][i]) |
|
|
| |
| |
| R_transformed, t_transformed = cam2world_to_world2cam(R_orig, t_orig) |
| |
| |
| |
| |
| |
| |
| W_img, H_img = K_matrix[0, 2] * 2, K_matrix[1, 2] * 2 |
| if W_img <= 0 or H_img <= 0: |
| |
| |
| print(f"Warning: Camera {i} has invalid dimensions (W={W_img}, H={H_img}) based on K. Skipping.") |
| continue |
|
|
| |
| corners_px = np.array([[0, 0], [W_img, 0], [W_img, H_img], [0, H_img]], dtype=float) |
|
|
| |
| |
| |
| try: |
| K_inv = np.linalg.inv(K_matrix) |
| except np.linalg.LinAlgError: |
| print(f"Warning: K matrix for camera {i} is singular. Skipping this camera.") |
| continue |
| |
| |
| |
| |
| corners_cam_homog = to_homogeneous(corners_px) @ K_inv.T |
| |
| |
| |
| scaled_cam_points = corners_cam_homog * camera_scale_factor |
|
|
| |
| world_coords_base = scaled_cam_points @ R_transformed.T + t_transformed |
|
|
| |
| apex_world = t_transformed.reshape(1, 3) |
|
|
| |
| pyramid_vertices_np = np.vstack((apex_world, world_coords_base)) |
| if not pyramid_vertices_np.flags['C_CONTIGUOUS'] or pyramid_vertices_np.dtype != np.float64: |
| pyramid_vertices_np = np.ascontiguousarray(pyramid_vertices_np, dtype=np.float64) |
|
|
| |
| lines_np = np.array([ |
| [0, 1], [0, 2], [0, 3], [0, 4], |
| [1, 2], [2, 3], [3, 4], [4, 1] |
| ], dtype=np.int32) |
|
|
| |
| camera_lineset = o3d.geometry.LineSet() |
| camera_lineset.points = o3d.utility.Vector3dVector(pyramid_vertices_np) |
| lines_np = np.ascontiguousarray(lines_np, dtype=np.int32) |
| camera_lineset.lines = o3d.utility.Vector2iVector(lines_np) |
|
|
| |
| try: |
| o3d_color = _plotly_rgb_to_normalized_o3d_color(current_cam_color_str) |
| except ValueError as e: |
| print(f"Warning: Invalid camera color string '{current_cam_color_str}' for camera {i}. Using default blue. Error: {e}") |
| o3d_color = [0.0, 0.0, 1.0] |
| camera_lineset.colors = o3d.utility.Vector3dVector(np.array([o3d_color] * len(lines_np), dtype=np.float64)) |
| |
| geometries.append(camera_lineset) |
|
|
| return geometries |