From b95032a84259d5a6281bfb5b126711a47da02285 Mon Sep 17 00:00:00 2001
From: Ben Benson <ben.benson@raspberrypi.com>
Date: Thu, 6 Jun 2024 11:15:09 +0100
Subject: utils: raspberrypi: ctt: Changed CTT handling of VC4 and PiSP

Changed how users select which platform to tune for. Now users
specify a command line argument, '-t', to specify which target
platform.

Signed-off-by: Ben Benson <ben.benson@raspberrypi.com>
Signed-off-by: David Plowman <david.plowman@raspberrypi.com>
Reviewed-by: Naushir Patuck <naush@raspberrypi.com>
Tested-by: Naushir Patuck <naush@raspberrypi.com>
Acked-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
Signed-off-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
---
 utils/raspberrypi/ctt/alsc_only.py             |  42 ++
 utils/raspberrypi/ctt/alsc_pisp.py             |  37 --
 utils/raspberrypi/ctt/alsc_vc4.py              |  37 --
 utils/raspberrypi/ctt/cac_only.py              |   9 +-
 utils/raspberrypi/ctt/ctt.py                   | 800 +++++++++++++++++++++++++
 utils/raspberrypi/ctt/ctt_image_load.py        |   1 -
 utils/raspberrypi/ctt/ctt_log.txt              |  31 -
 utils/raspberrypi/ctt/ctt_pisp.py              |  33 +-
 utils/raspberrypi/ctt/ctt_pretty_print_json.py |   8 +-
 utils/raspberrypi/ctt/ctt_run.py               | 772 ------------------------
 utils/raspberrypi/ctt/ctt_tools.py             |   3 +-
 utils/raspberrypi/ctt/ctt_vc4.py               |  33 +-
 12 files changed, 857 insertions(+), 949 deletions(-)
 create mode 100755 utils/raspberrypi/ctt/alsc_only.py
 delete mode 100755 utils/raspberrypi/ctt/alsc_pisp.py
 delete mode 100755 utils/raspberrypi/ctt/alsc_vc4.py
 create mode 100755 utils/raspberrypi/ctt/ctt.py
 delete mode 100644 utils/raspberrypi/ctt/ctt_log.txt
 delete mode 100755 utils/raspberrypi/ctt/ctt_run.py

(limited to 'utils/raspberrypi/ctt')

