#!/usr/bin/env python # coding: utf-8 # # šŸŒ OSM 3D Environment Generator - Gradio Web App # # **Created for easy 3D city modeling from OpenStreetMap data** # # This interactive web application allows you to: # - āœ… Enter latitude and longitude coordinates # - āœ… Specify search radius for buildings # - āœ… Generate 3D models from real map data # - āœ… Download GLB files for use in 3D software # - āœ… View models directly in the browser # # **Perfect for architects, urban planners, game developers, and 3D enthusiasts!** import gradio as gr import requests import pyproj import shapely.geometry as sg import trimesh import numpy as np import json import os import re import tempfile import shutil from typing import Tuple, List, Dict, Optional import time # OSM Overpass API URL OVERPASS_URL = "https://overpass-api.de/api/interpreter" def latlon_to_utm(lat: float, lon: float) -> Tuple[float, float]: """Convert WGS84 (lat/lon in degrees) to UTM (meters).""" proj = pyproj.Proj(proj="utm", zone=int((lon + 180) / 6) + 1, ellps="WGS84") x, y = proj(lon, lat) # Note: pyproj uses (lon, lat) order return x, y def fetch_osm_data(lat: float, lon: float, radius: int = 500) -> Optional[Dict]: """Fetch OSM data for buildings within a given radius of a coordinate.""" query = f""" [out:json]; ( way(around:{radius},{lat},{lon})[building]; ); out body; >; out skel qt; """ try: response = requests.get(OVERPASS_URL, params={"data": query}, timeout=30) if response.status_code == 200: data = response.json() return data else: return None except Exception as e: print(f"Error fetching OSM data: {e}") return None def parse_osm_data(osm_data: Dict) -> List[Dict]: """Extract building footprints and heights from OSM data.""" buildings = [] nodes = {} # Store node locations for element in osm_data["elements"]: if element["type"] == "node": lon, lat = element["lon"], element["lat"] x, y = latlon_to_utm(lat, lon) nodes[element["id"]] = (x, y) # Extract building footprints for element in osm_data["elements"]: if element["type"] == "way": if "tags" in element and "building" in element["tags"]: try: # Get height from tags height_str = element["tags"].get("height", "10") if isinstance(height_str, str): height_match = re.search(r'(\d+\.?\d*)', height_str) if height_match: height = float(height_match.group(1)) else: height = 10.0 else: height = float(height_str) footprint = [nodes[node_id] for node_id in element["nodes"] if node_id in nodes] if len(footprint) >= 3: if footprint[0] != footprint[-1]: footprint.append(footprint[0]) buildings.append({"footprint": footprint, "height": height}) except Exception as e: continue return buildings def create_3d_model(buildings: List[Dict]) -> trimesh.Scene: """Create a 3D model using trimesh with PROPER ORIENTATION FIX.""" scene = trimesh.Scene() for building in buildings: footprint = building["footprint"] height = building.get("height", 10) if height <= 0: continue try: polygon = sg.Polygon(footprint) if not polygon.is_valid: polygon = polygon.buffer(0) if not polygon.is_valid: continue except Exception: continue try: # Try triangle engine first, then earcut try: extruded = trimesh.creation.extrude_polygon(polygon, height, engine="triangle") except ValueError: try: extruded = trimesh.creation.extrude_polygon(polygon, height, engine="earcut") except ValueError: continue # āœ… PROPER ORIENTATION FIX - This is the solution you provided # This rotates the model so the front view shows properly transform_x = trimesh.transformations.rotation_matrix(np.pi/2, (1, 0, 0)) # Also rotate around Z-axis for proper left-right orientation transform_z = trimesh.transformations.rotation_matrix(np.pi, (0, 0, 1)) # Apply the transformations extruded.apply_transform(transform_x) extruded.apply_transform(transform_z) # Add to scene scene.add_geometry(extruded) except Exception: continue return scene def save_3d_model(scene: trimesh.Scene, filename: str) -> bool: """Export the 3D scene to a GLB file.""" try: scene.export(filename) return os.path.exists(filename) except Exception: return False def generate_3d_model(latitude: float, longitude: float, radius: int) -> Tuple[str, str, str]: """Main function to generate 3D model from coordinates.""" # Validate inputs if not (-90 <= latitude <= 90): return None, "āŒ Error: Latitude must be between -90 and 90", "" if not (-180 <= longitude <= 180): return None, "āŒ Error: Longitude must be between -180 and 180", "" if not (10 <= radius <= 2000): return None, "āŒ Error: Radius must be between 10 and 2000 meters", "" try: # Step 1: Fetch OSM data status_msg = f"šŸ” Fetching OSM data for coordinates: {latitude}, {longitude} with radius: {radius}m..." print(status_msg) osm_data = fetch_osm_data(latitude, longitude, radius) if not osm_data: return None, "āŒ Failed to fetch OSM data. Please check coordinates and try again.", "" # Step 2: Parse buildings status_msg += f"\nāœ… OSM data fetched successfully\nšŸ—ļø Parsing building data..." buildings = parse_osm_data(osm_data) if not buildings: return None, "āŒ No buildings found in this area. Try a different location or larger radius.", "" # Step 3: Create 3D model status_msg += f"\nāœ… Found {len(buildings)} buildings\nšŸ  Creating 3D model..." scene = create_3d_model(buildings) if len(scene.geometry) == 0: return None, "āŒ Could not create 3D model from the buildings found.", "" # Step 4: Save model timestamp = int(time.time()) filename = f"osm_3d_model_{timestamp}.glb" status_msg += f"\nāœ… 3D model created with {len(scene.geometry)} buildings\nšŸ’¾ Saving model..." if save_3d_model(scene, filename): file_size = os.path.getsize(filename) final_msg = f"\nāœ… SUCCESS! 3D model saved as {filename}\nšŸ“ File size: {file_size:,} bytes ({file_size/1024:.1f} KB)\nšŸŽ‰ Ready for download!" status_msg += final_msg # Create summary info summary = f"""šŸŒ **Location**: {latitude}, {longitude} šŸ“ **Radius**: {radius} meters šŸ¢ **Buildings Found**: {len(buildings)} šŸ”§ **3D Geometries Created**: {len(scene.geometry)} šŸ“ **File Size**: {file_size/1024:.1f} KB ā° **Generated**: {time.strftime('%Y-%m-%d %H:%M:%S')}""" return filename, status_msg, summary else: return None, "āŒ Failed to save 3D model file.", "" except Exception as e: return None, f"āŒ Unexpected error: {str(e)}", "" # Create Gradio interface def create_gradio_app(): """Create and configure the Gradio interface.""" with gr.Blocks(title="šŸŒ OSM 3D Generator", theme=gr.themes.Soft()) as app: # Header gr.Markdown(""" # šŸŒ OSM 3D Environment Generator **Transform real-world locations into 3D models!** Enter coordinates and radius to generate 3D building models from OpenStreetMap data. Perfect for architecture, urban planning, game development, and 3D visualization. """) with gr.Row(): with gr.Column(scale=1): # Input section gr.Markdown("## šŸ“ Location Settings") latitude = gr.Number( label="🌐 Latitude", value=40.748817, # Empire State Building precision=6, info="Enter latitude (-90 to 90)" ) longitude = gr.Number( label="🌐 Longitude", value=-73.985428, # Empire State Building precision=6, info="Enter longitude (-180 to 180)" ) radius = gr.Slider( label="šŸ“ Search Radius (meters)", minimum=10, maximum=2000, value=500, step=10, info="Larger radius = more buildings but slower processing" ) generate_btn = gr.Button( "šŸš€ Generate 3D Model", variant="primary", size="lg" ) # Examples gr.Markdown("### šŸŒ† Quick Examples") gr.Examples( examples=[ [40.748817, -73.985428, 500], # Empire State Building, NYC [48.858844, 2.294351, 300], # Eiffel Tower, Paris [51.500729, -0.124625, 400], # Big Ben, London [35.676098, 139.650311, 600], # Tokyo Station [37.819929, -122.478255, 350], # Golden Gate Bridge area ], inputs=[latitude, longitude, radius], label="Click to load famous locations" ) with gr.Column(scale=1): # Output section gr.Markdown("## šŸ“„ Generated Model") file_output = gr.File( label="šŸ“ Download 3D Model (.glb)", file_types=[".glb"], visible=False ) status_output = gr.Textbox( label="šŸ“Š Generation Status", lines=8, max_lines=15, placeholder="Click 'Generate 3D Model' to start...", interactive=False ) summary_output = gr.Markdown( "### šŸ“‹ Model Summary\nGeneration results will appear here..." ) # Info section with gr.Row(): gr.Markdown(""" ### šŸ’” Tips for Best Results - **Urban areas** work best (more buildings = better models) - **Start with 300-500m radius** for good balance of detail and speed - **Large cities** like NYC, Paris, Tokyo have excellent building data - **Rural areas** may have fewer or no buildings - **Generated .glb files** can be opened in Blender, Three.js, or online 3D viewers ### šŸ› ļø Technical Details - Uses **OpenStreetMap** data via Overpass API - Creates **proper 3D building heights** when available - Applies **correct orientation** for front-view display - Exports as **GLB format** (compatible with most 3D software) - **Processing time** varies by area complexity (typically 10-60 seconds) """) # Event handler def handle_generation(lat, lon, rad): """Handle the generation process and update UI.""" file_path, status, summary = generate_3d_model(lat, lon, rad) if file_path: return ( gr.update(value=file_path, visible=True), # file_output status, # status_output f"### šŸ“‹ Model Summary\n{summary}" # summary_output ) else: return ( gr.update(visible=False), # file_output status, # status_output "### āŒ Generation Failed\nPlease check the status above and try again." # summary_output ) # Connect the button generate_btn.click( fn=handle_generation, inputs=[latitude, longitude, radius], outputs=[file_output, status_output, summary_output] ) return app # Create and launch the app app = create_gradio_app() # Launch the app if __name__ == "__main__": app.launch( share=True, # Creates public link for Hugging Face server_name="0.0.0.0", # Allow external connections server_port=7860, # Standard port for Hugging Face show_error=True, debug=True ) else: # For Hugging Face Spaces app.launch() # ## šŸš€ Deployment Instructions for Hugging Face Spaces # # To deploy this app on Hugging Face Spaces: # # 1. **Create a new Space** on [Hugging Face](https://huggingface.co/spaces) # 2. **Select "Gradio" as the Space SDK** # 3. **Upload this notebook** or copy the code to `app.py` # 4. **Add requirements.txt** with these dependencies: # ``` # gradio # requests # pyproj # shapely # trimesh # numpy # ``` # 5. **Commit and push** - your app will automatically deploy! # # ### šŸ“ Alternative: Direct Python File # You can also copy all the Python code cells into a single `app.py` file for easier deployment. # # ### šŸ”§ Environment Variables (Optional) # For production deployment, consider adding: # - `GRADIO_SERVER_NAME=0.0.0.0` # - `GRADIO_SERVER_PORT=7860`