summaryrefslogtreecommitdiff
path: root/utils/raspberrypi/ctt
diff options
context:
space:
mode:
Diffstat (limited to 'utils/raspberrypi/ctt')
-rwxr-xr-xutils/raspberrypi/ctt/alsc_only.py34
-rw-r--r--utils/raspberrypi/ctt/colors.py30
-rwxr-xr-xutils/raspberrypi/ctt/convert_tuning.py46
-rwxr-xr-xutils/raspberrypi/ctt/ctt.py41
-rw-r--r--utils/raspberrypi/ctt/ctt_alsc.py6
-rw-r--r--utils/raspberrypi/ctt/ctt_awb.py4
-rw-r--r--utils/raspberrypi/ctt/ctt_ccm.py262
-rw-r--r--utils/raspberrypi/ctt/ctt_geq.py4
-rw-r--r--utils/raspberrypi/ctt/ctt_image_load.py41
-rw-r--r--utils/raspberrypi/ctt/ctt_lux.py4
-rw-r--r--utils/raspberrypi/ctt/ctt_macbeth_locator.py73
-rw-r--r--utils/raspberrypi/ctt/ctt_noise.py4
-rwxr-xr-x[-rw-r--r--]utils/raspberrypi/ctt/ctt_pretty_print_json.py194
-rw-r--r--utils/raspberrypi/ctt/ctt_ransac.py4
-rw-r--r--utils/raspberrypi/ctt/ctt_tools.py4
-rw-r--r--utils/raspberrypi/ctt/ctt_visualise.py43
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')