summaryrefslogtreecommitdiff
path: root/utils/raspberrypi/ctt/ctt.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.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.py')
-rwxr-xr-xutils/raspberrypi/ctt/ctt.py823
1 files changed, 823 insertions, 0 deletions
diff --git a/utils/raspberrypi/ctt/ctt.py b/utils/raspberrypi/ctt/ctt.py
new file mode 100755
index 00000000..5fe22e14
--- /dev/null
+++ b/utils/raspberrypi/ctt/ctt.py
@@ -0,0 +1,823 @@
+#!/usr/bin/env python3
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (C) 2019, Raspberry Pi (Trading) Limited
+#
+# ctt.py - camera tuning tool
+
+import os
+import sys
+from ctt_image_load import *
+from ctt_ccm import *
+from ctt_awb import *
+from ctt_alsc import *
+from ctt_lux import *
+from ctt_noise import *
+from ctt_geq import *
+from ctt_pretty_print_json import *
+import random
+import json
+import re
+
+"""
+This file houses the camera object, which is used to perform the calibrations.
+The camera object houses all the calibration images as attributes in two lists:
+ - imgs (macbeth charts)
+ - imgs_alsc (alsc correction images)
+Various calibrations are methods of the camera object, and the output is stored
+in a dictionary called self.json.
+Once all the caibration has been completed, the Camera.json is written into a
+json file.
+The camera object initialises its json dictionary by reading from a pre-written
+blank json file. This has been done to avoid reproducing the entire json file
+in the code here, thereby avoiding unecessary clutter.
+"""
+
+"""
+Get the colour and lux values from the strings of each inidvidual image
+"""
+def get_col_lux(string):
+ """
+ Extract colour and lux values from filename
+ """
+ col = re.search('([0-9]+)[kK](\.(jpg|jpeg|brcm|dng)|_.*\.(jpg|jpeg|brcm|dng))$',string)
+ lux = re.search('([0-9]+)[lL](\.(jpg|jpeg|brcm|dng)|_.*\.(jpg|jpeg|brcm|dng))$',string)
+ try:
+ col = col.group(1)
+ except AttributeError:
+ """
+ Catch error if images labelled incorrectly and pass reasonable defaults
+ """
+ return None,None
+ try:
+ lux = lux.group(1)
+ except AttributeError:
+ """
+ Catch error if images labelled incorrectly and pass reasonable defaults
+ Still returns colour if that has been found.
+ """
+ return col,None
+ return int( col ),int( lux )
+
+"""
+Camera object that is the backbone of the tuning tool.
+Input is the desired path of the output json.
+"""
+class Camera:
+ def __init__(self,jfile):
+ self.path = os.path.dirname(os.path.expanduser(__file__)) + '/'
+ if self.path == '/':
+ self.path = ''
+ self.imgs = []
+ self.imgs_alsc = []
+ self.log = 'Log created : '+ time.asctime(time.localtime(time.time()))
+ self.log_separator = '\n'+'-'*70+'\n'
+ self.jf = jfile
+ """
+ initial json dict populated by uncalibrated values
+ """
+ self.json = {
+ "rpi.black_level" : {
+ "black_level" : 4096
+ },
+ "rpi.dpc" : {
+ },
+ "rpi.lux" : {
+ "reference_shutter_speed": 10000,
+ "reference_gain": 1,
+ "reference_aperture": 1.0
+ },
+ "rpi.noise" : {
+ },
+ "rpi.geq" : {
+ },
+ "rpi.sdn": {
+ },
+ "rpi.awb": {
+ "priors" : [
+ {"lux": 0,"prior":[ 2000, 1.0, 3000, 0.0, 13000, 0.0]},
+ {"lux": 800,"prior":[ 2000, 0.0, 6000, 2.0, 13000, 2.0]},
+ {"lux": 1500,"prior":[ 2000, 0.0, 4000, 1.0, 6000, 6.0, 6500, 7.0, 7000, 1.0, 13000, 1.0]}
+ ],
+ "modes" : {
+ "auto" : { "lo" : 2500, "hi" : 8000 },
+ "incandescent" : { "lo" : 2500, "hi" : 3000 },
+ "tungsten" : { "lo" : 3000, "hi" : 3500 },
+ "fluorescent" : { "lo" : 4000, "hi" : 4700 },
+ "indoor" : { "lo" : 3000, "hi" : 5000 },
+ "daylight" : { "lo" : 5500, "hi" : 6500 },
+ "cloudy" : { "lo" : 7000, "hi" : 8600 }
+ },
+ "bayes" : 1
+ },
+ "rpi.agc" : {
+ "metering_modes" : {
+ "centre-weighted" : {
+ "weights" : [3, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0]
+ },
+ "spot" : {
+ "weights" : [2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ },
+ "matrix": {
+ "weights" : [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
+ }
+ },
+ "exposure_modes" : {
+ "normal" : {
+ "shutter" : [100, 10000, 30000, 60000, 120000],
+ "gain" : [1.0, 2.0, 4.0, 6.0, 6.0]
+ },
+ "sport": {
+ "shutter": [ 100, 5000, 10000, 20000, 120000 ],
+ "gain": [ 1.0, 2.0, 4.0, 6.0, 6.0 ]
+ }
+ },
+ "constraint_modes" : {
+ "normal" : [
+ {"bound" : "LOWER", "q_lo" : 0.98, "q_hi" : 1.0, "y_target" : [0, 0.5, 1000, 0.5]}
+ ],
+ "highlight": [
+ { "bound": "LOWER", "q_lo": 0.98, "q_hi": 1.0, "y_target": [ 0, 0.5, 1000, 0.5 ] },
+ { "bound": "UPPER", "q_lo": 0.98, "q_hi": 1.0, "y_target": [ 0, 0.8, 1000, 0.8 ] }
+ ]
+ },
+ "y_target" : [0, 0.16, 1000, 0.165, 10000, 0.17]
+ },
+ "rpi.alsc": {
+ 'omega' : 1.3,
+ 'n_iter' : 100,
+ 'luminance_strength' : 0.7,
+ },
+ "rpi.contrast" : {
+ "ce_enable": 1,
+ "gamma_curve": [
+ 0, 0,
+ 1024, 5040,
+ 2048, 9338,
+ 3072, 12356,
+ 4096, 15312,
+ 5120, 18051,
+ 6144, 20790,
+ 7168, 23193,
+ 8192, 25744,
+ 9216, 27942,
+ 10240, 30035,
+ 11264, 32005,
+ 12288, 33975,
+ 13312, 35815,
+ 14336, 37600,
+ 15360, 39168,
+ 16384, 40642,
+ 18432, 43379,
+ 20480, 45749,
+ 22528, 47753,
+ 24576, 49621,
+ 26624, 51253,
+ 28672, 52698,
+ 30720, 53796,
+ 32768, 54876,
+ 36864, 57012,
+ 40960, 58656,
+ 45056, 59954,
+ 49152, 61183,
+ 53248, 62355,
+ 57344, 63419,
+ 61440, 64476,
+ 65535, 65535
+ ]
+ },
+ "rpi.ccm": {
+ },
+ "rpi.sharpen":{
+ }
+ }
+
+
+ """
+ Perform colour correction calibrations by comparing macbeth patch colours
+ to standard macbeth chart colours.
+ """
+ def ccm_cal(self,do_alsc_colour):
+ if 'rpi.ccm' in self.disable:
+ return 1
+ print('\nStarting CCM calibration')
+ self.log_new_sec('CCM')
+ """
+ if image is greyscale then CCm makes no sense
+ """
+ if self.grey:
+ print('\nERROR: Can\'t do CCM on greyscale image!')
+ self.log += '\nERROR: Cannot perform CCM calibration '
+ self.log += 'on greyscale image!\nCCM aborted!'
+ del self.json['rpi.ccm']
+ return 0
+ a = time.time()
+ """
+ Check if alsc tables have been generated, if not then do ccm without
+ alsc
+ """
+ if (not "rpi.alsc" in self.disable) and do_alsc_colour:
+ """
+ case where ALSC colour has been done, so no errors should be
+ expected...
+ """
+ try:
+ cal_cr_list = self.json['rpi.alsc']['calibrations_Cr']
+ cal_cb_list = self.json['rpi.alsc']['calibrations_Cb']
+ self.log += '\nALSC tables found successfully'
+ except KeyError:
+ cal_cr_list,cal_cb_list=None,None
+ print('WARNING! No ALSC tables found for CCM!')
+ print('Performing CCM calibrations without ALSC correction...')
+ self.log += '\nWARNING: No ALSC tables found.\nCCM calibration '
+ self.log += 'performed without ALSC correction...'
+ else:
+ """
+ case where config options result in CCM done without ALSC colour tables
+ """
+ cal_cr_list,cal_cb_list=None,None
+ self.log += '\nWARNING: No ALSC tables found.\nCCM calibration '
+ self.log += 'performed without ALSC correction...'
+
+ """
+ Do CCM calibration
+ """
+ try:
+ ccms = ccm(self,cal_cr_list,cal_cb_list)
+ except ArithmeticError:
+ print('ERROR: Matrix is singular!\nTake new pictures and try again...')
+ self.log += '\nERROR: Singular matrix encountered during fit!'
+ self.log += '\nCCM aborted!'
+ return 1
+ """
+ Write output to json
+ """
+ self.json['rpi.ccm']['ccms'] = ccms
+ self.log += '\nCCM calibration written to json file'
+ print('Finished CCM calibration')
+
+ """
+ Auto white balance calibration produces a colour curve for
+ various colour temperatures, as well as providing a maximum 'wiggle room'
+ distance from this curve (transverse_neg/pos).
+ """
+ def awb_cal(self,greyworld,do_alsc_colour):
+ if 'rpi.awb' in self.disable:
+ return 1
+ print('\nStarting AWB calibration')
+ self.log_new_sec('AWB')
+ """
+ if image is greyscale then AWB makes no sense
+ """
+ if self.grey:
+ print('\nERROR: Can\'t do AWB on greyscale image!')
+ self.log += '\nERROR: Cannot perform AWB calibration '
+ self.log += 'on greyscale image!\nAWB aborted!'
+ del self.json['rpi.awb']
+ return 0
+ """
+ optional set greyworld (e.g. for noir cameras)
+ """
+ if greyworld:
+ self.json['rpi.awb']['bayes'] = 0
+ self.log += '\nGreyworld set'
+ """
+ Check if alsc tables have been generated, if not then do awb without
+ alsc correction
+ """
+ if (not "rpi.alsc" in self.disable) and do_alsc_colour:
+ try:
+ cal_cr_list = self.json['rpi.alsc']['calibrations_Cr']
+ cal_cb_list = self.json['rpi.alsc']['calibrations_Cb']
+ self.log += '\nALSC tables found successfully'
+ except KeyError:
+ cal_cr_list,cal_cb_list=None,None
+ print('ERROR, no ALSC calibrations found for AWB')
+ print('Performing AWB without ALSC tables')
+ self.log += '\nWARNING: No ALSC tables found.\nAWB calibration '
+ self.log += 'performed without ALSC correction...'
+ else:
+ cal_cr_list,cal_cb_list=None,None
+ self.log += '\nWARNING: No ALSC tables found.\nAWB calibration '
+ self.log += 'performed without ALSC correction...'
+ """
+ call calibration function
+ """
+ plot = "rpi.awb" in self.plot
+ awb_out = awb(self,cal_cr_list,cal_cb_list,plot)
+ ct_curve,transverse_neg,transverse_pos = awb_out
+ """
+ write output to json
+ """
+ self.json['rpi.awb']['ct_curve'] = ct_curve
+ self.json['rpi.awb']['sensitivity_r'] = 1.0
+ self.json['rpi.awb']['sensitivity_b'] = 1.0
+ self.json['rpi.awb']['transverse_pos'] = transverse_pos
+ self.json['rpi.awb']['transverse_neg'] = transverse_neg
+ self.log += '\nAWB calibration written to json file'
+ print('Finished AWB calibration')
+
+ """
+ Auto lens shading correction completely mitigates the effects of lens shading for ech
+ colour channel seperately, and then partially corrects for vignetting.
+ The extent of the correction depends on the 'luminance_strength' parameter.
+ """
+ def alsc_cal(self,luminance_strength,do_alsc_colour):
+ if 'rpi.alsc' in self.disable:
+ return 1
+ print('\nStarting ALSC calibration')
+ self.log_new_sec('ALSC')
+ """
+ check if alsc images have been taken
+ """
+ if len(self.imgs_alsc) == 0:
+ print('\nError:\nNo alsc calibration images found')
+ self.log += '\nERROR: No ALSC calibration images found!'
+ self.log += '\nALSC calibration aborted!'
+ return 1
+ self.json['rpi.alsc']['luminance_strength'] = luminance_strength
+ if self.grey and do_alsc_colour:
+ print('Greyscale camera so only luminance_lut calculated')
+ do_alsc_colour = False
+ self.log += '\nWARNING: ALSC colour correction cannot be done on '
+ self.log += 'greyscale image!\nALSC colour corrections forced off!'
+ """
+ call calibration function
+ """
+ plot = "rpi.alsc" in self.plot
+ alsc_out = alsc_all(self,do_alsc_colour,plot)
+ cal_cr_list,cal_cb_list,luminance_lut,av_corn = alsc_out
+ """
+ write ouput to json and finish if not do_alsc_colour
+ """
+ if not do_alsc_colour:
+ self.json['rpi.alsc']['luminance_lut'] = luminance_lut
+ self.json['rpi.alsc']['n_iter'] = 0
+ self.log += '\nALSC calibrations written to json file'
+ self.log += '\nNo colour calibrations performed'
+ print('Finished ALSC calibrations')
+ return 1
+
+ self.json['rpi.alsc']['calibrations_Cr'] = cal_cr_list
+ self.json['rpi.alsc']['calibrations_Cb'] = cal_cb_list
+ self.json['rpi.alsc']['luminance_lut'] = luminance_lut
+ self.log += '\nALSC colour and luminance tables written to json file'
+
+ """
+ The sigmas determine the strength of the adaptive algorithm, that
+ cleans up any lens shading that has slipped through the alsc. These are
+ determined by measuring a 'worst-case' difference between two alsc tables
+ that are adjacent in colour space. If, however, only one colour
+ temperature has been provided, then this difference can not be computed
+ as only one table is available.
+ To determine the sigmas you would have to estimate the error of an alsc
+ table with only the image it was taken on as a check. To avoid circularity,
+ dfault exaggerated sigmas are used, which can result in too much alsc and
+ is therefore not advised.
+ In general, just take another alsc picture at another colour temperature!
+ """
+
+ if len(self.imgs_alsc) == 1:
+ self.json['rpi.alsc']['sigma'] = 0.005
+ self.json['rpi.alsc']['sigma_Cb'] = 0.005
+ print('\nWarning:\nOnly one alsc calibration found' +
+ '\nStandard sigmas used for adaptive algorithm.')
+ print('Finished ALSC calibrations')
+ self.log += '\nWARNING: Only one colour temperature found in '
+ self.log += 'calibration images.\nStandard sigmas used for adaptive '
+ self.log += 'algorithm!'
+ return 1
+
+ """
+ obtain worst-case scenario residual sigmas
+ """
+ sigma_r,sigma_b = get_sigma(self,cal_cr_list,cal_cb_list)
+ """
+ write output to json
+ """
+ self.json['rpi.alsc']['sigma'] = np.round(sigma_r,5)
+ self.json['rpi.alsc']['sigma_Cb'] = np.round(sigma_b,5)
+ self.log += '\nCalibrated sigmas written to json file'
+ print('Finished ALSC calibrations')
+
+ """
+ Green equalisation fixes problems caused by discrepancies in green
+ channels. This is done by measuring the effect on macbeth chart patches,
+ which ideally would have the same green values throughout.
+ An upper bound linear model is fit, fixing a threshold for the green
+ differences that are corrected.
+ """
+ def geq_cal(self):
+ if 'rpi.geq' in self.disable:
+ return 1
+ print('\nStarting GEQ calibrations')
+ self.log_new_sec('GEQ')
+ """
+ perform calibration
+ """
+ plot = 'rpi.geq' in self.plot
+ slope,offset = geq_fit(self,plot)
+ """
+ write output to json
+ """
+ self.json['rpi.geq']['offset'] = offset
+ self.json['rpi.geq']['slope'] = slope
+ self.log += '\nGEQ calibrations written to json file'
+ print('Finished GEQ calibrations')
+
+ """
+ Lux calibrations allow the lux level of a scene to be estimated by a ratio
+ calculation. Lux values are used in the pipeline for algorithms such as AGC
+ and AWB
+ """
+ def lux_cal(self):
+ if 'rpi.lux' in self.disable:
+ return 1
+ print('\nStarting LUX calibrations')
+ self.log_new_sec('LUX')
+ """
+ The lux calibration is done on a single image. For best effects, the
+ image with lux level closest to 1000 is chosen.
+ """
+ luxes = [Img.lux for Img in self.imgs]
+ argmax = luxes.index(min(luxes, key=lambda l:abs(1000-l)))
+ Img = self.imgs[argmax]
+ self.log += '\nLux found closest to 1000: {} lx'.format(Img.lux)
+ self.log += '\nImage used: ' + Img.name
+ if Img.lux < 50:
+ self.log += '\nWARNING: Low lux could cause inaccurate calibrations!'
+ """
+ do calibration
+ """
+ lux_out,shutter_speed,gain = lux(self,Img)
+ """
+ write output to json
+ """
+ self.json['rpi.lux']['reference_shutter_speed'] = shutter_speed
+ self.json['rpi.lux']['reference_gain'] = gain
+ self.json['rpi.lux']['reference_lux'] = Img.lux
+ self.json['rpi.lux']['reference_Y'] = lux_out
+ self.log += '\nLUX calibrations written to json file'
+ print('Finished LUX calibrations')
+
+ """
+ Noise alibration attempts to describe the noise profile of the sensor. The
+ calibration is run on macbeth images and the final output is taken as the average
+ """
+ def noise_cal(self):
+ if 'rpi.noise' in self.disable:
+ return 1
+ print('\nStarting NOISE calibrations')
+ self.log_new_sec('NOISE')
+ """
+ run calibration on all images and sort by slope.
+ """
+ plot = "rpi.noise" in self.plot
+ noise_out = sorted([noise(self,Img,plot) for Img in self.imgs], key = lambda x:x[0])
+ self.log += '\nFinished processing images'
+ """
+ take the average of the interquartile
+ """
+ l = len(noise_out)
+ noise_out = np.mean(noise_out[l//4:1+3*l//4],axis=0)
+ self.log += '\nAverage noise profile: constant = {} '.format(int(noise_out[1]))
+ self.log += 'slope = {:.3f}'.format(noise_out[0])
+ """
+ write to json
+ """
+ self.json['rpi.noise']['reference_constant'] = int(noise_out[1])
+ self.json['rpi.noise']['reference_slope'] = round(noise_out[0],3)
+ self.log += '\nNOISE calibrations written to json'
+ print('Finished NOISE calibrations')
+
+ """
+ Removes json entries that are turned off
+ """
+ def json_remove(self,disable):
+ self.log_new_sec('Disabling Options',cal=False)
+ if len(self.disable) == 0:
+ self.log += '\nNothing disabled!'
+ return 1
+ for key in disable:
+ try:
+ del self.json[key]
+ self.log += '\nDisabled: '+key
+ except KeyError:
+ self.log += '\nERROR: '+key +' not found!'
+ """
+ writes the json dictionary to the raw json file then make pretty
+ """
+ def write_json(self):
+ """
+ Write json dictionary to file
+ """
+ jstring = json.dumps(self.json,sort_keys=False)
+ """
+ make it pretty :)
+ """
+ pretty_print_json(jstring,self.jf)
+
+ """
+ add a new section to the log file
+ """
+ def log_new_sec(self,section,cal=True):
+ self.log += '\n'+self.log_separator
+ self.log += section
+ if cal:
+ self.log += ' Calibration'
+ self.log += self.log_separator
+
+ """
+ write script arguments to log file
+ """
+ def log_user_input(self,json_output,directory,config,log_output):
+ self.log_new_sec('User Arguments',cal=False)
+ self.log += '\nJson file output: ' + json_output
+ self.log += '\nCalibration images directory: ' + directory
+ if config == None:
+ self.log += '\nNo configuration file input... using default options'
+ elif config == False:
+ self.log += '\nWARNING: Invalid configuration file path...'
+ self.log += ' using default options'
+ elif config == True:
+ self.log += '\nWARNING: Invalid syntax in configuration file...'
+ self.log += ' using default options'
+ else:
+ self.log += '\nConfiguration file: ' + config
+ if log_output == None:
+ self.log += '\nNo log file path input... using default: ctt_log.txt'
+ else:
+ self.log += '\nLog file output: ' + log_output
+
+ # if log_output
+
+ """
+ write log file
+ """
+ def write_log(self,filename):
+ if filename == None:
+ filename = 'ctt_log.txt'
+ self.log += '\n' + self.log_separator
+ with open(filename,'w') as logfile:
+ logfile.write(self.log)
+
+ """
+ Add all images from directory, pass into relevant list of images and
+ extrace lux and temperature values.
+ """
+ def add_imgs(self,directory,mac_config,blacklevel=-1):
+ self.log_new_sec('Image Loading',cal=False)
+ img_suc_msg = 'Image loaded successfully!'
+ print('\n\nLoading images from '+directory)
+ self.log += '\nDirectory: ' + directory
+ """
+ get list of files
+ """
+ filename_list = get_photos(directory)
+ print("Files found: {}".format(len(filename_list)))
+ self.log += '\nFiles found: {}'.format(len(filename_list))
+ """
+ iterate over files
+ """
+ filename_list.sort()
+ for filename in filename_list:
+ address = directory + filename
+ print('\nLoading image: '+filename)
+ self.log += '\n\nImage: ' + filename
+ """
+ obtain colour and lux value
+ """
+ col,lux = get_col_lux(filename)
+ """
+ Check if image is an alsc calibration image
+ """
+ if 'alsc' in filename:
+ Img = load_image(self,address,mac=False)
+ self.log += '\nIdentified as an ALSC image'
+ """
+ check if imagae data has been successfully unpacked
+ """
+ if Img == 0:
+ print('\nDISCARDED')
+ self.log += '\nImage discarded!'
+ continue
+ """
+ check that image colour temperature has been successfuly obtained
+ """
+ elif col != None:
+ """
+ if successful, append to list and continue to next image
+ """
+ Img.col = col
+ Img.name = filename
+ self.log += '\nColour temperature: {} K'.format(col)
+ self.imgs_alsc.append(Img)
+ if blacklevel != -1:
+ Img.blacklevel_16 = blacklevel
+ print(img_suc_msg)
+ continue
+ else:
+ print('Error! No colour temperature found!')
+ self.log += '\nWARNING: Error reading colour temperature'
+ self.log += '\nImage discarded!'
+ print('DISCARDED')
+ else:
+ self.log += '\nIdentified as macbeth chart image'
+ """
+ if image isn't an alsc correction then it must have a lux and a
+ colour temperature value to be useful
+ """
+ if lux == None:
+ print('DISCARDED')
+ self.log += '\nWARNING: Error reading lux value'
+ self.log += '\nImage discarded!'
+ continue
+ Img = load_image(self,address,mac_config)
+ """
+ check that image data has been successfuly unpacked
+ """
+ if Img == 0:
+ print('DISCARDED')
+ self.log += '\nImage discarded!'
+ continue
+ else:
+ """
+ if successful, append to list and continue to next image
+ """
+ Img.col,Img.lux = col,lux
+ Img.name = filename
+ self.log += '\nColour temperature: {} K'.format(col)
+ self.log += '\nLux value: {} lx'.format(lux)
+ if blacklevel != -1:
+ Img.blacklevel_16 = blacklevel
+ print(img_suc_msg)
+ self.imgs.append(Img)
+
+ print('\nFinished loading images')
+
+ """
+ Check that usable images have been found
+ Possible errors include:
+ - no macbeth chart
+ - incorrect filename/extension
+ - images from different cameras
+ """
+ def check_imgs(self):
+ self.log += '\n\nImages found:'
+ self.log += '\nMacbeth : {}'.format(len(self.imgs))
+ self.log += '\nALSC : {} '.format(len(self.imgs_alsc))
+ self.log += '\n\nCamera metadata'
+ """
+ check usable images found
+ """
+ if len(self.imgs) == 0:
+ print('\nERROR: No usable macbeth chart images found')
+ self.log += '\nERROR: No usable macbeth chart images found'
+ return 0
+ """
+ Double check that every image has come from the same camera...
+ """
+ all_imgs = self.imgs + self.imgs_alsc
+ camNames = list(set([Img.camName for Img in all_imgs]))
+ patterns = list(set([Img.pattern for Img in all_imgs]))
+ sigbitss = list(set([Img.sigbits for Img in all_imgs]))
+ blacklevels = list(set([Img.blacklevel_16 for Img in all_imgs]))
+ sizes = list(set([(Img.w,Img.h) for Img in all_imgs]))
+
+ if len(camNames)==1 and len(patterns)==1 and len(sigbitss)==1 and len(blacklevels) ==1 and len(sizes)== 1:
+ self.grey = (patterns[0] == 128)
+ self.blacklevel_16 = blacklevels[0]
+ self.log += '\nName: {}'.format(camNames[0])
+ self.log += '\nBayer pattern case: {}'.format(patterns[0])
+ if self.grey:
+ self.log += '\nGreyscale camera identified'
+ self.log += '\nSignificant bits: {}'.format(sigbitss[0])
+ self.log += '\nBlacklevel: {}'.format(blacklevels[0])
+ self.log += '\nImage size: w = {} h = {}'.format(sizes[0][0],sizes[0][1])
+ return 1
+ else:
+ print('\nERROR: Images from different cameras')
+ self.log += '\nERROR: Images are from different cameras'
+ return 0
+
+def run_ctt(json_output,directory,config,log_output):
+ """
+ check input files are jsons
+ """
+ if json_output[-5:] != '.json':
+ raise ArgError('\n\nError: Output must be a json file!')
+ if config != None:
+ """
+ check if config file is actually a json
+ """
+ if config[-5:] != '.json':
+ raise ArgError('\n\nError: Config file must be a json file!')
+ """
+ read configurations
+ """
+ try:
+ with open(config,'r') as config_json:
+ configs = json.load(config_json)
+ except FileNotFoundError:
+ configs = {}
+ config = False
+ except json.decoder.JSONDecodeError:
+ configs = {}
+ config = True
+
+ else:
+ configs = {}
+ """
+ load configurations from config file, if not given then set default
+ """
+ disable = get_config(configs,"disable",[],'list')
+ plot = get_config(configs,"plot",[],'list')
+ awb_d = get_config(configs,"awb",{},'dict')
+ greyworld = get_config(awb_d,"greyworld",0,'bool')
+ alsc_d = get_config(configs,"alsc",{},'dict')
+ do_alsc_colour = get_config(alsc_d,"do_alsc_colour",1,'bool')
+ luminance_strength = get_config(alsc_d,"luminance_strength",0.5,'num')
+ blacklevel = get_config(configs,"blacklevel",-1,'num')
+ macbeth_d = get_config(configs,"macbeth",{},'dict')
+ mac_small = get_config(macbeth_d,"small",0,'bool')
+ mac_show = get_config(macbeth_d,"show",0,'bool')
+ mac_config = (mac_small,mac_show)
+
+ if blacklevel < -1 or blacklevel >= 2**16:
+ print('\nInvalid blacklevel, defaulted to 64')
+ blacklevel = -1
+
+ if luminance_strength < 0 or luminance_strength > 1:
+ print('\nInvalid luminance_strength strength, defaulted to 0.5')
+ luminance_strength = 0.5
+
+ """
+ sanitise directory path
+ """
+ if directory[-1] != '/':
+ directory += '/'
+ """
+ initialise tuning tool and load images
+ """
+ try:
+ Cam = Camera(json_output)
+ Cam.log_user_input(json_output,directory,config,log_output)
+ Cam.disable = disable
+ Cam.plot = plot
+ Cam.add_imgs(directory,mac_config,blacklevel)
+ except FileNotFoundError:
+ raise ArgError('\n\nError: Input image directory not found!')
+
+
+ """
+ preform calibrations as long as check_imgs returns True
+ If alsc is activated then it must be done before awb and ccm since the alsc
+ tables are used in awb and ccm calibrations
+ ccm also technically does an awb but it measures this from the macbeth
+ chart in the image rather than using calibration data
+ """
+ if Cam.check_imgs():
+ Cam.json['rpi.black_level']['black_level'] = Cam.blacklevel_16
+ Cam.json_remove(disable)
+ print('\nSTARTING CALIBRATIONS')
+ Cam.alsc_cal(luminance_strength,do_alsc_colour)
+ Cam.geq_cal()
+ Cam.lux_cal()
+ Cam.noise_cal()
+ Cam.awb_cal(greyworld,do_alsc_colour)
+ Cam.ccm_cal(do_alsc_colour)
+ print('\nFINISHED CALIBRATIONS')
+ Cam.write_json()
+ Cam.write_log(log_output)
+ print('\nCalibrations written to: '+json_output)
+ if log_output == None:
+ log_output = 'ctt_log.txt'
+ print('Log file written to: '+log_output)
+ pass
+ else:
+ Cam.write_log(log_output)
+
+if __name__ == '__main__':
+ """
+ initialise calibration
+ """
+ if len(sys.argv) == 1:
+ print("""
+ Pisp Camera Tuning Tool version 1.0
+
+ Required Arguments:
+ '-i' : Calibration image directory.
+ '-o' : Name of output json file.
+
+ Optional Arguments:
+ '-c' : Config file for the CTT. If not passed, default parameters used.
+ '-l' : Name of output log file. If not passed, 'ctt_log.txt' used.
+ """)
+ quit(0)
+ else:
+ """
+ parse input arguments
+ """
+ json_output,directory,config,log_output = parse_input()
+ run_ctt(json_output,directory,config,log_output)