diff --git a/utils/raspberrypi/ctt/alsc_only.py b/utils/raspberrypi/ctt/alsc_only.py
new file mode 100755
index 00000000..a521c4ad
--- /dev/null
+++ b/utils/raspberrypi/ctt/alsc_only.py
@@ -0,0 +1,42 @@
+#!/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/alsc_pisp.py b/utils/raspberrypi/ctt/alsc_pisp.py
deleted file mode 100755
index d0034ae1..00000000
--- a/utils/raspberrypi/ctt/alsc_pisp.py
+++ /dev/null
@@ -1,37 +0,0 @@
-#!/usr/bin/env python3
-#
-# SPDX-License-Identifier: BSD-2-Clause
-#
-# Copyright (C) 2022, Raspberry Pi Ltd
-#
-# alsc_only.py - alsc tuning tool
-
-import sys
-
-from ctt_pisp import json_template, grid_size, target
-from ctt_run import run_ctt
-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:
-    '-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, json_template, grid_size, target, alsc_only=True)
diff --git a/utils/raspberrypi/ctt/alsc_vc4.py b/utils/raspberrypi/ctt/alsc_vc4.py
deleted file mode 100755
index caf6a174..00000000
--- a/utils/raspberrypi/ctt/alsc_vc4.py
+++ /dev/null
@@ -1,37 +0,0 @@
-#!/usr/bin/env python3
-#
-# SPDX-License-Identifier: BSD-2-Clause
-#
-# Copyright (C) 2022, Raspberry Pi (Trading) Limited
-#
-# alsc tuning tool
-
-import sys
-
-from ctt_vc4 import json_template, grid_size, target
-from ctt_run import run_ctt
-from ctt_tools import parse_input
-
-if __name__ == '__main__':
-    """
-    initialise calibration
-    """
-    if len(sys.argv) == 1:
-        print("""
-    VC4 Lens Shading 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, json_template, grid_size, target, alsc_only=True)
diff --git a/utils/raspberrypi/ctt/cac_only.py b/utils/raspberrypi/ctt/cac_only.py
index 2bb11ccc..1c0a8193 100644
--- a/utils/raspberrypi/ctt/cac_only.py
+++ b/utils/raspberrypi/ctt/cac_only.py
@@ -2,7 +2,7 @@
 #
 # SPDX-License-Identifier: BSD-2-Clause
 #
-# Copyright (C) 2023, Raspberry Pi (Trading) Limited
+# Copyright (C) 2023, Raspberry Pi (Trading) Ltd.
 #
 # cac_only.py - cac tuning tool
 
@@ -102,11 +102,11 @@ def cac(filelist, output_filepath, plot_results=False):
     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 YAML")
+    print("Successfully converted to JSON")
     f = open(str(output_filepath), "w+")
     f.write(sample)
     f.close()
-    print("Successfully written to yaml file")
+    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
@@ -139,5 +139,4 @@ if __name__ == "__main__":
             plot_results = True
 
     arg_output = argv[output_location + 1]
-    logfile = open("log.txt", "a+")
-    cac(filelist, arg_output, plot_results, logfile)
+    cac(filelist, arg_output, plot_results)
diff --git a/utils/raspberrypi/ctt/ctt.py b/utils/raspberrypi/ctt/ctt.py
new file mode 100755
index 00000000..522933bd
--- /dev/null
+++ b/utils/raspberrypi/ctt/ctt.py
@@ -0,0 +1,800 @@
+#!/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
+        """
+        self.json['rpi.cac']['cac'] = cacs
+        self.log += '\nCAC calibration written to json file'
+        print('Finished CAC calibration')
+
+
+    """
+    Auto white balance calibration produces a colour curve for
+    various colour temperatures, as well as providing a maximum 'wiggle room'
+    distance from this curve (transverse_neg/pos).
+    """
+    def awb_cal(self, greyworld, do_alsc_colour, 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):
+        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)
+        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])
+        self.json['rpi.noise']['reference_slope'] = round(noise_out[0], 3)
+        self.log += '\nNOISE calibrations written to json'
+        print('Finished NOISE calibrations')
+
+    """
+    Removes json entries that are turned off
+    """
+    def json_remove(self, disable):
+        self.log_new_sec('Disabling Options', cal=False)
+        if len(self.disable) == 0:
+            self.log += '\nNothing disabled!'
+            return 1
+        for key in disable:
+            try:
+                del self.json[key]
+                self.log += '\nDisabled: ' + key
+            except KeyError:
+                self.log += '\nERROR: ' + key + ' not found!'
+    """
+    writes the json dictionary to the raw json file then make pretty
+    """
+    def write_json(self, 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')
+    blacklevel = get_config(configs, "blacklevel", -1, 'num')
+    macbeth_d = get_config(configs, "macbeth", {}, 'dict')
+    mac_small = get_config(macbeth_d, "small", 0, 'bool')
+    mac_show = get_config(macbeth_d, "show", 0, 'bool')
+    mac_config = (mac_small, mac_show)
+
+    if blacklevel < -1 or blacklevel >= 2**16:
+        print('\nInvalid blacklevel, defaulted to 64')
+        blacklevel = -1
+
+    if luminance_strength < 0 or luminance_strength > 1:
+        print('\nInvalid luminance_strength strength, defaulted to 0.5')
+        luminance_strength = 0.5
+
+    """
+    sanitise directory path
+    """
+    if directory[-1] != '/':
+        directory += '/'
+    """
+    initialise tuning tool and load images
+    """
+    try:
+        Cam = Camera(json_output, 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)
+        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_image_load.py b/utils/raspberrypi/ctt/ctt_image_load.py
index ea5fa360..531de328 100644
--- a/utils/raspberrypi/ctt/ctt_image_load.py
+++ b/utils/raspberrypi/ctt/ctt_image_load.py
@@ -351,7 +351,6 @@ def dng_load_image(Cam, im_str):
         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()
-        Img.sizes = raw_im.sizes
 
     except Exception:
         print("\nERROR: failed to load DNG file", im_str)
diff --git a/utils/raspberrypi/ctt/ctt_log.txt b/utils/raspberrypi/ctt/ctt_log.txt
deleted file mode 100644
index 682e24e4..00000000
--- a/utils/raspberrypi/ctt/ctt_log.txt
+++ /dev/null
@@ -1,31 +0,0 @@
-Log created : Fri Aug 25 17:02:58 2023
-
-----------------------------------------------------------------------
-User Arguments
-----------------------------------------------------------------------
-
-Json file output: output.json
-Calibration images directory: ../ctt/
-No configuration file input... using default options
-No log file path input... using default: ctt_log.txt
-
-----------------------------------------------------------------------
-Image Loading
-----------------------------------------------------------------------
-
-Directory: ../ctt/
-Files found: 1
-
-Image: alsc_3000k_0.dng
-Identified as an ALSC image
-Colour temperature: 3000 K
-
-Images found:
-Macbeth : 0
-ALSC : 1 
-CAC: 0 
-
-Camera metadata
-ERROR: No usable macbeth chart images found
-
-----------------------------------------------------------------------
diff --git a/utils/raspberrypi/ctt/ctt_pisp.py b/utils/raspberrypi/ctt/ctt_pisp.py
index 862587a6..4c432f17 100755
--- a/utils/raspberrypi/ctt/ctt_pisp.py
+++ b/utils/raspberrypi/ctt/ctt_pisp.py
@@ -4,13 +4,8 @@
 #
 # Copyright (C) 2019, Raspberry Pi Ltd
 #
-# ctt_pisp.py - camera tuning tool for PiSP platforms
+# ctt_pisp.py - camera tuning tool data for PiSP platforms
 
-import os
-import sys
-
-from ctt_run import run_ctt
-from ctt_tools import parse_input
 
 json_template = {
     "rpi.black_level": {
@@ -207,29 +202,3 @@ json_template = {
 }
 
 grid_size = (32, 32)
-
-target = 'pisp'
-
-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, json_template, grid_size, target)
diff --git a/utils/raspberrypi/ctt/ctt_pretty_print_json.py b/utils/raspberrypi/ctt/ctt_pretty_print_json.py
index d3bd7d97..350cec65 100755
--- a/utils/raspberrypi/ctt/ctt_pretty_print_json.py
+++ b/utils/raspberrypi/ctt/ctt_pretty_print_json.py
@@ -108,6 +108,7 @@ def pretty_print(in_json: dict, custom_elems={}) -> str:
 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.',
@@ -117,7 +118,12 @@ if __name__ == "__main__":
     with open(args.input, 'r') as f:
         in_json = json.load(f)
 
-    out_json = pretty_print(in_json)
+    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_run.py b/utils/raspberrypi/ctt/ctt_run.py
deleted file mode 100755
index 074136a1..00000000
--- a/utils/raspberrypi/ctt/ctt_run.py
+++ /dev/null
@@ -1,772 +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 += '\nCCM 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.\nCCM calibration '
-            self.log += 'performed without ALSC correction...'
-
-        """
-        Write output to json
-        """
-        self.json['rpi.cac']['cac'] = cacs
-        self.log += '\nCCM calibration written to json file'
-        print('Finished CCM calibration')
-
-
-    """
-    Auto white balance calibration produces a colour curve for
-    various colour temperatures, as well as providing a maximum 'wiggle room'
-    distance from this curve (transverse_neg/pos).
-    """
-    def awb_cal(self, greyworld, do_alsc_colour, 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):
-        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)
-        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])
-        self.json['rpi.noise']['reference_slope'] = round(noise_out[0], 3)
-        self.log += '\nNOISE calibrations written to json'
-        print('Finished NOISE calibrations')
-
-    """
-    Removes json entries that are turned off
-    """
-    def json_remove(self, disable):
-        self.log_new_sec('Disabling Options', cal=False)
-        if len(self.disable) == 0:
-            self.log += '\nNothing disabled!'
-            return 1
-        for key in disable:
-            try:
-                del self.json[key]
-                self.log += '\nDisabled: ' + key
-            except KeyError:
-                self.log += '\nERROR: ' + key + ' not found!'
-    """
-    writes the json dictionary to the raw json file then make pretty
-    """
-    def write_json(self, 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')
-    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)
-    cac_d = get_config(configs, "cac", {}, 'dict')
-
-    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)
-        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)
diff --git a/utils/raspberrypi/ctt/ctt_tools.py b/utils/raspberrypi/ctt/ctt_tools.py
index 27c52193..50b01ecf 100644
--- a/utils/raspberrypi/ctt/ctt_tools.py
+++ b/utils/raspberrypi/ctt/ctt_tools.py
@@ -65,11 +65,12 @@ def parse_input():
     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
+    return json_output, directory, config, log_path, target
 
 
 """
diff --git a/utils/raspberrypi/ctt/ctt_vc4.py b/utils/raspberrypi/ctt/ctt_vc4.py
index 86acfd47..7154e110 100755
--- a/utils/raspberrypi/ctt/ctt_vc4.py
+++ b/utils/raspberrypi/ctt/ctt_vc4.py
@@ -4,13 +4,8 @@
 #
 # Copyright (C) 2019, Raspberry Pi Ltd
 #
-# ctt_vc4.py - camera tuning tool for VC4 platforms
+# ctt_vc4.py - camera tuning tool data for VC4 platforms
 
-import os
-import sys
-
-from ctt_run import run_ctt
-from ctt_tools import parse_input
 
 json_template = {
     "rpi.black_level": {
@@ -129,29 +124,3 @@ json_template = {
 }
 
 grid_size = (16, 12)
-
-target = 'bcm2835'
-
-if __name__ == '__main__':
-    """
-    initialise calibration
-    """
-    if len(sys.argv) == 1:
-        print("""
-    VC4 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, json_template, grid_size, target)
-- 
cgit v1.2.1