11import math
22import logging
33from enum import Enum
4- from typing import Optional , List , Tuple
4+ from typing import Optional , List , Tuple , Any , Union
55from concurrent .futures import ThreadPoolExecutor
66from io import BytesIO
77
1515from pydantic import BaseModel , validator
1616from 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+
1823from ..geometry import Point
1924from .base_data import BaseData
2025from .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 ) } " )
0 commit comments