diff --git a/utils/raspberrypi/ctt/alsc_only.py b/utils/raspberrypi/ctt/alsc_only.py deleted file mode 100755 index a521c4ad..00000000 --- a/utils/raspberrypi/ctt/alsc_only.py +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env python3 -# -# SPDX-License-Identifier: BSD-2-Clause -# -# Copyright (C) 2022, Raspberry Pi Ltd -# -# alsc tuning tool - -import sys - -from ctt import * -from ctt_tools import parse_input - -if __name__ == '__main__': - """ - initialise calibration - """ - if len(sys.argv) == 1: - print(""" - PiSP Lens Shading Camera Tuning Tool version 1.0 - - Required Arguments: - '-i' : Calibration image directory. - '-o' : Name of output json file. - - Optional Arguments: - '-t' : Target platform - 'pisp' or 'vc4'. Default 'vc4' - '-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, target = parse_input() - if target == 'pisp': - from ctt_pisp import json_template, grid_size - elif target == 'vc4': - from ctt_vc4 import json_template, grid_size - - run_ctt(json_output, directory, config, log_output, json_template, grid_size, target, alsc_only=True) diff --git a/utils/raspberrypi/ctt/cac_only.py b/utils/raspberrypi/ctt/cac_only.py deleted file mode 100644 index 1c0a8193..00000000 --- a/utils/raspberrypi/ctt/cac_only.py +++ /dev/null @@ -1,142 +0,0 @@ -#!/usr/bin/env python3 -# -# SPDX-License-Identifier: BSD-2-Clause -# -# Copyright (C) 2023, Raspberry Pi (Trading) Ltd. -# -# cac_only.py - cac tuning tool - - -# This file allows you to tune only the chromatic aberration correction -# Specify any number of files in the command line args, and it shall iterate through -# and generate an averaged cac table from all the input images, which you can then -# input into your tuning file. - -# Takes .dng files produced by the camera modules of the dots grid and calculates the chromatic abberation of each dot. -# Then takes each dot, and works out where it was in the image, and uses that to output a tables of the shifts -# across the whole image. - -from PIL import Image -import numpy as np -import rawpy -import sys -import getopt - -from ctt_cac import * - - -def cac(filelist, output_filepath, plot_results=False): - np.set_printoptions(precision=3) - np.set_printoptions(suppress=True) - - # Create arrays to hold all the dots data and their colour offsets - red_shift = [] # Format is: [[Dot Center X, Dot Center Y, x shift, y shift]] - blue_shift = [] - # Iterate through the files - # Multiple files is reccomended to average out the lens aberration through rotations - for file in filelist: - print("\n Processing file " + str(file)) - # Read the raw RGB values from the .dng file - with rawpy.imread(file) as raw: - rgb = raw.postprocess() - sizes = (raw.sizes) - - image_size = [sizes[2], sizes[3]] # Image size, X, Y - # Create a colour copy of the RGB values to use later in the calibration - imout = Image.new(mode="RGB", size=image_size) - rgb_image = np.array(imout) - # The rgb values need reshaping from a 1d array to a 3d array to be worked with easily - rgb.reshape((image_size[0], image_size[1], 3)) - rgb_image = rgb - - # Pass the RGB image through to the dots locating program - # Returns an array of the dots (colour rectangles around the dots), and an array of their locations - print("Finding dots") - dots, dots_locations = find_dots_locations(rgb_image) - - # Now, analyse each dot. Work out the centroid of each colour channel, and use that to work out - # by how far the chromatic aberration has shifted each channel - print('Dots found: ' + str(len(dots))) - - for dot, dot_location in zip(dots, dots_locations): - if len(dot) > 0: - if (dot_location[0] > 0) and (dot_location[1] > 0): - ret = analyse_dot(dot, dot_location) - red_shift.append(ret[0]) - blue_shift.append(ret[1]) - - # Take our arrays of red shifts and locations, push them through to be interpolated into a 9x9 matrix - # for the CAC block to handle and then store these as a .json file to be added to the camera - # tuning file - print("\nCreating output grid") - rx, ry, bx, by = shifts_to_yaml(red_shift, blue_shift, image_size) - - print("CAC correction complete!") - - # The json format that we then paste into the tuning file (manually) - sample = ''' - { - "rpi.cac" : - { - "strength": 1.0, - "lut_rx" : [ - rx_vals - ], - "lut_ry" : [ - ry_vals - ], - "lut_bx" : [ - bx_vals - ], - "lut_by" : [ - by_vals - ] - } - } - ''' - - # Below, may look incorrect, however, the PiSP (standard) dimensions are flipped in comparison to - # PIL image coordinate directions, hence why xr -> yr. Also, the shifts calculated are colour shifts, - # and the PiSP block asks for the values it should shift (hence the * -1, to convert from colour shift to a pixel shift) - sample = sample.replace("rx_vals", pprint_array(ry * -1)) - sample = sample.replace("ry_vals", pprint_array(rx * -1)) - sample = sample.replace("bx_vals", pprint_array(by * -1)) - sample = sample.replace("by_vals", pprint_array(bx * -1)) - print("Successfully converted to JSON") - f = open(str(output_filepath), "w+") - f.write(sample) - f.close() - print("Successfully written to json file") - ''' - If you wish to see a plot of the colour channel shifts, add the -p or --plots option - Can be a quick way of validating if the data/dots you've got are good, or if you need to - change some parameters/take some better images - ''' - if plot_results: - plot_shifts(red_shift, blue_shift) - - -if __name__ == "__main__": - argv = sys.argv - # Detect the input and output file paths - arg_output = "output.json" - arg_help = "{0} -i -o -p ".format(argv[0]) - opts, args = getopt.getopt(argv[1:], "hi:o:p", ["help", "input=", "output=", "plot"]) - - output_location = 0 - input_location = 0 - filelist = [] - plot_results = False - for i in range(len(argv)): - if ("-h") in argv[i]: - print(arg_help) # print the help message - sys.exit(2) - if "-o" in argv[i]: - output_location = i - if ".dng" in argv[i]: - filelist.append(argv[i]) - if "-p" in argv[i]: - plot_results = True - - arg_output = argv[output_location + 1] - cac(filelist, arg_output, plot_results) diff --git a/utils/raspberrypi/ctt/colors.py b/utils/raspberrypi/ctt/colors.py deleted file mode 100644 index cb4d236b..00000000 --- a/utils/raspberrypi/ctt/colors.py +++ /dev/null @@ -1,30 +0,0 @@ -# Program to convert from RGB to LAB color space -def RGB_to_LAB(RGB): # where RGB is a 1x3 array. e.g RGB = [100, 255, 230] - num = 0 - XYZ = [0, 0, 0] - # converted all the three R, G, B to X, Y, Z - X = RGB[0] * 0.4124 + RGB[1] * 0.3576 + RGB[2] * 0.1805 - Y = RGB[0] * 0.2126 + RGB[1] * 0.7152 + RGB[2] * 0.0722 - Z = RGB[0] * 0.0193 + RGB[1] * 0.1192 + RGB[2] * 0.9505 - - XYZ[0] = X / 255 * 100 - XYZ[1] = Y / 255 * 100 # XYZ Must be in range 0 -> 100, so scale down from 255 - XYZ[2] = Z / 255 * 100 - XYZ[0] = XYZ[0] / 95.047 # ref_X = 95.047 Observer= 2°, Illuminant= D65 - XYZ[1] = XYZ[1] / 100.0 # ref_Y = 100.000 - XYZ[2] = XYZ[2] / 108.883 # ref_Z = 108.883 - num = 0 - for value in XYZ: - if value > 0.008856: - value = value ** (0.3333333333333333) - else: - value = (7.787 * value) + (16 / 116) - XYZ[num] = value - num = num + 1 - - # L, A, B, values calculated below - L = (116 * XYZ[1]) - 16 - a = 500 * (XYZ[0] - XYZ[1]) - b = 200 * (XYZ[1] - XYZ[2]) - - return [L, a, b] diff --git a/utils/raspberrypi/ctt/convert_tuning.py b/utils/raspberrypi/ctt/convert_tuning.py deleted file mode 100755 index 83cf69d4..00000000 --- a/utils/raspberrypi/ctt/convert_tuning.py +++ /dev/null @@ -1,120 +0,0 @@ -#!/usr/bin/env python3 -# -# SPDX-License-Identifier: BSD-2-Clause -# -# Script to convert version 1.0 Raspberry Pi camera tuning files to version 2.0. -# -# Copyright 2022 Raspberry Pi Ltd - -import argparse -import json -import numpy as np -import sys - -from ctt_pretty_print_json import pretty_print -from ctt_pisp import grid_size as grid_size_pisp -from ctt_pisp import json_template as json_template_pisp -from ctt_vc4 import grid_size as grid_size_vc4 -from ctt_vc4 import json_template as json_template_vc4 - - -def interp_2d(in_ls, src_w, src_h, dst_w, dst_h): - - out_ls = np.zeros((dst_h, dst_w)) - for i in range(src_h): - out_ls[i] = np.interp(np.linspace(0, dst_w - 1, dst_w), - np.linspace(0, dst_w - 1, src_w), - in_ls[i]) - for i in range(dst_w): - out_ls[:,i] = np.interp(np.linspace(0, dst_h - 1, dst_h), - np.linspace(0, dst_h - 1, src_h), - out_ls[:src_h, i]) - return out_ls - - -def convert_target(in_json: dict, target: str): - - src_w, src_h = grid_size_pisp if target == 'vc4' else grid_size_vc4 - dst_w, dst_h = grid_size_vc4 if target == 'vc4' else grid_size_pisp - json_template = json_template_vc4 if target == 'vc4' else json_template_pisp - - # ALSC grid sizes - alsc = next(algo for algo in in_json['algorithms'] if 'rpi.alsc' in algo)['rpi.alsc'] - for colour in ['calibrations_Cr', 'calibrations_Cb']: - if colour not in alsc: - continue - for temperature in alsc[colour]: - in_ls = np.reshape(temperature['table'], (src_h, src_w)) - out_ls = interp_2d(in_ls, src_w, src_h, dst_w, dst_h) - temperature['table'] = np.round(out_ls.flatten(), 3).tolist() - - if 'luminance_lut' in alsc: - in_ls = np.reshape(alsc['luminance_lut'], (src_h, src_w)) - out_ls = interp_2d(in_ls, src_w, src_h, dst_w, dst_h) - alsc['luminance_lut'] = np.round(out_ls.flatten(), 3).tolist() - - # Denoise blocks - for i, algo in enumerate(in_json['algorithms']): - if list(algo.keys())[0] == 'rpi.sdn': - in_json['algorithms'][i] = {'rpi.denoise': json_template['rpi.sdn'] if target == 'vc4' else json_template['rpi.denoise']} - break - - # AGC mode weights - agc = next(algo for algo in in_json['algorithms'] if 'rpi.agc' in algo)['rpi.agc'] - if 'channels' in agc: - for i, channel in enumerate(agc['channels']): - target_agc_metering = json_template['rpi.agc']['channels'][i]['metering_modes'] - for mode, v in channel['metering_modes'].items(): - v['weights'] = target_agc_metering[mode]['weights'] - else: - for mode, v in agc["metering_modes"].items(): - target_agc_metering = json_template['rpi.agc']['channels'][0]['metering_modes'] - v['weights'] = target_agc_metering[mode]['weights'] - - # HDR - if target == 'pisp': - for i, algo in enumerate(in_json['algorithms']): - if list(algo.keys())[0] == 'rpi.hdr': - in_json['algorithms'][i] = {'rpi.hdr': json_template['rpi.hdr']} - - return in_json - - -def convert_v2(in_json: dict, target: str) -> str: - - if 'version' in in_json.keys() and in_json['version'] == 1.0: - converted = { - 'version': 2.0, - 'target': target, - 'algorithms': [{algo: config} for algo, config in in_json.items()] - } - else: - converted = in_json - - # Convert between vc4 <-> pisp targets. This is a best effort thing. - if converted['target'] != target: - converted = convert_target(converted, target) - converted['target'] = target - - grid_size = grid_size_vc4[0] if target == 'vc4' else grid_size_pisp[0] - return pretty_print(converted, custom_elems={'table': grid_size, 'luminance_lut': grid_size}) - - -if __name__ == "__main__": - parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter, description= - 'Convert the format of the Raspberry Pi camera tuning file from v1.0 to v2.0 and/or the vc4 <-> pisp targets.\n') - parser.add_argument('input', type=str, help='Input tuning file.') - parser.add_argument('-t', '--target', type=str, help='Target platform.', - choices=['pisp', 'vc4'], default='vc4') - parser.add_argument('output', type=str, nargs='?', - help='Output converted tuning file. If not provided, the input file will be updated in-place.', - default=None) - args = parser.parse_args() - - with open(args.input, 'r') as f: - in_json = json.load(f) - - out_json = convert_v2(in_json, args.target) - - with open(args.output if args.output is not None else args.input, 'w') as f: - f.write(out_json) diff --git a/utils/raspberrypi/ctt/ctt.py b/utils/raspberrypi/ctt/ctt.py deleted file mode 100755 index 93a8e8fb..00000000 --- a/utils/raspberrypi/ctt/ctt.py +++ /dev/null @@ -1,806 +0,0 @@ -#!/usr/bin/env python3 -# -# SPDX-License-Identifier: BSD-2-Clause -# -# Copyright (C) 2019, Raspberry Pi Ltd -# -# camera tuning tool - -import os -import sys -from ctt_image_load import * -from ctt_cac 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 pretty_print -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 three lists: - - imgs (macbeth charts) - - imgs_alsc (alsc correction images) - - imgs_cac (cac 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(r'([0-9]+)[kK](\.(jpg|jpeg|brcm|dng)|_.*\.(jpg|jpeg|brcm|dng))$', string) - lux = re.search(r'([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, json): - self.path = os.path.dirname(os.path.expanduser(__file__)) + '/' - if self.path == '/': - self.path = '' - self.imgs = [] - self.imgs_alsc = [] - self.imgs_cac = [] - 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 = json - - """ - Perform colour correction calibrations by comparing macbeth patch colours - to standard macbeth chart colours. - """ - def ccm_cal(self, do_alsc_colour, grid_size): - 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 ("rpi.alsc" not 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, grid_size) - 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') - - """ - Perform chromatic abberation correction using multiple dots images. - """ - def cac_cal(self, do_alsc_colour): - if 'rpi.cac' in self.disable: - return 1 - print('\nStarting CAC calibration') - self.log_new_sec('CAC') - """ - check if cac images have been taken - """ - if len(self.imgs_cac) == 0: - print('\nError:\nNo cac calibration images found') - self.log += '\nERROR: No CAC calibration images found!' - self.log += '\nCAC calibration aborted!' - return 1 - """ - if image is greyscale then CAC makes no sense - """ - if self.grey: - print('\nERROR: Can\'t do CAC on greyscale image!') - self.log += '\nERROR: Cannot perform CAC calibration ' - self.log += 'on greyscale image!\nCAC aborted!' - del self.json['rpi.cac'] - return 0 - a = time.time() - """ - Check if camera is greyscale or color. If not greyscale, then perform cac - """ - if do_alsc_colour: - """ - Here we have a color sensor. Perform cac - """ - try: - cacs = cac(self) - except ArithmeticError: - print('ERROR: Matrix is singular!\nTake new pictures and try again...') - self.log += '\nERROR: Singular matrix encountered during fit!' - self.log += '\nCAC aborted!' - return 1 - else: - """ - case where config options suggest greyscale camera. No point in doing CAC - """ - cal_cr_list, cal_cb_list = None, None - self.log += '\nWARNING: No ALSC tables found.\nCAC calibration ' - self.log += 'performed without ALSC correction...' - - """ - Write output to json - """ - if cacs: - self.json['rpi.cac']['cac'] = cacs - self.log += '\nCAC calibration written to json file' - print('Finished CAC calibration') - else: - self.log += "\nCAC calibration failed" - - - """ - 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, grid_size): - 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 ("rpi.alsc" not 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, grid_size) - 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, grid_size, max_gain=8.0): - 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, grid_size, max_gain=max_gain) - cal_cr_list, cal_cb_list, luminance_lut, av_corn = alsc_out - """ - write output 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, grid_size) - """ - 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 - """ - length = len(noise_out) - noise_out = np.mean(noise_out[length//4:1+3*length//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]) - # Results are better with about 40% higher deviation. - self.json['rpi.noise']['reference_slope'] = round(1.4 * 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, version=2.0, target='bcm2835', grid_size=(16, 12)): - """ - Write json dictionary to file using our version 2 format - """ - - out_json = { - "version": version, - 'target': target if target != 'vc4' else 'bcm2835', - "algorithms": [{name: data} for name, data in self.json.items()], - } - - with open(self.jf, 'w') as f: - f.write(pretty_print(out_json, - custom_elems={'table': grid_size[0], 'luminance_lut': grid_size[0]})) - - """ - 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 is None: - self.log += '\nNo configuration file input... using default options' - elif config is False: - self.log += '\nWARNING: Invalid configuration file path...' - self.log += ' using default options' - elif config is True: - self.log += '\nWARNING: Invalid syntax in configuration file...' - self.log += ' using default options' - else: - self.log += '\nConfiguration file: ' + config - if log_output is 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 is 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 is not 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') - elif 'cac' in filename: - Img = load_image(self, address, mac=False) - self.log += '\nIdentified as an CAC image' - Img.name = filename - self.log += '\nColour temperature: {} K'.format(col) - self.imgs_cac.append(Img) - if blacklevel != -1: - Img.blacklevel_16 = blacklevel - print(img_suc_msg) - continue - 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 is 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, macbeth=True): - self.log += '\n\nImages found:' - self.log += '\nMacbeth : {}'.format(len(self.imgs)) - self.log += '\nALSC : {} '.format(len(self.imgs_alsc)) - self.log += '\nCAC: {} '.format(len(self.imgs_cac)) - self.log += '\n\nCamera metadata' - """ - check usable images found - """ - if len(self.imgs) == 0 and macbeth: - print('\nERROR: No usable macbeth chart images found') - self.log += '\nERROR: No usable macbeth chart images found' - return 0 - elif len(self.imgs) == 0 and len(self.imgs_alsc) == 0 and len(self.imgs_cac) == 0: - print('\nERROR: No usable images found') - self.log += '\nERROR: No usable images found' - return 0 - """ - Double check that every image has come from the same camera... - """ - all_imgs = self.imgs + self.imgs_alsc + self.imgs_cac - 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 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, json_template, grid_size, target, alsc_only=False): - """ - check input files are jsons - """ - if json_output[-5:] != '.json': - raise ArgError('\n\nError: Output must be a json file!') - if config is not 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.8, 'num') - lsc_max_gain = get_config(alsc_d, "max_gain", 8.0, '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) - print("Read lsc_max_gain", lsc_max_gain) - - 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, json=json_template) - Cam.log_user_input(json_output, directory, config, log_output) - if alsc_only: - disable = set(Cam.json.keys()).symmetric_difference({"rpi.alsc"}) - 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(macbeth=not alsc_only): - if not alsc_only: - 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, grid_size, max_gain=lsc_max_gain) - Cam.geq_cal() - Cam.lux_cal() - Cam.noise_cal() - if "rpi.cac" in json_template: - Cam.cac_cal(do_alsc_colour) - Cam.awb_cal(greyworld, do_alsc_colour, grid_size) - Cam.ccm_cal(do_alsc_colour, grid_size) - - print('\nFINISHED CALIBRATIONS') - Cam.write_json(target=target, grid_size=grid_size) - Cam.write_log(log_output) - print('\nCalibrations written to: '+json_output) - if log_output is 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 Tuning Tool version 1.0 - Required Arguments: - '-i' : Calibration image directory. - '-o' : Name of output json file. - - Optional Arguments: - '-t' : Target platform - 'pisp' or 'vc4'. Default 'vc4' - '-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, target = parse_input() - if target == 'pisp': - from ctt_pisp import json_template, grid_size - elif target == 'vc4': - from ctt_vc4 import json_template, grid_size - - run_ctt(json_output, directory, config, log_output, json_template, grid_size, target) diff --git a/utils/raspberrypi/ctt/ctt_alsc.py b/utils/raspberrypi/ctt/ctt_alsc.py deleted file mode 100644 index 5d8b2ced..00000000 --- a/utils/raspberrypi/ctt/ctt_alsc.py +++ /dev/null @@ -1,309 +0,0 @@ -# SPDX-License-Identifier: BSD-2-Clause -# -# Copyright (C) 2019, Raspberry Pi Ltd -# -# camera tuning tool for ALSC (auto lens shading correction) - -from ctt_image_load import * -import matplotlib.pyplot as plt -from matplotlib import cm -from mpl_toolkits.mplot3d import Axes3D - - -""" -preform alsc calibration on a set of images -""" -def alsc_all(Cam, do_alsc_colour, plot, grid_size=(16, 12), max_gain=8.0): - imgs_alsc = Cam.imgs_alsc - grid_w, grid_h = grid_size - """ - create list of colour temperatures and associated calibration tables - """ - list_col = [] - list_cr = [] - list_cb = [] - list_cg = [] - for Img in imgs_alsc: - col, cr, cb, cg, size = alsc(Cam, Img, do_alsc_colour, plot, grid_size=grid_size, max_gain=max_gain) - list_col.append(col) - list_cr.append(cr) - list_cb.append(cb) - list_cg.append(cg) - Cam.log += '\n' - Cam.log += '\nFinished processing images' - w, h, dx, dy = size - Cam.log += '\nChannel dimensions: w = {} h = {}'.format(int(w), int(h)) - Cam.log += '\n16x12 grid rectangle size: w = {} h = {}'.format(dx, dy) - - """ - convert to numpy array for data manipulation - """ - list_col = np.array(list_col) - list_cr = np.array(list_cr) - list_cb = np.array(list_cb) - list_cg = np.array(list_cg) - - cal_cr_list = [] - cal_cb_list = [] - - """ - only do colour calculations if required - """ - if do_alsc_colour: - Cam.log += '\nALSC colour tables' - for ct in sorted(set(list_col)): - Cam.log += '\nColour temperature: {} K'.format(ct) - """ - average tables for the same colour temperature - """ - indices = np.where(list_col == ct) - ct = int(ct) - t_r = np.mean(list_cr[indices], axis=0) - t_b = np.mean(list_cb[indices], axis=0) - """ - force numbers to be stored to 3dp.... :( - """ - t_r = np.where((100*t_r) % 1 <= 0.05, t_r+0.001, t_r) - t_b = np.where((100*t_b) % 1 <= 0.05, t_b+0.001, t_b) - t_r = np.where((100*t_r) % 1 >= 0.95, t_r-0.001, t_r) - t_b = np.where((100*t_b) % 1 >= 0.95, t_b-0.001, t_b) - t_r = np.round(t_r, 3) - t_b = np.round(t_b, 3) - r_corners = (t_r[0], t_r[grid_w - 1], t_r[-1], t_r[-grid_w]) - b_corners = (t_b[0], t_b[grid_w - 1], t_b[-1], t_b[-grid_w]) - middle_pos = (grid_h // 2 - 1) * grid_w + grid_w - 1 - r_cen = t_r[middle_pos]+t_r[middle_pos + 1]+t_r[middle_pos + grid_w]+t_r[middle_pos + grid_w + 1] - r_cen = round(r_cen/4, 3) - b_cen = t_b[middle_pos]+t_b[middle_pos + 1]+t_b[middle_pos + grid_w]+t_b[middle_pos + grid_w + 1] - b_cen = round(b_cen/4, 3) - Cam.log += '\nRed table corners: {}'.format(r_corners) - Cam.log += '\nRed table centre: {}'.format(r_cen) - Cam.log += '\nBlue table corners: {}'.format(b_corners) - Cam.log += '\nBlue table centre: {}'.format(b_cen) - cr_dict = { - 'ct': ct, - 'table': list(t_r) - } - cb_dict = { - 'ct': ct, - 'table': list(t_b) - } - cal_cr_list.append(cr_dict) - cal_cb_list.append(cb_dict) - Cam.log += '\n' - else: - cal_cr_list, cal_cb_list = None, None - - """ - average all values for luminance shading and return one table for all temperatures - """ - lum_lut = np.mean(list_cg, axis=0) - lum_lut = np.where((100*lum_lut) % 1 <= 0.05, lum_lut+0.001, lum_lut) - lum_lut = np.where((100*lum_lut) % 1 >= 0.95, lum_lut-0.001, lum_lut) - lum_lut = list(np.round(lum_lut, 3)) - - """ - calculate average corner for lsc gain calculation further on - """ - corners = (lum_lut[0], lum_lut[15], lum_lut[-1], lum_lut[-16]) - Cam.log += '\nLuminance table corners: {}'.format(corners) - l_cen = lum_lut[5*16+7]+lum_lut[5*16+8]+lum_lut[6*16+7]+lum_lut[6*16+8] - l_cen = round(l_cen/4, 3) - Cam.log += '\nLuminance table centre: {}'.format(l_cen) - av_corn = np.sum(corners)/4 - - return cal_cr_list, cal_cb_list, lum_lut, av_corn - - -""" -calculate g/r and g/b for 32x32 points arranged in a grid for a single image -""" -def alsc(Cam, Img, do_alsc_colour, plot=False, grid_size=(16, 12), max_gain=8.0): - Cam.log += '\nProcessing image: ' + Img.name - grid_w, grid_h = grid_size - """ - get channel in correct order - """ - channels = [Img.channels[i] for i in Img.order] - """ - calculate size of single rectangle. - The divisions here must ensure the final row/column of cells has a non-zero number of - pixels. - """ - w, h = Img.w/2, Img.h/2 - dx, dy = int((w - 1) // (grid_w - 1)), int((h - 1) // (grid_h - 1)) - - """ - average the green channels into one - """ - av_ch_g = np.mean((channels[1:3]), axis=0) - if do_alsc_colour: - """ - obtain grid_w x grid_h grid of intensities for each channel and subtract black level - """ - g = get_grid(av_ch_g, dx, dy, grid_size) - Img.blacklevel_16 - r = get_grid(channels[0], dx, dy, grid_size) - Img.blacklevel_16 - b = get_grid(channels[3], dx, dy, grid_size) - Img.blacklevel_16 - """ - calculate ratios as 32 bit in order to be supported by medianBlur function - """ - cr = np.reshape(g/r, (grid_h, grid_w)).astype('float32') - cb = np.reshape(g/b, (grid_h, grid_w)).astype('float32') - cg = np.reshape(1/g, (grid_h, grid_w)).astype('float32') - """ - median blur to remove peaks and save as float 64 - """ - cr = cv2.medianBlur(cr, 3).astype('float64') - cr = cr/np.min(cr) # gain tables are easier for humans to read if the minimum is 1.0 - cb = cv2.medianBlur(cb, 3).astype('float64') - cb = cb/np.min(cb) - cg = cv2.medianBlur(cg, 3).astype('float64') - cg = cg/np.min(cg) - cg = [min(v, max_gain) for v in cg.flatten()] # never exceed the max luminance gain - - """ - debugging code showing 2D surface plot of vignetting. Quite useful for - for sanity check - """ - if plot: - hf = plt.figure(figsize=(8, 8)) - ha = hf.add_subplot(311, projection='3d') - """ - note Y is plotted as -Y so plot has same axes as image - """ - X, Y = np.meshgrid(range(grid_w), range(grid_h)) - ha.plot_surface(X, -Y, cr, cmap=cm.coolwarm, linewidth=0) - ha.set_title('ALSC Plot\nImg: {}\n\ncr'.format(Img.str)) - hb = hf.add_subplot(312, projection='3d') - hb.plot_surface(X, -Y, cb, cmap=cm.coolwarm, linewidth=0) - hb.set_title('cb') - hc = hf.add_subplot(313, projection='3d') - hc.plot_surface(X, -Y, cg, cmap=cm.coolwarm, linewidth=0) - hc.set_title('g') - # print(Img.str) - plt.show() - - return Img.col, cr.flatten(), cb.flatten(), cg, (w, h, dx, dy) - - else: - """ - only perform calculations for luminance shading - """ - g = get_grid(av_ch_g, dx, dy, grid_size) - Img.blacklevel_16 - cg = np.reshape(1/g, (grid_h, grid_w)).astype('float32') - cg = cv2.medianBlur(cg, 3).astype('float64') - cg = cg/np.min(cg) - cg = [min(v, max_gain) for v in cg.flatten()] # never exceed the max luminance gain - - if plot: - hf = plt.figure(figssize=(8, 8)) - ha = hf.add_subplot(1, 1, 1, projection='3d') - X, Y = np.meashgrid(range(grid_w), range(grid_h)) - ha.plot_surface(X, -Y, cg, cmap=cm.coolwarm, linewidth=0) - ha.set_title('ALSC Plot (Luminance only!)\nImg: {}\n\ncg').format(Img.str) - plt.show() - - return Img.col, None, None, cg.flatten(), (w, h, dx, dy) - - -""" -Compresses channel down to a grid of the requested size -""" -def get_grid(chan, dx, dy, grid_size): - grid_w, grid_h = grid_size - grid = [] - """ - since left and bottom border will not necessarily have rectangles of - dimension dx x dy, the 32nd iteration has to be handled separately. - """ - for i in range(grid_h - 1): - for j in range(grid_w - 1): - grid.append(np.mean(chan[dy*i:dy*(1+i), dx*j:dx*(1+j)])) - grid.append(np.mean(chan[dy*i:dy*(1+i), (grid_w - 1)*dx:])) - for j in range(grid_w - 1): - grid.append(np.mean(chan[(grid_h - 1)*dy:, dx*j:dx*(1+j)])) - grid.append(np.mean(chan[(grid_h - 1)*dy:, (grid_w - 1)*dx:])) - """ - return as np.array, ready for further manipulation - """ - return np.array(grid) - - -""" -obtains sigmas for red and blue, effectively a measure of the 'error' -""" -def get_sigma(Cam, cal_cr_list, cal_cb_list, grid_size): - Cam.log += '\nCalculating sigmas' - """ - provided colour alsc tables were generated for two different colour - temperatures sigma is calculated by comparing two calibration temperatures - adjacent in colour space - """ - """ - create list of colour temperatures - """ - cts = [cal['ct'] for cal in cal_cr_list] - # print(cts) - """ - calculate sigmas for each adjacent cts and return worst one - """ - sigma_rs = [] - sigma_bs = [] - for i in range(len(cts)-1): - sigma_rs.append(calc_sigma(cal_cr_list[i]['table'], cal_cr_list[i+1]['table'], grid_size)) - sigma_bs.append(calc_sigma(cal_cb_list[i]['table'], cal_cb_list[i+1]['table'], grid_size)) - Cam.log += '\nColour temperature interval {} - {} K'.format(cts[i], cts[i+1]) - Cam.log += '\nSigma red: {}'.format(sigma_rs[-1]) - Cam.log += '\nSigma blue: {}'.format(sigma_bs[-1]) - - """ - return maximum sigmas, not necessarily from the same colour temperature - interval - """ - sigma_r = max(sigma_rs) if sigma_rs else 0.005 - sigma_b = max(sigma_bs) if sigma_bs else 0.005 - Cam.log += '\nMaximum sigmas: Red = {} Blue = {}'.format(sigma_r, sigma_b) - - # print(sigma_rs, sigma_bs) - # print(sigma_r, sigma_b) - return sigma_r, sigma_b - - -""" -calculate sigma from two adjacent gain tables -""" -def calc_sigma(g1, g2, grid_size): - grid_w, grid_h = grid_size - """ - reshape into 16x12 matrix - """ - g1 = np.reshape(g1, (grid_h, grid_w)) - g2 = np.reshape(g2, (grid_h, grid_w)) - """ - apply gains to gain table - """ - gg = g1/g2 - if np.mean(gg) < 1: - gg = 1/gg - """ - for each internal patch, compute average difference between it and its 4 - neighbours, then append to list - """ - diffs = [] - for i in range(grid_h - 2): - for j in range(grid_w - 2): - """ - note indexing is incremented by 1 since all patches on borders are - not counted - """ - diff = np.abs(gg[i+1][j+1]-gg[i][j+1]) - diff += np.abs(gg[i+1][j+1]-gg[i+2][j+1]) - diff += np.abs(gg[i+1][j+1]-gg[i+1][j]) - diff += np.abs(gg[i+1][j+1]-gg[i+1][j+2]) - diffs.append(diff/4) - - """ - return mean difference - """ - mean_diff = np.mean(diffs) - return(np.round(mean_diff, 5)) diff --git a/utils/raspberrypi/ctt/ctt_awb.py b/utils/raspberrypi/ctt/ctt_awb.py deleted file mode 100644 index 4af1fe41..00000000 --- a/utils/raspberrypi/ctt/ctt_awb.py +++ /dev/null @@ -1,377 +0,0 @@ -# SPDX-License-Identifier: BSD-2-Clause -# -# Copyright (C) 2019, Raspberry Pi Ltd -# -# camera tuning tool for AWB - -from ctt_image_load import * -import matplotlib.pyplot as plt -from bisect import bisect_left -from scipy.optimize import fmin - - -""" -obtain piecewise linear approximation for colour curve -""" -def awb(Cam, cal_cr_list, cal_cb_list, plot, grid_size): - imgs = Cam.imgs - """ - condense alsc calibration tables into one dictionary - """ - if cal_cr_list is None: - colour_cals = None - else: - colour_cals = {} - for cr, cb in zip(cal_cr_list, cal_cb_list): - cr_tab = cr['table'] - cb_tab = cb['table'] - """ - normalise tables so min value is 1 - """ - cr_tab = cr_tab/np.min(cr_tab) - cb_tab = cb_tab/np.min(cb_tab) - colour_cals[cr['ct']] = [cr_tab, cb_tab] - """ - obtain data from greyscale macbeth patches - """ - rb_raw = [] - rbs_hat = [] - for Img in imgs: - Cam.log += '\nProcessing '+Img.name - """ - get greyscale patches with alsc applied if alsc enabled. - Note: if alsc is disabled then colour_cals will be set to None and the - function will just return the greyscale patches - """ - r_patchs, b_patchs, g_patchs = get_alsc_patches(Img, colour_cals, grid_size=grid_size) - """ - calculate ratio of r, b to g - """ - r_g = np.mean(r_patchs/g_patchs) - b_g = np.mean(b_patchs/g_patchs) - Cam.log += '\n r : {:.4f} b : {:.4f}'.format(r_g, b_g) - """ - The curve tends to be better behaved in so-called hatspace. - R, B, G represent the individual channels. The colour curve is plotted in - r, b space, where: - r = R/G - b = B/G - This will be referred to as dehatspace... (sorry) - Hatspace is defined as: - r_hat = R/(R+B+G) - b_hat = B/(R+B+G) - To convert from dehatspace to hastpace (hat operation): - r_hat = r/(1+r+b) - b_hat = b/(1+r+b) - To convert from hatspace to dehatspace (dehat operation): - r = r_hat/(1-r_hat-b_hat) - b = b_hat/(1-r_hat-b_hat) - Proof is left as an excercise to the reader... - Throughout the code, r and b are sometimes referred to as r_g and b_g - as a reminder that they are ratios - """ - r_g_hat = r_g/(1+r_g+b_g) - b_g_hat = b_g/(1+r_g+b_g) - Cam.log += '\n r_hat : {:.4f} b_hat : {:.4f}'.format(r_g_hat, b_g_hat) - rbs_hat.append((r_g_hat, b_g_hat, Img.col)) - rb_raw.append((r_g, b_g)) - Cam.log += '\n' - - Cam.log += '\nFinished processing images' - """ - sort all lits simultaneously by r_hat - """ - rbs_zip = list(zip(rbs_hat, rb_raw)) - rbs_zip.sort(key=lambda x: x[0][0]) - rbs_hat, rb_raw = list(zip(*rbs_zip)) - """ - unzip tuples ready for processing - """ - rbs_hat = list(zip(*rbs_hat)) - rb_raw = list(zip(*rb_raw)) - """ - fit quadratic fit to r_g hat and b_g_hat - """ - a, b, c = np.polyfit(rbs_hat[0], rbs_hat[1], 2) - Cam.log += '\nFit quadratic curve in hatspace' - """ - the algorithm now approximates the shortest distance from each point to the - curve in dehatspace. Since the fit is done in hatspace, it is easier to - find the actual shortest distance in hatspace and use the projection back - into dehatspace as an overestimate. - The distance will be used for two things: - 1) In the case that colour temperature does not strictly decrease with - increasing r/g, the closest point to the line will be chosen out of an - increasing pair of colours. - - 2) To calculate transverse negative an dpositive, the maximum positive - and negative distance from the line are chosen. This benefits from the - overestimate as the transverse pos/neg are upper bound values. - """ - """ - define fit function - """ - def f(x): - return a*x**2 + b*x + c - """ - iterate over points (R, B are x and y coordinates of points) and calculate - distance to line in dehatspace - """ - dists = [] - for i, (R, B) in enumerate(zip(rbs_hat[0], rbs_hat[1])): - """ - define function to minimise as square distance between datapoint and - point on curve. Squaring is monotonic so minimising radius squared is - equivalent to minimising radius - """ - def f_min(x): - y = f(x) - return((x-R)**2+(y-B)**2) - """ - perform optimisation with scipy.optmisie.fmin - """ - x_hat = fmin(f_min, R, disp=0)[0] - y_hat = f(x_hat) - """ - dehat - """ - x = x_hat/(1-x_hat-y_hat) - y = y_hat/(1-x_hat-y_hat) - rr = R/(1-R-B) - bb = B/(1-R-B) - """ - calculate euclidean distance in dehatspace - """ - dist = ((x-rr)**2+(y-bb)**2)**0.5 - """ - return negative if point is below the fit curve - """ - if (x+y) > (rr+bb): - dist *= -1 - dists.append(dist) - Cam.log += '\nFound closest point on fit line to each point in dehatspace' - """ - calculate wiggle factors in awb. 10% added since this is an upper bound - """ - transverse_neg = - np.min(dists) * 1.1 - transverse_pos = np.max(dists) * 1.1 - Cam.log += '\nTransverse pos : {:.5f}'.format(transverse_pos) - Cam.log += '\nTransverse neg : {:.5f}'.format(transverse_neg) - """ - set minimum transverse wiggles to 0.1 . - Wiggle factors dictate how far off of the curve the algorithm searches. 0.1 - is a suitable minimum that gives better results for lighting conditions not - within calibration dataset. Anything less will generalise poorly. - """ - if transverse_pos < 0.01: - transverse_pos = 0.01 - Cam.log += '\nForced transverse pos to 0.01' - if transverse_neg < 0.01: - transverse_neg = 0.01 - Cam.log += '\nForced transverse neg to 0.01' - - """ - generate new b_hat values at each r_hat according to fit - """ - r_hat_fit = np.array(rbs_hat[0]) - b_hat_fit = a*r_hat_fit**2 + b*r_hat_fit + c - """ - transform from hatspace to dehatspace - """ - r_fit = r_hat_fit/(1-r_hat_fit-b_hat_fit) - b_fit = b_hat_fit/(1-r_hat_fit-b_hat_fit) - c_fit = np.round(rbs_hat[2], 0) - """ - round to 4dp - """ - r_fit = np.where((1000*r_fit) % 1 <= 0.05, r_fit+0.0001, r_fit) - r_fit = np.where((1000*r_fit) % 1 >= 0.95, r_fit-0.0001, r_fit) - b_fit = np.where((1000*b_fit) % 1 <= 0.05, b_fit+0.0001, b_fit) - b_fit = np.where((1000*b_fit) % 1 >= 0.95, b_fit-0.0001, b_fit) - r_fit = np.round(r_fit, 4) - b_fit = np.round(b_fit, 4) - """ - The following code ensures that colour temperature decreases with - increasing r/g - """ - """ - iterate backwards over list for easier indexing - """ - i = len(c_fit) - 1 - while i > 0: - if c_fit[i] > c_fit[i-1]: - Cam.log += '\nColour temperature increase found\n' - Cam.log += '{} K at r = {} to '.format(c_fit[i-1], r_fit[i-1]) - Cam.log += '{} K at r = {}'.format(c_fit[i], r_fit[i]) - """ - if colour temperature increases then discard point furthest from - the transformed fit (dehatspace) - """ - error_1 = abs(dists[i-1]) - error_2 = abs(dists[i]) - Cam.log += '\nDistances from fit:\n' - Cam.log += '{} K : {:.5f} , '.format(c_fit[i], error_1) - Cam.log += '{} K : {:.5f}'.format(c_fit[i-1], error_2) - """ - find bad index - note that in python false = 0 and true = 1 - """ - bad = i - (error_1 < error_2) - Cam.log += '\nPoint at {} K deleted as '.format(c_fit[bad]) - Cam.log += 'it is furthest from fit' - """ - delete bad point - """ - r_fit = np.delete(r_fit, bad) - b_fit = np.delete(b_fit, bad) - c_fit = np.delete(c_fit, bad).astype(np.uint16) - """ - note that if a point has been discarded then the length has decreased - by one, meaning that decreasing the index by one will reassess the kept - point against the next point. It is therefore possible, in theory, for - two adjacent points to be discarded, although probably rare - """ - i -= 1 - - """ - return formatted ct curve, ordered by increasing colour temperature - """ - ct_curve = list(np.array(list(zip(b_fit, r_fit, c_fit))).flatten())[::-1] - Cam.log += '\nFinal CT curve:' - for i in range(len(ct_curve)//3): - j = 3*i - Cam.log += '\n ct: {} '.format(ct_curve[j]) - Cam.log += ' r: {} '.format(ct_curve[j+1]) - Cam.log += ' b: {} '.format(ct_curve[j+2]) - - """ - plotting code for debug - """ - if plot: - x = np.linspace(np.min(rbs_hat[0]), np.max(rbs_hat[0]), 100) - y = a*x**2 + b*x + c - plt.subplot(2, 1, 1) - plt.title('hatspace') - plt.plot(rbs_hat[0], rbs_hat[1], ls='--', color='blue') - plt.plot(x, y, color='green', ls='-') - plt.scatter(rbs_hat[0], rbs_hat[1], color='red') - for i, ct in enumerate(rbs_hat[2]): - plt.annotate(str(ct), (rbs_hat[0][i], rbs_hat[1][i])) - plt.xlabel('$\\hat{r}$') - plt.ylabel('$\\hat{b}$') - """ - optional set axes equal to shortest distance so line really does - looks perpendicular and everybody is happy - """ - # ax = plt.gca() - # ax.set_aspect('equal') - plt.grid() - plt.subplot(2, 1, 2) - plt.title('dehatspace - indoors?') - plt.plot(r_fit, b_fit, color='blue') - plt.scatter(rb_raw[0], rb_raw[1], color='green') - plt.scatter(r_fit, b_fit, color='red') - for i, ct in enumerate(c_fit): - plt.annotate(str(ct), (r_fit[i], b_fit[i])) - plt.xlabel('$r$') - plt.ylabel('$b$') - """ - optional set axes equal to shortest distance so line really does - looks perpendicular and everybody is happy - """ - # ax = plt.gca() - # ax.set_aspect('equal') - plt.subplots_adjust(hspace=0.5) - plt.grid() - plt.show() - """ - end of plotting code - """ - return(ct_curve, np.round(transverse_pos, 5), np.round(transverse_neg, 5)) - - -""" -obtain greyscale patches and perform alsc colour correction -""" -def get_alsc_patches(Img, colour_cals, grey=True, grid_size=(16, 12)): - """ - get patch centre coordinates, image colour and the actual - patches for each channel, remembering to subtract blacklevel - If grey then only greyscale patches considered - """ - grid_w, grid_h = grid_size - if grey: - cen_coords = Img.cen_coords[3::4] - col = Img.col - patches = [np.array(Img.patches[i]) for i in Img.order] - r_patchs = patches[0][3::4] - Img.blacklevel_16 - b_patchs = patches[3][3::4] - Img.blacklevel_16 - """ - note two green channels are averages - """ - g_patchs = (patches[1][3::4]+patches[2][3::4])/2 - Img.blacklevel_16 - else: - cen_coords = Img.cen_coords - col = Img.col - patches = [np.array(Img.patches[i]) for i in Img.order] - r_patchs = patches[0] - Img.blacklevel_16 - b_patchs = patches[3] - Img.blacklevel_16 - g_patchs = (patches[1]+patches[2])/2 - Img.blacklevel_16 - - if colour_cals is None: - return r_patchs, b_patchs, g_patchs - """ - find where image colour fits in alsc colour calibration tables - """ - cts = list(colour_cals.keys()) - pos = bisect_left(cts, col) - """ - if img colour is below minimum or above maximum alsc calibration colour, simply - pick extreme closest to img colour - """ - if pos % len(cts) == 0: - """ - this works because -0 = 0 = first and -1 = last index - """ - col_tabs = np.array(colour_cals[cts[-pos//len(cts)]]) - """ - else, perform linear interpolation between existing alsc colour - calibration tables - """ - else: - bef = cts[pos-1] - aft = cts[pos] - da = col-bef - db = aft-col - bef_tabs = np.array(colour_cals[bef]) - aft_tabs = np.array(colour_cals[aft]) - col_tabs = (bef_tabs*db + aft_tabs*da)/(da+db) - col_tabs = np.reshape(col_tabs, (2, grid_h, grid_w)) - """ - calculate dx, dy used to calculate alsc table - """ - w, h = Img.w/2, Img.h/2 - dx, dy = int(-(-(w-1)//grid_w)), int(-(-(h-1)//grid_h)) - """ - make list of pairs of gains for each patch by selecting the correct value - in alsc colour calibration table - """ - patch_gains = [] - for cen in cen_coords: - x, y = cen[0]//dx, cen[1]//dy - # We could probably do with some better spatial interpolation here? - col_gains = (col_tabs[0][y][x], col_tabs[1][y][x]) - patch_gains.append(col_gains) - - """ - multiply the r and b channels in each patch by the respective gain, finally - performing the alsc colour correction - """ - for i, gains in enumerate(patch_gains): - r_patchs[i] = r_patchs[i] * gains[0] - b_patchs[i] = b_patchs[i] * gains[1] - - """ - return greyscale patches, g channel and correct r, b channels - """ - return r_patchs, b_patchs, g_patchs diff --git a/utils/raspberrypi/ctt/ctt_cac.py b/utils/raspberrypi/ctt/ctt_cac.py deleted file mode 100644 index a1183989..00000000 --- a/utils/raspberrypi/ctt/ctt_cac.py +++ /dev/null @@ -1,250 +0,0 @@ -# SPDX-License-Identifier: BSD-2-Clause -# -# Copyright (C) 2023, Raspberry Pi Ltd -# -# ctt_cac.py - CAC (Chromatic Aberration Correction) tuning tool - -from PIL import Image -import numpy as np -import matplotlib.pyplot as plt -from matplotlib import cm - -from ctt_dots_locator import find_dots_locations - - -# This is the wrapper file that creates a JSON entry for you to append -# to your camera tuning file. -# It calculates the chromatic aberration at different points throughout -# the image and uses that to produce a martix that can then be used -# in the camera tuning files to correct this aberration. - - -def pprint_array(array): - # Function to print the array in a tidier format - array = array - output = "" - for i in range(len(array)): - for j in range(len(array[0])): - output += str(round(array[i, j], 2)) + ", " - # Add the necessary indentation to the array - output += "\n " - # Cut off the end of the array (nicely formats it) - return output[:-22] - - -def plot_shifts(red_shifts, blue_shifts): - # If users want, they can pass a command line option to show the shifts on a graph - # Can be useful to check that the functions are all working, and that the sample - # images are doing the right thing - Xs = np.array(red_shifts)[:, 0] - Ys = np.array(red_shifts)[:, 1] - Zs = np.array(red_shifts)[:, 2] - Zs2 = np.array(red_shifts)[:, 3] - Zs3 = np.array(blue_shifts)[:, 2] - Zs4 = np.array(blue_shifts)[:, 3] - - fig, axs = plt.subplots(2, 2) - ax = fig.add_subplot(2, 2, 1, projection='3d') - ax.scatter(Xs, Ys, Zs, cmap=cm.jet, linewidth=0) - ax.set_title('Red X Shift') - ax = fig.add_subplot(2, 2, 2, projection='3d') - ax.scatter(Xs, Ys, Zs2, cmap=cm.jet, linewidth=0) - ax.set_title('Red Y Shift') - ax = fig.add_subplot(2, 2, 3, projection='3d') - ax.scatter(Xs, Ys, Zs3, cmap=cm.jet, linewidth=0) - ax.set_title('Blue X Shift') - ax = fig.add_subplot(2, 2, 4, projection='3d') - ax.scatter(Xs, Ys, Zs4, cmap=cm.jet, linewidth=0) - ax.set_title('Blue Y Shift') - fig.tight_layout() - plt.show() - - -def shifts_to_yaml(red_shift, blue_shift, image_dimensions, output_grid_size=9): - # Convert the shifts to a numpy array for easier handling and initialise other variables - red_shifts = np.array(red_shift) - blue_shifts = np.array(blue_shift) - # create a grid that's smaller than the output grid, which we then interpolate from to get the output values - xrgrid = np.zeros((output_grid_size - 1, output_grid_size - 1)) - xbgrid = np.zeros((output_grid_size - 1, output_grid_size - 1)) - yrgrid = np.zeros((output_grid_size - 1, output_grid_size - 1)) - ybgrid = np.zeros((output_grid_size - 1, output_grid_size - 1)) - - xrsgrid = [] - xbsgrid = [] - yrsgrid = [] - ybsgrid = [] - xg = np.zeros((output_grid_size - 1, output_grid_size - 1)) - yg = np.zeros((output_grid_size - 1, output_grid_size - 1)) - - # Format the grids - numpy doesn't work for this, it wants a - # nice uniformly spaced grid, which we don't know if we have yet, hence the rather mundane setup - for x in range(output_grid_size - 1): - xrsgrid.append([]) - yrsgrid.append([]) - xbsgrid.append([]) - ybsgrid.append([]) - for y in range(output_grid_size - 1): - xrsgrid[x].append([]) - yrsgrid[x].append([]) - xbsgrid[x].append([]) - ybsgrid[x].append([]) - - image_size = (image_dimensions[0], image_dimensions[1]) - gridxsize = image_size[0] / (output_grid_size - 1) - gridysize = image_size[1] / (output_grid_size - 1) - - # Iterate through each dot, and it's shift values and put these into the correct grid location - for red_shift in red_shifts: - xgridloc = int(red_shift[0] / gridxsize) - ygridloc = int(red_shift[1] / gridysize) - xrsgrid[xgridloc][ygridloc].append(red_shift[2]) - yrsgrid[xgridloc][ygridloc].append(red_shift[3]) - - for blue_shift in blue_shifts: - xgridloc = int(blue_shift[0] / gridxsize) - ygridloc = int(blue_shift[1] / gridysize) - xbsgrid[xgridloc][ygridloc].append(blue_shift[2]) - ybsgrid[xgridloc][ygridloc].append(blue_shift[3]) - - # Now calculate the average pixel shift for each square in the grid - grid_incomplete = False - for x in range(output_grid_size - 1): - for y in range(output_grid_size - 1): - if xrsgrid[x][y]: - xrgrid[x, y] = np.mean(xrsgrid[x][y]) - else: - grid_incomplete = True - if yrsgrid[x][y]: - yrgrid[x, y] = np.mean(yrsgrid[x][y]) - else: - grid_incomplete = True - if xbsgrid[x][y]: - xbgrid[x, y] = np.mean(xbsgrid[x][y]) - else: - grid_incomplete = True - if ybsgrid[x][y]: - ybgrid[x, y] = np.mean(ybsgrid[x][y]) - else: - grid_incomplete = True - - if grid_incomplete: - raise RuntimeError("\nERROR: CAC measurements do not span the image!" - "\nConsider using improved CAC images, or remove them entirely.\n") - - # Next, we start to interpolate the central points of the grid that gets passed to the tuning file - input_grids = np.array([xrgrid, yrgrid, xbgrid, ybgrid]) - output_grids = np.zeros((4, output_grid_size, output_grid_size)) - - # Interpolate the centre of the grid - output_grids[:, 1:-1, 1:-1] = (input_grids[:, 1:, :-1] + input_grids[:, 1:, 1:] + input_grids[:, :-1, 1:] + input_grids[:, :-1, :-1]) / 4 - - # Edge cases: - output_grids[:, 1:-1, 0] = ((input_grids[:, :-1, 0] + input_grids[:, 1:, 0]) / 2 - output_grids[:, 1:-1, 1]) * 2 + output_grids[:, 1:-1, 1] - output_grids[:, 1:-1, -1] = ((input_grids[:, :-1, 7] + input_grids[:, 1:, 7]) / 2 - output_grids[:, 1:-1, -2]) * 2 + output_grids[:, 1:-1, -2] - output_grids[:, 0, 1:-1] = ((input_grids[:, 0, :-1] + input_grids[:, 0, 1:]) / 2 - output_grids[:, 1, 1:-1]) * 2 + output_grids[:, 1, 1:-1] - output_grids[:, -1, 1:-1] = ((input_grids[:, 7, :-1] + input_grids[:, 7, 1:]) / 2 - output_grids[:, -2, 1:-1]) * 2 + output_grids[:, -2, 1:-1] - - # Corner Cases: - output_grids[:, 0, 0] = (output_grids[:, 0, 1] - output_grids[:, 1, 1]) + (output_grids[:, 1, 0] - output_grids[:, 1, 1]) + output_grids[:, 1, 1] - output_grids[:, 0, -1] = (output_grids[:, 0, -2] - output_grids[:, 1, -2]) + (output_grids[:, 1, -1] - output_grids[:, 1, -2]) + output_grids[:, 1, -2] - output_grids[:, -1, 0] = (output_grids[:, -1, 1] - output_grids[:, -2, 1]) + (output_grids[:, -2, 0] - output_grids[:, -2, 1]) + output_grids[:, -2, 1] - output_grids[:, -1, -1] = (output_grids[:, -2, -1] - output_grids[:, -2, -2]) + (output_grids[:, -1, -2] - output_grids[:, -2, -2]) + output_grids[:, -2, -2] - - # Below, we swap the x and the y coordinates, and also multiply by a factor of -1 - # This is due to the PiSP (standard) dimensions being flipped in comparison to - # PIL image coordinate directions, hence why xr -> yr. Also, the shifts calculated are colour shifts, - # and the PiSP block asks for the values it should shift by (hence the * -1, to convert from colour shift to a pixel shift) - - output_grid_yr, output_grid_xr, output_grid_yb, output_grid_xb = output_grids * -1 - return output_grid_xr, output_grid_yr, output_grid_xb, output_grid_yb - - -def analyse_dot(dot, dot_location=[0, 0]): - # Scan through the dot, calculate the centroid of each colour channel by doing: - # pixel channel brightness * distance from top left corner - # Sum these, and divide by the sum of each channel's brightnesses to get a centroid for each channel - red_channel = np.array(dot)[:, :, 0] - y_num_pixels = len(red_channel[0]) - x_num_pixels = len(red_channel) - yred_weight = np.sum(np.dot(red_channel, np.arange(y_num_pixels))) - xred_weight = np.sum(np.dot(np.arange(x_num_pixels), red_channel)) - red_sum = np.sum(red_channel) - - green_channel = np.array(dot)[:, :, 1] - ygreen_weight = np.sum(np.dot(green_channel, np.arange(y_num_pixels))) - xgreen_weight = np.sum(np.dot(np.arange(x_num_pixels), green_channel)) - green_sum = np.sum(green_channel) - - blue_channel = np.array(dot)[:, :, 2] - yblue_weight = np.sum(np.dot(blue_channel, np.arange(y_num_pixels))) - xblue_weight = np.sum(np.dot(np.arange(x_num_pixels), blue_channel)) - blue_sum = np.sum(blue_channel) - - # We return this structure. It contains 2 arrays that contain: - # the locations of the dot center, along with the channel shifts in the x and y direction: - # [ [red_center_x, red_center_y, red_x_shift, red_y_shift], [blue_center_x, blue_center_y, blue_x_shift, blue_y_shift] ] - - return [[int(dot_location[0]) + int(len(dot) / 2), int(dot_location[1]) + int(len(dot[0]) / 2), xred_weight / red_sum - xgreen_weight / green_sum, yred_weight / red_sum - ygreen_weight / green_sum], [dot_location[0] + int(len(dot) / 2), dot_location[1] + int(len(dot[0]) / 2), xblue_weight / blue_sum - xgreen_weight / green_sum, yblue_weight / blue_sum - ygreen_weight / green_sum]] - - -def cac(Cam): - filelist = Cam.imgs_cac - - Cam.log += '\nCAC analysing files: {}'.format(str(filelist)) - np.set_printoptions(precision=3) - np.set_printoptions(suppress=True) - - # Create arrays to hold all the dots data and their colour offsets - red_shift = [] # Format is: [[Dot Center X, Dot Center Y, x shift, y shift]] - blue_shift = [] - # Iterate through the files - # Multiple files is reccomended to average out the lens aberration through rotations - for file in filelist: - Cam.log += '\nCAC processing file' - print("\n Processing file") - # Read the raw RGB values - rgb = file.rgb - image_size = [file.h, file.w] # Image size, X, Y - # Create a colour copy of the RGB values to use later in the calibration - imout = Image.new(mode="RGB", size=image_size) - rgb_image = np.array(imout) - # The rgb values need reshaping from a 1d array to a 3d array to be worked with easily - rgb.reshape((image_size[0], image_size[1], 3)) - rgb_image = rgb - - # Pass the RGB image through to the dots locating program - # Returns an array of the dots (colour rectangles around the dots), and an array of their locations - print("Finding dots") - Cam.log += '\nFinding dots' - dots, dots_locations = find_dots_locations(rgb_image) - - # Now, analyse each dot. Work out the centroid of each colour channel, and use that to work out - # by how far the chromatic aberration has shifted each channel - Cam.log += '\nDots found: {}'.format(str(len(dots))) - print('Dots found: ' + str(len(dots))) - - for dot, dot_location in zip(dots, dots_locations): - if len(dot) > 0: - if (dot_location[0] > 0) and (dot_location[1] > 0): - ret = analyse_dot(dot, dot_location) - red_shift.append(ret[0]) - blue_shift.append(ret[1]) - - # Take our arrays of red shifts and locations, push them through to be interpolated into a 9x9 matrix - # for the CAC block to handle and then store these as a .json file to be added to the camera - # tuning file - print("\nCreating output grid") - Cam.log += '\nCreating output grid' - try: - rx, ry, bx, by = shifts_to_yaml(red_shift, blue_shift, image_size) - except RuntimeError as e: - print(str(e)) - Cam.log += "\nCAC correction failed! CAC will not be enabled." - return {} - - print("CAC correction complete!") - Cam.log += '\nCAC correction complete!' - - # Give the JSON dict back to the main ctt program - return {"strength": 1.0, "lut_rx": list(rx.round(2).reshape(81)), "lut_ry": list(ry.round(2).reshape(81)), "lut_bx": list(bx.round(2).reshape(81)), "lut_by": list(by.round(2).reshape(81))} diff --git a/utils/raspberrypi/ctt/ctt_ccm.py b/utils/raspberrypi/ctt/ctt_ccm.py deleted file mode 100644 index 07c943a8..00000000 --- a/utils/raspberrypi/ctt/ctt_ccm.py +++ /dev/null @@ -1,404 +0,0 @@ -# SPDX-License-Identifier: BSD-2-Clause -# -# Copyright (C) 2019, Raspberry Pi Ltd -# -# camera tuning tool for CCM (colour correction matrix) - -from ctt_image_load import * -from ctt_awb import get_alsc_patches -import colors -from scipy.optimize import minimize -from ctt_visualise import visualise_macbeth_chart -import numpy as np -""" -takes 8-bit macbeth chart values, degammas and returns 16 bit -""" - -''' -This program has many options from which to derive the color matrix from. -The first is average. This minimises the average delta E across all patches of -the macbeth chart. Testing across all cameras yeilded this as the most color -accurate and vivid. Other options are avalible however. -Maximum minimises the maximum Delta E of the patches. It iterates through till -a minimum maximum is found (so that there is -not one patch that deviates wildly.) -This yields generally good results but overall the colors are less accurate -Have a fiddle with maximum and see what you think. -The final option allows you to select the patches for which to average across. -This means that you can bias certain patches, for instance if you want the -reds to be more accurate. -''' - -matrix_selection_types = ["average", "maximum", "patches"] -typenum = 0 # select from array above, 0 = average, 1 = maximum, 2 = patches -test_patches = [1, 2, 5, 8, 9, 12, 14] - -''' -Enter patches to test for. Can also be entered twice if you -would like twice as much bias on one patch. -''' - - -def degamma(x): - x = x / ((2 ** 8) - 1) # takes 255 and scales it down to one - x = np.where(x < 0.04045, x / 12.92, ((x + 0.055) / 1.055) ** 2.4) - x = x * ((2 ** 16) - 1) # takes one and scales up to 65535, 16 bit color - return x - - -def gamma(x): - # Take 3 long array of color values and gamma them - return [((colour / 255) ** (1 / 2.4) * 1.055 - 0.055) * 255 for colour in x] - - -""" -FInds colour correction matrices for list of images -""" - - -def ccm(Cam, cal_cr_list, cal_cb_list, grid_size): - global matrix_selection_types, typenum - imgs = Cam.imgs - """ - standard macbeth chart colour values - """ - m_rgb = np.array([ # these are in RGB - [116, 81, 67], # dark skin - [199, 147, 129], # light skin - [91, 122, 156], # blue sky - [90, 108, 64], # foliage - [130, 128, 176], # blue flower - [92, 190, 172], # bluish green - [224, 124, 47], # orange - [68, 91, 170], # purplish blue - [198, 82, 97], # moderate red - [94, 58, 106], # purple - [159, 189, 63], # yellow green - [230, 162, 39], # orange yellow - [35, 63, 147], # blue - [67, 149, 74], # green - [180, 49, 57], # red - [238, 198, 20], # yellow - [193, 84, 151], # magenta - [0, 136, 170], # cyan (goes out of gamut) - [245, 245, 243], # white 9.5 - [200, 202, 202], # neutral 8 - [161, 163, 163], # neutral 6.5 - [121, 121, 122], # neutral 5 - [82, 84, 86], # neutral 3.5 - [49, 49, 51] # black 2 - ]) - """ - convert reference colours from srgb to rgb - """ - m_srgb = degamma(m_rgb) # now in 16 bit color. - - # Produce array of LAB values for ideal color chart - m_lab = [colors.RGB_to_LAB(color / 256) for color in m_srgb] - - """ - reorder reference values to match how patches are ordered - """ - m_srgb = np.array([m_srgb[i::6] for i in range(6)]).reshape((24, 3)) - m_lab = np.array([m_lab[i::6] for i in range(6)]).reshape((24, 3)) - m_rgb = np.array([m_rgb[i::6] for i in range(6)]).reshape((24, 3)) - """ - reformat alsc correction tables or set colour_cals to None if alsc is - deactivated - """ - if cal_cr_list is None: - colour_cals = None - else: - colour_cals = {} - for cr, cb in zip(cal_cr_list, cal_cb_list): - cr_tab = cr['table'] - cb_tab = cb['table'] - """ - normalise tables so min value is 1 - """ - cr_tab = cr_tab / np.min(cr_tab) - cb_tab = cb_tab / np.min(cb_tab) - colour_cals[cr['ct']] = [cr_tab, cb_tab] - - """ - for each image, perform awb and alsc corrections. - Then calculate the colour correction matrix for that image, recording the - ccm and the colour tempertaure. - """ - ccm_tab = {} - for Img in imgs: - Cam.log += '\nProcessing image: ' + Img.name - """ - get macbeth patches with alsc applied if alsc enabled. - Note: if alsc is disabled then colour_cals will be set to None and no - the function will simply return the macbeth patches - """ - r, b, g = get_alsc_patches(Img, colour_cals, grey=False, grid_size=grid_size) - """ - do awb - Note: awb is done by measuring the macbeth chart in the image, rather - than from the awb calibration. This is done so the awb will be perfect - and the ccm matrices will be more accurate. - """ - r_greys, b_greys, g_greys = r[3::4], b[3::4], g[3::4] - r_g = np.mean(r_greys / g_greys) - b_g = np.mean(b_greys / g_greys) - r = r / r_g - b = b / b_g - """ - normalise brightness wrt reference macbeth colours and then average - each channel for each patch - """ - gain = np.mean(m_srgb) / np.mean((r, g, b)) - Cam.log += '\nGain with respect to standard colours: {:.3f}'.format(gain) - r = np.mean(gain * r, axis=1) - b = np.mean(gain * b, axis=1) - g = np.mean(gain * g, axis=1) - """ - calculate ccm matrix - """ - # ==== All of below should in sRGB ===## - sumde = 0 - ccm = do_ccm(r, g, b, m_srgb) - # This is the initial guess that our optimisation code works with. - original_ccm = ccm - r1 = ccm[0] - r2 = ccm[1] - g1 = ccm[3] - g2 = ccm[4] - b1 = ccm[6] - b2 = ccm[7] - ''' - COLOR MATRIX LOOKS AS BELOW - R1 R2 R3 Rval Outr - G1 G2 G3 * Gval = G - B1 B2 B3 Bval B - Will be optimising 6 elements and working out the third element using 1-r1-r2 = r3 - ''' - - x0 = [r1, r2, g1, g2, b1, b2] - ''' - We use our old CCM as the initial guess for the program to find the - optimised matrix - ''' - result = minimize(guess, x0, args=(r, g, b, m_lab), tol=0.01) - ''' - This produces a color matrix which has the lowest delta E possible, - based off the input data. Note it is impossible for this to reach - zero since the input data is imperfect - ''' - - Cam.log += ("\n \n Optimised Matrix Below: \n \n") - [r1, r2, g1, g2, b1, b2] = result.x - # The new, optimised color correction matrix values - optimised_ccm = [r1, r2, (1 - r1 - r2), g1, g2, (1 - g1 - g2), b1, b2, (1 - b1 - b2)] - - # This is the optimised Color Matrix (preserving greys by summing rows up to 1) - Cam.log += str(optimised_ccm) - Cam.log += "\n Old Color Correction Matrix Below \n" - Cam.log += str(ccm) - - formatted_ccm = np.array(original_ccm).reshape((3, 3)) - - ''' - below is a whole load of code that then applies the latest color - matrix, and returns LAB values for color. This can then be used - to calculate the final delta E - ''' - optimised_ccm_rgb = [] # Original Color Corrected Matrix RGB / LAB - optimised_ccm_lab = [] - - formatted_optimised_ccm = np.array(optimised_ccm).reshape((3, 3)) - after_gamma_rgb = [] - after_gamma_lab = [] - - for RGB in zip(r, g, b): - ccm_applied_rgb = np.dot(formatted_ccm, (np.array(RGB) / 256)) - optimised_ccm_rgb.append(gamma(ccm_applied_rgb)) - optimised_ccm_lab.append(colors.RGB_to_LAB(ccm_applied_rgb)) - - optimised_ccm_applied_rgb = np.dot(formatted_optimised_ccm, np.array(RGB) / 256) - after_gamma_rgb.append(gamma(optimised_ccm_applied_rgb)) - after_gamma_lab.append(colors.RGB_to_LAB(optimised_ccm_applied_rgb)) - ''' - Gamma After RGB / LAB - not used in calculations, only used for visualisation - We now want to spit out some data that shows - how the optimisation has improved the color matrices - ''' - Cam.log += "Here are the Improvements" - - # CALCULATE WORST CASE delta e - old_worst_delta_e = 0 - before_average = transform_and_evaluate(formatted_ccm, r, g, b, m_lab) - new_worst_delta_e = 0 - after_average = transform_and_evaluate(formatted_optimised_ccm, r, g, b, m_lab) - for i in range(24): - old_delta_e = deltae(optimised_ccm_lab[i], m_lab[i]) # Current Old Delta E - new_delta_e = deltae(after_gamma_lab[i], m_lab[i]) # Current New Delta E - if old_delta_e > old_worst_delta_e: - old_worst_delta_e = old_delta_e - if new_delta_e > new_worst_delta_e: - new_worst_delta_e = new_delta_e - - Cam.log += "Before color correction matrix was optimised, we got an average delta E of " + str(before_average) + " and a maximum delta E of " + str(old_worst_delta_e) - Cam.log += "After color correction matrix was optimised, we got an average delta E of " + str(after_average) + " and a maximum delta E of " + str(new_worst_delta_e) - - visualise_macbeth_chart(m_rgb, optimised_ccm_rgb, after_gamma_rgb, str(Img.col) + str(matrix_selection_types[typenum])) - ''' - The program will also save some visualisations of improvements. - Very pretty to look at. Top rectangle is ideal, Left square is - before optimisation, right square is after. - ''' - - """ - if a ccm has already been calculated for that temperature then don't - overwrite but save both. They will then be averaged later on - """ # Now going to use optimised color matrix, optimised_ccm - if Img.col in ccm_tab.keys(): - ccm_tab[Img.col].append(optimised_ccm) - else: - ccm_tab[Img.col] = [optimised_ccm] - Cam.log += '\n' - - Cam.log += '\nFinished processing images' - """ - average any ccms that share a colour temperature - """ - for k, v in ccm_tab.items(): - tab = np.mean(v, axis=0) - tab = np.where((10000 * tab) % 1 <= 0.05, tab + 0.00001, tab) - tab = np.where((10000 * tab) % 1 >= 0.95, tab - 0.00001, tab) - ccm_tab[k] = list(np.round(tab, 5)) - Cam.log += '\nMatrix calculated for colour temperature of {} K'.format(k) - - """ - return all ccms with respective colour temperature in the correct format, - sorted by their colour temperature - """ - sorted_ccms = sorted(ccm_tab.items(), key=lambda kv: kv[0]) - ccms = [] - for i in sorted_ccms: - ccms.append({ - 'ct': i[0], - 'ccm': i[1] - }) - return ccms - - -def guess(x0, r, g, b, m_lab): # provides a method of numerical feedback for the optimisation code - [r1, r2, g1, g2, b1, b2] = x0 - ccm = np.array([r1, r2, (1 - r1 - r2), - g1, g2, (1 - g1 - g2), - b1, b2, (1 - b1 - b2)]).reshape((3, 3)) # format the matrix correctly - return transform_and_evaluate(ccm, r, g, b, m_lab) - - -def transform_and_evaluate(ccm, r, g, b, m_lab): # Transforms colors to LAB and applies the correction matrix - # create list of matrix changed colors - realrgb = [] - for RGB in zip(r, g, b): - rgb_post_ccm = np.dot(ccm, np.array(RGB) / 256) # This is RGB values after the color correction matrix has been applied - realrgb.append(colors.RGB_to_LAB(rgb_post_ccm)) - # now compare that with m_lab and return numeric result, averaged for each patch - return (sumde(realrgb, m_lab) / 24) # returns an average result of delta E - - -def sumde(listA, listB): - global typenum, test_patches - sumde = 0 - maxde = 0 - patchde = [] # Create array of the delta E values for each patch. useful for optimisation of certain patches - for listA_item, listB_item in zip(listA, listB): - if maxde < (deltae(listA_item, listB_item)): - maxde = deltae(listA_item, listB_item) - patchde.append(deltae(listA_item, listB_item)) - sumde += deltae(listA_item, listB_item) - ''' - The different options specified at the start allow for - the maximum to be returned, average or specific patches - ''' - if typenum == 0: - return sumde - if typenum == 1: - return maxde - if typenum == 2: - output = sum([patchde[test_patch] for test_patch in test_patches]) - # Selects only certain patches and returns the output for them - return output - - -""" -calculates the ccm for an individual image. -ccms are calculated in rgb space, and are fit by hand. Although it is a 3x3 -matrix, each row must add up to 1 in order to conserve greyness, simplifying -calculation. -The initial CCM is calculated in RGB, and then optimised in LAB color space -This simplifies the initial calculation but then gets us the accuracy of -using LAB color space. -""" - - -def do_ccm(r, g, b, m_srgb): - rb = r-b - gb = g-b - rb_2s = (rb * rb) - rb_gbs = (rb * gb) - gb_2s = (gb * gb) - - r_rbs = rb * (m_srgb[..., 0] - b) - r_gbs = gb * (m_srgb[..., 0] - b) - g_rbs = rb * (m_srgb[..., 1] - b) - g_gbs = gb * (m_srgb[..., 1] - b) - b_rbs = rb * (m_srgb[..., 2] - b) - b_gbs = gb * (m_srgb[..., 2] - b) - - """ - Obtain least squares fit - """ - rb_2 = np.sum(rb_2s) - gb_2 = np.sum(gb_2s) - rb_gb = np.sum(rb_gbs) - r_rb = np.sum(r_rbs) - r_gb = np.sum(r_gbs) - g_rb = np.sum(g_rbs) - g_gb = np.sum(g_gbs) - b_rb = np.sum(b_rbs) - b_gb = np.sum(b_gbs) - - det = rb_2 * gb_2 - rb_gb * rb_gb - - """ - Raise error if matrix is singular... - This shouldn't really happen with real data but if it does just take new - pictures and try again, not much else to be done unfortunately... - """ - if det < 0.001: - raise ArithmeticError - - r_a = (gb_2 * r_rb - rb_gb * r_gb) / det - r_b = (rb_2 * r_gb - rb_gb * r_rb) / det - """ - Last row can be calculated by knowing the sum must be 1 - """ - r_c = 1 - r_a - r_b - - g_a = (gb_2 * g_rb - rb_gb * g_gb) / det - g_b = (rb_2 * g_gb - rb_gb * g_rb) / det - g_c = 1 - g_a - g_b - - b_a = (gb_2 * b_rb - rb_gb * b_gb) / det - b_b = (rb_2 * b_gb - rb_gb * b_rb) / det - b_c = 1 - b_a - b_b - - """ - format ccm - """ - ccm = [r_a, r_b, r_c, g_a, g_b, g_c, b_a, b_b, b_c] - - return ccm - - -def deltae(colorA, colorB): - return ((colorA[0] - colorB[0]) ** 2 + (colorA[1] - colorB[1]) ** 2 + (colorA[2] - colorB[2]) ** 2) ** 0.5 - # return ((colorA[1]-colorB[1]) * * 2 + (colorA[2]-colorB[2]) * * 2) * * 0.5 - # UNCOMMENT IF YOU WANT TO NEGLECT LUMINANCE FROM CALCULATION OF DELTA E diff --git a/utils/raspberrypi/ctt/ctt_config_example.json b/utils/raspberrypi/ctt/ctt_config_example.json deleted file mode 100644 index 1105862c..00000000 --- a/utils/raspberrypi/ctt/ctt_config_example.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "disable": [], - "plot": [], - "alsc": { - "do_alsc_colour": 1, - "luminance_strength": 0.8, - "max_gain": 8.0 - }, - "awb": { - "greyworld": 0 - }, - "blacklevel": -1, - "macbeth": { - "small": 0, - "show": 0 - } -} diff --git a/utils/raspberrypi/ctt/ctt_dots_locator.py b/utils/raspberrypi/ctt/ctt_dots_locator.py deleted file mode 100644 index 4945c04b..00000000 --- a/utils/raspberrypi/ctt/ctt_dots_locator.py +++ /dev/null @@ -1,118 +0,0 @@ -# SPDX-License-Identifier: BSD-2-Clause -# -# Copyright (C) 2023, Raspberry Pi Ltd -# -# find_dots.py - Used by CAC algorithm to convert image to set of dots - -''' -This file takes the black and white version of the image, along with -the color version. It then located the black dots on the image by -thresholding dark pixels. -In a rather fun way, the algorithm bounces around the thresholded area in a random path -We then use the maximum and minimum of these paths to determine the dot shape and size -This info is then used to return colored dots and locations back to the main file -''' - -import numpy as np -import random -from PIL import Image, ImageEnhance, ImageFilter - - -def find_dots_locations(rgb_image, color_threshold=100, dots_edge_avoid=75, image_edge_avoid=10, search_path_length=500, grid_scan_step_size=10, logfile=open("log.txt", "a+")): - # Initialise some starting variables - pixels = Image.fromarray(rgb_image) - pixels = pixels.convert("L") - enhancer = ImageEnhance.Contrast(pixels) - im_output = enhancer.enhance(1.4) - # We smooth it slightly to make it easier for the dot recognition program to locate the dots - im_output = im_output.filter(ImageFilter.GaussianBlur(radius=2)) - bw_image = np.array(im_output) - - location = [0, 0] - dots = [] - dots_location = [] - # the program takes away the edges - we don't want a dot that is half a circle, the - # centroids would all be wrong - for x in range(dots_edge_avoid, len(bw_image) - dots_edge_avoid, grid_scan_step_size): - for y in range(dots_edge_avoid, len(bw_image[0]) - dots_edge_avoid, grid_scan_step_size): - location = [x, y] - scrap_dot = False # A variable used to make sure that this is a valid dot - if (bw_image[location[0], location[1]] < color_threshold) and not (scrap_dot): - heading = "south" # Define a starting direction to move in - coords = [] - for i in range(search_path_length): # Creates a path of length `search_path_length`. This turns out to always be enough to work out the rough shape of the dot. - # Now make sure that the thresholded area doesn't come within 10 pixels of the edge of the image, ensures we capture all the CA - if ((image_edge_avoid < location[0] < len(bw_image) - image_edge_avoid) and (image_edge_avoid < location[1] < len(bw_image[0]) - image_edge_avoid)) and not (scrap_dot): - if heading == "south": - if bw_image[location[0] + 1, location[1]] < color_threshold: - # Here, notice it does not go south, but actually goes southeast - # This is crucial in ensuring that we make our way around the majority of the dot - location[0] = location[0] + 1 - location[1] = location[1] + 1 - heading = "south" - else: - # This happens when we reach a thresholded edge. We now randomly change direction and keep searching - dir = random.randint(1, 2) - if dir == 1: - heading = "west" - if dir == 2: - heading = "east" - - if heading == "east": - if bw_image[location[0], location[1] + 1] < color_threshold: - location[1] = location[1] + 1 - heading = "east" - else: - dir = random.randint(1, 2) - if dir == 1: - heading = "north" - if dir == 2: - heading = "south" - - if heading == "west": - if bw_image[location[0], location[1] - 1] < color_threshold: - location[1] = location[1] - 1 - heading = "west" - else: - dir = random.randint(1, 2) - if dir == 1: - heading = "north" - if dir == 2: - heading = "south" - - if heading == "north": - if bw_image[location[0] - 1, location[1]] < color_threshold: - location[0] = location[0] - 1 - heading = "north" - else: - dir = random.randint(1, 2) - if dir == 1: - heading = "west" - if dir == 2: - heading = "east" - # Log where our particle travels across the dot - coords.append([location[0], location[1]]) - else: - scrap_dot = True # We just don't have enough space around the dot, discard this one, and move on - if not scrap_dot: - # get the size of the dot surrounding the dot - x_coords = np.array(coords)[:, 0] - y_coords = np.array(coords)[:, 1] - hsquaresize = max(list(x_coords)) - min(list(x_coords)) - vsquaresize = max(list(y_coords)) - min(list(y_coords)) - # Create the bounding coordinates of the rectangle surrounding the dot - # Program uses the dotsize + half of the dotsize to ensure we get all that color fringing - extra_space_factor = 0.45 - top_left_x = (min(list(x_coords)) - int(hsquaresize * extra_space_factor)) - btm_right_x = max(list(x_coords)) + int(hsquaresize * extra_space_factor) - top_left_y = (min(list(y_coords)) - int(vsquaresize * extra_space_factor)) - btm_right_y = max(list(y_coords)) + int(vsquaresize * extra_space_factor) - # Overwrite the area of the dot to ensure we don't use it again - bw_image[top_left_x:btm_right_x, top_left_y:btm_right_y] = 255 - # Add the color version of the dot to the list to send off, along with some coordinates. - dots.append(rgb_image[top_left_x:btm_right_x, top_left_y:btm_right_y]) - dots_location.append([top_left_x, top_left_y]) - else: - # Dot was too close to the image border to be useable - pass - return dots, dots_location diff --git a/utils/raspberrypi/ctt/ctt_geq.py b/utils/raspberrypi/ctt/ctt_geq.py deleted file mode 100644 index 5a91ebb4..00000000 --- a/utils/raspberrypi/ctt/ctt_geq.py +++ /dev/null @@ -1,181 +0,0 @@ -# SPDX-License-Identifier: BSD-2-Clause -# -# Copyright (C) 2019, Raspberry Pi Ltd -# -# camera tuning tool for GEQ (green equalisation) - -from ctt_tools import * -import matplotlib.pyplot as plt -import scipy.optimize as optimize - - -""" -Uses green differences in macbeth patches to fit green equalisation threshold -model. Ideally, all macbeth chart centres would fall below the threshold as -these should be corrected by geq. -""" -def geq_fit(Cam, plot): - imgs = Cam.imgs - """ - green equalisation to mitigate mazing. - Fits geq model by looking at difference - between greens in macbeth patches - """ - geqs = np.array([geq(Cam, Img)*Img.againQ8_norm for Img in imgs]) - Cam.log += '\nProcessed all images' - geqs = geqs.reshape((-1, 2)) - """ - data is sorted by green difference and top half is selected since higher - green difference data define the decision boundary. - """ - geqs = np.array(sorted(geqs, key=lambda r: np.abs((r[1]-r[0])/r[0]))) - - length = len(geqs) - g0 = geqs[length//2:, 0] - g1 = geqs[length//2:, 1] - gdiff = np.abs(g0-g1) - """ - find linear fit by minimising asymmetric least square errors - in order to cover most of the macbeth images. - the philosophy here is that every macbeth patch should fall within the - threshold, hence the upper bound approach - """ - def f(params): - m, c = params - a = gdiff - (m*g0+c) - """ - asymmetric square error returns: - 1.95 * a**2 if a is positive - 0.05 * a**2 if a is negative - """ - return(np.sum(a**2+0.95*np.abs(a)*a)) - - initial_guess = [0.01, 500] - """ - Nelder-Mead is usually not the most desirable optimisation method - but has been chosen here due to its robustness to undifferentiability - (is that a word?) - """ - result = optimize.minimize(f, initial_guess, method='Nelder-Mead') - """ - need to check if the fit worked correectly - """ - if result.success: - slope, offset = result.x - Cam.log += '\nFit result: slope = {:.5f} '.format(slope) - Cam.log += 'offset = {}'.format(int(offset)) - """ - optional plotting code - """ - if plot: - x = np.linspace(max(g0)*1.1, 100) - y = slope*x + offset - plt.title('GEQ Asymmetric \'Upper Bound\' Fit') - plt.plot(x, y, color='red', ls='--', label='fit') - plt.scatter(g0, gdiff, color='b', label='data') - plt.ylabel('Difference in green channels') - plt.xlabel('Green value') - - """ - This upper bound asymmetric gives correct order of magnitude values. - The pipeline approximates a 1st derivative of a gaussian with some - linear piecewise functions, introducing arbitrary cutoffs. For - pessimistic geq, the model parameters have been increased by a - scaling factor/constant. - - Feel free to tune these or edit the json files directly if you - belive there are still mazing effects left (threshold too low) or if you - think it is being overcorrected (threshold too high). - We have gone for a one size fits most approach that will produce - acceptable results in most applications. - """ - slope *= 1.5 - offset += 201 - Cam.log += '\nFit after correction factors: slope = {:.5f}'.format(slope) - Cam.log += ' offset = {}'.format(int(offset)) - """ - clamp offset at 0 due to pipeline considerations - """ - if offset < 0: - Cam.log += '\nOffset raised to 0' - offset = 0 - """ - optional plotting code - """ - if plot: - y2 = slope*x + offset - plt.plot(x, y2, color='green', ls='--', label='scaled fit') - plt.grid() - plt.legend() - plt.show() - - """ - the case where for some reason the fit didn't work correctly - - Transpose data and then least squares linear fit. Transposing data - makes it robust to many patches where green difference is the same - since they only contribute to one error minimisation, instead of dragging - the entire linear fit down. - """ - - else: - print('\nError! Couldn\'t fit asymmetric lest squares') - print(result.message) - Cam.log += '\nWARNING: Asymmetric least squares fit failed! ' - Cam.log += 'Standard fit used could possibly lead to worse results' - fit = np.polyfit(gdiff, g0, 1) - offset, slope = -fit[1]/fit[0], 1/fit[0] - Cam.log += '\nFit result: slope = {:.5f} '.format(slope) - Cam.log += 'offset = {}'.format(int(offset)) - """ - optional plotting code - """ - if plot: - x = np.linspace(max(g0)*1.1, 100) - y = slope*x + offset - plt.title('GEQ Linear Fit') - plt.plot(x, y, color='red', ls='--', label='fit') - plt.scatter(g0, gdiff, color='b', label='data') - plt.ylabel('Difference in green channels') - plt.xlabel('Green value') - """ - Scaling factors (see previous justification) - The model here will not be an upper bound so scaling factors have - been increased. - This method of deriving geq model parameters is extremely arbitrary - and undesirable. - """ - slope *= 2.5 - offset += 301 - Cam.log += '\nFit after correction factors: slope = {:.5f}'.format(slope) - Cam.log += ' offset = {}'.format(int(offset)) - - if offset < 0: - Cam.log += '\nOffset raised to 0' - offset = 0 - - """ - optional plotting code - """ - if plot: - y2 = slope*x + offset - plt.plot(x, y2, color='green', ls='--', label='scaled fit') - plt.legend() - plt.grid() - plt.show() - - return round(slope, 5), int(offset) - - -"""" -Return green channels of macbeth patches -returns g0, g1 where -> g0 is green next to red -> g1 is green next to blue -""" -def geq(Cam, Img): - Cam.log += '\nProcessing image {}'.format(Img.name) - patches = [Img.patches[i] for i in Img.order][1:3] - g_patches = np.array([(np.mean(patches[0][i]), np.mean(patches[1][i])) for i in range(24)]) - Cam.log += '\n' - return(g_patches) diff --git a/utils/raspberrypi/ctt/ctt_image_load.py b/utils/raspberrypi/ctt/ctt_image_load.py deleted file mode 100644 index 531de328..00000000 --- a/utils/raspberrypi/ctt/ctt_image_load.py +++ /dev/null @@ -1,455 +0,0 @@ -# SPDX-License-Identifier: BSD-2-Clause -# -# Copyright (C) 2019-2020, Raspberry Pi Ltd -# -# 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 is 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 - """ - The DNG and TIFF/EP specifications use different IFDs to store the raw - image data and the Exif tags. DNG stores them in a SubIFD and in an Exif - IFD respectively (named "SubImage1" and "Photo" by pyexiv2), while - TIFF/EP stores them both in IFD0 (name "Image"). Both are used in "DNG" - files, with libcamera-apps following the DNG recommendation and - applications based on picamera2 following TIFF/EP. - - This code detects which tags are being used, and therefore extracts the - correct values. - """ - try: - Img.w = metadata['Exif.SubImage1.ImageWidth'].value - subimage = "SubImage1" - photo = "Photo" - except KeyError: - Img.w = metadata['Exif.Image.ImageWidth'].value - subimage = "Image" - photo = "Image" - Img.pad = 0 - Img.h = metadata[f'Exif.{subimage}.ImageLength'].value - white = metadata[f'Exif.{subimage}.WhiteLevel'].value - Img.sigbits = int(white).bit_length() - Img.fmt = (Img.sigbits - 4) // 2 - Img.exposure = int(metadata[f'Exif.{photo}.ExposureTime'].value * 1000000) - Img.againQ8 = metadata[f'Exif.{photo}.ISOSpeedRatings'].value * 256 / 100 - Img.againQ8_norm = Img.againQ8 / 256 - Img.camName = metadata['Exif.Image.Model'].value - Img.blacklevel = int(metadata[f'Exif.{subimage}.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[f'Exif.{subimage}.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] - Img.rgb = raw_im.postprocess() - - except Exception: - 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) - """ - handle errors smoothly if loading image failed - """ - if Img == 0: - return 0 - 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 is 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 diff --git a/utils/raspberrypi/ctt/ctt_lux.py b/utils/raspberrypi/ctt/ctt_lux.py deleted file mode 100644 index 46be1512..00000000 --- a/utils/raspberrypi/ctt/ctt_lux.py +++ /dev/null @@ -1,61 +0,0 @@ -# SPDX-License-Identifier: BSD-2-Clause -# -# Copyright (C) 2019, Raspberry Pi Ltd -# -# camera tuning tool for lux level - -from ctt_tools import * - - -""" -Find lux values from metadata and calculate Y -""" -def lux(Cam, Img): - shutter_speed = Img.exposure - gain = Img.againQ8_norm - aperture = 1 - Cam.log += '\nShutter speed = {}'.format(shutter_speed) - Cam.log += '\nGain = {}'.format(gain) - Cam.log += '\nAperture = {}'.format(aperture) - patches = [Img.patches[i] for i in Img.order] - channels = [Img.channels[i] for i in Img.order] - return lux_calc(Cam, Img, patches, channels), shutter_speed, gain - - -""" -perform lux calibration on bayer channels -""" -def lux_calc(Cam, Img, patches, channels): - """ - find means color channels on grey patches - """ - ap_r = np.mean(patches[0][3::4]) - ap_g = (np.mean(patches[1][3::4])+np.mean(patches[2][3::4]))/2 - ap_b = np.mean(patches[3][3::4]) - Cam.log += '\nAverage channel values on grey patches:' - Cam.log += '\nRed = {:.0f} Green = {:.0f} Blue = {:.0f}'.format(ap_r, ap_b, ap_g) - # print(ap_r, ap_g, ap_b) - """ - calculate channel gains - """ - gr = ap_g/ap_r - gb = ap_g/ap_b - Cam.log += '\nChannel gains: Red = {:.3f} Blue = {:.3f}'.format(gr, gb) - - """ - find means color channels on image and scale by gain - note greens are averaged together (treated as one channel) - """ - a_r = np.mean(channels[0])*gr - a_g = (np.mean(channels[1])+np.mean(channels[2]))/2 - a_b = np.mean(channels[3])*gb - Cam.log += '\nAverage channel values over entire image scaled by channel gains:' - Cam.log += '\nRed = {:.0f} Green = {:.0f} Blue = {:.0f}'.format(a_r, a_b, a_g) - # print(a_r, a_g, a_b) - """ - Calculate y with top row of yuv matrix - """ - y = 0.299*a_r + 0.587*a_g + 0.114*a_b - Cam.log += '\nY value calculated: {}'.format(int(y)) - # print(y) - return int(y) diff --git a/utils/raspberrypi/ctt/ctt_macbeth_locator.py b/utils/raspberrypi/ctt/ctt_macbeth_locator.py deleted file mode 100644 index f22dbf31..00000000 --- a/utils/raspberrypi/ctt/ctt_macbeth_locator.py +++ /dev/null @@ -1,757 +0,0 @@ -# SPDX-License-Identifier: BSD-2-Clause -# -# Copyright (C) 2019, Raspberry Pi Ltd -# -# camera tuning tool Macbeth chart locator - -from ctt_ransac import * -from ctt_tools import * -import warnings - -""" -NOTE: some custom functions have been used here to make the code more readable. -These are defined in tools.py if they are needed for reference. -""" - - -""" -Some inconsistencies between packages cause runtime warnings when running -the clustering algorithm. This catches these warnings so they don't flood the -output to the console -""" -def fxn(): - warnings.warn("runtime", RuntimeWarning) - - -""" -Define the success message -""" -success_msg = 'Macbeth chart located successfully' - -def find_macbeth(Cam, img, mac_config=(0, 0)): - small_chart, show = mac_config - print('Locating macbeth chart') - Cam.log += '\nLocating macbeth chart' - """ - catch the warnings - """ - warnings.simplefilter("ignore") - fxn() - - """ - Reference macbeth chart is created that will be correlated with the located - macbeth chart guess to produce a confidence value for the match. - """ - ref = cv2.imread(Cam.path + 'ctt_ref.pgm', flags=cv2.IMREAD_GRAYSCALE) - ref_w = 120 - ref_h = 80 - rc1 = (0, 0) - rc2 = (0, ref_h) - rc3 = (ref_w, ref_h) - rc4 = (ref_w, 0) - ref_corns = np.array((rc1, rc2, rc3, rc4), np.float32) - ref_data = (ref, ref_w, ref_h, ref_corns) - - """ - locate macbeth chart - """ - cor, mac, coords, msg = get_macbeth_chart(img, ref_data) - - # Keep a list that will include this and any brightened up versions of - # the image for reuse. - all_images = [img] - - """ - following bits of code tries to fix common problems with simple - techniques. - If now or at any point the best correlation is of above 0.75, then - nothing more is tried as this is a high enough confidence to ensure - reliable macbeth square centre placement. - """ - - """ - brighten image 2x - """ - if cor < 0.75: - a = 2 - img_br = cv2.convertScaleAbs(img, alpha=a, beta=0) - all_images.append(img_br) - cor_b, mac_b, coords_b, msg_b = get_macbeth_chart(img_br, ref_data) - if cor_b > cor: - cor, mac, coords, msg = cor_b, mac_b, coords_b, msg_b - - """ - brighten image 4x - """ - if cor < 0.75: - a = 4 - img_br = cv2.convertScaleAbs(img, alpha=a, beta=0) - all_images.append(img_br) - cor_b, mac_b, coords_b, msg_b = get_macbeth_chart(img_br, ref_data) - if cor_b > cor: - cor, mac, coords, msg = cor_b, mac_b, coords_b, msg_b - - """ - In case macbeth chart is too small, take a selection of the image and - attempt to locate macbeth chart within that. The scale increment is - root 2 - """ - """ - These variables will be used to transform the found coordinates at smaller - scales back into the original. If ii is still -1 after this section that - means it was not successful - """ - ii = -1 - w_best = 0 - h_best = 0 - d_best = 100 - """ - d_best records the scale of the best match. Macbeth charts are only looked - for at one scale increment smaller than the current best match in order to avoid - unecessarily searching for macbeth charts at small scales. - If a macbeth chart ha already been found then set d_best to 0 - """ - if cor != 0: - d_best = 0 - - """ - scale 3/2 (approx root2) - """ - if cor < 0.75: - imgs = [] - """ - get size of image - """ - shape = list(img.shape[:2]) - w, h = shape - """ - set dimensions of the subselection and the step along each axis between - selections - """ - w_sel = int(2*w/3) - h_sel = int(2*h/3) - w_inc = int(w/6) - h_inc = int(h/6) - """ - for each subselection, look for a macbeth chart - loop over this and any brightened up images that we made to increase the - likelihood of success - """ - for img_br in all_images: - for i in range(3): - for j in range(3): - w_s, h_s = i*w_inc, j*h_inc - img_sel = img_br[w_s:w_s+w_sel, h_s:h_s+h_sel] - cor_ij, mac_ij, coords_ij, msg_ij = get_macbeth_chart(img_sel, ref_data) - """ - if the correlation is better than the best then record the - scale and current subselection at which macbeth chart was - found. Also record the coordinates, macbeth chart and message. - """ - if cor_ij > cor: - cor = cor_ij - mac, coords, msg = mac_ij, coords_ij, msg_ij - ii, jj = i, j - w_best, h_best = w_inc, h_inc - d_best = 1 - - """ - scale 2 - """ - if cor < 0.75: - imgs = [] - shape = list(img.shape[:2]) - w, h = shape - w_sel = int(w/2) - h_sel = int(h/2) - w_inc = int(w/8) - h_inc = int(h/8) - # Again, loop over any brightened up images as well - for img_br in all_images: - for i in range(5): - for j in range(5): - w_s, h_s = i*w_inc, j*h_inc - img_sel = img_br[w_s:w_s+w_sel, h_s:h_s+h_sel] - cor_ij, mac_ij, coords_ij, msg_ij = get_macbeth_chart(img_sel, ref_data) - if cor_ij > cor: - cor = cor_ij - mac, coords, msg = mac_ij, coords_ij, msg_ij - ii, jj = i, j - w_best, h_best = w_inc, h_inc - d_best = 2 - - """ - The following code checks for macbeth charts at even smaller scales. This - slows the code down significantly and has therefore been omitted by default, - however it is not unusably slow so might be useful if the macbeth chart - is too small to be picked up to by the current subselections. - Use this for macbeth charts with side lengths around 1/5 image dimensions - (and smaller...?) it is, however, recommended that macbeth charts take up as - large as possible a proportion of the image. - """ - - if small_chart: - - if cor < 0.75 and d_best > 1: - imgs = [] - shape = list(img.shape[:2]) - w, h = shape - w_sel = int(w/3) - h_sel = int(h/3) - w_inc = int(w/12) - h_inc = int(h/12) - for i in range(9): - for j in range(9): - w_s, h_s = i*w_inc, j*h_inc - img_sel = img[w_s:w_s+w_sel, h_s:h_s+h_sel] - cor_ij, mac_ij, coords_ij, msg_ij = get_macbeth_chart(img_sel, ref_data) - if cor_ij > cor: - cor = cor_ij - mac, coords, msg = mac_ij, coords_ij, msg_ij - ii, jj = i, j - w_best, h_best = w_inc, h_inc - d_best = 3 - - if cor < 0.75 and d_best > 2: - imgs = [] - shape = list(img.shape[:2]) - w, h = shape - w_sel = int(w/4) - h_sel = int(h/4) - w_inc = int(w/16) - h_inc = int(h/16) - for i in range(13): - for j in range(13): - w_s, h_s = i*w_inc, j*h_inc - img_sel = img[w_s:w_s+w_sel, h_s:h_s+h_sel] - cor_ij, mac_ij, coords_ij, msg_ij = get_macbeth_chart(img_sel, ref_data) - if cor_ij > cor: - cor = cor_ij - mac, coords, msg = mac_ij, coords_ij, msg_ij - ii, jj = i, j - w_best, h_best = w_inc, h_inc - - """ - Transform coordinates from subselection to original image - """ - if ii != -1: - for a in range(len(coords)): - for b in range(len(coords[a][0])): - coords[a][0][b][1] += ii*w_best - coords[a][0][b][0] += jj*h_best - - """ - initialise coords_fit variable - """ - coords_fit = None - # print('correlation: {}'.format(cor)) - """ - print error or success message - """ - print(msg) - Cam.log += '\n' + str(msg) - if msg == success_msg: - coords_fit = coords - Cam.log += '\nMacbeth chart vertices:\n' - Cam.log += '{}'.format(2*np.round(coords_fit[0][0]), 0) - """ - if correlation is lower than 0.75 there may be a risk of macbeth chart - corners not having been located properly. It might be worth running - with show set to true to check where the macbeth chart centres have - been located. - """ - print('Confidence: {:.3f}'.format(cor)) - Cam.log += '\nConfidence: {:.3f}'.format(cor) - if cor < 0.75: - print('Caution: Low confidence guess!') - Cam.log += 'WARNING: Low confidence guess!' - # cv2.imshow('MacBeth', mac) - # represent(mac, 'MacBeth chart') - - """ - extract data from coords_fit and plot on original image - """ - if show and coords_fit is not None: - copy = img.copy() - verts = coords_fit[0][0] - cents = coords_fit[1][0] - - """ - draw circles at vertices of macbeth chart - """ - for vert in verts: - p = tuple(np.round(vert).astype(np.int32)) - cv2.circle(copy, p, 10, 1, -1) - """ - draw circles at centres of squares - """ - for i in range(len(cents)): - cent = cents[i] - p = tuple(np.round(cent).astype(np.int32)) - """ - draw black circle on white square, white circle on black square an - grey circle everywhere else. - """ - if i == 3: - cv2.circle(copy, p, 8, 0, -1) - elif i == 23: - cv2.circle(copy, p, 8, 1, -1) - else: - cv2.circle(copy, p, 8, 0.5, -1) - copy, _ = reshape(copy, 400) - represent(copy) - - return(coords_fit) - - -def get_macbeth_chart(img, ref_data): - """ - function returns coordinates of macbeth chart vertices and square centres, - along with an error/success message for debugging purposes. Additionally, - it scores the match with a confidence value. - - Brief explanation of the macbeth chart locating algorithm: - - Find rectangles within image - - Take rectangles within percentage offset of median perimeter. The - assumption is that these will be the macbeth squares - - For each potential square, find the 24 possible macbeth centre locations - that would produce a square in that location - - Find clusters of potential macbeth chart centres to find the potential - macbeth centres with the most votes, i.e. the most likely ones - - For each potential macbeth centre, use the centres of the squares that - voted for it to find macbeth chart corners - - For each set of corners, transform the possible match into normalised - space and correlate with a reference chart to evaluate the match - - Select the highest correlation as the macbeth chart match, returning the - correlation as the confidence score - """ - - """ - get reference macbeth chart data - """ - (ref, ref_w, ref_h, ref_corns) = ref_data - - """ - the code will raise and catch a MacbethError in case of a problem, trying - to give some likely reasons why the problem occred, hence the try/except - """ - try: - """ - obtain image, convert to grayscale and normalise - """ - src = img - src, factor = reshape(src, 200) - original = src.copy() - a = 125/np.average(src) - src_norm = cv2.convertScaleAbs(src, alpha=a, beta=0) - """ - This code checks if there are seperate colour channels. In the past the - macbeth locator ran on jpgs and this makes it robust to different - filetypes. Note that running it on a jpg has 4x the pixels of the - average bayer channel so coordinates must be doubled. - - This is best done in img_load.py in the get_patches method. The - coordinates and image width, height must be divided by two if the - macbeth locator has been run on a demosaicked image. - """ - if len(src_norm.shape) == 3: - src_bw = cv2.cvtColor(src_norm, cv2.COLOR_BGR2GRAY) - else: - src_bw = src_norm - original_bw = src_bw.copy() - """ - obtain image edges - """ - sigma = 2 - src_bw = cv2.GaussianBlur(src_bw, (0, 0), sigma) - t1, t2 = 50, 100 - edges = cv2.Canny(src_bw, t1, t2) - """ - dilate edges to prevent self-intersections in contours - """ - k_size = 2 - kernel = np.ones((k_size, k_size)) - its = 1 - edges = cv2.dilate(edges, kernel, iterations=its) - """ - find Contours in image - """ - conts, _ = cv2.findContours(edges, cv2.RETR_TREE, - cv2.CHAIN_APPROX_NONE) - if len(conts) == 0: - raise MacbethError( - '\nWARNING: No macbeth chart found!' - '\nNo contours found in image\n' - 'Possible problems:\n' - '- Macbeth chart is too dark or bright\n' - '- Macbeth chart is occluded\n' - ) - """ - find quadrilateral contours - """ - epsilon = 0.07 - conts_per = [] - for i in range(len(conts)): - per = cv2.arcLength(conts[i], True) - poly = cv2.approxPolyDP(conts[i], epsilon*per, True) - if len(poly) == 4 and cv2.isContourConvex(poly): - conts_per.append((poly, per)) - - if len(conts_per) == 0: - raise MacbethError( - '\nWARNING: No macbeth chart found!' - '\nNo quadrilateral contours found' - '\nPossible problems:\n' - '- Macbeth chart is too dark or bright\n' - '- Macbeth chart is occluded\n' - '- Macbeth chart is out of camera plane\n' - ) - - """ - sort contours by perimeter and get perimeters within percent of median - """ - conts_per = sorted(conts_per, key=lambda x: x[1]) - med_per = conts_per[int(len(conts_per)/2)][1] - side = med_per/4 - perc = 0.1 - med_low, med_high = med_per*(1-perc), med_per*(1+perc) - squares = [] - for i in conts_per: - if med_low <= i[1] and med_high >= i[1]: - squares.append(i[0]) - - """ - obtain coordinates of nomralised macbeth and squares - """ - square_verts, mac_norm = get_square_verts(0.06) - """ - for each square guess, find 24 possible macbeth chart centres - """ - mac_mids = [] - squares_raw = [] - for i in range(len(squares)): - square = squares[i] - squares_raw.append(square) - """ - convert quads to rotated rectangles. This is required as the - 'squares' are usually quite irregular quadrilaterls, so performing - a transform would result in exaggerated warping and inaccurate - macbeth chart centre placement - """ - rect = cv2.minAreaRect(square) - square = cv2.boxPoints(rect).astype(np.float32) - """ - reorder vertices to prevent 'hourglass shape' - """ - square = sorted(square, key=lambda x: x[0]) - square_1 = sorted(square[:2], key=lambda x: x[1]) - square_2 = sorted(square[2:], key=lambda x: -x[1]) - square = np.array(np.concatenate((square_1, square_2)), np.float32) - square = np.reshape(square, (4, 2)).astype(np.float32) - squares[i] = square - """ - find 24 possible macbeth chart centres by trasnforming normalised - macbeth square vertices onto candidate square vertices found in image - """ - for j in range(len(square_verts)): - verts = square_verts[j] - p_mat = cv2.getPerspectiveTransform(verts, square) - mac_guess = cv2.perspectiveTransform(mac_norm, p_mat) - mac_guess = np.round(mac_guess).astype(np.int32) - """ - keep only if candidate macbeth is within image border - (deprecated) - """ - in_border = True - # for p in mac_guess[0]: - # pptest = cv2.pointPolygonTest( - # img_con, - # tuple(p), - # False - # ) - # if pptest == -1: - # in_border = False - # break - - if in_border: - mac_mid = np.mean(mac_guess, - axis=1) - mac_mids.append([mac_mid, (i, j)]) - - if len(mac_mids) == 0: - raise MacbethError( - '\nWARNING: No macbeth chart found!' - '\nNo possible macbeth charts found within image' - '\nPossible problems:\n' - '- Part of the macbeth chart is outside the image\n' - '- Quadrilaterals in image background\n' - ) - - """ - reshape data - """ - for i in range(len(mac_mids)): - mac_mids[i][0] = mac_mids[i][0][0] - - """ - find where midpoints cluster to identify most likely macbeth centres - """ - clustering = cluster.AgglomerativeClustering( - n_clusters=None, - compute_full_tree=True, - distance_threshold=side*2 - ) - mac_mids_list = [x[0] for x in mac_mids] - - if len(mac_mids_list) == 1: - """ - special case of only one valid centre found (probably not needed) - """ - clus_list = [] - clus_list.append([mac_mids, len(mac_mids)]) - - else: - clustering.fit(mac_mids_list) - # try: - # clustering.fit(mac_mids_list) - # except RuntimeWarning as error: - # return(0, None, None, error) - - """ - create list of all clusters - """ - clus_list = [] - if clustering.n_clusters_ > 1: - for i in range(clustering.labels_.max()+1): - indices = [j for j, x in enumerate(clustering.labels_) if x == i] - clus = [] - for index in indices: - clus.append(mac_mids[index]) - clus_list.append([clus, len(clus)]) - clus_list.sort(key=lambda x: -x[1]) - - elif clustering.n_clusters_ == 1: - """ - special case of only one cluster found - """ - # print('only 1 cluster') - clus_list.append([mac_mids, len(mac_mids)]) - else: - raise MacbethError( - '\nWARNING: No macebth chart found!' - '\nNo clusters found' - '\nPossible problems:\n' - '- NA\n' - ) - - """ - keep only clusters with enough votes - """ - clus_len_max = clus_list[0][1] - clus_tol = 0.7 - for i in range(len(clus_list)): - if clus_list[i][1] < clus_len_max * clus_tol: - clus_list = clus_list[:i] - break - cent = np.mean(clus_list[i][0], axis=0)[0] - clus_list[i].append(cent) - - """ - represent most popular cluster centroids - """ - # copy = original_bw.copy() - # copy = cv2.cvtColor(copy, cv2.COLOR_GRAY2RGB) - # copy = cv2.resize(copy, None, fx=2, fy=2) - # for clus in clus_list: - # centroid = tuple(2*np.round(clus[2]).astype(np.int32)) - # cv2.circle(copy, centroid, 7, (255, 0, 0), -1) - # cv2.circle(copy, centroid, 2, (0, 0, 255), -1) - # represent(copy) - - """ - get centres of each normalised square - """ - reference = get_square_centres(0.06) - - """ - for each possible macbeth chart, transform image into - normalised space and find correlation with reference - """ - max_cor = 0 - best_map = None - best_fit = None - best_cen_fit = None - best_ref_mat = None - - for clus in clus_list: - clus = clus[0] - sq_cents = [] - ref_cents = [] - i_list = [p[1][0] for p in clus] - for point in clus: - i, j = point[1] - """ - remove any square that voted for two different points within - the same cluster. This causes the same point in the image to be - mapped to two different reference square centres, resulting in - a very distorted perspective transform since cv2.findHomography - simply minimises error. - This phenomenon is not particularly likely to occur due to the - enforced distance threshold in the clustering fit but it is - best to keep this in just in case. - """ - if i_list.count(i) == 1: - square = squares_raw[i] - sq_cent = np.mean(square, axis=0) - ref_cent = reference[j] - sq_cents.append(sq_cent) - ref_cents.append(ref_cent) - - """ - At least four squares need to have voted for a centre in - order for a transform to be found - """ - if len(sq_cents) < 4: - raise MacbethError( - '\nWARNING: No macbeth chart found!' - '\nNot enough squares found' - '\nPossible problems:\n' - '- Macbeth chart is occluded\n' - '- Macbeth chart is too dark or bright\n' - ) - - ref_cents = np.array(ref_cents) - sq_cents = np.array(sq_cents) - """ - find best fit transform from normalised centres to image - """ - h_mat, mask = cv2.findHomography(ref_cents, sq_cents) - if 'None' in str(type(h_mat)): - raise MacbethError( - '\nERROR\n' - ) - - """ - transform normalised corners and centres into image space - """ - mac_fit = cv2.perspectiveTransform(mac_norm, h_mat) - mac_cen_fit = cv2.perspectiveTransform(np.array([reference]), h_mat) - """ - transform located corners into reference space - """ - ref_mat = cv2.getPerspectiveTransform( - mac_fit, - np.array([ref_corns]) - ) - map_to_ref = cv2.warpPerspective( - original_bw, ref_mat, - (ref_w, ref_h) - ) - """ - normalise brigthness - """ - a = 125/np.average(map_to_ref) - map_to_ref = cv2.convertScaleAbs(map_to_ref, alpha=a, beta=0) - """ - find correlation with bw reference macbeth - """ - cor = correlate(map_to_ref, ref) - """ - keep only if best correlation - """ - if cor > max_cor: - max_cor = cor - best_map = map_to_ref - best_fit = mac_fit - best_cen_fit = mac_cen_fit - best_ref_mat = ref_mat - - """ - rotate macbeth by pi and recorrelate in case macbeth chart is - upside-down - """ - mac_fit_inv = np.array( - ([[mac_fit[0][2], mac_fit[0][3], - mac_fit[0][0], mac_fit[0][1]]]) - ) - mac_cen_fit_inv = np.flip(mac_cen_fit, axis=1) - ref_mat = cv2.getPerspectiveTransform( - mac_fit_inv, - np.array([ref_corns]) - ) - map_to_ref = cv2.warpPerspective( - original_bw, ref_mat, - (ref_w, ref_h) - ) - a = 125/np.average(map_to_ref) - map_to_ref = cv2.convertScaleAbs(map_to_ref, alpha=a, beta=0) - cor = correlate(map_to_ref, ref) - if cor > max_cor: - max_cor = cor - best_map = map_to_ref - best_fit = mac_fit_inv - best_cen_fit = mac_cen_fit_inv - best_ref_mat = ref_mat - - """ - Check best match is above threshold - """ - cor_thresh = 0.6 - if max_cor < cor_thresh: - raise MacbethError( - '\nWARNING: Correlation too low' - '\nPossible problems:\n' - '- Bad lighting conditions\n' - '- Macbeth chart is occluded\n' - '- Background is too noisy\n' - '- Macbeth chart is out of camera plane\n' - ) - """ - Following code is mostly representation for debugging purposes - """ - - """ - draw macbeth corners and centres on image - """ - copy = original.copy() - copy = cv2.resize(original, None, fx=2, fy=2) - # print('correlation = {}'.format(round(max_cor, 2))) - for point in best_fit[0]: - point = np.array(point, np.float32) - point = tuple(2*np.round(point).astype(np.int32)) - cv2.circle(copy, point, 4, (255, 0, 0), -1) - for point in best_cen_fit[0]: - point = np.array(point, np.float32) - point = tuple(2*np.round(point).astype(np.int32)) - cv2.circle(copy, point, 4, (0, 0, 255), -1) - copy = copy.copy() - cv2.circle(copy, point, 4, (0, 0, 255), -1) - - """ - represent coloured macbeth in reference space - """ - best_map_col = cv2.warpPerspective( - original, best_ref_mat, (ref_w, ref_h) - ) - best_map_col = cv2.resize( - best_map_col, None, fx=4, fy=4 - ) - a = 125/np.average(best_map_col) - best_map_col_norm = cv2.convertScaleAbs( - best_map_col, alpha=a, beta=0 - ) - # cv2.imshow('Macbeth', best_map_col) - # represent(copy) - - """ - rescale coordinates to original image size - """ - fit_coords = (best_fit/factor, best_cen_fit/factor) - - return(max_cor, best_map_col_norm, fit_coords, success_msg) - - """ - catch macbeth errors and continue with code - """ - except MacbethError as error: - return(0, None, None, error) diff --git a/utils/raspberrypi/ctt/ctt_noise.py b/utils/raspberrypi/ctt/ctt_noise.py deleted file mode 100644 index 0b18d83f..00000000 --- a/utils/raspberrypi/ctt/ctt_noise.py +++ /dev/null @@ -1,123 +0,0 @@ -# SPDX-License-Identifier: BSD-2-Clause -# -# Copyright (C) 2019, Raspberry Pi Ltd -# -# camera tuning tool noise calibration - -from ctt_image_load import * -import matplotlib.pyplot as plt - - -""" -Find noise standard deviation and fit to model: - - noise std = a + b*sqrt(pixel mean) -""" -def noise(Cam, Img, plot): - Cam.log += '\nProcessing image: {}'.format(Img.name) - stds = [] - means = [] - """ - iterate through macbeth square patches - """ - for ch_patches in Img.patches: - for patch in ch_patches: - """ - renormalise patch - """ - patch = np.array(patch) - patch = (patch-Img.blacklevel_16)/Img.againQ8_norm - std = np.std(patch) - mean = np.mean(patch) - stds.append(std) - means.append(mean) - - """ - clean data and ensure all means are above 0 - """ - stds = np.array(stds) - means = np.array(means) - means = np.clip(np.array(means), 0, None) - sq_means = np.sqrt(means) - - """ - least squares fit model - """ - fit = np.polyfit(sq_means, stds, 1) - Cam.log += '\nBlack level = {}'.format(Img.blacklevel_16) - Cam.log += '\nNoise profile: offset = {}'.format(int(fit[1])) - Cam.log += ' slope = {:.3f}'.format(fit[0]) - """ - remove any values further than std from the fit - - anomalies most likely caused by: - > ucharacteristically noisy white patch - > saturation in the white patch - """ - fit_score = np.abs(stds - fit[0]*sq_means - fit[1]) - fit_std = np.std(stds) - fit_score_norm = fit_score - fit_std - anom_ind = np.where(fit_score_norm > 1) - fit_score_norm.sort() - sq_means_clean = np.delete(sq_means, anom_ind) - stds_clean = np.delete(stds, anom_ind) - removed = len(stds) - len(stds_clean) - if removed != 0: - Cam.log += '\nIdentified and removed {} anomalies.'.format(removed) - Cam.log += '\nRecalculating fit' - """ - recalculate fit with outliers removed - """ - fit = np.polyfit(sq_means_clean, stds_clean, 1) - Cam.log += '\nNoise profile: offset = {}'.format(int(fit[1])) - Cam.log += ' slope = {:.3f}'.format(fit[0]) - - """ - if fit const is < 0 then force through 0 by - dividing by sq_means and fitting poly order 0 - """ - corrected = 0 - if fit[1] < 0: - corrected = 1 - ones = np.ones(len(means)) - y_data = stds/sq_means - fit2 = np.polyfit(ones, y_data, 0) - Cam.log += '\nOffset below zero. Fit recalculated with zero offset' - Cam.log += '\nNoise profile: offset = 0' - Cam.log += ' slope = {:.3f}'.format(fit2[0]) - # print('new fit') - # print(fit2) - - """ - plot fit for debug - """ - if plot: - x = np.arange(sq_means.max()//0.88) - fit_plot = x*fit[0] + fit[1] - plt.scatter(sq_means, stds, label='data', color='blue') - plt.scatter(sq_means[anom_ind], stds[anom_ind], color='orange', label='anomalies') - plt.plot(x, fit_plot, label='fit', color='red', ls=':') - if fit[1] < 0: - fit_plot_2 = x*fit2[0] - plt.plot(x, fit_plot_2, label='fit 0 intercept', color='green', ls='--') - plt.plot(0, 0) - plt.title('Noise Plot\nImg: {}'.format(Img.str)) - plt.legend(loc='upper left') - plt.xlabel('Sqrt Pixel Value') - plt.ylabel('Noise Standard Deviation') - plt.grid() - plt.show() - """ - End of plotting code - """ - - """ - format output to include forced 0 constant - """ - Cam.log += '\n' - if corrected: - fit = [fit2[0], 0] - return fit - - else: - return fit diff --git a/utils/raspberrypi/ctt/ctt_pisp.py b/utils/raspberrypi/ctt/ctt_pisp.py deleted file mode 100755 index 8a5fc03f..00000000 --- a/utils/raspberrypi/ctt/ctt_pisp.py +++ /dev/null @@ -1,823 +0,0 @@ -#!/usr/bin/env python3 -# -# SPDX-License-Identifier: BSD-2-Clause -# -# Copyright (C) 2019, Raspberry Pi Ltd -# -# ctt_pisp.py - camera tuning tool data for PiSP platforms - - -json_template = { - "rpi.black_level": { - "black_level": 4096 - }, - "rpi.lux": { - "reference_shutter_speed": 10000, - "reference_gain": 1, - "reference_aperture": 1.0 - }, - "rpi.dpc": { - "strength": 1 - }, - "rpi.noise": { - }, - "rpi.geq": { - }, - "rpi.denoise": - { - "normal": - { - "sdn": - { - "deviation": 0.6, - "strength": 0.95, - "deviation2": 3.2, - "deviation_no_tdn": 3.2, - "strength_no_tdn": 0.95 - }, - "cdn": - { - "deviation": 200, - "strength": 0.3 - }, - "tdn": - { - "deviation": 1.0, - "threshold": 0.05 - } - }, - "hdr": - { - "sdn": - { - "deviation": 0.6, - "strength": 0.95, - "deviation2": 3.2, - "deviation_no_tdn": 3.2, - "strength_no_tdn": 0.95 - }, - "cdn": - { - "deviation": 200, - "strength": 0.3 - }, - "tdn": - { - "deviation": 1.3, - "threshold": 0.1 - } - }, - "night": - { - "sdn": - { - "deviation": 0.6, - "strength": 0.95, - "deviation2": 3.2, - "deviation_no_tdn": 3.2, - "strength_no_tdn": 0.95 - }, - "cdn": - { - "deviation": 200, - "strength": 0.3 - }, - "tdn": - { - "deviation": 1.3, - "threshold": 0.1 - } - } - }, - "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": 7700}, - "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": 8000} - }, - "bayes": 1 - }, - "rpi.agc": - { - "channels": - [ - { - "comment": "Channel 0 is normal AGC", - "metering_modes": - { - "centre-weighted": - { - "weights": - [ - 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, - 0, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 0, - 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, - 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, - 1, 1, 2, 2, 2, 2, 3, 3, 3, 2, 2, 2, 2, 1, 1, - 1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 2, 2, 2, 1, 1, - 1, 1, 2, 2, 3, 3, 3, 4, 3, 3, 3, 2, 2, 1, 1, - 1, 1, 2, 2, 3, 3, 4, 4, 4, 3, 3, 2, 2, 1, 1, - 1, 1, 2, 2, 3, 3, 3, 4, 3, 3, 3, 2, 2, 1, 1, - 1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 2, 2, 2, 1, 1, - 1, 1, 2, 2, 2, 2, 3, 3, 3, 2, 2, 2, 2, 1, 1, - 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, - 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, - 0, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 0, - 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0 - ] - }, - "spot": - { - "weights": - [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 1, 2, 1, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 1, 2, 3, 2, 1, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 1, 2, 1, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 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, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 - ] - } - }, - "exposure_modes": - { - "normal": - { - "shutter": [ 100, 10000, 30000, 60000, 66666 ], - "gain": [ 1.0, 1.5, 2.0, 4.0, 8.0 ] - }, - "short": - { - "shutter": [ 100, 5000, 10000, 20000, 60000 ], - "gain": [ 1.0, 1.5, 2.0, 4.0, 8.0 ] - }, - "long": - { - "shutter": [ 100, 10000, 30000, 60000, 90000, 120000 ], - "gain": [ 1.0, 1.5, 2.0, 4.0, 8.0, 12.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 - ] - }, - ], - "shadows": [ - { - "bound": "LOWER", - "q_lo": 0.0, - "q_hi": 0.5, - "y_target": - [ - 0, 0.17, - 1000, 0.17 - ] - } - ] - }, - "y_target": - [ - 0, 0.16, - 1000, 0.165, - 10000, 0.17 - ] - }, - { - "comment": "Channel 1 is the HDR short channel", - "desaturate": 0, - "metering_modes": - { - "centre-weighted": - { - "weights": - [ - 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, - 0, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 0, - 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, - 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, - 1, 1, 2, 2, 2, 2, 3, 3, 3, 2, 2, 2, 2, 1, 1, - 1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 2, 2, 2, 1, 1, - 1, 1, 2, 2, 3, 3, 3, 4, 3, 3, 3, 2, 2, 1, 1, - 1, 1, 2, 2, 3, 3, 4, 4, 4, 3, 3, 2, 2, 1, 1, - 1, 1, 2, 2, 3, 3, 3, 4, 3, 3, 3, 2, 2, 1, 1, - 1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 2, 2, 2, 1, 1, - 1, 1, 2, 2, 2, 2, 3, 3, 3, 2, 2, 2, 2, 1, 1, - 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, - 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, - 0, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 0, - 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0 - ] - }, - "spot": - { - "weights": - [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 1, 2, 1, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 1, 2, 3, 2, 1, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 1, 2, 1, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 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, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 - ] - } - }, - "exposure_modes": - { - "normal": - { - "shutter": [ 100, 20000, 60000 ], - "gain": [ 1.0, 1.0, 1.0 ] - }, - "short": - { - "shutter": [ 100, 20000, 60000 ], - "gain": [ 1.0, 1.0, 1.0 ] - }, - "long": - { - "shutter": [ 100, 20000, 60000 ], - "gain": [ 1.0, 1.0, 1.0 ] - } - }, - "constraint_modes": - { - "normal": [ - { - "bound": "LOWER", - "q_lo": 0.95, - "q_hi": 1.0, - "y_target": - [ - 0, 0.5, - 1000, 0.5 - ] - }, - { - "bound": "UPPER", - "q_lo": 0.95, - "q_hi": 1.0, - "y_target": - [ - 0, 0.7, - 1000, 0.7 - ] - }, - { - "bound": "LOWER", - "q_lo": 0.0, - "q_hi": 0.2, - "y_target": - [ - 0, 0.002, - 1000, 0.002 - ] - } - ], - "highlight": [ - { - "bound": "LOWER", - "q_lo": 0.95, - "q_hi": 1.0, - "y_target": - [ - 0, 0.5, - 1000, 0.5 - ] - }, - { - "bound": "UPPER", - "q_lo": 0.95, - "q_hi": 1.0, - "y_target": - [ - 0, 0.7, - 1000, 0.7 - ] - }, - { - "bound": "LOWER", - "q_lo": 0.0, - "q_hi": 0.2, - "y_target": - [ - 0, 0.002, - 1000, 0.002 - ] - } - ], - "shadows": [ - { - "bound": "LOWER", - "q_lo": 0.95, - "q_hi": 1.0, - "y_target": - [ - 0, 0.5, - 1000, 0.5 - ] - }, - { - "bound": "UPPER", - "q_lo": 0.95, - "q_hi": 1.0, - "y_target": - [ - 0, 0.7, - 1000, 0.7 - ] - }, - { - "bound": "LOWER", - "q_lo": 0.0, - "q_hi": 0.2, - "y_target": - [ - 0, 0.002, - 1000, 0.002 - ] - } - ] - }, - "y_target": - [ - 0, 0.16, - 1000, 0.165, - 10000, 0.17 - ] - }, - { - "comment": "Channel 2 is the HDR long channel", - "desaturate": 0, - "metering_modes": - { - "centre-weighted": - { - "weights": - [ - 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, - 0, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 0, - 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, - 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, - 1, 1, 2, 2, 2, 2, 3, 3, 3, 2, 2, 2, 2, 1, 1, - 1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 2, 2, 2, 1, 1, - 1, 1, 2, 2, 3, 3, 3, 4, 3, 3, 3, 2, 2, 1, 1, - 1, 1, 2, 2, 3, 3, 4, 4, 4, 3, 3, 2, 2, 1, 1, - 1, 1, 2, 2, 3, 3, 3, 4, 3, 3, 3, 2, 2, 1, 1, - 1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 2, 2, 2, 1, 1, - 1, 1, 2, 2, 2, 2, 3, 3, 3, 2, 2, 2, 2, 1, 1, - 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, - 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, - 0, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 0, - 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0 - ] - }, - "spot": - { - "weights": - [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 1, 2, 1, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 1, 2, 3, 2, 1, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 1, 2, 1, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 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, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 - ] - } - }, - "exposure_modes": - { - "normal": - { - "shutter": [ 100, 20000, 30000, 60000 ], - "gain": [ 1.0, 2.0, 4.0, 8.0 ] - }, - "short": - { - "shutter": [ 100, 20000, 30000, 60000 ], - "gain": [ 1.0, 2.0, 4.0, 8.0 ] - }, - "long": - { - "shutter": [ 100, 20000, 30000, 60000 ], - "gain": [ 1.0, 2.0, 4.0, 8.0 ] - } - }, - "constraint_modes": - { - "normal": [ - ], - "highlight": [ - ], - "shadows": [ - ] - }, - "channel_constraints": - [ - { - "bound": "UPPER", - "channel": 4, - "factor": 8 - }, - { - "bound": "LOWER", - "channel": 4, - "factor": 2 - } - ], - "y_target": - [ - 0, 0.16, - 1000, 0.165, - 10000, 0.17 - ] - }, - { - "comment": "Channel 3 is the night mode channel", - "base_ev": 0.33, - "metering_modes": - { - "centre-weighted": - { - "weights": - [ - 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, - 0, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 0, - 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, - 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, - 1, 1, 2, 2, 2, 2, 3, 3, 3, 2, 2, 2, 2, 1, 1, - 1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 2, 2, 2, 1, 1, - 1, 1, 2, 2, 3, 3, 3, 4, 3, 3, 3, 2, 2, 1, 1, - 1, 1, 2, 2, 3, 3, 4, 4, 4, 3, 3, 2, 2, 1, 1, - 1, 1, 2, 2, 3, 3, 3, 4, 3, 3, 3, 2, 2, 1, 1, - 1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 2, 2, 2, 1, 1, - 1, 1, 2, 2, 2, 2, 3, 3, 3, 2, 2, 2, 2, 1, 1, - 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, - 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, - 0, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 0, - 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0 - ] - }, - "spot": - { - "weights": - [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 1, 2, 1, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 1, 2, 3, 2, 1, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 1, 2, 1, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 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, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 - ] - } - }, - "exposure_modes": - { - "normal": - { - "shutter": [ 100, 20000, 66666 ], - "gain": [ 1.0, 2.0, 4.0 ] - }, - "short": - { - "shutter": [ 100, 20000, 33333 ], - "gain": [ 1.0, 2.0, 4.0 ] - }, - "long": - { - "shutter": [ 100, 20000, 66666, 120000 ], - "gain": [ 1.0, 2.0, 4.0, 4.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 - ] - } - ], - "shadows": [ - { - "bound": "LOWER", - "q_lo": 0.98, - "q_hi": 1.0, - "y_target": - [ - 0, 0.5, - 1000, 0.5 - ] - } - ] - }, - "y_target": - [ - 0, 0.16, - 1000, 0.16, - 10000, 0.17 - ] - } - ] - }, - "rpi.alsc": { - 'omega': 1.3, - 'n_iter': 100, - 'luminance_strength': 0.8, - }, - "rpi.contrast": { - "ce_enable": 1, - "gamma_curve": [ - 0, 0, - 512, 2518, - 1024, 5033, - 1536, 7175, - 2048, 9309, - 2560, 10814, - 3072, 12312, - 3584, 13773, - 4096, 15225, - 4608, 16566, - 5120, 17899, - 5632, 19221, - 6144, 20534, - 6656, 21684, - 7168, 22826, - 7680, 24024, - 8192, 25212, - 9216, 27251, - 10240, 29167, - 11264, 30947, - 12288, 32696, - 13312, 34309, - 14336, 35849, - 15360, 37194, - 16384, 38445, - 17408, 39598, - 18432, 40732, - 19456, 41717, - 20480, 42687, - 22528, 44343, - 24576, 45871, - 26624, 47222, - 28672, 48441, - 30720, 49460, - 32768, 50470, - 34816, 51476, - 36864, 52480, - 38912, 53382, - 40960, 54294, - 43008, 55155, - 45056, 56035, - 47104, 56920, - 49152, 57824, - 51200, 58737, - 53248, 59666, - 55296, 60604, - 57344, 61558, - 59392, 62529, - 61440, 63516, - 63488, 64519, - 65535, 65535 - ] - }, - "rpi.ccm": { - }, - "rpi.cac": { - }, - "rpi.sharpen": { - "threshold": 0.75, - "limit": 0.5, - "strength": 1.0 - }, - "rpi.hdr": - { - "Off": - { - "cadence": [ 0 ] - }, - "MultiExposureUnmerged": - { - "cadence": [ 1, 2 ], - "channel_map": { "short": 1, "long": 2 } - }, - "SingleExposure": - { - "cadence": [1], - "channel_map": { "short": 1 }, - "spatial_gain": 2.0, - "tonemap_enable": 1 - }, - "MultiExposure": - { - "cadence": [1, 2], - "channel_map": { "short": 1, "long": 2 }, - "stitch_enable": 1, - "spatial_gain": 2.0, - "tonemap_enable": 1 - }, - "Night": - { - "cadence": [ 3 ], - "channel_map": { "night": 3 }, - "tonemap_enable": 1, - "tonemap": - [ - 0, 0, - 5000, 20000, - 10000, 30000, - 20000, 47000, - 30000, 55000, - 65535, 65535 - ] - } - } -} - -grid_size = (32, 32) diff --git a/utils/raspberrypi/ctt/ctt_pretty_print_json.py b/utils/raspberrypi/ctt/ctt_pretty_print_json.py deleted file mode 100755 index a4cae62d..00000000 --- a/utils/raspberrypi/ctt/ctt_pretty_print_json.py +++ /dev/null @@ -1,130 +0,0 @@ -#!/usr/bin/env python3 -# -# SPDX-License-Identifier: BSD-2-Clause -# -# Copyright 2022 Raspberry Pi Ltd -# -# Script to pretty print a Raspberry Pi tuning config JSON structure in -# version 2.0 and later formats. - -import argparse -import json -import textwrap - - -class Encoder(json.JSONEncoder): - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.indentation_level = 0 - self.hard_break = 120 - self.custom_elems = { - 'weights': 15, - 'table': 16, - 'luminance_lut': 16, - 'ct_curve': 3, - 'ccm': 3, - 'lut_rx': 9, - 'lut_bx': 9, - 'lut_by': 9, - 'lut_ry': 9, - 'gamma_curve': 2, - 'y_target': 2, - 'prior': 2, - 'tonemap': 2 - } - - def encode(self, o, node_key=None): - if isinstance(o, (list, tuple)): - # Check if we are a flat list of numbers. - if not any(isinstance(el, (list, tuple, dict)) for el in o): - s = ', '.join(json.dumps(el) for el in o) - if node_key in self.custom_elems.keys(): - # Special case handling to specify number of elements in a row for tables, ccm, etc. - self.indentation_level += 1 - sl = s.split(', ') - num = self.custom_elems[node_key] - chunk = [self.indent_str + ', '.join(sl[x:x + num]) for x in range(0, len(sl), num)] - t = ',\n'.join(chunk) - self.indentation_level -= 1 - output = f'\n{self.indent_str}[\n{t}\n{self.indent_str}]' - elif len(s) > self.hard_break - len(self.indent_str): - # Break a long list with wraps. - self.indentation_level += 1 - t = textwrap.fill(s, self.hard_break, break_long_words=False, - initial_indent=self.indent_str, subsequent_indent=self.indent_str) - self.indentation_level -= 1 - output = f'\n{self.indent_str}[\n{t}\n{self.indent_str}]' - else: - # Smaller lists can remain on a single line. - output = f' [ {s} ]' - return output - else: - # Sub-structures in the list case. - self.indentation_level += 1 - output = [self.indent_str + self.encode(el) for el in o] - self.indentation_level -= 1 - output = ',\n'.join(output) - return f' [\n{output}\n{self.indent_str}]' - - elif isinstance(o, dict): - self.indentation_level += 1 - output = [] - for k, v in o.items(): - if isinstance(v, dict) and len(v) == 0: - # Empty config block special case. - output.append(self.indent_str + f'{json.dumps(k)}: {{ }}') - else: - # Only linebreak if the next node is a config block. - sep = f'\n{self.indent_str}' if isinstance(v, dict) else '' - output.append(self.indent_str + f'{json.dumps(k)}:{sep}{self.encode(v, k)}') - output = ',\n'.join(output) - self.indentation_level -= 1 - return f'{{\n{output}\n{self.indent_str}}}' - - else: - return ' ' + json.dumps(o) - - @property - def indent_str(self) -> str: - return ' ' * self.indentation_level * self.indent - - def iterencode(self, o, **kwargs): - return self.encode(o) - - -def pretty_print(in_json: dict, custom_elems={}) -> str: - - if 'version' not in in_json or \ - 'target' not in in_json or \ - 'algorithms' not in in_json or \ - in_json['version'] < 2.0: - raise RuntimeError('Incompatible JSON dictionary has been provided') - - encoder = Encoder(indent=4, sort_keys=False) - encoder.custom_elems |= custom_elems - return encoder.encode(in_json) #json.dumps(in_json, cls=Encoder, indent=4, sort_keys=False) - - -if __name__ == "__main__": - parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter, description= - 'Prettify a version 2.0 camera tuning config JSON file.') - parser.add_argument('-t', '--target', type=str, help='Target platform', choices=['pisp', 'vc4'], default='vc4') - parser.add_argument('input', type=str, help='Input tuning file.') - parser.add_argument('output', type=str, nargs='?', - help='Output converted tuning file. If not provided, the input file will be updated in-place.', - default=None) - args = parser.parse_args() - - with open(args.input, 'r') as f: - in_json = json.load(f) - - if args.target == 'pisp': - from ctt_pisp import grid_size - elif args.target == 'vc4': - from ctt_vc4 import grid_size - - out_json = pretty_print(in_json, custom_elems={'table': grid_size[0], 'luminance_lut': grid_size[0]}) - - with open(args.output if args.output is not None else args.input, 'w') as f: - f.write(out_json) diff --git a/utils/raspberrypi/ctt/ctt_ransac.py b/utils/raspberrypi/ctt/ctt_ransac.py deleted file mode 100644 index 01bba302..00000000 --- a/utils/raspberrypi/ctt/ctt_ransac.py +++ /dev/null @@ -1,71 +0,0 @@ -# SPDX-License-Identifier: BSD-2-Clause -# -# Copyright (C) 2019, Raspberry Pi Ltd -# -# camera tuning tool RANSAC selector for Macbeth chart locator - -import numpy as np - -scale = 2 - - -""" -constructs normalised macbeth chart corners for ransac algorithm -""" -def get_square_verts(c_err=0.05, scale=scale): - """ - define macbeth chart corners - """ - b_bord_x, b_bord_y = scale*8.5, scale*13 - s_bord = 6*scale - side = 41*scale - x_max = side*6 + 5*s_bord + 2*b_bord_x - y_max = side*4 + 3*s_bord + 2*b_bord_y - c1 = (0, 0) - c2 = (0, y_max) - c3 = (x_max, y_max) - c4 = (x_max, 0) - mac_norm = np.array((c1, c2, c3, c4), np.float32) - mac_norm = np.array([mac_norm]) - - square_verts = [] - square_0 = np.array(((0, 0), (0, side), - (side, side), (side, 0)), np.float32) - offset_0 = np.array((b_bord_x, b_bord_y), np.float32) - c_off = side * c_err - offset_cont = np.array(((c_off, c_off), (c_off, -c_off), - (-c_off, -c_off), (-c_off, c_off)), np.float32) - square_0 += offset_0 - square_0 += offset_cont - """ - define macbeth square corners - """ - for i in range(6): - shift_i = np.array(((i*side, 0), (i*side, 0), - (i*side, 0), (i*side, 0)), np.float32) - shift_bord = np.array(((i*s_bord, 0), (i*s_bord, 0), - (i*s_bord, 0), (i*s_bord, 0)), np.float32) - square_i = square_0 + shift_i + shift_bord - for j in range(4): - shift_j = np.array(((0, j*side), (0, j*side), - (0, j*side), (0, j*side)), np.float32) - shift_bord = np.array(((0, j*s_bord), - (0, j*s_bord), (0, j*s_bord), - (0, j*s_bord)), np.float32) - square_j = square_i + shift_j + shift_bord - square_verts.append(square_j) - # print('square_verts') - # print(square_verts) - return np.array(square_verts, np.float32), mac_norm - - -def get_square_centres(c_err=0.05, scale=scale): - """ - define macbeth square centres - """ - verts, mac_norm = get_square_verts(c_err, scale=scale) - - centres = np.mean(verts, axis=1) - # print('centres') - # print(centres) - return np.array(centres, np.float32) diff --git a/utils/raspberrypi/ctt/ctt_ref.pgm b/utils/raspberrypi/ctt/ctt_ref.pgm deleted file mode 100644 index 9b9f4920..00000000 --- a/utils/raspberrypi/ctt/ctt_ref.pgm +++ /dev/null @@ -1,5 +0,0 @@ -P5 -# Reference macbeth chart -120 80 -255 -  !#!" #!"&&$#$#'"%&#+2///..../.........-()))))))))))))))))))(((-,*)'(&)#($%(%"###""!%""&"&&!$" #!$ !"! $&**" !#5.,%+,-5"0>;@@>@AAAACBCB=&<<5x|64RYVTSRRRMMNLKJJLH+&0gijgdeffmmnpnkji`#3bY! 3FHHIIIHIJIIJHIII@#?=7}:5Wcbcbdcb`^^`^^_^Y,'6r'7|;8Xfeeegeccb`^aba]Z+)q#3GHIIIIJIIJJIHIJI@&5=8~;8Zgghggedbdcbda^\Z+(;y)9z"3GIIJJJJJKJJJJJJJ@'4>9|=8Zhighgeeeedeca__[/)Bv&:|#3GJJIIJKKKJJJKKJK@&6>9~<8Yghegggffihccab^\/*Cz'9$  6IKJJMMMKMKKMKKMLC&2@9<9Yghhhhijiegdcebc^0)G(7% 6JLMMNMMKMMNMMMMMD&2@:~=9Xfghhjiigdgddedc`1)M}(:¾& "8LNOONNOMONNMMNOND'3@;=:Ziiigheegegegggdc1,Q~)8%# "9NNNPPPQOOOOONNOOD'0?;=;[iigeeegghgdedgea0-P(8Ý' "#$:NNOQPPRPQPOOPQPPD*1A;;:Yfghgghgghghhdggc3.\~);¤(&%%;OQQQRSSRPQQQQSQQF)3B<=:Wfhghhhihggghfhee4/f*:ä&%%%?RSSSSSTTTTSSSTTRE)5B=@:Ygiihhiiiihihiiif72p}(9Ʃ'#%&?TUTTTUUQSTTTTTVSF*3F>A;[ghjiihiiiihihije50r)6ƫ& &#%?SVVVUUUUUTUUVVUUG*5F=A;Yhijiiijjiiiiijje81t~)5ư' '$$=OQRRQQPRSRSSSSSSG+6D@?;Wefgggggfffgeeefc41x{*5( &&&'++++,,*-,-00-0100*-SUX\]]`_ffgiooopo=;X\bedbadbca`]\]ZZ;;<::8:;9983433110/-,...1//12410/..--+)"",---,-./,,.-/-0-( &&%+/0103322011223233)(34534767::;;==:=B9;BFGEEGIKJKIJGIJCD=<:76566554111/0/1.*+00233300/00//..,+*#")(*)++,++))*++**'!!&$*w¼1-_addc`ceccdccedbb?A|B>=>?@@?====;<:;:<:11r+.( !'%*zɠ42gjmllklomooonpopmHGD>AEDEFEECEECCCDDEC46׿0:Ѿ,!!&&,|ʡ61inknnoopoppoqqrqoEEFACGFFFFFFDFDDDDDDC5709+!"%%-~ʡ42inopppppoqqqrrsrnABC?DGGGGFFFFDFFDDEDC481;+!!"#*|ʡ62imoppppqqqqrtrqtrGDH?CGGGGGGGGFFFFFFDB381<Խ, !)}ˢ63mooppqqqqqqrrtvtoDHJACHHGGHGGFFFDDGGFD293>׽, $){ˢ53jpppqprqrrrttuvuo>HJAFHHHHHGGHGGFGGFFE283:ڽ- "*{̣53loqpqsqrrrtrutsvrAHHCGHIHHHHHHGFGHGGGD5;28, +}ʡ52mqoqpqrttttttuurpFIOCEHHIHHHHGHGGFFIGF8<48ۿ, (|ʢ41krqpqqqrrtrtuvtuoEHPBHHIIIHIIHIHGHGHHE7<58* (zʡ63kpqprqqstttutrvvoFOLEHHIIHIHHHIGHGIHGF4=5<* 'zȡ62lppqrqrrrtttuttvpAGMGHIIIIHIIIHHIIJHHG4<4<+ !){Ƞ62jopqqqqqrtttutttrEHOHFIIIIIJIIIIHIHIHI7>5;, !)zƟ53lppqqrqrtttuuuutsFIRHGJIJHJKJJJIIIIIIH9>5;+  !({Ŝ41joppprqrrrutttvvrIHTHCJJJJJIJIJJIJJJIH7=5;+ (u65gjlmmmnoopnpprpqoIHOIBIJJJIJJJJIIIHHHG8929ʾ' "&,-*)-01/,0/12102-+04448789<>>??AFAD@DBCIJNRWTSUXT[WUQUOKFEBBABA?>>=<<;;67942:<<<>9999864565363&(13335422./1/-+..+ !"&$$""$"&$%'()(''*+-0124688:<>>??A>?EBCHKOLJLNOSQOXQQVMLACGHGHIGFHGDCCBB@??7432233210111.,++,++%(++)*(''%%%$$#%&$# ")0/001120024455520+-U]`addcdhefeekecYGFJRXYYVWWZWVXXVZTOBF}K7Ybccddfeg`^]^]\[Z[*)OTTPPQPOKOLLJJLIK  !1;:9:<<===;=???A@9*/FJmxyxwyzzzxyzzz{zxLO]=.-y# !!2><=;==>=<<>@@@@A9-0IKnz||{|{||{}}~}}{zLO]>..~% $2==;<>>?===>@A@AB;+1JJo{|y{||}{||}}}}}yMT_>-.}# %2<=;=<@?>==>?A@AA9+3FMlz{{y|}}}}||}|}}{MTd>-,# %1<<<;==<<=>?A?@AA:,3INo{{y{||||}|}}|~}{RTd=/-}#!$0<<<=<<==>A@@>@AA:-2HInzz{{||{{}~~}}|}zMRd=++~# "$/;<==>;===@@@@>AA:+2KHn||y|||||{}~}|}|xMSd=+,}# ! "/:<=>@<<>=@@@@@AA;-3MFs||{{{y}z}}|}|}}yMWc>,)|! !1;>?>><<>@>>=>ABB;,0LHr{|{|}|y|}}}}}zNXc?()z# $/;;<=;<>>=>>>@@BB:,1IInyz||||||{||}{~|{NVc;('}# $0:<==<;>@>>>>@ABB:,/HLlx|}y{y{|y{|}}}}yMRd>~*(y" !&3:;<<;==@@=>AABBA;-3KLqz{|||y{}|}{}|~{zRQc9w)'y" !%1<<;=>===<=@@ABBC<.5IIlz{|}~~~|}{||~}}zMUd;p)$x" $2===<==@=<>=ABBBC?/0IGkz}}{||}{||y||}zyOVc7o'&~~z"#"#/;<:<;===@?AAA>07GGgwxz{yyxyzzyz{yuuHO\8v'$w~~}|||{~|{zxxxxv!"""'*+(+)*))()+,,.../0398;=<=>DCCDDCBBDHBCJMMLMPNPOJPKPSJDICCNMPONMNNOKHIFDBHE3/46433323.....*+,)( !##!!!!!$#$$#$#&"!!"(+**,,*+.//1478:<:33ACDFGGIIHIJLPKNMQFIPTTRVXVUXUUTXUSTNEGGFDEFAA>==;94877520-,))*(((('&$#!!" &%'FQPQR]dq=FQNLEznki^[YTPUOS;.%-/12322221/10//,/%#0@QQMKEH01NNQOQQOOMNNLKLJGB'&/AWOLKEF-,PQQPQPPQPOONMNNKE''0CZRMJEF,*NSQPPQOOOOMNNMKID('2D[QKIFF,*NPPPPPPNOONMMMJIF!'(2F]RLHDF+%MPPPPOOONONNMMKID)*4D^PLICF+&NPOOOPPOONMMKMKHD**6D_QJFC~F,'MPOOOOONONNKKIIIG,+7D^QIEB|E+&MONOOONNNNKMJKJHH,-8D]PIHEC,#LOOOONONNNKKKMKJF,*6CaMHIFD*%KONOMNMMKMKJJJIJE,,6B^MGHB}D+&LONOOONNMMMMKLKIA,,6A\MFIEE+&LNNMONNMMKKKKKIHF --6A[KFJCF*&LMONMNMNKKJMKJJIF **5>WKEF?}C*%KONNNJKKKMKJKJKID,*4;DIJDB?FEGE=7>8634.(&&(%&*&%%'+*)+*#%()''03364443233222243/-+133423333423766645789:><<<;<;=;><7766666741012.-13/-+-/(''&&&%%&$.%0()-%-#-#' #&(% )))hnYQg7(*))))*)**,--....../0/0001357666::;;>?>AA866666666656565300/20/.-*)(('((&&%)d=yoPBB;>;:76666666666755303033/,.-*(())('&')#)"##(+$+*#)) &  diff --git a/utils/raspberrypi/ctt/ctt_tools.py b/utils/raspberrypi/ctt/ctt_tools.py deleted file mode 100644 index 50b01ecf..00000000 --- a/utils/raspberrypi/ctt/ctt_tools.py +++ /dev/null @@ -1,150 +0,0 @@ -# SPDX-License-Identifier: BSD-2-Clause -# -# Copyright (C) 2019, Raspberry Pi Ltd -# -# camera tuning tool miscellaneous - -import time -import re -import binascii -import os -import cv2 -import numpy as np -import imutils -import sys -import matplotlib.pyplot as plt -from sklearn import cluster as cluster -from sklearn.neighbors import NearestCentroid as get_centroids - -""" -This file contains some useful tools, the details of which aren't important to -understanding of the code. They ar collated here to attempt to improve code -readability in the main files. -""" - - -""" -obtain config values, unless it doesnt exist, in which case pick default -Furthermore, it can check if the input is the correct type -""" -def get_config(dictt, key, default, ttype): - try: - val = dictt[key] - if ttype == 'string': - val = str(val) - elif ttype == 'num': - if 'int' not in str(type(val)): - if 'float' not in str(type(val)): - raise ValueError - elif ttype == 'dict': - if not isinstance(val, dict): - raise ValueError - elif ttype == 'list': - if not isinstance(val, list): - raise ValueError - elif ttype == 'bool': - ttype = int(bool(ttype)) - else: - val = dictt[key] - except (KeyError, ValueError): - val = default - return val - - -""" -argument parser -""" -def parse_input(): - arguments = sys.argv[1:] - if len(arguments) % 2 != 0: - raise ArgError('\n\nERROR! Enter value for each arguent passed.') - params = arguments[0::2] - vals = arguments[1::2] - args_dict = dict(zip(params, vals)) - json_output = get_config(args_dict, '-o', None, 'string') - directory = get_config(args_dict, '-i', None, 'string') - config = get_config(args_dict, '-c', None, 'string') - log_path = get_config(args_dict, '-l', None, 'string') - target = get_config(args_dict, '-t', "vc4", 'string') - if directory is None: - raise ArgError('\n\nERROR! No input directory given.') - if json_output is None: - raise ArgError('\n\nERROR! No output json given.') - return json_output, directory, config, log_path, target - - -""" -custom arg and macbeth error class -""" -class ArgError(Exception): - pass -class MacbethError(Exception): - pass - - -""" -correlation function to quantify match -""" -def correlate(im1, im2): - f1 = im1.flatten() - f2 = im2.flatten() - cor = np.corrcoef(f1, f2) - return cor[0][1] - - -""" -get list of files from directory -""" -def get_photos(directory='photos'): - filename_list = [] - for filename in os.listdir(directory): - if 'jp' in filename or '.dng' in filename: - filename_list.append(filename) - return filename_list - - -""" -display image for debugging... read at your own risk... -""" -def represent(img, name='image'): - # if type(img) == tuple or type(img) == list: - # for i in range(len(img)): - # name = 'image {}'.format(i) - # cv2.imshow(name, img[i]) - # else: - # cv2.imshow(name, img) - # cv2.waitKey(0) - # cv2.destroyAllWindows() - # return 0 - """ - code above displays using opencv, but this doesn't catch users pressing 'x' - with their mouse to close the window.... therefore matplotlib is used.... - (thanks a lot opencv) - """ - grid = plt.GridSpec(22, 1) - plt.subplot(grid[:19, 0]) - plt.imshow(img, cmap='gray') - plt.axis('off') - plt.subplot(grid[21, 0]) - plt.title('press \'q\' to continue') - plt.axis('off') - plt.show() - - # f = plt.figure() - # ax = f.add_subplot(211) - # ax2 = f.add_subplot(122) - # ax.imshow(img, cmap='gray') - # ax.axis('off') - # ax2.set_figheight(2) - # ax2.title('press \'q\' to continue') - # ax2.axis('off') - # plt.show() - - -""" -reshape image to fixed width without distorting -returns image and scale factor -""" -def reshape(img, width): - factor = width/img.shape[0] - return cv2.resize(img, None, fx=factor, fy=factor), factor diff --git a/utils/raspberrypi/ctt/ctt_vc4.py b/utils/raspberrypi/ctt/ctt_vc4.py deleted file mode 100755 index dd0c5414..00000000 --- a/utils/raspberrypi/ctt/ctt_vc4.py +++ /dev/null @@ -1,133 +0,0 @@ -#!/usr/bin/env python3 -# -# SPDX-License-Identifier: BSD-2-Clause -# -# Copyright (C) 2019, Raspberry Pi Ltd -# -# ctt_vc4.py - camera tuning tool data for VC4 platforms - - -json_template = { - "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, 66666], - "gain": [1.0, 2.0, 4.0, 6.0, 8.0] - }, - "short": { - "shutter": [100, 5000, 10000, 20000, 66666], - "gain": [1.0, 2.0, 4.0, 6.0, 8.0] - }, - "long": { - "shutter": [ 100, 10000, 30000, 60000, 120000 ], - "gain": [ 1.0, 2.0, 4.0, 6.0, 8.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": { - "threshold": 0.75, - "limit": 0.5, - "strength": 1.0 - } -} - -grid_size = (16, 12) diff --git a/utils/raspberrypi/ctt/ctt_visualise.py b/utils/raspberrypi/ctt/ctt_visualise.py deleted file mode 100644 index ed2339fd..00000000 --- a/utils/raspberrypi/ctt/ctt_visualise.py +++ /dev/null @@ -1,43 +0,0 @@ -""" -Some code that will save virtual macbeth charts that show the difference between optimised matrices and non optimised matrices - -The function creates an image that is 1550 by 1050 pixels wide, and fills it with patches which are 200x200 pixels in size -Each patch contains the ideal color, the color from the original matrix, and the color from the final matrix -_________________ -| | -| Ideal Color | -|_______________| -| Old | new | -| Color | Color | -|_______|_______| - -Nice way of showing how the optimisation helps change the colors and the color matricies -""" -import numpy as np -from PIL import Image - - -def visualise_macbeth_chart(macbeth_rgb, original_rgb, new_rgb, output_filename): - image = np.zeros((1050, 1550, 3), dtype=np.uint8) - colorindex = -1 - for y in range(6): - for x in range(4): # Creates 6 x 4 grid of macbeth chart - colorindex += 1 - xlocation = 50 + 250 * x # Means there is 50px of black gap between each square, more like the real macbeth chart. - ylocation = 50 + 250 * y - for g in range(200): - for i in range(100): - image[xlocation + i, ylocation + g] = macbeth_rgb[colorindex] - xlocation = 150 + 250 * x - ylocation = 50 + 250 * y - for i in range(100): - for g in range(100): - image[xlocation + i, ylocation + g] = original_rgb[colorindex] # Smaller squares below to compare the old colors with the new ones - xlocation = 150 + 250 * x - ylocation = 150 + 250 * y - for i in range(100): - for g in range(100): - image[xlocation + i, ylocation + g] = new_rgb[colorindex] - - img = Image.fromarray(image, 'RGB') - img.save(str(output_filename) + 'Generated Macbeth Chart.png') diff --git a/utils/raspberrypi/delayedctrls_parse.py b/utils/raspberrypi/delayedctrls_parse.py deleted file mode 100644 index 1decf73f..00000000 --- a/utils/raspberrypi/delayedctrls_parse.py +++ /dev/null @@ -1,113 +0,0 @@ -# SPDX-License-Identifier: BSD-2-Clause - -import re -import sys -import os - -if len(sys.argv) != 2: - print("Usage: {} ".format(sys.argv[0])) - sys.exit() - -infile = sys.argv[1] -insplit = os.path.splitext(infile) -outfile = insplit[0] + '_parsed' + insplit[1] - -frame_re = re.compile(r'frame (\d+) started') - -delays = { - 'Analogue Gain': 1, - 'Exposure': 2, - 'Vertical Blanking': 2 -} - -ctrl_action = { - 'Write': {}, - 'Get': {}, - 'Queue': {}, - 'No-op': {} -} - -ctrl_re = { - 'Write': re.compile(r'Setting (.*?) to (\d+) at index (\d+)'), - 'No-op': re.compile(r'Queue is empty, (.*?) (.*?) (.*?)'), - 'Get': re.compile(r'Reading (.*?) to (\d+) at index (\d+)'), - 'Queue': re.compile(r'Queuing (.*?) to (\d+) at index (\d+)') -} - -frame_num = -1 - -max_delay = 0 -for k, d in delays.items(): - if max_delay < d: - max_delay = d - -with open(infile) as f: - lines = f.readlines() - -for line in lines: - r = frame_re.search(line) - if r: - frame_num = int(r.group(1)) - - for (key, re) in ctrl_re.items(): - r = re.search(line) - if r: - ctrl_action[key][(frame_num, r.group(1))] = (r.group(2), r.group(3)) - -with open(outfile, 'wt') as f: - queueIndex = 1 - f.write('{:<10}{:<15}{:<12}{:<18}{}\n'.format('Frame', 'Action', 'Gain', 'Exposure', 'Vblank')) - for frame in range(0, frame_num + 1): - for (k, a) in ctrl_action.items(): - str = '{:<10}{:<10}'.format(frame, k) - - for c in delays.keys(): - # Tabulate all results - str += '{:>5} {:<10}'.format(a[(frame, c)][0] if (frame, c) in a.keys() else '---', - '[' + (a[(frame, c)][1] if (frame, c) in a.keys() else '-') + ']') - - f.write(str.strip() + '\n') - -# Test the write -> get matches the set delay. -for (frame, c) in ctrl_action['Write'].keys(): - set_value = ctrl_action['Write'][(frame, c)][0] - delay_frame = frame + delays[c] - if (delay_frame <= frame_num): - if (delay_frame, c) in ctrl_action['Get']: - get_value = ctrl_action['Get'][(delay_frame, c)][0] - if get_value != set_value: - print('Error: {} written at frame {} to value {} != {} at frame {}' - .format(c, frame, set_value, get_value, delay_frame)) - else: - print('Warning: {} written at frame {} to value {} did not get logged on frame {} - dropped frame?' - .format(c, frame, set_value, delay_frame)) - -# Test the queue -> write matches the set delay. -for (frame, c) in ctrl_action['Queue'].keys(): - set_value = ctrl_action['Queue'][(frame, c)][0] - delay_frame = frame + max_delay - delays[c] + 1 - if (delay_frame <= frame_num): - if (delay_frame, c) in ctrl_action['Write']: - write_value = ctrl_action['Write'][(delay_frame, c)][0] - if write_value != set_value: - print('Info: {} queued at frame {} to value {} != {} written at frame {}' - ' - lagging behind or double queue on a single frame!' - .format(c, frame, set_value, write_value, delay_frame)) - else: - print('Warning: {} queued at frame {} to value {} did not get logged on frame {} - dropped frame?' - .format(c, frame, set_value, delay_frame)) - -# Test the get -> write matches the set delay going backwards. -for (frame, c) in ctrl_action['Get'].keys(): - get_value = ctrl_action['Get'][(frame, c)][0] - delay_frame = frame - delays[c] - if (delay_frame >= 6): - if (delay_frame, c) in ctrl_action['Write']: - write_value = ctrl_action['Write'][(delay_frame, c)][0] - if get_value != write_value: - print('Info: {} got at frame {} to value {} != {} written at frame {}' - ' - lagging behind or double queue on a single frame!' - .format(c, frame, get_value, write_value, delay_frame)) - else: - print('Warning: {} got at frame {} to value {} did not get written on frame {}' - .format(c, frame, get_value, delay_frame))