calib3d.points API documentation
Description: Working with homogenous coordinates …

# Module `calib3d.points`

# Working with homogenous coordinates

The vector used to represent 2D and 3D points are vertical vectors, which are stored as 2D matrices in `numpy`. Furthemore, in homogenous coordinates: a 3D point (x,y,z) in the world is represented by a 4 element vector (𝜆x,𝜆y,𝜆z,𝜆) where 𝜆 ∈ ℝ₀.

To simplify access to x and y (and z) coordinates of those points as well as computations in homogenous coordinates, we defined the types `Point2D` (and `Point3D`) extending `numpy.ndarray`. Therefore, access to y coordinate of point is `point.y` instead of `point[1][0]` (`point[1][:]` for an array of points), and access to homogenous coordinates is made easy with `point.H`, while it is still possible to use point with any numpy operators.

Expand source code

    from abc import ABCMeta, abstractproperty
    import warnings
    import numpy as np

    __doc__ = r"""

    # Working with homogenous coordinates

    The vector used to represent 2D and 3D points are vertical vectors, which are stored as 2D matrices in `numpy`.
    Furthemore, in homogenous coordinates: a 3D point (x,y,z) in the world is represented by a 4 element vector
    (𝜆x,𝜆y,𝜆z,𝜆) where 𝜆 ∈ ℝ₀.

    To simplify access to x and y (and z) coordinates of those points as well as computations in homogenous coordinates,
    we defined the types [`Point2D`](#Point2D) (and [`Point3D`](#Point3D))
    extending `numpy.ndarray`. Therefore, access to y coordinate of point is `point.y` instead of
    `point[1][0]` (`point[1][:]` for an array of points), and access to homogenous coordinates is made easy with `point.H`,
    while it is still possible to use point with any numpy operators.

    """

    class HomogeneousCoordinatesPoint(np.ndarray, metaclass=ABCMeta):
        """ Extension of Numpy `np.ndarray` that implements generic homogenous coordinates points
            for `Point2D` and `Point3D` objects. The constructor supports multiple formats for creation
            of a single point or array of multiple points.

            Example with creation of `Point2D` objects, all formulations are equivalent:
            ```
            >>> x, y = 1, 2
            >>> Point2D(x, y)
            >>> Point2D(4*x, 4*y, 4)
            >>> Point2D(np.array([[x], [y]]))
            >>> Point2D(4*np.array([[x], [y], [1]]))
            Point2D([[1.],
                     [2.]])
            ```
        """
        def __new__(cls, *coords):
            if len(coords) == 1:
                if isinstance(coords, list):
                    coords = np.hstack(coords)
                else:
                    coords = coords[0]
            array = np.array(coords) if isinstance(coords, (tuple, list)) else coords
            invalid_shape_message = "Invalid input shape:\n" \
                "Expected a 2D np.array of shape ({l1},N) or (N,{l1},1) in non-homogenous coordinates\n" \
                "                       or shape ({l2},N) or (N,{l2},1) in homogenous coordinates\n" \
                "Received a np.array of shape {shape}".format(l1=cls.D, l2=cls.D+1, shape=array.shape)
            if len(array.shape) == 1:
                array = array[:, np.newaxis]
            elif len(array.shape) == 2:
                pass
            elif len(array.shape) == 3:
                array = array[..., 0].T
            else:
                raise ValueError(invalid_shape_message)

            if array.shape[0] == cls.D: # point(s) given in non-homogenous coordinates
                pass
            elif array.shape[0] == cls.D+1: # point(s) given in homogenous coordinates
                array = array[0:cls.D,:]/array[cls.D,:]
            elif array.shape[0] == 0: # point given from an empty list should be an empty point
                array = np.empty((cls.D,0))
            else:
                raise ValueError(invalid_shape_message)
            return array.astype(np.float64).view(cls)
        # def __array_ufunc__():
        #    TODO
        # def __array_wrap__(self, out_arr, context=None):
        #     return super().__array_wrap__(self, out_arr, context)
        @property
        @abstractproperty
        def _coord_names(self):
            raise NotImplementedError

        x = property(fget=lambda self: self._get_coord(0), fset=lambda self, value: self._set_coord(0, value), doc="Point's x component")
        y = property(fget=lambda self: self._get_coord(1), fset=lambda self, value: self._set_coord(1, value), doc="Point's y component")
        z = property(fget=lambda self: self._get_coord(2), fset=lambda self, value: self._set_coord(2, value), doc="Point's z component (only valid for `Point3D` objects)")

        @property
        def H(self):
            """ Point expressed in homogenous coordinates with an homogenous component equal to `1`.

            Example:
            ```
            >>> p = Point3D(1,2,3)
            >>> p.H
            array([[1.],
                   [2.],
                   [3.],
                   [1.]])
            ```
            """
            return np.vstack((self, np.ones((1, self.shape[1]))))
        # @property
        # def D(self):
        #     """ Returns the number of spacial dimensions """
        #     return len(self._coord_names)

        # /!\ a different getitem gives too much trouble with all numpy operator
        #def __getitem__(self, i):
        #    return self.__class__(super().__getitem__((slice(None), i)))
        #    if isinstance(i, int):
        #        #b, j = i
        #        j = i
        #        #if b is Ellipsis and isinstance(j, int):
        #        return super().__getitem__((slice(None), slice(j,j+1,None)))
        #    return super().__getitem__(i)

        # /!\ iter may conflict with numpy array getitem.
        def __iter__(self):
            return (self.__class__(self[:,i:i+1]) for i in range(self.shape[1]))

        def to_list(self):
            """ Transforms a single point to a python list.

            Raises:
                AssertionError if the object is an array of multiple points
            """
            assert self.shape[1] == 1, "to_list() method can only be used on single point {}".format(self.__class__.__name__)
            return self[:,0].flatten().tolist()

        def flatten(self):
            """ Flatten the points.

            .. todo:: integrate this in the __array_ufunc__ to prevent type forwarding
            """
            return np.asarray(super().flatten())

        def to_int_tuple(self):
            """ Transforms a single point to a python tuple with integer coordinates
            """
            return tuple(int(x) for x in self.to_list())

        def linspace(self, num):
            """ Linearly interpolate points in `num-1` intervals.

                Example:
                ```
                >>> Point2D([0,4,4],[0,0,4]).linspace(5)
                [[0. 1. 2. 3. 4. 4. 4. 4. 4. 4.]
                [0. 0. 0. 0. 0. 0. 1. 2. 3. 4.]]
                ```
            """
            return np.transpose(np.linspace(self[:,:-1], self[:,1:], num), axes=(1,2,0)).reshape(len(self._coord_names),-1)

        def close(self):
            """ Copy the first point in an array of points and place it at the end of that array,
                hence "closing" the polygon defined by the initial points.

                TODO: add Points2D and Points3D classes
            """
            assert self.shape[1] > 1, f"Invalid use of 'close' method: points' shape '{self.shape}' expected to be > 1 in the second dimension"
            return self.__class__(np.hstack((self, self[:,0:1])))

        _get_coord = lambda self, i:        np.asarray(super().__getitem__((i,0)))       if self.shape[1] == 1 else np.asarray(super().__getitem__(i))
        _set_coord = lambda self, i, value:            super().__setitem__((i,0), value) if self.shape[1] == 1 else            super().__setitem__((i), value)

    class Point(HomogeneousCoordinatesPoint):
        pass # easier type

    class Point2D(HomogeneousCoordinatesPoint):
        """ Numpy representation of a single 2D point or a list of 2D points
        """
        D = 2
        _coord_names = ("x","y")

    class Point3D(HomogeneousCoordinatesPoint):
        """ Numpy representation of a single 3D point or a list of 3D points
        """
        D = 3
        _coord_names = ("x","y","z")
        @property
        def V(self):
            array = self.H
            array[-1] = 0
            return VanishingPoint(array)

    class VanishingPoint(Point3D):
        """ Object allowing representation of Vanishing point (with null homogenous
            coordinate). Only the `H` attribute should be used. Handle with care.
        """
        def __new__(cls, array):
            warnings.warn("Vanishing Point feature has not yet been fully tested")
            obj = array.astype(np.float64).view(cls)
            obj.array = array
            return obj
        @property
        def H(self):
            return self.array
        def __getattribute__(self, attr_name):
            if attr_name not in ("H", "__array_finalize__", "array", "shape", "size", "ndim", "x", "y", "z", "_get_coord", "close", "__class__", "astype", "view"):
                raise AttributeError(f"VanishingPoint has no `{attr_name}` attribute.")
            return super().__getattribute__(attr_name)

