Ë
    ðì�i¼5  ã                   óV   — d Z ddlZddlmZ ddlmZ ddlmZ ddl	m
Z
  G d„ de«      Zy)	zY
PassportEye::Util: Geometry & math utilities

Author: Konstantin Tretyakov
License: MIT
é    N)ÚPCA)Úpyplot)Úpatches)Ú	transformc                   óŒ   — e Zd ZdZdd„Zd„ Zed„ «       Zed„ «       Zed„ «       Z	dd„Z
d	„ Zdd
„Zdd„Zdd„Zd„ Zedd„«       Zy)Ú
RotatedBoxa  
    RotatedBox represents a rectangular box centered at (cx,cy) with dimensions width x height,
    rotated by angle radians counterclockwise.

    >>> RotatedBox.from_points([[0,0], [2,1], [0,1], [2,0]])
    RotatedBox(cx=1.0, cy=0.5, width=2.0, height=1.0, angle=0.0)
    Nc                 ó�   — t        j                  |t         j                  ¬«      | _        || _        || _        || _        || _        y)z‹Creates a new RotatedBox.

        :param points: This parameter may be used to indicate the set of points used to create the box.
        ©ÚdtypeN)ÚnpÚasarrayÚfloat64ÚcenterÚwidthÚheightÚangleÚpoints)Úselfr   r   r   r   r   s         ú~/var/www/ITT-ServiceAPI-Services/ITT-Document-Scan/mrzscanner/mrzenv/lib/python3.12/site-packages/passporteye/util/geometry.pyÚ__init__zRotatedBox.__init__   s6   € ô
 —j‘j ¬r¯z©zÔ:ˆŒØˆŒ
ØˆŒØˆŒ
Øˆ�ó    c                 ó�   — dj                  | j                  | j                  | j                  | j                  | j
                  «      S )Nz<RotatedBox(cx={0}, cy={1}, width={2}, height={3}, angle={4}))ÚformatÚcxÚcyr   r   r   ©r   s    r   Ú__repr__zRotatedBox.__repr__"   sP   € ØM×TÑTÐUY×U\ÑU\Ð^b×^eÑ^eÐgk×gqÑgqÐsw×s~Ñs~ð  AE÷  AKñ  AKó  Lð  	Lr   c                 ó    — | j                   d   S )Nr   ©r   r   s    r   r   zRotatedBox.cx%   ó   € à�{‰{˜1‰~Ðr   c                 ó    — | j                   d   S )Né   r   r   s    r   r   zRotatedBox.cy)   r    r   c                 ó4   — | j                   | j                  z  S ©N)r   r   r   s    r   ÚareazRotatedBox.area-   s   € à�z‰z˜DŸK™KÑ'Ð'r   c                 ó,  — t        | j                  |d   z
  «      |k  xru t        | j                  |d   z
  «      |k  xrU t        | j                  |z
  «      |k  xr8 t        | j                  |z
  «      |k  xr t        | j
                  |z
  «      |k  S )z Method mainly useful for testingr   r"   )Úabsr   r   r   r   r   )r   r   r   r   r   Útols         r   Úapprox_equalzRotatedBox.approx_equal1   sš   € ä�4—7‘7˜V A™YÑ&Ó'¨#Ñ-ò Q´#°d·g±gÀÀqÁ	Ñ6IÓ2JÈSÑ2Pò QÔUXÐY]×YcÑYcÐfkÑYkÓUlÐorÑUrò QÜ�4—;‘; Ñ'Ó(¨3Ñ.òQÜ36°t·z±zÀEÑ7IÓ3JÈSÑ3Pð	Qr   c                 ó  — t        j                  t        j                  |«      t        j                  |«      gt        j                  |«       t        j                  |«      gg«      }t        j                  |t         j
                  ¬«      }t        j                  |j                  | j                  |z
  «      |z   }t        || j                  | j                  | j                  |z   t         j                  dz  z  «      S )zÞReturns a RotatedBox that is obtained by rotating this box around a given center by a given angle.

        >>> assert RotatedBox([2, 2], 2, 1, 0.1).rotated([1, 1], np.pi/2).approx_equal([0, 2], 2, 1, np.pi/2+0.1)
        r
   é   )r   ÚarrayÚcosÚsinr   r   ÚdotÚTr   r   r   r   r   Úpi)r   Úrotation_centerr   ÚrotÚtÚnew_cs         r   ÚrotatedzRotatedBox.rotated6   s­   € ô
 �h‰hœŸ™ ›¬¯©¨u«Ð6¼"¿&¹&À»-¸ÌÏÉÐPUËÐ8WÐXÓYˆÜ�J‰J�¬b¯j©jÔ9ˆÜ—‘�s—u‘u˜tŸ{™{¨Q™Ó0°1Ñ4ˆÜ˜% §¡¨T¯[©[¸4¿:¹:ÀeÑ;KÔPR×PUÑPUÐVWÑPWÑ:XÓYÐYr   c                 óL  — | j                   dz  |z   t        j                  t        j                  | j                  «      t        j
                  | j                  «      g«      z  }| j                  dz  |z   t        j                  t        j
                  | j                  «       t        j                  | j                  «      g«      z  }t        j                  | j                  | j                  g«      }t        j                  ||z
  |z
  ||z   |z
  ||z   |z   ||z
  |z   g«      S )ay  Converts this box to a polygon, i.e. 4x2 array, representing the four corners starting from lower left to upper left counterclockwise.

        :param margin_width: The additional "margin" that will be added to the box along its width dimension (from both sides) before conversion.
        :param margin_height: The additional "margin" that will be added to the box along its height dimension (from both sides) before conversion.

        >>> RotatedBox([0, 0], 4, 2, 0).as_poly()
        array([[-2., -1.],
               [ 2., -1.],
               [ 2.,  1.],
               [-2.,  1.]])
        >>> RotatedBox([0, 0], 4, 2, np.pi/4).as_poly()
        array([[-0.707..., -2.121...],
               [ 2.121...,  0.707...],
               [ 0.707...,  2.121...],
               [-2.121..., -0.707...]])
        >>> RotatedBox([0, 0], 4, 2, np.pi/2).as_poly()
        array([[ 1., -2.],
               [ 1.,  2.],
               [-1.,  2.],
               [-1., -2.]])
        >>> RotatedBox([0, 0], 0, 0, np.pi/2).as_poly(2, 1)
        array([[ 1., -2.],
               [ 1.,  2.],
               [-1.,  2.],
               [-1., -2.]])
        r+   )
