AbdelChoufani commited on
Commit
d40687b
Β·
1 Parent(s): 285ffdc

Add cleaned-up app.py and requirements.txt

Browse files
Files changed (2) hide show
  1. app.py +418 -0
  2. requirements.txt +6 -0
app.py ADDED
@@ -0,0 +1,418 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ # coding: utf-8
3
+
4
+ # # 🌍 OSM 3D Environment Generator - Gradio Web App
5
+ #
6
+ # **Created for easy 3D city modeling from OpenStreetMap data**
7
+ #
8
+ # This interactive web application allows you to:
9
+ # - βœ… Enter latitude and longitude coordinates
10
+ # - βœ… Specify search radius for buildings
11
+ # - βœ… Generate 3D models from real map data
12
+ # - βœ… Download GLB files for use in 3D software
13
+ # - βœ… View models directly in the browser
14
+ #
15
+ # **Perfect for architects, urban planners, game developers, and 3D enthusiasts!**
16
+
17
+
18
+
19
+
20
+
21
+
22
+
23
+
24
+
25
+ import gradio as gr
26
+ import requests
27
+ import pyproj
28
+ import shapely.geometry as sg
29
+ import trimesh
30
+ import numpy as np
31
+ import json
32
+ import os
33
+ import re
34
+ import tempfile
35
+ import shutil
36
+ from typing import Tuple, List, Dict, Optional
37
+ import time
38
+
39
+
40
+
41
+
42
+ # OSM Overpass API URL
43
+ OVERPASS_URL = "http://overpass-api.de/api/interpreter"
44
+
45
+ def latlon_to_utm(lat: float, lon: float) -> Tuple[float, float]:
46
+ """Convert WGS84 (lat/lon in degrees) to UTM (meters)."""
47
+ proj = pyproj.Proj(proj="utm", zone=int((lon + 180) / 6) + 1, ellps="WGS84")
48
+ x, y = proj(lon, lat) # Note: pyproj uses (lon, lat) order
49
+ return x, y
50
+
51
+ def fetch_osm_data(lat: float, lon: float, radius: int = 500) -> Optional[Dict]:
52
+ """Fetch OSM data for buildings within a given radius of a coordinate."""
53
+ query = f"""
54
+ [out:json];
55
+ (
56
+ way(around:{radius},{lat},{lon})[building];
57
+ );
58
+ out body;
59
+ >;
60
+ out skel qt;
61
+ """
62
+
63
+ try:
64
+ response = requests.get(OVERPASS_URL, params={"data": query}, timeout=30)
65
+ if response.status_code == 200:
66
+ data = response.json()
67
+ return data
68
+ else:
69
+ return None
70
+ except Exception as e:
71
+ print(f"Error fetching OSM data: {e}")
72
+ return None
73
+
74
+ def parse_osm_data(osm_data: Dict) -> List[Dict]:
75
+ """Extract building footprints and heights from OSM data."""
76
+ buildings = []
77
+ nodes = {}
78
+
79
+ # Store node locations
80
+ for element in osm_data["elements"]:
81
+ if element["type"] == "node":
82
+ lon, lat = element["lon"], element["lat"]
83
+ x, y = latlon_to_utm(lat, lon)
84
+ nodes[element["id"]] = (x, y)
85
+
86
+ # Extract building footprints
87
+ for element in osm_data["elements"]:
88
+ if element["type"] == "way":
89
+ if "tags" in element and "building" in element["tags"]:
90
+ try:
91
+ # Get height from tags
92
+ height_str = element["tags"].get("height", "10")
93
+ if isinstance(height_str, str):
94
+ height_match = re.search(r'(\d+\.?\d*)', height_str)
95
+ if height_match:
96
+ height = float(height_match.group(1))
97
+ else:
98
+ height = 10.0
99
+ else:
100
+ height = float(height_str)
101
+
102
+ footprint = [nodes[node_id] for node_id in element["nodes"] if node_id in nodes]
103
+
104
+ if len(footprint) >= 3:
105
+ if footprint[0] != footprint[-1]:
106
+ footprint.append(footprint[0])
107
+
108
+ buildings.append({"footprint": footprint, "height": height})
109
+
110
+ except Exception as e:
111
+ continue
112
+
113
+ return buildings
114
+
115
+ def create_3d_model(buildings: List[Dict]) -> trimesh.Scene:
116
+ """Create a 3D model using trimesh with PROPER ORIENTATION FIX."""
117
+ scene = trimesh.Scene()
118
+
119
+ for building in buildings:
120
+ footprint = building["footprint"]
121
+ height = building.get("height", 10)
122
+
123
+ if height <= 0:
124
+ continue
125
+
126
+ try:
127
+ polygon = sg.Polygon(footprint)
128
+ if not polygon.is_valid:
129
+ polygon = polygon.buffer(0)
130
+ if not polygon.is_valid:
131
+ continue
132
+ except Exception:
133
+ continue
134
+
135
+ try:
136
+ # Try triangle engine first, then earcut
137
+ try:
138
+ extruded = trimesh.creation.extrude_polygon(polygon, height, engine="triangle")
139
+ except ValueError:
140
+ try:
141
+ extruded = trimesh.creation.extrude_polygon(polygon, height, engine="earcut")
142
+ except ValueError:
143
+ continue
144
+
145
+ # βœ… PROPER ORIENTATION FIX - This is the solution you provided
146
+ # This rotates the model so the front view shows properly
147
+ transform_x = trimesh.transformations.rotation_matrix(np.pi/2, (1, 0, 0))
148
+
149
+ # Also rotate around Z-axis for proper left-right orientation
150
+ transform_z = trimesh.transformations.rotation_matrix(np.pi, (0, 0, 1))
151
+
152
+ # Apply the transformations
153
+ extruded.apply_transform(transform_x)
154
+ extruded.apply_transform(transform_z)
155
+
156
+ # Add to scene
157
+ scene.add_geometry(extruded)
158
+
159
+ except Exception:
160
+ continue
161
+
162
+ return scene
163
+
164
+ def save_3d_model(scene: trimesh.Scene, filename: str) -> bool:
165
+ """Export the 3D scene to a GLB file."""
166
+ try:
167
+ scene.export(filename)
168
+ return os.path.exists(filename)
169
+ except Exception:
170
+ return False
171
+
172
+
173
+
174
+
175
+ def generate_3d_model(latitude: float, longitude: float, radius: int) -> Tuple[str, str, str]:
176
+ """Main function to generate 3D model from coordinates."""
177
+
178
+ # Validate inputs
179
+ if not (-90 <= latitude <= 90):
180
+ return None, "❌ Error: Latitude must be between -90 and 90", ""
181
+
182
+ if not (-180 <= longitude <= 180):
183
+ return None, "❌ Error: Longitude must be between -180 and 180", ""
184
+
185
+ if not (10 <= radius <= 2000):
186
+ return None, "❌ Error: Radius must be between 10 and 2000 meters", ""
187
+
188
+ try:
189
+ # Step 1: Fetch OSM data
190
+ status_msg = f"πŸ” Fetching OSM data for coordinates: {latitude}, {longitude} with radius: {radius}m..."
191
+ print(status_msg)
192
+
193
+ osm_data = fetch_osm_data(latitude, longitude, radius)
194
+ if not osm_data:
195
+ return None, "❌ Failed to fetch OSM data. Please check coordinates and try again.", ""
196
+
197
+ # Step 2: Parse buildings
198
+ status_msg += f"\nβœ… OSM data fetched successfully\nπŸ—οΈ Parsing building data..."
199
+ buildings = parse_osm_data(osm_data)
200
+
201
+ if not buildings:
202
+ return None, "❌ No buildings found in this area. Try a different location or larger radius.", ""
203
+
204
+ # Step 3: Create 3D model
205
+ status_msg += f"\nβœ… Found {len(buildings)} buildings\n🏠 Creating 3D model..."
206
+ scene = create_3d_model(buildings)
207
+
208
+ if len(scene.geometry) == 0:
209
+ return None, "❌ Could not create 3D model from the buildings found.", ""
210
+
211
+ # Step 4: Save model
212
+ timestamp = int(time.time())
213
+ filename = f"osm_3d_model_{timestamp}.glb"
214
+
215
+ status_msg += f"\nβœ… 3D model created with {len(scene.geometry)} buildings\nπŸ’Ύ Saving model..."
216
+
217
+ if save_3d_model(scene, filename):
218
+ file_size = os.path.getsize(filename)
219
+ 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!"
220
+ status_msg += final_msg
221
+
222
+ # Create summary info
223
+ summary = f"""🌍 **Location**: {latitude}, {longitude}
224
+ πŸ“ **Radius**: {radius} meters
225
+ 🏒 **Buildings Found**: {len(buildings)}
226
+ πŸ”§ **3D Geometries Created**: {len(scene.geometry)}
227
+ πŸ“ **File Size**: {file_size/1024:.1f} KB
228
+ ⏰ **Generated**: {time.strftime('%Y-%m-%d %H:%M:%S')}"""
229
+
230
+ return filename, status_msg, summary
231
+ else:
232
+ return None, "❌ Failed to save 3D model file.", ""
233
+
234
+ except Exception as e:
235
+ return None, f"❌ Unexpected error: {str(e)}", ""
236
+
237
+
238
+
239
+
240
+
241
+ # Create Gradio interface
242
+ def create_gradio_app():
243
+ """Create and configure the Gradio interface."""
244
+
245
+ with gr.Blocks(title="🌍 OSM 3D Generator", theme=gr.themes.Soft()) as app:
246
+
247
+ # Header
248
+ gr.Markdown("""
249
+ # 🌍 OSM 3D Environment Generator
250
+
251
+ **Transform real-world locations into 3D models!**
252
+
253
+ Enter coordinates and radius to generate 3D building models from OpenStreetMap data.
254
+ Perfect for architecture, urban planning, game development, and 3D visualization.
255
+ """)
256
+
257
+ with gr.Row():
258
+ with gr.Column(scale=1):
259
+ # Input section
260
+ gr.Markdown("## πŸ“ Location Settings")
261
+
262
+ latitude = gr.Number(
263
+ label="🌐 Latitude",
264
+ value=40.748817, # Empire State Building
265
+ precision=6,
266
+ info="Enter latitude (-90 to 90)"
267
+ )
268
+
269
+ longitude = gr.Number(
270
+ label="🌐 Longitude",
271
+ value=-73.985428, # Empire State Building
272
+ precision=6,
273
+ info="Enter longitude (-180 to 180)"
274
+ )
275
+
276
+ radius = gr.Slider(
277
+ label="πŸ“ Search Radius (meters)",
278
+ minimum=10,
279
+ maximum=2000,
280
+ value=500,
281
+ step=10,
282
+ info="Larger radius = more buildings but slower processing"
283
+ )
284
+
285
+ generate_btn = gr.Button(
286
+ "πŸš€ Generate 3D Model",
287
+ variant="primary",
288
+ size="lg"
289
+ )
290
+
291
+ # Examples
292
+ gr.Markdown("### πŸŒ† Quick Examples")
293
+ gr.Examples(
294
+ examples=[
295
+ [40.748817, -73.985428, 500], # Empire State Building, NYC
296
+ [48.858844, 2.294351, 300], # Eiffel Tower, Paris
297
+ [51.500729, -0.124625, 400], # Big Ben, London
298
+ [35.676098, 139.650311, 600], # Tokyo Station
299
+ [37.819929, -122.478255, 350], # Golden Gate Bridge area
300
+ ],
301
+ inputs=[latitude, longitude, radius],
302
+ label="Click to load famous locations"
303
+ )
304
+
305
+ with gr.Column(scale=1):
306
+ # Output section
307
+ gr.Markdown("## πŸ“₯ Generated Model")
308
+
309
+ file_output = gr.File(
310
+ label="πŸ“ Download 3D Model (.glb)",
311
+ file_types=[".glb"],
312
+ visible=False
313
+ )
314
+
315
+ status_output = gr.Textbox(
316
+ label="πŸ“Š Generation Status",
317
+ lines=8,
318
+ max_lines=15,
319
+ placeholder="Click 'Generate 3D Model' to start...",
320
+ interactive=False
321
+ )
322
+
323
+ summary_output = gr.Markdown(
324
+ "### πŸ“‹ Model Summary\nGeneration results will appear here..."
325
+ )
326
+
327
+ # Info section
328
+ with gr.Row():
329
+ gr.Markdown("""
330
+ ### πŸ’‘ Tips for Best Results
331
+
332
+ - **Urban areas** work best (more buildings = better models)
333
+ - **Start with 300-500m radius** for good balance of detail and speed
334
+ - **Large cities** like NYC, Paris, Tokyo have excellent building data
335
+ - **Rural areas** may have fewer or no buildings
336
+ - **Generated .glb files** can be opened in Blender, Three.js, or online 3D viewers
337
+
338
+ ### πŸ› οΈ Technical Details
339
+
340
+ - Uses **OpenStreetMap** data via Overpass API
341
+ - Creates **proper 3D building heights** when available
342
+ - Applies **correct orientation** for front-view display
343
+ - Exports as **GLB format** (compatible with most 3D software)
344
+ - **Processing time** varies by area complexity (typically 10-60 seconds)
345
+ """)
346
+
347
+ # Event handler
348
+ def handle_generation(lat, lon, rad):
349
+ """Handle the generation process and update UI."""
350
+ file_path, status, summary = generate_3d_model(lat, lon, rad)
351
+
352
+ if file_path:
353
+ return (
354
+ gr.update(value=file_path, visible=True), # file_output
355
+ status, # status_output
356
+ f"### πŸ“‹ Model Summary\n{summary}" # summary_output
357
+ )
358
+ else:
359
+ return (
360
+ gr.update(visible=False), # file_output
361
+ status, # status_output
362
+ "### ❌ Generation Failed\nPlease check the status above and try again." # summary_output
363
+ )
364
+
365
+ # Connect the button
366
+ generate_btn.click(
367
+ fn=handle_generation,
368
+ inputs=[latitude, longitude, radius],
369
+ outputs=[file_output, status_output, summary_output]
370
+ )
371
+
372
+ return app
373
+
374
+
375
+
376
+
377
+ # Create and launch the app
378
+ app = create_gradio_app()
379
+
380
+ # Launch the app
381
+ if __name__ == "__main__":
382
+ app.launch(
383
+ share=True, # Creates public link for Hugging Face
384
+ server_name="0.0.0.0", # Allow external connections
385
+ server_port=7860, # Standard port for Hugging Face
386
+ show_error=True,
387
+ debug=True
388
+ )
389
+ else:
390
+ # For Hugging Face Spaces
391
+ app.launch()
392
+
393
+
394
+ # ## πŸš€ Deployment Instructions for Hugging Face Spaces
395
+ #
396
+ # To deploy this app on Hugging Face Spaces:
397
+ #
398
+ # 1. **Create a new Space** on [Hugging Face](https://huggingface.co/spaces)
399
+ # 2. **Select "Gradio" as the Space SDK**
400
+ # 3. **Upload this notebook** or copy the code to `app.py`
401
+ # 4. **Add requirements.txt** with these dependencies:
402
+ # ```
403
+ # gradio
404
+ # requests
405
+ # pyproj
406
+ # shapely
407
+ # trimesh
408
+ # numpy
409
+ # ```
410
+ # 5. **Commit and push** - your app will automatically deploy!
411
+ #
412
+ # ### πŸ“ Alternative: Direct Python File
413
+ # You can also copy all the Python code cells into a single `app.py` file for easier deployment.
414
+ #
415
+ # ### πŸ”§ Environment Variables (Optional)
416
+ # For production deployment, consider adding:
417
+ # - `GRADIO_SERVER_NAME=0.0.0.0`
418
+ # - `GRADIO_SERVER_PORT=7860`
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ gradio
2
+ requests
3
+ pyproj
4
+ shapely
5
+ trimesh
6
+ numpy