Show a patch.

GET /api/patches/18800/?format=api
HTTP 200 OK
Allow: GET, PUT, PATCH, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept

{
    "id": 18800,
    "url": "https://patchwork.libcamera.org/api/patches/18800/?format=api",
    "web_url": "https://patchwork.libcamera.org/patch/18800/",
    "project": {
        "id": 1,
        "url": "https://patchwork.libcamera.org/api/projects/1/?format=api",
        "name": "libcamera",
        "link_name": "libcamera",
        "list_id": "libcamera_core",
        "list_email": "libcamera-devel@lists.libcamera.org",
        "web_url": "",
        "scm_url": "",
        "webscm_url": ""
    },
    "msgid": "<20230706013926.218131-2-ben.benson@raspberrypi.com>",
    "date": "2023-07-06T01:39:24",
    "name": "[libcamera-devel,1/3] utils: raspberrypi: ctt: Improved color matrix fitting",
    "commit_ref": null,
    "pull_url": null,
    "state": "accepted",
    "archived": false,
    "hash": "23d5aedceacbe8b9b95576357b497a1205ea035b",
    "submitter": {
        "id": 171,
        "url": "https://patchwork.libcamera.org/api/people/171/?format=api",
        "name": "Ben Benson",
        "email": "ben.benson@raspberrypi.com"
    },
    "delegate": null,
    "mbox": "https://patchwork.libcamera.org/patch/18800/mbox/",
    "series": [
        {
            "id": 3963,
            "url": "https://patchwork.libcamera.org/api/series/3963/?format=api",
            "web_url": "https://patchwork.libcamera.org/project/libcamera/list/?series=3963",
            "date": "2023-07-06T01:39:23",
            "name": "Raspberry Pi Color Matrix and Tuning Tool Improvements",
            "version": 1,
            "mbox": "https://patchwork.libcamera.org/series/3963/mbox/"
        }
    ],
    "comments": "https://patchwork.libcamera.org/api/patches/18800/comments/",
    "check": "pending",
    "checks": "https://patchwork.libcamera.org/api/patches/18800/checks/",
    "tags": {},
    "headers": {
        "Return-Path": "<libcamera-devel-bounces@lists.libcamera.org>",
        "X-Original-To": "parsemail@patchwork.libcamera.org",
        "Delivered-To": "parsemail@patchwork.libcamera.org",
        "Received": [
            "from lancelot.ideasonboard.com (lancelot.ideasonboard.com\n\t[92.243.16.209])\n\tby patchwork.libcamera.org (Postfix) with ESMTPS id 51B0BC328C\n\tfor <parsemail@patchwork.libcamera.org>;\n\tFri,  7 Jul 2023 13:41:25 +0000 (UTC)",
            "from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id C78E0628BD;\n\tFri,  7 Jul 2023 15:41:23 +0200 (CEST)",
            "from mail-wm1-x335.google.com (mail-wm1-x335.google.com\n\t[IPv6:2a00:1450:4864:20::335])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 9133561E34\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri,  7 Jul 2023 14:07:45 +0200 (CEST)",
            "by mail-wm1-x335.google.com with SMTP id\n\t5b1f17b1804b1-3fbc5d5742eso19560125e9.3\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri, 07 Jul 2023 05:07:45 -0700 (PDT)",
            "from localhost.localdomain ([93.93.133.154])\n\tby smtp.gmail.com with ESMTPSA id\n\tz13-20020a7bc7cd000000b003fc0062f0f8sm1155915wmk.9.2023.07.07.05.07.44\n\t(version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);\n\tFri, 07 Jul 2023 05:07:44 -0700 (PDT)"
        ],
        "DKIM-Signature": [
            "v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org;\n\ts=mail; t=1688737283;\n\tbh=2Mogpen63STWa50YQSSO/K7AzmteN0FFW/s81WsDJxg=;\n\th=To:Date:In-Reply-To:References:Subject:List-Id:List-Unsubscribe:\n\tList-Archive:List-Post:List-Help:List-Subscribe:From:Reply-To:Cc:\n\tFrom;\n\tb=Iukeuk+LVhyLgMF71LFJMq5B7A9q9TnOUMxKbjAvxv3R0CdrZHfvuU2NUgt2UBo8/\n\tuqj6kzmcyhRSSVj9B9lDQp8r1JS5CRq+bEmEQ7B2bG57FX+kUumF59TOadsIqLgQfJ\n\tt+4QjHQS0h5g3fzLCqLAnZESnO+poVAqeZA2gyoKwfz8pIRhBA48Ia3BXxv6ygOpQb\n\t7EWeDNnXF5RZ75NbjcZl1P3gGnlz4BaMEuyg7Tq3fIJjb9Aymfx9ELUNIOa6h3tl47\n\tPTHQFNUsjl/Q0oPhLt+GEkJ6JzYZ2HBE74gpIrlhGyGFAfWxH1B/VqCyINfdkqHpwX\n\tt5VpHnsb3GeFQ==",
            "v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=raspberrypi.com; s=google; t=1688731665; x=1691323665;\n\th=content-transfer-encoding:mime-version:references:in-reply-to\n\t:message-id:date:subject:cc:to:from:from:to:cc:subject:date\n\t:message-id:reply-to;\n\tbh=eTAarbQDEqHf9g87p3Vbym91uqJ1g20prQRcYvbk7u8=;\n\tb=OccmR5Dc3OY1GSkoXK7e5Lc2ILqWehX7laxOtrHcGlYIajWg2KLJItjsDpGOSGWOGC\n\t5QERzJHHLi4qXm0hjah1CVVY/6TP4LL0xTVGKm5iG9cFvxK2sn2NLORCQp3gB9SU72jX\n\tJPlVtwNITz803aCrCCPLUBw29sEYmjvF14MlsDd3FKr3EmjGQAub5uVgoiRQv4lWOfQZ\n\taEyDRkWtc5HpA+pczlcnHerNVRjKSdGCyP+MXDpS0OyZOP9fhT0k/bglErU/DNnpVIGK\n\t8LTopvZS5gLmL29/xrL2huGxnbP4/bpvxUD05R/SrVExPC9IFB3KkPz9siczHK0ir2fC\n\tVXcw=="
        ],
        "Authentication-Results": "lancelot.ideasonboard.com; dkim=pass (2048-bit key; \n\tunprotected) header.d=raspberrypi.com\n\theader.i=@raspberrypi.com\n\theader.b=\"OccmR5Dc\"; dkim-atps=neutral",
        "X-Google-DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=1e100.net; s=20221208; t=1688731665; x=1691323665;\n\th=content-transfer-encoding:mime-version:references:in-reply-to\n\t:message-id:date:subject:cc:to:from:x-gm-message-state:from:to:cc\n\t:subject:date:message-id:reply-to;\n\tbh=eTAarbQDEqHf9g87p3Vbym91uqJ1g20prQRcYvbk7u8=;\n\tb=AJdniyZwabSdkvlAVWvPLduL+dQ32fQA6/nAwzuop7Swb20orYYVChZCmPAKND+Dvf\n\tuBslyZF6UYkxQN4jwGmeHGpGTyRK8AK/Y5f+L+UFqz9x0ncXQxj3ZOIp4lXNCw0b2gdB\n\tB6XdP6ccyttRwEDQ2MrsTFFzLsF4e+k+th4NlCSnoEJaxO22kDfg1+zO766wy994TN9r\n\tskdvZuBanGTnA7w9g9jIg9whW/I7jDtsrfcMZA9dupYe+s5DLq3x2U6txyEdl82rmNyl\n\t3vFzEElO4xr3Ynz2iz+8FW3GkX790IxPPTWpWIw+l2nxp7xNsF2SOWkiwizCE/NTw1Zh\n\t9tyQ==",
        "X-Gm-Message-State": "ABy/qLY0zg4dfNl9N3TZJho0fLfJWwLDRDJXIFsLtge/6rRVGNqqyHrd\n\taEGtJWZH4rhosrHyDNQCDIJg8o5t8K4jTX2MmwwI8g==",
        "X-Google-Smtp-Source": "APBJJlH0II3DB2XjD8X0bUbXvbnaQs7C/os+iqjk7yTsyIZTztGoLY0NdrvPN9KDdJvBB/IuymoJgA==",
        "X-Received": "by 2002:a05:600c:2111:b0:3fb:b832:d79a with SMTP id\n\tu17-20020a05600c211100b003fbb832d79amr3849254wml.38.1688731664750; \n\tFri, 07 Jul 2023 05:07:44 -0700 (PDT)",
        "To": "libcamera-devel@lists.libcamera.org",
        "Date": "Thu,  6 Jul 2023 02:39:24 +0100",
        "Message-Id": "<20230706013926.218131-2-ben.benson@raspberrypi.com>",
        "X-Mailer": "git-send-email 2.34.1",
        "In-Reply-To": "<20230706013926.218131-1-ben.benson@raspberrypi.com>",
        "References": "<20230706013926.218131-1-ben.benson@raspberrypi.com>",
        "MIME-Version": "1.0",
        "Content-Type": "text/plain; charset=UTF-8",
        "Content-Transfer-Encoding": "8bit",
        "X-Mailman-Approved-At": "Fri, 07 Jul 2023 15:41:21 +0200",
        "Subject": "[libcamera-devel] [PATCH 1/3] utils: raspberrypi: ctt: Improved\n\tcolor matrix fitting",
        "X-BeenThere": "libcamera-devel@lists.libcamera.org",
        "X-Mailman-Version": "2.1.29",
        "Precedence": "list",
        "List-Id": "<libcamera-devel.lists.libcamera.org>",
        "List-Unsubscribe": "<https://lists.libcamera.org/options/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=unsubscribe>",
        "List-Archive": "<https://lists.libcamera.org/pipermail/libcamera-devel/>",
        "List-Post": "<mailto:libcamera-devel@lists.libcamera.org>",
        "List-Help": "<mailto:libcamera-devel-request@lists.libcamera.org?subject=help>",
        "List-Subscribe": "<https://lists.libcamera.org/listinfo/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=subscribe>",
        "From": "Ben Benson via libcamera-devel <libcamera-devel@lists.libcamera.org>",
        "Reply-To": "Ben Benson <ben.benson@raspberrypi.com>",
        "Cc": "Ben Benson <ben.benson@raspberrypi.com>",
        "Errors-To": "libcamera-devel-bounces@lists.libcamera.org",
        "Sender": "\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>"
    },
    "content": "Added code which optimises the color matrices based off\ndelta E values for the calibration images. Working in LAB\ncolor space.\n\nSigned-off-by Ben Benson <ben.benson@raspberrypi.com>\n---\n utils/raspberrypi/ctt/colors.py        |  30 +++\n utils/raspberrypi/ctt/ctt_ccm.py       | 258 +++++++++++++++++++++----\n utils/raspberrypi/ctt/ctt_visualise.py |  38 ++++\n 3 files changed, 291 insertions(+), 35 deletions(-)\n create mode 100644 utils/raspberrypi/ctt/colors.py\n create mode 100644 utils/raspberrypi/ctt/ctt_visualise.py",
    "diff": "diff --git a/utils/raspberrypi/ctt/colors.py b/utils/raspberrypi/ctt/colors.py\nnew file mode 100644\nindex 00000000..bcd87e35\n--- /dev/null\n+++ b/utils/raspberrypi/ctt/colors.py\n@@ -0,0 +1,30 @@\n+# SIMPLE CODE TO CONVERT RGB VALUES TO LAB\n+def RGB_to_LAB(RGB):  # where RGB is a 1x3 array.   e.g RGB = [100, 255, 230]\n+    num = 0\n+    XYZ = [0, 0, 0]\n+    # converted all the three R, G, B to X, Y, Z\n+    X = RGB[0] * 0.4124 + RGB[1] * 0.3576 + RGB[2] * 0.1805\n+    Y = RGB[0] * 0.2126 + RGB[1] * 0.7152 + RGB[2] * 0.0722\n+    Z = RGB[0] * 0.0193 + RGB[1] * 0.1192 + RGB[2] * 0.9505\n+\n+    XYZ[0] = X / 255 * 100\n+    XYZ[1] = Y / 255 * 100  # XYZ Must be in range 0 -> 100, so scale down from 255\n+    XYZ[2] = Z / 255 * 100\n+    XYZ[0] = XYZ[0] / 95.047  # ref_X =  95.047   Observer= 2°, Illuminant= D65\n+    XYZ[1] = XYZ[1] / 100.0  # ref_Y = 100.000\n+    XYZ[2] = XYZ[2] / 108.883  # ref_Z = 108.883\n+    num = 0\n+    for value in XYZ:\n+        if value > 0.008856:\n+            value = value ** (0.3333333333333333)\n+        else:\n+            value = (7.787 * value) + (16 / 116)\n+        XYZ[num] = value\n+        num = num + 1\n+\n+    # L, A, B, values calculated below\n+    L = (116 * XYZ[1]) - 16\n+    a = 500 * (XYZ[0] - XYZ[1])\n+    b = 200 * (XYZ[1] - XYZ[2])\n+\n+    return [L, a, b]\ndiff --git a/utils/raspberrypi/ctt/ctt_ccm.py b/utils/raspberrypi/ctt/ctt_ccm.py\nindex 376cc712..bd44b4d8 100644\n--- a/utils/raspberrypi/ctt/ctt_ccm.py\n+++ b/utils/raspberrypi/ctt/ctt_ccm.py\n@@ -6,27 +6,66 @@\n \n from ctt_image_load import *\n from ctt_awb import get_alsc_patches\n-\n-\n+import colors\n+from scipy.optimize import minimize\n+from ctt_visualise import visualise_macbeth_chart\n+import numpy as np\n \"\"\"\n takes 8-bit macbeth chart values, degammas and returns 16 bit\n \"\"\"\n+\n+'''\n+This program has many options from which to derive the color matrix from.\n+The first is average. This minimises the average delta E across all patches of\n+the macbeth chart. Testing across all cameras yeilded this as the most color\n+accurate and vivid. Other options are avalible however.\n+Maximum minimises the maximum Delta E of the patches. It iterates through till\n+a minimum maximum is found (so that there is\n+not one patch that deviates wildly.)\n+This yeilds generally good results but overall the colors are less accurate\n+Have a fiddle with maximum and see what you think.\n+The final option allows you to select the patches for which to average across.\n+This means that you can bias certain patches, for instance if you want the\n+reds to be more accurate.\n+'''\n+\n+matrix_selection_types = [\"average\", \"maximum\", \"patches\"]\n+typenum = 0  # select from array above, 0 = average, 1 = maximum, 2 = patches\n+test_patches = [1, 2, 5, 8, 9, 12, 14]\n+\n+'''\n+Enter patches to test for. Can also be entered twice if you\n+would like twice as much bias on one patch.\n+'''\n+\n+\n def degamma(x):\n-    x = x / ((2**8)-1)\n-    x = np.where(x < 0.04045, x/12.92, ((x+0.055)/1.055)**2.4)\n-    x = x * ((2**16)-1)\n+    x = x / ((2 ** 8) - 1)  # takes 255 and scales it down to one\n+    x = np.where(x < 0.04045, x / 12.92, ((x + 0.055) / 1.055) ** 2.4)\n+    x = x * ((2 ** 16) - 1)  # takes one and scales up to 255\n     return x\n \n \n+def gamma(x):\n+    # return (x *  * (1 / 2.4)  *  1.055 - 0.055)\n+    e = []\n+    for i in range(len(x)):\n+        e.append(((x[i] / 255) ** (1 / 2.4) * 1.055 - 0.055) * 255)\n+    return e\n+\n+\n \"\"\"\n FInds colour correction matrices for list of images\n \"\"\"\n+\n+\n def ccm(Cam, cal_cr_list, cal_cb_list):\n+    global matrix_selection_types, typenum\n     imgs = Cam.imgs\n     \"\"\"\n     standard macbeth chart colour values\n     \"\"\"\n-    m_rgb = np.array([  # these are in sRGB\n+    m_rgb = np.array([  # these are in RGB\n         [116, 81, 67],    # dark skin\n         [199, 147, 129],  # light skin\n         [91, 122, 156],   # blue sky\n@@ -34,7 +73,7 @@ def ccm(Cam, cal_cr_list, cal_cb_list):\n         [130, 128, 176],  # blue flower\n         [92, 190, 172],   # bluish green\n         [224, 124, 47],   # orange\n-        [68, 91, 170],     # purplish blue\n+        [68, 91, 170],    # purplish blue\n         [198, 82, 97],    # moderate red\n         [94, 58, 106],    # purple\n         [159, 189, 63],   # yellow green\n@@ -52,16 +91,22 @@ def ccm(Cam, cal_cr_list, cal_cb_list):\n         [82, 84, 86],     # neutral 3.5\n         [49, 49, 51]      # black 2\n     ])\n-\n     \"\"\"\n     convert reference colours from srgb to rgb\n     \"\"\"\n-    m_srgb = degamma(m_rgb)\n+    m_srgb = degamma(m_rgb)  # now in 16 bit color.\n+\n+    m_lab = []\n+    for col in m_srgb:\n+        m_lab.append(colors.RGB_to_LAB(col / 256))\n+    # This produces matrix of LAB values for ideal color chart)\n+\n     \"\"\"\n     reorder reference values to match how patches are ordered\n     \"\"\"\n     m_srgb = np.array([m_srgb[i::6] for i in range(6)]).reshape((24, 3))\n-\n+    m_lab = np.array([m_lab[i::6] for i in range(6)]).reshape((24, 3))\n+    m_rgb = np.array([m_rgb[i::6] for i in range(6)]).reshape((24, 3))\n     \"\"\"\n     reformat alsc correction tables or set colour_cals to None if alsc is\n     deactivated\n@@ -76,8 +121,8 @@ def ccm(Cam, cal_cr_list, cal_cb_list):\n             \"\"\"\n             normalise tables so min value is 1\n             \"\"\"\n-            cr_tab = cr_tab/np.min(cr_tab)\n-            cb_tab = cb_tab/np.min(cb_tab)\n+            cr_tab = cr_tab / np.min(cr_tab)\n+            cb_tab = cb_tab / np.min(cb_tab)\n             colour_cals[cr['ct']] = [cr_tab, cb_tab]\n \n     \"\"\"\n@@ -94,6 +139,8 @@ def ccm(Cam, cal_cr_list, cal_cb_list):\n         the function will simply return the macbeth patches\n         \"\"\"\n         r, b, g = get_alsc_patches(Img, colour_cals, grey=False)\n+        # 256 values for each patch of sRGB values\n+\n         \"\"\"\n         do awb\n         Note: awb is done by measuring the macbeth chart in the image, rather\n@@ -101,34 +148,123 @@ def ccm(Cam, cal_cr_list, cal_cb_list):\n         and the ccm matrices will be more accurate.\n         \"\"\"\n         r_greys, b_greys, g_greys = r[3::4], b[3::4], g[3::4]\n-        r_g = np.mean(r_greys/g_greys)\n-        b_g = np.mean(b_greys/g_greys)\n+        r_g = np.mean(r_greys / g_greys)\n+        b_g = np.mean(b_greys / g_greys)\n         r = r / r_g\n         b = b / b_g\n-\n         \"\"\"\n         normalise brightness wrt reference macbeth colours and then average\n         each channel for each patch\n         \"\"\"\n-        gain = np.mean(m_srgb)/np.mean((r, g, b))\n+        gain = np.mean(m_srgb) / np.mean((r, g, b))\n         Cam.log += '\\nGain with respect to standard colours: {:.3f}'.format(gain)\n-        r = np.mean(gain*r, axis=1)\n-        b = np.mean(gain*b, axis=1)\n-        g = np.mean(gain*g, axis=1)\n-\n+        r = np.mean(gain * r, axis=1)\n+        b = np.mean(gain * b, axis=1)\n+        g = np.mean(gain * g, axis=1)\n         \"\"\"\n         calculate ccm matrix\n         \"\"\"\n+        # ==== All of below should in sRGB ===##\n+        sumde = 0\n         ccm = do_ccm(r, g, b, m_srgb)\n+        # This is the initial guess that our optimisation code works with.\n+\n+        r1 = ccm[0]\n+        r2 = ccm[1]\n+        g1 = ccm[3]\n+        g2 = ccm[4]\n+        b1 = ccm[6]\n+        b2 = ccm[7]\n+        '''\n+        COLOR MATRIX LOOKS AS BELOW\n+        R1 R2 R3   Rval     Outr\n+        G1 G2 G3  *  Gval  =  G\n+        B1 B2 B3   Bval     B\n+        Will be optimising 6 elements and working out the third element using 1-r1-r2 = r3\n+        '''\n+\n+        x0 = [r1, r2, g1, g2, b1, b2]\n+        '''\n+        We use our old CCM as the initial guess for the program to find the\n+        optimised matrix\n+        '''\n+        result = minimize(guess, x0, args=(r, g, b, m_lab), tol=0.0000000001)\n+        '''\n+        This produces a color matrix which has the lowest delta E possible,\n+        based off the input data. Note it is impossible for this to reach\n+        zero since inperfect data.\n+        '''\n+\n+        Cam.log += (\"\\n \\n Optimised Matrix Below: \\n \\n\")\n+        [r1, r2, g1, g2, b1, b2] = result.x\n+        # The new, optimised color correction matrix values\n+        optimised_ccm = [r1, r2, (1 - r1 - r2), g1, g2, (1 - g1 - g2), b1, b2, (1 - b1 - b2)]\n+        # This is the optimised Color Matrix (preserving greys by summing rows up to 1)\n+        Cam.log += str(optimised_ccm)\n+        Cam.log += \"\\n Old Color Correction Matrix Below \\n\"\n+        Cam.log += str(ccm)\n+\n+        formatted_ccm = np.array(ccm).reshape((3, 3))\n+\n+        '''\n+        below is a whole load of code that then applies the latest color\n+        matrix, and returns LAB values for color. This can then be used\n+        to calculate the final delta E\n+        '''\n+        optimised_ccm_rgb = []  # Original Color Corrected Matrix RGB / LAB\n+        optimised_ccm_lab = []\n+        for w in range(24):\n+            RGB = np.array([r[w], g[w], b[w]])\n+            ccm_applied_rgb = np.dot(formatted_ccm, (RGB / 256))\n+            optimised_ccm_rgb.append(gamma(ccm_applied_rgb))\n+            optimised_ccm_lab.append(colors.RGB_to_LAB(ccm_applied_rgb))\n+\n+        formatted_optimised_ccm = np.array(ccm).reshape((3, 3))\n+        after_gamma_rgb = []\n+        after_gamma_lab = []\n+        for w in range(24):\n+            RGB = np.array([r[w], g[w], b[w]])\n+            optimised_ccm_applied_rgb = np.dot(formatted_optimised_ccm, RGB / 256)\n+            after_gamma_rgb.append(gamma(optimised_ccm_applied_rgb))\n+            after_gamma_lab.append(colors.RGB_to_LAB(optimised_ccm_applied_rgb))\n+        '''\n+        Gamma After RGB / LAB\n+        We now want to spit out some data that shows\n+        how the optimisation has improved the color matricies\n+        '''\n+        Cam.log += \"Here are the Improvements\"\n+\n+        # CALCULATE WORST CASE delta e\n+        old_worst_delta_e = 0\n+        bavg = transform_and_evaluate(formatted_ccm, r, g, b, m_lab)\n+        new_worst_delta_e = 0\n+        aavg = transform_and_evaluate(formatted_optimised_ccm, r, g, b, m_lab)\n+        for i in range(24):\n+            old_delta_e = deltae(optimised_ccm_lab[i], m_lab[i])  # Current Old Delta E\n+            new_delta_e = deltae(after_gamma_lab[i], m_lab[i])  # Current New Delta E\n+            if old_delta_e > old_worst_delta_e:\n+                old_worst_delta_e = old_delta_e\n+            if new_delta_e > new_worst_delta_e:\n+                new_worst_delta_e = new_delta_e\n+\n+        Cam.log += \"Before color correction matrix was optimised, we got an average delta E of \" + str(bavg) + \" and a maximum delta E of \" + str(old_worst_delta_e)\n+        Cam.log += \"After color correction matrix was optimised, we got an average delta E of \" + str(aavg) + \" and a maximum delta E of \" + str(new_worst_delta_e)\n+\n+        visualise_macbeth_chart(m_rgb, optimised_ccm_rgb, after_gamma_rgb, str(Img.col) + str(matrix_selection_types[typenum]))\n+        '''\n+        The program will also save some visualisations of improvements.\n+        Very pretty to look at. Top rectangle is ideal, Left square is\n+        before optimisation, right square is after.\n+        '''\n \n         \"\"\"\n         if a ccm has already been calculated for that temperature then don't\n         overwrite but save both. They will then be averaged later on\n-        \"\"\"\n+        \"\"\"  # NOW GOING TO USE OPTIMISED COLOR MATRIX, optimised_ccm\n         if Img.col in ccm_tab.keys():\n-            ccm_tab[Img.col].append(ccm)\n+            ccm_tab[Img.col].append(optimised_ccm)\n         else:\n-            ccm_tab[Img.col] = [ccm]\n+            ccm_tab[Img.col] = [optimised_ccm]\n         Cam.log += '\\n'\n \n     Cam.log += '\\nFinished processing images'\n@@ -137,8 +273,8 @@ def ccm(Cam, cal_cr_list, cal_cb_list):\n     \"\"\"\n     for k, v in ccm_tab.items():\n         tab = np.mean(v, axis=0)\n-        tab = np.where((10000*tab) % 1 <= 0.05, tab+0.00001, tab)\n-        tab = np.where((10000*tab) % 1 >= 0.95, tab-0.00001, tab)\n+        tab = np.where((10000 * tab) % 1 <= 0.05, tab + 0.00001, tab)\n+        tab = np.where((10000 * tab) % 1 >= 0.95, tab - 0.00001, tab)\n         ccm_tab[k] = list(np.round(tab, 5))\n         Cam.log += '\\nMatrix calculated for colour temperature of {} K'.format(k)\n \n@@ -156,6 +292,50 @@ def ccm(Cam, cal_cr_list, cal_cb_list):\n     return ccms\n \n \n+def guess(x0, r, g, b, m_lab):       # provides a method of numerical feedback for the optimisation code\n+    [r1, r2, g1, g2, b1, b2] = x0\n+    ccm = np.array([r1, r2, (1 - r1 - r2),\n+                    g1, g2, (1 - g1 - g2),\n+                    b1, b2, (1 - b1 - b2)]).reshape((3, 3))  # format the matrix correctly\n+    return transform_and_evaluate(ccm, r, g, b, m_lab)\n+\n+\n+def transform_and_evaluate(ccm, r, g, b, m_lab):  # Transforms colors to LAB and applies the correction matrix\n+    # create list of matrix changed colors\n+    realrgb = []\n+    for i in range(len(r)):\n+        RGB = np.array([r[i], g[i], b[i]])\n+        rgb_post_ccm = np.dot(ccm, RGB)  # This is RGB values after the color correction matrix has been applied\n+        realrgb.append(colors.RGB_to_LAB(rgb_post_ccm))\n+    # now compare that with m_lab and return numeric result, averaged for each patch\n+    return (sumde(realrgb, m_lab) / 24)  # returns an average result of delta E\n+\n+\n+def sumde(listA, listB):\n+    global typenum, test_patches\n+    sumde = 0\n+    maxde = 0\n+    patchde = []\n+    for i in range(len(listA)):\n+        if maxde < (deltae(listA[i], listB[i])):\n+            maxde = deltae(listA[i], listB[i])\n+        patchde.append(deltae(listA[i], listB[i]))\n+        sumde += deltae(listA[i], listB[i])\n+    '''\n+    The different options specified at the start allow for\n+    the maximum to be returned, average or specific patches\n+    '''\n+    if typenum == 0:\n+        return sumde\n+    if typenum == 1:\n+        return maxde\n+    if typenum == 2:\n+        output = 0\n+        for y in range(len(test_patches)):\n+            output += patchde[test_patches[y]]   # grabs the specific patches (no need for averaging here)\n+        return output\n+\n+\n \"\"\"\n calculates the ccm for an individual image.\n ccms are calculate in rgb space, and are fit by hand. Although it is a 3x3\n@@ -164,12 +344,14 @@ calculation.\n Should you want to fit them in another space (e.g. LAB) we wish you the best of\n luck and send us the code when you are done! :-)\n \"\"\"\n+\n+\n def do_ccm(r, g, b, m_srgb):\n     rb = r-b\n     gb = g-b\n-    rb_2s = (rb*rb)\n-    rb_gbs = (rb*gb)\n-    gb_2s = (gb*gb)\n+    rb_2s = (rb * rb)\n+    rb_gbs = (rb * gb)\n+    gb_2s = (gb * gb)\n \n     r_rbs = rb * (m_srgb[..., 0] - b)\n     r_gbs = gb * (m_srgb[..., 0] - b)\n@@ -191,7 +373,7 @@ def do_ccm(r, g, b, m_srgb):\n     b_rb = np.sum(b_rbs)\n     b_gb = np.sum(b_gbs)\n \n-    det = rb_2*gb_2 - rb_gb*rb_gb\n+    det = rb_2 * gb_2 - rb_gb * rb_gb\n \n     \"\"\"\n     Raise error if matrix is singular...\n@@ -201,19 +383,19 @@ def do_ccm(r, g, b, m_srgb):\n     if det < 0.001:\n         raise ArithmeticError\n \n-    r_a = (gb_2*r_rb - rb_gb*r_gb)/det\n-    r_b = (rb_2*r_gb - rb_gb*r_rb)/det\n+    r_a = (gb_2 * r_rb - rb_gb * r_gb) / det\n+    r_b = (rb_2 * r_gb - rb_gb * r_rb) / det\n     \"\"\"\n     Last row can be calculated by knowing the sum must be 1\n     \"\"\"\n     r_c = 1 - r_a - r_b\n \n-    g_a = (gb_2*g_rb - rb_gb*g_gb)/det\n-    g_b = (rb_2*g_gb - rb_gb*g_rb)/det\n+    g_a = (gb_2 * g_rb - rb_gb * g_gb) / det\n+    g_b = (rb_2 * g_gb - rb_gb * g_rb) / det\n     g_c = 1 - g_a - g_b\n \n-    b_a = (gb_2*b_rb - rb_gb*b_gb)/det\n-    b_b = (rb_2*b_gb - rb_gb*b_rb)/det\n+    b_a = (gb_2 * b_rb - rb_gb * b_gb) / det\n+    b_b = (rb_2 * b_gb - rb_gb * b_rb) / det\n     b_c = 1 - b_a - b_b\n \n     \"\"\"\n@@ -222,3 +404,9 @@ def do_ccm(r, g, b, m_srgb):\n     ccm = [r_a, r_b, r_c, g_a, g_b, g_c, b_a, b_b, b_c]\n \n     return ccm\n+\n+\n+def deltae(colorA, colorB):\n+    return ((colorA[0] - colorB[0]) ** 2 + (colorA[1] - colorB[1]) ** 2 + (colorA[2] - colorB[2]) ** 2) ** 0.5\n+    # return ((colorA[1]-colorB[1]) *  * 2 + (colorA[2]-colorB[2]) *  * 2) *  * 0.5\n+    # UNCOMMENT IF YOU WANT TO NEGLECT LUMINANCE FROM CALCULATION OF DELTA E\ndiff --git a/utils/raspberrypi/ctt/ctt_visualise.py b/utils/raspberrypi/ctt/ctt_visualise.py\nnew file mode 100644\nindex 00000000..fe5381c6\n--- /dev/null\n+++ b/utils/raspberrypi/ctt/ctt_visualise.py\n@@ -0,0 +1,38 @@\n+# Some code that will save virtual macbeth charts that show the difference between optimised matricies and non optimised matricies\n+import numpy as np\n+from PIL import Image\n+'''make patch 200 by 200 pixels:\n+200x100 will be correct color\n+100x100 will be new color, 100x100 old color\n+if 50px gap between pixels, image will be 7 gaps + 6 patches wide, and 5 gaps + 4 patches high.\n+  y --->\n+x\n+|\n+|\n+'''\n+\n+\n+def visualise_macbeth_chart(macbeth_rgb, original_rgb, new_rgb, output_filename):\n+    image = np.zeros((1050, 1550, 3), dtype=np.uint8)\n+    colorindex = -1\n+    for y in range(6):\n+        for x in range(4):  # Creates 6 x 4 grid of macbeth chart\n+            colorindex += 1\n+            xlocation = 50 + 250 * x  # Means there is 50px of black gap between each square, more like the real macbeth chart.\n+            ylocation = 50 + 250 * y\n+            for g in range(200):\n+                for i in range(100):\n+                    image[xlocation + i, ylocation + g] = macbeth_rgb[colorindex]\n+            xlocation = 150 + 250 * x\n+            ylocation = 50 + 250 * y\n+            for i in range(100):\n+                for g in range(100):\n+                    image[xlocation + i, ylocation + g] = original_rgb[colorindex]  # Smaller squares below to compare the old colors with the new ones\n+            xlocation = 150 + 250 * x\n+            ylocation = 150 + 250 * y\n+            for i in range(100):\n+                for g in range(100):\n+                    image[xlocation + i, ylocation + g] = new_rgb[colorindex]\n+\n+    img = Image.fromarray(image, 'RGB')\n+    img.save(str(output_filename) + 'Generated Macbeth Chart.png')\n",
    "prefixes": [
        "libcamera-devel",
        "1/3"
    ]
}