""" Advanced 3D Reconstruction from Single Images with Responsible AI Features """ import gradio as gr import numpy as np import torch from PIL import Image from transformers import GLPNForDepthEstimation, GLPNImageProcessor import open3d as o3d import plotly.graph_objects as go import matplotlib.pyplot as plt import io import json import time from pathlib import Path import tempfile import zipfile import hashlib from datetime import datetime # ============================================================================ # RESPONSIBLE AI GUIDELINES # ============================================================================ RESPONSIBLE_AI_NOTICE = """ ## ⚠️ Responsible Use Guidelines ### Privacy & Consent - **Do not upload images containing identifiable people without their explicit consent** - **Do not use for surveillance, tracking, or monitoring individuals** - Facial features may be reconstructed in 3D - consider privacy implications - Remove metadata (EXIF) that may contain location or personal information ### Ethical Use - This tool is for **educational, research, and creative purposes only** - **Prohibited uses:** - Creating deepfakes or misleading 3D content - Unauthorized documentation of private property - Circumventing security systems - Generating 3D models for harassment or stalking - Commercial use without proper rights to source images ### Limitations & Bias - Models trained primarily on indoor Western architecture - May perform poorly on non-Western architectural styles - Scale is relative, not absolute - not suitable for precision measurements - Single viewpoint limitations - occluded areas are inferred, not captured ### Data Usage - Images are processed locally during your session - No images are stored or transmitted to external servers - Processing logs contain only technical metrics, no image content - You retain all rights to your uploaded images and generated 3D models **By using this tool, you agree to these responsible use guidelines.** """ # ============================================================================ # PRIVACY & SAFETY FUNCTIONS # ============================================================================ def check_image_safety(image): """Basic safety checks for uploaded images""" warnings = [] width, height = image.size if width * height > 10_000_000: warnings.append("⚠️ Very large image - consider resizing to improve processing speed") aspect_ratio = max(width, height) / min(width, height) if aspect_ratio > 3: warnings.append("⚠️ Unusual aspect ratio detected - ensure image doesn't contain unintended content") try: exif = image.getexif() if exif: has_gps = any(k for k in exif.keys() if k in [34853, 0x8825]) if has_gps: warnings.append("⚠️ GPS location data detected in image - consider removing EXIF data for privacy") except: pass return True, "\n".join(warnings) if warnings else None def generate_session_id(): """Generate anonymous session ID for logging""" return hashlib.sha256(str(datetime.now()).encode()).hexdigest()[:16] def content_policy_check(image): """Check if image content violates usage policies""" width, height = image.size if width < 100 or height < 100: return False, "Image too small - minimum 100x100 pixels required for meaningful reconstruction" return True, None # ============================================================================ # MODEL LOADING # ============================================================================ print("Loading GLPN model (lightweight)...") try: glpn_processor = GLPNImageProcessor.from_pretrained("vinvino02/glpn-nyu") glpn_model = GLPNForDepthEstimation.from_pretrained("vinvino02/glpn-nyu") print("✓ GLPN model loaded successfully!") except Exception as e: print(f"Error loading model: {e}") glpn_processor = None glpn_model = None # DPT will be loaded on demand dpt_model = None dpt_processor = None # ============================================================================ # CORE 3D RECONSTRUCTION # ============================================================================ def process_image(image, model_choice="GLPN (Recommended)", visualization_type="mesh"): """Optimized processing pipeline""" def _generate_quality_assessment(metrics): assessment = [] outlier_pct = (metrics['outliers_removed'] / metrics['initial_points']) * 100 if outlier_pct < 5: assessment.append("Very clean depth estimation") elif outlier_pct < 15: assessment.append("Good depth quality") else: assessment.append("High noise in depth estimation") if metrics['is_edge_manifold'] and metrics['is_vertex_manifold']: assessment.append("Excellent topology") elif metrics['is_vertex_manifold']: assessment.append("Good local topology") else: assessment.append("Topology issues present") if metrics['is_watertight']: assessment.append("Watertight mesh - ready for 3D printing!") else: assessment.append("Not watertight - needs repair for 3D printing") return "\n".join(f"- {item}" for item in assessment) if glpn_model is None: return None, None, None, "❌ Model failed to load. Please refresh the page.", None try: print("Starting reconstruction...") # Preprocess new_height = 480 if image.height > 480 else image.height new_height -= (new_height % 32) new_width = int(new_height * image.width / image.height) diff = new_width % 32 new_width = new_width - diff if diff < 16 else new_width + (32 - diff) new_size = (new_width, new_height) image = image.resize(new_size, Image.LANCZOS) # Depth estimation - select model if model_choice == "GLPN (Recommended)": processor = glpn_processor model = glpn_model else: # DPT (High Quality) global dpt_model, dpt_processor if dpt_model is None: print("Loading DPT model (first time only)...") from transformers import DPTForDepthEstimation, DPTImageProcessor dpt_processor = DPTImageProcessor.from_pretrained("Intel/dpt-large") dpt_model = DPTForDepthEstimation.from_pretrained("Intel/dpt-large") print("✓ DPT model loaded!") processor = dpt_processor model = dpt_model inputs = processor(images=image, return_tensors="pt") start_time = time.time() with torch.no_grad(): outputs = model(**inputs) predicted_depth = outputs.predicted_depth depth_time = time.time() - start_time # Process depth pad = 16 output = predicted_depth.squeeze().cpu().numpy() * 1000.0 output = output[pad:-pad, pad:-pad] image_cropped = image.crop((pad, pad, image.width - pad, image.height - pad)) depth_height, depth_width = output.shape img_width, img_height = image_cropped.size if depth_height != img_height or depth_width != img_width: from scipy import ndimage zoom_factors = (img_height / depth_height, img_width / depth_width) output = ndimage.zoom(output, zoom_factors, order=1) image = image_cropped # Depth visualization fig, ax = plt.subplots(1, 2, figsize=(14, 7)) ax[0].imshow(image) ax[0].set_title('Original Image', fontsize=14, fontweight='bold') ax[0].axis('off') im = ax[1].imshow(output, cmap='plasma') ax[1].set_title('Estimated Depth Map', fontsize=14, fontweight='bold') ax[1].axis('off') plt.colorbar(im, ax=ax[1], fraction=0.046, pad=0.04) plt.tight_layout() buf = io.BytesIO() plt.savefig(buf, format='png', dpi=150, bbox_inches='tight') buf.seek(0) depth_viz = Image.open(buf) plt.close() # Point cloud generation width, height = image.size if output.shape != (height, width): from scipy import ndimage zoom_factors = (height / output.shape[0], width / output.shape[1]) output = ndimage.zoom(output, zoom_factors, order=1) depth_image = (output * 255 / np.max(output)).astype(np.uint8) image_array = np.array(image) depth_o3d = o3d.geometry.Image(depth_image) image_o3d = o3d.geometry.Image(image_array) rgbd_image = o3d.geometry.RGBDImage.create_from_color_and_depth( image_o3d, depth_o3d, convert_rgb_to_intensity=False ) camera_intrinsic = o3d.camera.PinholeCameraIntrinsic() camera_intrinsic.set_intrinsics(width, height, 500, 500, width/2, height/2) pcd = o3d.geometry.PointCloud.create_from_rgbd_image(rgbd_image, camera_intrinsic) initial_points = len(pcd.points) # Clean point cloud cl, ind = pcd.remove_statistical_outlier(nb_neighbors=20, std_ratio=2.0) pcd = pcd.select_by_index(ind) outliers_removed = initial_points - len(pcd.points) # Estimate normals pcd.estimate_normals() pcd.orient_normals_to_align_with_direction() # Create mesh mesh_start = time.time() mesh = o3d.geometry.TriangleMesh.create_from_point_cloud_poisson( pcd, depth=9, n_threads=1 )[0] # Transfer colors pcd_tree = o3d.geometry.KDTreeFlann(pcd) mesh_colors = [] for vertex in mesh.vertices: [_, idx, _] = pcd_tree.search_knn_vector_3d(vertex, 1) mesh_colors.append(pcd.colors[idx[0]]) mesh.vertex_colors = o3d.utility.Vector3dVector(np.array(mesh_colors)) rotation = mesh.get_rotation_matrix_from_xyz((np.pi, 0, 0)) mesh.rotate(rotation, center=(0, 0, 0)) mesh_time = time.time() - mesh_start # Metrics mesh.compute_vertex_normals() metrics = { 'model_used': model_choice, 'depth_estimation_time': f"{depth_time:.2f}s", 'mesh_reconstruction_time': f"{mesh_time:.2f}s", 'total_time': f"{depth_time + mesh_time:.2f}s", 'initial_points': initial_points, 'outliers_removed': outliers_removed, 'final_points': len(pcd.points), 'vertices': len(mesh.vertices), 'triangles': len(mesh.triangles), 'is_edge_manifold': mesh.is_edge_manifold(), 'is_vertex_manifold': mesh.is_vertex_manifold(), 'is_watertight': mesh.is_watertight(), } # Surface area try: surface_area = mesh.get_surface_area() if surface_area > 0: metrics['surface_area'] = float(surface_area) else: vertices = np.asarray(mesh.vertices) triangles = np.asarray(mesh.triangles) v0 = vertices[triangles[:, 0]] v1 = vertices[triangles[:, 1]] v2 = vertices[triangles[:, 2]] cross = np.cross(v1 - v0, v2 - v0) areas = 0.5 * np.linalg.norm(cross, axis=1) metrics['surface_area'] = float(np.sum(areas)) except: metrics['surface_area'] = "Unable to compute" # Volume try: if mesh.is_watertight(): metrics['volume'] = float(mesh.get_volume()) else: metrics['volume'] = None except: metrics['volume'] = None # 3D visualization points = np.asarray(pcd.points) colors = np.asarray(pcd.colors) if visualization_type == "point_cloud": scatter = go.Scatter3d( x=points[:, 0], y=points[:, 1], z=points[:, 2], mode='markers', marker=dict( size=2, color=['rgb({},{},{})'.format(int(r*255), int(g*255), int(b*255)) for r, g, b in colors], ), name='Point Cloud' ) plotly_fig = go.Figure(data=[scatter]) plotly_fig.update_layout( scene=dict( xaxis=dict(visible=False), yaxis=dict(visible=False), zaxis=dict(visible=False), aspectmode='data' ), height=700, title="Point Cloud" ) else: # mesh vertices = np.asarray(mesh.vertices) triangles = np.asarray(mesh.triangles) if mesh.has_vertex_colors(): vertex_colors = np.asarray(mesh.vertex_colors) colors_rgb = ['rgb({},{},{})'.format(int(r*255), int(g*255), int(b*255)) for r, g, b in vertex_colors] mesh_trace = go.Mesh3d( x=vertices[:, 0], y=vertices[:, 1], z=vertices[:, 2], i=triangles[:, 0], j=triangles[:, 1], k=triangles[:, 2], vertexcolor=colors_rgb, opacity=0.95 ) else: mesh_trace = go.Mesh3d( x=vertices[:, 0], y=vertices[:, 1], z=vertices[:, 2], i=triangles[:, 0], j=triangles[:, 1], k=triangles[:, 2], color='lightblue', opacity=0.9 ) plotly_fig = go.Figure(data=[mesh_trace]) plotly_fig.update_layout( scene=dict( xaxis=dict(visible=False), yaxis=dict(visible=False), zaxis=dict(visible=False), aspectmode='data' ), height=700, title="3D Mesh" ) # Export files temp_dir = tempfile.mkdtemp() pcd_path = Path(temp_dir) / "point_cloud.ply" o3d.io.write_point_cloud(str(pcd_path), pcd) mesh_path = Path(temp_dir) / "mesh.ply" o3d.io.write_triangle_mesh(str(mesh_path), mesh) mesh_obj_path = Path(temp_dir) / "mesh.obj" o3d.io.write_triangle_mesh(str(mesh_obj_path), mesh) mesh_stl_path = Path(temp_dir) / "mesh.stl" o3d.io.write_triangle_mesh(str(mesh_stl_path), mesh) metrics_path = Path(temp_dir) / "metrics.json" with open(metrics_path, 'w') as f: json.dump(metrics, f, indent=2, default=str) zip_path = Path(temp_dir) / "reconstruction_complete.zip" with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf: zipf.write(pcd_path, pcd_path.name) zipf.write(mesh_path, mesh_path.name) zipf.write(mesh_obj_path, mesh_obj_path.name) zipf.write(mesh_stl_path, mesh_stl_path.name) zipf.write(metrics_path, metrics_path.name) assessment = _generate_quality_assessment(metrics) report = f""" ## Reconstruction Complete! ### Performance - **Processing Time**: {metrics['total_time']} - **Points**: {metrics['final_points']:,} - **Triangles**: {metrics['triangles']:,} ### Quality - **Topology**: {'Good' if metrics['is_vertex_manifold'] else 'Issues'} - **Watertight**: {'Yes' if metrics['is_watertight'] else 'No'} ### Assessment {assessment} **Download the complete package below!** """ return depth_viz, plotly_fig, str(zip_path), report, json.dumps(metrics, indent=2, default=str) except Exception as e: import traceback return None, None, None, f"Error: {str(e)}\n\n{traceback.format_exc()}", None def process_image_with_safeguards(image, model_choice="GLPN (Recommended)", visualization_type="mesh", consent_given=False): """Main processing with safeguards""" session_id = generate_session_id() if not consent_given: return None, None, None, "**You must agree to the Responsible Use Guidelines first.**", None if image is None: return None, None, None, "Please upload an image first.", None is_safe, safety_warning = check_image_safety(image) passes_policy, policy_message = content_policy_check(image) if not passes_policy: return None, None, None, f"{policy_message}", None try: result = process_image(image, model_choice, visualization_type) depth_viz, plotly_fig, zip_path, report, json_metrics = result if safety_warning: report = f"**Privacy Notice:**\n{safety_warning}\n\n{report}" metrics = json.loads(json_metrics) metrics['responsible_ai'] = { 'session_id': session_id, 'timestamp': datetime.now().isoformat(), 'consent_given': True } return depth_viz, plotly_fig, zip_path, report, json.dumps(metrics, indent=2) except Exception as e: return None, None, None, f"Error: {str(e)}", None # ============================================================================ # GRADIO INTERFACE # ============================================================================ with gr.Blocks(title="Responsible AI 3D Reconstruction", theme=gr.themes.Soft()) as demo: gr.Markdown(""" # 🏗️ 3D Reconstruction from Single Images Transform 2D photographs into 3D spatial models
This tool must be used ethically and legally. Review the guidelines in the first tab.