[{"id":27512,"web_url":"https://patchwork.libcamera.org/comment/27512/","msgid":"<CAHW6GYKtD4oXrV=_HFAkqyoNaBpM0eU02L9+G_T+jSBi10TNEw@mail.gmail.com>","date":"2023-07-10T09:55:33","subject":"Re: [libcamera-devel] [PATCH 1/3] utils: raspberrypi: ctt: Improved\n\tcolor matrix fitting","submitter":{"id":42,"url":"https://patchwork.libcamera.org/api/people/42/","name":"David Plowman","email":"david.plowman@raspberrypi.com"},"content":"Hi Ben\n\nThanks very much for this work!\n\nI'm going to make a few more minor or style related comments, but I'm\nhappy to regard most of them as being for future reference. We could\ngo on tweaking the Python forever, but with just a couple of\nexceptions, I'd rather not!\n\nOn Fri, 7 Jul 2023 at 14:41, Ben Benson via libcamera-devel\n<libcamera-devel@lists.libcamera.org> wrote:\n>\n> Added code which optimises the color matrices based off\n> delta E values for the calibration images. Working in LAB\n> color space.\n>\n> Signed-off-by Ben Benson <ben.benson@raspberrypi.com>\n\ns/by/by:/\n(We probably need to fix this one.)\n\nAs I've said, minor observations notwithstanding:\n\nReviewed-by: David Plowman <david.plowman@raspberrypi.com>\n\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\n>\n> diff --git a/utils/raspberrypi/ctt/colors.py b/utils/raspberrypi/ctt/colors.py\n> new file mode 100644\n> index 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\nNo need to shout!  :)\n\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]\n> diff --git a/utils/raspberrypi/ctt/ctt_ccm.py b/utils/raspberrypi/ctt/ctt_ccm.py\n> index 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\ns/yeilds/yields/   (but don't mind)\n\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\nI guess the comment here is not correct, it should be 65535 and not\n255. (This might be another change to make as it's a bit confusing for\nthe reader otherwise.)\n\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\nI guess we could have looked the Lab values up explicitly, but this is OK too.\n\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\ns/inperfect/imperfect/       (though I'm not overly fussed)\nor even \"the input data will be imperfect\".\n\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\ns/matricies/matrices/   but again, I don't really mind\n\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\nOne might consider renaming \"bavg\" as \"before_avg\" and \"aavg\" as\n\"after_avg\" to make this a bit more readable? But not urgent.\n\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\nThere's probably some 1 or 2-line Python idiom for the above... though\nI don't particularly feel there's a need to go and find it!\n\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\ns/calculate/calculated/\n\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\n> diff --git a/utils/raspberrypi/ctt/ctt_visualise.py b/utils/raspberrypi/ctt/ctt_visualise.py\n> new file mode 100644\n> index 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\nAs above (\"matrices\")\n\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\nMaybe worth a docstring to say what it's doing?\n\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> --\n> 2.34.1\n>\n\nThanks very much!\n\nDavid","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 6E214C323E\n\tfor <parsemail@patchwork.libcamera.org>;\n\tMon, 10 Jul 2023 09:55:49 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 8A085628BF;\n\tMon, 10 Jul 2023 11:55:48 +0200 (CEST)","from mail-qv1-xf36.google.com (mail-qv1-xf36.google.com\n\t[IPv6:2607:f8b0:4864:20::f36])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 2CD4A628BE\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tMon, 10 Jul 2023 11:55:46 +0200 (CEST)","by mail-qv1-xf36.google.com with SMTP id\n\t6a1803df08f44-635eb3a1d93so34111896d6.1\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tMon, 10 Jul 2023 02:55:46 -0700 (PDT)"],"DKIM-Signature":["v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org;\n\ts=mail; t=1688982948;\n\tbh=PYaM0SPcJA9wdYZ+n5dGO6cC+50ixvvYb5fYqE+302k=;\n\th=References:In-Reply-To:Date:To:Subject:List-Id:List-Unsubscribe:\n\tList-Archive:List-Post:List-Help:List-Subscribe:From:Reply-To:Cc:\n\tFrom;\n\tb=WYwuWjAl3l4je4scSzs5N3wxmUfJSksuJQ7X4r4C6j0G8YVUy62Wc1kd5Y0Bq05ke\n\tOMflMcO5tN15GQZ4pIFK2Xjp0DbWjXnvn0sS6yHxNqrzupBcZuoipE1K6t71Nv+1yD\n\tc8RzCOkZu/rBvuEE+mcctKj04PGPbr/fAj7Jg8wQvwgTFiX3Q/GNGkLy2Gl+SgcNzC\n\tp6Ykfp9dr4CHKydO+9EXtRmbQFBDxSGxSYKnW+Oxh8XmrcnC3aue6+v9QQs/wsuxlN\n\tV6uq0cdIbbPNZBb+S1N2DUJjkywhmNdGQdeiWDFgWBb27HraONAQ0pT6vE+doEmrJo\n\tt6Ko/Qrnn0ZnA==","v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=raspberrypi.com; s=google; t=1688982945; x=1691574945;\n\th=content-transfer-encoding:cc:to:subject:message-id:date:from\n\t:in-reply-to:references:mime-version:from:to:cc:subject:date\n\t:message-id:reply-to;\n\tbh=Cdu6MaSq8I4pVIDm48sMOvynRux8g6xbBVB+QavdbJQ=;\n\tb=Xansj4So9osn2fbb8wVEZ/b4P10U2ZuNFOec0yKCaULnPTRe4xOc/w8RgGK7BNALGT\n\t5pyY/SHpJZ3gJZphdsNEOZNGw4p1q0LtvJJbzKUUaPtjFkCRlJ1ONBokLbEqeOP2erlT\n\tXA9FES1wxeuqH8X2MlobeguWo7D42OGX06hnbCXc8cVSISWOn67XxUwLZvJm5TJabJl0\n\tUQY9c01zQ1ZWMykkavtwColpBEoTuxKpJftEM4kscb/O9tVgKbosZXvSve+wK4wupTxP\n\tMcNBLxbh7uYqXGlcbJnnOs/sk2rVZWvhtV1uCh5du/rMpbL5ygJLevkrQB3NlvGqCdMu\n\tCS4Q=="],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (2048-bit key; \n\tunprotected) header.d=raspberrypi.com\n\theader.i=@raspberrypi.com\n\theader.b=\"Xansj4So\"; dkim-atps=neutral","X-Google-DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=1e100.net; s=20221208; t=1688982945; x=1691574945;\n\th=content-transfer-encoding:cc:to:subject:message-id:date:from\n\t:in-reply-to:references:mime-version:x-gm-message-state:from:to:cc\n\t:subject:date:message-id:reply-to;\n\tbh=Cdu6MaSq8I4pVIDm48sMOvynRux8g6xbBVB+QavdbJQ=;\n\tb=PrP9WL5tMoZwcId5HoVxc40JKVQdq11oVNzyxImbDGSHlHTvZeyc2KuiK/gsOk1Sj2\n\t+NaKiPFq6JNe3mydmaZ8C4F6gnsCQzkLkffDbfMQPjV5yF6BC4dyFlyZ8/EXYSknWZTA\n\tM/lc2Bb4OWtlHRNUDQRWRFVefvZlvQ5zkdwOQlcQkjPYrNxZx/Mg2FoLaJFcfX6ykoqU\n\tYj+qGV+LzTVGRy71Fks5TlzZys9iep4Kb9RDAkRga+Xu5ufQctmURlLo2WYe5E8JmL+E\n\t2xe635zksfi846Io1LNBNoq5qhMNS9OuvP06DYFhVs5CIUiSQS1t5s9D0JbPdH1CJ1zR\n\tC5kA==","X-Gm-Message-State":"ABy/qLY8D/MS5fmEbERjUAjIZvzTHQJY21Kj6DLpbmelp0mucTbyRawa\n\tZRQ8bKDrVBrz13Nwcdg/VmzXBd8V32hjPUJITZbrDA==","X-Google-Smtp-Source":"APBJJlGbaCf0r0e01JYRHxYkWiEo6dLUCotrOFU7v5cyi8OrS3gAVvS0IMhMbjBpmeuJguGsrlVZKLsIqlzbBbg6OBc=","X-Received":"by 2002:a0c:b451:0:b0:625:af4b:415a with SMTP id\n\te17-20020a0cb451000000b00625af4b415amr10541525qvf.19.1688982944638;\n\tMon, 10 Jul 2023 02:55:44 -0700 (PDT)","MIME-Version":"1.0","References":"<20230706013926.218131-1-ben.benson@raspberrypi.com>\n\t<20230706013926.218131-2-ben.benson@raspberrypi.com>","In-Reply-To":"<20230706013926.218131-2-ben.benson@raspberrypi.com>","Date":"Mon, 10 Jul 2023 10:55:33 +0100","Message-ID":"<CAHW6GYKtD4oXrV=_HFAkqyoNaBpM0eU02L9+G_T+jSBi10TNEw@mail.gmail.com>","To":"Ben Benson <ben.benson@raspberrypi.com>","Content-Type":"text/plain; charset=\"UTF-8\"","Content-Transfer-Encoding":"quoted-printable","Subject":"Re: [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":"David Plowman via libcamera-devel <libcamera-devel@lists.libcamera.org>","Reply-To":"David Plowman <david.plowman@raspberrypi.com>","Cc":"libcamera-devel@lists.libcamera.org","Errors-To":"libcamera-devel-bounces@lists.libcamera.org","Sender":"\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>"}},{"id":27547,"web_url":"https://patchwork.libcamera.org/comment/27547/","msgid":"<CAEmqJPqCHKWE00EVMkrbqXoSqoYMpqbwQAxEdhbUkKC59knyGQ@mail.gmail.com>","date":"2023-07-12T12:21:34","subject":"Re: [libcamera-devel] [PATCH 1/3] utils: raspberrypi: ctt: Improved\n\tcolor matrix fitting","submitter":{"id":34,"url":"https://patchwork.libcamera.org/api/people/34/","name":"Naushir Patuck","email":"naush@raspberrypi.com"},"content":"Hi Ben,\n\nThank you for this work!\n\nOn Fri, 7 Jul 2023 at 14:41, Ben Benson via libcamera-devel\n<libcamera-devel@lists.libcamera.org> wrote:\n>\n> Added code which optimises the color matrices based off\n> delta E values for the calibration images. Working in LAB\n> color space.\n>\n> Signed-off-by Ben Benson <ben.benson@raspberrypi.com>\n\nWith David's minor style suggestions applied:\n\nReviewed-by: Naushir Patuck <naush@raspberrypi.com>\n\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\n>\n> diff --git a/utils/raspberrypi/ctt/colors.py b/utils/raspberrypi/ctt/colors.py\n> new file mode 100644\n> index 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]\n> diff --git a/utils/raspberrypi/ctt/ctt_ccm.py b/utils/raspberrypi/ctt/ctt_ccm.py\n> index 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\n> diff --git a/utils/raspberrypi/ctt/ctt_visualise.py b/utils/raspberrypi/ctt/ctt_visualise.py\n> new file mode 100644\n> index 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> --\n> 2.34.1\n>","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 01FD6BEFBE\n\tfor <parsemail@patchwork.libcamera.org>;\n\tWed, 12 Jul 2023 12:21:51 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 67D53628C0;\n\tWed, 12 Jul 2023 14:21:51 +0200 (CEST)","from mail-yw1-x112f.google.com (mail-yw1-x112f.google.com\n\t[IPv6:2607:f8b0:4864:20::112f])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 0D742628BB\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tWed, 12 Jul 2023 14:21:50 +0200 (CEST)","by mail-yw1-x112f.google.com with SMTP id\n\t00721157ae682-576a9507a9bso10159497b3.1\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tWed, 12 Jul 2023 05:21:49 -0700 (PDT)"],"DKIM-Signature":["v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org;\n\ts=mail; t=1689164511;\n\tbh=MNSBOXDbO0rTeVvuxrPh4owpJ7bbI+vBq7/w5qVOMJI=;\n\th=References:In-Reply-To:Date:To:Subject:List-Id:List-Unsubscribe:\n\tList-Archive:List-Post:List-Help:List-Subscribe:From:Reply-To:Cc:\n\tFrom;\n\tb=q1BgEflUPIvRpDbMIj5LAWGQjESrOPZp4hyqIsaj2ZdoMfGBuTL7eymLaqjtDqiVd\n\txhqaodIHdte70zi4MChnf4UoXJgFNXYldK7gFQii7mO5KaOxiXmX6vXX39jqZpOhp4\n\txcev+FqUoIBKPfrL4OniTtZWRmAZ8/ve4wXDVtOilT7CoA5KAkcRO27vRpFxzfAPrd\n\tPCoZj5teQuTqWaFjIm4g37pH07uOsZun/FT8YJixnltCZ5Ob/oM7y6WOEhWCBCWwLR\n\tfcYU8bM4lpFsTiKrpWcB+Bxs7Ct0TH2K+DnHRAfFme8JxYD3FAxWffCHrq2peNq/MT\n\tjOt3MsvTgAafg==","v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=raspberrypi.com; s=google; t=1689164509; x=1691756509;\n\th=content-transfer-encoding:cc:to:subject:message-id:date:from\n\t:in-reply-to:references:mime-version:from:to:cc:subject:date\n\t:message-id:reply-to;\n\tbh=LzAtWx0xU65NqcKEHx1NV0Pvrg3Ip9KLoD+AMUPLl8Y=;\n\tb=UqGQW109KbN6SRTvHrM4lJEMNRHRK6hOOs5Lu5GEKY4vnFy7cvwOUHXR5I6LMyx2fd\n\tg4KMitTYKMGHYcQEt4k7DBXdvTk9xKwK5MjoXfi8Xsp1Kl7J3L4iKQGaEoU5pbw0wQWL\n\tdZUJJwAepRVO+DY2+preMHhrCJEziklRlyuwfNmnzYZhk9nV5psMEVQ/qiTH9dFFJgBt\n\tKn0z/4jCRzdnJO1bVimTPSVMGhs0/khBDEyDdy9t97fY1Mv/+MztapgqRgHFeM2eS0nT\n\tGXv2SPslJEjjkPkcHvxCml+V0OnJ3bd6FOgmUkENZBmf4ADV04QBerpL8u/Q7vqpvAiE\n\tX9Qw=="],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (2048-bit key; \n\tunprotected) header.d=raspberrypi.com\n\theader.i=@raspberrypi.com\n\theader.b=\"UqGQW109\"; dkim-atps=neutral","X-Google-DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=1e100.net; s=20221208; t=1689164509; x=1691756509;\n\th=content-transfer-encoding:cc:to:subject:message-id:date:from\n\t:in-reply-to:references:mime-version:x-gm-message-state:from:to:cc\n\t:subject:date:message-id:reply-to;\n\tbh=LzAtWx0xU65NqcKEHx1NV0Pvrg3Ip9KLoD+AMUPLl8Y=;\n\tb=kl6c6eUiFBb4MZHewLZI+/eX40u1c0Nl0tI/pG/Z3HinShss6XU/3mtHQSIEM2/V82\n\tBlh+eOoqCX+b1QlhtojBOck7yi2WSbtQRRkwxNdL/7CYoOeys5++vS3k01hkOgp1KDUc\n\t0c3BJxRc4hFLZQOxck02JX6DKdXbz8JTnRVPDELuo0SArnopI220ebpECnySyo+FWte9\n\tpTrwO1Xj0z6Zy+s1pPMm4Ba3pOCgivMYExs4iXwoYRB0lC8FQ91ahlkQucHxGSfh1X6L\n\t4o2ct1JqNKqAFzWrwVG5LKksykBGbFlq0fO4Ck4RwK9sA56M8vFpDadkS/7X85Hhkv1y\n\tzYFw==","X-Gm-Message-State":"ABy/qLbvvtrGH6fkdAt8PUPRhH/1dvP5V6r0zSeAce7X74msBePfF+Yx\n\t79tNSNIGO7V652YIgYWoz26DWFPxfVQ0oz33TRGDpgKJNgZb3Kodwi8=","X-Google-Smtp-Source":"APBJJlErwEINLsisWg5o3M5QsXNicjm4vdfOFhMuBhQ3FIIqccD8BS8Qy/iKg1I+TzKhV1i0Tg2GOgNBervbjE/BXWw=","X-Received":"by 2002:a0d:cc0d:0:b0:56c:ffe4:4ca5 with SMTP id\n\to13-20020a0dcc0d000000b0056cffe44ca5mr2139935ywd.10.1689164508681;\n\tWed, 12 Jul 2023 05:21:48 -0700 (PDT)","MIME-Version":"1.0","References":"<20230706013926.218131-1-ben.benson@raspberrypi.com>\n\t<20230706013926.218131-2-ben.benson@raspberrypi.com>","In-Reply-To":"<20230706013926.218131-2-ben.benson@raspberrypi.com>","Date":"Wed, 12 Jul 2023 13:21:34 +0100","Message-ID":"<CAEmqJPqCHKWE00EVMkrbqXoSqoYMpqbwQAxEdhbUkKC59knyGQ@mail.gmail.com>","To":"Ben Benson <ben.benson@raspberrypi.com>","Content-Type":"text/plain; charset=\"UTF-8\"","Content-Transfer-Encoding":"quoted-printable","Subject":"Re: [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":"Naushir Patuck via libcamera-devel\n\t<libcamera-devel@lists.libcamera.org>","Reply-To":"Naushir Patuck <naush@raspberrypi.com>","Cc":"libcamera-devel@lists.libcamera.org","Errors-To":"libcamera-devel-bounces@lists.libcamera.org","Sender":"\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>"}}]