import os import shutil import html import logging import numpy as np import gradio as gr # Setup professional logging logging.basicConfig(level=logging.INFO, format="[%(asctime)s] %(levelname)s - %(message)s") logger = logging.getLogger("PaGeR-Showcase") # Setup static folders assets_dir = "assets" os.makedirs(assets_dir, exist_ok=True) # Create fallback simulation room point cloud if none exists def generate_fallback_ply(filepath): """ Generates a beautiful 3D test room shell (walls, floor, ceiling) with a red sphere and green sphere. Approximately 110,000 colored points. """ logger.info(f"Generating fallback point cloud at: {filepath}") points = [] num_points_wall = 15000 # Floor (y = -2.5) for _ in range(num_points_wall): x = np.random.uniform(-5, 5) z = np.random.uniform(-5, 5) y = -2.5 r = int((x + 5) * 25.5) g = 45 b = int((z + 5) * 25.5) points.append(f"{x} {y} {z} {r} {g} {b}") # Ceiling (y = 2.5) for _ in range(num_points_wall): x = np.random.uniform(-5, 5) z = np.random.uniform(-5, 5) y = 2.5 r = 45 g = int((x + 5) * 25.5) b = int((z + 5) * 25.5) points.append(f"{x} {y} {z} {r} {g} {b}") # 4 Walls (x = -5, x = 5, z = -5, z = 5) for _ in range(num_points_wall * 4): wall = np.random.choice([0, 1, 2, 3]) y = np.random.uniform(-2.5, 2.5) if wall == 0: # back wall z = -5 x = np.random.uniform(-5, 5) z = -5 r = 100 g = int((y + 2.5) * 51) b = int((x + 5) * 25.5) elif wall == 1: # front wall z = 5 x = np.random.uniform(-5, 5) z = 5 r = int((x + 5) * 25.5) g = 100 b = int((y + 2.5) * 51) elif wall == 2: # left wall x = -5 x = -5 z = np.random.uniform(-5, 5) r = int((z + 5) * 25.5) g = int((y + 2.5) * 51) b = 100 else: # right wall x = 5 x = 5 z = np.random.uniform(-5, 5) r = int((y + 2.5) * 51) g = int((z + 5) * 25.5) b = 150 points.append(f"{x} {y} {z} {r} {g} {b}") # Decorative Interior Sphere 1 (Red) for _ in range(20000): u = np.random.random() v = np.random.random() theta = u * 2.0 * np.pi phi = np.arccos(2.0 * v - 1.0) r = 1.0 x = 1.8 + r * np.sin(phi) * np.cos(theta) y = -1.2 + r * np.sin(phi) * np.sin(theta) z = 1.8 + r * np.cos(phi) points.append(f"{x} {y} {z} 239 68 68") # Decorative Interior Sphere 2 (Green) for _ in range(20000): u = np.random.random() v = np.random.random() theta = u * 2.0 * np.pi phi = np.arccos(2.0 * v - 1.0) r = 1.2 x = -2.2 + r * np.sin(phi) * np.cos(theta) y = -0.8 + r * np.sin(phi) * np.sin(theta) z = -2.2 + r * np.cos(phi) points.append(f"{x} {y} {z} 34 197 94") # Construct ASCII PLY format header header = f"""ply format ascii 1.0 element vertex {len(points)} property float x property float y property float z property uchar red property uchar green property uchar blue end_header """ with open(filepath, "w") as f: f.write(header + "\n".join(points)) logger.info(f"Fallback point cloud generated successfully with {len(points)} points.") # Pre-generate preview1 if missing preview_ply = os.path.join(assets_dir, "preview1.ply") if not os.path.exists(preview_ply): generate_fallback_ply(preview_ply) def get_ply_files(): """Scans the assets directory and returns a sorted list of PLY point cloud files.""" files = [f for f in os.listdir(assets_dir) if f.endswith(".ply")] logger.info(f"Scanning 'assets/' folder. Found PLY files: {files}") if not files: return [] return sorted([os.path.join(assets_dir, f) for f in files]) def get_viewer_html(ply_url): """Generates the embedded HTML template for the ThreeJS WebGL view.""" return f""" 3D Point Cloud Viewer
Loading 3D Point Cloud...
0%

Navigation

