11import math
2+ import os
23import requests
4+ import numpy as np
35from PIL import Image
46from io import BytesIO
57from pathlib import Path
@@ -96,10 +98,15 @@ def download_and_save(lat_max, lat_min, lon_min, lon_max, zoom, image_name):
9698 num_y = y_max - y_min + 1
9799
98100
99- # Replace it with these:
100- stitched_image = None
101101 tile_size = None
102102 headers = {'User-Agent' : 'Mozilla/5.0' }
103+ mmap_path = f"{ image_name } _canvas.dat"
104+
105+ # Clean up stale memmap from a previous run
106+ if os .path .exists (mmap_path ):
107+ os .remove (mmap_path )
108+
109+ canvas = None
103110
104111 image_min_lat = 90.0
105112 image_min_long = 180.0
@@ -108,23 +115,24 @@ def download_and_save(lat_max, lat_min, lon_min, lon_max, zoom, image_name):
108115
109116 for i , x in enumerate (range (x_min , x_max + 1 )):
110117 for j , y in enumerate (range (y_min , y_max + 1 )):
111- url = get_satellite_url (x , y , zoom ) # Add provider="mapbox" etc. if using your updated function
118+ url = get_satellite_url (x , y , zoom )
112119 response = requests .get (url , headers = headers )
113-
120+
114121 if response .status_code == 200 :
115122 tile = Image .open (BytesIO (response .content ))
116-
117- # --- LAZY CANVAS INITIALIZATION ---
118- # If this is the first valid tile, measure it and build the canvas
119- if stitched_image is None :
120- tile_size = tile .width # This will automatically be 256 or 512
123+
124+ # Lazy init: measure first tile and create disk-backed canvas
125+ if canvas is None :
126+ tile_size = tile .width
121127 print (f"Detected tile size: { tile_size } x{ tile_size } px" )
122- stitched_image = Image .new ('RGB' , (num_x * tile_size , num_y * tile_size ))
123-
124- # Paste using the dynamic tile_size instead of 256 or 512
125- stitched_image .paste (tile , (i * tile_size , j * tile_size ))
128+ print (f"Canvas: { num_x * tile_size } x{ num_y * tile_size } px (memmap)" )
129+ canvas = np .memmap (mmap_path , dtype = np .uint8 , mode = 'w+' ,
130+ shape = (num_y * tile_size , num_x * tile_size , 3 ))
131+
132+ tile_arr = np .array (tile .convert ('RGB' ))
133+ canvas [j * tile_size :(j + 1 ) * tile_size ,
134+ i * tile_size :(i + 1 ) * tile_size ] = tile_arr
126135
127- # Get corners
128136 corners = get_tile_corners (x , y , zoom )
129137 for _ , val in corners .items ():
130138 lat = val [0 ]
@@ -140,34 +148,32 @@ def download_and_save(lat_max, lat_min, lon_min, lon_max, zoom, image_name):
140148 else :
141149 print (f"Error: Could not download tile { x } ,{ y } " )
142150
143- # Failsafe in case the entire grid failed to download
144- if stitched_image is None :
151+ if canvas is None :
145152 print ("Error: Could not download any tiles. Canvas was never created." )
146153 return None
147-
148- # Determine the global pixel coordinate of the top-left corner of your stitched image
149- # (Which is the NW corner of tile x_min, y_min)
154+
150155 ref_pixel_x = x_min * tile_size
151156 ref_pixel_y = y_min * tile_size
152157
153- # Get the global pixel coordinates for your requested bounding box
154158 upper_left_x , upper_left_y = latlon_to_pixel (lat_max , lon_min , zoom , tile_size )
155159 lower_right_x , lower_right_y = latlon_to_pixel (lat_min , lon_max , zoom , tile_size )
156160
157- # Calculate the crop boundaries relative to the stitched image (0,0)
158- left = upper_left_x - ref_pixel_x
159- top = upper_left_y - ref_pixel_y
160- right = lower_right_x - ref_pixel_x
161- bottom = lower_right_y - ref_pixel_y
161+ left = int (upper_left_x - ref_pixel_x )
162+ top = int (upper_left_y - ref_pixel_y )
163+ right = int (lower_right_x - ref_pixel_x )
164+ bottom = int (lower_right_y - ref_pixel_y )
162165
163- # Crop the output image
164166 print (f"Cropping image to: { left , top , right , bottom } " )
165- stitched_image = stitched_image . crop (( left , top , right , bottom ) )
167+ cropped = Image . fromarray ( canvas [ top : bottom , left : right ] )
166168
167169 output_path = Path (f"{ image_name } .jpg" )
168170 output_path .parent .mkdir (parents = True , exist_ok = True )
169- stitched_image .save (output_path , "JPEG" , quality = 90 )
170-
171+ cropped .save (output_path , "JPEG" , quality = 90 )
172+
173+ # Clean up memmap file
174+ del canvas
175+ os .remove (mmap_path )
176+
171177 print (f"Rectangular satellite image saved as '{ image_name } .jpg'" )
172178 print (f"Bounds: { image_min_lat } ,{ image_min_long } { image_max_lat } ,{ image_max_long } " )
173179 return str (output_path )
0 commit comments