## Classes

`class HomogeneousCoordinatesPoint (*coords)`

  Extension of Numpy `np.ndarray` that implements generic homogenous coordinates points for `Point2D` and `Point3D` objects. The constructor supports multiple formats for creation of a single point or array of multiple points.

  Example with creation of `Point2D` objects, all formulations are equivalent:

      >>> x, y = 1, 2
      >>> Point2D(x, y)
      >>> Point2D(4*x, 4*y, 4)
      >>> Point2D(np.array([[x], [y]]))
      >>> Point2D(4*np.array([[x], [y], [1]]))
      Point2D([[1.],
               [2.]])

  Expand source code

      class HomogeneousCoordinatesPoint(np.ndarray, metaclass=ABCMeta):
          """ Extension of Numpy `np.ndarray` that implements generic homogenous coordinates points
              for `Point2D` and `Point3D` objects. The constructor supports multiple formats for creation
              of a single point or array of multiple points.

              Example with creation of `Point2D` objects, all formulations are equivalent:
              ```
              >>> x, y = 1, 2
              >>> Point2D(x, y)
              >>> Point2D(4*x, 4*y, 4)
              >>> Point2D(np.array([[x], [y]]))
              >>> Point2D(4*np.array([[x], [y], [1]]))
              Point2D([[1.],
                       [2.]])
              ```
          """
          def __new__(cls, *coords):
              if len(coords) == 1:
                  if isinstance(coords, list):
                      coords = np.hstack(coords)
                  else:
                      coords = coords[0]
              array = np.array(coords) if isinstance(coords, (tuple, list)) else coords
              invalid_shape_message = "Invalid input shape:\n" \
                  "Expected a 2D np.array of shape ({l1},N) or (N,{l1},1) in non-homogenous coordinates\n" \
                  "                       or shape ({l2},N) or (N,{l2},1) in homogenous coordinates\n" \
                  "Received a np.array of shape {shape}".format(l1=cls.D, l2=cls.D+1, shape=array.shape)
              if len(array.shape) == 1:
                  array = array[:, np.newaxis]
              elif len(array.shape) == 2:
                  pass
              elif len(array.shape) == 3:
                  array = array[..., 0].T
              else:
                  raise ValueError(invalid_shape_message)

              if array.shape[0] == cls.D: # point(s) given in non-homogenous coordinates
                  pass
              elif array.shape[0] == cls.D+1: # point(s) given in homogenous coordinates
                  array = array[0:cls.D,:]/array[cls.D,:]
              elif array.shape[0] == 0: # point given from an empty list should be an empty point
                  array = np.empty((cls.D,0))
              else:
                  raise ValueError(invalid_shape_message)
              return array.astype(np.float64).view(cls)
          # def __array_ufunc__():
          #    TODO
          # def __array_wrap__(self, out_arr, context=None):
          #     return super().__array_wrap__(self, out_arr, context)
          @property
          @abstractproperty
          def _coord_names(self):
              raise NotImplementedError

          x = property(fget=lambda self: self._get_coord(0), fset=lambda self, value: self._set_coord(0, value), doc="Point's x component")
          y = property(fget=lambda self: self._get_coord(1), fset=lambda self, value: self._set_coord(1, value), doc="Point's y component")
          z = property(fget=lambda self: self._get_coord(2), fset=lambda self, value: self._set_coord(2, value), doc="Point's z component (only valid for `Point3D` objects)")

          @property
          def H(self):
              """ Point expressed in homogenous coordinates with an homogenous component equal to `1`.

              Example:
              ```
              >>> p = Point3D(1,2,3)
              >>> p.H
              array([[1.],
                     [2.],
                     [3.],
                     [1.]])
              ```
              """
              return np.vstack((self, np.ones((1, self.shape[1]))))
          # @property
          # def D(self):
          #     """ Returns the number of spacial dimensions """
          #     return len(self._coord_names)

          # /!\ a different getitem gives too much trouble with all numpy operator
          #def __getitem__(self, i):
          #    return self.__class__(super().__getitem__((slice(None), i)))
          #    if isinstance(i, int):
          #        #b, j = i
          #        j = i
          #        #if b is Ellipsis and isinstance(j, int):
          #        return super().__getitem__((slice(None), slice(j,j+1,None)))
          #    return super().__getitem__(i)

          # /!\ iter may conflict with numpy array getitem.
          def __iter__(self):
              return (self.__class__(self[:,i:i+1]) for i in range(self.shape[1]))

          def to_list(self):
              """ Transforms a single point to a python list.

              Raises:
                  AssertionError if the object is an array of multiple points
              """
              assert self.shape[1] == 1, "to_list() method can only be used on single point {}".format(self.__class__.__name__)
              return self[:,0].flatten().tolist()

          def flatten(self):
              """ Flatten the points.

              .. todo:: integrate this in the __array_ufunc__ to prevent type forwarding
              """
              return np.asarray(super().flatten())

          def to_int_tuple(self):
              """ Transforms a single point to a python tuple with integer coordinates
              """
              return tuple(int(x) for x in self.to_list())

          def linspace(self, num):
              """ Linearly interpolate points in `num-1` intervals.

                  Example:
                  ```
                  >>> Point2D([0,4,4],[0,0,4]).linspace(5)
                  [[0. 1. 2. 3. 4. 4. 4. 4. 4. 4.]
                  [0. 0. 0. 0. 0. 0. 1. 2. 3. 4.]]
                  ```
              """
              return np.transpose(np.linspace(self[:,:-1], self[:,1:], num), axes=(1,2,0)).reshape(len(self._coord_names),-1)

          def close(self):
              """ Copy the first point in an array of points and place it at the end of that array,
                  hence "closing" the polygon defined by the initial points.

                  TODO: add Points2D and Points3D classes
              """
              assert self.shape[1] > 1, f"Invalid use of 'close' method: points' shape '{self.shape}' expected to be > 1 in the second dimension"
              return self.__class__(np.hstack((self, self[:,0:1])))

          _get_coord = lambda self, i:        np.asarray(super().__getitem__((i,0)))       if self.shape[1] == 1 else np.asarray(super().__getitem__(i))
          _set_coord = lambda self, i, value:            super().__setitem__((i,0), value) if self.shape[1] == 1 else            super().__setitem__((i), value)

  ### Ancestors

  - numpy.ndarray

  ### Subclasses

  - Point
  - Point2D
  - Point3D

  ### Instance variables

  `var H`

    Point expressed in homogenous coordinates with an homogenous component equal to `1`.

    Example:

        >>> p = Point3D(1,2,3)
        >>> p.H
        array([[1.],
               [2.],
               [3.],
               [1.]])

    Expand source code

        @property
        def H(self):
            """ Point expressed in homogenous coordinates with an homogenous component equal to `1`.

            Example:
            ```
            >>> p = Point3D(1,2,3)
            >>> p.H
            array([[1.],
                   [2.],
                   [3.],
                   [1.]])
            ```
            """
            return np.vstack((self, np.ones((1, self.shape[1]))))

  `var x`

    Point's x component

    Expand source code

        x = property(fget=lambda self: self._get_coord(0), fset=lambda self, value: self._set_coord(0, value), doc="Point's x component")

  `var y`

    Point's y component

    Expand source code

        y = property(fget=lambda self: self._get_coord(1), fset=lambda self, value: self._set_coord(1, value), doc="Point's y component")

  `var z`

    Point's z component (only valid for `Point3D` objects)

    Expand source code

        z = property(fget=lambda self: self._get_coord(2), fset=lambda self, value: self._set_coord(2, value), doc="Point's z component (only valid for `Point3D` objects)")

  ### Methods

  `def close(self)`

    Copy the first point in an array of points and place it at the end of that array, hence "closing" the polygon defined by the initial points.

    TODO: add Points2D and Points3D classes

    Expand source code

        def close(self):
            """ Copy the first point in an array of points and place it at the end of that array,
                hence "closing" the polygon defined by the initial points.

                TODO: add Points2D and Points3D classes
            """
            assert self.shape[1] > 1, f"Invalid use of 'close' method: points' shape '{self.shape}' expected to be > 1 in the second dimension"
            return self.__class__(np.hstack((self, self[:,0:1])))

  `def flatten(self)`

    Flatten the points.

    TODO

    integrate this in the array_ufunc to prevent type forwarding

    Expand source code

        def flatten(self):
            """ Flatten the points.

            .. todo:: integrate this in the __array_ufunc__ to prevent type forwarding
            """
            return np.asarray(super().flatten())

  `def linspace(self, num)`

    Linearly interpolate points in `num-1` intervals.

    Example:

        >>> Point2D([0,4,4],[0,0,4]).linspace(5)
        [[0. 1. 2. 3. 4. 4. 4. 4. 4. 4.]
        [0. 0. 0. 0. 0. 0. 1. 2. 3. 4.]]

    Expand source code

        def linspace(self, num):
            """ Linearly interpolate points in `num-1` intervals.

                Example:
                ```
                >>> Point2D([0,4,4],[0,0,4]).linspace(5)
                [[0. 1. 2. 3. 4. 4. 4. 4. 4. 4.]
                [0. 0. 0. 0. 0. 0. 1. 2. 3. 4.]]
                ```
            """
            return np.transpose(np.linspace(self[:,:-1], self[:,1:], num), axes=(1,2,0)).reshape(len(self._coord_names),-1)

  `def to_int_tuple(self)`

    Transforms a single point to a python tuple with integer coordinates

    Expand source code

        def to_int_tuple(self):
            """ Transforms a single point to a python tuple with integer coordinates
            """
            return tuple(int(x) for x in self.to_list())

  `def to_list(self)`

    Transforms a single point to a python list.

    ## Raises

    AssertionError if the object is an array of multiple points

    Expand source code

        def to_list(self):
            """ Transforms a single point to a python list.

            Raises:
                AssertionError if the object is an array of multiple points
            """
            assert self.shape[1] == 1, "to_list() method can only be used on single point {}".format(self.__class__.__name__)
            return self[:,0].flatten().tolist()

