Source code for machinevisiontoolbox.ImageCore

"""
Core Image class providing pixel storage, arithmetic operators, and colour-plane access.
"""

# pyright: reportMissingImports=false
from __future__ import annotations

import os
import os.path
import urllib
import warnings
from collections.abc import Iterator, Sequence
from math import nan
from pathlib import Path

import cv2 as cv
import numpy as np
import spatialmath.base as smb
from spatialmath import Polygon2
from spatialmath.base import islistof, isscalar

# from numpy.lib.arraysetops import isin
from machinevisiontoolbox.base import (
    draw_box,
    draw_circle,
    draw_labelbox,
    draw_line,
    draw_point,
    draw_text,
    float_image,
    int_image,
)
from machinevisiontoolbox.base.imageio import convert, idisp, iread, iwrite
from machinevisiontoolbox.ImageBlobs import ImageBlobsMixin
from machinevisiontoolbox.ImageColor import ImageColorMixin
from machinevisiontoolbox.ImageConstants import ImageConstantsMixin
from machinevisiontoolbox.ImageFiducials import ImageFiducialsMixin
from machinevisiontoolbox.ImageIO import ImageIOMixin
from machinevisiontoolbox.ImageLineFeatures import ImageLineFeaturesMixin
from machinevisiontoolbox.ImageMorph import ImageMorphMixin
from machinevisiontoolbox.ImageMultiview import ImageMultiviewMixin
from machinevisiontoolbox.ImagePointFeatures import ImagePointFeaturesMixin
from machinevisiontoolbox.ImageTorch import ImageTorchMixin
from machinevisiontoolbox.ImageProcessing import ImageProcessingMixin
from machinevisiontoolbox.ImageRegionFeatures import ImageRegionFeaturesMixin
from machinevisiontoolbox.ImageReshape import ImageReshapeMixin
from machinevisiontoolbox.ImageSpatial import ImageSpatialMixin, Kernel
from machinevisiontoolbox.ImageWholeFeatures import ImageWholeFeaturesMixin
from machinevisiontoolbox.mvtb_types import *

# import spatialmath.base.argcheck as argcheck


"""
This class encapsulates a Numpy array containing the pixel values.  The object
supports arithmetic using overloaded operators, as well a large number of methods.
"""