r   r   r,   r-   r   r.   r   r   r   Úvstack)r   Úmargin_widthÚmargin_heightÚv_horÚv_vertÚcs         r   Úas_polyzRotatedBox.as_poly@   sé   € ð6 —‘˜A‘ Ñ,¬b¯h©h¼¿¹¸t¿z¹zÓ8JÌBÏFÉFÐSW×S]ÑS]ÓL^Ð7_Ó.`Ñ`ˆØ—+‘+˜a‘- -Ñ/´·±¼B¿F¹FÀ4Ç:Á:Ó<NÐ;NÔPR×PVÑPVÐW[×WaÑWaÓPbÐ:cÓ1dÑdˆÜ�H‰H�d—g‘g˜tŸw™wÐ'Ó(ˆÜ�y‰y˜!˜e™) fÑ,¨a°%©i¸&Ñ.@À!ÀeÁ)ÈfÑBTÐVWÐZ_ÑV_ÐbhÑVhÐiÓjÐjr   c                 ó0  — |xs t        j                  «       }| j                  «       }|dk(  r|dd…ddgf   }|j                  dd«       |j                  dd«       |j                  d	d
«       t	        j
                  |fi |¤Ž}|j                  |«       |S )a•  Visualize the box on a matplotlib plot.
        :param mode: How should the box coordinates and angle be interpreted.
            - mode `'image'` corresponds to the situation where x coordinate of the box
              denotes the "row of an image" (ie. the Y coordinate of the plot, arranged downwards)
              and y coordinate of the box corresponds to the "column of an image",
              (ie X coordinate of the plot). In other words, box's x goes downwards and y - rightwards.
            - mode `'math'` corresponds to the "mathematics" situation where box's x and y correspond to the X and Y axes of the plot.
        :param ax: the matplotlib axis to draw on. If unspecified, the current axis is used.
        :param kwargs: arguments passed to the matplotlib's `Polygon` patch object. By default, fill is set to False, color to red and lw to 2.
        :return: The created Polygon object.
        ÚimageNr"   r   ÚfillFÚcolorÚrÚlwr+   )ÚpltÚgcar>   Ú
