# import # ------------------------------------------------------------------------------------------ from . import Exif, ColorSpace, imageType, channel import miam.processing.TMO_CCTF import miam.processing.ColorSpaceTransform as MCST import rawpy, colour, os, skimage.transform, copy import numpy as np import json import matplotlib.pyplot as plt # ------------------------------------------------------------------------------------------ # MIAM project 2020 # ------------------------------------------------------------------------------------------ # author: remi.cozot@univ-littoral.fr # ------------------------------------------------------------------------------------------ class Image(object): """ class image: attribute(s): name: image file name (str) colorData: array of pixels (np.ndarray) type: type of image (image.imageType) linear: image encoding is linear (boolean) colorSpace: colorspace (colour.models.RGB_COLOURSPACES) scalingFactor: scaling factor to [0..1] space (float) """ def __init__(self, colorData, name, type, linear, colorspace, scalingFactor): """ constructor """ self.name = name self.colorData = colorData self.shape = self.colorData.shape self.type = type self.linear = linear self.colorSpace = colorspace self.scalingFactor = scalingFactor # ---------------------------------------------------- # update May 2020 # ---------------------------------------------------- # mask mask shape is image shape with only one channel self.mask = None # ---------------------------------------------------- # ---------------------------------------------------- def isHDR(self): return self.type == imageType.imageType.HDR def __repr__(self): res = " Image{ name:" + self.name + "\n" + \ " shape: " + str(self.shape) + "\n" + \ " type: " + str(self.type) + "\n" + \ " color space: " + self.colorSpace.__repr__() + "\n" + \ " linear: " + str(self.linear) + "\n" + \ " scaling factor: " + str(self.scalingFactor) + "\n }" return res def __str__(self) : res = " Image{ name:" + self.name + "\n" + \ " shape: " + str(self.shape) + "\n" + \ " type: " + str(self.type) + "\n" + \ " color space: " + self.colorSpace.name + "\n" + \ " linear: " + str(self.linear) + "\n" + \ " scaling factor: " + str(self.scalingFactor) + "\n }" return res def writeImage(self, filename): """ Write Image: two output files are generated: filename+".json" contains additional data, filename+".jpg|hdr" contains colorData @params: filename - Required : filename WITHOUT extension [for example "../images/image00"] (Str) """ # outfile depend on image type ext, bitDepth = None, None if self.type == imageType.imageType.SDR: ext, bitDepth = "jpg", "uint8" elif self.type == imageType.imageType.HDR: ext, bitDepth = "hdr", "float32" else: print("WARNING[miam.image.Image.writeImage(",filename,"):: writing RAW image not yet supported!]") (fpath, fname) = os.path.split(filename) output = { 'name': self.name, 'filename': fname+"."+ext, 'shape': str(self.shape), 'type': str(self.type), 'colorspace': self.colorSpace.name, 'linear': str(self.linear), 'scaling': str(self.scalingFactor) } with open(filename+".json", 'w') as outfile: json.dump(output, outfile, indent=4) colour.write_image(self.colorData, filename+"."+ext, bit_depth=bitDepth, method='Imageio') def getChannelVector(self, channel): """ get channel vector: works only for sR|sG|sB, X|Y|Z and L|a|b """ # take into account colorSpace destColor = channel.colorSpace() image = MCST.ColorSpaceTransform().compute(self,dest=destColor) if channel.getValue() <3: imgVector = Image.array2vector(image.colorData) return imgVector[:,channel.getValue()] else: return None def getChannel(self, channel): """ get channel : works only for sR|sG|sB, X|Y|Z and L|a|b """ # take into account colorSpace destColor = channel.colorSpace() image = MCST.ColorSpaceTransform().compute(self,dest=destColor) if channel.getValue() <3: return image.colorData[:,:,channel.getValue()] else: return None def getMinMaxPerChannel(self): img = self.colorData R, G, B = img[:,:,0], img[:,:,1], img[:,:,2] minR, minG, minB = np.amin(R), np.amin(G), np.amin(B) maxR, maxG, maxB = np.amax(R), np.amax(G), np.amax(B) return ((minR,maxR),(minG,maxG),(minB,maxB)) def getDynamicRange(self, percentile = None, mode=None): """ return dynamic range of image @params: percentile - Optional : percentile if None: just remove zero values (Float) mode - Optional : "maxmin" (default)| "f-stops" (Str) """ Y_min,Y_max = None, None Y = self.getChannelVector(channel.channel.Y) if percentile == None : # remove zeros Y = vY[vY>0] # use min and max Y_min = np.amin(Y) Y_max = np.amax(Y) else: Y_min = np.percentile(Y,percentile) Y_max = np.percentile(Y,100-percentile) # take mode into account if not mode: mode = 'maxmin' if mode == "f-stops": return np.log2(Y_max)-np.log2(Y_min) if mode=="maxmin": return (Y_max,Y_min) # functionnal methods def removeZeros(self, minPencitile=None): # copy gimage res = copy.deepcopy(self) img = res.colorData # get channels R, G, B = img[:,:,0], img[:,:,1], img[:,:,2] # remove zero value per channel notZeroR, notZeroG, notZeroB = R[R>0], G[G>0],B[B>0] if minPencitile==None: # take none zero min of channels Rmin, Gmin, Bmin = np.amin(notZeroR), np.amin(notZeroG), np.amin(notZeroB) # replace zero values by min (per channel) R[R==0] = Rmin G[G==0] = Gmin B[B==0] = Bmin else: # replace zeros by min Pecentile Rmin = np.percentile(R,minPencitile) Gmin = np.percentile(G,minPencitile) Bmin = np.percentile(B,minPencitile) R[Rmax] = max res.colorData[res.colorData maxSize else 1 res.colorData = skimage.transform.resize(res.colorData, (x // factor, y // factor), anti_aliasing) res.shape = res.colorData.shape return res def resize(self,size=(None,None),anti_aliasing=False): res = copy.deepcopy(self) x, y, c = tuple(res.colorData.shape) ny,nx = size if nx and (not ny): factor = nx/x res.colorData = skimage.transform.resize(res.colorData, (nx, int(y * factor)), anti_aliasing) res.shape = res.colorData.shape elif (not nx) and ny: factor = ny/y res.colorData = skimage.transform.resize(res.colorData, (int(x * factor),ny), anti_aliasing) res.shape = res.colorData.shape elif nx and ny: res.colorData = skimage.transform.resize(res.colorData, (nx,ny), anti_aliasing) res.shape = res.colorData.shape return res def process(self, process, **kwargs): """ process image: (Object) process must have compute(image, **kwargs) method **kwargs optionnal parameters packed as dict """ return process.compute(self,**kwargs) def buildMaskByValue(self, channel, min, max, zero = 0, one=1): # convert into channel color space ch = MCST.ColorSpaceTransform().compute(self,dest=channel.colorSpace()).colorData[:,:,channel.getValue()] # mask ch[ch<=min] = zero ch[ch>=max] = zero ch[ch!=zero] = one res = Image.newImage(self.shape, colorSpaceName='sRGB',name='mask') res = copy.deepcopy(self) res.colorData[:,:,0] = ch res.colorData[:,:,1] = ch res.colorData[:,:,2] = ch return res # ---------------------------------------------------- # update May 2020 # ---------------------------------------------------- def addMask(self, one=True): [height, width, channel] = self.shape self.mask = np.ones((height, width)) if one else np.zeros((height, width)) def removeMask(self) : self.mask = None def hasMask(self): return isinstance(self.mask,np.ndarray) def binaryMask(self): return (self.mask[self.mask!=1] ==0).all() def isMaskOne(self): return (self.mask ==1).all() # ---------------------------------------------------- # ---------------------------------------------------- # plot def plot(self, ax, shortName=True, title=True): if not (self.type == imageType.imageType.HDR): ax.imshow(self.colorData) sep = '/' if ('/' in self.name) else '\\' name = self.name.split(sep)[-1] if shortName else self.name if title: ax.set_title(name+"("+self.colorSpace.name +"/"+ str(self.type)+")") else: # HDR ax.imshow(self.process(miam.processing.TMO_CCTF.TMO_CCTF(), function='sRGB').colorData) sep = '/' if ('/' in self.name) else '\\' name = self.name.split(sep)[-1] if shortName else self.name if title : ax.set_title(name+"("+self.colorSpace.name +"/"+ str(self.type)+"/"+ "sRGB_CCTF" +")") ax.axis("off") # magic operators def __add__(self, other): # create a copy res = copy.deepcopy(self) if isinstance(other,type(self)): # both are images if self.shape == other.shape: # both share same size if (not self.hasMask()) and (not other.hasMask()): # none has mask res.colorData = self.colorData + other.colorData else: # self or other has a mask if not self.hasMask(): self.addMask() if not other.hasMask(): other.addMask() # both have a mask res.colorData = self.colorData*self.mask[...,np.newaxis] \ + other.colorData*other.mask[...,np.newaxis] # mask res.mask = self.mask + other.mask # regularization res.colorData[res.mask>1] = res.colorData[res.mask>1] / res.mask[res.mask>1,np.newaxis] res.mask[res.mask>1] = 1 else: print("WARNING[Image.__add__(self,other): both image must have the same shape ! return a copy of ",self,"]") # hdr or sdr res.type = imageType.imageType.HDR if (self.isHDR() or other.isHDR()) else imageType.imageType.SDR elif isinstance(other, (int, float)): # sum with int or float res.colorData = self.colorData + other if not self.isHDR(): # if not HDR clip value res.colorData[res.colorData>1] = 1 res.colorData[res.colorData<0] = 0 return res def __radd__(self, other): return self.__add__(other) def __sub__(self, other): # create a copy res = copy.deepcopy(self) if isinstance(other,type(self)): # both are image if self.shape == self.other: # same size if (not self.hasMask()) and (not other.hasMask()): # mask management res.colorData = self.colorData - other.colorData else: # self or other has a mask if not self.hasMask(): self.addMask() if not other.hasMask(): other.addMask() # both have a mask res.colorData = self.colorData*self.mask[...,np.newaxis] \ - other.colorData*other.mask[...,np.newaxis] # mask res.mask = self.mask + other.mask[...,np.newaxis] res.mask[res.mask>1] = 1 else: print("WARNING[Image.__sub__(self,other): both image must have the same shape ! return a copy of ",self,"]") elif isinstance(other, (int, float)): res.colorData = self.colorData + other return res def __rsub__(self, other): # create a copy res = copy.deepcopy(self) if isinstance(other,type(self)): if self.shape == self.other: res.colorData = other.colorData - self.colorData else: print("WARNING[Image.__sub__(self,other): both image must have the same shape ! return a copy of ",self,"]") elif isinstance(other, (int, float)): res.colorData = other - self.colorData return res def __mul__ (self, other): # create a copy res = copy.deepcopy(self) if isinstance(other,type(self)): if self.shape == self.other: res.colorData = self.colorData * other.colorData else: print("WARNING[Image.__mul__(self,other): both image must have the same shape ! return a copy of ",self,"]") elif isinstance(other, (int, float)): res.colorData = self.colorData * other return res def __rmul__ (self, other): return self.__mul__(other) def __pow__(self,other): # create a copy res = copy.deepcopy(self) if isinstance(other, (int, float)): res.colorData = self.colorData**other return res def __rpow__(self,other): return self.__pow__(other) # class methods def readImage(filename,readExif=True): # default values scalingFactor = 1.0 type = None linear = None # image name path, name = os.path.split(filename) if readExif: # reading metadata then build exposure and colour space from exif exif = Exif.Exif.buildFromFileName(filename) colorspace = ColorSpace.ColorSpace.buildFromExif(exif) else: colorspace = ColorSpace.ColorSpace.build('sRGB') # extension sensitive splits = filename.split('.') ext = splits[-1].lower() # load raw file using rawpy if ext=="arw" or ext=="dng": outBit = 16 raw = rawpy.imread(filename) ppParams = rawpy.Params(demosaic_algorithm=None, half_size=False, four_color_rgb=False, dcb_iterations=0, dcb_enhance=False, fbdd_noise_reduction=rawpy.FBDDNoiseReductionMode.Off, noise_thr=None, median_filter_passes=0, use_camera_wb=True, # default False use_auto_wb=False, user_wb=None, output_color=rawpy.ColorSpace.sRGB, # output in SRGB output_bps=outBit, # default 8 user_flip=None, user_black=None, user_sat=None, no_auto_bright=False, auto_bright_thr=None, adjust_maximum_thr=0.75, bright=1.0, highlight_mode=rawpy.HighlightMode.Clip, exp_shift=None, exp_preserve_highlights=0.0, no_auto_scale=False, gamma=None, # linear output chromatic_aberration=None, bad_pixels_path=None) imgDouble = colour.utilities.as_float_array(raw.postprocess(ppParams))/(pow(2,16)-1) raw.close() type = imageType.RAW linear = True # load jpg, tiff, hdr file using colour else: imgDouble = colour.read_image(filename, bit_depth='float32', method='Imageio') imgDouble = Image.forceColorData3(imgDouble) type = imageType.imageType.SDR linear = False # post processing for HDR scaling to [ ,1] if ext =="hdr": imgDouble, scalingFactor = Image.scaleMaxOne(imgDouble) type = imageType.imageType.HDR linear = True #return Image(imgDouble, filename, type, linear, colorspace, scalingFactor) # long name return Image(imgDouble, name, type, linear, colorspace, scalingFactor) # short name def read(filename,exif=True): return Image.readImage(filename,readExif=exif) def newImage(shape, colorSpaceName=None, color=None, type=None,name=None): """ description """ # default values if not colorSpaceName : colorSpaceName = 'sRGB' if not color : color = np.asarray([0.0,0.0,0.0]) if not type : type : imageType.imageType.SDR linear = False if ((type==imageType.imageType.SDR) and (colorSpaceName=='sRGB')) else True if not name : name = "no name["+colorSpaceName+"]" scalingFactor = 1.0 # colorSpace colorSpace = ColorSpace.ColorSpace.build(colorSpaceName) # colorData if len(shape) == 2 : (h,w) = shape newShape = (h,w,3) else: newShape = shape colorData = np.ones(newShape) colorData[:,:,0] = colorData[:,:,0]*color[0] colorData[:,:,1] = colorData[:,:,1]*color[1] colorData[:,:,2] = colorData[:,:,2]*color[2] return Image(colorData, name, type, linear, colorSpace, scalingFactor) def forceColorData3(imgDouble): """ force color data to hace 3 channels """ # force image to 3 channels if len(imgDouble.shape) == 2: # single channel image h,w = imgDouble.shape img3 = np.ones([h,w,3]) img3[:,:,0] = imgDouble img3[:,:,1] = imgDouble img3[:,:,2] = imgDouble else: h,w,c = imgDouble.shape if c==4: # remove alpha channel img3 = np.ones([h,w,3]) img3[:,:,0] = imgDouble[:,:,0] img3[:,:,1] = imgDouble[:,:,1] img3[:,:,2] = imgDouble[:,:,2] else: img3 = imgDouble return img3 def array2vector(img): """ transform 2D array of color data to vector """ if len(img.shape) ==2 : x,y = img.shape c = 1 else: x,y,c = img.shape return np.reshape(img, (x * y, c)) def scaleMaxOne(img): """ scale image colorData in [0, 1] space """ imgVector = Image.array2vector(img) R, G, B = imgVector[:,0], imgVector[:,1], imgVector[:,2] maxRGB = max([np.amax(R), np.amax(G), np.amax(B)]) return img/maxRGB, 1.0/maxRGB