Spaces:
Running
Running
| #!/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` | |