Skip to content

Commit 37eeeb5

Browse files
Martha Morrisseydrewbo
Martha Morrissey
andauthored
Supertiles (#172)
* super tile start * writing windows * working super tiles * clean up supertiles take 1 * generalize over zoom to work for going up x number of zoom levels as specified in config * remove prints * test for overzoom * clean up main * fix test * fix test again * circle ci env variable for config * fix images command * config format * circle token * change how tokens are read * remove print * config env variable * config env variable * fix circler ci yaml * fix how env is injected * fix config * fix environment variables in tox * option to read access token as environment variable * update docs about access token * Minor supertiling cleanup Co-authored-by: Drew Bollinger <[email protected]>
1 parent 5f15bcd commit 37eeeb5

File tree

7 files changed

+107
-8
lines changed

7 files changed

+107
-8
lines changed

.circleci/config.yml

+4-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ common: &common
1111
command: .circleci/install_tippecanoe.sh
1212
- run:
1313
name: run tox
14-
command: ~/.local/bin/tox
14+
command: |
15+
~/.local/bin/tox
16+
1517
jobs:
1618
"python-3.6":
1719
<<: *common
@@ -94,4 +96,4 @@ workflows:
9496
tags:
9597
only: /^[0-9]+.*/
9698
branches:
97-
ignore: /.*/
99+
ignore: /.*/

docs/parameters.rst

+5-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Here is the full list of configuration parameters you can specify in a ``config.
2929
Label Maker expects to receive imagery tiles that are 256 x 256 pixels. You can specific the source of the imagery with one of:
3030

3131
A template string for a tiled imagery service. Note that you will generally need an API key to obtain images and there may be associated costs. The above example requires a `Mapbox access token <https://www.mapbox.com/help/how-access-tokens-work/>`_. Also see `OpenAerialMap <https://openaerialmap.org/>`_ for open imagery.
32+
The access token for TMS image formats can be read from an environment variable https://api.mapbox.com/v4/mapbox.satellite/{z}/{x}/{y}.jpg?access_token={ACCESS_TOKEN}" or added directly the imagery string.
3233

3334
A GeoTIFF file location. Works with local files: ``'http://oin-hotosm.s3.amazonaws.com/593ede5ee407d70011386139/0/3041615b-2bdb-40c5-b834-36f580baca29.tif'``
3435

@@ -67,4 +68,7 @@ Here is the full list of configuration parameters you can specify in a ``config.
6768
An optional list of integers representing the number of pixels to offset imagery. For example ``[15, -5]`` will move the images 15 pixels right and 5 pixels up relative to the requested tile bounds.
6869

6970
**tms_image_format**: string
70-
An option string that has the downloaded imagery's format such as `.jpg` or `.png` when it isn't provided by the endpoint
71+
An option string that has the downloaded imagery's format such as `.jpg` or `.png` when it isn't provided by the endpoint
72+
73+
**over_zoom**: int
74+
An integer greater than 0. If set for XYZ tiles, it will fetch tiles from `zoom` + `over_zoom`, to create higher resolution tiles which fill out the bounds of the original zoom level.

label_maker/utils.py

+49-3
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
11
# pylint: disable=unused-argument
22
"""Provide utility functions"""
3+
import os
34
from os import path as op
45
from urllib.parse import urlparse, parse_qs
56

6-
from mercantile import bounds
7+
from mercantile import bounds, Tile, children
78
from PIL import Image
9+
import io
810
import numpy as np
911
import requests
1012
import rasterio
1113
from rasterio.crs import CRS
1214
from rasterio.warp import transform, transform_bounds
15+
from rasterio.windows import Window
1316

1417
WGS84_CRS = CRS.from_epsg(4326)
1518

19+
class SafeDict(dict):
20+
def __missing__(self, key):
21+
return '{' + key + '}'
22+
1623
def url(tile, imagery):
1724
"""Return a tile url provided an imagery template and a tile"""
1825
return imagery.replace('{x}', tile[0]).replace('{y}', tile[1]).replace('{z}', tile[2])
@@ -40,11 +47,50 @@ def download_tile_tms(tile, imagery, folder, kwargs):
4047

4148
image_format = get_image_format(imagery, kwargs)
4249

50+
if os.environ.get('ACCESS_TOKEN'):
51+
token = os.environ.get('ACCESS_TOKEN')
52+
imagery = imagery.format_map(SafeDict(ACCESS_TOKEN=token))
53+
4354
r = requests.get(url(tile.split('-'), imagery),
4455
auth=kwargs.get('http_auth'))
4556
tile_img = op.join(folder, '{}{}'.format(tile, image_format))
46-
with open(tile_img, 'wb')as w:
47-
w.write(r.content)
57+
tile = tile.split('-')
58+
59+
over_zoom = kwargs.get('over_zoom')
60+
if over_zoom:
61+
new_zoom = over_zoom + kwargs.get('zoom')
62+
# get children
63+
child_tiles = children(int(tile[0]), int(tile[1]), int(tile[2]), zoom=new_zoom)
64+
child_tiles.sort()
65+
66+
new_dim = 256 * (2 * over_zoom)
67+
68+
w_lst = []
69+
for i in range (2 * over_zoom):
70+
for j in range(2 * over_zoom):
71+
window = Window(i * 256, j * 256, 256, 256)
72+
w_lst.append(window)
73+
74+
# request children
75+
with rasterio.open(tile_img, 'w', driver='jpeg', height=new_dim,
76+
width=new_dim, count=3, dtype=rasterio.uint8) as w:
77+
for num, t in enumerate(child_tiles):
78+
t = [str(t[0]), str(t[1]), str(t[2])]
79+
r = requests.get(url(t, imagery),
80+
auth=kwargs.get('http_auth'))
81+
img = np.array(Image.open(io.BytesIO(r.content)), dtype=np.uint8)
82+
try:
83+
img = img.reshape((256, 256, 3)) # 4 channels returned from some endpoints, but not all
84+
except ValueError:
85+
img = img.reshape((256, 256, 4))
86+
img = img[:, :, :3]
87+
img = np.rollaxis(img, 2, 0)
88+
w.write(img, window=w_lst[num])
89+
else:
90+
r = requests.get(url(tile, imagery),
91+
auth=kwargs.get('http_auth'))
92+
with open(tile_img, 'wb')as w:
93+
w.write(r.content)
4894
return tile_img
4995

5096
def get_tile_tif(tile, imagery, folder, kwargs):

label_maker/validate.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,6 @@
3434
'imagery_offset': {'type': 'list', 'schema': {'type': 'integer'}, 'minlength': 2, 'maxlength': 2},
3535
'split_vals': {'type': 'list', 'schema': {'type': 'float'}},
3636
'split_names': {'type': 'list', 'schema': {'type': 'string'}},
37-
'tms_image_format': {'type': 'string'}
37+
'tms_image_format': {'type': 'string'},
38+
'over_zoom': {'type': 'integer', 'min': 1}
3839
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{"country": "portugal",
2+
"bounding_box": [
3+
-9.4575,
4+
38.8467,
5+
-9.4510,
6+
38.8513
7+
],
8+
"zoom": 17,
9+
"classes": [
10+
{ "name": "Water Tower", "filter": ["==", "man_made", "water_tower"] },
11+
{ "name": "Building", "filter": ["has", "building"] },
12+
{ "name": "Farmland", "filter": ["==", "landuse", "farmland"] },
13+
{ "name": "Ruins", "filter": ["==", "historic", "ruins"] },
14+
{ "name": "Parking", "filter": ["==", "amenity", "parking"] },
15+
{ "name": "Roads", "filter": ["has", "highway"] }
16+
],
17+
"imagery": "https://api.mapbox.com/v4/mapbox.satellite/{z}/{x}/{y}.jpg?access_token={ACCESS_TOKEN}",
18+
"background_ratio": 1,
19+
"ml_type": "classification",
20+
"seed": 19,
21+
"split_names": ["train", "test", "val"],
22+
"split_vals": [0.7, 0.2, 0.1],
23+
"over_zoom": 1
24+
}

test/integration/test_classification_package.py

+21
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ def setUpClass(cls):
2121
copyfile('test/fixtures/integration/labels-cl.npz', 'integration-cl-split/labels.npz')
2222
copytree('test/fixtures/integration/tiles', 'integration-cl-split/tiles')
2323

24+
25+
makedirs('integration-cl-overzoom')
26+
copyfile('test/fixtures/integration/labels-cl.npz', 'integration-cl-overzoom/labels.npz')
27+
2428
makedirs('integration-cl-img-f')
2529
copyfile('test/fixtures/integration/labels-cl-img-f.npz', 'integration-cl-img-f/labels.npz')
2630
copytree('test/fixtures/integration/tiles_png', 'integration-cl-img-f/tiles')
@@ -29,6 +33,7 @@ def setUpClass(cls):
2933
def tearDownClass(cls):
3034
rmtree('integration-cl')
3135
rmtree('integration-cl-split')
36+
rmtree('integration-cl-overzoom')
3237
rmtree('integration-cl-img-f')
3338

3439
def test_cli(self):
@@ -80,6 +85,22 @@ def test_cli_3way_split(self):
8085
self.assertEqual(data['y_test'].shape, (2, 7))
8186
self.assertEqual(data['y_val'].shape, (1, 7))
8287

88+
def test_overzoom(self):
89+
"""Verify data.npz produced by CLI when overzoom is used"""
90+
cmd = 'label-maker images --dest integration-cl-overzoom --config test/fixtures/integration/config_overzoom.integration.json'
91+
cmd = cmd.split(' ')
92+
subprocess.run(cmd, universal_newlines=True)
93+
94+
cmd = 'label-maker package --dest integration-cl-overzoom --config test/fixtures/integration/config_overzoom.integration.json'
95+
cmd = cmd.split(' ')
96+
subprocess.run(cmd, universal_newlines=True)
97+
98+
data = np.load('integration-cl-overzoom/data.npz')
99+
100+
self.assertEqual(data['x_train'].shape, (6, 512, 512, 3))
101+
self.assertEqual(data['x_test'].shape, (2, 512, 512, 3))
102+
self.assertEqual(data['x_val'].shape, (1, 512, 512, 3))
103+
83104
def test_tms_img_format(self):
84105
"""Verify data.npz produced by CLI"""
85106

tox.ini

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
envlist = py37,py36
33

44
[testenv]
5+
passenv = ACCESS_TOKEN
56
extras = test
67
commands=
78
python -m pytest --cov label_maker --cov-report term-missing --ignore=venv
@@ -46,4 +47,4 @@ include_trailing_comma = True
4647
multi_line_output = 3
4748
line_length = 90
4849
known_first_party = label_maker
49-
default_section = THIRDPARTY
50+
default_section = THIRDPARTY

0 commit comments

Comments
 (0)