123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542 |
- # 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[R<Rmin] = Rmin
- G[G<Gmin] = Gmin
- B[B<Bmin] = Bmin
- # combine channels
- img[:,:,0], img[:,:,1], img[:,:,2] = R, G, B
- res.colorData = img
- return res
- def clip(self, min=0.0, max=1.0):
- res = copy.deepcopy(self)
- res.colorData[res.colorData>max] = max
- res.colorData[res.colorData<min] = min
- return res
- def smartResize(self,maxSize=400,anti_aliasing=False):
- res = copy.deepcopy(self)
- x, y, c = tuple(res.colorData.shape)
- maxImage = max(x,y)
- factor = maxImage/maxSize if maxImage > 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
|