Skip to content

Commit 1c85c46

Browse files
committed
updates and changes. adding in projection from geo to pixel
1 parent ad8f434 commit 1c85c46

File tree

1 file changed

+77
-34
lines changed

1 file changed

+77
-34
lines changed

labelbox/data/annotation_types/data/tiled_image.py

Lines changed: 77 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88
import requests
99
import numpy as np
1010

11-
from retry import retry #TODO not part of the package atm. need to add in?
12-
import tensorflow as f
11+
from retry import retry
1312
from PIL import Image
1413
from pyproj import Transformer
15-
from pydantic import BaseModel, validator, conlist
14+
from pygeotile.point import Point as PygeoPoint
15+
from pydantic import BaseModel, validator
1616
from pydantic.class_validators import root_validator
1717

1818
from ..geometry import Point
@@ -27,6 +27,8 @@
2727
logging.basicConfig(level=logging.INFO)
2828
logger = logging.getLogger(__name__)
2929

30+
#TODO: need to add pyproj, pygeotile, retry to dependencies
31+
3032

3133
class EPSG(Enum):
3234
""" Provides the EPSG for tiled image assets that are currently supported.
@@ -61,8 +63,10 @@ def validate_bounds_not_equal(cls, bounds):
6163
first_bound = bounds[0]
6264
second_bound = bounds[1]
6365

64-
if first_bound == second_bound:
65-
raise AssertionError(f"Bounds cannot be equal, contains {bounds}")
66+
if first_bound.x == second_bound.x or \
67+
first_bound.y == second_bound.y:
68+
raise ValueError(
69+
f"Bounds on either axes cannot be equal, currently {bounds}")
6670
return bounds
6771

6872
#bounds are assumed to be in EPSG 4326 as that is what leaflet assumes
@@ -236,7 +240,9 @@ def _fetch_image_for_bounds(self,
236240
y_tile_end: int,
237241
zoom: int,
238242
multithread=True) -> np.ndarray:
239-
"""Fetches the tiles and combines them into a single image
243+
"""Fetches the tiles and combines them into a single image.
244+
245+
If a tile cannot be fetched, a padding of expected tile size is instead added.
240246
"""
241247
tiles = {}
242248
if multithread:
@@ -248,16 +254,25 @@ def _fetch_image_for_bounds(self,
248254

249255
rows = []
250256
for y in range(y_tile_start, y_tile_end + 1):
251-
rows.append(
252-
np.hstack([
253-
tiles[(x, y)].result()
254-
for x in range(x_tile_start, x_tile_end + 1)
255-
]))
257+
row = []
258+
for x in range(x_tile_start, x_tile_end + 1):
259+
try:
260+
row.append(tiles[(x, y)].result())
261+
except:
262+
row.append(
263+
np.zeros(shape=(self.tile_size, self.tile_size, 3),
264+
dtype=np.uint8))
265+
rows.append(np.hstack(row))
256266
#no multithreading
257267
else:
258268
for x in range(x_tile_start, x_tile_end + 1):
259269
for y in range(y_tile_start, y_tile_end + 1):
260-
tiles[(x, y)] = self._fetch_tile(x, y, zoom)
270+
try:
271+
tiles[(x, y)] = self._fetch_tile(x, y, zoom)
272+
except:
273+
tiles[(x, y)] = np.zeros(shape=(self.tile_size,
274+
self.tile_size, 3),
275+
dtype=np.uint8)
261276

262277
rows = []
263278
for y in range(y_tile_start, y_tile_end + 1):
@@ -269,26 +284,18 @@ def _fetch_image_for_bounds(self,
269284

270285
return np.vstack(rows)
271286

272-
@retry(delay=1, tries=6, backoff=2, max_delay=16)
287+
@retry(delay=1, tries=5, backoff=2, max_delay=8)
273288
def _fetch_tile(self, x: int, y: int, z: int) -> np.ndarray:
274289
"""
275-
Fetches the image and returns an np array. If the image cannot be fetched,
276-
a padding of expected tile size is instead added.
290+
Fetches the image and returns an np array.
277291
"""
278-
try:
279-
data = requests.get(self.tile_layer.url.format(x=x, y=y, z=z))
280-
data.raise_for_status()
281-
decoded = np.array(Image.open(BytesIO(data.content)))[..., :3]
282-
if decoded.shape[:2] != (self.tile_size, self.tile_size):
283-
logger.warning(
284-
f"Unexpected tile size {decoded.shape}. Results aren't guarenteed to be correct."
285-
)
286-
except:
292+
data = requests.get(self.tile_layer.url.format(x=x, y=y, z=z))
293+
data.raise_for_status()
294+
decoded = np.array(Image.open(BytesIO(data.content)))[..., :3]
295+
if decoded.shape[:2] != (self.tile_size, self.tile_size):
287296
logger.warning(
288-
f"Unable to successfully find tile. for z,x,y: {z},{x},{y} "
289-
"Padding is being added as a result.")
290-
decoded = np.zeros(shape=(self.tile_size, self.tile_size, 3),
291-
dtype=np.uint8)
297+
f"Unexpected tile size {decoded.shape}. Results aren't guarenteed to be correct."
298+
)
292299
return decoded
293300

294301
def _crop_to_bounds(
@@ -334,7 +341,6 @@ def validate_zoom_levels(cls, zoom_levels):
334341
return zoom_levels
335342

336343

337-
#TODO: we will need to update the [data] package to also require pyproj
338344
class EPSGTransformer(BaseModel):
339345
"""Transformer class between different EPSG's. Useful when wanting to project
340346
in different formats.
@@ -370,15 +376,52 @@ def geo_and_geo(self, src_epsg: EPSG, tgt_epsg: EPSG) -> None:
370376
f"Cannot be used for Simple transformations. Found {src_epsg} and {tgt_epsg}"
371377
)
372378
self.transform_function = Transformer.from_crs(src_epsg.value,
373-
tgt_epsg.value)
379+
tgt_epsg.value).transform
380+
381+
def _get_ranges(self, bounds: np.ndarray):
382+
"""helper function to get the range between bounds.
383+
384+
returns a tuple (x_range, y_range)"""
385+
x_range = np.max(bounds[:, 0]) - np.min(bounds[:, 0])
386+
y_range = np.max(bounds[:, 1]) - np.min(bounds[:, 1])
387+
return (x_range, y_range)
388+
389+
def geo_and_pixel(self,
390+
src_epsg,
391+
pixel_bounds: TiledBounds,
392+
geo_bounds: TiledBounds,
393+
zoom=0):
394+
#TODO: pixel to geo
395+
if src_epsg == EPSG.SIMPLEPIXEL:
396+
pass
397+
398+
#geo to pixel - converts a point in geo coords to pixel coords
399+
else:
400+
pixel_bounds = pixel_bounds.bounds
401+
geo_bounds = geo_bounds.bounds
402+
403+
local_bounds = np.array(
404+
[(point.x, point.y) for point in pixel_bounds], dtype=np.int)
405+
#convert geo bounds to pixel bounds. assumes geo bounds are in wgs84/EPS4326 per leaflet
406+
global_bounds = np.array([
407+
PygeoPoint.from_latitude_longitude(
408+
latitude=point.y, longitude=point.x).pixels(zoom)
409+
for point in geo_bounds
410+
])
411+
412+
#get the range of pixels for both sets of bounds to use as a multiplification factor
413+
global_x_range, global_y_range = self._get_ranges(global_bounds)
414+
local_x_range, local_y_range = self._get_ranges(local_bounds)
415+
416+
def transform(x: int, y: int):
417+
return (x * (local_x_range) / (global_x_range),
418+
y * (local_y_range) / (global_y_range))
374419

375-
#TODO
376-
def geo_and_pixel(self, src_epsg, geojson):
377-
pass
420+
self.transform_function = transform
378421

379422
def __call__(self, point: Point):
380423
if self.transform_function is not None:
381-
res = self.transform_function.transform(point.x, point.y)
424+
res = self.transform_function(point.x, point.y)
382425
return Point(x=res[0], y=res[1])
383426
else:
384427
raise Exception("No transformation has been set.")

0 commit comments

Comments
 (0)