Commit 1213b32b authored by Oleg Dzhimiev's avatar Oleg Dzhimiev
Browse files

initial + splitting composite images

parent 58bb6b94
Loading
Loading
Loading
Loading

elphel_exif.py

0 → 100644
+176 −0
Original line number 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 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 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