`class Point (*coords)`

  Extension of Numpy `np.ndarray` that implements generic homogenous coordinates points for `Point2D` and `Point3D` objects. The constructor supports multiple formats for creation of a single point or array of multiple points.

  Example with creation of `Point2D` objects, all formulations are equivalent:

      >>> x, y = 1, 2
      >>> Point2D(x, y)
      >>> Point2D(4*x, 4*y, 4)
      >>> Point2D(np.array([[x], [y]]))
      >>> Point2D(4*np.array([[x], [y], [1]]))
      Point2D([[1.],
               [2.]])

  Expand source code

      class Point(HomogeneousCoordinatesPoint):
          pass # easier type

  ### Ancestors

  - HomogeneousCoordinatesPoint
  - numpy.ndarray

  ### Inherited members

  - `HomogeneousCoordinatesPoint`:
    - `H`
    - `close`
    - `flatten`
    - `linspace`
    - `to_int_tuple`
    - `to_list`
    - `x`
    - `y`
    - `z`
`class Point2D (*coords)`

  Numpy representation of a single 2D point or a list of 2D points

  Expand source code

      class Point2D(HomogeneousCoordinatesPoint):
          """ Numpy representation of a single 2D point or a list of 2D points
          """
          D = 2
          _coord_names = ("x","y")

  ### Ancestors

  - HomogeneousCoordinatesPoint
  - numpy.ndarray

  ### Class variables

  `var D`

  ### Inherited members

  - `HomogeneousCoordinatesPoint`:
    - `H`
    - `close`
    - `flatten`
    - `linspace`
    - `to_int_tuple`
    - `to_list`
    - `x`
    - `y`
    - `z`
