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
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)