def draw_point(image,
               pos,
               marker='+',
               text=None,
               color=None,
               font=cv.FONT_HERSHEY_SIMPLEX,
               fontsize=0.3,
               fontthickness=2):
    """
    Draw marker in image using OpenCV

    :param image: image to draw into
    :type image: ndarray(h,w) or ndarray(h,w,nc)
    :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 color: text color, defaults to None
    :type color: str or array_like(3), optional
    :param font: OpenCV font, defaults to cv.FONT_HERSHEY_SIMPLEX
    :type font: str, optional
    :param fontsize: OpenCV font scale, defaults to 0.3
    :type fontsize: float, optional
    :param fontthickness: font thickness in pixels, defaults to 2
    :type fontthickness: int, optional

    The text label is placed to the right of the marker, and vertically centred.
    The color of the marker can be different to the color of the text, the
    marker color is specified by a single letter in the marker string.

    Multiple points can be marked if ``pos`` is a 2xn array or a list of 
    coordinate pairs.  If a label is provided every point will have the same
    label. However, the text is processed with ``format`` and is provided with
    a single argument, the point index (starting at zero).
    """

    if not isinstance(color, int) and len(image.shape) == 2:
        raise TypeError("can't draw color into a greyscale image")

    if isinstance(pos, np.ndarray) and pos.shape[0] == 2:
        x = pos[0, :]
        y = pos[1, :]
    elif isinstance(pos, (tuple, list)):
        if base.islistof(pos, (tuple, list)):
            x = [z[0] for z in pos]
            y = [z[1] for z in pos]
        else:
            x = pos[0]
            y = pos[1]

    if isinstance(color, str):
        color = color_bgr(color)

    for i, xy in enumerate(zip(x, y)):
        s = marker
        if text:
            s += ' ' + text.format(i)
        cv.putText(image, s, xy, font, fontsize, color, fontthickness)
def iread(filename, *args, verbose=True, **kwargs):
    """
    Read image from file

    :param file: file name or URL
    :type file: string
    :param kwargs: key word arguments 
    :return: image and filename
    :rtype: tuple or list of tuples, tuple is (image, filename) where image is
        a 2D, 3D or 4D NumPy array

    - ``image, path = iread(file)`` reads the specified image file and returns the
      image as a NumPy matrix, as well as the absolute path name.  The
      image can by greyscale or color in any of the wide range of formats
      supported by the OpenCV ``imread`` function.

    - ``image, url = iread(url)`` as above but reads the image from the given
      URL.

    If ``file`` is a list or contains a wildcard, the result will be a list of
    ``(image, path)`` tuples.  They will be sorted by path.

    - ``iread(filename, dtype="uint8", grey=None, greymethod=601, reduce=1,
      gamma=None, roi=None)``


    Extra options include:

        - 'uint8'         return an image with 8-bit unsigned integer pixels in
          the range 0 to 255

        - 'double'        return an image with double precision floating point
          pixels in the range 0 to 1.
        - 'grey'          convert image to greyscale, if it's color, using
          ITU rec 601
        - 'grey_709'      convert image to greyscale, if it's color, using
          ITU rec 709
        - 'gamma',G       apply this gamma correction, either numeric or 'sRGB'
        - 'reduce',R      decimate image by R in both dimensions
        - 'roi',R         apply the region of interest R to each image,
          where R=[umin umax; vmin vmax].

    :param dtype: a NumPy dtype string such as "uint8", "int16", "float32"
    :type dtype: str
    :param grey: convert to grey scale
    :type grey: bool
    :param greymethod: ITU recommendation, either 601 [default] or 709
    :type greymethod: int
    :param reduce: subsample image by this amount in u- and v-dimensions
    :type reduce: int
    :param gamma: gamma decoding, either the exponent of "sRGB"
    :type gamma: float or str
    :param roi: extract region of interest [umin, umax, vmin vmax]
    :type roi: array_like(4)

    Example:

    .. runblock:: pycon

        >>> from machinevisiontoolbox import iread, idisp
        >>> im, file = iread('flowers1.png')
        >>> idisp(im)

    .. note::

        - A greyscale image is returned as an HxW matrix
        - A color image is returned as an HxWx3 matrix
        - A greyscale image sequence is returned as an HxWxN matrix where N is
          the sequence length
        - A color image sequence is returned as an HxWx3xN matrix where N is
          the sequence length

    :references:

        - Robotics, Vision & Control, Section 10.1, P. Corke, Springer 2011.
    """

    if isinstance(filename, str) and (filename.startswith("http://") or filename.startswith("https://")):
        # reading from a URL

        resp = urllib.request.urlopen(filename)
        array = np.asarray(bytearray(resp.read()), dtype="uint8")
        image = cv.imdecode(array, -1)
        image = convert(image, **kwargs)
        return (image, filename)

    elif isinstance(filename, (str, Path)):
        # reading from a file

        path = Path(filename).expanduser()

        if any([c in "?*" for c in str(path)]):
            # contains wildcard characters, glob it
            # recurse and return a list
            # https://stackoverflow.com/questions/51108256/how-to-take-a-pathname-string-with-wildcards-and-resolve-the-glob-with-pathlib
    
            parts = path.parts[1:] if path.is_absolute() else path.parts
            p = Path(path.root).glob(str(Path("").joinpath(*parts)))
            pathlist = list(p)

            if len(pathlist) == 0 and not path.is_absolute():
                # look in the toolbox image folder
                path = Path(__file__).parent.parent / "images" / path
                parts = path.parts[1:] if path.is_absolute() else path.parts
                p = Path(path.root).glob(str(Path("").joinpath(*parts)))
                pathlist = list(p)
            
            if len(pathlist) == 0:
                raise ValueError("can't expand wildcard")

            imlist = []
            pathlist.sort()
            for p in pathlist:
                imlist.append(iread(p, **kwargs))
            return imlist

        else:
            # read single file

            if not path.exists():
                if path.is_absolute():
                    raise ValueError(f"file {filename} does not exist")
                # file doesn't exist
                # see if it matches the supplied images
                path = Path(__file__).parent.parent / "images" / path

                if not path.exists():
                    raise ValueError(f"file {filename} does not exist, and not found in supplied images")

            # read the image
            # TODO not sure the following will work on Windows
            image = cv.imread(path.as_posix())  # default read-in as BGR
            image = convert(image, **kwargs)
            if image is None:
                # TODO check ValueError
                raise ValueError(f"Could not read {filename}")

            return (image, str(path))

    elif islistof(filename, (str, Path)):
        # list of filenames or URLs
        # assume none of these are wildcards, TODO should check
        out = []
        for file in filename:
            out.append(iread(file, *kwargs))
        return out
    else:
        raise ValueError(filename, 'invalid filename')
