{"id":26529,"url":"https://patchwork.libcamera.org/api/1.1/patches/26529/?format=json","web_url":"https://patchwork.libcamera.org/patch/26529/","project":{"id":1,"url":"https://patchwork.libcamera.org/api/1.1/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":"<20260420183949.110548-9-mzamazal@redhat.com>","date":"2026-04-20T18:39:46","name":"[RFC,v2,8/8] utils/tuning: Add LSC scripts","commit_ref":null,"pull_url":null,"state":"new","archived":false,"hash":"f61862f5a6c5374e93b385ea0b6c0b57fd5462d2","submitter":{"id":177,"url":"https://patchwork.libcamera.org/api/1.1/people/177/?format=json","name":"Milan Zamazal","email":"mzamazal@redhat.com"},"delegate":null,"mbox":"https://patchwork.libcamera.org/patch/26529/mbox/","series":[{"id":5883,"url":"https://patchwork.libcamera.org/api/1.1/series/5883/?format=json","web_url":"https://patchwork.libcamera.org/project/libcamera/list/?series=5883","date":"2026-04-20T18:39:38","name":"LSC for SoftISP simple pipeline","version":2,"mbox":"https://patchwork.libcamera.org/series/5883/mbox/"}],"comments":"https://patchwork.libcamera.org/api/patches/26529/comments/","check":"pending","checks":"https://patchwork.libcamera.org/api/patches/26529/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 A3421BDCBD\n\tfor <parsemail@patchwork.libcamera.org>;\n\tMon, 20 Apr 2026 18:40:23 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 55A0062EFD;\n\tMon, 20 Apr 2026 20:40:23 +0200 (CEST)","from us-smtp-delivery-124.mimecast.com\n\t(us-smtp-delivery-124.mimecast.com [170.10.133.124])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id B97D762EF6\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tMon, 20 Apr 2026 20:40:21 +0200 (CEST)","from mx-prod-mc-06.mail-002.prod.us-west-2.aws.redhat.com\n\t(ec2-35-165-154-97.us-west-2.compute.amazonaws.com [35.165.154.97])\n\tby relay.mimecast.com with ESMTP with STARTTLS (version=TLSv1.3,\n\tcipher=TLS_AES_256_GCM_SHA384) id us-mta-616-Haj7pC93PJq8vKSs1TiTmg-1;\n\tMon, 20 Apr 2026 14:40:18 -0400","from mx-prod-int-06.mail-002.prod.us-west-2.aws.redhat.com\n\t(mx-prod-int-06.mail-002.prod.us-west-2.aws.redhat.com\n\t[10.30.177.93])\n\t(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)\n\tkey-exchange X25519 server-signature RSA-PSS (2048 bits)\n\tserver-digest SHA256) (No client certificate requested)\n\tby mx-prod-mc-06.mail-002.prod.us-west-2.aws.redhat.com (Postfix)\n\twith ESMTPS id B7C22180036E; Mon, 20 Apr 2026 18:40:17 +0000 (UTC)","from mzamazal-thinkpadp1gen7.tpbc.com (unknown [10.44.48.16])\n\tby mx-prod-int-06.mail-002.prod.us-west-2.aws.redhat.com (Postfix)\n\twith ESMTP id A6221180047F; Mon, 20 Apr 2026 18:40:15 +0000 (UTC)"],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (1024-bit key;\n\tunprotected) header.d=redhat.com header.i=@redhat.com\n\theader.b=\"dTI8PA4Q\"; dkim-atps=neutral","DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com;\n\ts=mimecast20190719; t=1776710420;\n\th=from:from:reply-to:subject:subject:date:date:message-id:message-id:\n\tto:to:cc:cc:mime-version:mime-version:content-type:content-type:\n\tcontent-transfer-encoding:content-transfer-encoding:\n\tin-reply-to:in-reply-to:references:references;\n\tbh=9sWLN5zFsvsjo7uIP+VvJtokKl+xHqKEhtQ8BXwskRc=;\n\tb=dTI8PA4QpXfX2Q5eNDfhVefI1+VakYln35SKeeHYg3Gn3CazS8xYk6iuRIx0cerk3CsuPW\n\t8e3/WMo/HX6/YyW6XpIEmC4FK6Xmtr4x8zURfoVIEF9Lcwpsmbprde5leEbrCB/pRf8k1u\n\tUvgVCy/PVdCcxLlpPzUAE+LCz5ZPBTM=","X-MC-Unique":"Haj7pC93PJq8vKSs1TiTmg-1","X-Mimecast-MFC-AGG-ID":"Haj7pC93PJq8vKSs1TiTmg_1776710417","From":"Milan Zamazal <mzamazal@redhat.com>","To":"libcamera-devel@lists.libcamera.org","Cc":"Derek Gielen <derekgielen@outlook.com>,\n\tHans de Goede <johannes.goede@oss.qualcomm.com>,\n\tAron Dosti <aron-dosti@hotmail.com>, Milan Zamazal <mzamazal@redhat.com>","Subject":"[RFC PATCH v2 8/8] utils/tuning: Add LSC scripts","Date":"Mon, 20 Apr 2026 20:39:46 +0200","Message-ID":"<20260420183949.110548-9-mzamazal@redhat.com>","In-Reply-To":"<20260420183949.110548-1-mzamazal@redhat.com>","References":"<20260420183949.110548-1-mzamazal@redhat.com>","MIME-Version":"1.0","X-Scanned-By":"MIMEDefang 3.4.1 on 10.30.177.93","X-Mimecast-Spam-Score":"0","X-Mimecast-MFC-PROC-ID":"JF6LWjGlCD4v81BBJbogK3USnkbeD4Fjyox4OLikmJQ_1776710417","X-Mimecast-Originator":"redhat.com","Content-Transfer-Encoding":"8bit","content-type":"text/plain; charset=\"US-ASCII\"; x-default=true","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-developed-by: Aron Dosti <aron-dosti@hotmail.com>\nSigned-off-by: Aron Dosti <aron-dosti@hotmail.com>\nSigned-off-by: Derek Gielen <derekgielen@outlook.com>\nSigned-off-by: Milan Zamazal <mzamazal@redhat.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 000000000..60bc37ec2\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 000000000..7a0aaeb8a\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":["RFC","v2","8/8"]}