`class Point3D (*coords)`

  Numpy representation of a single 3D point or a list of 3D points

  Expand source code

      class Point3D(HomogeneousCoordinatesPoint):
          """ Numpy representation of a single 3D point or a list of 3D points
          """
          D = 3
          _coord_names = ("x","y","z")
          @property
          def V(self):
              array = self.H
              array[-1] = 0
              return VanishingPoint(array)

  ### Ancestors

  - HomogeneousCoordinatesPoint
  - numpy.ndarray

  ### Subclasses

  - VanishingPoint

  ### Class variables

  `var D`

  ### Instance variables

  `var V`
    Expand source code

        @property
        def V(self):
            array = self.H
            array[-1] = 0
            return VanishingPoint(array)

  ### Inherited members

  - `HomogeneousCoordinatesPoint`:
    - `H`
    - `close`
    - `flatten`
    - `linspace`
    - `to_int_tuple`
    - `to_list`
    - `x`
    - `y`
    - `z`
`class VanishingPoint (array)`

  Object allowing representation of Vanishing point (with null homogenous coordinate). Only the `H` attribute should be used. Handle with care.

  Expand source code

      class VanishingPoint(Point3D):
          """ Object allowing representation of Vanishing point (with null homogenous
              coordinate). Only the `H` attribute should be used. Handle with care.
          """
          def __new__(cls, array):
              warnings.warn("Vanishing Point feature has not yet been fully tested")
              obj = array.astype(np.float64).view(cls)
              obj.array = array
              return obj
          @property
          def H(self):
              return self.array
          def __getattribute__(self, attr_name):
              if attr_name not in ("H", "__array_finalize__", "array", "shape", "size", "ndim", "x", "y", "z", "_get_coord", "close", "__class__", "astype", "view"):
                  raise AttributeError(f"VanishingPoint has no `{attr_name}` attribute.")
              return super().__getattribute__(attr_name)

  ### Ancestors

  - Point3D
  - HomogeneousCoordinatesPoint
  - numpy.ndarray

  ### Inherited members

  - `Point3D`:
    - `H`
    - `close`
    - `flatten`
    - `linspace`
    - `to_int_tuple`
    - `to_list`
    - `x`
    - `y`
    - `z`