aletrn commited on
Commit
ddd32eb
·
1 Parent(s): bb53beb

[refactor] clean download_extent function

Browse files
Files changed (2) hide show
  1. src/io/tms2geotiff.py +73 -172
  2. src/utilities/constants.py +10 -4
src/io/tms2geotiff.py CHANGED
@@ -34,14 +34,13 @@ import io
34
  import itertools
35
  import math
36
  import re
37
- import sqlite3
38
  import time
39
 
40
  from PIL import Image
41
 
42
  from src import app_logger
43
- from src.utilities.constants import EARTH_EQUATORIAL_RADIUS
44
-
45
 
46
  Image.MAX_IMAGE_PIXELS = None
47
 
@@ -64,16 +63,16 @@ re_coords_split = re.compile('[ ,;]+')
64
 
65
 
66
  def from4326_to3857(lat, lon):
67
- xtile = math.radians(lon) * EARTH_EQUATORIAL_RADIUS
68
- ytile = math.log(math.tan(math.radians(45 + lat / 2.0))) * EARTH_EQUATORIAL_RADIUS
69
- return xtile, ytile
70
 
71
 
72
  def deg2num(lat, lon, zoom):
73
  n = 2 ** zoom
74
- xtile = ((lon + 180) / 360 * n)
75
- ytile = (1 - math.asinh(math.tan(math.radians(lat))) / math.pi) * n / 2
76
- return xtile, ytile
77
 
78
 
79
  def is_empty(im):
@@ -89,56 +88,40 @@ def is_empty(im):
89
  return extrema[0] == (0, 0)
90
 
91
 
92
- def mbtiles_init(dbname):
93
- db = sqlite3.connect(dbname, isolation_level=None)
94
- cur = db.cursor()
95
- cur.execute("BEGIN")
96
- cur.execute("CREATE TABLE IF NOT EXISTS metadata (name TEXT PRIMARY KEY, value TEXT)")
97
- cur.execute("CREATE TABLE IF NOT EXISTS tiles ("
98
- "zoom_level INTEGER NOT NULL, "
99
- "tile_column INTEGER NOT NULL, "
100
- "tile_row INTEGER NOT NULL, "
101
- "tile_data BLOB NOT NULL, "
102
- "UNIQUE (zoom_level, tile_column, tile_row)"
103
- ")")
104
- cur.execute("COMMIT")
105
- return db
106
-
107
-
108
- def paste_tile(bigim, base_size, tile, corner_xy, bbox):
109
  if tile is None:
110
- return bigim
111
  im = Image.open(io.BytesIO(tile))
112
  mode = 'RGB' if im.mode == 'RGB' else 'RGBA'
113
  size = im.size
114
- if bigim is None:
115
  base_size[0] = size[0]
116
  base_size[1] = size[1]
117
- newim = Image.new(mode, (
118
  size[0] * (bbox[2] - bbox[0]), size[1] * (bbox[3] - bbox[1])))
119
  else:
120
- newim = bigim
121
 
122
  dx = abs(corner_xy[0] - bbox[0])
123
  dy = abs(corner_xy[1] - bbox[1])
124
  xy0 = (size[0] * dx, size[1] * dy)
125
  if mode == 'RGB':
126
- newim.paste(im, xy0)
127
  else:
128
  if im.mode != mode:
129
  im = im.convert(mode)
130
  if not is_empty(im):
131
- newim.paste(im, xy0)
132
  im.close()
133
- return newim
134
 
135
 
136
  def get_tile(url):
137
- retry = 3
138
  while 1:
139
  try:
140
  app_logger.debug(f"image tile url to download: {url}.")
141
- r = SESSION.get(url, timeout=60)
142
  break
143
  except Exception as request_tile_exception:
144
  app_logger.error(f"retry {retry}, request_tile_exception:{request_tile_exception}.")
@@ -156,44 +139,10 @@ def print_progress(progress, total, done=False):
156
  app_logger.info('Downloaded image %d/%d, %.2f%%' % (progress, total, progress * 100 / total))
157
 
158
 