Drag Mouse Look Around
W, A, S, D Fly / Move Ground
SPACE / E Move UP
SHIFT / Q Move DOWN

Settings

Points: - FPS: 0
""" def update_viewer_iframe(selected_file): """ Constructs a responsive iframe utilizing a secure inline HTML source. Also handles route mappings based on the Gradio major version. """ if not selected_file: logger.warning("No file selected in update_viewer_iframe!") return "No file selected." # Map to the absolute path on the host, correcting cross-platform path separators abs_ply_path = os.path.abspath(selected_file).replace("\\", "/") # Check the Gradio version to decide correct file route mapping gradio_major_version = int(gr.__version__.split(".")[0]) if gradio_major_version >= 5: # Gradio 5 and 6 require /gradio_api/ prefix for custom assets ply_url = f"/gradio_api/file={abs_ply_path}" else: # Legacy fallback ply_url = f"/file={abs_ply_path}" logger.info(f"Target file: '{selected_file}'") logger.info(f"Resolved path: '{abs_ply_path}'") logger.info(f"Gradio Major Version: {gradio_major_version}. Generated URL mapping: '{ply_url}'") raw_html = get_viewer_html(ply_url) escaped_html = html.escape(raw_html) return f'' def handle_file_upload(file_obj): """Saves user-uploaded .ply files to assets/ and selects them.""" if file_obj is None: return gr.update() dest_filename = os.path.basename(file_obj.name) dest_path = os.path.join(assets_dir, dest_filename) logger.info(f"User uploaded file. Storing to: {dest_path}") shutil.copy(file_obj.name, dest_path) updated_files = get_ply_files() return gr.update(choices=updated_files, value=dest_path) def refresh_file_list(): """Manual scan update for files placed into assets/ via git or CLI.""" logger.info("Manual assets/ directory scan triggered.") updated_files = get_ply_files() default_val = updated_files[0] if updated_files else None return gr.update(choices=updated_files, value=default_val) # Handle theme configuration based on Gradio version theme = gr.themes.Default( primary_hue="blue", neutral_hue="slate", ).set( body_background_fill="*neutral_950", block_background_fill="*neutral_900", block_border_color="*neutral_800", button_secondary_background_fill="*neutral_800" ) gradio_major_version = int(gr.__version__.split(".")[0]) logger.info(f"Starting application layout. Detected Gradio Major: {gradio_major_version}") blocks_kwargs = {} launch_kwargs = { "allowed_paths": [os.path.abspath(assets_dir)] } # Resolve the Gradio 6 theme warning by adjusting kwarg targets if gradio_major_version >= 6: launch_kwargs["theme"] = theme else: blocks_kwargs["theme"] = theme with gr.Blocks(title="PaGeR Point Cloud Showcase", **blocks_kwargs) as demo: gr.Markdown( """ # 🪐 Panoramic Point Cloud Viewer Load and navigate large panoramic point cloud files (up to **1,000,000+ points**). The camera places you directly in the **middle POV** on load, rendering optimal spatial depth. """ ) with gr.Row(): with gr.Column(scale=3): file_dropdown = gr.Dropdown( choices=get_ply_files(), value=get_ply_files()[0] if get_ply_files() else None, label="📁 Select Point Cloud File", interactive=True, ) with gr.Column(scale=1): refresh_btn = gr.Button("🔄 Scan assets/", size="sm") with gr.Column(scale=2): upload_zone = gr.File( label="📤 Drag & Drop New .PLY File Here", file_types=[".ply"], type="filepath", height=80 ) # Core interactive WebGL viewport component viewer_component = gr.HTML( value=update_viewer_iframe(file_dropdown.value) if file_dropdown.value else "" ) # Event routing file_dropdown.change( fn=update_viewer_iframe, inputs=file_dropdown, outputs=viewer_component ) upload_zone.upload( fn=handle_file_upload, inputs=upload_zone, outputs=file_dropdown ) refresh_btn.click( fn=refresh_file_list, inputs=[], outputs=file_dropdown ) # Static asset paths for custom resource streaming gr.set_static_paths(paths=[assets_dir]) if __name__ == "__main__": logger.info(f"Launching Gradio with arguments: {launch_kwargs}") demo.launch(**launch_kwargs)