Source code for RRtoolbox.lib.image

# -*- coding: utf-8 -*-
"""
Bundle of methods for handling images. Rather than manipulating specialized
operations in images methods in this module are used for loading, outputting
and format-converting methods, as well as color manipulation.

SUPPORTED FORMATS

see http://docs.opencv.org/2.4/modules/highgui/doc/reading_and_writing_images_and_video.html#imread

+---------------------------+--------------------------------------------------+
| Format                    | Extension                                        |
+===========================+==================================================+
| Windows bitmaps           | \*.bmp, \*.dib (always supported)                |
+---------------------------+--------------------------------------------------+
| JPEG files                | \*.jpeg, \*.jpg, \*.jpe (see the Notes section)  |
+---------------------------+--------------------------------------------------+
| JPEG 2000 files           | \*.jp2 (see the Notes section)                   |
+---------------------------+--------------------------------------------------+
| Portable Network Graphics | \*.png (see the Notes section)                   |
+---------------------------+--------------------------------------------------+
| Portable image format     | \*.pbm, \*.pgm, \*.ppm (always supported)        |
+---------------------------+--------------------------------------------------+
| Sun rasters               | \*.sr, \*.ras (always supported)                 |
+---------------------------+--------------------------------------------------+
| TIFF files                | \*.tiff, \*.tif (see the Notes section)          |
+---------------------------+--------------------------------------------------+
"""
from __future__ import division
from __future__ import print_function
from __future__ import absolute_import

from builtins import zip
from builtins import range
from past.builtins import basestring
from builtins import object
from .directory import getData, mkPath, getPath, increment_if_exits
from .config import FLOAT,INT,MANAGER
import cv2
import os
import numpy as np
from .arrayops.basic import anorm, polygonArea, im2shapeFormat, angle, vectorsAngles, overlay, \
    standarizePoints, splitPoints
from .root import glob
#from pyqtgraph import QtGui #BUG in pydev ImportError: cannot import name QtOpenGL
from .cache import Cache, ResourceManager
from collections import MutableSequence
from .directory import getData, strdifference, changedir, checkFile, getFileHandle, \
    increment_if_exits, mkPath
import matplotlib.axes
import matplotlib.figure
from .plotter import Plotim, limitaxis
from .serverServices import parseString, string_is_socket_address


supported_formats = ("bmp","dib","jpeg","jpg","jpe","jp2","png","pbm","pgm","ppm","sr","ras","tiff","tif")