159
- def mbtiles_save(db, img_data, xy, zoom, img_format):
160
- if not img_data:
161
- return
162
- im = Image.open(io.BytesIO(img_data))
163
- if im.format == 'PNG':
164
- current_format = 'png'
165
- elif im.format == 'JPEG':
166
- current_format = 'jpg'
167
- elif im.format == 'WEBP':
168
- current_format = 'webp'
169
- else:
170
- current_format = 'image/' + im.format.lower()
171
- x, y = xy
172
- y = 2 ** zoom - 1 - y
173
- cur = db.cursor()
174
- if img_format is None or img_format == current_format:
175
- cur.execute("REPLACE INTO tiles VALUES (?,?,?,?)", (
176
- zoom, x, y, img_data))
177
- return img_format or current_format
178
- buf = io.BytesIO()
179
- if img_format == 'png':
180
- im.save(buf, 'PNG')
181
- elif img_format == 'jpg':
182
- im.save(buf, 'JPEG', quality=93)
183
- elif img_format == 'webp':
184
- im.save(buf, 'WEBP')
185
- else:
186
- im.save(buf, img_format.split('/')[-1].upper())
187
- cur.execute("REPLACE INTO tiles VALUES (?,?,?,?)", (
188
- zoom, x, y, buf.getvalue()))
189
- return img_format
190
-
191
-
192
  def download_extent(
193
  source, lat0, lon0, lat1, lon1, zoom,
194
- mbtiles=None, save_image=True,
195
- progress_callback=print_progress,
196
- callback_interval=0.05
197
  ):
198
  x0, y0 = deg2num(lat0, lon0, zoom)
199
  x1, y1 = deg2num(lat1, lon1, zoom)
