Loading elphel_exif.py 0 → 100644 +176 −0 Original line number Original line Diff line number Diff line __author__ = "Oleg Dzhimiev" __copyright__ = "Copyright (C) 2018 Elphel Inc." __license__ = "GPL-3.0+" __email__ = "oleg@elphel.com" ''' Example usage: test.py: import elphel_exif info = elphel_exif.Exif("test.jpeg") print(info) print(info.MakerNote) print(info.exif.toString()) ''' from PIL import Image from PIL.ExifTags import TAGS, GPSTAGS # Might be needed by some applications like: # panoramic of Eyesis4Pi, composite images def elphel_add_to_PIL_TAGS(): TAGS[0x0129] = 'Page' TAGS[0xC62F] = 'Camera Serial Number' # Open file and Parse def elphel_get_exif(f): ret = {} i = Image.open(f) info = i._getexif() elphel_add_to_PIL_TAGS() for tag, value in info.items(): decoded = TAGS.get(tag, tag) if decoded == 'GPSInfo': gret = {} for gtag,gval in value.items(): gdecoded = GPSTAGS.get(gtag,gtag) gret[gdecoded] = gval value = gret if decoded == 'MakerNote': value = elphel_MakerNote_parse(value) ret[decoded] = value return ret # Init with default values def elphel_MakerNote_defaults(): ret = {} # defaults ret['COLOR_MODE'] = 0 return ret # Based on JP46_Reader_camera.java's jp46Reorder() def elphel_MakerNote_parse(MakerNote): ret = {} ret = elphel_MakerNote_defaults() ret['GAIN_R' ] = MakerNote[0]/65536.0 ret['GAIN_G'] = MakerNote[1]/65536.0 ret['GAIN_GB'] = MakerNote[2]/65536.0 ret['GAIN_B'] = MakerNote[3]/65536.0 ret['BLACK_R'] = (MakerNote[4]>>24)/256.0 ret['BLACK_G'] = (MakerNote[5]>>24)/256.0 ret['BLACK_GB'] = (MakerNote[6]>>24)/256.0 ret['BLACK_B'] = (MakerNote[7]>>24)/256.0 ret['GAMMA_R'] = ((MakerNote[4]>>16)&0xff)/100.0 ret['GAMMA_G'] = ((MakerNote[5]>>16)&0xff)/100.0 ret['GAMMA_GB'] = ((MakerNote[6]>>16)&0xff)/100.0 ret['GAMMA_B'] = ((MakerNote[7]>>16)&0xff)/100.0 ret['GAMMA_SCALE_R'] = MakerNote[4]&0xffff ret['GAMMA_SCALE_G'] = MakerNote[5]&0xffff ret['GAMMA_SCALE_GB'] = MakerNote[6]&0xffff ret['GAMMA_SCALE_B'] = MakerNote[7]&0xffff if len(MakerNote)>=14: if (MakerNote[10]&0xc0000000)!=0: composite = 1 else: composite = 0 ret['COMPOSITE'] = composite if composite: ret['HEIGHT1'] = MakerNote[11]&0xffff ret['BLANK1'] = (MakerNote[11]>>16)&0xffff ret['HEIGHT2'] = MakerNote[12]&0xffff ret['BLANK2'] = (MakerNote[12]>>16)&0xffff ret['HEIGHT3'] = (MakerNote[9]>>16)-ret['HEIGHT1']-ret['BLANK1']-ret['HEIGHT2']-ret['BLANK2'] ret['FLIPH1'] = (MakerNote[10]>>24)&0x1 ret['FLIPV1'] = (MakerNote[10]>>25)&0x1 ret['FLIPH2'] = (MakerNote[10]>>26)&0x1 ret['FLIPV2'] = (MakerNote[10]>>27)&0x1 ret['FLIPH3'] = (MakerNote[10]>>28)&0x1 ret['FLIPV3'] = (MakerNote[10]>>29)&0x1 ret['PORTRAIT'] = (MakerNote[13]>>7)&0x1 ret['YTABLEFORC'] = (MakerNote[13]>>15)&0x1 ret['QUALITY'] = MakerNote[13]&0x7f ret['CQUALITY'] = (MakerNote[13]>>8)&0x7f if ret['CQUALITY']==0: ret['CQUALITY'] = ret['QUALITY'] ret['CORING_INDEX_Y'] = (MakerNote[13]>>16)&0x7f ret['CORING_INDEX_C'] = (MakerNote[13]>>24)&0x7f if ret['CORING_INDEX_C']==0: ret['CORING_INDEX_C'] = ret['CORING_INDEX_Y'] if len(MakerNote)>=12: ret['WOI_LEFT'] = MakerNote[8]&0xffff ret['WOI_WIDTH'] = MakerNote[8]>>16 ret['WOI_TOP'] = MakerNote[9]&0xffff ret['WOI_HEIGHT'] = MakerNote[9]>>16 ret['FLIPH'] = MakerNote[10]&0x1 ret['FLIPV'] = (MakerNote[10]>>1)&0x1 ret['BAYER_MODE'] = (MakerNote[10]>>2)&0x3 ret['COLOR_MODE'] = (MakerNote[10]>>4)&0x0f ret['DCM_HOR'] = (MakerNote[10]>>8)&0x0f ret['DCM_VERT'] = (MakerNote[10]>>12)&0x0f ret['BIN_HOR'] = (MakerNote[10]>>16)&0x0f ret['BIN_VERT'] = (MakerNote[10]>>20)&0x0f # HW specific temperature readings if len(MakerNote)>=16: ret['TEMP1'] = (MakerNote[14]>> 0)&0xffff ret['TEMP2'] = (MakerNote[14]>>16)&0xffff ret['TEMP3'] = (MakerNote[14]>> 0)&0xffff ret['TEMP4'] = (MakerNote[14]>>16)&0xffff return ret # Class class Exif: def __init__(self,f): self.f = f self.data = elphel_get_exif(f) self.MakerNote = self.data['MakerNote'] #self.color_mode = self.data['MakerNote']['COLOR_MODE'] def toString(self): res = '' for k in self.data: if k!='MakerNote': res += str(k)+": "+str(self.data[k])+"\n" res += "MakerNote: \n" for k in self.data['MakerNote']: res += " "+str(k)+": "+str(self.data['MakerNote'][k])+"\n" return res[:-1] jp4-to-png.py 0 → 100644 +89 −0 Original line number Original line Diff line number Diff line #!/usr/bin/env python3 import jp4 #import cv2 import numpy as np import sys from PIL import Image, ImageOps try: filename = sys.argv[1] except IndexError: filename = "test.jp4" img = jp4.JP4(filename) # print exif #print(img.exif.toString()) # deblock if JP4 (auto read from exif) skip if other img.deblock() img.demosaic_bilinear() img.saturation() I = img.image[...,[2,1,0]].astype(np.uint8) res = Image.fromarray(I) #res.save("result.png") # also works #res.save("result.jpeg") # for Eyesis4pi exif = img.exif mn = exif.MakerNote #print(exif.MakerNote) # common # WOI_WIDTH # FLIPH # FLIPV # PORTRAIT # COMPOSITE # individual # HEIGHTx, 1..3 # FLIPHx, 1..3 # FLIPVx, 1..3 # BLANKx, only 1 and 2 if mn['COMPOSITE']: W = mn['WOI_WIDTH'] H = mn['WOI_HEIGHT'] FH = mn['FLIPH'] FV = mn['FLIPV'] #if FV: # I = v_offset = 0 for i in range(1,4): try: height = mn['HEIGHT'+str(i)] #fliph = mn['FLIPH'+str(i)] flipv = mn['FLIPV'+str(i)] except KeyError: # break for '_9' break try: blank = mn['BLANK'+str(i)] except KeyError: blank = 0 crop = res.crop((0,v_offset,W,v_offset+height)) if flipv: crop = ImageOps.flip(crop) # if eyesis4pi, 1&2 - rotate 90CW, 3 - 90CCW if i<3: crop = crop.rotate(-90,expand=1) else: crop = crop.rotate(90,expand=1) crop.save("result_"+str(i)+".jpeg") v_offset += height + blank jp4.py 0 → 100644 +398 −0 Original line number Original line Diff line number Diff line __copyright__ = "Copyright 2018, Elphel, Inc." __license__ = "GPL-3.0+" __maintainer__ = "Oleg Dzhimiev" __email__ = "oleg@elphel.com" import numpy as np #import imageio import elphel_exif from PIL import Image, ImageOps ''' Python image manipulation libs: Lib Reads image as Exif Image manipulations Comment Pillow(PIL) Image + + likes np.uint8 arrays imageio numpy.array - - almost useless opencv numpy.array - + Recommendations: ''' # clear from code def get_exif(filename): exif = elphel_exif.Exif(filename) # exif.f - filename # exif.data - parsed (as dict) exif with parsed MakeNote # exif.MakerNote - parsed as dict return exif # clear from code def naur(a): b0 = a[0] b0 = np.reshape(b0,(1,b0.shape[0])) b = a[:-1] b = np.concatenate((b0,b),axis=0) return b # clear from code def nabr(a): b0 = a[-1] b0 = np.reshape(b0,(1,b0.shape[0])) b = a[1:] b = np.concatenate((b,b0),axis=0) return b # clear from code def nalc(a): b0 = a[::,0] b0 = np.reshape(b0,(b0.shape[0],1)) b = a[::,:-1] b = np.concatenate((b0,b),axis=1) return b # clear from code def narc(a): b0 = a[::,-1] b0 = np.reshape(b0,(b0.shape[0],1)) b = a[::,1:] b = np.concatenate((b,b0),axis=1) return b class JP4: def __init__(self,filename="test.jp4"): # open image using PIL #i = Image.open(filename) #self.px = i.load() #self.px = scipy.misc.imread(filename, flatten=False, mode='RGB') #self.h, self.w, self.chn = self.px.shape self.exif = get_exif(filename) # not JP4 - no deblock self.need_deblocking = True if self.exif.MakerNote['COLOR_MODE']==5 else False self.need_demosaicing = True if self.need_deblocking else False #print(self.exif.color_mode) #print(self.exif.MakerNote['GAIN_R']) #print(self.exif.MakerNote['GAIN_G']) #print(self.exif.MakerNote['GAIN_B']) #print(self.exif.MakerNote['GAIN_GB']) if self.exif.MakerNote['BAYER_MODE']==0: self.bayer = [["Gr","R"],["B","Gb"]] elif self.exif.MakerNote['BAYER_MODE']==1: self.bayer = [["R","Gr"],["Gb","B"]] elif self.exif.MakerNote['BAYER_MODE']==2: self.bayer = [["B","Gb"],["Gr","R"]] elif self.exif.MakerNote['BAYER_MODE']==3: self.bayer = [["Gb","B"],["R","Gr"]] # open image # TODO: do not open twice (the 1st opening was when reading exif) im = Image.open(filename) # TODO: handle orientation # imageio - takes care about orientation # PIL - does not, which is fine - just need to change bayer mosaic # rotated 90 CW if self.exif.data['Orientation']==6: # keep bayer pass # horizontal mirror then rotated 270 CW elif self.exif.data['Orientation']==5: # change bayer mosaic if self.bayer==[["Gr","R"],["B","Gb"]]: self.bayer = [["B","Gb"],["Gr","R"]] #im = ImageOps.mirror(im) #im = im.rotate(270) # TODO: change bayer due to flips here # 'L' is 8 bit mode #self.image = scipy.misc.imread(filename, flatten=False, mode='L') #im_arr = imageio.imread(filename) #im = im.rotate(90) #im = ImageOps.mirror(im) #im_arr = im.getdata() im_arr = np.fromstring(im.tobytes(), dtype=np.uint8) im_arr = im_arr.reshape((im.size[1], im.size[0])) print(im_arr.shape) self.image = im_arr self.h, self.w = self.image.shape # in JP4 format the 16x16 block is arranged in 8x32, # where color channels are grouped in 8x8 (8x8Gr, 8x8R, 8x8B, 8x8Gb) # the 1st line of 8x32 blocks is the left half of the image # the 2nd line of 8x32 blocks is the right half def deblock(self): if not self.need_deblocking: return 0 # tmp copy I = np.copy(self.image) W = self.w H = self.h bayer = self.bayer # 16x16 block block = np.zeros((16,16)) # 16xW stripe = np.zeros((16,W)) I = np.reshape(I,(H>>4,16,W)) for i in range(I.shape[0]): # stripe 16xW stripe = np.copy(I[i]) for j in range(0,W,16): if j<W/2: k = 0 l = 2*j else: k = 8 l = int(2*(j-W/2)) # gr r b gb block[0::2,0::2] = stripe[k:k+8,l+ 0:l+ 0+8] block[0::2,1::2] = stripe[k:k+8,l+ 8:l+ 8+8] block[1::2,0::2] = stripe[k:k+8,l+16:l+16+8] block[1::2,1::2] = stripe[k:k+8,l+24:l+24+8] I[i,0:16,j:j+16] = block I = np.reshape(I,(H,W)) self.image = I # gamma encode, gamma correct # incorrect, do not use def apply_gamma(self): print("apply gamma") #I = np.copy(self.image.astype(np.int32)) I = np.copy(self.image) gain_r = self.exif.MakerNote['GAIN_R'] gain_gr = self.exif.MakerNote['GAIN_G'] gain_b = self.exif.MakerNote['GAIN_B'] gain_gb = self.exif.MakerNote['GAIN_GB'] I[0::2,0::2] = I[0::2,0::2]**(1/gain_gr) I[0::2,1::2] = I[0::2,1::2]**(1/gain_r) I[1::2,0::2] = I[1::2,0::2]**(1/gain_b) I[1::2,1::2] = I[1::2,1::2]**(1/gain_gb) self.image = I # unapply gamma # incorrect, do not use def linearize(self): print("unapply gamma") I = np.copy(self.image.astype(np.float)) gain_r = self.exif.MakerNote['GAIN_R'] gain_gr = self.exif.MakerNote['GAIN_G'] gain_b = self.exif.MakerNote['GAIN_B'] gain_gb = self.exif.MakerNote['GAIN_GB'] I[0::2,0::2] = I[0::2,0::2]**gain_gr I[0::2,1::2] = I[0::2,1::2]**gain_r I[1::2,0::2] = I[1::2,0::2]**gain_b I[1::2,1::2] = I[1::2,1::2]**gain_gb # OpenCV uses BGR format self.image = I # simple but incorrect, with 2x downsampling # from here: https://groups.google.com/forum/#!topic/pydc1394/KycTwjyBDV0 def demosaic_simple_downsample(self): if not self.need_demosaicing: return 0 # no need in clipping I = np.copy(self.image) R = I[0::2,1::2] G = I[1::2,1::2]/2 + I[0::2,0::2]/2 B = I[1::2,0::2] # OpenCV uses BGR format I = np.dstack([B,G,R]) self.image = I def demosaic_bilinear_slow_and_true(self): if not self.need_demosaicing: return 0 bayer = self.bayer I = np.copy(self.image.astype(np.float)) R = np.copy(I) G = np.copy(I) B = np.copy(I) h,w = I.shape for j in range(h): for i in range(w): jt = j+1 if j==0 else j-1 jb = j-1 if j==h-1 else j+1 il = i+1 if i==0 else i-1 ir = i-1 if i==w-1 else i+1 px_0 = I[j,i] px_t, px_b, px_l, px_r = I[jt,i], I[jb,i], I[j,il], I[j,ir] px_tl,px_tr,px_bl,px_br = I[jt,il],I[jt,ir],I[jb,il],I[jb,ir] if (bayer[j%2][i%2]=="Gr"): R[j,i] = (px_l + px_r)/2 G[j,i] = (4*px_0 + px_tl + px_tr + px_bl + px_br)/8 B[j,i] = (px_t + px_b)/2 elif (bayer[j%2][i%2]=="R"): R[j,i] = px_0 G[j,i] = (px_t + px_b + px_l + px_r)/4 B[j,i] = (px_tl + px_tr + px_bl + px_br)/4 elif (bayer[j%2][i%2]=="B"): R[j,i] = (px_tl + px_tr + px_bl + px_br)/4 G[j,i] = (px_t + px_b + px_l + px_r)/4 B[j,i] = px_0 elif (bayer[j%2][i%2]=="Gb"): R[j,i] = (px_t + px_b)/2 G[j,i] = (4*px_0 + px_tl + px_tr + px_bl + px_br)/8 B[j,i] = (px_l + px_r)/2 # OpenCV uses BGR format I = np.dstack([B,G,R]) self.image = I return I def demosaic_bilinear(self): if not self.need_demosaicing: return 0 bayer = self.bayer # int16 is enough, int32 - native and faster? need to test I = np.copy(self.image.astype(np.int32)) R = np.copy(I) G = np.copy(I) B = np.copy(I) # this depends on bayer p00 = I[0::2,0::2] p01 = I[0::2,1::2] p10 = I[1::2,0::2] p11 = I[1::2,1::2] if bayer==[["Gr","R"],["B","Gb"]]: R[0::2,0::2] = (p01 + nalc(p01))/2 G[0::2,0::2] = (4*p00 + p11 + naur(p11) + nalc(p11 + naur(p11)))/8 B[0::2,0::2] = (p10 + naur(p10))/2 R[0::2,1::2] = p01 G[0::2,1::2] = (p00 + narc(p00) + p11 + naur(p11))/4 B[0::2,1::2] = (p10 + naur(p10) + narc(p10 + naur(p10)))/4 R[1::2,0::2] = (p01 + nabr(p01) + nalc(p01 + nabr(p01)))/4 G[1::2,0::2] = (p11 + nalc(p11) + p00 + nabr(p00))/4 B[1::2,0::2] = p10 R[1::2,1::2] = (p01 + nabr(p01))/2 G[1::2,1::2] = (4*p11 + p00 + nabr(p00) + narc(p00 + nabr(p00)))/8 B[1::2,1::2] = (p10 + narc(p10))/2 elif bayer==[["R","Gr"],["Gb","B"]]: R[0::2,0::2] = p00 G[0::2,0::2] = (p01 + nalc(p01) + p10 + naur(p10))/4 B[0::2,0::2] = (p11 + naur(p11) + nalc(p11 + naur(p11)))/4 R[0::2,1::2] = (p00 + narc(p00))/2 G[0::2,1::2] = (4*p01 + p10 + naur(p10) + narc(p10 + naur(p10)))/8 B[0::2,1::2] = (p11 + naur(p11))/2 R[1::2,0::2] = (p00 + nabr(p00))/2 G[1::2,0::2] = (4*p10 + p01 + nabr(p01) + nalc(p01 + nabr(p01)))/8 B[1::2,0::2] = (p11 + nalc(p11))/2 R[1::2,1::2] = (p00 + nabr(p00) + narc(p00 + nabr(p00)))/4 G[1::2,1::2] = (p01 + nabr(p01) + p10 + narc(p10))/4 B[1::2,1::2] = p11 elif bayer==[["B","Gb"],["Gr","R"]]: R[0::2,0::2] = (p11 + naur(p11) + nalc(p11 + naur(p11)))/4 G[0::2,0::2] = (p10 + naur(p10) + p01 + nalc(p01))/4 B[0::2,0::2] = p00 R[0::2,1::2] = (p11 + naur(p11))/2 G[0::2,1::2] = (4*p01 + p10 + naur(p10) + narc(p10 + naur(p10)))/8 B[0::2,1::2] = (p00 + narc(p00))/2 R[1::2,0::2] = (p11 + nalc(p11))/2 G[1::2,0::2] = (4*p10 + p01 + nabr(p01) + nalc(p01 + nabr(p01)))/8 B[1::2,0::2] = (p00 + nabr(p00))/2 R[1::2,1::2] = p11 G[1::2,1::2] = (p10 + narc(p10) + p01 + nabr(p01))/4 B[1::2,1::2] = (p00 + nabr(p00) + narc(p00 + nabr(p00)))/4 # OpenCV uses BGR format I = np.dstack([B,G,R]) self.image = I return I def saturation(self): I = self.image.astype(np.int32) # use '2' for now s = 2 r = I[:,:,2] g = I[:,:,1] b = I[:,:,0] Y = 0.299*r+0.5870*g+ 0.144*b Cb = 128+s*(-0.1687*r-0.3313*g+ 0.500*b) Cr = 128+s*( 0.5*r-0.4187*g-0.0813*b) Cb[Cb<0]=0 Cb[Cb>255]=255 Cr[Cr<0]=0 Cr[Cr>255]=255 r = Y + 1.402*(Cr-128) g = Y - 0.34414*(Cb-128)-0.71414*(Cr-128) b = Y + 1.772*(Cb-128) r[r<0]=0 r[r>255]=255 g[g<0]=0 g[g>255]=255 b[b<0]=0 b[b>255]=255 # OpenCV uses BGR format I = np.dstack([b,g,r]) self.image = I return I Loading
elphel_exif.py 0 → 100644 +176 −0 Original line number Original line Diff line number Diff line __author__ = "Oleg Dzhimiev" __copyright__ = "Copyright (C) 2018 Elphel Inc." __license__ = "GPL-3.0+" __email__ = "oleg@elphel.com" ''' Example usage: test.py: import elphel_exif info = elphel_exif.Exif("test.jpeg") print(info) print(info.MakerNote) print(info.exif.toString()) ''' from PIL import Image from PIL.ExifTags import TAGS, GPSTAGS # Might be needed by some applications like: # panoramic of Eyesis4Pi, composite images def elphel_add_to_PIL_TAGS(): TAGS[0x0129] = 'Page' TAGS[0xC62F] = 'Camera Serial Number' # Open file and Parse def elphel_get_exif(f): ret = {} i = Image.open(f) info = i._getexif() elphel_add_to_PIL_TAGS() for tag, value in info.items(): decoded = TAGS.get(tag, tag) if decoded == 'GPSInfo': gret = {} for gtag,gval in value.items(): gdecoded = GPSTAGS.get(gtag,gtag) gret[gdecoded] = gval value = gret if decoded == 'MakerNote': value = elphel_MakerNote_parse(value) ret[decoded] = value return ret # Init with default values def elphel_MakerNote_defaults(): ret = {} # defaults ret['COLOR_MODE'] = 0 return ret # Based on JP46_Reader_camera.java's jp46Reorder() def elphel_MakerNote_parse(MakerNote): ret = {} ret = elphel_MakerNote_defaults() ret['GAIN_R' ] = MakerNote[0]/65536.0 ret['GAIN_G'] = MakerNote[1]/65536.0 ret['GAIN_GB'] = MakerNote[2]/65536.0 ret['GAIN_B'] = MakerNote[3]/65536.0 ret['BLACK_R'] = (MakerNote[4]>>24)/256.0 ret['BLACK_G'] = (MakerNote[5]>>24)/256.0 ret['BLACK_GB'] = (MakerNote[6]>>24)/256.0 ret['BLACK_B'] = (MakerNote[7]>>24)/256.0 ret['GAMMA_R'] = ((MakerNote[4]>>16)&0xff)/100.0 ret['GAMMA_G'] = ((MakerNote[5]>>16)&0xff)/100.0 ret['GAMMA_GB'] = ((MakerNote[6]>>16)&0xff)/100.0 ret['GAMMA_B'] = ((MakerNote[7]>>16)&0xff)/100.0 ret['GAMMA_SCALE_R'] = MakerNote[4]&0xffff ret['GAMMA_SCALE_G'] = MakerNote[5]&0xffff ret['GAMMA_SCALE_GB'] = MakerNote[6]&0xffff ret['GAMMA_SCALE_B'] = MakerNote[7]&0xffff if len(MakerNote)>=14: if (MakerNote[10]&0xc0000000)!=0: composite = 1 else: composite = 0 ret['COMPOSITE'] = composite if composite: ret['HEIGHT1'] = MakerNote[11]&0xffff ret['BLANK1'] = (MakerNote[11]>>16)&0xffff ret['HEIGHT2'] = MakerNote[12]&0xffff ret['BLANK2'] = (MakerNote[12]>>16)&0xffff ret['HEIGHT3'] = (MakerNote[9]>>16)-ret['HEIGHT1']-ret['BLANK1']-ret['HEIGHT2']-ret['BLANK2'] ret['FLIPH1'] = (MakerNote[10]>>24)&0x1 ret['FLIPV1'] = (MakerNote[10]>>25)&0x1 ret['FLIPH2'] = (MakerNote[10]>>26)&0x1 ret['FLIPV2'] = (MakerNote[10]>>27)&0x1 ret['FLIPH3'] = (MakerNote[10]>>28)&0x1 ret['FLIPV3'] = (MakerNote[10]>>29)&0x1 ret['PORTRAIT'] = (MakerNote[13]>>7)&0x1 ret['YTABLEFORC'] = (MakerNote[13]>>15)&0x1 ret['QUALITY'] = MakerNote[13]&0x7f ret['CQUALITY'] = (MakerNote[13]>>8)&0x7f if ret['CQUALITY']==0: ret['CQUALITY'] = ret['QUALITY'] ret['CORING_INDEX_Y'] = (MakerNote[13]>>16)&0x7f ret['CORING_INDEX_C'] = (MakerNote[13]>>24)&0x7f if ret['CORING_INDEX_C']==0: ret['CORING_INDEX_C'] = ret['CORING_INDEX_Y'] if len(MakerNote)>=12: ret['WOI_LEFT'] = MakerNote[8]&0xffff ret['WOI_WIDTH'] = MakerNote[8]>>16 ret['WOI_TOP'] = MakerNote[9]&0xffff ret['WOI_HEIGHT'] = MakerNote[9]>>16 ret['FLIPH'] = MakerNote[10]&0x1 ret['FLIPV'] = (MakerNote[10]>>1)&0x1 ret['BAYER_MODE'] = (MakerNote[10]>>2)&0x3 ret['COLOR_MODE'] = (MakerNote[10]>>4)&0x0f ret['DCM_HOR'] = (MakerNote[10]>>8)&0x0f ret['DCM_VERT'] = (MakerNote[10]>>12)&0x0f ret['BIN_HOR'] = (MakerNote[10]>>16)&0x0f ret['BIN_VERT'] = (MakerNote[10]>>20)&0x0f # HW specific temperature readings if len(MakerNote)>=16: ret['TEMP1'] = (MakerNote[14]>> 0)&0xffff ret['TEMP2'] = (MakerNote[14]>>16)&0xffff ret['TEMP3'] = (MakerNote[14]>> 0)&0xffff ret['TEMP4'] = (MakerNote[14]>>16)&0xffff return ret # Class class Exif: def __init__(self,f): self.f = f self.data = elphel_get_exif(f) self.MakerNote = self.data['MakerNote'] #self.color_mode = self.data['MakerNote']['COLOR_MODE'] def toString(self): res = '' for k in self.data: if k!='MakerNote': res += str(k)+": "+str(self.data[k])+"\n" res += "MakerNote: \n" for k in self.data['MakerNote']: res += " "+str(k)+": "+str(self.data['MakerNote'][k])+"\n" return res[:-1]
jp4-to-png.py 0 → 100644 +89 −0 Original line number Original line Diff line number Diff line #!/usr/bin/env python3 import jp4 #import cv2 import numpy as np import sys from PIL import Image, ImageOps try: filename = sys.argv[1] except IndexError: filename = "test.jp4" img = jp4.JP4(filename) # print exif #print(img.exif.toString()) # deblock if JP4 (auto read from exif) skip if other img.deblock() img.demosaic_bilinear() img.saturation() I = img.image[...,[2,1,0]].astype(np.uint8) res = Image.fromarray(I) #res.save("result.png") # also works #res.save("result.jpeg") # for Eyesis4pi exif = img.exif mn = exif.MakerNote #print(exif.MakerNote) # common # WOI_WIDTH # FLIPH # FLIPV # PORTRAIT # COMPOSITE # individual # HEIGHTx, 1..3 # FLIPHx, 1..3 # FLIPVx, 1..3 # BLANKx, only 1 and 2 if mn['COMPOSITE']: W = mn['WOI_WIDTH'] H = mn['WOI_HEIGHT'] FH = mn['FLIPH'] FV = mn['FLIPV'] #if FV: # I = v_offset = 0 for i in range(1,4): try: height = mn['HEIGHT'+str(i)] #fliph = mn['FLIPH'+str(i)] flipv = mn['FLIPV'+str(i)] except KeyError: # break for '_9' break try: blank = mn['BLANK'+str(i)] except KeyError: blank = 0 crop = res.crop((0,v_offset,W,v_offset+height)) if flipv: crop = ImageOps.flip(crop) # if eyesis4pi, 1&2 - rotate 90CW, 3 - 90CCW if i<3: crop = crop.rotate(-90,expand=1) else: crop = crop.rotate(90,expand=1) crop.save("result_"+str(i)+".jpeg") v_offset += height + blank
jp4.py 0 → 100644 +398 −0 Original line number Original line Diff line number Diff line __copyright__ = "Copyright 2018, Elphel, Inc." __license__ = "GPL-3.0+" __maintainer__ = "Oleg Dzhimiev" __email__ = "oleg@elphel.com" import numpy as np #import imageio import elphel_exif from PIL import Image, ImageOps ''' Python image manipulation libs: Lib Reads image as Exif Image manipulations Comment Pillow(PIL) Image + + likes np.uint8 arrays imageio numpy.array - - almost useless opencv numpy.array - + Recommendations: ''' # clear from code def get_exif(filename): exif = elphel_exif.Exif(filename) # exif.f - filename # exif.data - parsed (as dict) exif with parsed MakeNote # exif.MakerNote - parsed as dict return exif # clear from code def naur(a): b0 = a[0] b0 = np.reshape(b0,(1,b0.shape[0])) b = a[:-1] b = np.concatenate((b0,b),axis=0) return b # clear from code def nabr(a): b0 = a[-1] b0 = np.reshape(b0,(1,b0.shape[0])) b = a[1:] b = np.concatenate((b,b0),axis=0) return b # clear from code def nalc(a): b0 = a[::,0] b0 = np.reshape(b0,(b0.shape[0],1)) b = a[::,:-1] b = np.concatenate((b0,b),axis=1) return b # clear from code def narc(a): b0 = a[::,-1] b0 = np.reshape(b0,(b0.shape[0],1)) b = a[::,1:] b = np.concatenate((b,b0),axis=1) return b class JP4: def __init__(self,filename="test.jp4"): # open image using PIL #i = Image.open(filename) #self.px = i.load() #self.px = scipy.misc.imread(filename, flatten=False, mode='RGB') #self.h, self.w, self.chn = self.px.shape self.exif = get_exif(filename) # not JP4 - no deblock self.need_deblocking = True if self.exif.MakerNote['COLOR_MODE']==5 else False self.need_demosaicing = True if self.need_deblocking else False #print(self.exif.color_mode) #print(self.exif.MakerNote['GAIN_R']) #print(self.exif.MakerNote['GAIN_G']) #print(self.exif.MakerNote['GAIN_B']) #print(self.exif.MakerNote['GAIN_GB']) if self.exif.MakerNote['BAYER_MODE']==0: self.bayer = [["Gr","R"],["B","Gb"]] elif self.exif.MakerNote['BAYER_MODE']==1: self.bayer = [["R","Gr"],["Gb","B"]] elif self.exif.MakerNote['BAYER_MODE']==2: self.bayer = [["B","Gb"],["Gr","R"]] elif self.exif.MakerNote['BAYER_MODE']==3: self.bayer = [["Gb","B"],["R","Gr"]] # open image # TODO: do not open twice (the 1st opening was when reading exif) im = Image.open(filename) # TODO: handle orientation # imageio - takes care about orientation # PIL - does not, which is fine - just need to change bayer mosaic # rotated 90 CW if self.exif.data['Orientation']==6: # keep bayer pass # horizontal mirror then rotated 270 CW elif self.exif.data['Orientation']==5: # change bayer mosaic if self.bayer==[["Gr","R"],["B","Gb"]]: self.bayer = [["B","Gb"],["Gr","R"]] #im = ImageOps.mirror(im) #im = im.rotate(270) # TODO: change bayer due to flips here # 'L' is 8 bit mode #self.image = scipy.misc.imread(filename, flatten=False, mode='L') #im_arr = imageio.imread(filename) #im = im.rotate(90) #im = ImageOps.mirror(im) #im_arr = im.getdata() im_arr = np.fromstring(im.tobytes(), dtype=np.uint8) im_arr = im_arr.reshape((im.size[1], im.size[0])) print(im_arr.shape) self.image = im_arr self.h, self.w = self.image.shape # in JP4 format the 16x16 block is arranged in 8x32, # where color channels are grouped in 8x8 (8x8Gr, 8x8R, 8x8B, 8x8Gb) # the 1st line of 8x32 blocks is the left half of the image # the 2nd line of 8x32 blocks is the right half def deblock(self): if not self.need_deblocking: return 0 # tmp copy I = np.copy(self.image) W = self.w H = self.h bayer = self.bayer # 16x16 block block = np.zeros((16,16)) # 16xW stripe = np.zeros((16,W)) I = np.reshape(I,(H>>4,16,W)) for i in range(I.shape[0]): # stripe 16xW stripe = np.copy(I[i]) for j in range(0,W,16): if j<W/2: k = 0 l = 2*j else: k = 8 l = int(2*(j-W/2)) # gr r b gb block[0::2,0::2] = stripe[k:k+8,l+ 0:l+ 0+8] block[0::2,1::2] = stripe[k:k+8,l+ 8:l+ 8+8] block[1::2,0::2] = stripe[k:k+8,l+16:l+16+8] block[1::2,1::2] = stripe[k:k+8,l+24:l+24+8] I[i,0:16,j:j+16] = block I = np.reshape(I,(H,W)) self.image = I # gamma encode, gamma correct # incorrect, do not use def apply_gamma(self): print("apply gamma") #I = np.copy(self.image.astype(np.int32)) I = np.copy(self.image) gain_r = self.exif.MakerNote['GAIN_R'] gain_gr = self.exif.MakerNote['GAIN_G'] gain_b = self.exif.MakerNote['GAIN_B'] gain_gb = self.exif.MakerNote['GAIN_GB'] I[0::2,0::2] = I[0::2,0::2]**(1/gain_gr) I[0::2,1::2] = I[0::2,1::2]**(1/gain_r) I[1::2,0::2] = I[1::2,0::2]**(1/gain_b) I[1::2,1::2] = I[1::2,1::2]**(1/gain_gb) self.image = I # unapply gamma # incorrect, do not use def linearize(self): print("unapply gamma") I = np.copy(self.image.astype(np.float)) gain_r = self.exif.MakerNote['GAIN_R'] gain_gr = self.exif.MakerNote['GAIN_G'] gain_b = self.exif.MakerNote['GAIN_B'] gain_gb = self.exif.MakerNote['GAIN_GB'] I[0::2,0::2] = I[0::2,0::2]**gain_gr I[0::2,1::2] = I[0::2,1::2]**gain_r I[1::2,0::2] = I[1::2,0::2]**gain_b I[1::2,1::2] = I[1::2,1::2]**gain_gb # OpenCV uses BGR format self.image = I # simple but incorrect, with 2x downsampling # from here: https://groups.google.com/forum/#!topic/pydc1394/KycTwjyBDV0 def demosaic_simple_downsample(self): if not self.need_demosaicing: return 0 # no need in clipping I = np.copy(self.image) R = I[0::2,1::2] G = I[1::2,1::2]/2 + I[0::2,0::2]/2 B = I[1::2,0::2] # OpenCV uses BGR format I = np.dstack([B,G,R]) self.image = I def demosaic_bilinear_slow_and_true(self): if not self.need_demosaicing: return 0 bayer = self.bayer I = np.copy(self.image.astype(np.float)) R = np.copy(I) G = np.copy(I) B = np.copy(I) h,w = I.shape for j in range(h): for i in range(w): jt = j+1 if j==0 else j-1 jb = j-1 if j==h-1 else j+1 il = i+1 if i==0 else i-1 ir = i-1 if i==w-1 else i+1 px_0 = I[j,i] px_t, px_b, px_l, px_r = I[jt,i], I[jb,i], I[j,il], I[j,ir] px_tl,px_tr,px_bl,px_br = I[jt,il],I[jt,ir],I[jb,il],I[jb,ir] if (bayer[j%2][i%2]=="Gr"): R[j,i] = (px_l + px_r)/2 G[j,i] = (4*px_0 + px_tl + px_tr + px_bl + px_br)/8 B[j,i] = (px_t + px_b)/2 elif (bayer[j%2][i%2]=="R"): R[j,i] = px_0 G[j,i] = (px_t + px_b + px_l + px_r)/4 B[j,i] = (px_tl + px_tr + px_bl + px_br)/4 elif (bayer[j%2][i%2]=="B"): R[j,i] = (px_tl + px_tr + px_bl + px_br)/4 G[j,i] = (px_t + px_b + px_l + px_r)/4 B[j,i] = px_0 elif (bayer[j%2][i%2]=="Gb"): R[j,i] = (px_t + px_b)/2 G[j,i] = (4*px_0 + px_tl + px_tr + px_bl + px_br)/8 B[j,i] = (px_l + px_r)/2 # OpenCV uses BGR format I = np.dstack([B,G,R]) self.image = I return I def demosaic_bilinear(self): if not self.need_demosaicing: return 0 bayer = self.bayer # int16 is enough, int32 - native and faster? need to test I = np.copy(self.image.astype(np.int32)) R = np.copy(I) G = np.copy(I) B = np.copy(I) # this depends on bayer p00 = I[0::2,0::2] p01 = I[0::2,1::2] p10 = I[1::2,0::2] p11 = I[1::2,1::2] if bayer==[["Gr","R"],["B","Gb"]]: R[0::2,0::2] = (p01 + nalc(p01))/2 G[0::2,0::2] = (4*p00 + p11 + naur(p11) + nalc(p11 + naur(p11)))/8 B[0::2,0::2] = (p10 + naur(p10))/2 R[0::2,1::2] = p01 G[0::2,1::2] = (p00 + narc(p00) + p11 + naur(p11))/4 B[0::2,1::2] = (p10 + naur(p10) + narc(p10 + naur(p10)))/4 R[1::2,0::2] = (p01 + nabr(p01) + nalc(p01 + nabr(p01)))/4 G[1::2,0::2] = (p11 + nalc(p11) + p00 + nabr(p00))/4 B[1::2,0::2] = p10 R[1::2,1::2] = (p01 + nabr(p01))/2 G[1::2,1::2] = (4*p11 + p00 + nabr(p00) + narc(p00 + nabr(p00)))/8 B[1::2,1::2] = (p10 + narc(p10))/2 elif bayer==[["R","Gr"],["Gb","B"]]: R[0::2,0::2] = p00 G[0::2,0::2] = (p01 + nalc(p01) + p10 + naur(p10))/4 B[0::2,0::2] = (p11 + naur(p11) + nalc(p11 + naur(p11)))/4 R[0::2,1::2] = (p00 + narc(p00))/2 G[0::2,1::2] = (4*p01 + p10 + naur(p10) + narc(p10 + naur(p10)))/8 B[0::2,1::2] = (p11 + naur(p11))/2 R[1::2,0::2] = (p00 + nabr(p00))/2 G[1::2,0::2] = (4*p10 + p01 + nabr(p01) + nalc(p01 + nabr(p01)))/8 B[1::2,0::2] = (p11 + nalc(p11))/2 R[1::2,1::2] = (p00 + nabr(p00) + narc(p00 + nabr(p00)))/4 G[1::2,1::2] = (p01 + nabr(p01) + p10 + narc(p10))/4 B[1::2,1::2] = p11 elif bayer==[["B","Gb"],["Gr","R"]]: R[0::2,0::2] = (p11 + naur(p11) + nalc(p11 + naur(p11)))/4 G[0::2,0::2] = (p10 + naur(p10) + p01 + nalc(p01))/4 B[0::2,0::2] = p00 R[0::2,1::2] = (p11 + naur(p11))/2 G[0::2,1::2] = (4*p01 + p10 + naur(p10) + narc(p10 + naur(p10)))/8 B[0::2,1::2] = (p00 + narc(p00))/2 R[1::2,0::2] = (p11 + nalc(p11))/2 G[1::2,0::2] = (4*p10 + p01 + nabr(p01) + nalc(p01 + nabr(p01)))/8 B[1::2,0::2] = (p00 + nabr(p00))/2 R[1::2,1::2] = p11 G[1::2,1::2] = (p10 + narc(p10) + p01 + nabr(p01))/4 B[1::2,1::2] = (p00 + nabr(p00) + narc(p00 + nabr(p00)))/4 # OpenCV uses BGR format I = np.dstack([B,G,R]) self.image = I return I def saturation(self): I = self.image.astype(np.int32) # use '2' for now s = 2 r = I[:,:,2] g = I[:,:,1] b = I[:,:,0] Y = 0.299*r+0.5870*g+ 0.144*b Cb = 128+s*(-0.1687*r-0.3313*g+ 0.500*b) Cr = 128+s*( 0.5*r-0.4187*g-0.0813*b) Cb[Cb<0]=0 Cb[Cb>255]=255 Cr[Cr<0]=0 Cr[Cr>255]=255 r = Y + 1.402*(Cr-128) g = Y - 0.34414*(Cb-128)-0.71414*(Cr-128) b = Y + 1.772*(Cb-128) r[r<0]=0 r[r>255]=255 g[g<0]=0 g[g>255]=255 b[b<0]=0 b[b>255]=255 # OpenCV uses BGR format I = np.dstack([b,g,r]) self.image = I return I