summaryrefslogtreecommitdiff
path: root/utils/raspberrypi/ctt/ctt_image_load.py
diff options
context:
space:
mode:
authorNaushir Patuck <naush@raspberrypi.com>2020-05-03 16:49:53 +0100
committerLaurent Pinchart <laurent.pinchart@ideasonboard.com>2020-05-11 23:54:45 +0300
commitc01cfe14f5540ba96b458088185ac7ae90bb3534 (patch)
treef9112e0195de83ea1b20cf81cb62144cd50174f9 /utils/raspberrypi/ctt/ctt_image_load.py
parent0db2c8dc75e466e7648dc1b95380495c6a126349 (diff)
libcamera: utils: Raspberry Pi Camera Tuning Tool
Initial implementation of the Raspberry Pi (BCM2835) Camera Tuning Tool. All code is licensed under the BSD-2-Clause terms. Copyright (c) 2019-2020 Raspberry Pi Trading Ltd. Signed-off-by: Naushir Patuck <naush@raspberrypi.com> Acked-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
Diffstat (limited to 'utils/raspberrypi/ctt/ctt_image_load.py')
-rw-r--r--utils/raspberrypi/ctt/ctt_image_load.py428
1 files changed, 428 insertions, 0 deletions
diff --git a/utils/raspberrypi/ctt/ctt_image_load.py b/utils/raspberrypi/ctt/ctt_image_load.py
new file mode 100644
index 00000000..dd7adb16
--- /dev/null
+++ b/utils/raspberrypi/ctt/ctt_image_load.py
@@ -0,0 +1,428 @@
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (C) 2019-2020, Raspberry Pi (Trading) Limited
+#
+# ctt_image_load.py - camera tuning tool image loading
+
+from ctt_tools import *
+from ctt_macbeth_locator import *
+import json
+import pyexiv2 as pyexif
+import rawpy as raw
+
+
+"""
+Image class load image from raw data and extracts metadata.
+
+Once image is extracted from data, it finds 24 16x16 patches for each
+channel, centred at the macbeth chart squares
+"""
+class Image:
+ def __init__(self,buf):
+ self.buf = buf
+ self.patches = None
+ self.saturated = False
+
+ '''
+ obtain metadata from buffer
+ '''
+ def get_meta(self):
+ self.ver = ba_to_b(self.buf[4:5])
+ self.w = ba_to_b(self.buf[0xd0:0xd2])
+ self.h = ba_to_b(self.buf[0xd2:0xd4])
+ self.pad = ba_to_b(self.buf[0xd4:0xd6])
+ self.fmt = self.buf[0xf5]
+ self.sigbits = 2*self.fmt + 4
+ self.pattern = self.buf[0xf4]
+ self.exposure = ba_to_b(self.buf[0x90:0x94])
+ self.againQ8 = ba_to_b(self.buf[0x94:0x96])
+ self.againQ8_norm = self.againQ8/256
+ camName = self.buf[0x10:0x10+128]
+ camName_end = camName.find(0x00)
+ self.camName = self.buf[0x10:0x10+128][:camName_end].decode()
+
+ """
+ Channel order depending on bayer pattern
+ """
+ bayer_case = {
+ 0 : (0,1,2,3), #red
+ 1 : (2,0,3,1), #green next to red
+ 2 : (3,2,1,0), #green next to blue
+ 3 : (1,0,3,2), #blue
+ 128 : (0,1,2,3) #arbitrary order for greyscale casw
+ }
+ self.order = bayer_case[self.pattern]
+
+ '''
+ manual blacklevel - not robust
+ '''
+ if 'ov5647' in self.camName:
+ self.blacklevel = 16
+ else:
+ self.blacklevel = 64
+ self.blacklevel_16 = self.blacklevel << (6)
+ return 1
+
+ '''
+ print metadata for debug
+ '''
+ def print_meta(self):
+ print('\nData:')
+ print(' ver = {}'.format(self.ver))
+ print(' w = {}'.format(self.w))
+ print(' h = {}'.format(self.h))
+ print(' pad = {}'.format(self.pad))
+ print(' fmt = {}'.format(self.fmt))
+ print(' sigbits = {}'.format(self.sigbits))
+ print(' pattern = {}'.format(self.pattern))
+ print(' exposure = {}'.format(self.exposure))
+ print(' againQ8 = {}'.format(self.againQ8))
+ print(' againQ8_norm = {}'.format(self.againQ8_norm))
+ print(' camName = {}'.format(self.camName))
+ print(' blacklevel = {}'.format(self.blacklevel))
+ print(' blacklevel_16 = {}'.format(self.blacklevel_16))
+
+ return 1
+
+ """
+ get image from raw scanline data
+ """
+ def get_image(self,raw):
+ self.dptr = []
+ """
+ check if data is 10 or 12 bits
+ """
+ if self.sigbits == 10:
+ """
+ calc length of scanline
+ """
+ lin_len = ((((((self.w+self.pad+3)>>2)) * 5)+31)>>5) * 32
+ """
+ stack scan lines into matrix
+ """
+ raw = np.array(raw).reshape(-1,lin_len).astype(np.int64)[:self.h,...]
+ """
+ separate 5 bits in each package, stopping when w is satisfied
+ """
+ ba0 = raw[...,0:5*((self.w+3)>>2):5]
+ ba1 = raw[...,1:5*((self.w+3)>>2):5]
+ ba2 = raw[...,2:5*((self.w+3)>>2):5]
+ ba3 = raw[...,3:5*((self.w+3)>>2):5]
+ ba4 = raw[...,4:5*((self.w+3)>>2):5]
+ """
+ assemble 10 bit numbers
+ """
+ ch0 = np.left_shift((np.left_shift(ba0,2) + (ba4%4)),6)
+ ch1 = np.left_shift((np.left_shift(ba1,2) + (np.right_shift(ba4,2)%4)),6)
+ ch2 = np.left_shift((np.left_shift(ba2,2) + (np.right_shift(ba4,4)%4)),6)
+ ch3 = np.left_shift((np.left_shift(ba3,2) + (np.right_shift(ba4,6)%4)),6)
+ """
+ interleave bits
+ """
+ mat = np.empty((self.h,self.w),dtype=ch0.dtype)
+
+ mat[...,0::4] = ch0
+ mat[...,1::4] = ch1
+ mat[...,2::4] = ch2
+ mat[...,3::4] = ch3
+
+ """
+ There is som eleaking memory somewhere in the code. This code here
+ seemed to make things good enough that the code would run for
+ reasonable numbers of images, however this is techincally just a
+ workaround. (sorry)
+ """
+ ba0,ba1,ba2,ba3,ba4 = None,None,None,None,None
+ del ba0,ba1,ba2,ba3,ba4
+ ch0,ch1,ch2,ch3 = None,None,None,None
+ del ch0,ch1,ch2,ch3
+
+ """
+ same as before but 12 bit case
+ """
+ elif self.sigbits == 12:
+ lin_len = ((((((self.w+self.pad+1)>>1)) * 3)+31)>>5) * 32
+ raw = np.array(raw).reshape(-1,lin_len).astype(np.int64)[:self.h,...]
+ ba0 = raw[...,0:3*((self.w+1)>>1):3]
+ ba1 = raw[...,1:3*((self.w+1)>>1):3]
+ ba2 = raw[...,2:3*((self.w+1)>>1):3]
+ ch0 = np.left_shift((np.left_shift(ba0,4) + ba2%16),4)
+ ch1 = np.left_shift((np.left_shift(ba1,4) + (np.right_shift(ba2,4))%16),4)
+ mat = np.empty((self.h,self.w),dtype=ch0.dtype)
+ mat[...,0::2] = ch0
+ mat[...,1::2] = ch1
+
+ else:
+ """
+ data is neither 10 nor 12 or incorrect data
+ """
+ print('ERROR: wrong bit format, only 10 or 12 bit supported')
+ return 0
+
+ """
+ separate bayer channels
+ """
+ c0 = mat[0::2,0::2]
+ c1 = mat[0::2,1::2]
+ c2 = mat[1::2,0::2]
+ c3 = mat[1::2,1::2]
+ self.channels = [c0,c1,c2,c3]
+ return 1
+
+ """
+ obtain 16x16 patch centred at macbeth square centre for each channel
+ """
+ def get_patches(self,cen_coords,size=16):
+ """
+ obtain channel widths and heights
+ """
+ ch_w,ch_h = self.w,self.h
+ cen_coords = list(np.array((cen_coords[0])).astype(np.int32))
+ self.cen_coords = cen_coords
+ """
+ squares are ordered by stacking macbeth chart columns from
+ left to right. Some useful patch indices:
+ white = 3
+ black = 23
+ 'reds' = 9,10
+ 'blues' = 2,5,8,20,22
+ 'greens' = 6,12,17
+ greyscale = 3,7,11,15,19,23
+ """
+ all_patches = []
+ for ch in self.channels:
+ ch_patches = []
+ for cen in cen_coords:
+ '''
+ macbeth centre is placed at top left of central 2x2 patch
+ to account for rounding
+ Patch pixels are sorted by pixel brightness so spatial
+ information is lost.
+ '''
+ patch = ch[cen[1]-7:cen[1]+9,cen[0]-7:cen[0]+9].flatten()
+ patch.sort()
+ if patch[-5] == (2**self.sigbits-1)*2**(16-self.sigbits):
+ self.saturated = True
+ ch_patches.append(patch)
+ # print('\nNew Patch\n')
+ all_patches.append(ch_patches)
+ # print('\n\nNew Channel\n\n')
+ self.patches = all_patches
+ return 1
+
+def brcm_load_image(Cam, im_str):
+ """
+ Load image where raw data and metadata is in the BRCM format
+ """
+ try:
+ """
+ create byte array
+ """
+ with open(im_str,'rb') as image:
+ f = image.read()
+ b = bytearray(f)
+ """
+ return error if incorrect image address
+ """
+ except FileNotFoundError:
+ print('\nERROR:\nInvalid image address')
+ Cam.log += '\nWARNING: Invalid image address'
+ return 0
+
+ """
+ return error if problem reading file
+ """
+ if f == None:
+ print('\nERROR:\nProblem reading file')
+ Cam.log += '\nWARNING: Problem readin file'
+ return 0
+
+ # print('\nLooking for EOI and BRCM header')
+ """
+ find end of image followed by BRCM header by turning
+ bytearray into hex string and string matching with regexp
+ """
+ start = -1
+ match = bytearray(b'\xff\xd9@BRCM')
+ match_str = binascii.hexlify(match)
+ b_str = binascii.hexlify(b)
+ """
+ note index is divided by two to go from string to hex
+ """
+ indices = [m.start()//2 for m in re.finditer(match_str,b_str)]
+ # print(indices)
+ try:
+ start = indices[0] + 3
+ except IndexError:
+ print('\nERROR:\nNo Broadcom header found')
+ Cam.log += '\nWARNING: No Broadcom header found!'
+ return 0
+ """
+ extract data after header
+ """
+ # print('\nExtracting data after header')
+ buf = b[start:start+32768]
+ Img = Image(buf)
+ Img.str = im_str
+ # print('Data found successfully')
+
+ """
+ obtain metadata
+ """
+ # print('\nReading metadata')
+ Img.get_meta()
+ Cam.log += '\nExposure : {} us'.format(Img.exposure)
+ Cam.log += '\nNormalised gain : {}'.format(Img.againQ8_norm)
+ # print('Metadata read successfully')
+
+ """
+ obtain raw image data
+ """
+ # print('\nObtaining raw image data')
+ raw = b[start+32768:]
+ Img.get_image(raw)
+ """
+ delete raw to stop memory errors
+ """
+ raw = None
+ del raw
+ # print('Raw image data obtained successfully')
+
+ return Img
+
+def dng_load_image(Cam, im_str):
+ try:
+ Img = Image(None)
+
+ # RawPy doesn't load all the image tags that we need, so we use py3exiv2
+ metadata = pyexif.ImageMetadata(im_str)
+ metadata.read()
+
+ Img.ver = 100 # random value
+ Img.w = metadata['Exif.SubImage1.ImageWidth'].value
+ Img.pad = 0
+ Img.h = metadata['Exif.SubImage1.ImageLength'].value
+ white = metadata['Exif.SubImage1.WhiteLevel'].value
+ Img.sigbits = int(white).bit_length()
+ Img.fmt = (Img.sigbits - 4) // 2
+ Img.exposure = int(metadata['Exif.Photo.ExposureTime'].value*1000000)
+ Img.againQ8 = metadata['Exif.Photo.ISOSpeedRatings'].value*256/100
+ Img.againQ8_norm = Img.againQ8 / 256
+ Img.camName = metadata['Exif.Image.Model'].value
+ Img.blacklevel = int(metadata['Exif.SubImage1.BlackLevel'].value[0])
+ Img.blacklevel_16 = Img.blacklevel << (16 - Img.sigbits)
+ bayer_case = {
+ '0 1 1 2': (0, (0, 1, 2, 3)),
+ '1 2 0 1': (1, (2, 0, 3, 1)),
+ '2 1 1 0': (2, (3, 2, 1, 0)),
+ '1 0 2 1': (3, (1, 0, 3, 2))
+ }
+ cfa_pattern = metadata['Exif.SubImage1.CFAPattern'].value
+ Img.pattern = bayer_case[cfa_pattern][0]
+ Img.order = bayer_case[cfa_pattern][1]
+
+ # Now use RawPy tp get the raw Bayer pixels
+ raw_im = raw.imread(im_str)
+ raw_data = raw_im.raw_image
+ shift = 16 - Img.sigbits
+ c0 = np.left_shift(raw_data[0::2,0::2].astype(np.int64), shift)
+ c1 = np.left_shift(raw_data[0::2,1::2].astype(np.int64), shift)
+ c2 = np.left_shift(raw_data[1::2,0::2].astype(np.int64), shift)
+ c3 = np.left_shift(raw_data[1::2,1::2].astype(np.int64), shift)
+ Img.channels = [c0, c1, c2, c3]
+
+ except:
+ print("\nERROR: failed to load DNG file", im_str)
+ print("Either file does not exist or is incompatible")
+ Cam.log += '\nERROR: DNG file does not exist or is incompatible'
+ raise
+
+ return Img
+
+
+'''
+load image from file location and perform calibration
+check correct filetype
+
+mac boolean is true if image is expected to contain macbeth chart and false
+if not (alsc images don't have macbeth charts)
+'''
+def load_image(Cam,im_str,mac_config=None,show=False,mac=True,show_meta=False):
+ """
+ check image is correct filetype
+ """
+ if '.jpg' in im_str or '.jpeg' in im_str or '.brcm' in im_str or '.dng' in im_str:
+ if '.dng' in im_str:
+ Img = dng_load_image(Cam, im_str)
+ else:
+ Img = brcm_load_image(Cam, im_str)
+ if show_meta:
+ Img.print_meta()
+
+ if mac:
+ """
+ find macbeth centres, discarding images that are too dark or light
+ """
+ av_chan = (np.mean(np.array(Img.channels),axis=0)/(2**16))
+ av_val = np.mean(av_chan)
+ # print(av_val)
+ if av_val < Img.blacklevel_16/(2**16)+1/64:
+ macbeth = None
+ print('\nError: Image too dark!')
+ Cam.log += '\nWARNING: Image too dark!'
+ else:
+ macbeth = find_macbeth(Cam,av_chan,mac_config)
+
+ """
+ if no macbeth found return error
+ """
+ if macbeth == None:
+ print('\nERROR: No macbeth chart found')
+ return 0
+ mac_cen_coords = macbeth[1]
+ # print('\nMacbeth centres located successfully')
+
+ """
+ obtain image patches
+ """
+ # print('\nObtaining image patches')
+ Img.get_patches(mac_cen_coords)
+ if Img.saturated:
+ print('\nERROR: Macbeth patches have saturated')
+ Cam.log += '\nWARNING: Macbeth patches have saturated!'
+ return 0
+
+ """
+ clear memory
+ """
+ Img.buf = None
+ del Img.buf
+
+ # print('Image patches obtained successfully')
+
+ """
+ optional debug
+ """
+ if show and __name__ == '__main__':
+ copy = sum(Img.channels)/2**18
+ copy = np.reshape(copy,(Img.h//2,Img.w//2)).astype(np.float64)
+ copy,_ = reshape(copy,800)
+ represent(copy)
+
+ return Img
+
+ """
+ return error if incorrect filetype
+ """
+ else:
+ # print('\nERROR:\nInvalid file extension')
+ return 0
+
+"""
+bytearray splice to number little endian
+"""
+def ba_to_b(b):
+ total = 0
+ for i in range(len(b)):
+ total += 256**i * b[i]
+ return total