def plot_point(pos, marker="bs", text=None, ax=None, textargs=None, **kwargs):
    """
    Plot a point using matplotlib

    :param pos: position of marker
    :type pos: array_like(2), ndarray(2,n), list of 2-tuples
    :param marker: matplotlub marker style, defaults to 'bs'
    :type marker: str or list of str, optional
    :param text: text label, defaults to None
    :type text: str, optional
    :param ax: axes to plot in, defaults to ``gca()``
    :type ax: Axis, optional
    :return: the matplotlib object
    :rtype: list of Text and Line2D instances

    Plot one or more points, with optional text label.

    - The color of the marker can be different to the color of the text, the
      marker color is specified by a single letter in the marker string.

    - A point can have multiple markers, given as a list, which will be
      overlaid, for instance ``["rx", "ro"]`` will give a тиВ symbol.

    - The optional text label is placed to the right of the marker, and
      vertically aligned.

    - Multiple points can be marked if ``pos`` is a 2xn array or a list of
      coordinate pairs.  If a label is provided every point will have the same
      label.

    Examples:

    - ``plot_point((1,2))`` plot default marker at coordinate (1,2)
    - ``plot_point((1,2), 'r*')`` plot red star at coordinate (1,2)
    - ``plot_point((1,2), 'r*', 'foo')`` plot red star at coordinate (1,2) and
      label it as 'foo'
    - ``plot_point(p, 'r*')`` plot red star at points defined by columns of
      ``p``.
    - ``plot_point(p, 'r*', 'foo')`` plot red star at points defined by columns
      of ``p`` and label them all as 'foo'
    - ``plot_point(p, 'r*', '{0}')`` plot red star at points defined by columns
      of ``p`` and label them sequentially from 0
    - ``plot_point(p, 'r*', ('{1:.1f}', z))`` plot red star at points defined by
      columns of ``p`` and label them all with successive elements of ``z``.
    """
    if isinstance(pos, np.ndarray):
        if pos.ndim == 1:
            x = pos[0]
            y = pos[1]
        elif pos.ndim == 2 and pos.shape[0] == 2:
            x = pos[0, :]
            y = pos[1, :]
    elif isinstance(pos, (tuple, list)):
        # [x, y]
        # [(x,y), (x,y), ...]
        # [xlist, ylist]
        # [xarray, yarray]
        if base.islistof(pos, (tuple, list)):
            x = [z[0] for z in pos]
            y = [z[1] for z in pos]
        elif base.islistof(pos, np.ndarray):
            x = pos[0]
            y = pos[1]
        else:
            x = pos[0]
            y = pos[1]

    textopts = {
        "fontsize": 12,
        "horizontalalignment": "left",
        "verticalalignment": "center",
    }
    if textargs is not None:
        textopts = {**textopts, **textargs}

    if ax is None:
        ax = plt.gca()

    handles = []
    if isinstance(marker, (list, tuple)):
        for m in marker:
            handles.append(plt.plot(x, y, m, **kwargs))
    else:
        handles.append(plt.plot(x, y, marker, **kwargs))
    if text is not None:
        if isinstance(text, str):
            # simple string, but might have format chars
            for i, xy in enumerate(zip(x, y)):
                handles.append(plt.text(xy[0], xy[1], " " + text.format(i), **textopts))
        elif isinstance(text, (tuple, list)):
            for i, xy in enumerate(zip(x, y)):
                handles.append(
                    plt.text(
                        xy[0],
                        xy[1],
                        " " + text[0].format(i, *[d[i] for d in text[1:]]),
                        **textopts
                    )
                )
    return handles
