# fixed_animated_pointcloud.py import gradio as gr import cv2 import numpy as np import plotly.graph_objects as go import plotly.io as pio import json import os from datetime import datetime def create_animated_point_cloud( video, resolution: int = 256, density: float = 0.25, depth: float = 0.5, point_size: float = 3.0, max_frames: int = 10, frame_step: int = 5, progress=gr.Progress() ): if video is None: return None, "Upload a short video first", None, None cap = cv2.VideoCapture(video) if not cap.isOpened(): return None, "Cannot open video", None, None total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) max_possible = min(max_frames, (total_frames // frame_step) + 1) progress(0, desc="Reading video...") all_points = [] processed = 0 for i in range(0, total_frames, frame_step): if len(all_points) >= max_frames: break cap.set(cv2.CAP_PROP_POS_FRAMES, i) ret, frame = cap.read() if not ret: break small = cv2.resize(frame, (resolution, resolution)) gray = cv2.cvtColor(small, cv2.COLOR_BGR2GRAY).astype(np.float32) / 255.0 rgb = cv2.cvtColor(small, cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0 mask = np.random.rand(*gray.shape) < density ys, xs = np.nonzero(mask) zs = (gray[mask] - 0.5) * depth * 8 xs_norm = (xs / resolution - 0.5) * 12 ys_norm = (0.5 - ys / resolution) * 12 colors = rgb[mask].tolist() all_points.append({ 'x': xs_norm.tolist(), 'y': ys_norm.tolist(), 'z': zs.tolist(), 'color': colors }) processed += 1 progress(processed / max_possible, desc=f"Extracted {processed}/{max_possible} frames...") cap.release() if not all_points: return None, "No frames processed", None, None # ====================== BUILD FIGURE ====================== initial = all_points[0] initial_colors = [f"rgb({int(255*r)},{int(255*g)},{int(255*b)})" for r,g,b in initial['color']] trace = go.Scatter3d( x=initial['x'], y=initial['y'], z=initial['z'], mode='markers', marker=dict( size=point_size, color=initial_colors, opacity=0.85 ) ) fig = go.Figure(data=[trace]) fig.update_layout( scene=dict( aspectmode='cube', xaxis_title='X', yaxis_title='Y', zaxis_title='Depth (brightness)', camera=dict(eye=dict(x=0, y=0, z=2.8), up=dict(x=0, y=1, z=0), center=dict(x=0, y=0, z=0)) ), title=f"Animated Point Cloud — {len(all_points)} frames (camera-persistent)", height=650, margin=dict(l=0, r=0, b=0, t=90), ) # Data for JS: convert colors into Plotly color strings points_data = [] for pts in all_points: rgb_colors = [f"rgb({int(255*r)},{int(255*g)},{int(255*b)})" for r,g,b in pts['color']] points_data.append({'x': pts['x'], 'y': pts['y'], 'z': pts['z'], 'color': rgb_colors}) # Unique filename timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") html_path = f"animated_pointcloud_{timestamp}.html" # Create the HTML using Plotly CDN html_str = pio.to_html( fig, include_plotlyjs='cdn', full_html=True, default_width="100%", default_height="650px" ) # Robust loop + overlay script (now also controls point size) overlay_id = f"pc_ctrl_{timestamp}" loop_script = f""" """ html_str = html_str.replace('', loop_script + '') with open(html_path, 'w', encoding='utf-8') as f: f.write(html_str) # Iframe src logic (Hugging Face Spaces vs local) if os.environ.get("SPACE_ID"): space_url = f"https://{os.environ['SPACE_ID']}.hf.space" iframe_src = f"{space_url}/file={html_path}" else: iframe_src = f"/file={html_path}" iframe_html = f''' ''' # Static preview (first frame) with chosen point size preview_fig = go.Figure(data=[trace]) preview_fig.update_layout(scene=fig.layout.scene, height=500) status = f"✅ Done! {len(all_points)} frames • Point size: {point_size} • Download: {html_path}" return preview_fig, status, iframe_html, html_path # ====================== GRADIO INTERFACE ====================== demo = gr.Interface( fn=create_animated_point_cloud, inputs=[ gr.Video(label="Upload Video (short = faster)"), gr.Slider(128, 1024, value=256, step=64, label="Resolution"), gr.Slider(0.05, 0.5, value=0.25, step=0.05, label="Point Density"), gr.Slider(0.2, 1.5, value=0.5, step=0.1, label="Depth Intensity"), gr.Slider(0.2, 12, value=3.0, step=0.2, label="Point Size"), gr.Slider(4, 65, value=10, step=1, label="Max Frames"), gr.Slider(2, 10, value=5, step=1, label="Frame Step (higher = faster)") ], outputs=[ gr.Plot(label="Static Preview (first frame)"), gr.Textbox(label="Status"), gr.HTML(label="🎥 Live Interactive 3D Animation (Top-down + Looping)"), gr.File(label="↓ Download HTML (offline use)") ], title="Video → Interactive Animated 3D Point Cloud (camera-persistent)", description="Progress bar shows processing • Full 3D animation appears directly on the page • Live point size & FPS control in animation", flagging_mode="never" ) if __name__ == "__main__": demo.launch()