Create app.py
Browse files
app.py
ADDED
@@ -0,0 +1,237 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio as gr
|
2 |
+
from PIL import Image
|
3 |
+
import tempfile
|
4 |
+
import os
|
5 |
+
from pathlib import Path
|
6 |
+
import shutil
|
7 |
+
import base64
|
8 |
+
import requests
|
9 |
+
import re
|
10 |
+
import time
|
11 |
+
from PIL import ImageEnhance
|
12 |
+
import concurrent.futures
|
13 |
+
import asyncio
|
14 |
+
import aiohttp
|
15 |
+
import io
|
16 |
+
|
17 |
+
class StreetViewDownloader:
|
18 |
+
def __init__(self):
|
19 |
+
self.headers = {
|
20 |
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
|
21 |
+
}
|
22 |
+
# Using a smaller session for better connection reuse
|
23 |
+
self.session = requests.Session()
|
24 |
+
|
25 |
+
def extract_panoid(self, url):
|
26 |
+
"""Extract panorama ID from Google Maps URL."""
|
27 |
+
pattern = r'!1s([A-Za-z0-9-_]+)!'
|
28 |
+
match = re.search(pattern, url)
|
29 |
+
if match:
|
30 |
+
return match.group(1)
|
31 |
+
raise ValueError("Could not find panorama ID in URL")
|
32 |
+
|
33 |
+
async def download_tile_async(self, session, panoid, x, y, adjusted_y, zoom, output_dir):
|
34 |
+
"""Download a single tile asynchronously."""
|
35 |
+
tile_url = f"https://streetviewpixels-pa.googleapis.com/v1/tile?cb_client=maps_sv.tactile&panoid={panoid}&x={x}&y={adjusted_y}&zoom={zoom}"
|
36 |
+
output_file = Path(output_dir) / f"tile_{x}_{y}.jpg"
|
37 |
+
|
38 |
+
try:
|
39 |
+
async with session.get(tile_url, headers=self.headers) as response:
|
40 |
+
if response.status == 200:
|
41 |
+
content = await response.read()
|
42 |
+
if len(content) > 1000:
|
43 |
+
output_file.write_bytes(content)
|
44 |
+
return (x, y)
|
45 |
+
except Exception as e:
|
46 |
+
print(f"Error downloading tile {x},{y}: {str(e)}")
|
47 |
+
return None
|
48 |
+
|
49 |
+
async def download_tiles_async(self, panoid, output_dir):
|
50 |
+
"""Download tiles asynchronously with reduced resolution."""
|
51 |
+
Path(output_dir).mkdir(parents=True, exist_ok=True)
|
52 |
+
|
53 |
+
# Reduced parameters for faster download
|
54 |
+
zoom = 2 # Reduced zoom level (less detail but faster)
|
55 |
+
cols = 16 # Reduced number of horizontal tiles
|
56 |
+
rows = 8 # Reduced number of vertical tiles
|
57 |
+
vertical_offset = 2 # Adjusted for the new grid
|
58 |
+
|
59 |
+
print(f"Downloading {cols * rows} tiles for panorama...")
|
60 |
+
|
61 |
+
async with aiohttp.ClientSession() as session:
|
62 |
+
tasks = []
|
63 |
+
for x in range(cols):
|
64 |
+
for y in range(rows):
|
65 |
+
adjusted_y = y - (rows // 2) + vertical_offset
|
66 |
+
task = self.download_tile_async(session, panoid, x, y, adjusted_y, zoom, output_dir)
|
67 |
+
tasks.append(task)
|
68 |
+
|
69 |
+
downloaded_tiles = []
|
70 |
+
for result in await asyncio.gather(*tasks):
|
71 |
+
if result:
|
72 |
+
downloaded_tiles.append(result)
|
73 |
+
|
74 |
+
return cols, rows, downloaded_tiles
|
75 |
+
|
76 |
+
def download_tiles(self, panoid, output_dir):
|
77 |
+
"""Synchronous wrapper for async download."""
|
78 |
+
return asyncio.run(self.download_tiles_async(panoid, output_dir))
|
79 |
+
|
80 |
+
def create_360_panorama(self, directory, cols, rows, downloaded_tiles, output_file):
|
81 |
+
"""Create an equirectangular 360° panorama from tiles with optimized processing."""
|
82 |
+
directory = Path(directory)
|
83 |
+
|
84 |
+
# Find a valid tile to get dimensions
|
85 |
+
valid_tile = None
|
86 |
+
for x, y in downloaded_tiles:
|
87 |
+
tile_path = directory / f"tile_{x}_{y}.jpg"
|
88 |
+
if tile_path.exists():
|
89 |
+
valid_tile = Image.open(tile_path)
|
90 |
+
break
|
91 |
+
|
92 |
+
if not valid_tile:
|
93 |
+
raise Exception("No valid tiles found in directory")
|
94 |
+
|
95 |
+
tile_width, tile_height = valid_tile.size
|
96 |
+
valid_tile.close()
|
97 |
+
|
98 |
+
# Create the panorama at optimized resolution
|
99 |
+
panorama_width = tile_width * cols
|
100 |
+
panorama_height = tile_height * rows
|
101 |
+
|
102 |
+
# Use RGB mode directly for better performance
|
103 |
+
panorama = Image.new('RGB', (panorama_width, panorama_height))
|
104 |
+
|
105 |
+
# Process tiles in parallel
|
106 |
+
def process_tile(tile_info):
|
107 |
+
x, y = tile_info
|
108 |
+
tile_path = directory / f"tile_{x}_{y}.jpg"
|
109 |
+
if tile_path.exists():
|
110 |
+
with Image.open(tile_path) as tile:
|
111 |
+
if tile.getbbox():
|
112 |
+
return (x * tile_width, y * tile_height, tile.copy())
|
113 |
+
return None
|
114 |
+
|
115 |
+
# Use ThreadPoolExecutor for parallel processing
|
116 |
+
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
|
117 |
+
tile_results = list(executor.map(process_tile, downloaded_tiles))
|
118 |
+
|
119 |
+
# Paste all valid tiles
|
120 |
+
for result in tile_results:
|
121 |
+
if result:
|
122 |
+
x, y, tile = result
|
123 |
+
panorama.paste(tile, (x, y))
|
124 |
+
tile.close()
|
125 |
+
|
126 |
+
# Crop out any remaining black regions
|
127 |
+
bbox = panorama.getbbox()
|
128 |
+
if bbox:
|
129 |
+
panorama = panorama.crop(bbox)
|
130 |
+
|
131 |
+
# Quick enhancement
|
132 |
+
panorama = self.enhance_panorama(panorama)
|
133 |
+
|
134 |
+
# Save with optimized settings
|
135 |
+
panorama.save(output_file, 'JPEG', quality=95, optimize=True)
|
136 |
+
return output_file
|
137 |
+
|
138 |
+
def enhance_panorama(self, panorama):
|
139 |
+
"""Quick enhancement with minimal processing."""
|
140 |
+
enhancer = ImageEnhance.Contrast(panorama)
|
141 |
+
panorama = enhancer.enhance(1.1)
|
142 |
+
return panorama
|
143 |
+
|
144 |
+
def process_street_view(url, progress=gr.Progress()):
|
145 |
+
"""Process the Street View URL with progress tracking."""
|
146 |
+
try:
|
147 |
+
temp_dir = tempfile.mkdtemp()
|
148 |
+
progress(0.1, desc="Initializing...")
|
149 |
+
|
150 |
+
downloader = StreetViewDownloader()
|
151 |
+
panoid = downloader.extract_panoid(url)
|
152 |
+
progress(0.2, desc="Extracted panorama ID...")
|
153 |
+
|
154 |
+
tiles_dir = os.path.join(temp_dir, f"{panoid}_tiles")
|
155 |
+
output_file = os.path.join(temp_dir, f"{panoid}_360panorama.jpg")
|
156 |
+
|
157 |
+
progress(0.3, desc="Downloading tiles...")
|
158 |
+
cols, rows, downloaded_tiles = downloader.download_tiles(panoid, tiles_dir)
|
159 |
+
|
160 |
+
progress(0.7, desc="Creating panorama...")
|
161 |
+
final_path = downloader.create_360_panorama(tiles_dir, cols, rows, downloaded_tiles, output_file)
|
162 |
+
|
163 |
+
progress(0.8, desc="Cleaning up...")
|
164 |
+
shutil.rmtree(tiles_dir)
|
165 |
+
|
166 |
+
progress(0.9, desc="Preparing viewer...")
|
167 |
+
with open(final_path, 'rb') as img_file:
|
168 |
+
img_data = base64.b64encode(img_file.read()).decode()
|
169 |
+
|
170 |
+
viewer_html = f"""
|
171 |
+
<html>
|
172 |
+
<head>
|
173 |
+
<script src="https://aframe.io/releases/1.4.0/aframe.min.js"></script>
|
174 |
+
<style>
|
175 |
+
.viewer-container {{ height: 500px; width: 100%; }}
|
176 |
+
</style>
|
177 |
+
</head>
|
178 |
+
<body>
|
179 |
+
<div class="viewer-container">
|
180 |
+
<a-scene embedded>
|
181 |
+
<a-sky src="data:image/jpeg;base64,{img_data}"></a-sky>
|
182 |
+
<a-camera position="0 0 0" wasd-controls-enabled="false"></a-camera>
|
183 |
+
</a-scene>
|
184 |
+
</div>
|
185 |
+
</body>
|
186 |
+
</html>
|
187 |
+
"""
|
188 |
+
|
189 |
+
progress(1.0, desc="Done!")
|
190 |
+
return final_path, viewer_html
|
191 |
+
|
192 |
+
except Exception as e:
|
193 |
+
return None, f"Error: {str(e)}"
|
194 |
+
|
195 |
+
def create_gradio_interface():
|
196 |
+
with gr.Blocks() as app:
|
197 |
+
gr.Markdown("# Fast Street View 360° Panorama Downloader")
|
198 |
+
gr.Markdown("Enter a Google Street View URL to download and view the panorama")
|
199 |
+
|
200 |
+
with gr.Row():
|
201 |
+
url_input = gr.Textbox(
|
202 |
+
label="Street View URL",
|
203 |
+
placeholder="Paste Google Street View URL here..."
|
204 |
+
)
|
205 |
+
|
206 |
+
with gr.Row():
|
207 |
+
submit_btn = gr.Button("Download and Process")
|
208 |
+
|
209 |
+
with gr.Row():
|
210 |
+
with gr.Column():
|
211 |
+
output_file = gr.File(label="Download Panorama")
|
212 |
+
with gr.Column():
|
213 |
+
viewer = gr.HTML(label="360° Viewer")
|
214 |
+
|
215 |
+
submit_btn.click(
|
216 |
+
fn=process_street_view,
|
217 |
+
inputs=[url_input],
|
218 |
+
outputs=[output_file, viewer]
|
219 |
+
)
|
220 |
+
|
221 |
+
gr.Markdown("""
|
222 |
+
### Instructions:
|
223 |
+
1. Open Google Street View at your desired location
|
224 |
+
2. Copy the URL from your browser
|
225 |
+
3. Paste the URL in the input box above
|
226 |
+
4. Click "Download and Process"
|
227 |
+
5. Wait for processing (usually takes less than a minute)
|
228 |
+
6. Download the panorama or explore it in the 360° viewer
|
229 |
+
|
230 |
+
Note: This is an optimized version that provides faster processing with slightly reduced image quality.
|
231 |
+
""")
|
232 |
+
|
233 |
+
return app
|
234 |
+
|
235 |
+
if __name__ == "__main__":
|
236 |
+
app = create_gradio_interface()
|
237 |
+
app.launch(share=True)
|