@@ -202,59 +151,13 @@ def download_extent(
202
  if y0 > y1:
203
  y0, y1 = y1, y0
204
 
205
- db = None
206
- mbt_img_format = None
207
- if mbtiles:
208
- db = mbtiles_init(mbtiles)
209
- cur = db.cursor()
210
- cur.execute("BEGIN")
211
- cur.execute("REPLACE INTO metadata VALUES ('name', ?)", (source,))
212
- cur.execute("REPLACE INTO metadata VALUES ('type', 'overlay')")
213
- cur.execute("REPLACE INTO metadata VALUES ('version', '1.1')")
214
- cur.execute("REPLACE INTO metadata VALUES ('description', ?)", (source,))
215
- cur.execute("SELECT value FROM metadata WHERE name='format'")
216
- row = cur.fetchone()
217
- if row and row[0]:
218
- mbt_img_format = row[0]
219
- else:
220
- cur.execute("REPLACE INTO metadata VALUES ('format', 'png')")
221
-
222
- lat_min = min(lat0, lat1)
223
- lat_max = max(lat0, lat1)
224
- lon_min = min(lon0, lon1)
225
- lon_max = max(lon0, lon1)
226
- bounds = [lon_min, lat_min, lon_max, lat_max]
227
- cur.execute("SELECT value FROM metadata WHERE name='bounds'")
228
- row = cur.fetchone()
229
- if row and row[0]:
230
- last_bounds = [float(x) for x in row[0].split(',')]
231
- bounds[0] = min(last_bounds[0], bounds[0])
232
- bounds[1] = min(last_bounds[1], bounds[1])
233
- bounds[2] = max(last_bounds[2], bounds[2])
234
- bounds[3] = max(last_bounds[3], bounds[3])
235
- cur.execute("REPLACE INTO metadata VALUES ('bounds', ?)", (
236
- ",".join(map(str, bounds)),))
237
- cur.execute("REPLACE INTO metadata VALUES ('center', ?)", ("%s,%s,%d" % (
238
- (lon_max + lon_min) / 2, (lat_max + lat_min) / 2, zoom),))
239
- cur.execute("""
240
- INSERT INTO metadata VALUES ('minzoom', ?)
241
- ON CONFLICT(name) DO UPDATE SET value=excluded.value
242
- WHERE CAST(excluded.value AS INTEGER)<CAST(metadata.value AS INTEGER)
243
- """, (str(zoom),))
244
- cur.execute("""
245
- INSERT INTO metadata VALUES ('maxzoom', ?)
246
- ON CONFLICT(name) DO UPDATE SET value=excluded.value
247
- WHERE CAST(excluded.value AS INTEGER)>CAST(metadata.value AS INTEGER)
248
- """, (str(zoom),))
249
- cur.execute("COMMIT")
250
-
251
  corners = tuple(itertools.product(
252
  range(math.floor(x0), math.ceil(x1)),
253
  range(math.floor(y0), math.ceil(y1))))
254
- totalnum = len(corners)
255
  futures = {}
256
  done_num = 0
257
- progress_callback(done_num, totalnum, False)
258
  last_done_num = 0
259
  last_callback = time.monotonic()
260
  cancelled = False
@@ -263,67 +166,65 @@ def download_extent(
263
  future = executor.submit(get_tile, source.format(z=zoom, x=x, y=y))
264
  futures[future] = (x, y)
265
  bbox = (math.floor(x0), math.floor(y0), math.ceil(x1), math.ceil(y1))
266
- bigim = None
267
- base_size = [256, 256]
268
- while futures:
269
- done, _ = concurrent.futures.wait(
270
- futures.keys(), timeout=callback_interval,
271
- return_when=concurrent.futures.FIRST_COMPLETED
272
- )
273
- cur = None
274
- if mbtiles:
275
- cur = db.cursor()
276
- cur.execute("BEGIN")
277
- for fut in done:
278
- img_data = fut.result()
279
- xy = futures[fut]
280
- if save_image:
281
- bigim = paste_tile(bigim, base_size, img_data, xy, bbox)
282
- if mbtiles:
283
- new_format = mbtiles_save(db, img_data, xy, zoom, mbt_img_format)
284
- if not mbt_img_format:
285
- cur.execute(
286
- "UPDATE metadata SET value=? WHERE name='format'",
287
- (new_format,))
288
- mbt_img_format = new_format
289
- del futures[fut]
290
- done_num += 1
291
- if mbtiles:
292
- cur.execute("COMMIT")
293
- if time.monotonic() > last_callback + callback_interval:
294
- try:
295
- progress_callback(done_num, totalnum, (done_num > last_done_num))
296
- except TaskCancelled:
297
- for fut in futures.keys():
298
- fut.cancel()
299
- futures.clear()
300
- cancelled = True
301
- break
302
- last_callback = time.monotonic()
303
- last_done_num = done_num
304
  if cancelled:
305
  raise TaskCancelled()
306
- progress_callback(done_num, totalnum, True)
307
 
308
  if not save_image:
309
  return None, None
310
 
311
- xfrac = x0 - bbox[0]
312
- yfrac = y0 - bbox[1]
313
- x2 = round(base_size[0] * xfrac)
314
- y2 = round(base_size[1] * yfrac)
315
- imgw = round(base_size[0] * (x1 - x0))
316
- imgh = round(base_size[1] * (y1 - y0))
317
- retim = bigim.crop((x2, y2, x2 + imgw, y2 + imgh))
318
- if retim.mode == 'RGBA' and retim.getextrema()[3] == (255, 255):
319
- retim = retim.convert('RGB')
320
- bigim.close()
321
  xp0, yp0 = from4326_to3857(lat0, lon0)
322
  xp1, yp1 = from4326_to3857(lat1, lon1)
323
- pwidth = abs(xp1 - xp0) / retim.size[0]
324
- pheight = abs(yp1 - yp0) / retim.size[1]
325
- matrix = (min(xp0, xp1), pwidth, 0, max(yp0, yp1), 0, -pheight)
326
- return retim, matrix
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
327
 
328
 
329
  class TaskCancelled(RuntimeError):
 
34
  import itertools
35
  import math
36
  import re
 
37
  import time
38
 
39
  from PIL import Image
40
 
41
  from src import app_logger
42
+ from src.utilities.constants import EARTH_EQUATORIAL_RADIUS, RETRY_DOWNLOAD, TIMEOUT_DOWNLOAD, TILE_SIZE, \
43
+ CALLBACK_INTERVAL_DOWNLOAD
44
 
45
  Image.MAX_IMAGE_PIXELS = None
46
 
 
63
 
64
 
65
  def from4326_to3857(lat, lon):
66
+ x_tile = math.radians(lon) * EARTH_EQUATORIAL_RADIUS
67
+ y_tile = math.log(math.tan(math.radians(45 + lat / 2.0))) * EARTH_EQUATORIAL_RADIUS
68
+ return x_tile, y_tile
69
 
70
 
71
  def deg2num(lat, lon, zoom):
72
  n = 2 ** zoom
73
+ x_tile = ((lon + 180) / 360 * n)
74
+ y_tile = (1 - math.asinh(math.tan(math.radians(lat))) / math.pi) * n / 2
75
+ return x_tile, y_tile
76
 
77
 
78
  def is_empty(im):
 
88
  return extrema[0] == (0, 0)
89
 
90
 
91
+ def paste_tile(big_im, base_size, tile, corner_xy, bbox):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  if tile is None:
93
+ return big_im
94
  im = Image.open(io.BytesIO(tile))
95
  mode = 'RGB' if im.mode == 'RGB' else 'RGBA'
96
  size = im.size
97
+ if big_im is None:
98
  base_size[0] = size[0]
99
  base_size[1] = size[1]
100
+ new_im = Image.new(mode, (
101
  size[0] * (bbox[2] - bbox[0]), size[1] * (bbox[3] - bbox[1])))
102
  else:
103
+ new_im = big_im
104
 
105
  dx = abs(corner_xy[0] - bbox[0])
106
  dy = abs(corner_xy[1] - bbox[1])
107
  xy0 = (size[0] * dx, size[1] * dy)
108
  if mode == 'RGB':
109
+ new_im.paste(im, xy0)
110
  else:
111
  if im.mode != mode:
112
  im = im.convert(mode)
113
  if not is_empty(im):
114
+ new_im.paste(im, xy0)
115
  im.close()
116
+ return new_im
117
 
118
 
119
  def get_tile(url):
120
+ retry = RETRY_DOWNLOAD
121
  while 1:
122
  try:
123
  app_logger.debug(f"image tile url to download: {url}.")
124
+ r = SESSION.get(url, timeout=TIMEOUT_DOWNLOAD)
125
  break
126
  except Exception as request_tile_exception:
127
  app_logger.error(f"retry {retry}, request_tile_exception:{request_tile_exception}.")
 
139
  app_logger.info('Downloaded image %d/%d, %.2f%%' % (progress, total, progress * 100 / total))
140
 
141
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  def download_extent(
143
  source, lat0, lon0, lat1, lon1, zoom,
144
+ save_image=True, progress_callback=print_progress,
145
+ callback_interval=CALLBACK_INTERVAL_DOWNLOAD
 
146
  ):
147
  x0, y0 = deg2num(lat0, lon0, zoom)
148
  x1, y1 = deg2num(lat1, lon1, zoom)
 
151
  if y0 > y1:
152
  y0, y1 = y1, y0
153
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  corners = tuple(itertools.product(
155
  range(math.floor(x0), math.ceil(x1)),
156
  range(math.floor(y0), math.ceil(y1))))
157
+ total_num = len(corners)
158
  futures = {}
159
  done_num = 0
160
+ progress_callback(done_num, total_num, False)
161
  last_done_num = 0
162
  last_callback = time.monotonic()
163
  cancelled = False
 
166
  future = executor.submit(get_tile, source.format(z=zoom, x=x, y=y))
167
  futures[future] = (x, y)
168
  bbox = (math.floor(x0), math.floor(y0), math.ceil(x1), math.ceil(y1))
169
+ big_im = None
170
+ base_size = [TILE_SIZE, TILE_SIZE]
171
+ big_im, cancelled, done_num = run_future_tile_download(
172
+ base_size, bbox, big_im, callback_interval, cancelled, done_num, futures, last_callback, last_done_num,
173
+ progress_callback, save_image, total_num
174
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  if cancelled:
176
  raise TaskCancelled()
177
+ progress_callback(done_num, total_num, True)
178
 
179
  if not save_image:
180
  return None, None
181
 
182
+ x_frac = x0 - bbox[0]
183
+ y_frac = y0 - bbox[1]
184
+ x2 = round(base_size[0] * x_frac)
185
+ y2 = round(base_size[1] * y_frac)
186
+ img_w = round(base_size[0] * (x1 - x0))
187
+ img_h = round(base_size[1] * (y1 - y0))
188
+ ret_im = big_im.crop((x2, y2, x2 + img_w, y2 + img_h))
189
+ if ret_im.mode == 'RGBA' and ret_im.getextrema()[3] == (255, 255):
190
+ ret_im = ret_im.convert('RGB')
191
+ big_im.close()
192
  xp0, yp0 = from4326_to3857(lat0, lon0)
193
  xp1, yp1 = from4326_to3857(lat1, lon1)
194
+ p_width = abs(xp1 - xp0) / ret_im.size[0]
195
+ p_height = abs(yp1 - yp0) / ret_im.size[1]
196
+ matrix = (min(xp0, xp1), p_width, 0, max(yp0, yp1), 0, -p_height)
197
+ return ret_im, matrix
198
+
199
+
200
+ def run_future_tile_download(
201
+ base_size, bbox, big_im, callback_interval, cancelled, done_num, futures, last_callback, last_done_num,
202
+ progress_callback, save_image, total_num
203
+ ):
204
+ while futures:
205
+ done, _ = concurrent.futures.wait(
206
+ futures.keys(), timeout=callback_interval,
207
+ return_when=concurrent.futures.FIRST_COMPLETED
208
+ )
209
+ for fut in done:
210
+ img_data = fut.result()
211
+ xy = futures[fut]
212
+ if save_image:
213
+ big_im = paste_tile(big_im, base_size, img_data, xy, bbox)
214
+ del futures[fut]
215
+ done_num += 1
216
+ if time.monotonic() > last_callback + callback_interval:
217
+ try:
218
+ progress_callback(done_num, total_num, (done_num > last_done_num))
219
+ except TaskCancelled:
220
+ for fut in futures.keys():
221
+ fut.cancel()
222
+ futures.clear()
223
+ cancelled = True
224
+ break
225
+ last_callback = time.monotonic()
226
+ last_done_num = done_num
227
+ return big_im, cancelled, done_num
228
 
229
 
230
  class TaskCancelled(RuntimeError):
src/utilities/constants.py CHANGED
@@ -13,9 +13,15 @@ MODEL_DECODER_NAME = "sam_vit_h_4b8939.decoder.onnx"
13
  TILE_SIZE = 256
14
  EARTH_EQUATORIAL_RADIUS = 6378137.0
15
  DEFAULT_TMS = 'https://tile.openstreetmap.org/{z}/{x}/{y}.png'
16
- WKT_3857 = 'PROJCS["WGS 84 / Pseudo-Mercator",GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,'
17
- WKT_3857 += 'AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]],PROJECTION["Mercator_1SP"],PARAMETER["central_meridian",0],'
18
- WKT_3857 += 'PARAMETER["scale_factor",1],PARAMETER["false_easting",0],PARAMETER["false_northing",0],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["X",EAST],AXIS["Y",NORTH],EXTENSION["PROJ4",'
19
- WKT_3857 += '"+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs"],AUTHORITY["EPSG","3857"]]'
 
 
 
20
  SERVICE_NAME = "sam-gis"
21
  DEFAULT_LOG_LEVEL = 'INFO'
 
 
 
 
13
  TILE_SIZE = 256
14
  EARTH_EQUATORIAL_RADIUS = 6378137.0
15
  DEFAULT_TMS = 'https://tile.openstreetmap.org/{z}/{x}/{y}.png'
16
+ WKT_3857 = 'PROJCS["WGS 84 / Pseudo-Mercator",GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,'
17
+ WKT_3857 += 'AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],'
18
+ WKT_3857 += 'UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]],'
19
+ WKT_3857 += 'PROJECTION["Mercator_1SP"],PARAMETER["central_meridian",0],PARAMETER["scale_factor",1],'
20
+ WKT_3857 += 'PARAMETER["false_easting",0],PARAMETER["false_northing",0],UNIT["metre",1,AUTHORITY["EPSG","9001"]],'
21
+ WKT_3857 += 'AXIS["X",EAST],AXIS["Y",NORTH],EXTENSION["PROJ4","+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 '
22
+ WKT_3857 += '+x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs"],AUTHORITY["EPSG","3857"]]'
23
  SERVICE_NAME = "sam-gis"
24
  DEFAULT_LOG_LEVEL = 'INFO'
25
+ RETRY_DOWNLOAD = 3
26
+ TIMEOUT_DOWNLOAD = 60
27
+ CALLBACK_INTERVAL_DOWNLOAD = 0.05