From c01cfe14f5540ba96b458088185ac7ae90bb3534 Mon Sep 17 00:00:00 2001 From: Naushir Patuck Date: Sun, 3 May 2020 16:49:53 +0100 Subject: 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 Acked-by: Laurent Pinchart Signed-off-by: Laurent Pinchart --- utils/raspberrypi/ctt/ctt.py | 823 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 823 insertions(+) create mode 100755 utils/raspberrypi/ctt/ctt.py (limited to 'utils/raspberrypi/ctt/ctt.py') 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) -- cgit v1.2.1