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` | |