[docs] class Image( ImageIOMixin, ImageConstantsMixin, ImageProcessingMixin, ImageMorphMixin, ImageSpatialMixin, ImageColorMixin, ImageReshapeMixin, ImageBlobsMixin, ImageWholeFeaturesMixin, ImageRegionFeaturesMixin, ImageFiducialsMixin, ImageLineFeaturesMixin, ImagePointFeaturesMixin, ImageMultiviewMixin, ImageTorchMixin, ): def __init__( self, image: Image | np.ndarray | None = None, colororder: str | dict | None = None, copy: bool = False, size: tuple | list | None = None, dtype: Dtype | None = None, name: str | None = None, id: int | None = None, domain=None, binary: bool = False, **kwargs, ) -> None: """ Create an Image instance :param image: image data :type image: array_like(H,W), :class:`Image` :param colororder: order of color channels :type colororder: str, dict :param copy: copy the image data, defaults to False :type copy: bool, optional :param size: new size for the image, defaults to None :type size: tuple, optional :param dtype: data type for image, defaults to same type as ``image`` :type dtype: str or NumPy dtype, optional :param name: name of image, defaults to None :type name: str, optional :param id: numeric id of image, typically a sequence number, defaults to None :type id: int, optional :param domain: domain of image, defaults to None :type domain: array_like(W), array_like(H), optional :param binary: create binary image, non-zero values are set to True, defaults to False :type binary: bool, optional :raises TypeError: unknown type passed to constructor Create a new :class:`Image` instance which contains pixel values as well as information about image size, datatype, color planes and domain. The pixel data is stored in a NumPy array, encapsulated by the object. An image is considered to be a two-dimensional (width x height) grid of pixel values, that lie within one or more "color" planes. The image data can be specified by: - a NumPy 2D or 3D array for a greyscale or color image respectively. For the latter case, the last index represents the color plane:: Image(nd.zeros((100, 200))) Image(nd.zeros((100, 200, 4)), colororder="WXYZ") - a lists of lists of pixel values, each inner list must have the same number of elements (columns):: Image([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) - a range of class methods that serve as constructors for common image types such as :meth:`Zeros`, :meth:`Constant`, :meth:`Random` and :meth:`String`, or read an image from a file :meth:`Read`. - an existing :class:`Image` instance. **Pixel datatype** The ``dtype`` of an image comes from the encapsulated NumPy pixel array. If ``image`` is an :class:`Image` instance or NumPy ndarray then its dtype is determined by the NumPy ndarray. If ``image`` is given as a list of lists and ``dtype`` is not given, then the :class:`Image` type is: - float32 if the list contains any floating point values, otherwise - the smallest signed or unsigned int that can represent its value span. An image can have boolean pixel values. When used in a numerical expression, its values will be cast to integer values of 0 or 1 representing False and True respectively. **Color planes** Images can have multiple planes, typically three (representing the primary colors red, green and blue) but *any number* is possible. In the underlying Numpy array, these planes are identified by an integer plane index (the last dimension of the 3D array). Rather than rely on a limiting convention such as planes being in the order RGB or BGR, the :class:`Image` contains a dictionary that maps the name of a color plane to its index value. The color plane order can be specified as a dict or a string, eg:: Image(img, colororder="RGB") Image(img, colororder="red:green:blue:alpha") Image(img, colororder=dict("R"=2, "G"=1, "B"=0)) Image planes can be referenced by their index or by their name, eg.:: img.plane(0) img.plane("alpha") :seealso: :meth:`colororder` :meth:`colororder_str` **Image domain** An :class:`Image` has a width and height in units of pixels, but for some applications it is useful to specify the pixel coordinates in other units, perhaps metres, or latitude/longitude angles, or for a spherical image as azimuth and colatitude angles. The domain is specified by two 1D arrays that map the pixel coordinate to the domain variable. **Binary images** If ``binary`` is True, the image is converted to a binary image, where zero valued pixels are set to False and all other values are set to True. To create an image where pixels have integer values of 0 and 1 use the ``dtype`` option:: Image([[0, 3], [4, 0]]) # pixel values are 0, 3, 4, 0 Image([[0, 3], [4, 0]], binary=True) # pixel values are: False, True, True, False Image([[0, 3], [4, 0]], binary=True, dtype="uint8") # pixel values are 0, 1, 1, 0 **Reshaping** Frequently we need to create an image from 1D data, for example:: 1: Y0 Y1 Y2 ... # (N,) 2: R0 G0 B0 R1 G1 B1 ... # (3N,) Or a 2D array with one or more rows:: 3: Y0 Y1 Y2 ... # (1, N) 4: R0 G0 B0 R1 G1 B1 ... # (1,3N) 5: R0 R1 R2 ... # (3,N) G0 G1 G2 ... B0 B1 B2 ... Or a 2D array with one or more columns:: 6: Y0 # (N,1) Y1 Y2 . . 7: R0 # (3N,1) G0 B0 R1 . . 8: R0 G0 B0 # (N,3) R1 G1 B1 R2 G2 B2 . . The ``size`` option can be used to reshape the data to the specified size. The number of planes is determined from any of: ``colororder``, the third element of ``size``, or the number of row/columns in the data (formats 5 or 8). For formats 2, 4 or 7 the number of planes must be given explicitly in ``colororder``, the third element of ``size``. :seealso: :meth:`view1d` Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> import numpy as np >>> img = Image([[1, 2], [3, 4]]) >>> print(img) >>> img.print() >>> Image(np.array([[1, 2], [3, 4]])) >>> Image([[1, 2], [3, 1000]]) >>> Image([[0.1, 0.2], [0.3, 0.4]]) >>> Image([[True, False], [False, True]]) .. warning:: If an image is constructed from an existing :class:`Image` instance or a Numpy array, the encapsulated Numpy array is, by default, a *reference* to the passed image data. Use the option ``copy=True`` if you want to copy the data. """ self._name = None self._colororder = None self.id = id self.domain = domain if isinstance(name, Path): name = str(name) if isinstance(image, np.ndarray): pass elif isinstance(image, self.__class__): # Image instance name = image.name colororder = image.colororder image = image._A elif isinstance(image, list): # list of lists # attempt to convert it to an ndarray try: image = np.array(image) except ValueError: raise ValueError("bad list of lists, check all rows have same length") if dtype is None: # no type given, automatically choose it if np.issubdtype(image.dtype, np.floating): # list contained a float dtype = np.float32 elif np.issubdtype(image.dtype, np.integer): # list contained only ints, convert to int/uint8 of smallest # size to contain all values if image.min() < 0: # value is signed for type in ["int8", "int16", "int32"]: if (image.max() <= np.iinfo(type).max) and ( image.min() >= np.iinfo(type).min ): dtype = np.dtype(type) break else: # value is unsigned for type in ["uint8", "uint16", "uint32"]: if image.max() <= np.iinfo(type).max: dtype = np.dtype(type) break else: raise ValueError("bad argument passed to Image constructor") if not isinstance(image, np.ndarray): raise ValueError( "bad argument passed to Image constructor: must be ndarray or list of lists" ) if binary: image = image > 0 # change type of array if dtype was specified if dtype is not None: image = image.astype(dtype) self.name = name color_dict = Image.colororder2dict(colororder) if isinstance(size, self.__class__): # size is an Image instance, ignore the size/shape and use the image's shape size = size.size # reshape the image data to match the specified size or shape if size is not None: newsize = [size[1], size[0]] if self.colororder is not None: nplanes = len(color_dict) if len(size) == 3: if nplanes != size[2]: raise ValueError( "colororder length does not match number of planes in size" ) newsize.append(nplanes) elif len(size) == 3: newsize.append(size[2]) if image.ndim == 1: # 1D array # Y0 Y1 Y2 ... # R0 G0 B0 R1 G1 B1 ... image = image.reshape(*newsize) elif image.ndim == 2: if image.shape[1] > image.shape[0]: # wide image, reshape to width x height x nplanes # Y0 Y1 Y2 ... # R0 G0 B0 R1 G1 B1 ... # R0 R1 R2 ... # G0 G1 G2 ... # B0 B1 B2 ... if len(newsize) == 3: if image.shape[0] != newsize[2]: raise ValueError( "specified number of color plane (size, colororder) does not match number of rows in data" ) else: if image.shape[0] > 1: newsize.append(image.shape[0]) image = image.T.reshape(*newsize) else: # tall image, reshape to height x width x nplanes # Y0 # Y1 # Y2 # . # . # R0 G0 B0 # R1 G1 B1 # R2 G2 B2 # . # . if len(newsize) == 3: if image.shape[1] != newsize[2]: raise ValueError( "specified number of color plane (size, colororder) does not match number of columns in data" ) else: if image.shape[1] > 1: newsize.append(image.shape[1]) image = image.reshape(*newsize) if image.ndim not in (2, 3): raise ValueError( "bad ndarray passed to Image constructor: must be 2D or 3D array" ) if image.ndim == 3 and image.shape[2] == 1: image = image[:, :, 0] # squeeze out singleton plane # assign the image to the object, copying if requested if copy: self._A = image.copy() else: self._A = image # final check that colororder length matches number of planes if colororder is not None: if len(color_dict) != self.nplanes: raise ValueError("colororder length does not match number of planes") if colororder is None: if self.nplanes == 3: self.colororder = "RGB" warnings.warn("defaulting color to RGB") else: self.colororder = color_dict self.name = name __array_ufunc__ = None # allow Image matrices operators with NumPy values def __str__(self) -> str: """ Single line summary of image parameters :return: single line summary of image :rtype: str Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image.Read('flowers1.png') >>> str(img) """ s = f"Image: {self.width} x {self.height} ({self.dtype})" if self.colororder is not None: co = self.colororder_str or "" s += ", " + co else: s += f", {self.nplanes} anonymous plane{'' if self.nplanes == 1 else 's'}" if self.id is not None: s += f", id={self.id}" if self.name is not None: name = self.name # if it's a long name, take from rightmost / and add ellipsis if len(name) > 20: k = [i for i, c in enumerate(name) if c == "/"] if len(k) >= 2: name = name[k[-2] :] else: name = name[-20:] name = "..." + name s += f" [{name}]" if self.domain is not None: s += f", u::{self.domain[0][0]:.3g}:{self.domain[0][-1]:.3g}, v::{self.domain[1][0]:.3g}:{self.domain[1][-1]:.3g}" nnan = np.sum(np.isnan(self._A)) ninf = np.sum(np.isinf(self._A)) if nnan + ninf > 0: s += " (contains " if nnan > 0: s += f"{nnan}xNaN{'s' if nnan > 1 else ''}" if ninf > 0: s += f" {ninf}xInf{'s' if ninf > 1 else ''}" s += ")" return s def rprint(self, **kwargs) -> "Image": """ Print image pixels in compact format and return image :param fmt: format string, defaults to None :type fmt: str, optional :param separator: value separator, defaults to single space :type separator: str, optional :param precision: precision for floating point pixel values, defaults to 2 :type precision: int, optional :param header: print image summary header, defaults to True :type header: bool, optional :param file: file to print to, defaults to None :type file: file, optional :return: the printed image :rtype: :class:`Image` Very compact display of pixel numerical values in grid layout and return the image itself. Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image.Squares(1, 10).rprint() >>> print(img) >>> img = Image.Squares(1, 10, dtype='float').rprint(precision=1, header=True) >>> print(img) The function returns the image, which is why we see the image ``repr`` value after the printed pixels in this python intrepreter. The `rprint` method is particularly useful in a method chain, for example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image.Random(3).rprint() >>> print(img) # return result of print() is the image itself .. note:: - For a boolean image True and False are displayed as 1 and 0 respectively. - For a multiplane images the planes are printed sequentially, along with the plane's name. :seealso: :meth:`print` :meth:`Image.strhcat` :meth:`Image.showpixels` """ self.print(**kwargs) return self def print( self, fmt: str | None = None, separator: str = " ", precision: int = 2, header: bool = False, file=None, ) -> None: """ Print image pixels in compact format :param fmt: format string, defaults to None :type fmt: str, optional :param separator: value separator, defaults to single space :type separator: str, optional :param precision: precision for floating point pixel values, defaults to 2 :type precision: int, optional :param header: print image summary header, defaults to True :type header: bool, optional :param file: file to print to, defaults to None :type file: file, optional :return: the printed image :rtype: :class:`Image` Very compact display of pixel numerical values in grid layout. Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image.Squares(1, 10) >>> img.print() >>> img = Image.Squares(1, 10, dtype='float') >>> img.print(precision=1, header=True) .. note:: - For a boolean image True and False are displayed as 1 and 0 respectively. - For a multiplane images the planes are printed sequentially, along with the plane's name. :seealso: :meth:`rprint` :meth:`Image.strhcat` :meth:`Image.showpixels` """ def format_plane(plane, fmt, indent=" "): rows = [] for v in plane.vspan(): row = indent for u in plane.uspan(): row += (fmt or "{}").format(plane._A[v, u]) rows.append(row) return rows if fmt is None: if self.isint: width = max(len(str(self.max())), len(str(self.min()))) fmt = f"{separator}{{:{width}d}}" elif self.isbool: width = 1 fmt = f"{separator}{{:{width}d}}" elif self.isfloat: ff = f"{{:.{precision}f}}" width = max(len(ff.format(self.max())), len(ff.format(self.min()))) fmt = f"{separator}{{:{width}.{precision}f}}" if header: print(self, file=file) if self.iscolor: plane_names = (self.colororder_str or "").split(":") for i, plane in enumerate(self.planes()): print(f" plane {plane_names[i]}:") print("\n".join(format_plane(plane, fmt, indent=" ")), file=file) else: print("\n".join(format_plane(self, fmt)), file=file) @classmethod def strhcat( cls, *images: "Image", widths: int | Sequence[int] = 1, arraysep: str = " |", labels: Sequence[str] | None = None, ) -> str: """Format several small images concatenated horizontally :param images: one or more images to be formatted horizontally concatenated :type images: :class:`Image` instances :param widths: number of digits for the formatted array elements, defaults to 1. If scalar applies to all images, if list applies to each image. :type widths: int or list of ints, optional :param arraysep: separator between arrays, defaults to ``" |"`` :type arraysep: str, optional :param labels: list of labels for each array, defaults to None :type labels: list of str, optional :return: multiline string containing formatted arrays :rtype: str :raises ValueError: if the arrays have different numbers of rows AUTO_EDIT For image processing this is useful for displaying small test images. The arrays are formatted and concatenated horizontally with a vertical separator. Each array has a header row that indicates the column number. Each row has a header column that indicates the row number. .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> A = Image.Random((5,5), maxval=9) >>> print(Image.strhcat(A)) >>> print(Image.strhcat(A, widths=2)) >>> B = Image.Random((5,5), maxval=9) >>> print(Image.strhcat(A, B)) >>> print(Image.strhcat(A, B, labels=("A:", "B:"))) The number of rows in each image must be the same, but the number of columns can vary. :seealso: :meth:`Image.print` :meth:`Image.showpixels` :func:`Image` """ # convert to a list of numpy arrays arrays = [image._A for image in images] # check that all arrays have the same number of rows if len(set([array.shape[0] for array in arrays])) != 1: raise ValueError("All arrays must have the same number of rows") if isinstance(widths, int): widths = [widths] * len(arrays) else: widths = list(widths) # add the header rows, which indicate the column number. 2-digit column # numbers are shown with the digits one above the other. stitle = "" # array title row s10 = " " * 5 # array column number, 10s digit s1 = " " * 5 # array column number, 1s digit divider = " " * 5 # divider between column number header and array values s = "" tens = False # has a tens row for i, array in enumerate(arrays): # iterate over the input arrays width = widths[i] # make the pixel value format string based on the passed width fmt = f" {{:{width}d}}" # build the title row if labels is not None: stitle += " " * (len(s10) - len(stitle)) + labels[i] # build the column number header rows for col in range(array.shape[1]): # the 10s digits if col // 10 == 0: s10 += " " * (width + 1) else: s10 += fmt.format(col // 10) tens = True # the 1s digits s1 += fmt.format(col % 10) divider += " " * width + "-" s10 += " " * len(arraysep) s1 += " " * len(arraysep) divider += " " * len(arraysep) # concatenate the header rows s = stitle + "\n" if tens: s += s10 + "\n" # only include if there are 10s digits s += s1 + "\n" + divider + "\n" # add the element values, row by row for row in range(array.shape[0]): # add the row number s += f"{row:3d}: " # for each array, add the elements for this row for array, width in zip(arrays, widths): # make the pixel value format string based on the width of the value fmt = f" {{:{width}d}}" for col in range(array.shape[1]): s += fmt.format(array[row, col]) if array is not arrays[-1]: s += arraysep s += "\n" return s def __repr__(self) -> str: """ Readable representation of image parameters :return: summary of image enclosed in angle brackets :rtype: str Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image.Read('flowers1.png') >>> img """ return f"<{str(self)}>" def copy(self, copy: bool = True) -> "Image": """ Create image copy :param copy: copy the image data :type copy: bool :return: copy of image :rtype: :class:`Image` Create a new :class:`Image` instance which contains a copy of the original image data. If ``copy`` is False the new :class:`Image` instance contains a reference to the original image data. """ return self.__class__(self, copy=copy) # ------------------------- properties ------------------------------ # @property def colororder(self) -> dict[str, int] | None: """ Set/get color order of image The color order is a dict where the key is the color plane name, eg. 'R' and the value is the index of the plane in the 3D pixel value array. Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image.Read('flowers1.png') >>> img.colororder >>> img.colororder = "BGR" >>> img.colororder When setting the color order the value can be any of: * simple string, one plane per character, eg. ``"RGB"`` * colon separated string, eg. ``"R:G:B"``, ``"L*:a*:b*"`` * dictionary, eg. ``dict(R=0, G=1, B=2)`` For the first two cases the color plane indices are implicit in the order in the string. .. note:: Changing the color order does not change the order of the planes in the image array, it simply changes their label. :seealso: :meth:`colororder_str` :meth:`colordict` :meth:`plane` :meth:`red` :meth:`green` :meth:`blue` """ return self._colororder @colororder.setter def colororder(self, colororder: str | dict[str, int]) -> None: cdict = Image.colordict(colororder) if self.nplanes is not None and len(cdict) != self.nplanes: raise ValueError("colororder length does not match number of planes") self._colororder = cdict @staticmethod def colordict(colororder) -> dict[str, int]: """ Parse a color order specification :param colororder: order of color channels :type colororder: str, dict :raises ValueError: ``colororder`` not a string or dict :return: dictionary mapping color names to plane indices :rtype: dict The color order the value can be given in a variety of forms: * simple string, one plane per character, eg. ``"RGB"`` * colon separated string, eg. ``"R:G:B"``, ``"L*:a*:b*"`` * dictionary, eg. ``dict(R=0, G=1, B=2)`` Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> Image.colordict('RGB') >>> Image.colordict('red:green:blue') >>> Image.colordict({'L*': 0, 'U*': 1, 'V*': 2}) :deprecated: use :meth:`colororder2dict` """ if isinstance(colororder, dict): cdict = colororder elif isinstance(colororder, str): if ":" in colororder: colororder = colororder.split(":") else: colororder = list(colororder) cdict = {} for i, color in enumerate(colororder): cdict[color] = i else: raise ValueError("color order must be a dict or string") return cdict @staticmethod def colororder2dict(colororder, start: int = 0) -> dict[str, int]: """ Parse a color order specification to a color dictionary :param colororder: order of color channels :type colororder: str, dict :param start: starting index for plane numbering, defaults to 0 :type start: int, optional :raises ValueError: ``colororder`` not a string or dict :return: dictionary mapping color names to plane indices :rtype: dict The color order the value can be given in a variety of forms: AUTO_EDIT * simple string, one plane per character, eg. ``"RGB"`` * colon separated string, eg. ``"R:G:B"``, ``"L*:a*:b*"`` * dictionary, eg. ``dict(R=0, G=1, B=2)`` Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> Image.colororder2dict('RGB') >>> Image.colororder2dict('red:green:blue') >>> Image.colororder2dict({'L*': 0, 'U*': 1, 'V*': 2}) """ if isinstance(colororder, dict): # already a dict if start > 0: # need to renumber the planes cdict = {} for key, value in colororder.items(): cdict[key] = value + start return cdict else: return colororder elif isinstance(colororder, str): if ":" in colororder: colororder = colororder.split(":") else: colororder = list(colororder) cdict = {} for i, color in enumerate(colororder): cdict[color] = i + start return cdict elif colororder is None: return {} else: raise ValueError("color order must be a dict or string") @staticmethod def colordict2list(cdict: dict[str, int]) -> list[str]: """ Convert a color dictionary to a list of color plane names :param cdict: dictionary mapping color names to plane indices :type cdict: dict :return: list of color channels in plane order :rtype: list Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> Image.colordict2list({'R': 0, 'G': 1, 'B': 2}) .. note:: The color planes are sorted by their index value. There is no check that the lowest plane index is zero. :seealso: :meth:`colordict2str` :meth:`colororder2dict` """ return [x[0] for x in sorted(cdict.items(), key=lambda x: x[1])] @staticmethod def colordict2str(cdict: dict[str, int]) -> str: """ Convert a color dictionary to a color order string :param cdict: dictionary mapping color names to plane indices :type cdict: dict :return: color channel names in plane order as a colon-separated string :rtype: str Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> Image.colordict2str({'R': 0, 'G': 1, 'B': 2}) .. note:: The color planes are sorted by their index value. There is no check that the lowest plane index is zero. :seealso: :meth:`colordict2list` :meth:`colororder2dict` AUTO_EDIT """ return ":".join(Image.colordict2list(cdict)) @property def colororder_str(self) -> str | None: """ Image color order as a string :return: Image color plane order as a colon separated string. :rtype: str Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image.Read('flowers1.png') >>> img.colororder_str :seealso: :meth:`colororder` """ if self.colororder is not None: s = sorted(self.colororder.items(), key=lambda x: x[1]) return ":".join([x[0] for x in s]) else: return None @property def name(self) -> str | None: """ Set/get image name An image has a string-valued name that can be read and written. The name is shown by the Image repr and when images are displayed graphically. Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image.Read('flowers1.png') >>> img.name[-70:] >>> img.name = 'my image' >>> img.name .. note:: Images loaded from a file have their name initially set to the full file pathname. :seealso: :meth:`Read` """ return self._name @name.setter def name(self, name: str | None) -> None: self._name = name # ---- image type ---- # @property def isfloat(self) -> bool: """ Image has floating point pixel values? :return: True if image has floating point values :rtype: bool Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image.Read('flowers1.png') >>> img.isfloat >>> img = Image.Read('flowers1.png', dtype='float32') >>> img.isfloat :seealso: :meth:`isint` :meth:`isbool` """ return np.issubdtype(self.dtype, np.floating) @property def isint(self) -> bool: """ Image has integer values? :return: True if image has integer pixel values :rtype: bool Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image.Read('flowers1.png') >>> img.isint >>> img = Image.Read('flowers1.png', dtype='float32') >>> img.isint :seealso: :meth:`isfloat` :meth:`isbool` """ return np.issubdtype(self.dtype, np.integer) @property def isbool(self) -> bool: """ Image has bolean values? :return: True if image has boolean pixel values :rtype: bool Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image.Read('flowers1.png') > 200 >>> img.isint >>> img.isbool :seealso: :meth:`isint` :meth:`isfloat` """ return np.issubdtype(self.dtype, bool) @property def dtype(self): """ Datatype of image :return: NumPy datatype of image :rtype: numpy.dtype Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image.Read('flowers1.png') >>> img.dtype >>> img = Image.Read('flowers1.png', dtype='float32') >>> img.dtype :seealso: :meth:`to` :meth:`to_int` :meth:`to_float` :meth:`like` :meth:`cast` :func:`numpy.dtype` """ return self._A.dtype @property def numnan(self) -> int: """ Number of NaN pixels in image :return: number of NaN pixels in image :rtype: int Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> from numpy import nan >>> img = Image([[1, 2], [3, nan]], dtype='float32') >>> img.numnan :seealso: :meth:`numinf` """ return int(np.isnan(self._A).sum()) @property def numinf(self) -> int: """ Number of Inf pixels in image :return: number of Inf pixels in image :rtype: int Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> from numpy import inf >>> img = Image([[1, 2], [3, inf]], dtype='float32') >>> img.numinf :seealso: :meth:`numnan` """ return int(np.isinf(self._A).sum()) # ---- image dimension ---- # @property def width(self) -> int: """ Image width :return: Width of image in pixels :rtype: int Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image.Read('flowers1.png') >>> img.width :seealso: :meth:`height` :meth:`size` :meth:`umax` """ return self._A.shape[1] @property def height(self) -> int: """ Image height :return: Height of image in pixels :rtype: int Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image.Read('flowers1.png') >>> img.height :seealso: :meth:`width` :meth:`size` :meth:`vmax` """ return self._A.shape[0] @property def umax(self) -> int: """ Image maximum u-coordinate :return: Maximum u-coordinate in image in pixels :rtype: int Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image.Read('flowers1.png') >>> img.umax >>> img.width :seealso: :meth:`width` """ return self._A.shape[1] - 1 @property def vmax(self) -> int: """ Image maximum v-coordinate :return: Maximum v-coordinate in image in pixels :rtype: int Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image.Read('flowers1.png') >>> img.vmax >>> img.height :seealso: :meth:`height` """ return self._A.shape[0] - 1 def uspan(self, step: int = 1) -> np.ndarray: """ Linear span of image horizontally :param step: step size, defaults to 1 :type step: int, optional :return: 1D array of values [0 ... width-1] :rtype: ndarray(W) Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> import numpy as np >>> img = Image.Read('flowers1.png') >>> with np.printoptions(threshold=10): >>> img.uspan() .. note:: If the image has a ``domain`` specified the horizontal component of this is returned instead. .. warning:: Computed using :meth:`numpy.arange` and for ``step>1`` the maximum coordinate may not be returned. :seealso: :meth:`umax` :meth:`vspan` """ if self.domain is None: return np.arange(0, self.width, step) else: return self.domain[0] def vspan(self, step: int = 1) -> np.ndarray: """ Linear span of image vertically :param step: step size, defaults to 1 :type step: int, optional :return: 1D array of values [0 ... height-1] :rtype: ndarray(H) Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> import numpy as np >>> img = Image.Read('flowers1.png') >>> with np.printoptions(threshold=10): >>> img.vspan() .. note:: If the image has a ``domain`` specified the vertical component of this is returned instead. .. warning:: Computed using :meth:`numpy.arange` and for ``step>1`` the maximum coordinate may not be returned. :seealso: :meth:`vmax` :meth:`uspan` """ if self.domain is None: return np.arange(0, self.height, step) else: return self.domain[1] @property def size(self) -> tuple[int, int]: """ Image size :return: Size of image (width, height) :rtype: tuple Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image.Read('flowers1.png') >>> img.size .. note:: The dimensions are in a different order compared to :meth:`shape`. :seealso: :meth:`width` :meth:`height` :meth:`shape` """ return (self._A.shape[1], self._A.shape[0]) @property def centre(self) -> tuple[float, float]: """ Coordinate of centre pixel :return: Coordinate (u,v) of the centre pixel :rtype: tuple Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image.Zeros((50,50)) >>> img.centre >>> img = Image.Zeros((51,51)) >>> img.centre .. note:: If the image has an even dimension the centre will lie between pixels. :seealso: :meth:`center` :meth:`centre_int` """ return (self._A.shape[1] / 2, self._A.shape[0] / 2) @property def center(self) -> tuple[float, float]: """ Coordinate of center pixel :return: Coordinate (u,v) of the centre pixel :rtype: tuple Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image.Zeros((50,50)) >>> img.center >>> img = Image.Zeros((51,51)) >>> img.center .. note:: - If the image has an even dimension the centre will lie between pixels. - Same as ``centre``, just US spelling :seealso: :meth:`center_int` """ return self.centre @property def centre_int(self) -> tuple[int, int]: """ Coordinate of centre pixel as integer :return: Coordinate (u,v) of the centre pixel :rtype: tuple Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image.Zeros((50,50)) >>> img.centre_int >>> img = Image.Zeros((51,51)) >>> img.centre_int .. note:: If the image has an even dimension the centre coordinate will be truncated toward zero. :seealso: :meth:`centre` """ return (self._A.shape[1] // 2, self._A.shape[0] // 2) @property def center_int(self) -> tuple[int, int]: """ Coordinate of centre pixel as integer :return: Coordinate (u,v) of the centre pixel :rtype: tuple Integer coordinate of centre pixel. Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image.Zeros((50,50)) >>> img.center_int >>> img = Image.Zeros((51,51)) >>> img.center_int .. note:: - If the image has an even dimension the centre coordinate will be truncated toward zero. - Same as ``centre_int``, just US spelling :seealso: :meth:`center` """ return self.centre_int @property def npixels(self) -> int: """ Number of pixels in image plane :return: Number of pixels in image plane: width x height :rtype: int .. note:: Number of planes is not considered. Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image.Read('flowers1.png') >>> img.npixels >>> img.width * img.height :seealso: :meth:`size` """ return self._A.shape[0] * self._A.shape[1] @property def shape(self) -> tuple[int, int] | tuple[int, int, int]: """ Image shape :return: Shape of internal NumPy array :rtype: 2-tuple, or 3-tuple if color Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image.Read('flowers1.png') >>> img.shape >>> img = Image.Read('street.png') >>> img.shape .. note:: The dimensions are in a different order compared to :meth:`size`. :seealso: :meth:`size` :meth:`nplanes` :meth:`ndim` :meth:`iscolor` """ return self._A.shape @property def ndim(self) -> int: """ Number of image array dimensions :return: number of image dimensions :rtype: int Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image.Read('flowers1.png') >>> img.ndim >>> img = Image.Read('street.png') >>> img.ndim :seealso: :meth:`nplanes` :meth:`shape` """ return self._A.ndim def contains(self, p: Array2d) -> bool | np.ndarray: """ Test if coordinate lies within image :param p: pixel coordinate :type p: array_like(2), ndarray(2,N) :return: whether pixel coordinate lies within image bounds :rtype: bool, ndarray(N) Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> import numpy as np >>> img = Image.Zeros(10) >>> img.contains((4,6)) >>> img.contains((-1, 7)) >>> img.contains(np.array([[4, 6], [-1, 7], [10, 10]]).T) """ if isinstance(p, np.ndarray) and p.ndim == 2 and p.shape[0] == 2: u = p[0, :] v = p[1, :] else: u = p[0] v = p[1] return np.logical_and.reduce((u >= 0, v >= 0, u < self.width, v < self.height)) # ---- color related ---- # @property def iscolor(self) -> bool: """ Image has color pixels? :return: Image is color :rtype: bool :return: number of image dimensions :rtype: int Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image.Read('flowers1.png') >>> img.iscolor >>> img = Image.Read('street.png') >>> img.iscolor :seealso: :meth:`isrgb` :meth:`nplanes` """ return self._A.ndim > 2 @property def isbgr(self) -> bool: """ Image has BGR color order? :return: Image has BGR color order :rtype: bool Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image.Read('flowers1.png') >>> img.isbgr .. note:: Is False if image is not color. :seealso: :meth:`colororder` """ return self.colororder_str == "B:G:R" @property def isrgb(self) -> bool: """ Image has RGB color order? :return: Image has RGB color order :rtype: bool Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image.Read('flowers1.png') >>> img.isrgb .. note:: Is False if image is not color. :seealso: :meth:`colororder` """ return self.colororder_str == "R:G:B" def to(self, dtype: Dtype) -> "Image": """ Convert image datatype :param dtype: Numpy data type :type dtype: str or NumPy dtype :return: image :rtype: :class:`Image` Create a new image, same size as input image, with pixels of a different datatype. Integer values are scaled according to the maximum value of the datatype, floating values are in the range 0.0 to 1.0. Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image.Random(3) >>> img.print() >>> img.to('float').print() >>> img = Image.Random(3, dtype='float') >>> img.print() >>> img.to('uint8').print() :seealso: :meth:`astype` :meth:`to_int` :meth:`to_float` """ # convert image to different type, does rescaling # as just changes type dtype = np.dtype(dtype) # convert to dtype if it's a string return self.__class__(self.array_as(dtype), dtype=dtype) def astype(self, dtype: Dtype) -> "Image": """ Cast image datatype :param dtype: Numpy data type :type dtype: str or NumPy dtype :return: image :rtype: :class:`Image` Create a new image, same size as input image, with pixels of a different datatype. Values are retained, only the datatype is changed. Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image.Random(3) >>> img.print() >>> img.astype('float').print() >>> img = Image.Random(3, dtype='float') >>> img.print() >>> img.astype('uint8').print() :seealso: :meth:`to` """ return self.__class__(self._A.astype(dtype), dtype=dtype) # ---- NumPy array access ---- # @property def image(self) -> Array2d | Array3d: """ Image as NumPy array :return: image as a NumPy array :rtype: ndarray(H,W) or ndarray(H,W,3) Return a reference to the encapsulated NumPy array that holds the pixel values. Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image.Read('flowers1.png') >>> img >>> type(img) >>> type(img.image) .. note:: For a color image the color plane order is given by the colororder dictionary. .. deprecated:: 1.0.3 Use :meth:`array` instead. :seealso: :meth:`A` :meth:`colororder` """ warnings.warn( "image property will be deprecated in v2.0, use .array instead", DeprecationWarning, stacklevel=2, ) return self._A # type: ignore[return-value] @property def array(self) -> np.ndarray: """ Image as NumPy array :return: image as a NumPy array :rtype: ndarray(H,W) or ndarray(H,W,3) Return a reference to the encapsulated NumPy array that holds the pixel values. Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image.Read('flowers1.png') >>> type(img) >>> type(img.array) .. note:: For a color image the color plane order is given by the colororder dictionary. .. warning:: This property is for accessing the NumPy image data for passing to a NumPy, OpenCV or other function. It is not intended for direct manipulation of the image data -- doing so may make the state of the Image instance inconsistent. :seealso: :meth:`array_as` :meth:`rgb` :meth:`bgr` :meth:`colororder` """ return self._A @property def A(self) -> Array2d | Array3d: """ Set/get the NumPy array containing pixel values **Getting** :return: image as a NumPy array :rtype: ndarray(H,W) or ndarray(H,W,3) Return a reference to the encapsulated NumPy array that holds the pixel values. **Setting** Replace the encapsulated NumPy array with another. Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> import numpy as np >>> img = Image.Read('flowers1.png') >>> img >>> type(img) >>> type(img.A) >>> img.A = np.zeros((50,50)) >>> img .. deprecated:: 1.0.3 Use :meth:`array` instead for accessing the NumPy image data and the :meth:`Image` constructor for creating a new ``Image`` with a different array. :seealso: :meth:`array` :meth:`image` """ return self._A # type: ignore[return-value] @A.setter def A(self, A: Array2d | Array3d) -> None: warnings.warn( "A setter will be deprecated in v2.0, use Image constructor instead", DeprecationWarning, stacklevel=2, ) self._A = A @property def rgb(self) -> np.ndarray: # type: ignore[return-value] """ Image as NumPy array in RGB color order :raises ValueError: image is greyscale :return: image as a NumPy array in RGB color order :rtype: ndarray(H,W,3) The image is guaranteed to be in RGB order irrespective of current color order. :seealso: :meth:`image` :meth:`bgr` :meth:`colororder` """ if not self.iscolor: raise ValueError("greyscale image has no rgb property") if self.isrgb: return self._A elif self.isbgr: return self._A[:, :, ::-1] @property def bgr(self) -> np.ndarray: # type: ignore[return-value] """ Image as NumPy array in BGR color order :raises ValueError: image is greyscale :return: image as a NumPy array in BGR color order :rtype: ndarray(H,W,3) The image is guaranteed to be in BGR (OpenCV standard) order irrespective of current color order. :seealso: :meth:`image` :meth:`rgb` :meth:`colororder` """ if not self.iscolor: raise ValueError("greyscale image has no bgr property") if self.isbgr: return self._A elif self.isrgb: return self._A[:, :, ::-1] # ------------------------- datatype operations ----------------------- # def to_int(self, intclass: Dtype = "uint8") -> np.ndarray: """ Image as integer NumPy array :param intclass: name of NumPy supported integer class, default is 'uint8' :type intclass: str or NumPy dtype, optional :return: NumPy array with integer values :rtype: ndarray(H,W) or ndarray(H,W,P) Return a NumPy array with pixels converted to the integer class ``intclass``. For the case where the input image is: * a floating point class, the pixel values are scaled from an input range of [0,1] to a range spanning zero to the maximum positive value of the output integer class. * an integer class, then the pixels are scaled and cast to ``intclass``. The scale factor is the ratio of the maximum value of the input and output integer classes. * boolean class, False is mapped to zero and True is mapped to the maximum positive value. Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image([[5_000, 10_000], [30_000, 60_000]]) >>> img >>> img.to_int('uint8') >>> img.to_int('uint32') >>> img = Image([[0.0, 0.3], [0.5, 1.0]]) >>> img >>> img.to_int('uint8') >>> img = Image([[False, True], [True, False]]) >>> img >>> img.to_int('uint8') >>> img.to_int('uint16') .. note:: Works for greyscale or color (arbitrary number of planes) image .. deprecated:: 1.0.3 Use :meth:`array_as` instead. :seealso: :meth:`array_as` :meth:`to_float` :meth:`cast` :meth:`like` """ warnings.warn( "to_int property will be deprecated in v2.0, use .array_as(int_type) instead", DeprecationWarning, stacklevel=2, ) return int_image(self._A, intclass) def to_float(self, floatclass: Dtype = "float32") -> np.ndarray: """ Image as float NumPy array :param floatclass: 'single', 'double', 'float32' [default], 'float64' :type floatclass: str or NumPy dtype :return: NumPy array with floating point values :rtype: ndarray(H,W) or ndarray(H,W,P) Return a NumPy array with pixels converted to the floating point class ``floatclass`` and the values span the range 0 to 1. For the case where the input image is: * an integer class, the pixel values are scaled from an input range spanning zero to the maximum positive value of the integer class to [0.0, 1.0] * a floating class, the pixels are cast to change type but not their value. * boolean class, False is mapped to 0.0 and True is mapped to 1.0. Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image([[50, 100], [150, 200]]) >>> img >>> img.to_float() >>> img = Image([[0.0, 0.3], [0.5, 1.0]]) >>> img >>> img.to_float('float64') >>> img = Image([[False, True], [True, False]]) >>> img.to_float() .. note:: Works for greyscale or color (arbitrary number of planes) image .. deprecated:: 1.0.3 Use :meth:`array_as` instead. :seealso: :meth:`array_as` :meth:`to_int` :meth:`cast` :meth:`like` """ warnings.warn( "to_float property will be deprecated in v2.0, use .array_as(float_type) instead", DeprecationWarning, stacklevel=2, ) return float_image(self._A, floatclass) def array_as(self, dtype: Dtype | None = None) -> np.ndarray: """ Convert Image to NumPy array of specified type :param dtype: data type of the output array :type dtype: str or np.dtype :return: NumPy array with specified type :rtype: ndarray(H,W) or ndarray(H,W,P) Return a NumPy array with pixels converted to the specified data type. ========= ======================================== ==================================== Input Output int Output float ========= ======================================== ==================================== float scaled [-1,1] → [min_int, max_int] cast, values unchanged int scaled by max_int_output/max_int_input scaled [min_int, max_int] → [-1, 1] uint scaled by max_int_output/max_int_input scaled [0, max_int] → [0, 1] bool False → 0, True → max_int False → 0.0, True → 1.0 ========= ======================================== ==================================== Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image([[5_000, 10_000], [30_000, 60_000]]) # uint16 image >>> img >>> img.array_as('uint8') >>> img.array_as('uint32') >>> img.array_as('float') >>> img = Image([[0.0, 0.3], [0.5, 1.0]]) >>> img >>> img.array_as('uint8') >>> img = Image([[False, True], [True, False]]) >>> img >>> img.array_int('uint8') >>> img.array_int('float') .. note:: Works for greyscale or color (arbitrary number of planes) image .. warning:: This method assumes that all integer values are unsigned. :seealso: :meth:`array` :func:`array_float` :meth:`cast` :meth:`like` """ if dtype is None: return self._A else: dtype = np.dtype(dtype) # convert to dtype if it's a string if np.issubdtype(dtype, np.integer) or np.issubdtype(dtype, np.bool_): return int_image(self._A, dtype) elif np.issubdtype(dtype, np.floating): return float_image(self._A, dtype) else: raise ValueError("unsupported dtype specified") def cast(self, value: int | float) -> Any: """ Cast value to same type as image :param value: value to cast :type value: scalar, ndarray :return: value cast to same type as image :rtype: numpy type, ndarray The value, scalar or integer, is **cast** to the same type as the image. The result has the same numeric value, but the type is changed. Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image.Read('flowers1.png') >>> x = img.cast(12.5) >>> x >>> type(x) .. note:: Scalars are cast to NumPy types not native Python types. :seealso: :meth:`like` """ return self._A.dtype.type(value) def like(self, value: int | float, maxint: int | None = None) -> Any: """ Convert value to the same type as image :param value: scalar or NumPy array :type value: scalar, ndarray :param maxint: maximum integer value for an integer image, defaults to maximum positive value of the class :type maxint: int, optional :raises ValueError: [description] :return: converted value :rtype: NumPy type The value, scalar or integer, is **converted** to the same type as the image. The result is optionally rescaled and cast: * Float to float: values are cast * Float to int: values in the interval [0, 1] are scaled to the interval [0, ``maxint``] and then cast * Int to float: values in the interval [0, ``maxint``] are scaled to the interval [0, 1] and then cast. * Int to int: values are scaled and cast. .. warning:: For integer to integer conversion, integer values greater than the maximum value of the destination class are wrapped not clipped. Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image.Read('flowers1.png') >>> img.like(0.5) :seealso: :meth:`cast` :meth:`to_int` :meth:`to_float` """ if self.isint: # matching to an integer image if ( isinstance(value, np.ndarray) and np.issubdtype(value.dtype, np.integer) or isinstance(value, (int, np.integer)) ): # already an integer, cast it to right sort return self.cast(value) else: # it's a float, rescale it then cast return self.cast(value * self.maxval) else: # matching to a float image if ( isinstance(value, np.ndarray) and np.issubdtype(value.dtype, np.floating) or isinstance(value, (float, np.floating)) ): # already a float of some sort, cast it to the right sort return self.cast(value) else: # it's an int. We use hints to determine the size, otherwise # get it from the type if maxint is None: if isinstance(value, np.ndarray): maxint = np.iinfo(value.dtype).max else: maxint = np.iinfo(type(value)).max elif isinstance(maxint, int): pass elif isinstance(maxint, str) or isinstance(maxint, np.dtype): maxint = np.iinfo(maxint).max else: raise ValueError("bad max value specified") return self.cast(value / maxint) @property def minval(self): """ Minimum value of image datatype :return: Minimum value :rtype: int or float For the datatype of the image, return its minimum possible value Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image.Zeros(20, dtype='float32') >>> img.minval >>> img = Image.Zeros(20, dtype='uint8') >>> img.minval :seealso: :meth:`maxval` """ if self.isint: return np.iinfo(self.dtype).min else: return np.finfo(self.dtype).min @property def maxval(self): """ Maximum value of image datatype :return: Maximum value :rtype: int or float For the datatype of the image, return its maximum possible value Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image.Zeros(20, dtype='float32') >>> img.maxval >>> img = Image.Zeros(20, dtype='uint8') >>> img.maxval :seealso: :meth:`minval` """ if self.isint: return np.iinfo(self.dtype).max else: return np.finfo(self.dtype).max @property def true(self) -> int | float: """ True value for logical image :return: Value used as true :rtype: int or float The true value is 1.0 for a floating point image and maximum integer value for an integer value. Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image.Zeros(20, dtype='float32') >>> img.true >>> img = Image.Zeros(20, dtype='uint8') >>> img.true :seealso: :meth:`false` :meth:`maxval` """ if self.isint: return self.maxval else: return 1.0 @property def false(self) -> int | float: """ False value for logical image :return: Value used as true :rtype: int or float The false value is 0.0 for a floating point image and 0 value for an integer value. Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image.Zeros(20, dtype='float32') >>> img.false >>> img = Image.Zeros(20, dtype='uint8') >>> img.false :seealso: :meth:`true` """ return 0 # ------------------------- color plane access -------------------------- # @property def nplanes(self) -> int: """ Number of color planes :return: Number of color planes, or None if image is empty :rtype: int For a 2D or greyscale image this is one, otherwise it is the third dimension of the image. Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image.Read('street.png') >>> img.nplanes >>> img = Image.Read('flowers1.png') >>> img.nplanes .. note:: A greyscale image is stored internally as a 2D NumPy array which has zero planes, but ``nplanes`` will return 1 in that case. :seealso: :meth:`iscolor` :meth:`shape` :meth:`ndim` """ if self._A is None: return 0 elif self._A.ndim == 2: return 1 else: return int(self._A.shape[-1]) # type: ignore[return-value] def planes(self) -> Iterator[Image]: """ Iterator for image planes :return out: image containing a single plane :rtype: :class:`Image` Iterator that returns sequential image planes. Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image.Read("flowers4.png") # in BGR order >>> for plane in img.planes(): >>> print(plane) """ for i in range(self.nplanes): yield self.plane(i) def plane(self, planes) -> "Image": """ Extract plane(s) from color image :param planes: planes to extract :type planes: int, list, str :raises ValueError: if image is not color :return out: image containing only the selected planes :rtype: :class:`Image` Create a new image from the selected planes of the image. Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image.Read("flowers4.png") # in BGR order >>> img.colororder_str >>> img.nplanes >>> red = img.plane(0) # red plane >>> red >>> red.iscolor >>> red.nplanes >>> green_blue = img.plane('G:B') # green and blue planes >>> green_blue >>> green_blue.iscolor >>> green_blue.nplanes >>> red_blue = img.plane([0, 2]) # blue and red planes >>> red_blue .. note:: - This can also be performed using the overloaded ``__getitem__`` operator. - To select more than one plane, use either a sequence of integers or a string of colon separated plane names. - For a single-plane image the index must be zero. :seealso: :meth:`red` :meth:`green` :meth:`blue` :meth:``__getitem__`` """ if isinstance(planes, int): if planes == 0 and self.nplanes == 1: return self if planes < 0 or planes >= self.nplanes: raise ValueError("plane index out of range") iplanes = [planes] colororder = None elif isinstance(planes, str): if self.colororder is None: raise ValueError("plane names require colororder") iplanes = [] colororder = {} if ":" in planes: planes = planes.split(":") planes = [p for p in planes if p != ""] for plane in planes: try: i = self.colororder[plane] iplanes.append(i) colororder[plane] = len(colororder) # copy to new dict except KeyError: raise ValueError("bad plane name specified") elif isinstance(planes, (tuple, list)): if self.colororder is None: raise ValueError("plane names require colororder") colororder = {} for plane in planes: if not isinstance(plane, int) or plane < 0 or plane >= self.nplanes: raise ValueError("plane index invalid or out of range") colorname = [k for k, v in self.colororder.items() if v == plane][0] colororder[colorname] = plane iplanes = planes else: raise ValueError("bad plane specified") if isinstance(iplanes, list) and len(iplanes) == 1: iplanes = iplanes[0] colororder = None return self.__class__(self._A[:, :, iplanes], colororder=colororder) def __getitem__( self, keys: int | str | tuple[slice, slice] | tuple[slice, slice, slice] ): """ Return pixel value or slice from image :param keys: slices to extract :type keys: int, str, tuple of int or slice :return: slice of image :rtype: :class:`Image` This is a Swiss-army knife method for accessing subregions of an ``Image`` by uv-region and/or plane. A ``key`` can be: - a 2-tuple of integers, eg. ``img[u,v]``, return this pixel as a scalar. If the image has multiple planes, the result is an ndarray over planes. - a 3-tuple of integers, eg, ``img[u,v,p]``, for a multiplane image return this pixel from specified plane as a scalar. - a 2-tuple containing at least one slice object, eg. ``img[100:110, 200:300]``, return this region as an ``Image``. If the image has multiple planes, the result is a multiplane ``Image``. - a 3-tuple containing at least one slice objects, return this region of uv and planes as an ``Image`` with one or more planes. - an int, return this plane as an ``Image``. - a string, return this named plane or planes as an ``Image``. Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image.Read("flowers4.png") # in RGB order >>> red = img[0] # red plane >>> red # greyscale image >>> green = img["G"] >>> green # greyscale image >>> roi = img[100:200, 300:400] >>> roi # color image >>> roi = img[100:200, 300:400, 1:] >>> roi # 2-plane color image >>> roi = img[100, :] # column 100 >>> roi # color image >>> pix = img[100, 200, 1] # green pixel at (100,200) as scalar >>> pix >>> pix = img[100, 200] # RGB vector at (100,200) as ndarray >>> pix .. note:: If the result is a single row or column the result is a 1xn or nx1 ``Image`` instance. If the result is a single plane the result is a greyscale image. .. note:: Indexing pixel values this way is slow, use :meth:`pixel(u,v)` for faster access, or ``img.A[v,u]`` for direct access to the underlying NumPy array. .. warning:: The order of the indices is column, row and plane. This is the opposite of the order used for NumPy index on the underlying array. It is consistent with the column-first convention used across the Toolbox and is consistent with the :math:`(u,v)` coordinate system for images. .. versionadded:: 1.0.0 The order of the indices changed to column, row, plane. Previously it was row, column, plane. :seealso: :meth:`red` :meth:`green` :meth:`blue` :meth:`plane` :meth:`roi` :meth:`pixel` """ def fixdims(out, shape, keys): # deal with the fact that some of the keys may have reduced the # dimensionality of the array, eg. a slice of span 0 or 1, or an integer # key. # shape: (nrows, ncols, nplanes) in NumPy order # key: (rowspec, colspec, planespec) in NumPy order def lenkey(key, max): # compute the span of a particular key if isinstance(key, int): # int key has a span on 1 return 1 elif isinstance(key, slice): # slice key has a span depending on the slice and the corresponding # array dimension. we have to essentially replicate the logic of # slice() here. start = key.start if key.start is not None else 0 stop = key.stop if key.stop is not None else max step = key.step if key.step is not None else 1 n = stop - start if n < step: return 1 else: return n // step if len(shape) == 2: dims = [lenkey(keys[0], shape[0]), lenkey(keys[1], shape[1])] elif len(shape) == 3: dims = [ lenkey(keys[0], shape[0]), lenkey(keys[1], shape[1]), lenkey(keys[2], shape[2]), ] # ignore loss of color dimension if dims[2] == 1: dims = dims[:2] return out.reshape(dims) if ( isinstance(keys, tuple) and len(keys) == 2 and isinstance(keys[0], int) and isinstance(keys[1], int) ): # integer keys for row and column, the result is a scalar or vector over planes return self._A[keys[1], keys[0]] elif ( isinstance(keys, tuple) and len(keys) == 3 and isinstance(keys[0], int) and isinstance(keys[1], int) and isinstance(keys[2], int) ): # integer keys for row and column, the result is a scalar or vector over planes return self._A[keys[1], keys[0], keys[2]] elif isinstance(keys, int): # single integer index, it's a color plane index return self.__class__(self._A[..., keys]) elif isinstance(keys, str): # color plane by name return self.plane(keys) elif isinstance(keys, (list, tuple)): # by slices if self.iscolor: if len(keys) == 2: keys = (keys[1], keys[0], slice(None)) elif len(keys) == 3: keys = (keys[1], keys[0], keys[2]) else: raise ValueError("invalid number of slices") # slice the data out of the ndarray out = self._A[keys] if out.ndim < 3: out = fixdims(out, self._A.shape, keys) else: # greyscale image if len(keys) == 2: keys = (keys[1], keys[0]) else: raise ValueError("invalid number of slices") # slice the data out of the ndarray out = self._A[keys] if out.ndim < 2: out = fixdims(out, self._A.shape, keys) # a singleton plane dimensions is a grey scale image if out.ndim == 3 and out.shape[2] == 1: out = out.squeeze(2) colororder = None if out.ndim == 3: # 3 slices, select uv-region and planes colororder = (self.colororder_str or "").split(":") colororder = colororder[keys[2]] # type: ignore[index] colororder = ":".join(colororder) return self.__class__(out, colororder=colororder) else: raise ValueError("invalid slice") def pixel(self, u: int, v: int) -> int | float | np.ndarray: """ Return pixel value :param u: column coordinate :type u: int :param v: row coordinate :type v: int :return: pixel value :rtype: int, float or ndarray Return the specified pixel. If the image has multiple planes, the result is a vector over planes. .. note:: This method is faster than the more general :meth:`__getitem__` for the individual pixel case. .. warning:: The order of the indices is column, row and plane. This is the opposite of the order used for NumPy index on the underlying array. It is consistent with the column-first convention used across the Toolbox and is consistent with the :math:`(u,v)` coordinate system for images. Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image.Read("flowers4.png", mono=True) >>> pix = img.pixel(100, 200) >>> pix # grey scale pixel value >>> img = Image.Read("flowers4.png") >>> pix = img.pixel(100, 200) >>> pix # color pixel value (R, G, B) :seealso: :meth:`__getitem__` :meth:`roi` """ return self._A[v, u] def pixels_mask( self, mask: Image | Polygon2 | list[Polygon2], coords: bool = False, return_mask: bool = False, ): """ Return pixel values at locations specified by a mask :param mask: the selection mask as either non-zero pixels in a 2D image or the area covered by a polygon or list of polygons :type mask: :class:`Image`, a single :class:`Polygon2` or a list of :class:`Polygon2` :param coords: include pixel coordinates in output, defaults to False :type coords: bool, optional :param return_mask: also return the mask as an Image, defaults to False :type return_mask: bool, optional :return: array of pixel values and optionally coordinates of pixels selected by the mask, optionally also the mask as an Image :rtype: :class:`Array2d`, :class:`Array2d` and :class:`Image` For an image with P planes and a mask that selects N pixels, return an PxN array of pixel values. If ``coords`` is True, the result is an (P+2)xN array where the first two rows are the u and v coordinates of the selected pixels. Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> from spatialmath import Polygon2 >>> img = Image.Read("flowers4.png") >>> polygon = Polygon2([(300, 400), (360, 400), (330, 450)], close=True) >>> pixels = img.pixels_mask(polygon, coords=True) >>> pixels.shape >>> pixels[:,:5] # first 5 pixels, with coordinates :seealso: :meth:`pixel` :meth:`__getitem__` :meth:`roi` """ if isinstance(mask, Image): if mask.ndim != 2: raise ValueError("mask must be a 2D image") if mask.shape != self.shape[:2]: raise ValueError("mask must be same shape as image") mask_array = mask.array else: # its a Polygon2 or list of Polygon2, we create a mask image from it mask_array = Image.Polygons(self.size, mask, color=1, dtype="uint8").array v, u = np.where(mask_array > 0) # Access the pixel values in the original image pixel_values = self._A[v, u, ...].T # optionally add the if coords: pixel_values = np.vstack((u, v, pixel_values)) if return_mask: return pixel_values, Image(mask_array) else: return pixel_values def red(self) -> "Image": """ Extract the red plane of a color image :raises ValueError: if image is not color :return out: greyscale image representing the red image plane :rtype: :class:`Image` Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image.Read("flowers4.png") >>> red = img.red() # red plane >>> red.disp() >>> red >>> red.iscolor .. plot:: from machinevisiontoolbox import Image Image.Read("flowers4.png").red().disp() :seealso: :meth:`plane` :meth:`green` :meth:`blue` """ return self.plane("R") def green(self) -> "Image": """ Extract the green plane of a color image :raises ValueError: if image is not color :return out: greyscale image representing the green image plane :rtype: :class:`Image` Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image.Read("flowers4.png") >>> green = img.green() # green plane >>> green.disp() >>> green >>> green.iscolor .. plot:: from machinevisiontoolbox import Image Image.Read("flowers4.png").green().disp() :seealso: :meth:`plane` :meth:`red` :meth:`blue` """ return self.plane("G") def blue(self) -> "Image": """ Extract the blue plane of a color image :raises ValueError: if image is not color :return out: greyscale image representing the blue image plane :rtype: :class:`Image` Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image.Read("flowers4.png") >>> blue = img.blue() # blue plane >>> blue.disp() >>> blue >>> blue.iscolor .. plot:: from machinevisiontoolbox import Image Image.Read("flowers4.png").blue().disp() :seealso: :meth:`plane` :meth:`red` :meth:`green` """ return self.plane("B") # I think these are not used anymore # def astype(self, type): # return self.__class__(self.A, dtype=type) # ------------------------- operators ------------------------------ # # arithmetic def __mul__(self, other) -> "Image": """ Overloaded ``*`` operator :return: element-wise product of images :rtype: :class:`Image` Compute the product of an Image with another image or a scalar. Supports: * image ``*`` image, element-wise * scalar ``*`` image * image ``*`` scalar Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image([[1, 2], [3, 4]]) >>> z = img * img >>> z.array >>> z = 2 * img >>> z.array ..warning:: Values will be wrapped not clipped to the range of the pixel datatype. """ return self._binop(self, other, lambda x, y: x * y) def __rmul__(self, other) -> "Image": return self._binop(self, other, lambda x, y: y * x) def __imul__(self, other) -> "Image": """ Overloaded in-place ``*=`` operator :return: self, modified in place :rtype: :class:`Image` Multiply an Image in place by another image or a scalar. :seealso: :meth:`__mul__` """ self._A = self._binop(self, other, lambda x, y: x * y)._A return self def __pow__(self, other) -> "Image": """ Overloaded ``**`` operator :return: element-wise exponent of image :rtype: :class:`Image` Compute the element-wise power of an Image. Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image([[1, 2], [3, 4]]) >>> z = img**3 >>> z.array ..warning:: Values will be wrapped not clipped to the range of the pixel datatype. """ if not isscalar(other): raise ValueError("exponent must be a scalar") return self._binop(self, other, lambda x, y: x**y) def __add__(self, other) -> "Image": """ Overloaded ``+`` operator :return: element-wise addition of images :rtype: :class:`Image` Compute the sum of an Image with another image or a scalar. Supports: * image ``+`` image, element-wise * scalar ``+`` image * image ``+`` scalar Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image([[1, 2], [3, 4]]) >>> z = img + img >>> z.array >>> z = 10 + img >>> z.array ..warning:: Values will be wrapped not clipped to the range of the pixel datatype. """ return self._binop(self, other, lambda x, y: x + y) def __radd__(self, other) -> "Image": return self._binop(self, other, lambda x, y: y + x) def __iadd__(self, other) -> "Image": """ Overloaded in-place ``+=`` operator :return: self, modified in place :rtype: :class:`Image` Add another image or a scalar to an Image in place. :seealso: :meth:`__add__` """ self._A = self._binop(self, other, lambda x, y: x + y)._A return self def __sub__(self, other) -> "Image": """ Overloaded ``-`` operator :return: element-wise subtraction of images :rtype: :class:`Image` Compute the difference of an Image with another image or a scalar. Supports: * image ``-`` image, element-wise * scalar ``-`` image * image ``-`` scalar Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image([[1, 2], [3, 4]]) >>> z = img - img >>> z.array >>> z = img - 1 >>> z.array ..warning:: Values will be wrapped not clipped to the range of the pixel datatype. """ return self._binop(self, other, lambda x, y: x - y) def __rsub__(self, other) -> "Image": return self._binop(self, other, lambda x, y: y - x) def __isub__(self, other) -> "Image": """ Overloaded in-place ``-=`` operator :return: self, modified in place :rtype: :class:`Image` Subtract another image or a scalar from an Image in place. :seealso: :meth:`__sub__` """ self._A = self._binop(self, other, lambda x, y: x - y)._A return self def __truediv__(self, other) -> "Image": """ Overloaded ``/`` operator :return: element-wise division of images :rtype: :class:`Image` Compute the quotient of an Image with another image or a scalar. Supports: * image ``/`` image, element-wise * scalar ``/`` image * image ``/`` scalar Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image([[1, 2], [3, 4]]) >>> z = img / img >>> z.array >>> z = img / 2 >>> z.array .. note:: The resulting values are floating point. """ return self._binop(self, other, lambda x, y: x / y) def __rtruediv__(self, other) -> "Image": return self._binop(self, other, lambda x, y: y / x) def __itruediv__(self, other) -> "Image": """ Overloaded in-place ``/=`` operator :return: self, modified in place :rtype: :class:`Image` Divide an Image in place by another image or a scalar. :seealso: :meth:`__truediv__` """ self._A = self._binop(self, other, lambda x, y: x / y)._A return self def __floordiv__(self, other) -> "Image": """ Overloaded ``//`` operator :return: element-wise floored division of images :rtype: :class:`Image` Compute the integer quotient of an Image with another image or a scalar. Supports: * image ``//`` image * scalar ``//`` image * image ``//`` scalar Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image([[1, 2], [3, 4]]) >>> z = img / 2 >>> z.array >>> z = img // 2 >>> z.array """ return self._binop(self, other, lambda x, y: x // y) def __rfloordiv__(self, other) -> "Image": return self._binop(self, other, lambda x, y: y // x) def __ifloordiv__(self, other) -> "Image": """ Overloaded in-place ``//=`` operator :return: self, modified in place :rtype: :class:`Image` Divide an Image in place by another image or a scalar, rounding down. :seealso: :meth:`__floordiv__` """ self._A = self._binop(self, other, lambda x, y: x // y)._A return self def __neg__(self) -> "Image": """ Overloaded unary ``-`` operator :return: element-wise negation of image :rtype: :class:`Image` Compute the element-wise negation of an Image. Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image([[1, -2], [-3, 4]], dtype='int8') >>> img.print() >>> neg_img = -img >>> neg_img.print() """ return self._unop(self, lambda x: -x) # bitwise def __and__(self, other) -> "Image": """ Overloaded ``&`` operator :return: element-wise binary-and of images :rtype: :class:`Image` Compute the binary-and of an Image with another image or a scalar. Supports: * image ``&`` image * scalar ``&`` image * image ``&`` scalar Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image([[1, 2], [3, 4]]) >>> z = img & Image([[2, 2], [2, 2]]) >>> z.array >>> z = img & 1 >>> z.array """ return self._binop(self, other, lambda x, y: x & y) def __iand__(self, other) -> "Image": """ Overloaded in-place ``&=`` operator :return: self, modified in place :rtype: :class:`Image` Bitwise-AND an Image in place with another image or a scalar. :seealso: :meth:`__and__` """ self._A = self._binop(self, other, lambda x, y: x & y)._A return self def __or__(self, other) -> "Image": """ Overloaded ``|`` operator :return: element-wise binary-or of images :rtype: :class:`Image` Compute the binary-or of an Image with another image or a scalar. Supports: * image ``|`` image * scalar ``|`` image * image ``|`` scalar Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image([[1, 2], [3, 4]]) >>> z = img | Image([[2, 2], [2, 2]]) >>> z.array >>> z = img | 1 >>> z.array """ return self._binop(self, other, lambda x, y: x | y) def __ior__(self, other) -> "Image": """ Overloaded in-place ``|=`` operator :return: self, modified in place :rtype: :class:`Image` Bitwise-OR an Image in place with another image or a scalar. :seealso: :meth:`__or__` """ self._A = self._binop(self, other, lambda x, y: x | y)._A return self def __xor__(self, other) -> "Image": """ Overloaded ``^`` operator :return: element-wise binary-xor of images :rtype: :class:`Image` Compute the binary-xor of an Image with another image or a scalar. Supports: * image ``^`` image * scalar ``^`` image * image ``^`` scalar Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image([[1, 2], [3, 4]]) >>> z = img ^ Image([[2, 2], [2, 2]]) >>> z.array >>> z = img ^ 1 >>> z.array """ return self._binop(self, other, lambda x, y: x ^ y) def __ixor__(self, other) -> "Image": """ Overloaded in-place ``^=`` operator :return: self, modified in place :rtype: :class:`Image` Bitwise-XOR an Image in place with another image or a scalar. :seealso: :meth:`__xor__` """ self._A = self._binop(self, other, lambda x, y: x ^ y)._A return self def __lshift__(self, other) -> "Image": """ Overloaded ``<<`` operator :return: element-wise binary-left-shift of images :rtype: :class:`Image` Left shift pixel values in an Image. Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image([[1, 2], [3, 4]]) >>> z = img << 1 >>> z.array """ if not isinstance(other, int): raise ValueError("left shift must be by integer amount") return self._binop(self, other, lambda x, y: x << y) def __ilshift__(self, other) -> "Image": """ Overloaded in-place ``<<=`` operator :return: self, modified in place :rtype: :class:`Image` Left-shift pixel values of an Image in place. :seealso: :meth:`__lshift__` """ if not isinstance(other, int): raise ValueError("left shift must be by integer amount") self._A = self._binop(self, other, lambda x, y: x << y)._A return self def __rshift__(self, other) -> "Image": """ Overloaded ``>>`` operator :return: element-wise binary-right-shift of images :rtype: :class:`Image` Right shift pixel values in an Image. Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image([[1, 2], [3, 4]]) >>> z = img >> 2 >>> z.array """ if not isinstance(other, int): raise ValueError("left shift must be by integer amount") return self._binop(self, other, lambda x, y: x >> y) def __irshift__(self, other) -> "Image": """ Overloaded in-place ``>>=`` operator :return: self, modified in place :rtype: :class:`Image` Right-shift pixel values of an Image in place. :seealso: :meth:`__rshift__` """ if not isinstance(other, int): raise ValueError("right shift must be by integer amount") self._A = self._binop(self, other, lambda x, y: x >> y)._A return self def __mod__(self, other) -> "Image": """ Overloaded ``%`` operator :return: image with stacked planes :rtype: :class:`Image` ``img1 % img2`` results in an image with the planes of ``img1`` followed by the planes of ``img2``. The two images must have the same number of rows and columns and data type. The color order of the resulting image is the color order of ``img1`` followed by the color order of ``img2``. The operation also supports stacking a scalar as a plane onto an image, in which case the resulting image has one more plane than the original image and the new plane is filled with the scalar value. Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image([[1, 2], [3, 4]]) >>> z = img % Image([[5, 6], [7, 8]]) >>> print(z.nplanes) >>> z = img % 0 >>> print(z.nplanes) :seealso: :meth:`Pstack` """ if smb.isscalar(other): other = Image.Constant(self.size, value=other, dtype=str(self._A.dtype)) return self.Pstack((self, other)) def __imod__(self, other) -> "Image": """ Overloaded in-place ``%=`` operator :return: self, modified in place :rtype: :class:`Image` Stack the planes of another image (or a scalar plane) onto this Image in place. :seealso: :meth:`__mod__` """ if smb.isscalar(other): other = Image.Constant(self.size, value=other, dtype=str(self._A.dtype)) self._A = self.Pstack((self, other))._A return self # relational def __eq__(self, other) -> "Image": """ Overloaded ``==`` operator :return: element-wise comparison of pixels :rtype: bool :class:`Image` Compute the equality between an Image and another image or a scalar. Supports: * image ``==`` image * scalar ``==`` image * image ``==`` scalar Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image([[1, 2], [3, 4]]) >>> (img == 2).print() >>> (img == Image([[0, 2], [3, 4]])).print() :seealso: :meth:`true` :meth:`false` """ return self._binop(self, other, lambda x, y: x == y) def __ne__(self, other) -> "Image": """ Overloaded ``!=`` operator :return: element-wise comparison of pixels :rtype: bool :class:`Image` Compute the inequality between an Image and another image or a scalar. Supports: * image ``!=`` image * scalar ``!=`` image * image ``!=`` scalar Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image([[1, 2], [3, 4]]) >>> (img != 2).print() >>> (img != Image([[0, 2], [3, 4]])).print() :seealso: :meth:`true` :meth:`false` """ return self._binop(self, other, lambda x, y: x != y) def __gt__(self, other) -> "Image": """ Overloaded ``>`` operator :return: element-wise comparison of pixels :rtype: bool :class:`Image` Compute the inequality between an Image and another image or a scalar. Supports: * image ``>`` image * scalar ``>`` image * image ``>`` scalar Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image([[1, 2], [3, 4]]) >>> (img > 2).print() >>> (img > Image([[0, 2], [3, 4]])).print() :seealso: :meth:`true` :meth:`false` """ return self._binop(self, other, lambda x, y: x > y) def __ge__(self, other) -> "Image": """ Overloaded ``>=`` operator :return: element-wise comparison of pixels :rtype: bool :class:`Image` Compute the inequality between an Image and another image or a scalar. Supports: * image ``>=`` image * scalar ``>=`` image * image ``>=`` scalar Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image([[1, 2], [3, 4]]) >>> (img >= 2).print() >>> (img >= Image([[0, 2], [3, 4]])).print() :seealso: :meth:`true` :meth:`false` """ return self._binop(self, other, lambda x, y: x >= y) def __lt__(self, other) -> "Image": """ Overloaded ``<`` operator :return: element-wise comparison of images :rtype: bool :class:`Image` Compute the inequality between an Image and another image or a scalar. Supports: * image ``<`` image * scalar ``<`` image * image ``<`` scalar Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image([[1, 2], [3, 4]]) >>> (img < 2).print() >>> (img < Image([[0, 2], [3, 4]])).print() :seealso: :meth:`true` :meth:`false` """ return self._binop(self, other, lambda x, y: x < y) def __le__(self, other) -> "Image": """ Overloaded ``<=`` operator :return: element-wise comparison of images :rtype: bool :class:`Image` Compute the inequality between an Image and another image or a scalar. Supports: * image ``<=`` image * scalar ``<=`` image * image ``<=`` scalar Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image([[1, 2], [3, 4]]) >>> (img <= 2).print() >>> (img <= Image([[0, 2], [3, 4]])).print() :seealso: :meth:`true` :meth:`false` """ return self._binop(self, other, lambda x, y: x <= y) def __invert__(self) -> "Image": """ Overloaded ``~`` operator :return: element-wise inversion of logical values :rtype: boo, :class:`Image` Returns logical not operation where image values are interpretted as: * floating image: True is 1.0, False is 0.0 * integer image: True is maximum value, False is 0 True is 1 and False is 0. Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image([[True, False], [False, True]]) >>> z = ~img >>> z.print() """ return self._unop(self, lambda x: ~x) @staticmethod def _binop(left, right, op, logical=False) -> "Image": if isinstance(right, left.__class__): # both images if left.nplanes == right.nplanes: return left.__class__(op(left._A, right._A), colororder=left.colororder) elif left.nplanes > 1 and right.nplanes == 1: # left image is multiplane, right is singleton out = [] for i in range(left.nplanes): out.append(op(left._A[:, :, i], right._A)) return left.__class__(np.stack(out, axis=2), colororder=left.colororder) elif left.nplanes == 1 and right.nplanes > 1: # right image is multiplane, left is singleton out = [] for i in range(right.nplanes): out.append(op(left._A, right._A[:, :, i])) return right.__class__( np.stack(out, axis=2), colororder=right.colororder ) else: raise ValueError("planes mismatch") else: # right is a scalar or numpy array return left.__class__(op(left._A, right), colororder=left.colororder) @staticmethod def _logicalop(left, right, op) -> "Image": true = left.cast(left.true) false = left.cast(left.false) if isinstance(right, left.__class__): # image OP image out = np.where(op(left._A, right._A), true, false) else: out = np.where(op(left._A, right), true, false) return left.__class__(out, colororder=left.colororder) @staticmethod def _unop(left, op) -> "Image": return left.__class__(op(left._A), colororder=left.colororder) # ---------------------------- functions ---------------------------- # def abs(self) -> "Image": """ Absolute value of image :return: element-wise absolute value of image :rtype: :class:`Image` Return element-wise absolute value of pixel values. Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image([[-1, 2], [3, -4]], dtype='int8') >>> img.abs().print() """ return self._unop(self, np.abs) def sqrt(self) -> "Image": """ Square root of image :return: element-wise square root of image :rtype: :class:`Image` Return element-wise square root of pixel values. Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image([[1, 2], [3, 4]]) >>> img.sqrt().print() """ return self._unop(self, np.sqrt) # ---------------------------- graphics ---------------------------- # def draw_line( self, start: tuple[int, int] | np.ndarray, end: tuple[int, int] | np.ndarray, **kwargs: Any, ) -> None: """ Draw line into image :param start: start coordinate (u,v) :type start: array_like(2) :param end: end coordinate (u,v) :type end: array_like(2) :param kwargs: parameters passed to :func:`~machinevisiontoolbox.base.graphics.draw_line` Example, "burn" a line into the Mona Lisa image:: >>> from machinevisiontoolbox import Image >>> img = Image.Read("monalisa.png") >>> img.draw_line((20,30), (500,600), thickness=5, color="orange") >>> img.disp() .. plot:: from machinevisiontoolbox import Image img = Image.Read("monalisa.png") img.draw_line((20,30), (500,600), thickness=5, color="orange") img.disp() .. note:: If ``image`` has multiple planes then ``color`` should have the same number of elements as the image has planes. If it is a scalar that value is used for each color plane. For a color image ``color`` can be a string color name. :seealso: :func:`~machinevisiontoolbox.base.graphics.draw_line` """ draw_line(self._A, start, end, **kwargs) def draw_circle( self, centre: tuple[int, int] | np.ndarray, radius: int, center: tuple[int, int] | np.ndarray | None = None, **kwargs: Any, ) -> None: """ Draw circle into image :param centre: centre coordinate (u,v) :type centre: array_like(2) :param radius: circle radius in pixels :type radius: int :param kwargs: parameters passed to :func:`~machinevisiontoolbox.base.graphics.draw_circle` Example, "burn" some circles (confetti) into the Mona Lisa:: >>> from machinevisiontoolbox import Image >>> import matplotlib as mpl >>> img = Image.Read("monalisa.png") >>> colors = mpl.color_sequences["petroff10"] >>> for color in colors: ... u = np.random.randint(20, img.umax-20) v = np.random.randint(20, img.vmax-20) r = np.random.randint(10, 50) img.draw_circle((u, v), r, thickness=-1, color=[255*c for c in color]) >>> img.disp(img) .. plot:: from machinevisiontoolbox import Image import matplotlib as mpl img = Image.Read("monalisa.png") for color in mpl.color_sequences["petroff10"]: u = np.random.randint(20, img.umax-20) v = np.random.randint(20, img.vmax-20) r = np.random.randint(10, 50) img.draw_circle((u, v), r, thickness=-1, color=[255*c for c in color]) img.disp(img) .. note:: If ``image`` has multiple planes then ``color`` should have the same number of elements as the image has planes. If it is a scalar that value is used for each color plane. For a color image ``color`` can be a string color name. :seealso: :func:`~machinevisiontoolbox.base.graphics.draw_circle` """ if center is not None and centre is None: centre = center draw_circle(self._A, centre, radius, **kwargs) def draw_box(self, **kwargs: Any) -> None: """ Draw box into image :param kwargs: parameters passed to :func:`~machinevisiontoolbox.base.graphics.draw_box` There are myriad ways to specify the corners of the box. Example, draw boxes over the eyes of the Mona Lisa:: >>> from machinevisiontoolbox import Image >>> img = Image.Read("monalisa.png") >>> img.draw_box(lb=(245,170), rt=(290, 210), color="yellow", thickness=5) >>> img.draw_box(lb=(315, 175), rt=(370,205), color="blue", thickness=-1) >>> img.disp() .. plot:: from machinevisiontoolbox import Image img = Image.Read("monalisa.png") img.draw_box(lt=(245,210), rb=(290, 170), color="yellow", thickness=5) img.draw_box(lt=(315, 205), rb=(370,175), color="blue", thickness=-1) img.disp() .. note:: If ``image`` has multiple planes then ``color`` should have the same number of elements as the image has planes. If it is a scalar that value is used for each color plane. For a color image ``color`` can be a string color name. :seealso: :func:`~machinevisiontoolbox.base.graphics.draw_box` """ draw_box(self._A, **kwargs) def draw_labelbox(self, text: str, **kwargs: Any) -> None: """ Draw label box into image :param text: text for the box label :type text: str :param kwargs: parameters passed to :func:`~machinevisiontoolbox.base.graphics.draw_labelbox` to specify the box position, color, etc. The box position is specified by the parameters accepted by :func:`~machinevisiontoolbox.base.graphics.draw_box` Example:: >>> from machinevisiontoolbox import Image >>> img = Image.Read("monalisa.png") >>> img.draw_labelbox("Face", lb=(243,111), rt=(394,329), color="yellow", fontheight=20) >>> img.disp() .. plot:: from machinevisiontoolbox import Image img = Image.Read("monalisa.png") img.draw_labelbox("Face", lb=(243,111), rt=(394,329), color="yellow", fontheight=20) img.disp() .. note:: If ``image`` has multiple planes then ``color`` should have the same number of elements as the image has planes. If it is a scalar that value is used for each color plane. For a color image ``color`` can be a string color name. :seealso: :func:`~machinevisiontoolbox.base.graphics.draw_labelbox` """ draw_labelbox(self._A, text, **kwargs) def draw_text( self, pos: tuple[int, int] | np.ndarray, text: str, **kwargs: Any ) -> None: """ Draw text into image :param pos: text position (u,v) :type pos: array_like(2) :param text: text to draw :type text: str :param kwargs: parameters passed to :func:`~machinevisiontoolbox.base.graphics.draw_text` Example:: >>> from machinevisiontoolbox import Image >>> img = Image.Read("monalisa.png") >>> img.draw_text((340,290), "Smile!", fontheight=40, color="yellow") >>> img.disp() .. plot:: from machinevisiontoolbox import Image img = Image.Read("monalisa.png") img.draw_text((340,290), "Smile!", fontheight=40, color="yellow") img.disp() .. note:: If ``image`` has multiple planes then ``color`` should have the same number of elements as the image has planes. If it is a scalar that value is used for each color plane. For a color image ``color`` can be a string color name. :seealso: :func:`~machinevisiontoolbox.base.graphics.draw_text` """ draw_text(self._A, pos, text, **kwargs) def draw_point( self, pos: tuple[int, int] | np.ndarray, marker: str = "+", text: str | None = None, **kwargs: Any, ) -> None: """ Draw a marker in image :param pos: position of marker :type pos: array_like(2), ndarray(2,n), list of 2-tuples :param marker: marker character, defaults to "+" :type marker: str, optional :param text: text label, defaults to None :type text: str, optional :param kwargs: parameters passed to :func:`~machinevisiontoolbox.base.graphics.draw_point` Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image.Read("monalisa.png") >>> img.draw_point((270,194), "+", "eye", fontsize=1, color="yellow") >>> img.draw_point((293,246), "*", "nose", fontsize=1, color="blue") >>> img.disp() .. plot:: from machinevisiontoolbox import Image img = Image.Read("monalisa.png") img.draw_point((270,194), "+", "eye", fontsize=1, color="yellow") img.draw_point((293,246), "*", "nose", fontsize=1, color="blue") img.disp() .. note:: If ``image`` has multiple planes then ``color`` should have the same number of elements as the image has planes. If it is a scalar that value is used for each color plane. For a color image ``color`` can be a string color name. :seealso: :func:`~machinevisiontoolbox.base.graphics.draw_text` """ draw_point(self._A, pos, marker, text, **kwargs) @classmethod def Pstack(cls, images, colororder=None): """ Concatenation of image planes :param images: images to concatenate plane-wise :type images: iterable of :class:`Image` :param colororder: color order for the result, defaults to None :type colororder: str, optional :raises ValueError: all images must have the same dtype :raises ValueError: all images must have the same size :return: plane-stacked image :rtype: :class:`Image` Create a new image by stacking the planes of the input images. All images must have the same width, height and dtype. The resulting image has a number of planes equal to the sum of planes in all input images. If ``colororder`` is not specified and all images have color orders defined, the result's color order is constructed by concatenating the color orders of the input images. Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> r = Image.Random(size=(100, 120), colororder='R') >>> g = Image.Random(size=(100, 120), colororder='G') >>> b = Image.Random(size=(100, 120), colororder='B') >>> rgb = Image.Pstack((r, g, b)) >>> rgb.nplanes >>> rgb.colororder_str :seealso: :meth:`Vstack` :meth:`Hstack` AUTO_EDIT """ nplanes = images[0].nplanes for image in images[1:]: if image.dtype != images[0].dtype: raise ValueError("all planes must have the same dtype") if image.size != images[0].size: raise ValueError("all planes must have the same width x height") nplanes += image.nplanes if colororder is None: if all([im.colororder is not None for im in images]): # attempt to create color order from the images colororder = images[0].colororder ip = len(colororder) for image in images[1:]: colororder |= Image.colororder2dict(image.colororder, start=ip) else: colororder = None else: if len(Image.colororder2dict(colororder)) != nplanes: raise ValueError("colororder does not match number of planes") return Image( np.concatenate([np.atleast_3d(im._A) for im in images], axis=2), colororder=colororder, ) def fliplr(self) -> "Image": """ Flip image left-right :return: horizontally flipped image :rtype: :class:`Image` Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image([[1, 2, 3], [4, 5, 6]]) >>> img.fliplr().A :seealso: :meth:`flipud` """ return self.__class__(np.fliplr(self._A), colororder=self.colororder) def flipud(self) -> "Image": """ Flip image up-down :return: vertically flipped image :rtype: :class:`Image` Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image([[1, 2, 3], [4, 5, 6]]) >>> img.flipud().A :seealso: :meth:`fliplr` """ return self.__class__(np.flipud(self._A), colororder=self.colororder) def fixbad(self, nan=0.0, posinf=None, neginf=None): """ Fix bad values in image :param nan: value to replace NaN values, defaults to 0.0 :type nan: scalar or array-like(n), optional :param posinf: value to replace positive infinity values, defaults to maximum value of image dtype :type posinf: scalar or array-like(n), optional :param neginf: value to replace negative infinity values, defaults to negative maximum value of image dtype :type neginf: scalar or array-like(n), optional :return: image with bad values fixed :rtype: :class:`~machinevisiontoolbox.ImageCore.Image` instance Return an image where any pixel that contains a NaN or Inf values has been replaced by the specified replacement values. The replacement value can be a scalar, in which case all bad values are replaced by the same value, or an array-like of length n, where n is the number of channels in the image, in which case each channel is replaced by the corresponding value. By default NaN values are replaced by 0, positive infinity values are replaced by the maximum value of the image dtype, and negative infinity values are replaced by the negative maximum value of the image dtype. Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> from numpy import nan, inf >>> img = Image([[1, 2, 3], [4, nan, 6], [7, 8, inf]]) >>> img.fixbad(posinf=99).print() """ out = self._A.copy() if posinf is None: posinf = np.finfo(out.dtype).max if neginf is None: neginf = -np.finfo(out.dtype).max mask = np.isnan(out) if out.ndim == 2: out[mask] = nan else: mask = mask.any(axis=2) out[mask, :] = nan mask = np.isposinf(out) if out.ndim == 2: out[mask] = posinf else: mask = mask.any(axis=2) out[mask, :] = posinf mask = np.isneginf(out) if out.ndim == 2: out[mask] = neginf else: mask = mask.any(axis=2) out[mask, :] = neginf return self.__class__(out, colororder=self.colororder)
if __name__ == "__main__": from pathlib import Path import pytest pytest.main( [str(Path(__file__).parent.parent.parent / "tests" / "test_core.py"), "-v"] )