{"id":25964,"url":"https://patchwork.libcamera.org/api/patches/25964/?format=json","web_url":"https://patchwork.libcamera.org/patch/25964/","project":{"id":1,"url":"https://patchwork.libcamera.org/api/projects/1/?format=json","name":"libcamera","link_name":"libcamera","list_id":"libcamera_core","list_email":"libcamera-devel@lists.libcamera.org","web_url":"","scm_url":"","webscm_url":""},"msgid":"<20260126104256.119697-7-rick.w.ten.wolde@gmail.com>","date":"2026-01-26T10:42:54","name":"[6/7] utils/tuning: Add LSC scripts","commit_ref":null,"pull_url":null,"state":"new","archived":false,"hash":"f54deb28530dc7ab7a5fc7d04296dea0ded577e9","submitter":{"id":257,"url":"https://patchwork.libcamera.org/api/people/257/?format=json","name":"Rick ten Wolde","email":"rick.w.ten.wolde@gmail.com"},"delegate":null,"mbox":"https://patchwork.libcamera.org/patch/25964/mbox/","series":[{"id":5739,"url":"https://patchwork.libcamera.org/api/series/5739/?format=json","web_url":"https://patchwork.libcamera.org/project/libcamera/list/?series=5739","date":"2026-01-26T10:42:48","name":"LSC for SoftISP simple pipeline","version":1,"mbox":"https://patchwork.libcamera.org/series/5739/mbox/"}],"comments":"https://patchwork.libcamera.org/api/patches/25964/comments/","check":"pending","checks":"https://patchwork.libcamera.org/api/patches/25964/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 792CBC32E7\n\tfor <parsemail@patchwork.libcamera.org>;\n\tMon, 26 Jan 2026 10:48:18 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id E5B4C61FD1;\n\tMon, 26 Jan 2026 11:48:16 +0100 (CET)","from mail-ej1-x62b.google.com (mail-ej1-x62b.google.com\n\t[IPv6:2a00:1450:4864:20::62b])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 3399C61FD3\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tMon, 26 Jan 2026 11:43:08 +0100 (CET)","by mail-ej1-x62b.google.com with SMTP id\n\ta640c23a62f3a-b8850aa5b56so622017866b.2\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tMon, 26 Jan 2026 02:43:08 -0800 (PST)","from castortop.wolde.loc (195-240-110-192.fixed.kpn.net.\n\t[195.240.110.192]) by smtp.gmail.com with ESMTPSA id\n\ta640c23a62f3a-b885b7661f7sm599220366b.54.2026.01.26.02.43.06\n\t(version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);\n\tMon, 26 Jan 2026 02:43:06 -0800 (PST)"],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (2048-bit key;\n\tunprotected) header.d=gmail.com header.i=@gmail.com\n\theader.b=\"BiVQyINT\"; dkim-atps=neutral","DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=gmail.com; s=20230601; t=1769424187; x=1770028987;\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=UGMhLAQJP7biuVIZMSesOWnaMhbq21WkJYjcWc6nhwQ=;\n\tb=BiVQyINTRtuMoSFKmFBtaVpfqeZqz5rQ2dsi5KveDjpb6HbnvFC6nY4p9Gp7Ky1FNn\n\taNoxO989p3Uj8w5HI8hZFe5/iymYryWIJkwnxpxHhieoHI3pY74Q3oRHZqyV8igzWEWh\n\t/RJPfhpxy9RnOBcrFAdLDSJVYQcuLWxZ8JxcznD9y5BRB+cys5R108IyOiIL989l7u8A\n\tFnnW8eGa5M4BOrcZo7NX3+tuTFZzZB9QIXZrzK0aENkx5GrAFKzfVEmO3RlRgS73slHE\n\t70/Osve22Q0mM20lWvMQpuaBLIWC0lK/p1aT9olgut+Oh18G2cyzB4aI6oHtBayUehK/\n\t5N/Q==","X-Google-DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=1e100.net; s=20230601; t=1769424187; x=1770028987;\n\th=content-transfer-encoding:mime-version:references:in-reply-to\n\t:message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from\n\t:to:cc:subject:date:message-id:reply-to;\n\tbh=UGMhLAQJP7biuVIZMSesOWnaMhbq21WkJYjcWc6nhwQ=;\n\tb=GRjrO6DEpuWyK6wPf6Vy3Z1uiqL3tQwHmT5Id1MKtgZjmy2sDCTcLNDYTfQQvKCoPM\n\tSe5WWomiDym4F6eftqqMCpFU2hUdsNkVSgLbmwAu+4no7Nsm9pd8WfKBKnl7J1olAa8V\n\t+7ylBK32OZ3ECWmCAAubLjmkwhJI92pGxYc+LfzFNLSy6my6kAyiujL7vzPvVrDuhQAA\n\tdsiVUBHyeEhJ/5kcooY3jH057pIR1y59RpJn+KcZTG0GAlpoKIvUEjpzlBy7YjN+QnBH\n\t4EXVMdJgOgnLExZF6KouqNb3PcVW+3VwKQXt2AWFLAJv39aUAdJ1bGfjRnbDZs+FcDMf\n\to9RQ==","X-Gm-Message-State":"AOJu0YxRH0bogJ1fMlXyM3OrS/UDU7cZnp7MK+x73eLUl22zaGHp0qFW\n\t/Hp5pHCxCZfRWuL6Si5mIY+uEvqkdBx7SsgyYk0UJfHAa5wnEuvF4P55+cNrPw==","X-Gm-Gg":"AZuq6aKkFGrITgDbsYMZL0+WXnfr8juzpyaHNIrYRiN9M4XW4zmPSOss7JfqcjKhT8r\n\toSbfsHQAAyozfIRm7iSi3h9a2QYpJRiX9Dg67iw9scMcCSN00GPDbSALWU5y1jR0pgxp4GZrPhL\n\tX99er6sn02sPsvkr69ZvsSUglmmUek6dv0Pdd8jedmRwN7Y/UWDDlejtNIHiGaY6gdYreGIxR8p\n\tTAgpgpnOPrY2MvWnyl2QTXnsTM+V/lGqCmV3rAPJpFgbp0vey57G+sTmsJPYv7kMXyhsBcELj+A\n\tNQOUO2Glx8MCHqBGq6shoyn8PQiIumIxCMPst7L6HiRpLbY3HmLTJp+bFKYvRI0Fc7EFABmQxtc\n\tMtJx7xXIrtkUoDs+Q6aQlcg7sH5YBM+iLC539Ot/Fnio5A2F8VLCf6didyvdRg7u/GtK+NF/8j0\n\thFUtp87gTOXFcbmmqwprn/QuKIOb/7B2/zt1KPl/0sEiaY9VAFpo0Je9tkgjuh7m4=","X-Received":"by 2002:a17:907:7b85:b0:b88:2848:dec8 with SMTP id\n\ta640c23a62f3a-b8d2e8330ddmr324052666b.54.1769424187157; \n\tMon, 26 Jan 2026 02:43:07 -0800 (PST)","From":"Rick ten Wolde <rick.w.ten.wolde@gmail.com>","To":"libcamera-devel@lists.libcamera.org","Cc":"xander.c.pronk@gmail.com, derekgielen@outlook.com,\n\t22012540@student.hhs.nl, \n\trick.w.ten.wolde@gmail.com, johannes.goede@oss.qualcomm.com,\n\tAron Dosti <aron-dosti@hotmail.com>","Subject":"[PATCH 6/7] utils/tuning: Add LSC scripts","Date":"Mon, 26 Jan 2026 11:42:54 +0100","Message-ID":"<20260126104256.119697-7-rick.w.ten.wolde@gmail.com>","X-Mailer":"git-send-email 2.51.0","In-Reply-To":"<20260126104256.119697-1-rick.w.ten.wolde@gmail.com>","References":"<20260126104256.119697-1-rick.w.ten.wolde@gmail.com>","MIME-Version":"1.0","Content-Transfer-Encoding":"8bit","X-Mailman-Approved-At":"Mon, 26 Jan 2026 11:48:05 +0100","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: Derek Gielen <derekgielen@outlook.com>\n\nAdd script for the conversion of the tuning file to a format usable by\nthe lens shading correction shader.\n\nAdd script for visualizing the lens shading correction using pyplot.\n\nCo-authored-by: Aron Dosti <aron-dosti@hotmail.com>\nSigned-off-by: Aron Dosti <aron-dosti@hotmail.com>\nSigned-off-by: Derek Gielen <derekgielen@outlook.com>\n---\n utils/tuning/exportTuningToLscShader.py | 120 ++++++++++++++++++++++++\n utils/tuning/generate_lsc_map_plot.py   |  76 +++++++++++++++\n 2 files changed, 196 insertions(+)\n create mode 100644 utils/tuning/exportTuningToLscShader.py\n create mode 100644 utils/tuning/generate_lsc_map_plot.py","diff":"diff --git a/utils/tuning/exportTuningToLscShader.py b/utils/tuning/exportTuningToLscShader.py\nnew file mode 100644\nindex 00000000..4f07aa1e\n--- /dev/null\n+++ b/utils/tuning/exportTuningToLscShader.py\n@@ -0,0 +1,120 @@\n+import yaml\n+import numpy as np\n+import argparse\n+import sys\n+\n+# --- Configuration ---\n+GRID_W = 16\n+GRID_H = 16\n+\n+# Formula constants\n+BLACK_LEVEL = 1024.0\n+SCALE_FACTOR = 3071  # Divisor to map the range to 0-255\n+\n+def load_and_process_yaml(input_filename, target_ct):\n+    # 1. Load the YAML file\n+    try:\n+        with open(input_filename, 'r') as f:\n+            data = yaml.safe_load(f)\n+    except FileNotFoundError:\n+        print(f\"Error: Input file '{input_filename}' not found.\")\n+        return None, None, None\n+\n+    # 2. Find the LensShadingCorrection block\n+    lsc_data = None\n+    for algo in data['algorithms']:\n+        if 'LensShadingCorrection' in algo:\n+            lsc_data = algo['LensShadingCorrection']\n+            break\n+\n+    if not lsc_data:\n+        print(\"Error: LensShadingCorrection block not found.\")\n+        return None, None, None\n+\n+    # 3. Extract the set for the specific Color Temperature (CT) provided in arguments\n+    sets = lsc_data['sets']\n+    target_set = next((item for item in sets if item['ct'] == target_ct), None)\n+\n+    if not target_set:\n+        print(f\"Error: Set for Color Temperature {target_ct} not found in '{input_filename}'.\")\n+        return None, None, None\n+\n+    print(f\"Found data for CT {target_ct}. Applying formula: (x - {int(BLACK_LEVEL)}) / {SCALE_FACTOR} ...\")\n+\n+    # 4. Get Raw Data\n+    r_raw = np.array(target_set['r'])\n+    b_raw = np.array(target_set['b'])\n+    gr_raw = np.array(target_set['gr'])\n+    gb_raw = np.array(target_set['gb'])\n+\n+    # Calculate Green Channel (Average of GR and GB)\n+    g_raw = (gr_raw + gb_raw) / 2.0\n+\n+    # 5. Define the calculation logic\n+    def apply_formula(data_array):\n+        \"\"\"\n+        Applies the specific user formula:\n+        1. Subtract Black Level (1024)\n+        2. Divide by the Scale Factor (3071)\n+        3. Multiply by 255 and convert to integer\n+        \"\"\"\n+        result = ((data_array - BLACK_LEVEL) / SCALE_FACTOR) * 255\n+        return result.astype(int)\n+\n+    # 6. Apply calculation to all channels\n+    r_final = apply_formula(r_raw)\n+    g_final = apply_formula(g_raw)\n+    b_final = apply_formula(b_raw)\n+\n+    return r_final, g_final, b_final\n+\n+def save_custom_grid_yaml(output_filename, r, g, b, target_ct):\n+\n+    # Helper function to format the array as a visual grid string\n+    def format_array_as_grid_string(arr):\n+        lines = []\n+        # Loop through the array in chunks of 16 (GRID_W)\n+        for i in range(0, len(arr), GRID_W):\n+            row = arr[i:i+GRID_W]\n+            # Join numbers with commas\n+            row_str = \", \".join(map(str, row))\n+            lines.append(f\"    {row_str}\")\n+        # Wrap in brackets to form a valid YAML list, but visually formatted\n+        return \"[\\n\" + \",\\n\".join(lines) + \"\\n  ]\"\n+\n+    # Write the file manually to ensure specific formatting\n+    with open(output_filename, 'w') as f:\n+        f.write(f\"description: 'LSC Fixed Formula ((x-{int(BLACK_LEVEL)})/{SCALE_FACTOR})'\\n\")\n+        f.write(f\"source_ct: {target_ct}\\n\")\n+        f.write(f\"grid_size: [{GRID_W}, {GRID_H}]\\n\")\n+        f.write(f\"formula_used: '(RawValue - {int(BLACK_LEVEL)}) / {SCALE_FACTOR} -> [0..255]'\\n\")\n+        f.write(f\"channels:\\n\")\n+\n+        f.write(\"  red: \" + format_array_as_grid_string(r) + \"\\n\")\n+        f.write(\"  green: \" + format_array_as_grid_string(g) + \"\\n\")\n+        f.write(\"  blue: \" + format_array_as_grid_string(b) + \"\\n\")\n+\n+    print(f\"Success! Saved formatted grid to '{output_filename}'\")\n+\n+# --- Main Execution ---\n+if __name__ == \"__main__\":\n+    # 1. Setup Argument Parser\n+    parser = argparse.ArgumentParser(description=\"Convert LSC YAML data to shader grid format.\")\n+    parser.add_argument(\"ct\", type=int, help=\"The Color Temperature to process (e.g. 2700, 5000, 6500)\")\n+\n+    # 2. Parse arguments\n+    args = parser.parse_args()\n+    ct_val = args.ct\n+\n+    # 3. Construct filenames based on the CT value\n+    # Assumes input file is named 'tuning_XXXX.yaml'\n+    input_file = f'tuning{ct_val}.yaml'\n+    output_file = f'lsc_shader_16x16_{ct_val}_fixed.yaml'\n+\n+    print(f\"--- Processing for Color Temp: {ct_val} ---\")\n+\n+    # 4. Run Process\n+    r, g, b = load_and_process_yaml(input_file, ct_val)\n+\n+    if r is not None:\n+        save_custom_grid_yaml(output_file, r, g, b, ct_val)\ndiff --git a/utils/tuning/generate_lsc_map_plot.py b/utils/tuning/generate_lsc_map_plot.py\nnew file mode 100644\nindex 00000000..67b3a041\n--- /dev/null\n+++ b/utils/tuning/generate_lsc_map_plot.py\n@@ -0,0 +1,76 @@\n+import yaml\n+import numpy as np\n+import matplotlib.pyplot as plt\n+\n+# 1. Load the data\n+try:\n+    with open('tuning2700.yaml', 'r') as f:\n+        data = yaml.safe_load(f)\n+except FileNotFoundError:\n+    print(\"Error: 'tuning.yaml' not found. Please ensure the file exists.\")\n+    exit()\n+\n+# 2. Find LensShadingCorrection safely\n+lsc_data = None\n+for algo in data['algorithms']:\n+    if 'LensShadingCorrection' in algo:\n+        lsc_data = algo['LensShadingCorrection']\n+        break\n+\n+if not lsc_data:\n+    print(\"Error: LensShadingCorrection block not found in YAML.\")\n+    exit()\n+\n+# 3. Extract the set for disirable Kelvin\n+kelvin = 2700\n+sets = lsc_data['sets']\n+target_set = next((item for item in sets if item['ct'] == kelvin), None)\n+\n+if not target_set:\n+    print(\"Error: CT 6500 not found in sets.\")\n+    exit()\n+\n+# 4. Get lists and normalize (1024 = 1.0 gain)\n+r_list = np.array(target_set['r'])\n+gr_list = np.array(target_set['gr'])\n+gb_list = np.array(target_set['gb'])\n+b_list = np.array(target_set['b'])\n+\n+r_norm = r_list / 1024.0\n+b_norm = b_list / 1024.0\n+# Average the two greens for the shader\n+g_norm = (gr_list + gb_list) / 2.0 / 1024.0\n+\n+# 5. Reshape into 17x17 Grids\n+grid_size = (17, 17)\n+r_grid = r_norm.reshape(grid_size)\n+g_grid = g_norm.reshape(grid_size)\n+b_grid = b_norm.reshape(grid_size)\n+\n+# 6. Visualization\n+# We create 3 separate plots to see the data distribution correctly\n+fig, axs = plt.subplots(1, 3, figsize=(15, 5))\n+\n+# Plot Red\n+im1 = axs[0].imshow(r_grid, cmap='viridis')\n+axs[0].set_title('Red Gain Map')\n+fig.colorbar(im1, ax=axs[0])\n+\n+# Plot Green\n+im2 = axs[1].imshow(g_grid, cmap='viridis')\n+axs[1].set_title('Green Gain Map')\n+fig.colorbar(im2, ax=axs[1])\n+\n+# Plot Blue\n+im3 = axs[2].imshow(b_grid, cmap='viridis')\n+axs[2].set_title('Blue Gain Map')\n+fig.colorbar(im3, ax=axs[2])\n+\n+plt.suptitle(f\"LSC Gain Maps (Center ~1.0, Corners > 1.0) for collor temprature {kelvin}\")\n+plt.show()\n+\n+# 7. Prepare Texture for Export (Optional)\n+# Stack them for your shader: (17, 17, 3)\n+lsc_texture = np.dstack((r_grid, g_grid, b_grid))\n+print(f\"Final Texture Shape: {lsc_texture.shape}\")\n+print(f\"Sample Blue Value at Corner: {b_grid[0,0]}\")\n","prefixes":["6/7"]}