Skip to content

Commit e07b11e

Browse files
committed
updates
1 parent 50e7083 commit e07b11e

File tree

2 files changed

+137
-57
lines changed

2 files changed

+137
-57
lines changed

labelbox/data/annotation_types/data/tiled_image.py

Lines changed: 81 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import math
22
import logging
33
from enum import Enum
4-
from typing import Optional, List, Tuple
4+
from typing import Optional, List, Tuple, Any, Union
55
from concurrent.futures import ThreadPoolExecutor
66
from io import BytesIO
77

@@ -15,6 +15,11 @@
1515
from pydantic import BaseModel, validator
1616
from pydantic.class_validators import root_validator
1717

18+
from labelbox.data.annotation_types.geometry.polygon import Polygon
19+
from labelbox.data.annotation_types.geometry.point import Point
20+
from labelbox.data.annotation_types.geometry.line import Line
21+
from labelbox.data.annotation_types.geometry.rectangle import Rectangle
22+
1823
from ..geometry import Point
1924
from .base_data import BaseData
2025
from .raster import RasterData
@@ -326,56 +331,33 @@ class EPSGTransformer(BaseModel):
326331
Requires as input a Point object.
327332
"""
328333

329-
class ProjectionTransformer(Transformer):
330-
"""Custom class to help represent a Transformer that will play
331-
nicely with Pydantic.
332-
333-
Accepts a PyProj Transformer object.
334-
"""
335-
336-
@classmethod
337-
def __get_validators__(cls):
338-
yield cls.validate
339-
340-
@classmethod
341-
def validate(cls, v):
342-
if not isinstance(v, Transformer):
343-
raise Exception("Needs to be a Transformer class")
344-
return v
334+
class Config:
335+
arbitrary_types_allowed = True
345336

346-
transform_function: Optional[ProjectionTransformer] = None
337+
transformer: Any
347338

348-
def _is_simple(self, epsg: EPSG) -> bool:
339+
@staticmethod
340+
def _is_simple(epsg: EPSG) -> bool:
349341
return epsg == EPSG.SIMPLEPIXEL
350342

351-
def _get_ranges(self, bounds: np.ndarray):
343+
@staticmethod
344+
def _get_ranges(bounds: np.ndarray):
352345
"""helper function to get the range between bounds.
353346
354347
returns a tuple (x_range, y_range)"""
355348
x_range = np.max(bounds[:, 0]) - np.min(bounds[:, 0])
356349
y_range = np.max(bounds[:, 1]) - np.min(bounds[:, 1])
357350
return (x_range, y_range)
358351

359-
def _min_max_x_y(self, bounds: np.ndarray):
352+
@staticmethod
353+
def _min_max_x_y(bounds: np.ndarray):
360354
"""returns the min x, max x, min y, max y of a numpy array
361355
"""
362356
return np.min(bounds[:, 0]), np.max(bounds[:, 0]), np.min(
363357
bounds[:, 1]), np.max(bounds[:, 1])
364358

365-
def geo_and_geo(self, src_epsg: EPSG, tgt_epsg: EPSG) -> None:
366-
"""method to change from one projection to another projection.
367-
368-
supports EPSG transformations not Simple.
369-
"""
370-
if self._is_simple(src_epsg) or self._is_simple(tgt_epsg):
371-
raise Exception(
372-
f"Cannot be used for Simple transformations. Found {src_epsg} and {tgt_epsg}"
373-
)
374-
self.transform_function = Transformer.from_crs(src_epsg.value,
375-
tgt_epsg.value,
376-
always_xy=True).transform
377-
378-
def geo_and_pixel(self,
359+
@classmethod
360+
def geo_and_pixel(cls,
379361
src_epsg,
380362
pixel_bounds: TiledBounds,
381363
geo_bounds: TiledBounds,
@@ -386,11 +368,8 @@ def geo_and_pixel(self,
386368
geo_bounds_epsg = geo_bounds.epsg
387369
geo_bounds = geo_bounds.bounds
388370

389-
#TODO: think about renaming local/global?
390-
#local = pixel
391-
#global = geo
392371
local_bounds = np.array([(point.x, point.y) for point in pixel_bounds],
393-
dtype=np.int)
372+
dtype=int)
394373
#convert geo bounds to pixel bounds. assumes geo bounds are in wgs84/EPS4326 per leaflet
395374
global_bounds = np.array([
396375
PygeoPoint.from_latitude_longitude(latitude=point.y,
@@ -399,16 +378,16 @@ def geo_and_pixel(self,
399378
])
400379

401380
#get the range of pixels for both sets of bounds to use as a multiplification factor
402-
local_x_range, local_y_range = self._get_ranges(local_bounds)
403-
global_x_range, global_y_range = self._get_ranges(global_bounds)
381+
local_x_range, local_y_range = cls._get_ranges(bounds=local_bounds)
382+
global_x_range, global_y_range = cls._get_ranges(bounds=global_bounds)
404383

405384
if src_epsg == EPSG.SIMPLEPIXEL:
406385

407386
def transform(x: int, y: int):
408387
scaled_xy = (x * (global_x_range) / (local_x_range),
409388
y * (global_y_range) / (local_y_range))
410389

411-
minx, _, miny, _ = self._min_max_x_y(global_bounds)
390+
minx, _, miny, _ = cls._min_max_x_y(bounds=global_bounds)
412391
x, y = map(lambda i, j: i + j, scaled_xy, (minx, miny))
413392

414393
point = PygeoPoint.from_pixel(pixel_x=x, pixel_y=y,
@@ -419,23 +398,22 @@ def transform(x: int, y: int):
419398
always_xy=True).transform(
420399
point[1], point[0])
421400

422-
self.transform_function = transform
401+
return transform
423402

424-
#geo to pixel - converts a point in geo coords to pixel coords
425403
#handles 4326 from lat,lng
426404
elif src_epsg == EPSG.EPSG4326:
427405

428406
def transform(x: int, y: int):
429407
point_in_px = PygeoPoint.from_latitude_longitude(
430408
latitude=y, longitude=x).pixels(zoom)
431409

432-
minx, _, miny, _ = self._min_max_x_y(global_bounds)
410+
minx, _, miny, _ = cls._min_max_x_y(global_bounds)
433411
x, y = map(lambda i, j: i - j, point_in_px, (minx, miny))
434412

435413
return (x * (local_x_range) / (global_x_range),
436414
y * (local_y_range) / (global_y_range))
437415

438-
self.transform_function = transform
416+
return transform
439417

440418
#handles 3857 from meters
441419
elif src_epsg == EPSG.EPSG3857:
@@ -444,17 +422,67 @@ def transform(x: int, y: int):
444422
point_in_px = PygeoPoint.from_meters(meter_y=y,
445423
meter_x=x).pixels(zoom)
446424

447-
minx, _, miny, _ = self._min_max_x_y(global_bounds)
425+
minx, _, miny, _ = cls._min_max_x_y(global_bounds)
448426
x, y = map(lambda i, j: i - j, point_in_px, (minx, miny))
449427

450428
return (x * (local_x_range) / (global_x_range),
451429
y * (local_y_range) / (global_y_range))
452430

453-
self.transform_function = transform
431+
return transform
432+
433+
@classmethod
434+
def create_geo_to_geo_transformer(cls, src_epsg: EPSG,
435+
tgt_epsg: EPSG) -> None:
436+
"""method to change from one projection to another projection.
437+
438+
supports EPSG transformations not Simple.
439+
"""
440+
if cls._is_simple(epsg=src_epsg) or cls._is_simple(epsg=tgt_epsg):
441+
raise Exception(
442+
f"Cannot be used for Simple transformations. Found {src_epsg} and {tgt_epsg}"
443+
)
454444

455-
def __call__(self, point: Point):
456-
if self.transform_function is not None:
457-
res = self.transform_function(point.x, point.y)
458-
return Point(x=res[0], y=res[1])
445+
return EPSGTransformer(transformer=Transformer.from_crs(
446+
src_epsg.value, tgt_epsg.value, always_xy=True).transform)
447+
448+
@classmethod
449+
def create_geo_to_pixel_transformer(cls,
450+
src_epsg,
451+
pixel_bounds: TiledBounds,
452+
geo_bounds: TiledBounds,
453+
zoom=0):
454+
"""method to change from a geo projection to Simple"""
455+
456+
transform_function = cls.geo_and_pixel(src_epsg=src_epsg,
457+
pixel_bounds=pixel_bounds,
458+
geo_bounds=geo_bounds,
459+
zoom=zoom)
460+
return EPSGTransformer(transformer=transform_function)
461+
462+
@classmethod
463+
def create_pixel_to_geo_transformer(cls,
464+
src_epsg,
465+
pixel_bounds: TiledBounds,
466+
geo_bounds: TiledBounds,
467+
zoom=0):
468+
"""method to change from a geo projection to Simple"""
469+
transform_function = cls.geo_and_pixel(src_epsg=src_epsg,
470+
pixel_bounds=pixel_bounds,
471+
geo_bounds=geo_bounds,
472+
zoom=zoom)
473+
return EPSGTransformer(transformer=transform_function)
474+
475+
def _get_point_obj(self, point) -> Point:
476+
point = self.transformer(point.x, point.y)
477+
return Point(x=point[0], y=point[1])
478+
479+
def __call__(self, shape: Union[Point, Line, Rectangle, Polygon]):
480+
if isinstance(shape, Point):
481+
return self._get_point_obj(shape)
482+
if isinstance(shape, Line) or isinstance(shape, Polygon):
483+
return Line(points=[self._get_point_obj(p) for p in shape.points])
484+
if isinstance(shape, Rectangle):
485+
return Rectangle(start=self._get_point_obj(shape.start),
486+
end=self._get_point_obj(shape.end))
459487
else:
460-
raise Exception("No transformation has been set.")
488+
raise ValueError(f"Unsupported type found: {type(shape)}")

tests/data/annotation_types/test_tiled_image.py

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import pytest
2+
from labelbox.data.annotation_types.geometry.polygon import Polygon
23
from labelbox.data.annotation_types.geometry.point import Point
4+
from labelbox.data.annotation_types.geometry.line import Line
5+
from labelbox.data.annotation_types.geometry.rectangle import Rectangle
36
from labelbox.data.annotation_types.data.tiled_image import (EPSG, TiledBounds,
47
TileLayer,
5-
TiledImageData)
8+
TiledImageData,
9+
EPSGTransformer)
610
from pydantic import ValidationError
711

812

@@ -46,6 +50,54 @@ def test_create_tiled_image_data():
4650
assert tiled_image_data.zoom_levels == zoom_levels
4751

4852

49-
#TODO: create a test from 4326->SIMPLE->3857->4326
50-
def test_epsg_projections():
51-
pass
53+
def test_epsg_point_projections():
54+
zoom = 4
55+
56+
bounds_simple = TiledBounds(epsg=EPSG.SIMPLEPIXEL,
57+
bounds=[Point(x=0, y=0),
58+
Point(x=256, y=256)])
59+
60+
bounds_3857 = TiledBounds(epsg=EPSG.EPSG3857,
61+
bounds=[
62+
Point(x=-104.150390625, y=30.789036751261136),
63+
Point(x=-81.8701171875, y=45.920587344733654)
64+
])
65+
bounds_4326 = TiledBounds(epsg=EPSG.EPSG4326,
66+
bounds=[
67+
Point(x=-104.150390625, y=30.789036751261136),
68+
Point(x=-81.8701171875, y=45.920587344733654)
69+
])
70+
71+
point = Point(x=-11016716.012685884, y=5312679.21393289)
72+
point_two = Point(x=-12016716.012685884, y=5212679.21393289)
73+
point_three = Point(x=-13016716.012685884, y=5412679.21393289)
74+
75+
line = Line(points=[point, point_two, point_three])
76+
polygon = Polygon(points=[point, point_two, point_three])
77+
rectangle = Rectangle(start=point, end=point_three)
78+
79+
shapes_to_test = [point, line, polygon, rectangle]
80+
81+
transformer_3857_simple = EPSGTransformer.create_geo_to_pixel_transformer(
82+
src_epsg=EPSG.EPSG3857,
83+
pixel_bounds=bounds_simple,
84+
geo_bounds=bounds_3857,
85+
zoom=zoom)
86+
transformer_3857_4326 = EPSGTransformer.create_geo_to_geo_transformer(
87+
src_epsg=EPSG.EPSG3857,
88+
tgt_epsg=EPSG.EPSG4326,
89+
)
90+
transformer_4326_simple = EPSGTransformer.create_geo_to_pixel_transformer(
91+
src_epsg=EPSG.EPSG4326,
92+
pixel_bounds=bounds_simple,
93+
geo_bounds=bounds_4326,
94+
zoom=zoom)
95+
96+
for shape in shapes_to_test:
97+
shape_simple = transformer_3857_simple(shape=shape)
98+
99+
shape_4326 = transformer_3857_4326(shape=shape)
100+
101+
other_simple_shape = transformer_4326_simple(shape=shape_4326)
102+
103+
assert shape_simple == other_simple_shape

0 commit comments

Comments
 (0)