[docs]def transposeIm(im): if len(im.shape) == 2: return im.transpose(1, 0) else: return im.transpose(1, 0, 2)
#from matplotlib import colors # colors to use green = (0, 255, 0) red = (0, 0, 255) white = (255, 255, 255) orange = (51, 103, 236) black = (0, 0, 0) blue = (255, 0, 0) # dictionary of colors to use colors = { "blue":blue, "green":green, "red":red, "white":white, "orange":orange, "black":black} # look for these as <numpy array>.dtype.names bgra_dtype = np.dtype({'b': (np.uint8, 0), 'g': (np.uint8, 1), 'r': (np.uint8, 2), 'a': (np.uint8, 3)})
[docs]def plt2bgr(image): if isinstance(image, matplotlib.axes.SubplotBase): image = fig2bgr(image.figure) elif isinstance(image, matplotlib.figure.Figure): image = fig2bgr(image) return image
[docs]def plt2bgra(image): if isinstance(image, matplotlib.axes.SubplotBase): image = fig2bgra(image.figure) elif isinstance(image, matplotlib.figure.Figure): image = fig2bgra(image) return image
[docs]def fig2bgr(fig): """ Convert a Matplotlib figure to a RGB image. :param fig: a matplotlib figure :return: RGB image. """ fig.canvas.draw() buf = np.fromstring(fig.canvas.tostring_rgb(), dtype=np.uint8, sep='') # get bgr return buf.reshape(fig.canvas.get_width_height()[::-1] + (3,))
[docs]def np2str(arr): return arr.tostring()
[docs]def str2np(string,shape): buf = np.fromstring(string, dtype=np.uint8, sep='') # get bgr return buf.reshape(shape)
[docs]def fig2bgra(fig): """ Convert a Matplotlib figure to a RGBA image. :param fig: a matplotlib figure :return: RGBA image. """ #http://www.icare.univ-lille1.fr/drupal/node/1141 #http://stackoverflow.com/questions/7821518/matplotlib-save-plot-to-numpy-array # draw the renderer fig.canvas.draw() # Get the RGBA buffer from the figure buf = np.fromstring(fig.canvas.tostring_argb(), dtype=np.uint8 ) # get bgra buf = buf.reshape(fig.canvas.get_width_height()[::-1] + (4,)) # reshape to h,w,c return np.roll(buf,3,axis = 2) # correct channels
[docs]def qi2np(qimage, dtype ='array'): """ Convert QImage to numpy.ndarray. The dtype defaults to uint8 for QImage.Format_Indexed8 or `bgra_dtype` (i.e. a record array) for 32bit color images. You can pass a different dtype to use, or 'array' to get a 3D uint8 array for color images. source from: https://kogs-www.informatik.uni-hamburg.de/~meine/software/vigraqt/qimage2ndarray.py """ from pyqtgraph import QtGui if qimage.isNull(): raise IOError("Image is Null") result_shape = (qimage.height(), qimage.width()) temp_shape = (qimage.height(), qimage.bytesPerLine() * 8 / qimage.depth()) if qimage.format() in (QtGui.QImage.Format_ARGB32_Premultiplied, QtGui.QImage.Format_ARGB32, QtGui.QImage.Format_RGB32): if dtype == 'rec': dtype = bgra_dtype elif dtype == 'array': dtype = np.uint8 result_shape += (4, ) temp_shape += (4, ) elif qimage.format() == QtGui.QImage.Format_Indexed8: dtype = np.uint8 else: raise ValueError("qi2np only supports 32bit and 8bit images") # FIXME: raise error if alignment does not match buf = qimage.bits().asstring(qimage.numBytes()) result = np.frombuffer(buf, dtype).reshape(result_shape) if result_shape != temp_shape: result = result[:,:result_shape[1]] if qimage.format() == QtGui.QImage.Format_RGB32 and dtype == np.uint8: result = result[...,:3] return result
[docs]def np2qi(array): """ Convert numpy array to Qt Image. source from: https://kogs-www.informatik.uni-hamburg.de/~meine/software/vigraqt/qimage2ndarray.py :param array: :return: """ if np.ndim(array) == 2: return gray2qi(array) elif np.ndim(array) == 3: return rgb2qi(array) raise ValueError("can only convert 2D or 3D arrays")
[docs]def gray2qi(gray): """ Convert the 2D numpy array `gray` into a 8-bit QImage with a gray colormap. The first dimension represents the vertical image axis. ATTENTION: This QImage carries an attribute `ndimage` with a reference to the underlying numpy array that holds the data. On Windows, the conversion into a QPixmap does not copy the data, so that you have to take care that the QImage does not get garbage collected (otherwise PyQt will throw away the wrapper, effectively freeing the underlying memory - boom!). source from: https://kogs-www.informatik.uni-hamburg.de/~meine/software/vigraqt/qimage2ndarray.py """ from pyqtgraph import QtGui if len(gray.shape) != 2: raise ValueError("gray2QImage can only convert 2D arrays") gray = np.require(gray, np.uint8, 'C') h, w = gray.shape result = QtGui.QImage(gray.data, w, h, QtGui.QImage.Format_Indexed8) result.ndarray = gray # let object live to avoid garbage collection """ for i in xrange(256): result.setColor(i, QtGui.QColor(i, i, i).rgb())""" return result
[docs]def rgb2qi(rgb): """ Convert the 3D numpy array `rgb` into a 32-bit QImage. `rgb` must have three dimensions with the vertical, horizontal and RGB image axes. ATTENTION: This QImage carries an attribute `ndimage` with a reference to the underlying numpy array that holds the data. On Windows, the conversion into a QPixmap does not copy the data, so that you have to take care that the QImage does not get garbage collected (otherwise PyQt will throw away the wrapper, effectively freeing the underlying memory - boom!). source from: https://kogs-www.informatik.uni-hamburg.de/~meine/software/vigraqt/qimage2ndarray.py """ from pyqtgraph import QtGui if len(rgb.shape) != 3: raise ValueError("rgb2QImage can only convert 3D arrays") if rgb.shape[2] not in (3, 4): raise ValueError("rgb2QImage can expects the last dimension to contain " "exactly three (R,G,B) or four (R,G,B,A) channels") h, w, channels = rgb.shape # Qt expects 32bit BGRA data for color images: bgra = np.empty((h, w, 4), np.uint8, 'C') bgra[:,:,2] = rgb[:,:,2] bgra[:,:,1] = rgb[:,:,1] bgra[:,:,0] = rgb[:,:,0] # dstack, dsplit, stack if rgb.shape[2] == 3: bgra[...,3].fill(255) fmt = QtGui.QImage.Format_RGB32 else: bgra[...,3] = rgb[...,3] fmt = QtGui.QImage.Format_ARGB32 result = QtGui.QImage(bgra.data, w, h, fmt) result.ndarray = bgra # let object live to avoid garbage collection return result
# STABLE FUNCTIONS
[docs]def bgra2bgr(im,bgrcolor = colors["white"]): """ Convert BGR to BGRA image. :param im: image :param bgrcolor: BGR color representing transparency. (information is lost when converting BGRA to BGR) e.g. [200,200,200]. :return: """ # back[chanel] = bgr[chanel]*(bgr[3]/255.0) + back[chanel]*(1-bgr[3]/255.0) temp=im.shape im2 = np.zeros((temp[0],temp[1],3), np.uint8) im2[:,:,:] = bgrcolor for c in range(0,3): #looping over channels im2[:,:,c] = im[:,:,c]*(im[:,:,3]/255.0) + im2[:,:,c]*(1.0-im[:,:,3]/255.0) return im2
[docs]def convertAs(fns, base = None, folder = None, name=None, ext = None, overwrite = False, loader = None, simulate=False): """ Reads a file and save as other file based in a pattern. :param fns: file name or list of file names. It supports glob operations. By default glob operations ignore folders. :param base: path to place images. :param folder: (None) folder to place images in base's path. If True it uses the folder in which image was loaded. If None, not folder is used. :param name: string for formatting new name of image with the {name} tag. Ex: if name is 'new_{name}' and image is called 'img001' then the formatted new image's name is 'new_img001' :param ext: (None) extension to save all images. If None uses the same extension as the loaded image. :param overwrite: (False) If True and the destine filename for saving already exists then it is replaced, else a new filename is generated with an index "{name}_{index}.{extension}" :param loader: (None) loader for the image file to change image attributes. If None reads the original images untouched. :param simulate: (False) if True, no saving is performed but the status is returned to confirm what images where adequately processed. :return: list of statuses (0 - no error, 1 - image not loaded, 2 - image not saved, 3 - error in processing image) """ if loader is None: loader = loadFunc(1) if isinstance(fns, basestring): filelist = glob(fns) # list else: # is an iterator filelist = [] for f in fns: filelist.extend(glob(f)) if base is None: base = '' # ensures that path from base ends with separator if base: base = os.path.join(base,"") replaceparts = getData(base) # from base get parts # ensures that extension starts with point "." if isinstance(ext,basestring) and not ext.startswith("."): ext = "."+ext # correct extension status = [] for file in filelist: parts = getData(file) # file parts # replace drive if replaceparts[0]: parts[0] = replaceparts[0] # replace root if replaceparts[1]: if folder is True: parts[1] = os.path.join(replaceparts[1], os.path.split(os.path.split(parts[1])[0])[1],"") elif isinstance(folder,basestring): parts[1] = os.path.join(replaceparts[1], folder, "") else: parts[1] = replaceparts[1] # to replace basic name if isinstance(name,basestring): parts[2] = name.format(name=parts[2]) if isinstance(ext,basestring): parts[3] = ext # replace extension newfile = "".join(parts) if not overwrite: newfile = increment_if_exits(newfile) try: im = loader(file) # image not loaded if im is None: status.append((file,1,newfile)) continue # image successfully saved if simulate: status.append((file,0,newfile)) continue else: mkPath("".join(parts[:2])) if cv2.imwrite(newfile,im): status.append((file,0,newfile)) continue # image not saved status.append((file,2,newfile)) except: # an error in the process status.append((file,3,newfile)) return status
[docs]def checkLoaded(obj, fn="", raiseError = False): """ Simple function to determine if variable is valid. :param obj: loaded object :param fn: path of file :param raiseError: if True and obj is None, raise :return: None """ if obj is not None: print(fn, " Loaded...") else: print(fn, " Could not be loaded...") if raiseError: raise
[docs]def loadcv(path, flags=-1, shape=None): """ Simple function to load using opencv. :param path: path to image. :param flag: openCV flags: +-------+------------------------------+--------+ | value | openCV flag | output | +=======+==============================+========+ | (1) | cv2.CV_LOAD_IMAGE_COLOR | BGR | +-------+------------------------------+--------+ | (0) | cv2.CV_LOAD_IMAGE_GRAYSCALE | GRAY | +-------+------------------------------+--------+ | (-1) | cv2.CV_LOAD_IMAGE_UNCHANGED | format | +-------+------------------------------+--------+ :param shape: shape to resize image. :return: loaded image """ im = cv2.imread(path, flags) if shape: im = cv2.resize(im,shape) return im
[docs]def loadsfrom(path, flags=cv2.IMREAD_COLOR): """ Loads Image from URL or file. :param path: filepath or url :param flags: openCV flags: +-------+------------------------------+--------+ | value | openCV flag | output | +=======+==============================+========+ | (1) | cv2.CV_LOAD_IMAGE_COLOR | BGR | +-------+------------------------------+--------+ | (0) | cv2.CV_LOAD_IMAGE_GRAYSCALE | GRAY | +-------+------------------------------+--------+ | (-1) | cv2.CV_LOAD_IMAGE_UNCHANGED | format | +-------+------------------------------+--------+ :return: """ if isinstance(path,basestring): if path.endswith(".npy"):# reads numpy arrays return np.lib.load(path, None) resp = getFileHandle(path) # download the image else: resp = path # assume path is a file-like object ie. cStringIO or file #nparr = np.asarray(bytearray(resp.read()), dtype=dtype) # convert it to a NumPy array nparr = np.fromstring(resp.read(), dtype=np.uint8) image = cv2.imdecode(nparr, flags=flags) # decode using OpenCV format return image
[docs]def interpretImage(toparse, flags): """ Interprets to get image. :param toparse: string to parse or array. It can interpret: * connection to server (i.e. host:port) * path to file (e.g. /path_to_image/image_name.ext) * URL to image (e.g. http://domain.com/path_to_image/image_name.ext) * image as string (i.g. numpy converted to string) * image itself (i.e. numpy array) :param flags: openCV flags: +-------+------------------------------+--------+ | value | openCV flag | output | +=======+==============================+========+ | (1) | cv2.CV_LOAD_IMAGE_COLOR | BGR | +-------+------------------------------+--------+ | (0) | cv2.CV_LOAD_IMAGE_GRAYSCALE | GRAY | +-------+------------------------------+--------+ | (-1) | cv2.CV_LOAD_IMAGE_UNCHANGED | format | +-------+------------------------------+--------+ :return: image or None if not successfull """ # test it is from server if string_is_socket_address(toparse): #process request to server toparse = parseString(toparse,5) # test is object itself if type(toparse).__module__ == np.__name__: # test numpy array if flags == 1: return im2shapeFormat(toparse,(0,0,3)) if flags == 0: return im2shapeFormat(toparse,(0,0)) return toparse # test image in string try: return cv2.imdecode(toparse,flags) except TypeError: # test path to file or URL return loadsfrom(toparse,flags)
[docs]class ImFactory(object): """ image factory for RRToolbox to create scripts to standardize loading images and provide lazy loading (it can load images from disk with the customized options and/or create mapping images to load when needed) to conserve memory. .. warning:: In development. """ _interpolations = {"nearest": 0, "bilinear":1, "bicubic":2, "area":3, "lanczos":4} _convertions = {} def __init__(self, **kwargs): """ :param kwargs: :return: An image can be represented as a matrix of width "W" and height "H" with elements called pixels,each pixel is a representation of a color in one point of a plane (2D dimension). In the case of openCV and many other libraries for image manipulation, the use of numpy arrays as base for image representation is becoming the standard (numpy is a fast and powerful library for array manipulation and one of the main modules for scientific development in python). A numpy matrix with n rows and m columns has a shape (n,m), that in an Image is H,W which in a Cartesian plane would be y,x. if image is W,H = 100,100 then dsize = (W,H) = (300,100) would be the same as fsize = (fx,fy) = (3,1) after the image is loaded in a numpy array the image would have shape (n,m) = (rows,cols) = (H,W) = im.shape """ self.path = None # path to use to load image self.mmap_mode = None # mapping file modes self.mmap_path = None # path to create numpy file; None, do not create mapping file self.w = None self.h = None self.fx = None self.fy = None self.convert = None self.interpolation = None self.throw = True self.update(**kwargs) #TODO not finished
[docs] def update(self, **kwargs): for key,value in kwargs.items(): if hasattr(self,key): setattr(self,key,value) else: raise Exception("Not attribute '{}'".format(key))
[docs] def get_Func(self): """ gets the loading function """ pass
[docs] def get_code(self): """ get the script code """ pass
[docs] def get_errorFunc(self, path=None, throw=None): def errorFunc(im): if throw and im is None: if checkFile(path): if getData(path)[-1] in supported_formats: raise Exception("Not enough permissions to load '{}'".format(path)) else: raise Exception("Failed to load '{}'. Format not supported".format(path)) else: raise Exception("Missing file '{}'".format(path)) return {None:errorFunc}
[docs] def get_loadFunc(self, flag=None): def loadFunc(path): return cv2.imread(path, flag) return {"im":loadFunc}
[docs] def get_resizeFunc(self, dsize= None, dst=None, fx=None, fy=None, interpolation=None): # see http://docs.opencv.org/2.4/modules/imgproc/doc/geometric_transformations.html#resize fx, fy, interpolation= fx or 0, fy or 0, interpolation or 0 def resizeFunc(im): return cv2.resize(im, dsize, dst, fx, fy, interpolation) return {"im":resizeFunc}
[docs] def get_mapFunc(self, flag = None, RGB = None, mpath=None,mode=None,func=None,dsize= None, dst=None, fx=None, fy=None, interpolation=None): def mapFunc(path): if mpath == "*": # save mmap in working directory drive,dirname,(filename,ext) = "","",getData(path)[-2:] elif mpath:# save mmap in mpath drive,dirname,filename,ext = getData(changedir(path,mpath)) else: # save mmap in image path drive,dirname,filename,ext = getData(path) # THIS CREATES ONE HASHED FILE hashed = hash("{}{}{}{}{}{}".format(flag,RGB,dsize,fx,fy,interpolation)) savepath = "{}{}{}{}.{}.npy".format(drive,dirname,filename,ext,hashed) try: # load from map return np.lib.load(savepath,mode) # mapper(savepath,None,mode,True)[0]# except IOError: # create object and map im = func(path) if im is None: # this is regardless of throw flag raise Exception("Failed to load image to map") np.save(savepath,im) return np.lib.load(savepath,mode) # mapper(savepath,im,mode,True)[0]# return {"im":mapFunc}
[docs] def get_transposeFunc(self): def transposeFunc(im): if len(im.shape) == 2: return im.transpose(1,0) else: return im.transpose(1,0,2) # np.ascontiguousarray? http://stackoverflow.com/a/27601130/5288758 return {"im":transposeFunc}
[docs] def get_convertionFunc(self, code): def convertionFunc(im): return cv2.cvtColor(im,code) return {"im":convertionFunc}
[docs] def get_np2qi(self): return {"im":np2qi}
[docs]def loadFunc(flag = 0, dsize= None, dst=None, fx=None, fy=None, interpolation=None, mmode = None, mpath = None, throw = True, keepratio = True): """ Creates a function that loads image array from path, url, server, string or directly from numpy array (supports databases). :param flag: (default: 0) 0 to read as gray, 1 to read as BGR, -1 to read as BGRA, 2 to read as RGB, -2 to read as RGBA. It supports openCV flags: * cv2.CV_LOAD_IMAGE_COLOR * cv2.CV_LOAD_IMAGE_GRAYSCALE * cv2.CV_LOAD_IMAGE_UNCHANGED +-------+-------------------------------+--------+ | value | openCV flag | output | +=======+===============================+========+ | (2) | N/A | RGB | +-------+-------------------------------+--------+ | (1) | cv2.CV_LOAD_IMAGE_COLOR | BGR | +-------+-------------------------------+--------+ | (0) | cv2.CV_LOAD_IMAGE_GRAYSCALE | GRAY | +-------+-------------------------------+--------+ | (-1) | cv2.CV_LOAD_IMAGE_UNCHANGED | BGRA | +-------+-------------------------------+--------+ | (-2) | N/A | RGBA | +-------+-------------------------------+--------+ :param dsize: (None) output image size; if it equals zero, it is computed as: \texttt{dsize = Size(round(fx*src.cols), round(fy*src.rows))} If (integer,None) or (None,integer) it completes the values according to keepratio parameter. :param dst: (None) output image; it has the size dsize (when it is non-zero) or the size computed from src.size(), fx, and fy; the type of dst is uint8. :param fx: scale factor along the horizontal axis :param fy: scale factor along the vertical axis :param interpolation: interpolation method compliant with opencv: +-----+-----------------+-------------------------------------------------------+ |flag | Operation | Description | +=====+=================+=======================================================+ |(0) | INTER_NEAREST | nearest-neighbor interpolation | +-----+-----------------+-------------------------------------------------------+ |(1) | INTER_LINEAR | bilinear interpolation (used by default) | +-----+-----------------+-------------------------------------------------------+ |(2) | INTER_CUBIC | bicubic interpolation over 4x4 pixel neighborhood | +-----+-----------------+-------------------------------------------------------+ |(3) | INTER_AREA | resampling using pixel area relation. | | | | It may be a preferred method for image decimation, | | | | as it gives moire’-free results. But when the image | | | | is zoomed, it is similar to the INTER_NEAREST method. | +-----+-----------------+-------------------------------------------------------+ |(4) | INTER_LANCZOS4 | Lanczos interpolation over 8x8 pixel neighborhood | +-----+-----------------+-------------------------------------------------------+ :param mmode: (None) mmode to create mapped file. if mpath is specified loads image, converts to mapped file and then loads mapping file with mode {None, 'r+', 'r', 'w+', 'c'} (it is slow for big images). If None, loads mapping file to memory (useful to keep image copy for session even if original image is deleted or modified). :param mpath: (None) path to create mapped file. None, do not create mapping file "", uses path directory; "*", uses working directory; else, uses specified directory. :param keepratio: True to keep image ratio when completing data from dsize,fx and fy, False to not keep ratio. .. note:: If mmode is None and mpath is given it creates mmap file but loads from it to memory. It is useful to create physical copy of data to keep loading from (data can be reloaded even if original file is moved or deleted). :return loader function """ # create factory functions def errorFunc(im,path): if im is None: if checkFile(path): if getData(path)[-1][1:] in supported_formats: raise Exception("Not enough permissions to load '{}'".format(path)) else: raise Exception("Failed to load '{}'. Format not supported".format(path)) else: raise Exception("Missing file '{}'".format(path)) RGB = False if abs(flag)==2: # determine if needs to do conversion from BGR to RGB flag = flag//2 # get normal flag RGB = True def loadfunc(path): im = interpretImage(path, flag) # load func if throw: errorFunc(im,path) # if not loaded throw error if flag<0 and im.shape[2]!=4: if RGB: return cv2.cvtColor(im,cv2.COLOR_BGR2RGBA) return cv2.cvtColor(im,cv2.COLOR_BGR2BGRA) if RGB: if flag < 0: return cv2.cvtColor(im,cv2.COLOR_BGRA2RGBA) else: return cv2.cvtColor(im,cv2.COLOR_BGR2RGB) return im if dsize or dst or fx or fy: if fx is None and fy is None: fx=fy=1.0 elif keepratio: if fx is not None and fy is None: fy = fx elif fy is not None and fx is None: fx = fy else: if fx is not None and fy is None: fy = 1 elif fy is not None and fx is None: fx = 1 interpolation= interpolation or 0 if keepratio: def calc_dsize(shape,dsize=None): """ calculates dsize to keep the image's ratio. :param shape: image shape :param dsize: dsize tuple with a None value :return: calculated dsize """ x,y = dsize sy,sx = shape[:2] if x is not None and y is None: dsize = x,int(sy*(x*1.0/sx)) elif y is not None and x is None: dsize = int(sx*(y*1.0/sy)),y else: dsize = sx,sy return dsize else: def calc_dsize(shape,dsize=None): """ calculates dsize without keeping the image's ratio. :param shape: image shape :param dsize: dsize tuple with a None value :return: calculated dsize """ dsize = list(dsize) ndsize = shape[:2][::-1] for i,val in enumerate(dsize): if val is None: dsize[i] = ndsize[i] dsize = tuple(dsize) return dsize if dsize is not None and None in dsize: def resizefunc(path): img = loadfunc(path) return cv2.resize(img,calc_dsize(img.shape,dsize),dst, fx, fy, interpolation) else: def resizefunc(path): return cv2.resize(loadfunc(path),dsize, dst, fx, fy, interpolation) func = resizefunc else: func = loadfunc if mmode or mpath is not None: # if there is a mmode, or mpath is string def mapfunc(path): if mpath == "*": # save mmap in working directory drive,dirname,(filename,ext) = "","",getData(path)[-2:] elif mpath:# save mmap in mpath drive,dirname,filename,ext = getData(changedir(path,mpath)) else: # save mmap in image path drive,dirname,filename,ext = getData(path) """ # THIS CREATES A FOLDER TO MEMOIZE def dummy(path,flag=0,dsize=0,fx=0,fy=0,interpolation=0): return func(path) savepath = "{}{}{}{}".format(drive,dirname,filename,ext) return memoize(savepath,mmap_mode=mmode)(dummy)(path,flag,dsize,fx,fy,interpolation)""" """ # THIS CREATES TWO FILES BUT ONLY MEMOIZE ONE STATE OF IM ARGUMENTS savepath = "{}{}{}{}.{}".format(drive,dirname,filename,ext,"memoized") comps = ("flag","dsize","fx","fy","interpolation") try: # load from map data = mapper(savepath, mmode=mmode)[0] bad = [i for i in comps if data[i] != locals()[i]] if bad: raise IOError else: return data["im"] except IOError: # create object and map im = func(path) if im is None: # this is regardless of throw flag raise Exception("Failed to load image to map") data = dict(im=im,flag=flag,dsize=dsize,fx=fx,fy=fy,interpolation=interpolation) return mapper(savepath,data,mmode)[0]["im"]""" """ # THIS CREATES TWO HASHED FILES hashed = hash("{}{}{}{}{}{}".format(flag,RGB,dsize,fx,fy,interpolation)) savepath = "{}{}{}{}.{}".format(drive,dirname,filename,ext,hashed) try: # load from map im = mapper(savepath, mmode=mmode)[0] return im except IOError: # create object and map im = func(path) if im is None: # this is regardless of throw flag raise Exception("Failed to load image to map") return mapper(savepath,im,mmode)[0]""" # THIS CREATES ONE HASHED FILE hashed = hash("{}{}{}{}{}{}".format(flag,RGB,dsize,fx,fy,interpolation)) savepath = "{}{}{}{}.{}.npy".format(drive,dirname,filename,ext,hashed) try: # load from map return np.lib.load(savepath, mmode) # mapper(savepath,None,mmode,True)[0]# except IOError: # create object and map im = func(path) if im is None: # this is regardless of throw flag raise Exception("Failed to load image to map") np.save(savepath,im) return np.lib.load(savepath, mmode) # mapper(savepath,im,mmode,True)[0]# func = mapfunc # factory function def loader(path): try: return func(path) except Exception as e: if throw: raise return loader # factory function
[docs]class ImLoader(object): """ Class to load image array from path, url, server, string or directly from numpy array (supports databases). :param flag: (default: 0) 0 to read as gray, 1 to read as BGR, -1 to read as BGRA, 2 to read as RGB, -2 to read as RGBA. It supports openCV flags: * cv2.CV_LOAD_IMAGE_COLOR * cv2.CV_LOAD_IMAGE_GRAYSCALE * cv2.CV_LOAD_IMAGE_UNCHANGED +-------+-------------------------------+--------+ | value | openCV flag | output | +=======+===============================+========+ | (2) | N/A | RGB | +-------+-------------------------------+--------+ | (1) | cv2.CV_LOAD_IMAGE_COLOR | BGR | +-------+-------------------------------+--------+ | (0) | cv2.CV_LOAD_IMAGE_GRAYSCALE | GRAY | +-------+-------------------------------+--------+ | (-1) | cv2.CV_LOAD_IMAGE_UNCHANGED | BGRA | +-------+-------------------------------+--------+ | (-2) | N/A | RGBA | +-------+-------------------------------+--------+ :param dsize: (None) output image size; if it equals zero, it is computed as: \texttt{dsize = Size(round(fx*src.cols), round(fy*src.rows))} :param dst: (None) output image; it has the size dsize (when it is non-zero) or the size computed from src.size(), fx, and fy; the type of dst is uint8. :param fx: scale factor along the horizontal axis; when it equals 0, it is computed as \texttt{(double)dsize.width/src.cols} :param fy: scale factor along the vertical axis; when it equals 0, it is computed as \texttt{(double)dsize.height/src.rows} :param interpolation: interpolation method compliant with opencv: +-----+-----------------+-------------------------------------------------------+ |flag | Operation | Description | +=====+=================+=======================================================+ |(0) | INTER_NEAREST | nearest-neighbor interpolation | +-----+-----------------+-------------------------------------------------------+ |(1) | INTER_LINEAR | bilinear interpolation (used by default) | +-----+-----------------+-------------------------------------------------------+ |(2) | INTER_CUBIC | bicubic interpolation over 4x4 pixel neighborhood | +-----+-----------------+-------------------------------------------------------+ |(3) | INTER_AREA | resampling using pixel area relation. | | | | It may be a preferred method for image decimation, | | | | as it gives moire’-free results. But when the image | | | | is zoomed, it is similar to the INTER_NEAREST method. | +-----+-----------------+-------------------------------------------------------+ |(4) | INTER_LANCZOS4 | Lanczos interpolation over 8x8 pixel neighborhood | +-----+-----------------+-------------------------------------------------------+ :param mmode: (None) mmode to create mapped file. if mpath is specified loads image, converts to mapped file and then loads mapping file with mode {None, 'r+', 'r', 'w+', 'c'} (it is slow for big images). If None, loads mapping file to memory (useful to keep image copy for session even if original image is deleted or modified). :param mpath: (None) path to create mapped file. None, do not create mapping file "", uses path directory; "*", uses working directory; else, uses specified directory. .. note:: If mmode is None and mpath is given it creates mmap file but loads from it to memory. It is useful to create physical copy of data to keep loading from (data can be reloaded even if original file is moved or deleted). """ def __init__(self,path, flag = 0, dsize= None, dst=None, fx=None, fy=None, interpolation=None, mmode = None, mpath = None, throw = True): self.path = path self._flag = flag self._dsize = dsize self._dst = dst self._fx = fx self._fy = fy self._interpolation = interpolation self._mmode = mmode self._mpath = mpath self._throw = throw self.load = loadFunc(flag, dsize, dst, fx, fy, interpolation, mmode, mpath, throw) #i = loadFunc.__doc__.find(":param") #__init__.__doc__ = loadFunc.__doc__[:i] + __init__.__doc__ + loadFunc.__doc__[i:] # builds documentation dynamically #del i # job done, delete def __call__(self): return self.load(self.path)
[docs] def getConfiguration(self,**kwargs): """ Get Custom configuration from default configuration. :param kwargs: keys to customize default configuration. If no key is provided default configuration is returned. :return: dictionary of configuration """ temp = {"flag":self._flag,"dsize":self._dsize,"dst":self._dst,"fx":self._fx,"fy":self._fy, "interpolation":self._interpolation,"mmode":self._mmode,"mpath":self._mpath,"throw":self._throw} if kwargs: temp.update(kwargs) return temp
[docs] def temp(self,**kwargs): """ loads from temporal loader created with customized and default parameters. :param kwargs: keys to customize default configuration. :return: loaded image. """ if len(kwargs)==1 and "path" in kwargs: return self.load(kwargs["path"]) path = kwargs.get("path",self.path) # get path return loadFunc(**self.getConfiguration(**kwargs))(path) # build a new loader and load path
[docs]class PathLoader(MutableSequence): """ Class to standardize loading images from list of paths and offer lazy evaluations. :param fns: list of paths :param loader: path loader (loadcv,loadsfrom, or function from loadFunc) Also see:: :func:`loadcv`, :func:`loadsfrom`, :func:`loadFunc` Example:: fns = ["/path to/image 1.ext","/path to/image 2.ext"] imgs = pathLoader(fns) print imgs[0] # loads image in path 0 print imgs[1] # loads image in path 1 """ def __init__(self, fns = None, loader = None): # create factory functions self._fns = fns or [] self._loader = loader or loadFunc() def __call__(self): """ if called returns the list of paths """ return self._fns def __getitem__(self, key): return self._loader(self._fns[key]) def __setitem__(self, key, value): self._fns[key] = value def __delitem__(self, key): del self._fns[key] def __len__(self): return len(self._fns)
[docs] def insert(self, index, value): self._fns.insert(index,value)
[docs]class LoaderDict(ResourceManager): """ Class to standardize loading objects and manage memory efficiently. :param loader: default loader for objects (e.g. load from file or create instance object) :param maxMemory: (None) max memory in specified unit to keep in check optimization (it does not mean that memory never surpasses maxMemory). :param margin: (0.8) margin from maxMemory to trigger optimization. It is in percentage of maxMemory ranging from 0 (0%) to maximum 1 (100%). So optimal memory is inside range: maxMemory*margin < Memory < maxMemory :param unit: (MB) maxMemory unit, it can be GB (Gigabytes), MB (Megabytes), B (bytes) :param all: if True used memory is from all alive references, if False used memory is only from keptAlive references. :param config: (Not Implemented) """ def __init__(self, loader = None, maxMemory = None, margin = 0.8, unit = "MB", all = True, config = None): super(LoaderDict, self).__init__(maxMemory, margin, unit, all) # create factory functions #if config is None: from config import MANAGER as config #self._config = config self._default_loader = loader or loadFunc()
[docs] def register(self, key, path = None, method=None): if method is not None: def func(): return method(func.path) else: def func(): return self._default_loader(func.path) func.path = path super(LoaderDict, self).register(key=key, method=func)
[docs]def try_loads(fns, func = cv2.imread, paths = None, debug = False, addpath=False): """ Try to load images from paths. :param fns: list of file names :param func: loader function :param paths: paths to try. By default it loads working dir and test path :param debug: True to show debug messages :param addpath: add path as second argument :return: image else None """ default = ("", str(MANAGER["TESTPATH"])) if paths is None: paths = [] if isinstance(paths,basestring): paths = [paths] paths = list(paths) for i in default: if i not in paths: paths.append(i) for fn in fns: for path in paths: try: if path[-1] not in ("/","\\"): # ensures path path += "/" except: pass path += fn im = func(path) # foreground if im is not None: if debug: print(path, " Loaded...") if addpath: return im,path return im
[docs]def hist_match(source, template, alpha = None): """ Adjust the pixel values of an image to match those of a template image. :param source: image to transform colors to template :param template: template image () :param alpha: :return: transformed source """ # theory https://en.wikipedia.org/wiki/Histogram_matching # explanation http://dsp.stackexchange.com/a/16175 and # http://fourier.eng.hmc.edu/e161/lectures/contrast_transform/node3.html # based on implementation http://stackoverflow.com/a/33047048/5288758 # see http://www.mathworks.com/help/images/ref/imhistmatch.html if len(source.shape)>2: matched = np.zeros_like(source) for i in range(3): matched[:,:,i] = hist_match(source[:,:,i], template[:,:,i]) return matched else: oldshape = source.shape source = source.ravel() template = template.ravel() # get the set of unique pixel values and their corresponding indices and # counts s_values, bin_idx, s_counts = np.unique(source, return_inverse=True, return_counts=True) t_values, t_counts = np.unique(template, return_counts=True) # take the cumsum of the counts and normalize by the number of pixels to # get the empirical cumulative distribution functions for the source and # template images (maps pixel value --> quantile) s_quantiles = np.cumsum(s_counts).astype(np.float64) s_quantiles /= s_quantiles[-1] t_quantiles = np.cumsum(t_counts).astype(np.float64) t_quantiles /= t_quantiles[-1] # interpolate linearly to find the pixel values in the template image # that correspond most closely to the quantiles in the source image interp_t_values = np.interp(s_quantiles, t_quantiles, t_values) return interp_t_values[bin_idx].reshape(oldshape) # reconstruct image
############################# GETCOORS ############################################ # http://docs.opencv.org/master/db/d5b/tutorial_py_mouse_handling.html # http://docs.opencv.org/modules/highgui/doc/qt_new_functions.html
[docs]class ImCoors(object): """ Image's coordinates class. Example:: a = ImCoors(np.array([(116, 161), (295, 96), (122, 336), (291, 286)])) print a.__dict__ print "mean depend on min and max: ", a.mean print a.__dict__ print "after mean max has been already been calculated: ", a.max a.data = np.array([(116, 161), (295, 96)]) print a.__dict__ print "mean and all its dependencies are processed again: ", a.mean """ def __init__(self, pts, dtype=FLOAT, deg=False): """ Initiliazes ImCoors. :param pts: list of points :param dtype: return data as dtype. Default is config.FLOAT """ self._pts = pts # supports bigger numbers self._dtype = dtype self._deg = deg @property def pts(self): return self._pts @pts.setter def pts(self, value): getattr(self,"__dict__").clear() self._pts = value @pts.deleter def pts(self): raise Exception("Cannot delete attribute") @property def dtype(self): return self._dtype @dtype.setter def dtype(self,value): getattr(self,"__dict__").clear() self._dtype = value @dtype.deleter def dtype(self): raise Exception("Cannot delete attribute") # DATA METHODS def __len__(self): return len(self._pts) @Cache
[docs] def max(self): """ Maximum in each axis. :return: x_max, y_max """ #self.max_x, self.max_y = np.max(self.data,0) return tuple(np.max(self._pts, 0))
@Cache
[docs] def min(self): """ Minimum in each axis. :return: x_min, y_min """ #self.min_x, self.min_y = np.min(self.data,0) return tuple(np.min(self._pts, 0))
@Cache
[docs] def rectbox(self): """ Rectangular box enclosing points (origin and end point or rectangle). :return: (x0,y0),(x,y) """ return (self.min,self.max)
@Cache
[docs] def boundingRect(self): """ Rectangular box dimensions enclosing points. example:: P = np.ones((400,400)) a = ImCoors(np.array([(116, 161), (295, 96), (122, 336), (291, 286)])) x0,y0,w,h = a.boundingRect P[y0:y0+h,x0:x0+w] = 0 :return: x0,y0,w,h """ return cv2.boundingRect(self._pts)
@Cache
[docs] def minAreaRect(self): return cv2.minAreaRect(self._pts)
@Cache
[docs] def rotatedBox(self): """ Rotated rectangular box enclosing points. :return: 4 points. """ try: # opencv 2 return self._dtype(cv2.cv.BoxPoints(self.minAreaRect)) except AttributeError: # opencv 3 return self._dtype(cv2.boxPoints(self.minAreaRect))
@Cache
[docs] def boxCenter(self): """ Mean in each axis. :return: x_mean, y_mean """ #self.mean_x = (self.max_x+self.min_x)/2 #self.mean_y = (self.max_y+self.min_y)/2 xX,xY = self.max nX,nY = self.min return tuple(self._dtype((xX + nX, xY + nY)) / 2)
@Cache
[docs] def mean(self): """ Center or mean. :return: x,y """ # http://hyperphysics.phy-astr.gsu.edu/hbase/cm.html # https://www.grc.nasa.gov/www/K-12/airplane/cg.html #self.center_x, self.center_y = np.sum(self.data,axis=0)/len(self.data) #map(int,np.mean(self.data,0)) #tuple(np.sum(self.data,axis=0)/len(self.data)) return tuple(np.mean(self._pts, 0, dtype = self._dtype))
center = mean @Cache
[docs] def area(self): """ Area of points. :return: area number """ return polygonArea(self._pts)
@Cache
[docs] def rectangularArea(self): """ Area of rectangle enclosing points aligned with x,y axes. :return: area number """ #method 1, it is not precise in rotation (x0,y0),(x,y) = self.rectbox return self.dtype(np.abs((x-x0)*(y-y0)))
@Cache
[docs] def rotatedRectangularArea(self): """ Area of Rotated rectangle enclosing points. :return: area number """ return polygonArea(self.rotatedBox)
@Cache
[docs] def rectangularity(self): """ Ratio that represent a perfect square aligned with x,y axes. :return: ratio from 1 to 0, 1 representing a perfect rectangle. """ #method 1 #cx,cy = self.center #bcx,bcy=self.boxCenter #return (cx)/bcx,(cy)/bcy # x_ratio, y_ratio #method 2 return self.dtype(self.area/self.rectangularArea)
@Cache
[docs] def rotatedRectangularity(self): """ Ratio that represent a perfect rotated square fitting points. :return: ratio from 1 to 0, 1 representing a perfect rotated rectangle. """ # prevent unstable values if self.area < 1: area = 0 # needed to prevent false values else: area = self.area return self.dtype(area/self.rotatedRectangularArea)
@Cache
[docs] def regularity(self): """ Ratio of forms with similar measurements and angles. e.g. squares and rectangles have rect angles so they are regular. For regularity object must give 1. :return: """ # TODO this algorithm is still imperfect pi = angle((1,0),(0,1),deg=self._deg) # get pi value in radian or degrees av = self.vertexesAngles return pi*(len(av))/np.sum(av) # pi*number_agles/sum_angles
@Cache
[docs] def relativeVectors(self): """ Form vectors from points. :return: array of vectors [V0, ... , (V[n] = x[n+1]-x[n],y[n+1]-y[n])]. """ pts = np.array(self._pts) pts = np.append(pts,[pts[0]],axis=0) # adds last vector from last and first point. return np.stack([np.diff(pts[:, 0]), np.diff(pts[:, 1])], 1)
@Cache
[docs] def vertexesAngles(self): """ Relative angle of vectors formed by vertexes. i.e. angle between vectors "v01" formed by points "p0-p1" and "v12" formed by points "p1-p2" where "p1" is seen as a vertex (where vectors cross). :return: angles. """ vs = self.relativeVectors # get all vectors from points. vs = np.roll(np.append(vs,[vs[-1]],axis=0),2) # add last vector to first position return np.array([angle(vs[i-1],vs[i],deg=self._deg) for i in range(1,len(vs))],self._dtype) # caculate angles
@Cache
[docs] def pointsAngles(self): """ Angle of vectors formed by points in Cartesian plane with respect to x axis. i.e. angle between vector "v01" (formed by points "p0-p1") and vector unity in axis x. :return: angles. """ vs = self.relativeVectors # get all vectors from points. return vectorsAngles(pts=vs, dtype=self._dtype, deg=self._deg)
@Cache
[docs] def vectorsAngles(self): """ Angle of vectors in Cartesian plane with respect to x axis. i.e. angle between vector "v0" (formed by point "p0" and the origin) and vector unity in axis x. :return: angles. """ return np.array([angle((1,0),i,deg=self._deg) for i in self._pts],self._dtype) # caculate angles with respect to x axis
[docs]def drawcoorpoints(vis,points,col_out=black,col_in=red,radius=2): """ Funtion to draw points. :param vis: image array. :param points: list of points. :param col_out: outer color of point. :param col_in: inner color of point. :param radius: radius of drawn points. :return: """ points = np.array(points,INT) radius_in = radius-1 for x,y in points: cv2.circle(vis, (x,y), radius, col_out, -1) cv2.circle(vis, (x,y), radius_in, col_in, -1) return vis
[docs]def myline(img, pt1, pt2, color, thickness=None): """ Funtion to draw points (experimental). :param img: :param pt1: :param pt2: :param color: :param thickness: :return: """ # y=m*x+b x1,y1=np.array(pt1,dtype=FLOAT) x2,y2=np.array(pt2,dtype=FLOAT) m = (y2-y1)/(x2-x1) xmin,xmax = np.sort([x1,x2]) xvect = np.arange(xmin,xmax+1).astype('int') yvect = np.array(xvect*m+int(y1-x1*m),dtype=np.int) for i in zip(yvect,xvect): #img.itemset(i,color) img[i]=color
[docs]def drawcooraxes(vis,points,col_out=black,col_in=green,radius=2): """ Function to draw axes instead of points. :param vis: image array. :param points: list of points. :param col_out: outer color of point. :param col_in: inner color of point. :param radius: radius of drawn points. :return: """ points = np.array(points,INT) thickness = radius-1 h1, w1 = vis.shape[:2] # obtaining image dimensions for i in points: h1pt1 = (0,i[1]) h1pt2 = (w1,i[1]) w2pt1 = (i[0],0) w2pt2 = (i[0],h1) cv2.line(vis, h1pt1, h1pt2, col_in, thickness) cv2.line(vis, w2pt1, w2pt2, col_in, thickness) vis = drawcoorpoints(vis,points,col_out,col_in,radius) return vis
[docs]def drawcoorpolyline(vis,points,col_out=black,col_in=red,radius=2): """ Function to draw interaction with points to obtain polygonal. :param vis: image array. :param points: list of points. :param col_out: outer color of point. :param col_in: inner color of point. :param radius: radius of drawn points. :return: """ thickness = radius-1 if len(points)>1: points = np.array(points,INT) cv2.polylines(vis,[points],False, col_in, thickness) """ for i in range(len(points)-1): pt1 = (points[i][0], points[i][1]) pt2 = (points[i+1][0], points[i+1][1]) cv2.line(vis, pt1, pt2, col_in, thickness)""" else: vis = drawcoorpoints(vis,points,col_out,col_in,radius) return vis
[docs]def drawcoorarea(vis,points,col_out=black,col_in=red,radius=2): """ Function to draw interaction with points to obtain area. :param vis: image array. :param points: list of points. :param col_out: outer color of point. :param col_in: inner color of point. :param radius: radius of drawn points. :return: """ if len(points) > 2: mask = np.zeros(vis.shape[:2]) cv2.drawContours(mask,[np.array(points,np.int32)],0,1,-1) vis = overlay(vis,np.array([(0,)*len(col_in),col_in])[mask.astype(int)],alpha=mask*0.5) vis = drawcoorpoints(vis,points,col_out,col_in,radius) else: vis = drawcoorpoints(vis,points,col_out,col_in,radius) return vis
[docs]def drawcoorpolyArrow(vis,points,col_out=black,col_in=red,radius=2): """ Function to draw interaction with vectors to obtain polygonal. :param vis: image array. :param points: list of points. :param col_out: outer color of point. :param col_in: inner color of point. :param radius: radius of drawn points. :return: """ points = np.array(points,INT) thickness = radius-1 if len(points)>1: for i in range(len(points)-1): pt1 = (points[i][0], points[i][1]) pt2 = (points[i+1][0], points[i+1][1]) cv2.arrowedLine(vis, pt1, pt2, col_in, thickness) vis = drawcoorpoints(vis,points,col_out,col_in,radius) # draw points else: vis = drawcoorpoints(vis,points,col_out,col_in,radius) return vis
[docs]def drawcoorperspective(vis,points,col_out=black,col_in=red,radius=2): """ Function to draw interaction with points to obtain perspective. :param vis: image array. :param points: list of points. :param col_out: outer color of point. :param col_in: inner color of point. :param radius: radius of drawn points. :return: """ points = np.array(points,INT) thickness = radius-1 if len(points)>1 and len(points)<5: for i in range(len(points)-1): if i%2: for j in range(i+1,min(len(points),i+3)): if j%2: #print "i=",i," j=",j pt1 = (points[i][0], points[i][1]) pt2 = (points[j][0], points[j][1]) cv2.arrowedLine(vis, pt1, pt2, col_in, thickness) else: for j in range(i+1,min(len(points),i+3)): #print "i=",i," j=",j pt1 = (points[i][0], points[i][1]) pt2 = (points[j][0], points[j][1]) cv2.arrowedLine(vis, pt1, pt2, col_in, thickness) vis = drawcoorpoints(vis,points,col_out,col_in,radius) # draw points else: vis = drawcoorpoints(vis,points,col_out,col_in,radius) return vis
[docs]def limitaxispoints(c,maxc,minc=0): """ Limit a point in axis. :param c: list of points.. :param maxc: maximum value of point. :param minc: minimum value of point. :return: return limited points. """ x = np.zeros(len(c),dtype=np.int) for i,j in enumerate(c): x[i] = limitaxis(j,maxc,minc) return tuple(x)
[docs]class GetCoors(Plotim): """ Create window to select points from image. :param im: image to get points. :param win: window name. :param updatefunc: function to draw interaction with points. (e.g. limitaxispoints, drawcoorperspective, etc.). :param prox: proximity to identify point. :param radius: radius of drawn points. :param unique: If True no point can be repeated, else selected points can be repeated. :param col_out: outer color of point. :param col_in: inner color of point. """ def __init__(self, im, win = "get coordinates", updatefunc=drawcoorpoints, unique=True, col_out=black, col_in=red): # functions super(GetCoors, self).__init__(win, im) # assign functions self.updatefunc = updatefunc self.userupdatefunc = updatefunc self.prox = 8 # proximity to keypoint # initialize user variables self.radius = 3 self.unique = unique self.col_out = col_out self.col_in = col_in # initialize control variables self.interpolation=cv2.INTER_AREA self._coors = [] self.rcoors = [] # rendered coordinates self.coorlen = 0 self.showstats = False self.mapdata2 = [None,None,None] self.data2 = np.zeros((self.rH,self.rW,1),dtype=np.uint8) self.drawcooraxes = drawcooraxes self.drawcoorperspective = drawcoorperspective self.drawcoorpolyline = drawcoorpolyline self.drawcoorpoints = drawcoorpoints self.controlText[0].extend([" No. coordinates: {self.coorlen}. "]) self.cmdeval.update({"points":"self.updatefunc = self.drawcoorpoints", "polyline":"self.updatefunc = self.drawcoorpolyline", "perspective":"self.updatefunc = self.drawcoorperspective", "axes":"self.updatefunc = self.drawcooraxes", "user":"self.updatefunc = self.userupdatefunc", "end":["self.updatecoors()","self.mousefunc()"]}) self.cmdlist.extend(["unique","showstats","user","points","polyline","perspective","axes"]) #self.coors # return coordinates @property def coors(self): return self._coors @coors.setter def coors(self,value): self._coors = value self.updatecoors() @coors.deleter def coors(self): self._coors = [] self.updatecoors()
[docs] def drawstats(self, points, col_out=black, col_in=green, radius=2): """ :param self: :param points: :param col_out: :param col_in: :param radius: :return: """ vis = self.rimg p = ImCoors(points) self.data2 = np.zeros((vis.shape[0],vis.shape[1],1),dtype=np.uint8) drawcooraxes(vis,[p.boxCenter],col_out,col_in,radius) drawcooraxes(self.data2,[p.boxCenter],1,1,self.prox) drawcooraxes(vis,[p.mean],col_in,col_out,radius) drawcooraxes(self.data2,[p.mean],2,2,self.prox) p1 = ImCoors(self.coors) self.mapdata2 = [None,"center at "+str(p1.boxCenter),"mean at "+str(p1.mean)]
[docs] def updatecoors(self): """ :param self: :return: """ self.coorlen = len(self.coors) self.updaterenderer() if self.coors != []: self.rcoors = self.coors[:] newc = self.coors[:] for j,i in enumerate(self.coors): newc[j] = self.real2render(i[0],i[1]) self.rcoors[j] = limitaxispoints(newc[j],10000,-10000) if self.showstats: self.drawstats(newc, radius=self.radius) self.rimg = self.updatefunc(self.rimg,newc,self.col_out,self.col_in,self.radius) else: self.data2[:] = 0 self.coordinateText = [["xy({self.x},{self.y})"]]
[docs] def mousefunc(self): """ :param self: :return: """ # control system controlled = self.builtincontrol() drawed = False # get nearest coordinate to pointer isnear = False if self.coors != [] and self.rx is not None and self.ry is not None: # vals = anorm(np.int32(self.coors) - (self.x, self.y)) # relative to real coordinates vals = anorm(np.int32(self.rcoors) - (self.rx, self.ry)) # relative to rendered coordinates near_point = np.logical_and(vals < self.prox, vals == np.min(vals)) if np.any(near_point): # if near point idx = np.where(near_point)[0] # get index isnear = True val = self.coors[idx[0]] count = self.coors.count(val) self.coordinateText = [["point "+str(idx[0])+" at "+str(val)+"x"+str(count)]] else: self.coordinateText = [["xy({self.x},{self.y})"]] # coordinate system if not controlled and bool(self.flags): if self.event== cv2.EVENT_RBUTTONDBLCLK: # if middle button DELETE ALL COORDINATES self.coors = [] self.img = self.data.copy() drawed = True elif isnear and self.event== cv2.EVENT_RBUTTONDOWN: # if right button DELETE NEAREST COORDINATE self.coors.pop(idx[0]) # if more than one point delete first drawed = True elif self.event== cv2.EVENT_LBUTTONDOWN: # if left button ADD COORDINATE val = (self.x,self.y) if not self.coors.count(val) or not self.unique: self.coors.append(val) drawed = True # update renderer if (controlled or drawed): self.updatecoors() if self.y is not None and self.x is not None: if self.showstats: data = self.mapdata2[self.data2[self.ry,self.rx]] if not isnear and data is not None: self.coordinateText = [[data]] self.builtinplot(self.data[int(self.y),int(self.x)])
[docs]def getcoors(im, win ="get coordinates", updatefunc=drawcoorpoints, coors = None, prox=8, radius = 3, unique=True, col_out=black, col_in=red): self = GetCoors(im, win, updatefunc, unique=unique, col_out=col_out, col_in=col_in) self.radius = radius self.prox = prox if coors is not None: self.coors = standarizePoints(coors,aslist=True) self.show(clean=False) coors = self.coors self.clean() return coors
[docs]def separe(values, sep, axis=0): """ Separate values from separator or threshold. :param values: list of values :param sep: peparator value :param axis: axis in each value :return: lists of greater values, list of lesser values """ greater,lesser = [],[] for i in values: if i[axis]>sep: greater.append(i) else: lesser.append(i) return greater,lesser
[docs]def getrectcoors(*data): """ Get ordered points. :param data: list of points :return: [Top_left,Top_right,Bottom_left,Bottom_right] """ #[Top_left,Top_right,Bottom_left,Bottom_right] #img, win = "get pixel coordinates", updatefunc = drawpoint if len(data)==1: # points points = data[0] else: # img, win points = getcoors(*data) p = ImCoors(points) min_x,min_y = p.min max_x,max_y = p.max Top_left = (min_x,min_y) Top_right = (max_x,min_y) Bottom_left = (min_x,max_y) Bottom_right = (max_x,max_y) return [Top_left,Top_right,Bottom_left,Bottom_right]
[docs]def quadrants(points): """ Separate points respect to center of gravity point. :param points: list of points :return: [[Top_left],[Top_right],[Bottom_left],[Bottom_right]] """ # group points on 4 quadrants # [Top_left,Top_right,Bottom_left,Bottom_right] p = ImCoors(points) # points data x,y -> (width,height) mean_x, mean_y = p.mean Bottom,Top = separe(points,mean_y,axis=1) Top_right,Top_left = separe(Top,mean_x,axis=0) Bottom_right,Bottom_left = separe(Bottom,mean_x,axis=0) return [Top_left,Top_right,Bottom_left,Bottom_right]
[docs]def getgeometrycoors(*data): """ Get filled object coordinates. (function in progress) """ #[Top_left,Top_right,Bottom_left,Bottom_right] #img, win = "get pixel coordinates", updatefunc = drawpoint if len(data)==1: # points points = data[0] else: # img, win points = getcoors(*data) return points
[docs]def random_color(channels = 1, min=0, max=256): """ Random color. :param channels: number of channels :param min: min color in any channel :param max: max color in any channel :return: random color """ return [np.random.randint(min,max) for i in range(channels)]
[docs]class Image(object): """ Structure to load and save images """ def __init__(self, name=None, ext=None, path=None, shape=None, verbosity=False): self._loader = loadFunc(-1,dsize=None,throw=False) self._shape = None self.shape=shape # it is the inverted of dsize self.ext=ext self.name=name self.path=path self._RGB=None self._RGBA = None self._gray=None self._BGRA=None self._BGR=None self.overwrite = False self.verbosity = verbosity self.log_saved = None self.log_loaded = None self.last_loaded = None @property def shape(self): return self._shape @shape.setter def shape(self,value): if value != self._shape: if value is not None: value = value[1],value[0] # invert, for value is shape and we need dsize self._loader = loadFunc(-1,dsize=value,throw=False) self._shape = value @shape.deleter def shape(self): del self._shape @property def ext(self): if self._ext is None: return "" return self._ext @ext.setter def ext(self,value): try: if not value.startswith("."): # ensures path value = "."+value except: pass self._ext = value @ext.deleter def ext(self): del self._ext @property def path(self): if self._path is None: return "" return self._path @path.setter def path(self,value): try: if value[-1] not in ("/","\\"): # ensures path value += "/" except: pass self._path = value @path.deleter def path(self): del self._path @property def BGRA(self): if self._BGRA is None: self.load() return self._BGRA @BGRA.setter def BGRA(self,value): self._BGRA = value @BGRA.deleter def BGRA(self): self._BGRA = None @property def BGR(self): if self._BGR is None: self.load() return self._BGR @BGR.setter def BGR(self,value): self._BGR = value @BGR.deleter def BGR(self): self._BGR = None @property def RGB(self): if self._RGB is None: self._RGB = cv2.cvtColor(self.BGR, cv2.COLOR_BGR2RGB) return self._RGB @RGB.setter def RGB(self,value): self._RGB = value @RGB.deleter def RGB(self): self._RGB = None @property def RGBA(self): if self._RGBA is None: self._RGBA = cv2.cvtColor(self.BGRA, cv2.COLOR_BGRA2RGBA) return self._RGBA @RGBA.setter def RGBA(self,value): self._RGBA = value @RGBA.deleter def RGBA(self): self._RGBA = None @property def gray(self): if self._gray is None: self._gray = cv2.cvtColor(self.BGR, cv2.COLOR_BGR2GRAY) return self._gray @gray.setter def gray(self,value): self._gray = value @gray.deleter def gray(self): self._gray = None
[docs] def save(self, name=None, image=None, overwrite = None): """ save restored image in path. :param name: filename, string to format or path to save image. if path is not a string it would be replaced with the string "{path}restored_{name}{ext}" to format with the formatting "{path}", "{name}" and "{ext}" from the baseImage variable. :param image: (self.BGRA) :param overwrite: If True and the destine filename for saving already exists then it is replaced, else a new filename is generated with an index "{filename}_{index}.{extension}" :return: saved path, status (True for success and False for fail) """ if name is None: name = self.name if name is None: raise Exception("name parameter needed") if image is None: image = self.BGRA if overwrite is None: overwrite = self.overwrite bbase, bpath, bname = getData(self.path) bext = self.ext # format path if user has specified so data = getData(name.format(path="".join((bbase, bpath)), name=bname, ext=bext)) # complete any data lacking in path for i,(n,b) in enumerate(zip(data,(bbase, bpath, bname, bext))): if not n: data[i] = b # joint parts to get string fn = "".join(data) mkPath(getPath(fn)) if not overwrite: fn = increment_if_exits(fn) if cv2.imwrite(fn,image): if self.verbosity: print("Saved: {}".format(fn)) if self.log_saved is not None: self.log_saved.append(fn) return fn, True else: if self.verbosity: print("{} could not be saved".format(fn)) return fn, False
[docs] def load(self, name = None, path = None, shape = None): if name is None: name = self.name if path is None: path = self.path if path is None: path = "" if shape is not None: self.shape = shape data = try_loads([name,name+self.ext], paths=path, func= self._loader, addpath=True) if data is None: raise Exception("Image not Loaded") img, last_loaded = data if self.log_loaded is not None: self.log_loaded.append(last_loaded) if self.verbosity: print("loaded: {}".format(last_loaded)) self.last_loaded = last_loaded self._RGB=None self._RGBA = None self._gray=None if img.shape[2] == 3: self.BGR = img self.BGRA = cv2.cvtColor(img,cv2.COLOR_BGR2BGRA) else: self.BGRA = img self.BGR = cv2.cvtColor(img,cv2.COLOR_BGRA2BGR) return self