| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | """ |
| | Visualization utilities for Panoptic Recon 3D model outputs. |
| | |
| | This module provides functions for: |
| | - 2D segmentation visualization |
| | - Depth map visualization |
| | - 3D mesh extraction and PLY export |
| | """ |
| |
|
| | from pathlib import Path |
| | from typing import Optional, Tuple, Union |
| |
|
| | import numpy as np |
| |
|
| | |
| | try: |
| | import matplotlib.pyplot as plt |
| | import matplotlib.patches as mpatches |
| | HAS_MATPLOTLIB = True |
| | except ImportError: |
| | HAS_MATPLOTLIB = False |
| |
|
| | try: |
| | from PIL import Image |
| | HAS_PIL = True |
| | except ImportError: |
| | HAS_PIL = False |
| |
|
| | try: |
| | from skimage import measure |
| | HAS_SKIMAGE = True |
| | except ImportError: |
| | HAS_SKIMAGE = False |
| |
|
| | try: |
| | from scipy.spatial import KDTree |
| | HAS_SCIPY = True |
| | except ImportError: |
| | HAS_SCIPY = False |
| |
|
| |
|
| | def create_color_palette() -> np.ndarray: |
| | """Create Front3D color palette for semantic classes. |
| | |
| | Returns: |
| | Color palette as numpy array (N, 3) with uint8 RGB values. |
| | """ |
| | return np.array([ |
| | (0, 0, 0), |
| | (174, 199, 232), |
| | (152, 223, 138), |
| | (31, 119, 180), |
| | (255, 187, 120), |
| | (188, 189, 34), |
| | (140, 86, 75), |
| | (255, 152, 150), |
| | (214, 39, 40), |
| | (197, 176, 213), |
| | (148, 103, 189), |
| | (196, 156, 148), |
| | (23, 190, 207), |
| | (178, 76, 76), |
| | (247, 182, 210), |
| | (66, 188, 102), |
| | (219, 219, 141), |
| | (140, 57, 197), |
| | (202, 185, 52), |
| | (51, 176, 203), |
| | (200, 54, 131), |
| | (92, 193, 61), |
| | (78, 71, 183), |
| | (172, 114, 82), |
| | (255, 127, 14), |
| | (91, 163, 138), |
| | (153, 98, 156), |
| | (140, 153, 101), |
| | (158, 218, 229), |
| | (100, 125, 154), |
| | (178, 127, 135), |
| | (120, 185, 128), |
| | (146, 111, 194), |
| | (44, 160, 44), |
| | (112, 128, 144), |
| | (96, 207, 209), |
| | (227, 119, 194), |
| | (213, 92, 176), |
| | (94, 106, 211), |
| | (82, 84, 163), |
| | (100, 85, 144), |
| | (172, 172, 172), |
| | ], dtype=np.uint8) |
| |
|
| |
|
| | def colorize_segmentation( |
| | segmentation: np.ndarray, |
| | palette: Optional[np.ndarray] = None, |
| | ) -> np.ndarray: |
| | """Colorize segmentation map. |
| | |
| | Args: |
| | segmentation: Segmentation map (H, W) with class indices. |
| | palette: Color palette (N, 3). Uses default if None. |
| | |
| | Returns: |
| | Colorized image (H, W, 3) as uint8. |
| | """ |
| | if palette is None: |
| | palette = create_color_palette() |
| | |
| | |
| | seg_clipped = np.clip(segmentation, 0, len(palette) - 1) |
| | return palette[seg_clipped] |
| |
|
| |
|
| | def visualize_2d_segmentation( |
| | image: np.ndarray, |
| | panoptic_2d: np.ndarray, |
| | output_path: Optional[Union[str, Path]] = None, |
| | alpha: float = 0.6, |
| | figsize: Tuple[int, int] = (18, 6), |
| | dpi: int = 150, |
| | ) -> Optional[np.ndarray]: |
| | """Visualize 2D panoptic segmentation overlaid on image. |
| | |
| | Args: |
| | image: Original RGB image (H, W, C). |
| | panoptic_2d: Panoptic segmentation map (H, W). |
| | output_path: Path to save visualization. If None, returns array. |
| | alpha: Blend alpha for overlay. |
| | figsize: Figure size. |
| | dpi: DPI for saved figure. |
| | |
| | Returns: |
| | Overlay image as numpy array if output_path is None. |
| | """ |
| | if not HAS_MATPLOTLIB: |
| | raise ImportError("matplotlib required for visualization") |
| | if not HAS_PIL: |
| | raise ImportError("PIL required for visualization") |
| | |
| | |
| | palette = create_color_palette() |
| | colored_seg = colorize_segmentation(panoptic_2d, palette) |
| | |
| | |
| | if image.shape[:2] != panoptic_2d.shape: |
| | image_pil = Image.fromarray(image) |
| | image_pil = image_pil.resize((panoptic_2d.shape[1], panoptic_2d.shape[0]), Image.LANCZOS) |
| | image = np.array(image_pil) |
| | |
| | |
| | overlay = (image.astype(np.float32) * (1 - alpha) + colored_seg.astype(np.float32) * alpha) |
| | overlay = overlay.clip(0, 255).astype(np.uint8) |
| | |
| | if output_path is None: |
| | return overlay |
| | |
| | |
| | fig, axes = plt.subplots(1, 3, figsize=figsize) |
| | |
| | axes[0].imshow(image) |
| | axes[0].set_title('Original Image', fontsize=14, fontweight='bold') |
| | axes[0].axis('off') |
| | |
| | axes[1].imshow(colored_seg) |
| | axes[1].set_title('Panoptic Segmentation', fontsize=14, fontweight='bold') |
| | axes[1].axis('off') |
| | |
| | axes[2].imshow(overlay) |
| | axes[2].set_title('Overlay', fontsize=14, fontweight='bold') |
| | axes[2].axis('off') |
| | |
| | plt.tight_layout() |
| | plt.savefig(output_path, dpi=dpi, bbox_inches='tight') |
| | plt.close() |
| | |
| | print(f"✓ Saved 2D segmentation visualization to: {output_path}") |
| | return None |
| |
|
| |
|
| | def visualize_depth_map( |
| | depth_2d: np.ndarray, |
| | output_path: Optional[Union[str, Path]] = None, |
| | vmin: float = 0.0, |
| | vmax: float = 6.0, |
| | cmap: str = 'viridis', |
| | figsize: Tuple[int, int] = (10, 8), |
| | dpi: int = 150, |
| | ) -> Optional[np.ndarray]: |
| | """Visualize depth map. |
| | |
| | Args: |
| | depth_2d: Depth map (H, W). |
| | output_path: Path to save visualization. If None, returns array. |
| | vmin: Minimum depth for colormap. |
| | vmax: Maximum depth for colormap. |
| | cmap: Matplotlib colormap name. |
| | figsize: Figure size. |
| | dpi: DPI for saved figure. |
| | |
| | Returns: |
| | Colorized depth as numpy array if output_path is None. |
| | """ |
| | if not HAS_MATPLOTLIB: |
| | raise ImportError("matplotlib required for visualization") |
| | |
| | |
| | depth_norm = (depth_2d - vmin) / (vmax - vmin) |
| | depth_norm = np.clip(depth_norm, 0, 1) |
| | |
| | |
| | cm = plt.get_cmap(cmap) |
| | depth_colored = (cm(depth_norm)[:, :, :3] * 255).astype(np.uint8) |
| | |
| | if output_path is None: |
| | return depth_colored |
| | |
| | fig, ax = plt.subplots(1, 1, figsize=figsize) |
| | |
| | im = ax.imshow(depth_2d, cmap=cmap, vmin=vmin, vmax=vmax) |
| | ax.set_title('Depth Map', fontsize=14, fontweight='bold') |
| | ax.axis('off') |
| | |
| | cbar = plt.colorbar(im, ax=ax, fraction=0.046, pad=0.04) |
| | cbar.set_label('Depth (m)', rotation=270, labelpad=20, fontsize=12) |
| | |
| | plt.tight_layout() |
| | plt.savefig(output_path, dpi=dpi, bbox_inches='tight') |
| | plt.close() |
| | |
| | print(f"✓ Saved depth map visualization to: {output_path}") |
| | return None |
| |
|
| |
|
| | def get_mesh( |
| | distance_field: np.ndarray, |
| | iso_value: float = 1.0, |
| | spacing: Tuple[float, float, float] = (1.0, 1.0, 1.0), |
| | ) -> Tuple[np.ndarray, np.ndarray]: |
| | """Extract mesh from distance field using marching cubes. |
| | |
| | Args: |
| | distance_field: 3D distance field (D, H, W). |
| | iso_value: Iso-surface value. |
| | spacing: Voxel spacing. |
| | |
| | Returns: |
| | vertices: Mesh vertices (N, 3). |
| | faces: Mesh faces (M, 3). |
| | """ |
| | if not HAS_SKIMAGE: |
| | raise ImportError("scikit-image required for mesh extraction") |
| | |
| | vertices, faces, _, _ = measure.marching_cubes( |
| | distance_field, |
| | level=iso_value, |
| | spacing=spacing |
| | ) |
| | return vertices, faces |
| |
|
| |
|
| | def write_ply( |
| | vertices: np.ndarray, |
| | output_file: Union[str, Path], |
| | colors: Optional[np.ndarray] = None, |
| | faces: Optional[np.ndarray] = None, |
| | ) -> None: |
| | """Write PLY file. |
| | |
| | Args: |
| | vertices: Vertex positions (N, 3). |
| | output_file: Output PLY file path. |
| | colors: Optional vertex colors (N, 3) as uint8. |
| | faces: Optional face indices (M, 3). |
| | """ |
| | with open(output_file, "w") as f: |
| | f.write("ply\n") |
| | f.write("format ascii 1.0\n") |
| | f.write(f"element vertex {len(vertices)}\n") |
| | f.write("property float x\n") |
| | f.write("property float y\n") |
| | f.write("property float z\n") |
| | |
| | if colors is not None: |
| | f.write("property uchar red\n") |
| | f.write("property uchar green\n") |
| | f.write("property uchar blue\n") |
| | |
| | if faces is not None and len(faces) > 0: |
| | f.write(f"element face {len(faces)}\n") |
| | f.write("property list uchar uint vertex_indices\n") |
| | |
| | f.write("end_header\n") |
| | |
| | |
| | if colors is not None: |
| | for v, c in zip(vertices, colors): |
| | f.write(f"{v[0]} {v[1]} {v[2]} {int(c[0])} {int(c[1])} {int(c[2])}\n") |
| | else: |
| | for v in vertices: |
| | f.write(f"{v[0]} {v[1]} {v[2]}\n") |
| | |
| | |
| | if faces is not None: |
| | for face in faces: |
| | f.write(f"3 {face[0]} {face[1]} {face[2]}\n") |
| |
|
| |
|
| | def save_3d_mesh( |
| | geometry_3d: np.ndarray, |
| | semantic_3d: np.ndarray, |
| | output_path: Union[str, Path], |
| | iso_value: float = 1.0, |
| | voxel_size: float = 0.03, |
| | ) -> bool: |
| | """Extract and save 3D mesh with semantic colors. |
| | |
| | Args: |
| | geometry_3d: 3D geometry/TSDF (D, H, W). |
| | semantic_3d: 3D semantic segmentation (D, H, W). |
| | output_path: Output PLY file path. |
| | iso_value: Iso-surface value for mesh extraction. |
| | voxel_size: Voxel size in meters. |
| | |
| | Returns: |
| | True if successful, False otherwise. |
| | """ |
| | if not HAS_SKIMAGE: |
| | print("Warning: scikit-image not installed. Cannot save PLY mesh.") |
| | return False |
| | if not HAS_SCIPY: |
| | print("Warning: scipy not installed. Cannot color mesh by semantics.") |
| | |
| | try: |
| | |
| | vertices, faces = get_mesh( |
| | geometry_3d, |
| | iso_value=iso_value, |
| | spacing=(voxel_size, voxel_size, voxel_size) |
| | ) |
| | |
| | colors = None |
| | if HAS_SCIPY and np.any(semantic_3d): |
| | |
| | nonzero_coords = np.stack(semantic_3d.nonzero(), axis=-1) |
| | |
| | if len(nonzero_coords) > 0: |
| | |
| | labels_kd = KDTree(nonzero_coords) |
| | palette = create_color_palette() |
| | |
| | |
| | semantic_clipped = np.clip(semantic_3d, 0, len(palette) - 1).astype(np.uint32) |
| | color_volume = palette[semantic_clipped] |
| | |
| | |
| | |
| | vertex_indices = (vertices / voxel_size).astype(int) |
| | neighbor_inds = labels_kd.query(vertex_indices)[1] |
| | neighbors = labels_kd.data[neighbor_inds].astype(int) |
| | |
| | |
| | neighbors = np.clip(neighbors, 0, np.array(color_volume.shape[:3]) - 1) |
| | colors = color_volume[neighbors[:, 0], neighbors[:, 1], neighbors[:, 2]] |
| | |
| | |
| | write_ply(vertices, output_path, colors, faces) |
| | print(f"✓ Saved 3D mesh to: {output_path}") |
| | print(f" Vertices: {len(vertices)}, Faces: {len(faces)}") |
| | return True |
| | |
| | except Exception as e: |
| | print(f"Warning: Failed to save 3D mesh: {e}") |
| | return False |
| |
|
| |
|
| | def save_outputs( |
| | outputs, |
| | output_dir: Union[str, Path], |
| | original_image: Optional[np.ndarray] = None, |
| | save_mesh: bool = True, |
| | save_depth: bool = True, |
| | save_segmentation: bool = True, |
| | save_numpy: bool = True, |
| | ) -> dict: |
| | """Save all model outputs to directory. |
| | |
| | Args: |
| | outputs: PanopticRecon3DOutput from model. |
| | output_dir: Output directory. |
| | original_image: Optional original input image for visualization. |
| | save_mesh: Whether to save 3D mesh PLY files. |
| | save_depth: Whether to save depth visualization. |
| | save_segmentation: Whether to save segmentation visualization. |
| | save_numpy: Whether to save raw numpy arrays. |
| | |
| | Returns: |
| | Dictionary of saved file paths. |
| | """ |
| | output_dir = Path(output_dir) |
| | output_dir.mkdir(parents=True, exist_ok=True) |
| | |
| | saved_files = {} |
| | |
| | |
| | outputs_np = outputs.to_numpy() |
| | |
| | |
| | if save_numpy: |
| | for name, arr in outputs_np.items(): |
| | npy_path = output_dir / f"{name}.npy" |
| | np.save(npy_path, arr) |
| | saved_files[f"{name}_npy"] = str(npy_path) |
| | |
| | |
| | if save_segmentation and original_image is not None: |
| | seg_path = output_dir / "panoptic_2d_visualization.png" |
| | visualize_2d_segmentation( |
| | original_image, |
| | outputs_np["panoptic_seg_2d"], |
| | seg_path |
| | ) |
| | saved_files["segmentation_vis"] = str(seg_path) |
| | |
| | |
| | if save_depth: |
| | depth_path = output_dir / "depth_visualization.png" |
| | visualize_depth_map( |
| | outputs_np["depth_2d"], |
| | depth_path |
| | ) |
| | saved_files["depth_vis"] = str(depth_path) |
| | |
| | |
| | if save_mesh: |
| | |
| | semantic_mesh_path = output_dir / "mesh_semantic.ply" |
| | if save_3d_mesh( |
| | outputs_np["geometry_3d"], |
| | outputs_np["semantic_seg_3d"], |
| | semantic_mesh_path |
| | ): |
| | saved_files["semantic_mesh"] = str(semantic_mesh_path) |
| | |
| | |
| | panoptic_mesh_path = output_dir / "mesh_panoptic.ply" |
| | if save_3d_mesh( |
| | outputs_np["geometry_3d"], |
| | outputs_np["panoptic_seg_3d"], |
| | panoptic_mesh_path |
| | ): |
| | saved_files["panoptic_mesh"] = str(panoptic_mesh_path) |
| | |
| | return saved_files |
| |
|
| |
|