setdefaultr   ÚPolygonÚ	add_patch)r   ÚmodeÚaxÚkwargsÚpolyÚps         r   ÚplotzRotatedBox.plot`   s‰   € ð Š_”3—7‘7“9ˆØ�|‰|‹~ˆØ�7Š?Øš˜1˜Q˜%˜‘=ˆDØ×Ñ˜& %Ô(Ø×Ñ˜' 3Ô'Ø×Ñ˜$ Ô"Ü�O‰O˜DÑ+ FÑ+ˆØ
�‰�QŒØˆr   c                 óÐ  — t         j                  dz  | j                  z
  dz  t         j                  z  }t        j                  ||| j
                  d   |z  | j
                  d   |z  gd¬«      }| j                  ||«      \  }}t        t        | j
                  d   | j                  dz  z
  |z
  |z  |z
  «      d«      }	t        | j
                  d   | j                  dz  z   |z   |z  |z
  «      }
t        t        | j
                  d   | j                  dz  z
  |z
  |z  |z
  «      d«      }t        | j
                  d   | j                  dz  z   |z   |z  |z
  «      }||	|
…||…f   S )aÉ  Extracts the contents of this box from a given image.
        For that the image is "unrotated" by the appropriate angle, and the corresponding part is extracted from it.

        Returns an image with dimensions height*scale x width*scale.
        Note that the box coordinates are interpreted as "image coordinates" (i.e. x is row and y is column),
        and box angle is considered to be relative to the vertical (i.e. np.pi/2 is "normal orientation")

        :param img: a numpy ndarray suitable for image processing via skimage.
        :param scale: the RotatedBox is scaled by this value before performing the extraction.
            This is necessary when, for example, the location of a particular feature is determined using a smaller image,
            yet then the corresponding area needs to be extracted from the original, larger image.
            The scale parameter in this case should be width_of_larger_image/width_of_smaller_image.
        :param margin_width: The margin that should be added to the width dimension of the box from each size.
            This value is given wrt actual box dimensions (i.e. not scaled).
        :param margin_height: The margin that should be added to the height dimension of the box from each side.
        :return: a numpy ndarray, corresponding to the extracted region (aligned straight).

        TODO: This could be made more efficient if we avoid rotating the full image and cut out the ROI from it beforehand.
        r+   é´   r"   r   T)r   r   Úresize)r   r1   r   r   Úrotater   Ú_compensate_rotation_shiftÚmaxÚintr   r   )r   ÚimgÚscaler9   r:   Ú	rotate_byÚimg_rotatedÚshift_cÚshift_rÚr1Úr2Úc1Úc2s                r   Úextract_from_imagezRotatedBox.extract_from_imagew   s^  € ô( —U‘U˜1‘W˜tŸz™zÑ)¨3Ñ.¬r¯u©uÑ4ˆ	Ü×&Ñ& s°)ÀTÇ[Á[ÐQRÁ^ÐTYÑEYÐ[_×[fÑ[fÐghÑ[iÐjoÑ[oÐDpÐy}Ô~ˆð  ×:Ñ:¸3ÀÓFÑˆ�ä”�d—k‘k !‘n t§{¡{°1¡}Ñ4°}ÑDÀeÑKÈgÑUÓVÐXYÓZˆÜ�$—+‘+˜a‘. 4§;¡;¨q¡=Ñ0°=Ñ@À%ÑGÈ'ÑQÓRˆÜ”�d—k‘k !‘n t§z¡z°!¡|Ñ3°lÑBÀEÑIÈGÑSÓTÐVWÓXˆÜ�$—+‘+˜a‘. 4§:¡:¨a¡<Ñ/°,Ñ>ÀÑEÈÑOÓPˆØ˜2˜b˜5 " R %˜<Ñ(Ð(r   c                 ón  — t        j                  | j                  d   |z  | j                  d   |z  g«      }t        j                  |¬«      }t        j                  t         j
                  dz  | j                  z
  ¬«      }t        j                  | ¬«      }||z   |z   }|j                  d   |j                  d   }	}t        j                  ddgd|dz
  g|	dz
  |dz
  g|	dz
  dgg«      }
|j                  |
«      }
|
dd…df   j                  «       }|
dd…df   j                  «       }|
dd…df   j                  «       }|
dd…df   j                  «       }||f}t        j                  |¬«      }||z   }d|j                  d<   ||j                  |«      z
  j                  «       j                  «       S )aó  This is an auxiliary method used by extract_from_image.
        It is needed due to particular specifics of the skimage.transform.rotate implementation.
        Namely, when you use rotate(... , resize=True), the rotated image is rotated and shifted by certain amount.
        Thus when we need to cut out the box from the image, we need to account for this shift.
        We do this by repeating the computation from skimage.transform.rotate here.

        TODO: This makes the code uncomfortably coupled to SKImage (e.g. this logic is appropriate for skimage 0.12.1, but not for 0.11,
        and no one knows what happens in later versions). A solution would be to use skimage.transform.warp with custom settings, but we can think of it later.
        r"   r   )Útranslationr+   )ÚrotationN)r   r   r"   )r   r   r   r   ÚSimilarityTransformr1   r   Úshaper,   ÚinverseÚminrU   ÚparamsÚravelÚtolist)r   rW   rX   ÚctrÚtform1Útform2Útform3ÚtformÚrowsÚcolsÚcornersÚmincÚminrÚmaxcÚmaxrrc   Útform4s                    r   rT   z%RotatedBox._compensate_rotation_shift—   sœ  € ô �j‰j˜$Ÿ+™+ a™.¨Ñ.°·±¸A±¸uÑ0DÐEÓFˆÜ×.Ñ.¸3Ô?ˆÜ×.Ñ.¼¿¹¸a¹À$Ç*Á*Ñ8LÔMˆÜ×.Ñ.¸C¸4Ô@ˆØ˜‘ &Ñ(ˆà—Y‘Y˜q‘\ 3§9¡9¨Q¡<ˆdˆÜ—(‘(Ø�ˆFØ��q‘ˆMØ�A‰X�t˜a‘xÐ Ø�A‰X�qˆMð	
ó ˆð —-‘- Ó(ˆØ’q˜!�t‰}× Ñ Ó"ˆØ’q˜!�t‰}× Ñ Ó"ˆØ’q˜!�t‰}× Ñ Ó"ˆØ’q˜!�t‰}× Ñ Ó"ˆð ˜T�lˆÜ×.Ñ.¸;ÔGˆØ˜‘ˆØ#ˆ�‰�Q‰ð �e—m‘m CÓ(Ñ(×/Ñ/Ó1×8Ñ8Ó:Ð:r   c                 ód  — t        j                  | t         j                  ¬«      } | j                  d   dk(  rt	        | d   ddd| ¬«      S t        d«      j                  | «      }t        j                  |j                  d   |j                  d   «      t         j                  z  }t        |t         j                  z
  «      |k  r+|dkD  r|t         j                  z
  n|t         j                  z   }|j                  | «      }t        j                  |d«      }t        j                  |d«      }||z
  }|d	k(  s|d
k(  rc| j                  d   dk  rQt	        t        j                  |j                  j                  ||z   dz  «      |j                   z   |d   |d   || ¬«      S |d
k(  r€t#        |dd…df   «      }t%        |«      }	||	dz     ||	dz  dz     }}
t        j&                  |dd…df   |
k\  |dd…df   |k  «      }t        j)                  | |dd…f   d	«      }| |_        |S t-        d|z  «      ‚)a2  
        Interpret a given point cloud as a RotatedBox, using PCA to determine the potential orientation (the longest component becomes width)
        This is basically an approximate version of a min-area-rectangle algorithm.
        TODO: Test whether using a true min-area-rectangle algorithm would be more precise or faster.

        :param points: An n x 2 numpy array of coordinates.
        :param box_type: The kind of method used to estimate the "box".
            Possible values:
                - `'bb'`, denoting the "bounding box" approach (min/max coordinates of the points correspond to box limits)
                - `'mrz`, denoting a slightly modified technique, suited for MRZ zone detection from contour images.
                          Here the assumption is that the upper and lower bounds of the box are better estimated as the
                          10% and 90% quantile of the corresponding coordinates (rather than 0% and 100%, i.e. min and max).
                          This helps against accidental noise in the contour.
                          The `'mrz'` correction is only applied when there are at least 10 points in the set.
        :returns: a RotatedBox, bounding the given set of points, oriented according to the principal components.

        >>> RotatedBox.from_points([[0,0]])
        RotatedBox(cx=0.0, cy=0.0, width=0.0, height=0.0, angle=0.0)
        >>> assert RotatedBox.from_points([[0,0], [1,1], [2,2]]).approx_equal([1, 1], np.sqrt(8), 0, np.pi/4)
        >>> assert RotatedBox.from_points([[0,0], [1,1], [0,1], [1,0]]).approx_equal([0.5, 0.5], 1, 1, 0.0) # The angle is rather arbitrary here
        >>> assert RotatedBox.from_points([[0,0], [2,1], [0,1], [2,0]]).approx_equal([1, 0.5], 2, 1, 0)
        >>> assert RotatedBox.from_points([[0,0], [2,4], [0,4], [2,0]]).approx_equal([1, 2], 4, 2, np.pi/2)
        >>> assert RotatedBox.from_points([[0,0], [1,1.5], [2,0]]).approx_equal([1, 0.75], 2, 1.5, 0)
        >>> assert RotatedBox.from_points([[0,0], [0,1], [1,1]]).approx_equal([0.25, 0.75], np.sqrt(2), np.sqrt(2)/2, np.pi/4)
        r
   r   r"   g        )r   r   r   r   r+   )r   r"   ©r   r   ÚbbÚmrzé
   Né	   z$Unknown parameter value: box_type=%s)r   r   r   rf   r   r   ÚfitÚarctan2Úcomponents_r1   r'   r   rh   rU   r/   r0   Úmean_ÚsortedÚlenÚlogical_andÚfrom_pointsr   Ú
