[{"id":38633,"web_url":"https://patchwork.libcamera.org/comment/38633/","msgid":"<85eck9ihox.fsf@mzamazal-thinkpadp1gen7.tpbc.csb>","date":"2026-04-20T18:56:46","subject":"Re: [RFC PATCH v2 8/8] utils/tuning: Add LSC scripts","submitter":{"id":177,"url":"https://patchwork.libcamera.org/api/people/177/","name":"Milan Zamazal","email":"mzamazal@redhat.com"},"content":"Milan Zamazal <mzamazal@redhat.com> writes:\n\n> From: Derek Gielen <derekgielen@outlook.com>\n>\n> Add script for the conversion of the tuning file to a format usable by\n> the lens shading correction shader.\n>\n> Add script for visualizing the lens shading correction using pyplot.\n>\n> Co-developed-by: Aron Dosti <aron-dosti@hotmail.com>\n> Signed-off-by: Aron Dosti <aron-dosti@hotmail.com>\n> Signed-off-by: Derek Gielen <derekgielen@outlook.com>\n> Signed-off-by: Milan Zamazal <mzamazal@redhat.com>\n> ---\n>  utils/tuning/exportTuningToLscShader.py | 120 ++++++++++++++++++++++++\n>  utils/tuning/generate_lsc_map_plot.py   |  76 +++++++++++++++\n\nThese files are missing licence information.  They are separate works so\nit's not clear what licence applies.  Could you please clarify?\n\n(If you have no idea and the files are not derivative works of something\nelse, I can see the other Python scripts in utils/tuning/ use\nGPL-2.0-or-later, so it might be a good choice.)\n\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\n>\n> diff --git a/utils/tuning/exportTuningToLscShader.py b/utils/tuning/exportTuningToLscShader.py\n> new file mode 100644\n> index 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)\n> diff --git a/utils/tuning/generate_lsc_map_plot.py b/utils/tuning/generate_lsc_map_plot.py\n> new file mode 100644\n> index 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]}\")","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 59333BDCBD\n\tfor <parsemail@patchwork.libcamera.org>;\n\tMon, 20 Apr 2026 18:57:02 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 6E98762EDE;\n\tMon, 20 Apr 2026 20:57:01 +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 D9F6B62EB2\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tMon, 20 Apr 2026 20:56:59 +0200 (CEST)","from mail-pl1-f199.google.com (mail-pl1-f199.google.com\n\t[209.85.214.199]) by relay.mimecast.com with ESMTP with STARTTLS\n\t(version=TLSv1.3, cipher=TLS_AES_256_GCM_SHA384) id\n\tus-mta-50-3UzxrlfkO5SqcRsJNe4bEA-1; Mon, 20 Apr 2026 14:56:56 -0400","by mail-pl1-f199.google.com with SMTP id\n\td9443c01a7336-2b24a00d12cso34796555ad.1\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tMon, 20 Apr 2026 11:56:56 -0700 (PDT)","from mzamazal-thinkpadp1gen7.tpbc.csb\n\t(ip-77-48-47-2.net.vodafone.cz. [77.48.47.2])\n\tby smtp.gmail.com with ESMTPSA id\n\td9443c01a7336-2b5faa30047sm110457065ad.28.2026.04.20.11.56.50\n\t(version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);\n\tMon, 20 Apr 2026 11:56:54 -0700 (PDT)"],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (1024-bit key;\n\tunprotected) header.d=redhat.com header.i=@redhat.com\n\theader.b=\"A7eE4HOG\"; dkim-atps=neutral","DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com;\n\ts=mimecast20190719; t=1776711418;\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\tin-reply-to:in-reply-to:references:references;\n\tbh=gOiQdmtVSd4bbhhlWidJNJg6ZowZTlxGV84T1JDdSMM=;\n\tb=A7eE4HOGRyNsG59NWQQFiS8Y9GkP2/VsxcyOpZS1TLwDso4p2VNmKY2bAVejKw3EB36Gyi\n\tq0wkFFnJUeaNTBqtv4eyAIjAjoc8L1H8LLogm2Var1E5Z8zGsJywlv6OaNSXKP267wgbr4\n\toJA/CRt0dtVo0+B1e8g1q3CAtpZpqbo=","X-MC-Unique":"3UzxrlfkO5SqcRsJNe4bEA-1","X-Mimecast-MFC-AGG-ID":"3UzxrlfkO5SqcRsJNe4bEA_1776711415","X-Google-DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=1e100.net; s=20251104; t=1776711415; x=1777316215;\n\th=mime-version:user-agent:message-id:date:references:in-reply-to\n\t:subject:cc:to:from:x-gm-gg:x-gm-message-state:from:to:cc:subject\n\t:date:message-id:reply-to;\n\tbh=gOiQdmtVSd4bbhhlWidJNJg6ZowZTlxGV84T1JDdSMM=;\n\tb=BVVuYkn2cEUovm6NtOKHVzyT6DIFQXEn0MhT74JL6sA77PDoMVj5QT/6MT9u3rmNtu\n\twruYzESITIOR250b611r1pxVRfsNEWbV8C8KSg4UNZCXCL3qPv92TC9GQtlU/fscvbFI\n\t9IjbFaM75XBWcjXs2Kf/MlNyHD4mhpqLrfa3B+0jfZFxbJ6uPLKF8Zl7gkFGJKW2wN4w\n\tVivVuhEztLcpqDGV3w2QCuREpu9G+mJ4gZcU3Jzx2SDO1JG6jE9MkEFaQ1cTUmI4QZRH\n\tm3o0TTsSsWM2qooDoK4XMX7Wm3oKZ+YBQ3NT3NzbqWZu0fRYdiHMnefkm1xxn8HlYHVA\n\tdvzw==","X-Gm-Message-State":"AOJu0YyKL8cL47jzX3K+7CJjkPuyAtHsSmybGDyqEcf9CGlegAYn0pds\n\tuuTOc09KKcr2E6tzNQN8gsqbjPgBobeu1usOqgiB/VrnBlLtJUn9Ccx1S/pvPAfpx8CQ1V89COf\n\tUmVu//25o14MNIxyE6TWPd00KPgdjjWyU4G3LeZQ0h3MTBbRW+kqumW4q1lqOMXbGLlvdmI1neY\n\tk=","X-Gm-Gg":"AeBDievXTZhSRSu7FDvD9DsOm3KJfyQckdVlbkUupMXsk3cEiU0wD0uCrEpoQYJBR1e\n\tX66tgISTF+fV6mdfdslgL/DnJZ3nmNl1pldxASvsKhRyr3VIG1XwyjPykGvwXzJFrqKzyJkRDMH\n\t3/d8L1DGNOdayDYEyY1AxSqURSC7w3pM35bwDhKiyzRzUtoaieBMiUO0X+N2AOBq3mDohATzQzG\n\tHExVhKFZSzM0RmdRWEW82eQPfLJyfrg1ZBykvBrmC/e+wQ1oMRWMGu86zn1RXaoDH2n9WlNQJus\n\tcgi0IxTgBEAOPeuiadfwRHA3M9YzZCb80TM8ywMr0yw6XTWM11R1Z95N4JKjNV946QJ0851S+jR\n\t1yOtBlp7+ia2GdIIfN89+b462EqansY8kDvHcKIDy6D3E1l0qdryzn2zoeAbDKz15BHxDTWsOl1\n\tpxgXlcWQ7p4g==","X-Received":["by 2002:a17:903:907:b0:2b0:606b:6fd3 with SMTP id\n\td9443c01a7336-2b5f9e77061mr144171095ad.5.1776711415181; \n\tMon, 20 Apr 2026 11:56:55 -0700 (PDT)","by 2002:a17:903:907:b0:2b0:606b:6fd3 with SMTP id\n\td9443c01a7336-2b5f9e77061mr144170825ad.5.1776711414580; \n\tMon, 20 Apr 2026 11:56:54 -0700 (PDT)"],"From":"Milan Zamazal <mzamazal@redhat.com>","To":"libcamera-devel@lists.libcamera.org","Cc":"Derek Gielen <derekgielen@outlook.com>,  Hans de Goede\n\t<johannes.goede@oss.qualcomm.com>, Aron Dosti <aron-dosti@hotmail.com>","Subject":"Re: [RFC PATCH v2 8/8] utils/tuning: Add LSC scripts","In-Reply-To":"<20260420183949.110548-9-mzamazal@redhat.com> (Milan Zamazal's\n\tmessage of \"Mon, 20 Apr 2026 20:39:46 +0200\")","References":"<20260420183949.110548-1-mzamazal@redhat.com>\n\t<20260420183949.110548-9-mzamazal@redhat.com>","Date":"Mon, 20 Apr 2026 20:56:46 +0200","Message-ID":"<85eck9ihox.fsf@mzamazal-thinkpadp1gen7.tpbc.csb>","User-Agent":"Gnus/5.13 (Gnus v5.13)","MIME-Version":"1.0","X-Mimecast-Spam-Score":"0","X-Mimecast-MFC-PROC-ID":"CFIW9XEd6ibbmQIp5BXFHljtLjjdlhUA-SKnjcOJDg8_1776711415","X-Mimecast-Originator":"redhat.com","Content-Type":"text/plain","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>"}}]