Exemple #4
0
    def __init__(self,
                 arg=None,
                 colororder='BGR',
                 iscolor=None,
                 checksize=True,
                 checktype=True,
                 **kwargs):
        """
        An image class for MVT

            :param arg: image
            :type arg: Image, list of Images, numpy array, list of numpy arrays,
            filename string, list of filename strings
            :param colororder: order of color channels ('BGR' or 'RGB')
            :type colororder: string
            :param checksize: if reading a sequence, check all are the same size
            :type checksize: bool
            :param iscolor: True if input images are color
            :type iscolor: bool

        """

        if arg is None:
            # empty image
            self._width = None
            self._height = None
            self._numimagechannels = None
            self._numimages = None
            self._dtype = None
            self._colororder = None
            self._imlist = None
            self._iscolor = None
            self._filenamelist = None
            # self._colorspace = None  # TODO consider for xyz/Lab etc?
            return

        elif isinstance(arg, (str, Path)) or islistof(arg, str):
            # string, name of an image file to read in
            images = iread(arg, **kwargs)

            # result is a tuple(image, filename) or a list of tuples

            # TODO once iread, then filter through imlist and arrange into
            # proper numimages and numchannels, based on user inputs, though
            # default to single list

            # NOTE stylistic change to line below
            # if (iscolor is False) and (imlist[0].ndim == 3):

            if isinstance(images, list):
                # image wildcard read is a tuple of lists, make a sequence
                self._imlist, self._filenamelist = zip(*images)

            elif isinstance(images, tuple):
                # singleton image, make it a list
                shape = images[0].shape
                if len(shape) == 2:
                    # 2D image - clearly greyscale
                    self._iscolor = False
                    self._numimages = 1
                elif len(shape) == 3:
                    # 3D image - color or greyscale sequence
                    if shape[2] == 3 or iscolor:
                        # color image
                        self._iscolor = True
                        self._numimages = 1
                    else:
                        self._iscolor = False
                        self._numimages = shape[2]

                elif len(shape) == 4 and shape[2] == 3:
                    # 4D image - color sequence
                    self._iscolor = True
                    self._numimages = shape[3]
                else:
                    raise ValueError('bad array dimensions')

                self._imlist = [images[0]]
                self._filenamelist = [images[1]]

        elif isinstance(arg, Image):
            # Image instance
            self._imlist = arg._imlist
            self._filenamelist = arg._filenamelist

        elif islistof(arg, Image):
            # list of Image instances
            # assuming Images are all of the same size

            shape = [im.shape for im in arg]
            if any(sh != shape[0] for sh in shape):
                raise ValueError(
                    arg, 'input list of Image objects must \
                    be of the same shape')

            # TODO replace with list comprehension or itertools/chain method
            self._imlist = []
            self._filenamelist = []
            for imobj in arg:
                for im in imobj:
                    self._imlist.append(im.image)
                    self._filenamelist.append(im.filename)

        elif islistof(arg, np.ndarray):
            # list of images, with each item being a numpy array
            # imlist = TODO deal with iscolor=False case

            if (iscolor is False) and (arg[0].ndim == 3):
                imlist = []
                for i in range(len(arg)):
                    for j in range(arg[i].shape[2]):
                        imlist.append(arg[i][0:, 0:, j])
                self._imlist = imlist
            else:
                self._imlist = arg

            self._filenamelist = [None] * len(self._imlist)

        elif Image.isimage(arg):
            # is an actual image or sequence of images compounded into
            # single ndarray
            # make this into a list of images
            # if color:
            arg = Image.getimage(arg)
            if arg.ndim == 4:
                # assume (W,H,3,N)
                self._imlist = [
                    Image.getimage(arg[0:, 0:, 0:, i])
                    for i in range(arg.shape[3])
                ]
            elif arg.ndim == 3:
                # could be single (W,H,3) -> 1 colour image
                # or (W,H,N) -> N grayscale images
                if not arg.shape[2] == 3:
                    self._imlist = [
                        Image.getimage(arg[0:, 0:, i])
                        for i in range(arg.shape[2])
                    ]
                elif (arg.shape[2] == 3) and iscolor:
                    # manually specified iscolor is True
                    # single colour image
                    self._imlist = [Image.getimage(arg)]
                elif (arg.shape[2] == 3) and (iscolor is None):
                    # by default, we will assume that a (W,H,3) with
                    # unspecified iscolor is a color image, as the
                    # 3-sequence greyscale case is much less common
                    self._imlist = [Image.getimage(arg)]
                else:
                    self._imlist = [
                        Image.getimage(arg[0:, 0:, i])
                        for i in range(arg.shape[2])
                    ]

            elif arg.ndim == 2:
                # single (W,H)
                self._imlist = [Image.getimage(arg)]

            else:
                raise ValueError(arg, 'unknown rawimage.shape')

            self._filenamelist = [None] * len(self._imlist)

        else:
            raise ValueError('bad argument to Image constructor')

        # check list of images for size consistency

        # VERY IMPORTANT!
        # We assume that the image stack has the same size image for the
        # entire list. TODO maybe in the future, we remove this assumption,
        # which can cause errors if not adhered to,
        # but for now we simply check the shape of each image in the list

        # TODO shape = [img.shape for img in self._imlist[]]
        # if any(shape[i] != list):
        #   raise
        if checksize:
            shapes = [im.shape for im in self._imlist]
            if np.any([shape != shapes[0] for shape in shapes[1:]]):
                raise ValueError(arg, 'inconsistent input image shape')

        self._height = self._imlist[0].shape[0]
        self._width = self._imlist[0].shape[1]

        # ability for user to specify iscolor manually to remove ambiguity
        if iscolor is None:
            # our best guess
            shape = self._imlist[0].shape
            self._iscolor = (len(shape) == 3) and (shape[2] == 3)
        else:
            self._iscolor = iscolor

        self._numimages = len(self._imlist)

        if self._imlist[0].ndim == 3:
            self._numimagechannels = self._imlist[0].shape[2]
        elif self._imlist[0].ndim == 2:
            self._numimagechannels = 1
        else:
            raise ValueError(
                self._numimagechannels, 'unknown number of \
                                image channels')

        # check uniform type
        dtype = [im.dtype for im in self._imlist]
        if checktype:
            if np.any([dtype[i] != dtype[0] for i in range(len(dtype))]):
                raise ValueError(arg, 'inconsistent input image dtype')
        self._dtype = self._imlist[0].dtype

        validcolororders = ('RGB', 'BGR')
        # TODO add more valid colororders
        # assume some default: BGR because we import with mvt with
        # opencv's imread(), which imports as BGR by default
        if colororder in validcolororders:
            self._colororder = colororder
        else:
            raise ValueError(colororder, 'unknown colororder input')
