Contxt_Fetcher / app.py
AbdelChoufani's picture
Add converted Gradio app
3141721
#!/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`