11"""Models for the base data types."""
22
3- from enum import Enum
4- from typing import List , Tuple , NamedTuple
5- import numpy as np
63import math
7- from shapely .geometry import Polygon
4+ from enum import Enum
5+ from typing import List , NamedTuple , Tuple , Union
86
7+ import numpy as np
98from pydantic import BaseModel
9+ from shapely .geometry import Polygon
1010
1111
1212class ImageRefMode (str , Enum ):
@@ -373,7 +373,9 @@ def is_horizontally_connected(
373373 return False
374374
375375 @classmethod
376- def enclosing_bbox (cls , boxes : List ["BoundingBox" ]) -> "BoundingBox" :
376+ def enclosing_bbox (
377+ cls , boxes : List [Union ["BoundingBox" , "BoundingRectangle" ]]
378+ ) -> "BoundingBox" :
377379 """Create a bounding box that covers all of the given boxes."""
378380 if not boxes :
379381 raise ValueError ("No bounding boxes provided for union." )
@@ -385,15 +387,21 @@ def enclosing_bbox(cls, boxes: List["BoundingBox"]) -> "BoundingBox":
385387 CoordOrigin to compute their union."
386388 )
387389
388- left = min (box .l for box in boxes )
389- right = max (box .r for box in boxes )
390+ # transform every BRectangle in the encloser BBox
391+ boxes_post = [
392+ box .to_bounding_box () if isinstance (box , BoundingRectangle ) else box
393+ for box in boxes
394+ ]
395+
396+ left = min (box .l for box in boxes_post )
397+ right = max (box .r for box in boxes_post )
390398
391399 if origin == CoordOrigin .TOPLEFT :
392- top = min (box .t for box in boxes )
393- bottom = max (box .b for box in boxes )
400+ top = min (box .t for box in boxes_post )
401+ bottom = max (box .b for box in boxes_post )
394402 elif origin == CoordOrigin .BOTTOMLEFT :
395- top = max (box .t for box in boxes )
396- bottom = min (box .b for box in boxes )
403+ top = max (box .t for box in boxes_post )
404+ bottom = min (box .b for box in boxes_post )
397405 else :
398406 raise ValueError ("BoundingBoxes have different CoordOrigin" )
399407
@@ -437,6 +445,7 @@ def y_union_with(self, other: "BoundingBox") -> float:
437445 return max (0.0 , max (self .t , other .t ) - min (self .b , other .b ))
438446 raise ValueError ("Unsupported CoordOrigin" )
439447
448+
440449class Coord2D (NamedTuple ):
441450 """A 2D coordinate with x and y components."""
442451
@@ -503,6 +512,82 @@ def centre(self):
503512 self .r_y0 + self .r_y1 + self .r_y2 + self .r_y3
504513 ) / 4.0
505514
515+ @property
516+ def l (self ): # noqa: E743
517+ """Left value of the inclosing rectangle."""
518+ return min ([self .r_x0 , self .r_x1 , self .r_x2 , self .r_x3 ])
519+
520+ @property
521+ def r (self ):
522+ """Right value of the inclosing rectangle."""
523+ return max ([self .r_x0 , self .r_x1 , self .r_x2 , self .r_x3 ])
524+
525+ @property
526+ def t (self ):
527+ """Top value of the inclosing rectangle."""
528+ if self .coord_origin == CoordOrigin .BOTTOMLEFT :
529+ top = max ([self .r_y0 , self .r_y1 , self .r_y2 , self .r_y3 ])
530+ else :
531+ top = min ([self .r_y0 , self .r_y1 , self .r_y2 , self .r_y3 ])
532+ return top
533+
534+ @property
535+ def b (self ):
536+ """Bottom value of the inclosing rectangle."""
537+ if self .coord_origin == CoordOrigin .BOTTOMLEFT :
538+ bottom = min ([self .r_y0 , self .r_y1 , self .r_y2 , self .r_y3 ])
539+ else :
540+ bottom = max ([self .r_y0 , self .r_y1 , self .r_y2 , self .r_y3 ])
541+ return bottom
542+
543+ def resize_by_scale (self , x_scale : float , y_scale : float ):
544+ """resize_by_scale."""
545+ rect_to_bbox = self .to_bounding_box ()
546+ return BoundingBox (
547+ l = rect_to_bbox .l * x_scale ,
548+ r = rect_to_bbox .r * x_scale ,
549+ t = rect_to_bbox .t * y_scale ,
550+ b = rect_to_bbox .b * y_scale ,
551+ coord_origin = self .coord_origin ,
552+ )
553+
554+ def scale_to_size (self , old_size : Size , new_size : Size ):
555+ """scale_to_size."""
556+ return self .resize_by_scale (
557+ x_scale = new_size .width / old_size .width ,
558+ y_scale = new_size .height / old_size .height ,
559+ )
560+
561+ def scaled (self , scale : float ):
562+ """scaled."""
563+ return self .resize_by_scale (x_scale = scale , y_scale = scale )
564+
565+ def normalized (self , page_size : Size ):
566+ """normalized."""
567+ return self .scale_to_size (
568+ old_size = page_size , new_size = Size (height = 1.0 , width = 1.0 )
569+ )
570+
571+ def expand_by_scale (self , x_scale : float , y_scale : float ) -> "BoundingBox" :
572+ """expand_to_size."""
573+ rect_to_bbox = self .to_bounding_box ()
574+ if self .coord_origin == CoordOrigin .TOPLEFT :
575+ return BoundingBox (
576+ l = rect_to_bbox .l - rect_to_bbox .width * x_scale ,
577+ r = rect_to_bbox .r + rect_to_bbox .width * x_scale ,
578+ t = rect_to_bbox .t - rect_to_bbox .height * y_scale ,
579+ b = rect_to_bbox .b + rect_to_bbox .height * y_scale ,
580+ coord_origin = self .coord_origin ,
581+ )
582+ elif self .coord_origin == CoordOrigin .BOTTOMLEFT :
583+ return BoundingBox (
584+ l = rect_to_bbox .l - rect_to_bbox .width * x_scale ,
585+ r = rect_to_bbox .r + rect_to_bbox .width * x_scale ,
586+ t = rect_to_bbox .t + rect_to_bbox .height * y_scale ,
587+ b = rect_to_bbox .b - rect_to_bbox .height * y_scale ,
588+ coord_origin = self .coord_origin ,
589+ )
590+
506591 def to_bounding_box (self ) -> BoundingBox :
507592 """Convert to a BoundingBox representation."""
508593 if self .coord_origin == CoordOrigin .BOTTOMLEFT :
@@ -524,8 +609,12 @@ def to_bounding_box(self) -> BoundingBox:
524609 )
525610
526611 @classmethod
527- def from_bounding_box (cls , bbox : BoundingBox ) -> "BoundingRectangle" :
612+ def from_bounding_box (
613+ cls , bbox : Union ["BoundingRectangle" , BoundingBox ]
614+ ) -> "BoundingRectangle" :
528615 """Convert a BoundingBox into a BoundingRectangle."""
616+ if isinstance (bbox , BoundingRectangle ):
617+ return bbox
529618 return cls (
530619 r_x0 = bbox .l ,
531620 r_y0 = bbox .b ,
@@ -546,7 +635,7 @@ def to_polygon(self) -> List[Coord2D]:
546635 Coord2D (self .r_x2 , self .r_y2 ),
547636 Coord2D (self .r_x3 , self .r_y3 ),
548637 ]
549-
638+
550639 def to_list (self ) -> List [Tuple ]:
551640 """Convert to a list of tuple point coordinates."""
552641 return [
@@ -555,14 +644,30 @@ def to_list(self) -> List[Tuple]:
555644 (self .r_x2 , self .r_y2 ),
556645 (self .r_x3 , self .r_y3 ),
557646 ]
558-
647+
648+ def as_tuple (self ) -> Tuple [float , float , float , float , float , float , float , float ]:
649+ """as_tuple."""
650+ return (
651+ self .r_x0 ,
652+ self .r_y0 ,
653+ self .r_x1 ,
654+ self .r_y1 ,
655+ self .r_x2 ,
656+ self .r_y2 ,
657+ self .r_x3 ,
658+ self .r_y3 ,
659+ )
660+
559661 def to_shapely_polygon (self ) -> Polygon :
560- return Polygon ([
561- (self .r_x0 , self .r_y0 ),
562- (self .r_x1 , self .r_y1 ),
563- (self .r_x2 , self .r_y2 ),
564- (self .r_x3 , self .r_y3 ),
565- ])
662+ """To shapely polygon."""
663+ return Polygon (
664+ [
665+ (self .r_x0 , self .r_y0 ),
666+ (self .r_x1 , self .r_y1 ),
667+ (self .r_x2 , self .r_y2 ),
668+ (self .r_x3 , self .r_y3 ),
669+ ]
670+ )
566671
567672 def to_bottom_left_origin (self , page_height : float ) -> "BoundingRectangle" :
568673 """Convert coordinates to use bottom-left origin.
@@ -615,14 +720,11 @@ def to_top_left_origin(self, page_height: float) -> "BoundingRectangle":
615720 def intersection_over_union (
616721 self , other : "BoundingRectangle" , eps : float = 1.0e-6
617722 ) -> float :
618- """intersection_over_union."""
619-
723+ """Intersection_over_union."""
620724 polygon_other = other .to_shapely_polygon ()
621725 current_polygon = self .to_shapely_polygon ()
622-
726+
623727 intersection_area = current_polygon .intersection (polygon_other ).area
624728 union_area = current_polygon .union (polygon_other ).area
625-
626- return intersection_area / (union_area + eps )
627-
628729
730+ return intersection_area / (union_area + eps )
0 commit comments