def colorname(arg, colorspace='rgb'):
    """
    Map between color names and RGB values

    :param name: name of a color or name of a 3-element color array
    :type name: string or (numpy, tuple, list)
    :param colorspace: name of colorspace (eg 'rgb' or 'xyz' or 'xy' or 'ab')
    :type colorspace: string
    :return out: output
    :rtype out: named tuple, name of color, numpy array in colorspace

    - ``name`` is a string/list/set of color names, then ``colorname`` returns
      a 3-tuple of rgb tristimulus values.

    Example:

    .. runblock:: pycon

    .. note::

        - Color name may contain a wildcard, eg. "?burnt"
        - Based on the standard X11 color database rgb.txt
        - Tristiumuls values are [0,1]

    :references:

        - Robotics, Vision & Control, Chapter 14.3, P. Corke, Springer 2011.
    """
    # I'd say str in, 3 tuple out, or 3-element array like (numpy, tuple, list)
    #  in and str out

    # load rgbtable (rbg.txt as a dictionary)
    global _rgbdict

    if _rgbdict is None:
        _rgbdict = _loadrgbdict('rgb.txt')

    if isinstance(arg, str) or base.islistof(arg, str):
        # string, or list of strings
        if isinstance(arg, str):
            return _rgbdict[arg]
        else:
            return [_rgbdict[name] for name in arg]

    elif isinstance(arg, (np.ndarray, tuple, list)):
        # map numeric tuple to color name

        n = np.array(arg).flatten()  # convert tuple or list into np array
        table = np.vstack([rgb for rgb in _rgbdict.values()])

        if colorspace in ('rgb', 'xyz', 'lab'):
            if len(n) != 3:
                raise ValueError('color value must have 3 elements')
            if colorspace in ('xyz', 'lab'):
                table = colorconvert(table, 'rgb', colorspace)
            dist = np.linalg.norm(table - n, axis=1)
            k = np.argmin(dist)
            return list(_rgbdict.keys())[k]

        elif colorspace in ('xy', 'ab'):
            if len(n) != 2:
                raise ValueError('color value must have 2 elements')

            if colorspace == 'xy':
                table = colorconvert(table, 'rgb', 'xyz')
                with np.errstate(divide='ignore', invalid='ignore'):
                    table = table[:, 0:2] / np.tile(np.sum(table, axis=1),
                                                    (2, 1)).T
            elif colorspace == 'ab':
                table = colorconvert(table, 'rgb', 'Lab')
                table = table[:, 1:3]

            dist = np.linalg.norm(table - n, axis=1)
            k = np.nanargmin(dist)
            return list(_rgbdict.keys())[k]
        else:
            raise ValueError('unknown colorspace')
    else:
        raise ValueError('arg is of unknown type')
