Patch Detail
Show a patch.
GET /api/patches/20216/?format=api
{ "id": 20216, "url": "https://patchwork.libcamera.org/api/patches/20216/?format=api", "web_url": "https://patchwork.libcamera.org/patch/20216/", "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": "<20240606101512.375178-3-david.plowman@raspberrypi.com>", "date": "2024-06-06T10:15:08", "name": "[2/6] utils: raspberrypi: ctt: Added CAC support to the CTT", "commit_ref": null, "pull_url": null, "state": "accepted", "archived": false, "hash": "c325fcfe9187c2368fd891f47f93f520ecfd0029", "submitter": { "id": 42, "url": "https://patchwork.libcamera.org/api/people/42/?format=api", "name": "David Plowman", "email": "david.plowman@raspberrypi.com" }, "delegate": null, "mbox": "https://patchwork.libcamera.org/patch/20216/mbox/", "series": [ { "id": 4367, "url": "https://patchwork.libcamera.org/api/series/4367/?format=api", "web_url": "https://patchwork.libcamera.org/project/libcamera/list/?series=4367", "date": "2024-06-06T10:15:06", "name": "Raspberry Pi Camera Tuning Tool updates", "version": 1, "mbox": "https://patchwork.libcamera.org/series/4367/mbox/" } ], "comments": "https://patchwork.libcamera.org/api/patches/20216/comments/", "check": "pending", "checks": "https://patchwork.libcamera.org/api/patches/20216/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 6758BBD87C\n\tfor <parsemail@patchwork.libcamera.org>;\n\tThu, 6 Jun 2024 10:15:38 +0000 (UTC)", "from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id C77E265466;\n\tThu, 6 Jun 2024 12:15:37 +0200 (CEST)", "from mail-ej1-x633.google.com (mail-ej1-x633.google.com\n\t[IPv6:2a00:1450:4864:20::633])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id AD9B265448\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tThu, 6 Jun 2024 12:15:31 +0200 (CEST)", "by mail-ej1-x633.google.com with SMTP id\n\ta640c23a62f3a-a6c7ae658d0so96298266b.0\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tThu, 06 Jun 2024 03:15:31 -0700 (PDT)", "from pi5-davidp.pitowers.org\n\t([2001:4d4e:300:1f:c732:5d0a:406b:ae46])\n\tby smtp.gmail.com with ESMTPSA id\n\ta640c23a62f3a-a6c805c59a2sm75809866b.50.2024.06.06.03.15.29\n\t(version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);\n\tThu, 06 Jun 2024 03:15:29 -0700 (PDT)" ], "Authentication-Results": "lancelot.ideasonboard.com; dkim=pass (2048-bit key;\n\tunprotected) header.d=raspberrypi.com header.i=@raspberrypi.com\n\theader.b=\"goNdh4Tm\"; dkim-atps=neutral", "DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=raspberrypi.com; s=google; t=1717668931; x=1718273731;\n\tdarn=lists.libcamera.org; \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=wdHkH3C7MJAL7S/KkV0mAs9iOvg4M7ZTCbcpXPlDQSI=;\n\tb=goNdh4Tmr7Lfyck6i4W2R1kGknyAiQbKMfGpN4j3kKafD5BElOAhGrtQXbdnUpBgQr\n\tEDb9FfI/WZYjuuMf0aAiZfxAPsjg07Ym2PpXCPW36ZFyMJyOkqQSYUYqpzB6RkaHZaTH\n\t+w2NaAELegyD8clWjataePiVbmA7ywB4sDD3At3JJmmrPQehiZqmRA1+GmsNDnbBOP4L\n\tHC652H146lDjJrLzDu1XMm2gU81hGPkbQo8Hh2OipB06EnvCLnJA1ksAnYFBZghJ7x/J\n\tQo72DHnrYRLz3wD9hkcX4k9WKWaw1IdVIgcb07NZxVXp2fT5xs1d5xhKqJXVm/gAB2RH\n\tV5fQ==", "X-Google-DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=1e100.net; s=20230601; t=1717668931; x=1718273731;\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=wdHkH3C7MJAL7S/KkV0mAs9iOvg4M7ZTCbcpXPlDQSI=;\n\tb=Tzh30l66hbRe19e692YMAwvOvRmSV7SxhaGyghas8ExGpTE7WB5TnnfBpNSAMXcBTG\n\tElykjzkld3Z7ypposdWAL4XsGt6x16vQ80zJLeSv3k22P3Mqr296TmHWxSbIywc2yqOP\n\ta08HvLk9C2VVp2E+zDIlBVdnWjBZjkZjKUPh7PxCr0vnakK7xBgGIowE6j7G5unGym77\n\tII7CQo2T9E6bWwJVCT9w/SZ2JzcB/KabgqeB0HFPLzat3wwhUKVHugKINURb4W8hdgU7\n\tztrPsbpzp5IOZPm5QF7jLXIJGASkw3BX5hibx2jP8hyuSk0blL6YJTKAAQZIZnS9Z3ob\n\t6Kcg==", "X-Gm-Message-State": "AOJu0YyFlRl/Uag0ZUwGlamoBt+e8GIvr62UuCjn5nQeklHaH/WOgyC9\n\tEkpv+9Rao+tCzfze5NjQkALN/zk3JaQ2Ff3VFjf9NoCByXjdv/+wlRPWDk3Tw5FCFlvafbwk0Aa\n\t1", "X-Google-Smtp-Source": "AGHT+IHLv2Xy3/iVIue1uDrWWRfxR5JPE86PG0xb712ib9OGCnjPnv7YmreitMaxarfMwpbXyF0SbA==", "X-Received": "by 2002:a17:906:3595:b0:a66:713a:5017 with SMTP id\n\ta640c23a62f3a-a699f670be7mr409036366b.29.1717668930201; \n\tThu, 06 Jun 2024 03:15:30 -0700 (PDT)", "From": "David Plowman <david.plowman@raspberrypi.com>", "To": "libcamera-devel@lists.libcamera.org", "Cc": "Ben Benson <benbenson2004@gmail.com>,\n\tBen Benson <ben.benson@raspberrypi.com>,\n\tNaushir Patuck <naush@raspberrypi.com>", "Subject": "[PATCH 2/6] utils: raspberrypi: ctt: Added CAC support to the CTT", "Date": "Thu, 6 Jun 2024 11:15:08 +0100", "Message-Id": "<20240606101512.375178-3-david.plowman@raspberrypi.com>", "X-Mailer": "git-send-email 2.39.2", "In-Reply-To": "<20240606101512.375178-1-david.plowman@raspberrypi.com>", "References": "<20240606101512.375178-1-david.plowman@raspberrypi.com>", "MIME-Version": "1.0", "Content-Transfer-Encoding": "8bit", "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>", "Errors-To": "libcamera-devel-bounces@lists.libcamera.org", "Sender": "\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>" }, "content": "From: Ben Benson <benbenson2004@gmail.com>\n\nAdded the ability to tune the chromatic aberration correction\nwithin the ctt. There are options for cac_only or to tune as part\nof a larger tuning process. CTT will now recognise any files that\nbegin with \"cac\" as being chromatic aberration tuning files.\n\nSigned-off-by: Ben Benson <ben.benson@raspberrypi.com>\nReviewed-by: Naushir Patuck <naush@raspberrypi.com>\n---\n utils/raspberrypi/ctt/alsc_pisp.py | 2 +-\n utils/raspberrypi/ctt/cac_only.py | 143 +++++++++++\n utils/raspberrypi/ctt/ctt_cac.py | 228 ++++++++++++++++++\n utils/raspberrypi/ctt/ctt_dots_locator.py | 118 +++++++++\n utils/raspberrypi/ctt/ctt_image_load.py | 2 +\n utils/raspberrypi/ctt/ctt_log.txt | 31 +++\n utils/raspberrypi/ctt/ctt_pisp.py | 2 +\n .../raspberrypi/ctt/ctt_pretty_print_json.py | 4 +\n utils/raspberrypi/ctt/ctt_run.py | 85 ++++++-\n 9 files changed, 606 insertions(+), 9 deletions(-)\n create mode 100644 utils/raspberrypi/ctt/cac_only.py\n create mode 100644 utils/raspberrypi/ctt/ctt_cac.py\n create mode 100644 utils/raspberrypi/ctt/ctt_dots_locator.py\n create mode 100644 utils/raspberrypi/ctt/ctt_log.txt", "diff": "diff --git a/utils/raspberrypi/ctt/alsc_pisp.py b/utils/raspberrypi/ctt/alsc_pisp.py\nindex 499aecd1..d0034ae1 100755\n--- a/utils/raspberrypi/ctt/alsc_pisp.py\n+++ b/utils/raspberrypi/ctt/alsc_pisp.py\n@@ -2,7 +2,7 @@\n #\n # SPDX-License-Identifier: BSD-2-Clause\n #\n-# Copyright (C) 2022, Raspberry Pi (Trading) Limited\n+# Copyright (C) 2022, Raspberry Pi Ltd\n #\n # alsc_only.py - alsc tuning tool\n \ndiff --git a/utils/raspberrypi/ctt/cac_only.py b/utils/raspberrypi/ctt/cac_only.py\nnew file mode 100644\nindex 00000000..2bb11ccc\n--- /dev/null\n+++ b/utils/raspberrypi/ctt/cac_only.py\n@@ -0,0 +1,143 @@\n+#!/usr/bin/env python3\n+#\n+# SPDX-License-Identifier: BSD-2-Clause\n+#\n+# Copyright (C) 2023, Raspberry Pi (Trading) Limited\n+#\n+# cac_only.py - cac tuning tool\n+\n+\n+# This file allows you to tune only the chromatic aberration correction\n+# Specify any number of files in the command line args, and it shall iterate through\n+# and generate an averaged cac table from all the input images, which you can then\n+# input into your tuning file.\n+\n+# Takes .dng files produced by the camera modules of the dots grid and calculates the chromatic abberation of each dot.\n+# Then takes each dot, and works out where it was in the image, and uses that to output a tables of the shifts\n+# across the whole image.\n+\n+from PIL import Image\n+import numpy as np\n+import rawpy\n+import sys\n+import getopt\n+\n+from ctt_cac import *\n+\n+\n+def cac(filelist, output_filepath, plot_results=False):\n+ np.set_printoptions(precision=3)\n+ np.set_printoptions(suppress=True)\n+\n+ # Create arrays to hold all the dots data and their colour offsets\n+ red_shift = [] # Format is: [[Dot Center X, Dot Center Y, x shift, y shift]]\n+ blue_shift = []\n+ # Iterate through the files\n+ # Multiple files is reccomended to average out the lens aberration through rotations\n+ for file in filelist:\n+ print(\"\\n Processing file \" + str(file))\n+ # Read the raw RGB values from the .dng file\n+ with rawpy.imread(file) as raw:\n+ rgb = raw.postprocess()\n+ sizes = (raw.sizes)\n+\n+ image_size = [sizes[2], sizes[3]] # Image size, X, Y\n+ # Create a colour copy of the RGB values to use later in the calibration\n+ imout = Image.new(mode=\"RGB\", size=image_size)\n+ rgb_image = np.array(imout)\n+ # The rgb values need reshaping from a 1d array to a 3d array to be worked with easily\n+ rgb.reshape((image_size[0], image_size[1], 3))\n+ rgb_image = rgb\n+\n+ # Pass the RGB image through to the dots locating program\n+ # Returns an array of the dots (colour rectangles around the dots), and an array of their locations\n+ print(\"Finding dots\")\n+ dots, dots_locations = find_dots_locations(rgb_image)\n+\n+ # Now, analyse each dot. Work out the centroid of each colour channel, and use that to work out\n+ # by how far the chromatic aberration has shifted each channel\n+ print('Dots found: ' + str(len(dots)))\n+\n+ for dot, dot_location in zip(dots, dots_locations):\n+ if len(dot) > 0:\n+ if (dot_location[0] > 0) and (dot_location[1] > 0):\n+ ret = analyse_dot(dot, dot_location)\n+ red_shift.append(ret[0])\n+ blue_shift.append(ret[1])\n+\n+ # Take our arrays of red shifts and locations, push them through to be interpolated into a 9x9 matrix\n+ # for the CAC block to handle and then store these as a .json file to be added to the camera\n+ # tuning file\n+ print(\"\\nCreating output grid\")\n+ rx, ry, bx, by = shifts_to_yaml(red_shift, blue_shift, image_size)\n+\n+ print(\"CAC correction complete!\")\n+\n+ # The json format that we then paste into the tuning file (manually)\n+ sample = '''\n+ {\n+ \"rpi.cac\" :\n+ {\n+ \"strength\": 1.0,\n+ \"lut_rx\" : [\n+ rx_vals\n+ ],\n+ \"lut_ry\" : [\n+ ry_vals\n+ ],\n+ \"lut_bx\" : [\n+ bx_vals\n+ ],\n+ \"lut_by\" : [\n+ by_vals\n+ ]\n+ }\n+ }\n+ '''\n+\n+ # Below, may look incorrect, however, the PiSP (standard) dimensions are flipped in comparison to\n+ # PIL image coordinate directions, hence why xr -> yr. Also, the shifts calculated are colour shifts,\n+ # and the PiSP block asks for the values it should shift (hence the * -1, to convert from colour shift to a pixel shift)\n+ sample = sample.replace(\"rx_vals\", pprint_array(ry * -1))\n+ sample = sample.replace(\"ry_vals\", pprint_array(rx * -1))\n+ sample = sample.replace(\"bx_vals\", pprint_array(by * -1))\n+ sample = sample.replace(\"by_vals\", pprint_array(bx * -1))\n+ print(\"Successfully converted to YAML\")\n+ f = open(str(output_filepath), \"w+\")\n+ f.write(sample)\n+ f.close()\n+ print(\"Successfully written to yaml file\")\n+ '''\n+ If you wish to see a plot of the colour channel shifts, add the -p or --plots option\n+ Can be a quick way of validating if the data/dots you've got are good, or if you need to\n+ change some parameters/take some better images\n+ '''\n+ if plot_results:\n+ plot_shifts(red_shift, blue_shift)\n+\n+\n+if __name__ == \"__main__\":\n+ argv = sys.argv\n+ # Detect the input and output file paths\n+ arg_output = \"output.json\"\n+ arg_help = \"{0} -i <input> -o <output> -p <plot results>\".format(argv[0])\n+ opts, args = getopt.getopt(argv[1:], \"hi:o:p\", [\"help\", \"input=\", \"output=\", \"plot\"])\n+\n+ output_location = 0\n+ input_location = 0\n+ filelist = []\n+ plot_results = False\n+ for i in range(len(argv)):\n+ if (\"-h\") in argv[i]:\n+ print(arg_help) # print the help message\n+ sys.exit(2)\n+ if \"-o\" in argv[i]:\n+ output_location = i\n+ if \".dng\" in argv[i]:\n+ filelist.append(argv[i])\n+ if \"-p\" in argv[i]:\n+ plot_results = True\n+\n+ arg_output = argv[output_location + 1]\n+ logfile = open(\"log.txt\", \"a+\")\n+ cac(filelist, arg_output, plot_results, logfile)\ndiff --git a/utils/raspberrypi/ctt/ctt_cac.py b/utils/raspberrypi/ctt/ctt_cac.py\nnew file mode 100644\nindex 00000000..5a4c5101\n--- /dev/null\n+++ b/utils/raspberrypi/ctt/ctt_cac.py\n@@ -0,0 +1,228 @@\n+# SPDX-License-Identifier: BSD-2-Clause\n+#\n+# Copyright (C) 2023, Raspberry Pi Ltd\n+#\n+# ctt_cac.py - CAC (Chromatic Aberration Correction) tuning tool\n+\n+from PIL import Image\n+import numpy as np\n+import matplotlib.pyplot as plt\n+from matplotlib import cm\n+\n+from ctt_dots_locator import find_dots_locations\n+\n+\n+# This is the wrapper file that creates a JSON entry for you to append\n+# to your camera tuning file.\n+# It calculates the chromatic aberration at different points throughout\n+# the image and uses that to produce a martix that can then be used\n+# in the camera tuning files to correct this aberration.\n+\n+\n+def pprint_array(array):\n+ # Function to print the array in a tidier format\n+ array = array\n+ output = \"\"\n+ for i in range(len(array)):\n+ for j in range(len(array[0])):\n+ output += str(round(array[i, j], 2)) + \", \"\n+ # Add the necessary indentation to the array\n+ output += \"\\n \"\n+ # Cut off the end of the array (nicely formats it)\n+ return output[:-22]\n+\n+\n+def plot_shifts(red_shifts, blue_shifts):\n+ # If users want, they can pass a command line option to show the shifts on a graph\n+ # Can be useful to check that the functions are all working, and that the sample\n+ # images are doing the right thing\n+ Xs = np.array(red_shifts)[:, 0]\n+ Ys = np.array(red_shifts)[:, 1]\n+ Zs = np.array(red_shifts)[:, 2]\n+ Zs2 = np.array(red_shifts)[:, 3]\n+ Zs3 = np.array(blue_shifts)[:, 2]\n+ Zs4 = np.array(blue_shifts)[:, 3]\n+\n+ fig, axs = plt.subplots(2, 2)\n+ ax = fig.add_subplot(2, 2, 1, projection='3d')\n+ ax.scatter(Xs, Ys, Zs, cmap=cm.jet, linewidth=0)\n+ ax.set_title('Red X Shift')\n+ ax = fig.add_subplot(2, 2, 2, projection='3d')\n+ ax.scatter(Xs, Ys, Zs2, cmap=cm.jet, linewidth=0)\n+ ax.set_title('Red Y Shift')\n+ ax = fig.add_subplot(2, 2, 3, projection='3d')\n+ ax.scatter(Xs, Ys, Zs3, cmap=cm.jet, linewidth=0)\n+ ax.set_title('Blue X Shift')\n+ ax = fig.add_subplot(2, 2, 4, projection='3d')\n+ ax.scatter(Xs, Ys, Zs4, cmap=cm.jet, linewidth=0)\n+ ax.set_title('Blue Y Shift')\n+ fig.tight_layout()\n+ plt.show()\n+\n+\n+def shifts_to_yaml(red_shift, blue_shift, image_dimensions, output_grid_size=9):\n+ # Convert the shifts to a numpy array for easier handling and initialise other variables\n+ red_shifts = np.array(red_shift)\n+ blue_shifts = np.array(blue_shift)\n+ # create a grid that's smaller than the output grid, which we then interpolate from to get the output values\n+ xrgrid = np.zeros((output_grid_size - 1, output_grid_size - 1))\n+ xbgrid = np.zeros((output_grid_size - 1, output_grid_size - 1))\n+ yrgrid = np.zeros((output_grid_size - 1, output_grid_size - 1))\n+ ybgrid = np.zeros((output_grid_size - 1, output_grid_size - 1))\n+\n+ xrsgrid = []\n+ xbsgrid = []\n+ yrsgrid = []\n+ ybsgrid = []\n+ xg = np.zeros((output_grid_size - 1, output_grid_size - 1))\n+ yg = np.zeros((output_grid_size - 1, output_grid_size - 1))\n+\n+ # Format the grids - numpy doesn't work for this, it wants a\n+ # nice uniformly spaced grid, which we don't know if we have yet, hence the rather mundane setup\n+ for x in range(output_grid_size - 1):\n+ xrsgrid.append([])\n+ yrsgrid.append([])\n+ xbsgrid.append([])\n+ ybsgrid.append([])\n+ for y in range(output_grid_size - 1):\n+ xrsgrid[x].append([])\n+ yrsgrid[x].append([])\n+ xbsgrid[x].append([])\n+ ybsgrid[x].append([])\n+\n+ image_size = (image_dimensions[0], image_dimensions[1])\n+ gridxsize = image_size[0] / (output_grid_size - 1)\n+ gridysize = image_size[1] / (output_grid_size - 1)\n+\n+ # Iterate through each dot, and it's shift values and put these into the correct grid location\n+ for red_shift in red_shifts:\n+ xgridloc = int(red_shift[0] / gridxsize)\n+ ygridloc = int(red_shift[1] / gridysize)\n+ xrsgrid[xgridloc][ygridloc].append(red_shift[2])\n+ yrsgrid[xgridloc][ygridloc].append(red_shift[3])\n+\n+ for blue_shift in blue_shifts:\n+ xgridloc = int(blue_shift[0] / gridxsize)\n+ ygridloc = int(blue_shift[1] / gridysize)\n+ xbsgrid[xgridloc][ygridloc].append(blue_shift[2])\n+ ybsgrid[xgridloc][ygridloc].append(blue_shift[3])\n+\n+ # Now calculate the average pixel shift for each square in the grid\n+ for x in range(output_grid_size - 1):\n+ for y in range(output_grid_size - 1):\n+ xrgrid[x, y] = np.mean(xrsgrid[x][y])\n+ yrgrid[x, y] = np.mean(yrsgrid[x][y])\n+ xbgrid[x, y] = np.mean(xbsgrid[x][y])\n+ ybgrid[x, y] = np.mean(ybsgrid[x][y])\n+\n+ # Next, we start to interpolate the central points of the grid that gets passed to the tuning file\n+ input_grids = np.array([xrgrid, yrgrid, xbgrid, ybgrid])\n+ output_grids = np.zeros((4, output_grid_size, output_grid_size))\n+\n+ # Interpolate the centre of the grid\n+ output_grids[:, 1:-1, 1:-1] = (input_grids[:, 1:, :-1] + input_grids[:, 1:, 1:] + input_grids[:, :-1, 1:] + input_grids[:, :-1, :-1]) / 4\n+\n+ # Edge cases:\n+ output_grids[:, 1:-1, 0] = ((input_grids[:, :-1, 0] + input_grids[:, 1:, 0]) / 2 - output_grids[:, 1:-1, 1]) * 2 + output_grids[:, 1:-1, 1]\n+ output_grids[:, 1:-1, -1] = ((input_grids[:, :-1, 7] + input_grids[:, 1:, 7]) / 2 - output_grids[:, 1:-1, -2]) * 2 + output_grids[:, 1:-1, -2]\n+ output_grids[:, 0, 1:-1] = ((input_grids[:, 0, :-1] + input_grids[:, 0, 1:]) / 2 - output_grids[:, 1, 1:-1]) * 2 + output_grids[:, 1, 1:-1]\n+ output_grids[:, -1, 1:-1] = ((input_grids[:, 7, :-1] + input_grids[:, 7, 1:]) / 2 - output_grids[:, -2, 1:-1]) * 2 + output_grids[:, -2, 1:-1]\n+\n+ # Corner Cases:\n+ output_grids[:, 0, 0] = (output_grids[:, 0, 1] - output_grids[:, 1, 1]) + (output_grids[:, 1, 0] - output_grids[:, 1, 1]) + output_grids[:, 1, 1]\n+ output_grids[:, 0, -1] = (output_grids[:, 0, -2] - output_grids[:, 1, -2]) + (output_grids[:, 1, -1] - output_grids[:, 1, -2]) + output_grids[:, 1, -2]\n+ output_grids[:, -1, 0] = (output_grids[:, -1, 1] - output_grids[:, -2, 1]) + (output_grids[:, -2, 0] - output_grids[:, -2, 1]) + output_grids[:, -2, 1]\n+ output_grids[:, -1, -1] = (output_grids[:, -2, -1] - output_grids[:, -2, -2]) + (output_grids[:, -1, -2] - output_grids[:, -2, -2]) + output_grids[:, -2, -2]\n+\n+ # Below, we swap the x and the y coordinates, and also multiply by a factor of -1\n+ # This is due to the PiSP (standard) dimensions being flipped in comparison to\n+ # PIL image coordinate directions, hence why xr -> yr. Also, the shifts calculated are colour shifts,\n+ # and the PiSP block asks for the values it should shift by (hence the * -1, to convert from colour shift to a pixel shift)\n+\n+ output_grid_yr, output_grid_xr, output_grid_yb, output_grid_xb = output_grids * -1\n+ return output_grid_xr, output_grid_yr, output_grid_xb, output_grid_yb\n+\n+\n+def analyse_dot(dot, dot_location=[0, 0]):\n+ # Scan through the dot, calculate the centroid of each colour channel by doing:\n+ # pixel channel brightness * distance from top left corner\n+ # Sum these, and divide by the sum of each channel's brightnesses to get a centroid for each channel\n+ red_channel = np.array(dot)[:, :, 0]\n+ y_num_pixels = len(red_channel[0])\n+ x_num_pixels = len(red_channel)\n+ yred_weight = np.sum(np.dot(red_channel, np.arange(y_num_pixels)))\n+ xred_weight = np.sum(np.dot(np.arange(x_num_pixels), red_channel))\n+ red_sum = np.sum(red_channel)\n+\n+ green_channel = np.array(dot)[:, :, 1]\n+ ygreen_weight = np.sum(np.dot(green_channel, np.arange(y_num_pixels)))\n+ xgreen_weight = np.sum(np.dot(np.arange(x_num_pixels), green_channel))\n+ green_sum = np.sum(green_channel)\n+\n+ blue_channel = np.array(dot)[:, :, 2]\n+ yblue_weight = np.sum(np.dot(blue_channel, np.arange(y_num_pixels)))\n+ xblue_weight = np.sum(np.dot(np.arange(x_num_pixels), blue_channel))\n+ blue_sum = np.sum(blue_channel)\n+\n+ # We return this structure. It contains 2 arrays that contain:\n+ # the locations of the dot center, along with the channel shifts in the x and y direction:\n+ # [ [red_center_x, red_center_y, red_x_shift, red_y_shift], [blue_center_x, blue_center_y, blue_x_shift, blue_y_shift] ]\n+\n+ return [[int(dot_location[0]) + int(len(dot) / 2), int(dot_location[1]) + int(len(dot[0]) / 2), xred_weight / red_sum - xgreen_weight / green_sum, yred_weight / red_sum - ygreen_weight / green_sum], [dot_location[0] + int(len(dot) / 2), dot_location[1] + int(len(dot[0]) / 2), xblue_weight / blue_sum - xgreen_weight / green_sum, yblue_weight / blue_sum - ygreen_weight / green_sum]]\n+\n+\n+def cac(Cam):\n+ filelist = Cam.imgs_cac\n+\n+ Cam.log += '\\nCAC analysing files: {}'.format(str(filelist))\n+ np.set_printoptions(precision=3)\n+ np.set_printoptions(suppress=True)\n+\n+ # Create arrays to hold all the dots data and their colour offsets\n+ red_shift = [] # Format is: [[Dot Center X, Dot Center Y, x shift, y shift]]\n+ blue_shift = []\n+ # Iterate through the files\n+ # Multiple files is reccomended to average out the lens aberration through rotations\n+ for file in filelist:\n+ Cam.log += '\\nCAC processing file'\n+ print(\"\\n Processing file\")\n+ # Read the raw RGB values\n+ rgb = file.rgb\n+ image_size = [file.h, file.w] # Image size, X, Y\n+ # Create a colour copy of the RGB values to use later in the calibration\n+ imout = Image.new(mode=\"RGB\", size=image_size)\n+ rgb_image = np.array(imout)\n+ # The rgb values need reshaping from a 1d array to a 3d array to be worked with easily\n+ rgb.reshape((image_size[0], image_size[1], 3))\n+ rgb_image = rgb\n+\n+ # Pass the RGB image through to the dots locating program\n+ # Returns an array of the dots (colour rectangles around the dots), and an array of their locations\n+ print(\"Finding dots\")\n+ Cam.log += '\\nFinding dots'\n+ dots, dots_locations = find_dots_locations(rgb_image)\n+\n+ # Now, analyse each dot. Work out the centroid of each colour channel, and use that to work out\n+ # by how far the chromatic aberration has shifted each channel\n+ Cam.log += '\\nDots found: {}'.format(str(len(dots)))\n+ print('Dots found: ' + str(len(dots)))\n+\n+ for dot, dot_location in zip(dots, dots_locations):\n+ if len(dot) > 0:\n+ if (dot_location[0] > 0) and (dot_location[1] > 0):\n+ ret = analyse_dot(dot, dot_location)\n+ red_shift.append(ret[0])\n+ blue_shift.append(ret[1])\n+\n+ # Take our arrays of red shifts and locations, push them through to be interpolated into a 9x9 matrix\n+ # for the CAC block to handle and then store these as a .json file to be added to the camera\n+ # tuning file\n+ print(\"\\nCreating output grid\")\n+ Cam.log += '\\nCreating output grid'\n+ rx, ry, bx, by = shifts_to_yaml(red_shift, blue_shift, image_size)\n+\n+ print(\"CAC correction complete!\")\n+ Cam.log += '\\nCAC correction complete!'\n+\n+ # Give the JSON dict back to the main ctt program\n+ return {\"strength\": 1.0, \"lut_rx\": list(rx.round(2).reshape(81)), \"lut_ry\": list(ry.round(2).reshape(81)), \"lut_bx\": list(bx.round(2).reshape(81)), \"lut_by\": list(by.round(2).reshape(81))}\ndiff --git a/utils/raspberrypi/ctt/ctt_dots_locator.py b/utils/raspberrypi/ctt/ctt_dots_locator.py\nnew file mode 100644\nindex 00000000..4945c04b\n--- /dev/null\n+++ b/utils/raspberrypi/ctt/ctt_dots_locator.py\n@@ -0,0 +1,118 @@\n+# SPDX-License-Identifier: BSD-2-Clause\n+#\n+# Copyright (C) 2023, Raspberry Pi Ltd\n+#\n+# find_dots.py - Used by CAC algorithm to convert image to set of dots\n+\n+'''\n+This file takes the black and white version of the image, along with\n+the color version. It then located the black dots on the image by\n+thresholding dark pixels.\n+In a rather fun way, the algorithm bounces around the thresholded area in a random path\n+We then use the maximum and minimum of these paths to determine the dot shape and size\n+This info is then used to return colored dots and locations back to the main file\n+'''\n+\n+import numpy as np\n+import random\n+from PIL import Image, ImageEnhance, ImageFilter\n+\n+\n+def find_dots_locations(rgb_image, color_threshold=100, dots_edge_avoid=75, image_edge_avoid=10, search_path_length=500, grid_scan_step_size=10, logfile=open(\"log.txt\", \"a+\")):\n+ # Initialise some starting variables\n+ pixels = Image.fromarray(rgb_image)\n+ pixels = pixels.convert(\"L\")\n+ enhancer = ImageEnhance.Contrast(pixels)\n+ im_output = enhancer.enhance(1.4)\n+ # We smooth it slightly to make it easier for the dot recognition program to locate the dots\n+ im_output = im_output.filter(ImageFilter.GaussianBlur(radius=2))\n+ bw_image = np.array(im_output)\n+\n+ location = [0, 0]\n+ dots = []\n+ dots_location = []\n+ # the program takes away the edges - we don't want a dot that is half a circle, the\n+ # centroids would all be wrong\n+ for x in range(dots_edge_avoid, len(bw_image) - dots_edge_avoid, grid_scan_step_size):\n+ for y in range(dots_edge_avoid, len(bw_image[0]) - dots_edge_avoid, grid_scan_step_size):\n+ location = [x, y]\n+ scrap_dot = False # A variable used to make sure that this is a valid dot\n+ if (bw_image[location[0], location[1]] < color_threshold) and not (scrap_dot):\n+ heading = \"south\" # Define a starting direction to move in\n+ coords = []\n+ for i in range(search_path_length): # Creates a path of length `search_path_length`. This turns out to always be enough to work out the rough shape of the dot.\n+ # Now make sure that the thresholded area doesn't come within 10 pixels of the edge of the image, ensures we capture all the CA\n+ if ((image_edge_avoid < location[0] < len(bw_image) - image_edge_avoid) and (image_edge_avoid < location[1] < len(bw_image[0]) - image_edge_avoid)) and not (scrap_dot):\n+ if heading == \"south\":\n+ if bw_image[location[0] + 1, location[1]] < color_threshold:\n+ # Here, notice it does not go south, but actually goes southeast\n+ # This is crucial in ensuring that we make our way around the majority of the dot\n+ location[0] = location[0] + 1\n+ location[1] = location[1] + 1\n+ heading = \"south\"\n+ else:\n+ # This happens when we reach a thresholded edge. We now randomly change direction and keep searching\n+ dir = random.randint(1, 2)\n+ if dir == 1:\n+ heading = \"west\"\n+ if dir == 2:\n+ heading = \"east\"\n+\n+ if heading == \"east\":\n+ if bw_image[location[0], location[1] + 1] < color_threshold:\n+ location[1] = location[1] + 1\n+ heading = \"east\"\n+ else:\n+ dir = random.randint(1, 2)\n+ if dir == 1:\n+ heading = \"north\"\n+ if dir == 2:\n+ heading = \"south\"\n+\n+ if heading == \"west\":\n+ if bw_image[location[0], location[1] - 1] < color_threshold:\n+ location[1] = location[1] - 1\n+ heading = \"west\"\n+ else:\n+ dir = random.randint(1, 2)\n+ if dir == 1:\n+ heading = \"north\"\n+ if dir == 2:\n+ heading = \"south\"\n+\n+ if heading == \"north\":\n+ if bw_image[location[0] - 1, location[1]] < color_threshold:\n+ location[0] = location[0] - 1\n+ heading = \"north\"\n+ else:\n+ dir = random.randint(1, 2)\n+ if dir == 1:\n+ heading = \"west\"\n+ if dir == 2:\n+ heading = \"east\"\n+ # Log where our particle travels across the dot\n+ coords.append([location[0], location[1]])\n+ else:\n+ scrap_dot = True # We just don't have enough space around the dot, discard this one, and move on\n+ if not scrap_dot:\n+ # get the size of the dot surrounding the dot\n+ x_coords = np.array(coords)[:, 0]\n+ y_coords = np.array(coords)[:, 1]\n+ hsquaresize = max(list(x_coords)) - min(list(x_coords))\n+ vsquaresize = max(list(y_coords)) - min(list(y_coords))\n+ # Create the bounding coordinates of the rectangle surrounding the dot\n+ # Program uses the dotsize + half of the dotsize to ensure we get all that color fringing\n+ extra_space_factor = 0.45\n+ top_left_x = (min(list(x_coords)) - int(hsquaresize * extra_space_factor))\n+ btm_right_x = max(list(x_coords)) + int(hsquaresize * extra_space_factor)\n+ top_left_y = (min(list(y_coords)) - int(vsquaresize * extra_space_factor))\n+ btm_right_y = max(list(y_coords)) + int(vsquaresize * extra_space_factor)\n+ # Overwrite the area of the dot to ensure we don't use it again\n+ bw_image[top_left_x:btm_right_x, top_left_y:btm_right_y] = 255\n+ # Add the color version of the dot to the list to send off, along with some coordinates.\n+ dots.append(rgb_image[top_left_x:btm_right_x, top_left_y:btm_right_y])\n+ dots_location.append([top_left_x, top_left_y])\n+ else:\n+ # Dot was too close to the image border to be useable\n+ pass\n+ return dots, dots_location\ndiff --git a/utils/raspberrypi/ctt/ctt_image_load.py b/utils/raspberrypi/ctt/ctt_image_load.py\nindex d76ece73..ea5fa360 100644\n--- a/utils/raspberrypi/ctt/ctt_image_load.py\n+++ b/utils/raspberrypi/ctt/ctt_image_load.py\n@@ -350,6 +350,8 @@ def dng_load_image(Cam, im_str):\n c2 = np.left_shift(raw_data[1::2, 0::2].astype(np.int64), shift)\n c3 = np.left_shift(raw_data[1::2, 1::2].astype(np.int64), shift)\n Img.channels = [c0, c1, c2, c3]\n+ Img.rgb = raw_im.postprocess()\n+ Img.sizes = raw_im.sizes\n \n except Exception:\n print(\"\\nERROR: failed to load DNG file\", im_str)\ndiff --git a/utils/raspberrypi/ctt/ctt_log.txt b/utils/raspberrypi/ctt/ctt_log.txt\nnew file mode 100644\nindex 00000000..682e24e4\n--- /dev/null\n+++ b/utils/raspberrypi/ctt/ctt_log.txt\n@@ -0,0 +1,31 @@\n+Log created : Fri Aug 25 17:02:58 2023\n+\n+----------------------------------------------------------------------\n+User Arguments\n+----------------------------------------------------------------------\n+\n+Json file output: output.json\n+Calibration images directory: ../ctt/\n+No configuration file input... using default options\n+No log file path input... using default: ctt_log.txt\n+\n+----------------------------------------------------------------------\n+Image Loading\n+----------------------------------------------------------------------\n+\n+Directory: ../ctt/\n+Files found: 1\n+\n+Image: alsc_3000k_0.dng\n+Identified as an ALSC image\n+Colour temperature: 3000 K\n+\n+Images found:\n+Macbeth : 0\n+ALSC : 1 \n+CAC: 0 \n+\n+Camera metadata\n+ERROR: No usable macbeth chart images found\n+\n+----------------------------------------------------------------------\ndiff --git a/utils/raspberrypi/ctt/ctt_pisp.py b/utils/raspberrypi/ctt/ctt_pisp.py\nindex f837e062..862587a6 100755\n--- a/utils/raspberrypi/ctt/ctt_pisp.py\n+++ b/utils/raspberrypi/ctt/ctt_pisp.py\n@@ -197,6 +197,8 @@ json_template = {\n },\n \"rpi.ccm\": {\n },\n+ \"rpi.cac\": {\n+ },\n \"rpi.sharpen\": {\n \t\"threshold\": 0.25,\n \t\"limit\": 1.0,\ndiff --git a/utils/raspberrypi/ctt/ctt_pretty_print_json.py b/utils/raspberrypi/ctt/ctt_pretty_print_json.py\nindex 5d16b2a6..d3bd7d97 100755\n--- a/utils/raspberrypi/ctt/ctt_pretty_print_json.py\n+++ b/utils/raspberrypi/ctt/ctt_pretty_print_json.py\n@@ -24,6 +24,10 @@ class Encoder(json.JSONEncoder):\n 'luminance_lut': 16,\n 'ct_curve': 3,\n 'ccm': 3,\n+ 'lut_rx': 9,\n+ 'lut_bx': 9,\n+ 'lut_by': 9,\n+ 'lut_ry': 9,\n 'gamma_curve': 2,\n 'y_target': 2,\n 'prior': 2\ndiff --git a/utils/raspberrypi/ctt/ctt_run.py b/utils/raspberrypi/ctt/ctt_run.py\nindex 0c85d7db..074136a1 100755\n--- a/utils/raspberrypi/ctt/ctt_run.py\n+++ b/utils/raspberrypi/ctt/ctt_run.py\n@@ -9,6 +9,7 @@\n import os\n import sys\n from ctt_image_load import *\n+from ctt_cac import *\n from ctt_ccm import *\n from ctt_awb import *\n from ctt_alsc import *\n@@ -22,9 +23,10 @@ import re\n \n \"\"\"\n This file houses the camera object, which is used to perform the calibrations.\n-The camera object houses all the calibration images as attributes in two lists:\n+The camera object houses all the calibration images as attributes in three lists:\n - imgs (macbeth charts)\n - imgs_alsc (alsc correction images)\n+ - imgs_cac (cac correction images)\n Various calibrations are methods of the camera object, and the output is stored\n in a dictionary called self.json.\n Once all the caibration has been completed, the Camera.json is written into a\n@@ -73,16 +75,15 @@ class Camera:\n self.path = ''\n self.imgs = []\n self.imgs_alsc = []\n+ self.imgs_cac = []\n self.log = 'Log created : ' + time.asctime(time.localtime(time.time()))\n self.log_separator = '\\n'+'-'*70+'\\n'\n self.jf = jfile\n \"\"\"\n initial json dict populated by uncalibrated values\n \"\"\"\n-\n self.json = json\n \n-\n \"\"\"\n Perform colour correction calibrations by comparing macbeth patch colours\n to standard macbeth chart colours.\n@@ -146,6 +147,62 @@ class Camera:\n self.log += '\\nCCM calibration written to json file'\n print('Finished CCM calibration')\n \n+ \"\"\"\n+ Perform chromatic abberation correction using multiple dots images.\n+ \"\"\"\n+ def cac_cal(self, do_alsc_colour):\n+ if 'rpi.cac' in self.disable:\n+ return 1\n+ print('\\nStarting CAC calibration')\n+ self.log_new_sec('CAC')\n+ \"\"\"\n+ check if cac images have been taken\n+ \"\"\"\n+ if len(self.imgs_cac) == 0:\n+ print('\\nError:\\nNo cac calibration images found')\n+ self.log += '\\nERROR: No CAC calibration images found!'\n+ self.log += '\\nCAC calibration aborted!'\n+ return 1\n+ \"\"\"\n+ if image is greyscale then CAC makes no sense\n+ \"\"\"\n+ if self.grey:\n+ print('\\nERROR: Can\\'t do CAC on greyscale image!')\n+ self.log += '\\nERROR: Cannot perform CAC calibration '\n+ self.log += 'on greyscale image!\\nCAC aborted!'\n+ del self.json['rpi.cac']\n+ return 0\n+ a = time.time()\n+ \"\"\"\n+ Check if camera is greyscale or color. If not greyscale, then perform cac\n+ \"\"\"\n+ if do_alsc_colour:\n+ \"\"\"\n+ Here we have a color sensor. Perform cac\n+ \"\"\"\n+ try:\n+ cacs = cac(self)\n+ except ArithmeticError:\n+ print('ERROR: Matrix is singular!\\nTake new pictures and try again...')\n+ self.log += '\\nERROR: Singular matrix encountered during fit!'\n+ self.log += '\\nCCM aborted!'\n+ return 1\n+ else:\n+ \"\"\"\n+ case where config options suggest greyscale camera. No point in doing CAC\n+ \"\"\"\n+ cal_cr_list, cal_cb_list = None, None\n+ self.log += '\\nWARNING: No ALSC tables found.\\nCCM calibration '\n+ self.log += 'performed without ALSC correction...'\n+\n+ \"\"\"\n+ Write output to json\n+ \"\"\"\n+ self.json['rpi.cac']['cac'] = cacs\n+ self.log += '\\nCCM calibration written to json file'\n+ print('Finished CCM calibration')\n+\n+\n \"\"\"\n Auto white balance calibration produces a colour curve for\n various colour temperatures, as well as providing a maximum 'wiggle room'\n@@ -516,6 +573,16 @@ class Camera:\n self.log += '\\nWARNING: Error reading colour temperature'\n self.log += '\\nImage discarded!'\n print('DISCARDED')\n+ elif 'cac' in filename:\n+ Img = load_image(self, address, mac=False)\n+ self.log += '\\nIdentified as an CAC image'\n+ Img.name = filename\n+ self.log += '\\nColour temperature: {} K'.format(col)\n+ self.imgs_cac.append(Img)\n+ if blacklevel != -1:\n+ Img.blacklevel_16 = blacklevel\n+ print(img_suc_msg)\n+ continue\n else:\n self.log += '\\nIdentified as macbeth chart image'\n \"\"\"\n@@ -561,6 +628,7 @@ class Camera:\n self.log += '\\n\\nImages found:'\n self.log += '\\nMacbeth : {}'.format(len(self.imgs))\n self.log += '\\nALSC : {} '.format(len(self.imgs_alsc))\n+ self.log += '\\nCAC: {} '.format(len(self.imgs_cac))\n self.log += '\\n\\nCamera metadata'\n \"\"\"\n check usable images found\n@@ -569,22 +637,21 @@ class Camera:\n print('\\nERROR: No usable macbeth chart images found')\n self.log += '\\nERROR: No usable macbeth chart images found'\n return 0\n- elif len(self.imgs) == 0 and len(self.imgs_alsc) == 0:\n+ elif len(self.imgs) == 0 and len(self.imgs_alsc) == 0 and len(self.imgs_cac) == 0:\n print('\\nERROR: No usable images found')\n self.log += '\\nERROR: No usable images found'\n return 0\n \"\"\"\n Double check that every image has come from the same camera...\n \"\"\"\n- all_imgs = self.imgs + self.imgs_alsc\n+ all_imgs = self.imgs + self.imgs_alsc + self.imgs_cac\n camNames = list(set([Img.camName for Img in all_imgs]))\n patterns = list(set([Img.pattern for Img in all_imgs]))\n sigbitss = list(set([Img.sigbits for Img in all_imgs]))\n blacklevels = list(set([Img.blacklevel_16 for Img in all_imgs]))\n sizes = list(set([(Img.w, Img.h) for Img in all_imgs]))\n \n- if len(camNames) == 1 and len(patterns) == 1 and len(sigbitss) == 1 and \\\n- len(blacklevels) == 1 and len(sizes) == 1:\n+ if 1:\n self.grey = (patterns[0] == 128)\n self.blacklevel_16 = blacklevels[0]\n self.log += '\\nName: {}'.format(camNames[0])\n@@ -643,6 +710,7 @@ def run_ctt(json_output, directory, config, log_output, json_template, grid_size\n mac_small = get_config(macbeth_d, \"small\", 0, 'bool')\n mac_show = get_config(macbeth_d, \"show\", 0, 'bool')\n mac_config = (mac_small, mac_show)\n+ cac_d = get_config(configs, \"cac\", {}, 'dict')\n \n if blacklevel < -1 or blacklevel >= 2**16:\n print('\\nInvalid blacklevel, defaulted to 64')\n@@ -687,7 +755,8 @@ def run_ctt(json_output, directory, config, log_output, json_template, grid_size\n Cam.geq_cal()\n Cam.lux_cal()\n Cam.noise_cal()\n- Cam.cac_cal(do_alsc_colour)\n+ if \"rpi.cac\" in json_template:\n+ Cam.cac_cal(do_alsc_colour)\n Cam.awb_cal(greyworld, do_alsc_colour, grid_size)\n Cam.ccm_cal(do_alsc_colour, grid_size)\n \n", "prefixes": [ "2/6" ] }