ValueError)r   Úbox_typeÚmr   Úpoints_transformedÚllÚurÚwhÚh_coordÚnÚbottomÚtopÚvalid_pointsÚrbs                 r   r†   zRotatedBox.from_points½   s  € ô6 —‘˜F¬"¯*©*Ô5ˆØ�<‰<˜‰?˜aÒÜ˜f Q™i¨s¸3ÀcÐRXÔYÐYä�‹F�J‰J�vÓˆä—‘˜AŸM™M¨#Ñ.°·±¸cÑ0BÓCÄbÇeÁeÑKˆÜˆu”r—u‘u‰}Ó Ò%ð &+¨Q¢Y�EœBŸE™E’M°E¼B¿E¹E±MˆEØŸ[™[¨Ó0ÐÜ�V‰VÐ&¨Ó*ˆÜ�V‰VÐ&¨Ó*ˆØ�"‰Wˆð �tÒ ¨EÒ 1°f·l±lÀ1±oÈÒ6Jô œbŸf™f Q§]¡]§_¡_°r¸"±u¸a±iÓ@À1Ç7Á7ÑJÐRTÐUVÑRWÐ`bÐcdÑ`eÐmrð  |Bô  Cð  CØ˜Òô
 Ð/²°!°Ñ4Ó5ˆGÜ�G“ˆAØ! ! B¡$™-¨°°1±°R±©�CˆFÜŸ>™>Ð*<ºQ¸q¸SÑ*AÀ6Ñ*IÐK]Ò^_Ð`aÐ^aÑKbÐdgÑKgÓhˆLÜ×'Ñ'¨¨|ºQ¨Ñ(?ÀÓFˆBØˆBŒIØˆIäÐCÀhÑNÓOÐOr   r$   )g�íµ ÷Æ°>rz   )r@   N)g      ð?é   r”   )r{   )Ú__name__Ú
__module__Ú__qualname__Ú__doc__r   r   Úpropertyr   r   r%   r)   r6   r>   rO   ra   rT   Ústaticmethodr†   © r   r   r   r      sˆ   „ ñó	òLð ñó ðð ñó ðð ñ(ó ð(óQò
Zókó@ó.)ò@$;ðL ò>Pó ñ>Pr   r   )r˜   Únumpyr   Úsklearn.decompositionr   Ú
matplotlibr   rE   r   Úskimager   Úobjectr   r›   r   r   ú<module>r¡      s+   ðñó Ý %Ý $Ý Ý ônP�õ nPr   