diff options
Diffstat (limited to 'utils/raspberrypi/ctt')
-rwxr-xr-x | utils/raspberrypi/ctt/alsc_only.py | 34 | ||||
-rw-r--r-- | utils/raspberrypi/ctt/colors.py | 30 | ||||
-rwxr-xr-x | utils/raspberrypi/ctt/convert_tuning.py | 46 | ||||
-rwxr-xr-x | utils/raspberrypi/ctt/ctt.py | 41 | ||||
-rw-r--r-- | utils/raspberrypi/ctt/ctt_alsc.py | 6 | ||||
-rw-r--r-- | utils/raspberrypi/ctt/ctt_awb.py | 4 | ||||
-rw-r--r-- | utils/raspberrypi/ctt/ctt_ccm.py | 262 | ||||
-rw-r--r-- | utils/raspberrypi/ctt/ctt_geq.py | 4 | ||||
-rw-r--r-- | utils/raspberrypi/ctt/ctt_image_load.py | 41 | ||||
-rw-r--r-- | utils/raspberrypi/ctt/ctt_lux.py | 4 | ||||
-rw-r--r-- | utils/raspberrypi/ctt/ctt_macbeth_locator.py | 73 | ||||
-rw-r--r-- | utils/raspberrypi/ctt/ctt_noise.py | 4 | ||||
-rwxr-xr-x[-rw-r--r--] | utils/raspberrypi/ctt/ctt_pretty_print_json.py | 194 | ||||
-rw-r--r-- | utils/raspberrypi/ctt/ctt_ransac.py | 4 | ||||
-rw-r--r-- | utils/raspberrypi/ctt/ctt_tools.py | 4 | ||||
-rw-r--r-- | utils/raspberrypi/ctt/ctt_visualise.py | 43 |
16 files changed, 592 insertions, 202 deletions
diff --git a/utils/raspberrypi/ctt/alsc_only.py b/utils/raspberrypi/ctt/alsc_only.py new file mode 100755 index 00000000..092aa40e --- /dev/null +++ b/utils/raspberrypi/ctt/alsc_only.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (C) 2022, Raspberry Pi (Trading) Limited +# +# alsc tuning tool + +from ctt import * + + +if __name__ == '__main__': + """ + initialise calibration + """ + if len(sys.argv) == 1: + print(""" + Pisp Camera Tuning Tool version 1.0 + + Required Arguments: + '-i' : Calibration image directory. + '-o' : Name of output json file. + + Optional Arguments: + '-c' : Config file for the CTT. If not passed, default parameters used. + '-l' : Name of output log file. If not passed, 'ctt_log.txt' used. + """) + quit(0) + else: + """ + parse input arguments + """ + json_output, directory, config, log_output = parse_input() + run_ctt(json_output, directory, config, log_output, alsc_only=True) diff --git a/utils/raspberrypi/ctt/colors.py b/utils/raspberrypi/ctt/colors.py new file mode 100644 index 00000000..cb4d236b --- /dev/null +++ b/utils/raspberrypi/ctt/colors.py @@ -0,0 +1,30 @@ +# 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 new file mode 100755 index 00000000..f4504d45 --- /dev/null +++ b/utils/raspberrypi/ctt/convert_tuning.py @@ -0,0 +1,46 @@ +#!/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 sys + +from ctt_pretty_print_json import pretty_print + + +def convert_v2(in_json: dict) -> str: + + if 'version' in in_json.keys() and in_json['version'] != 1.0: + print(f'The JSON config reports version {in_json["version"]} that is incompatible with this tool.') + sys.exit(-1) + + converted = { + 'version': 2.0, + 'target': 'bcm2835', + 'algorithms': [{algo: config} for algo, config in in_json.items()] + } + + return pretty_print(converted) + + +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.\n') + 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) + + out_json = convert_v2(in_json) + + 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 index 15064634..bbe960b0 100755 --- a/utils/raspberrypi/ctt/ctt.py +++ b/utils/raspberrypi/ctt/ctt.py @@ -2,9 +2,9 @@ # # SPDX-License-Identifier: BSD-2-Clause # -# Copyright (C) 2019, Raspberry Pi (Trading) Limited +# Copyright (C) 2019, Raspberry Pi Ltd # -# ctt.py - camera tuning tool +# camera tuning tool import os import sys @@ -15,7 +15,7 @@ from ctt_alsc import * from ctt_lux import * from ctt_noise import * from ctt_geq import * -from ctt_pretty_print_json import * +from ctt_pretty_print_json import pretty_print import random import json import re @@ -350,7 +350,7 @@ class Camera: alsc_out = alsc_all(self, do_alsc_colour, plot) cal_cr_list, cal_cb_list, luminance_lut, av_corn = alsc_out """ - write ouput to json and finish if not do_alsc_colour + write output to json and finish if not do_alsc_colour """ if not do_alsc_colour: self.json['rpi.alsc']['luminance_lut'] = luminance_lut @@ -511,13 +511,17 @@ class Camera: """ def write_json(self): """ - Write json dictionary to file + Write json dictionary to file using our version 2 format """ - jstring = json.dumps(self.json, sort_keys=False) - """ - make it pretty :) - """ - pretty_print_json(jstring, self.jf) + + out_json = { + "version": 2.0, + 'target': 'bcm2835', + "algorithms": [{name: data} for name, data in self.json.items()], + } + + with open(self.jf, 'w') as f: + f.write(pretty_print(out_json)) """ add a new section to the log file @@ -664,7 +668,7 @@ class Camera: - incorrect filename/extension - images from different cameras """ - def check_imgs(self): + 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)) @@ -672,10 +676,14 @@ class Camera: """ check usable images found """ - if len(self.imgs) == 0: + 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: + 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... """ @@ -704,7 +712,7 @@ class Camera: return 0 -def run_ctt(json_output, directory, config, log_output): +def run_ctt(json_output, directory, config, log_output, alsc_only=False): """ check input files are jsons """ @@ -766,6 +774,8 @@ def run_ctt(json_output, directory, config, log_output): try: Cam = Camera(json_output) 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) @@ -779,8 +789,9 @@ def run_ctt(json_output, directory, config, log_output): ccm also technically does an awb but it measures this from the macbeth chart in the image rather than using calibration data """ - if Cam.check_imgs(): - Cam.json['rpi.black_level']['black_level'] = Cam.blacklevel_16 + 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) diff --git a/utils/raspberrypi/ctt/ctt_alsc.py b/utils/raspberrypi/ctt/ctt_alsc.py index 89e86469..b0201ac4 100644 --- a/utils/raspberrypi/ctt/ctt_alsc.py +++ b/utils/raspberrypi/ctt/ctt_alsc.py @@ -1,8 +1,8 @@ # SPDX-License-Identifier: BSD-2-Clause # -# Copyright (C) 2019, Raspberry Pi (Trading) Limited +# Copyright (C) 2019, Raspberry Pi Ltd # -# ctt_alsc.py - camera tuning tool for ALSC (auto lens shading correction) +# camera tuning tool for ALSC (auto lens shading correction) from ctt_image_load import * import matplotlib.pyplot as plt @@ -132,7 +132,7 @@ def alsc(Cam, Img, do_alsc_colour, plot=False): """ average the green channels into one """ - av_ch_g = np.mean((channels[1:2]), axis=0) + av_ch_g = np.mean((channels[1:3]), axis=0) if do_alsc_colour: """ obtain 16x12 grid of intensities for each channel and subtract black level diff --git a/utils/raspberrypi/ctt/ctt_awb.py b/utils/raspberrypi/ctt/ctt_awb.py index 3c8cd902..5ba6f978 100644 --- a/utils/raspberrypi/ctt/ctt_awb.py +++ b/utils/raspberrypi/ctt/ctt_awb.py @@ -1,8 +1,8 @@ # SPDX-License-Identifier: BSD-2-Clause # -# Copyright (C) 2019, Raspberry Pi (Trading) Limited +# Copyright (C) 2019, Raspberry Pi Ltd # -# ctt_awb.py - camera tuning tool for AWB +# camera tuning tool for AWB from ctt_image_load import * import matplotlib.pyplot as plt diff --git a/utils/raspberrypi/ctt/ctt_ccm.py b/utils/raspberrypi/ctt/ctt_ccm.py index cebecfc2..59753e33 100644 --- a/utils/raspberrypi/ctt/ctt_ccm.py +++ b/utils/raspberrypi/ctt/ctt_ccm.py @@ -1,32 +1,68 @@ # SPDX-License-Identifier: BSD-2-Clause # -# Copyright (C) 2019, Raspberry Pi (Trading) Limited +# Copyright (C) 2019, Raspberry Pi Ltd # -# ctt_ccm.py - camera tuning tool for CCM (colour correction matrix) +# 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) - x = np.where(x < 0.04045, x/12.92, ((x+0.055)/1.055)**2.4) - x = x * ((2**16)-1) + 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): + global matrix_selection_types, typenum imgs = Cam.imgs """ standard macbeth chart colour values """ - m_rgb = np.array([ # these are in sRGB + m_rgb = np.array([ # these are in RGB [116, 81, 67], # dark skin [199, 147, 129], # light skin [91, 122, 156], # blue sky @@ -34,7 +70,7 @@ def ccm(Cam, cal_cr_list, cal_cb_list): [130, 128, 176], # blue flower [92, 190, 172], # bluish green [224, 124, 47], # orange - [68, 91, 170], # purplish blue + [68, 91, 170], # purplish blue [198, 82, 97], # moderate red [94, 58, 106], # purple [159, 189, 63], # yellow green @@ -52,16 +88,20 @@ def ccm(Cam, cal_cr_list, cal_cb_list): [82, 84, 86], # neutral 3.5 [49, 49, 51] # black 2 ]) - """ convert reference colours from srgb to rgb """ - m_srgb = degamma(m_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 @@ -76,8 +116,8 @@ def ccm(Cam, cal_cr_list, cal_cb_list): """ normalise tables so min value is 1 """ - cr_tab = cr_tab/np.min(cr_tab) - cb_tab = cb_tab/np.min(cb_tab) + cr_tab = cr_tab / np.min(cr_tab) + cb_tab = cb_tab / np.min(cb_tab) colour_cals[cr['ct']] = [cr_tab, cb_tab] """ @@ -94,6 +134,8 @@ def ccm(Cam, cal_cr_list, cal_cb_list): the function will simply return the macbeth patches """ r, b, g = get_alsc_patches(Img, colour_cals, grey=False) + # 256 values for each patch of sRGB values + """ do awb Note: awb is done by measuring the macbeth chart in the image, rather @@ -101,34 +143,123 @@ def ccm(Cam, cal_cr_list, cal_cb_list): 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_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)) + 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) - + 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(ccm) + ccm_tab[Img.col].append(optimised_ccm) else: - ccm_tab[Img.col] = [ccm] + ccm_tab[Img.col] = [optimised_ccm] Cam.log += '\n' Cam.log += '\nFinished processing images' @@ -137,8 +268,8 @@ def ccm(Cam, cal_cr_list, cal_cb_list): """ 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) + 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) @@ -156,20 +287,65 @@ def ccm(Cam, cal_cr_list, cal_cb_list): 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 calculate in rgb space, and are fit by hand. Although it is a 3x3 +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. -Should you want to fit them in another space (e.g. LAB) we wish you the best of -luck and send us the code when you are done! :-) +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) + 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) @@ -191,7 +367,7 @@ def do_ccm(r, g, b, m_srgb): b_rb = np.sum(b_rbs) b_gb = np.sum(b_gbs) - det = rb_2*gb_2 - rb_gb*rb_gb + det = rb_2 * gb_2 - rb_gb * rb_gb """ Raise error if matrix is singular... @@ -201,19 +377,19 @@ def do_ccm(r, g, b, m_srgb): 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 + 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_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_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 """ @@ -222,3 +398,9 @@ def do_ccm(r, g, b, m_srgb): 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_geq.py b/utils/raspberrypi/ctt/ctt_geq.py index 2aa668f1..5a91ebb4 100644 --- a/utils/raspberrypi/ctt/ctt_geq.py +++ b/utils/raspberrypi/ctt/ctt_geq.py @@ -1,8 +1,8 @@ # SPDX-License-Identifier: BSD-2-Clause # -# Copyright (C) 2019, Raspberry Pi (Trading) Limited +# Copyright (C) 2019, Raspberry Pi Ltd # -# ctt_geq.py - camera tuning tool for GEQ (green equalisation) +# camera tuning tool for GEQ (green equalisation) from ctt_tools import * import matplotlib.pyplot as plt diff --git a/utils/raspberrypi/ctt/ctt_image_load.py b/utils/raspberrypi/ctt/ctt_image_load.py index 66adb237..d76ece73 100644 --- a/utils/raspberrypi/ctt/ctt_image_load.py +++ b/utils/raspberrypi/ctt/ctt_image_load.py @@ -1,8 +1,8 @@ # SPDX-License-Identifier: BSD-2-Clause # -# Copyright (C) 2019-2020, Raspberry Pi (Trading) Limited +# Copyright (C) 2019-2020, Raspberry Pi Ltd # -# ctt_image_load.py - camera tuning tool image loading +# camera tuning tool image loading from ctt_tools import * from ctt_macbeth_locator import * @@ -301,17 +301,35 @@ def dng_load_image(Cam, im_str): metadata.read() Img.ver = 100 # random value - Img.w = metadata['Exif.SubImage1.ImageWidth'].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['Exif.SubImage1.ImageLength'].value - white = metadata['Exif.SubImage1.WhiteLevel'].value + 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['Exif.Photo.ExposureTime'].value*1000000) - Img.againQ8 = metadata['Exif.Photo.ISOSpeedRatings'].value*256/100 + 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['Exif.SubImage1.BlackLevel'].value[0]) + 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)), @@ -319,7 +337,7 @@ def dng_load_image(Cam, im_str): '2 1 1 0': (2, (3, 2, 1, 0)), '1 0 2 1': (3, (1, 0, 3, 2)) } - cfa_pattern = metadata['Exif.SubImage1.CFAPattern'].value + cfa_pattern = metadata[f'Exif.{subimage}.CFAPattern'].value Img.pattern = bayer_case[cfa_pattern][0] Img.order = bayer_case[cfa_pattern][1] @@ -358,6 +376,11 @@ def load_image(Cam, im_str, mac_config=None, show=False, mac=True, show_meta=Fal 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() diff --git a/utils/raspberrypi/ctt/ctt_lux.py b/utils/raspberrypi/ctt/ctt_lux.py index 4e7785ef..46be1512 100644 --- a/utils/raspberrypi/ctt/ctt_lux.py +++ b/utils/raspberrypi/ctt/ctt_lux.py @@ -1,8 +1,8 @@ # SPDX-License-Identifier: BSD-2-Clause # -# Copyright (C) 2019, Raspberry Pi (Trading) Limited +# Copyright (C) 2019, Raspberry Pi Ltd # -# ctt_lux.py - camera tuning tool for lux level +# camera tuning tool for lux level from ctt_tools import * diff --git a/utils/raspberrypi/ctt/ctt_macbeth_locator.py b/utils/raspberrypi/ctt/ctt_macbeth_locator.py index cae1d334..f22dbf31 100644 --- a/utils/raspberrypi/ctt/ctt_macbeth_locator.py +++ b/utils/raspberrypi/ctt/ctt_macbeth_locator.py @@ -1,8 +1,8 @@ # SPDX-License-Identifier: BSD-2-Clause # -# Copyright (C) 2019, Raspberry Pi (Trading) Limited +# Copyright (C) 2019, Raspberry Pi Ltd # -# ctt_macbeth_locator.py - camera tuning tool Macbeth chart locator +# camera tuning tool Macbeth chart locator from ctt_ransac import * from ctt_tools import * @@ -57,6 +57,10 @@ def find_macbeth(Cam, img, mac_config=(0, 0)): """ 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. @@ -71,6 +75,7 @@ def find_macbeth(Cam, img, mac_config=(0, 0)): 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 @@ -81,6 +86,7 @@ def find_macbeth(Cam, img, mac_config=(0, 0)): 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 @@ -128,23 +134,26 @@ def find_macbeth(Cam, img, mac_config=(0, 0)): 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 i in range(3): - for j in range(3): - 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 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 + 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 @@ -157,17 +166,19 @@ def find_macbeth(Cam, img, mac_config=(0, 0)): h_sel = int(h/2) w_inc = int(w/8) h_inc = int(h/8) - for i in range(5): - for j in range(5): - 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 = 2 + # 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 @@ -238,7 +249,7 @@ def find_macbeth(Cam, img, mac_config=(0, 0)): print error or success message """ print(msg) - Cam.log += '\n' + msg + Cam.log += '\n' + str(msg) if msg == success_msg: coords_fit = coords Cam.log += '\nMacbeth chart vertices:\n' @@ -606,7 +617,7 @@ def get_macbeth_chart(img, ref_data): '\nNot enough squares found' '\nPossible problems:\n' '- Macbeth chart is occluded\n' - '- Macbeth chart is too dark of bright\n' + '- Macbeth chart is too dark or bright\n' ) ref_cents = np.array(ref_cents) diff --git a/utils/raspberrypi/ctt/ctt_noise.py b/utils/raspberrypi/ctt/ctt_noise.py index 0afcf8f8..0b18d83f 100644 --- a/utils/raspberrypi/ctt/ctt_noise.py +++ b/utils/raspberrypi/ctt/ctt_noise.py @@ -1,8 +1,8 @@ # SPDX-License-Identifier: BSD-2-Clause # -# Copyright (C) 2019, Raspberry Pi (Trading) Limited +# Copyright (C) 2019, Raspberry Pi Ltd # -# ctt_noise.py - camera tuning tool noise calibration +# camera tuning tool noise calibration from ctt_image_load import * import matplotlib.pyplot as plt diff --git a/utils/raspberrypi/ctt/ctt_pretty_print_json.py b/utils/raspberrypi/ctt/ctt_pretty_print_json.py index d38ae617..3e3b8475 100644..100755 --- a/utils/raspberrypi/ctt/ctt_pretty_print_json.py +++ b/utils/raspberrypi/ctt/ctt_pretty_print_json.py @@ -1,106 +1,116 @@ +#!/usr/bin/env python3 +# # SPDX-License-Identifier: BSD-2-Clause # -# Copyright (C) 2019, Raspberry Pi (Trading) Limited +# Copyright 2022 Raspberry Pi Ltd # -# ctt_pretty_print_json.py - camera tuning tool JSON formatter - -import sys - - -class JSONPrettyPrinter(object): - """ - Take a collapsed JSON file and make it more readable - """ - def __init__(self, fout): - self.state = { - "indent": 0, - "inarray": [False], - "arraycount": [], - "skipnewline": True, - "need_indent": False, - "need_space": False, +# 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 = { + 'table': 16, + 'luminance_lut': 16, + 'ct_curve': 3, + 'ccm': 3, + 'gamma_curve': 2, + 'y_target': 2, + 'prior': 2 } - self.fout = fout - - def newline(self): - if not self.state["skipnewline"]: - self.fout.write('\n') - self.state["need_indent"] = True - self.state["need_space"] = False - self.state["skipnewline"] = True - - def write(self, c): - if self.state["need_indent"]: - self.fout.write(' ' * self.state["indent"] * 4) - self.state["need_indent"] = False - if self.state["need_space"]: - self.fout.write(' ') - self.state["need_space"] = False - self.fout.write(c) - self.state["skipnewline"] = False - - def process_char(self, c): - if c == '{': - self.newline() - self.write(c) - self.state["indent"] += 1 - self.newline() - elif c == '}': - self.state["indent"] -= 1 - self.newline() - self.write(c) - elif c == '[': - self.newline() - self.write(c) - self.state["indent"] += 1 - self.newline() - self.state["inarray"] = [True] + self.state["inarray"] - self.state["arraycount"] = [0] + self.state["arraycount"] - elif c == ']': - self.state["indent"] -= 1 - self.newline() - self.state["inarray"].pop(0) - self.state["arraycount"].pop(0) - self.write(c) - elif c == ':': - self.write(c) - self.state["need_space"] = True - elif c == ',': - if not self.state["inarray"][0]: - self.write(c) - self.newline() + 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: - self.write(c) - self.state["arraycount"][0] += 1 - if self.state["arraycount"][0] == 16: - self.state["arraycount"][0] = 0 - self.newline() + # 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: - self.state["need_space"] = True - elif c.isspace(): - pass + # 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: - self.write(c) + 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) -> 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') - def print(self, string): - for c in string: - self.process_char(c) - self.newline() + return json.dumps(in_json, cls=Encoder, indent=4, sort_keys=False) -def pretty_print_json(str_in, output_filename): - with open(output_filename, "w") as fout: - printer = JSONPrettyPrinter(fout) - printer.print(str_in) +if __name__ == "__main__": + parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter, description= + 'Prettify a version 2.0 camera tuning config JSON file.') + 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 __name__ == '__main__': - if len(sys.argv) != 2: - print("Usage: %s filename" % sys.argv[0]) - sys.exit(1) + out_json = pretty_print(in_json) - input_filename = sys.argv[1] - with open(input_filename, "r") as fin: - printer = JSONPrettyPrinter(sys.stdout) - printer.print(fin.read()) + 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 index 11515a4f..01bba302 100644 --- a/utils/raspberrypi/ctt/ctt_ransac.py +++ b/utils/raspberrypi/ctt/ctt_ransac.py @@ -1,8 +1,8 @@ # SPDX-License-Identifier: BSD-2-Clause # -# Copyright (C) 2019, Raspberry Pi (Trading) Limited +# Copyright (C) 2019, Raspberry Pi Ltd # -# ctt_ransac.py - camera tuning tool RANSAC selector for Macbeth chart locator +# camera tuning tool RANSAC selector for Macbeth chart locator import numpy as np diff --git a/utils/raspberrypi/ctt/ctt_tools.py b/utils/raspberrypi/ctt/ctt_tools.py index 8728ff16..27c52193 100644 --- a/utils/raspberrypi/ctt/ctt_tools.py +++ b/utils/raspberrypi/ctt/ctt_tools.py @@ -1,8 +1,8 @@ # SPDX-License-Identifier: BSD-2-Clause # -# Copyright (C) 2019, Raspberry Pi (Trading) Limited +# Copyright (C) 2019, Raspberry Pi Ltd # -# ctt_tools.py - camera tuning tool miscellaneous +# camera tuning tool miscellaneous import time import re diff --git a/utils/raspberrypi/ctt/ctt_visualise.py b/utils/raspberrypi/ctt/ctt_visualise.py new file mode 100644 index 00000000..ed2339fd --- /dev/null +++ b/utils/raspberrypi/ctt/ctt_visualise.py @@ -0,0 +1,43 @@ +""" +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') |