-
Notifications
You must be signed in to change notification settings - Fork 41
/
Copy pathmbtiler.py
395 lines (320 loc) · 10 KB
/
mbtiler.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
from __future__ import with_statement
from __future__ import division
import os
import sys
import math
import traceback
import itertools
import mercantile
import rasterio
import numpy as np
import sqlite3
from multiprocessing import Pool
from rasterio._io import virtual_file_to_buffer
from riomucho.single_process_pool import MockTub
from io import BytesIO
from PIL import Image
from rasterio import transform
from rasterio.warp import reproject, transform_bounds
from rasterio.enums import Resampling
from rio_rgbify.encoders import data_to_rgb
buffer = bytes if sys.version_info > (3,) else buffer
work_func = None
global_args = None
src = None
def _main_worker(inpath, g_work_func, g_args):
"""
Util for setting global vars w/ a Pool
"""
global work_func
global global_args
global src
work_func = g_work_func
global_args = g_args
src = rasterio.open(inpath)
def _encode_as_webp(data, profile=None, affine=None):
"""
Uses BytesIO + PIL to encode a (3, 512, 512)
array into a webp bytearray.
Parameters
-----------
data: ndarray
(3 x 512 x 512) uint8 RGB array
profile: None
ignored
affine: None
ignored
Returns
--------
contents: bytearray
webp-encoded bytearray of the provided input data
"""
with BytesIO() as f:
im = Image.fromarray(np.rollaxis(data, 0, 3))
im.save(f, format="webp", lossless=True)
return f.getvalue()
def _encode_as_png(data, profile, dst_transform):
"""
Uses rasterio's virtual file system to encode a (3, 512, 512)
array as a png-encoded bytearray.
Parameters
-----------
data: ndarray
(3 x 512 x 512) uint8 RGB array
profile: dictionary
dictionary of kwargs for png writing
affine: Affine
affine transform for output tile
Returns
--------
contents: bytearray
png-encoded bytearray of the provided input data
"""
profile["affine"] = dst_transform
with rasterio.open("/vsimem/tileimg", "w", **profile) as dst:
dst.write(data)
contents = bytearray(virtual_file_to_buffer("/vsimem/tileimg"))
return contents
def _tile_worker(tile):
"""
For each tile, and given an open rasterio src, plus a`global_args` dictionary
with attributes of `base_val`, `interval`, `round_digits` and a `writer_func`,
warp a continous single band raster to a 512 x 512 mercator tile,
then encode this tile into RGB.
Parameters
-----------
tile: list
[x, y, z] indices of tile
Returns
--------
tile, buffer
tuple with the input tile, and a bytearray with the data encoded into
the format created in the `writer_func`
"""
x, y, z = tile
bounds = [
c
for i in (
mercantile.xy(*mercantile.ul(x, y + 1, z)),
mercantile.xy(*mercantile.ul(x + 1, y, z)),
)
for c in i
]
toaffine = transform.from_bounds(*bounds + [512, 512])
out = np.empty((512, 512), dtype=src.meta["dtype"])
reproject(
rasterio.band(src, 1),
out,
dst_transform=toaffine,
dst_crs="EPSG:3857",
resampling=Resampling.bilinear,
)
out = data_to_rgb(out, global_args["base_val"], global_args["interval"], global_args["round_digits"])
return tile, global_args["writer_func"](out, global_args["kwargs"].copy(), toaffine)
def _tile_range(min_tile, max_tile):
"""
Given a min and max tile, return an iterator of
all combinations of this tile range
Parameters
-----------
min_tile: list
[x, y, z] of minimun tile
max_tile:
[x, y, z] of minimun tile
Returns
--------
tiles: iterator
iterator of [x, y, z] tiles
"""
min_x, min_y, _ = min_tile
max_x, max_y, _ = max_tile
return itertools.product(range(min_x, max_x + 1), range(min_y, max_y + 1))
def _make_tiles(bbox, src_crs, minz, maxz):
"""
Given a bounding box, zoom range, and source crs,
find all tiles that would intersect
Parameters
-----------
bbox: list
[w, s, e, n] bounds
src_crs: str
the source crs of the input bbox
minz: int
minumum zoom to find tiles for
maxz: int
maximum zoom to find tiles for
Returns
--------
tiles: generator
generator of [x, y, z] tiles that intersect
the provided bounding box
"""
w, s, e, n = transform_bounds(*[src_crs, "EPSG:4326"] + bbox, densify_pts=0)
EPSILON = 1.0e-10
w += EPSILON
s += EPSILON
e -= EPSILON
n -= EPSILON
for z in range(minz, maxz + 1):
for x, y in _tile_range(mercantile.tile(w, n, z), mercantile.tile(e, s, z)):
yield [x, y, z]
class RGBTiler:
"""
Takes continous source data of an arbitrary bit depth and encodes it
in parallel into RGB tiles in an MBTiles file. Provided with a context manager:
```
with RGBTiler(inpath, outpath, min_z, max_x, **kwargs) as tiler:
tiler.run(processes)
```
Parameters
-----------
inpath: string
filepath of the source file to read and encode
outpath: string
filepath of the output `mbtiles`
min_z: int
minimum zoom level to tile
max_z: int
maximum zoom level to tile
Keyword Arguments
------------------
baseval: float
the base value of the RGB numbering system.
(will be treated as zero for this encoding)
Default=0
interval: float
the interval at which to encode
Default=1
round_digits: int
Erased less significant digits
Default=0
format: str
output tile image format (png or webp)
Default=png
bounding_tile: list
[x, y, z] of bounding tile; limits tiled output to this extent
Returns
--------
None
"""
def __init__(
self,
inpath,
outpath,
min_z,
max_z,
interval=1,
base_val=0,
round_digits=0,
bounding_tile=None,
**kwargs
):
self.run_function = _tile_worker
self.inpath = inpath
self.outpath = outpath
self.min_z = min_z
self.max_z = max_z
self.bounding_tile = bounding_tile
if not "format" in kwargs:
writer_func = _encode_as_png
self.image_format = "png"
elif kwargs["format"].lower() == "png":
writer_func = _encode_as_png
self.image_format = "png"
elif kwargs["format"].lower() == "webp":
writer_func = _encode_as_webp
self.image_format = "webp"
else:
raise ValueError(
"{0} is not a supported filetype!".format(kwargs["format"])
)
# global kwargs not used if output is webp
self.global_args = {
"kwargs": {
"driver": "PNG",
"dtype": "uint8",
"height": 512,
"width": 512,
"count": 3,
"crs": "EPSG:3857",
},
"base_val": base_val,
"interval": interval,
"round_digits": round_digits,
"writer_func": writer_func,
}
def __enter__(self):
return self
def __exit__(self, ext_t, ext_v, trace):
if ext_t:
traceback.print_exc()
def run(self, processes=4):
"""
Warp, encode, and tile
"""
# get the bounding box + crs of the file to tile
with rasterio.open(self.inpath) as src:
bbox = list(src.bounds)
src_crs = src.crs
# remove the output filepath if it exists
if os.path.exists(self.outpath):
os.unlink(self.outpath)
# create a connection to the mbtiles file
conn = sqlite3.connect(self.outpath)
cur = conn.cursor()
# create the tiles table
cur.execute(
"CREATE TABLE tiles "
"(zoom_level integer, tile_column integer, "
"tile_row integer, tile_data blob);"
)
# create empty metadata
cur.execute("CREATE TABLE metadata (name text, value text);")
conn.commit()
# populate metadata with required fields
cur.execute(
"INSERT INTO metadata " "(name, value) " "VALUES ('format', ?);",
(self.image_format,),
)
cur.execute("INSERT INTO metadata " "(name, value) " "VALUES ('name', '');")
cur.execute(
"INSERT INTO metadata " "(name, value) " "VALUES ('description', '');"
)
cur.execute("INSERT INTO metadata " "(name, value) " "VALUES ('version', '1');")
cur.execute(
"INSERT INTO metadata " "(name, value) " "VALUES ('type', 'baselayer');"
)
conn.commit()
if processes == 1:
# use mock pool for profiling / debugging
self.pool = MockTub(
_main_worker, (self.inpath, self.run_function, self.global_args)
)
else:
self.pool = Pool(
processes,
_main_worker,
(self.inpath, self.run_function, self.global_args),
)
# generator of tiles to make
if self.bounding_tile is None:
tiles = _make_tiles(bbox, src_crs, self.min_z, self.max_z)
else:
constrained_bbox = list(mercantile.bounds(self.bounding_tile))
tiles = _make_tiles(constrained_bbox, "EPSG:4326", self.min_z, self.max_z)
for tile, contents in self.pool.imap_unordered(self.run_function, tiles):
x, y, z = tile
# mbtiles use inverse y indexing
tiley = int(math.pow(2, z)) - y - 1
# insert tile object
cur.execute(
"INSERT INTO tiles "
"(zoom_level, tile_column, tile_row, tile_data) "
"VALUES (?, ?, ?, ?);",
(z, x, tiley, buffer(contents)),
)
conn.commit()
conn.close()
self.pool.close()
self.pool.join()
return None