def plot_point(pos, marker='bs', text=None, ax=None, color=None, **kwargs):
    """
    Plot a point using matplotlib

    :param pos: position of marker
    :type pos: array_like(2), ndarray(2,n), list of 2-tuples
    :param marker: matplotlub marker style, defaults to 'bs'
    :type marker: str or list of str, optional
    :param text: text label, defaults to None
    :type text: str, optional
    :param ax: axes to plot in, defaults to ``gca()````
    :type ax: Axis, optional
    :param color: text color, defaults to None
    :type color: str or array_like(3), optional

    The color of the marker can be different to the color of the text,
    the marker color is specified by a single letter in the marker string.

    A point can multiple markers which will be overlaid, for instance ``["rx",
    "ro"]`` will give a тиВ symbol.

    The optional text label is placed to the right of the marker, and vertically
    aligned. 
    
    Multiple points can be marked if ``pos`` is a 2xn array or a list of
    coordinate pairs.  If a label is provided every point will have the same
    label. However, the text is processed with ``format`` and is provided with a
    single argument, the point index (starting at zero).


    """

    if isinstance(pos, np.ndarray) and pos.shape[0] == 2:
        x = pos[0, :]
        y = pos[1, :]
    elif isinstance(pos, (tuple, list)):
        # [x, y]
        # [(x,y), (x,y), ...]
        # [xlist, ylist]
        # [xarray, yarray]
        if base.islistof(pos, (tuple, list)):
            x = [z[0] for z in pos]
            y = [z[1] for z in pos]
        elif base.islistof(pos, np.ndarray):
            x = pos[0]
            y = pos[1]
        else:
            x = pos[0]
            y = pos[1]

    if ax is None:
        ax = plt.gca()
    if isinstance(marker, (list, tuple)):
        for m in marker:
            plt.plot(x, y, m, **kwargs)
    else:
        plt.plot(x, y, marker)
    if text:
        try:
            for i, xy in enumerate(zip(x, y)):
                plt.text(xy[0],
                         xy[1],
                         ' ' + text.format(i),
                         horizontalalignment='left',
                         verticalalignment='center',
                         color=color,
                         **kwargs)
        except:
            plt.text(x,
                     y,
                     ' ' + text,
                     horizontalalignment='left',
                     verticalalignment='center',
                     color=color,
                     **kwargs)