From patchwork Mon Apr 20 18:39:46 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Milan Zamazal X-Patchwork-Id: 26529 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id A3421BDCBD for ; Mon, 20 Apr 2026 18:40:23 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 55A0062EFD; Mon, 20 Apr 2026 20:40:23 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=redhat.com header.i=@redhat.com header.b="dTI8PA4Q"; dkim-atps=neutral Received: from us-smtp-delivery-124.mimecast.com (us-smtp-delivery-124.mimecast.com [170.10.133.124]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id B97D762EF6 for ; Mon, 20 Apr 2026 20:40:21 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com; s=mimecast20190719; t=1776710420; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=9sWLN5zFsvsjo7uIP+VvJtokKl+xHqKEhtQ8BXwskRc=; b=dTI8PA4QpXfX2Q5eNDfhVefI1+VakYln35SKeeHYg3Gn3CazS8xYk6iuRIx0cerk3CsuPW 8e3/WMo/HX6/YyW6XpIEmC4FK6Xmtr4x8zURfoVIEF9Lcwpsmbprde5leEbrCB/pRf8k1u UvgVCy/PVdCcxLlpPzUAE+LCz5ZPBTM= Received: from mx-prod-mc-06.mail-002.prod.us-west-2.aws.redhat.com (ec2-35-165-154-97.us-west-2.compute.amazonaws.com [35.165.154.97]) by relay.mimecast.com with ESMTP with STARTTLS (version=TLSv1.3, cipher=TLS_AES_256_GCM_SHA384) id us-mta-616-Haj7pC93PJq8vKSs1TiTmg-1; Mon, 20 Apr 2026 14:40:18 -0400 X-MC-Unique: Haj7pC93PJq8vKSs1TiTmg-1 X-Mimecast-MFC-AGG-ID: Haj7pC93PJq8vKSs1TiTmg_1776710417 Received: from mx-prod-int-06.mail-002.prod.us-west-2.aws.redhat.com (mx-prod-int-06.mail-002.prod.us-west-2.aws.redhat.com [10.30.177.93]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by mx-prod-mc-06.mail-002.prod.us-west-2.aws.redhat.com (Postfix) with ESMTPS id B7C22180036E; Mon, 20 Apr 2026 18:40:17 +0000 (UTC) Received: from mzamazal-thinkpadp1gen7.tpbc.com (unknown [10.44.48.16]) by mx-prod-int-06.mail-002.prod.us-west-2.aws.redhat.com (Postfix) with ESMTP id A6221180047F; Mon, 20 Apr 2026 18:40:15 +0000 (UTC) From: Milan Zamazal To: libcamera-devel@lists.libcamera.org Cc: Derek Gielen , Hans de Goede , Aron Dosti , Milan Zamazal 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-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: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" From: Derek Gielen Add script for the conversion of the tuning file to a format usable by the lens shading correction shader. Add script for visualizing the lens shading correction using pyplot. Co-developed-by: Aron Dosti Signed-off-by: Aron Dosti Signed-off-by: Derek Gielen Signed-off-by: Milan Zamazal --- utils/tuning/exportTuningToLscShader.py | 120 ++++++++++++++++++++++++ utils/tuning/generate_lsc_map_plot.py | 76 +++++++++++++++ 2 files changed, 196 insertions(+) create mode 100644 utils/tuning/exportTuningToLscShader.py create mode 100644 utils/tuning/generate_lsc_map_plot.py diff --git a/utils/tuning/exportTuningToLscShader.py b/utils/tuning/exportTuningToLscShader.py new file mode 100644 index 000000000..60bc37ec2 --- /dev/null +++ b/utils/tuning/exportTuningToLscShader.py @@ -0,0 +1,120 @@ +import yaml +import numpy as np +import argparse +import sys + +# --- Configuration --- +GRID_W = 16 +GRID_H = 16 + +# Formula constants +BLACK_LEVEL = 1024.0 +SCALE_FACTOR = 3071 # Divisor to map the range to 0-255 + +def load_and_process_yaml(input_filename, target_ct): + # 1. Load the YAML file + try: + with open(input_filename, 'r') as f: + data = yaml.safe_load(f) + except FileNotFoundError: + print(f"Error: Input file '{input_filename}' not found.") + return None, None, None + + # 2. Find the LensShadingCorrection block + lsc_data = None + for algo in data['algorithms']: + if 'LensShadingCorrection' in algo: + lsc_data = algo['LensShadingCorrection'] + break + + if not lsc_data: + print("Error: LensShadingCorrection block not found.") + return None, None, None + + # 3. Extract the set for the specific Color Temperature (CT) provided in arguments + sets = lsc_data['sets'] + target_set = next((item for item in sets if item['ct'] == target_ct), None) + + if not target_set: + print(f"Error: Set for Color Temperature {target_ct} not found in '{input_filename}'.") + return None, None, None + + print(f"Found data for CT {target_ct}. Applying formula: (x - {int(BLACK_LEVEL)}) / {SCALE_FACTOR} ...") + + # 4. Get Raw Data + r_raw = np.array(target_set['r']) + b_raw = np.array(target_set['b']) + gr_raw = np.array(target_set['gr']) + gb_raw = np.array(target_set['gb']) + + # Calculate Green Channel (Average of GR and GB) + g_raw = (gr_raw + gb_raw) / 2.0 + + # 5. Define the calculation logic + def apply_formula(data_array): + """ + Applies the specific user formula: + 1. Subtract Black Level (1024) + 2. Divide by the Scale Factor (3071) + 3. Multiply by 255 and convert to integer + """ + result = ((data_array - BLACK_LEVEL) / SCALE_FACTOR) * 255 + return result.astype(int) + + # 6. Apply calculation to all channels + r_final = apply_formula(r_raw) + g_final = apply_formula(g_raw) + b_final = apply_formula(b_raw) + + return r_final, g_final, b_final + +def save_custom_grid_yaml(output_filename, r, g, b, target_ct): + + # Helper function to format the array as a visual grid string + def format_array_as_grid_string(arr): + lines = [] + # Loop through the array in chunks of 16 (GRID_W) + for i in range(0, len(arr), GRID_W): + row = arr[i:i + GRID_W] + # Join numbers with commas + row_str = ", ".join(map(str, row)) + lines.append(f" {row_str}") + # Wrap in brackets to form a valid YAML list, but visually formatted + return "[\n" + ",\n".join(lines) + "\n ]" + + # Write the file manually to ensure specific formatting + with open(output_filename, 'w') as f: + f.write(f"description: 'LSC Fixed Formula ((x-{int(BLACK_LEVEL)})/{SCALE_FACTOR})'\n") + f.write(f"source_ct: {target_ct}\n") + f.write(f"grid_size: [{GRID_W}, {GRID_H}]\n") + f.write(f"formula_used: '(RawValue - {int(BLACK_LEVEL)}) / {SCALE_FACTOR} -> [0..255]'\n") + f.write(f"channels:\n") + + f.write(" red: " + format_array_as_grid_string(r) + "\n") + f.write(" green: " + format_array_as_grid_string(g) + "\n") + f.write(" blue: " + format_array_as_grid_string(b) + "\n") + + print(f"Success! Saved formatted grid to '{output_filename}'") + +# --- Main Execution --- +if __name__ == "__main__": + # 1. Setup Argument Parser + parser = argparse.ArgumentParser(description="Convert LSC YAML data to shader grid format.") + parser.add_argument("ct", type=int, help="The Color Temperature to process (e.g. 2700, 5000, 6500)") + + # 2. Parse arguments + args = parser.parse_args() + ct_val = args.ct + + # 3. Construct filenames based on the CT value + # Assumes input file is named 'tuning_XXXX.yaml' + input_file = f'tuning{ct_val}.yaml' + output_file = f'lsc_shader_16x16_{ct_val}_fixed.yaml' + + print(f"--- Processing for Color Temp: {ct_val} ---") + + # 4. Run Process + r, g, b = load_and_process_yaml(input_file, ct_val) + + if r is not None: + save_custom_grid_yaml(output_file, r, g, b, ct_val) diff --git a/utils/tuning/generate_lsc_map_plot.py b/utils/tuning/generate_lsc_map_plot.py new file mode 100644 index 000000000..7a0aaeb8a --- /dev/null +++ b/utils/tuning/generate_lsc_map_plot.py @@ -0,0 +1,76 @@ +import yaml +import numpy as np +import matplotlib.pyplot as plt + +# 1. Load the data +try: + with open('tuning2700.yaml', 'r') as f: + data = yaml.safe_load(f) +except FileNotFoundError: + print("Error: 'tuning.yaml' not found. Please ensure the file exists.") + exit() + +# 2. Find LensShadingCorrection safely +lsc_data = None +for algo in data['algorithms']: + if 'LensShadingCorrection' in algo: + lsc_data = algo['LensShadingCorrection'] + break + +if not lsc_data: + print("Error: LensShadingCorrection block not found in YAML.") + exit() + +# 3. Extract the set for disirable Kelvin +kelvin = 2700 +sets = lsc_data['sets'] +target_set = next((item for item in sets if item['ct'] == kelvin), None) + +if not target_set: + print("Error: CT 6500 not found in sets.") + exit() + +# 4. Get lists and normalize (1024 = 1.0 gain) +r_list = np.array(target_set['r']) +gr_list = np.array(target_set['gr']) +gb_list = np.array(target_set['gb']) +b_list = np.array(target_set['b']) + +r_norm = r_list / 1024.0 +b_norm = b_list / 1024.0 +# Average the two greens for the shader +g_norm = (gr_list + gb_list) / 2.0 / 1024.0 + +# 5. Reshape into 17x17 Grids +grid_size = (17, 17) +r_grid = r_norm.reshape(grid_size) +g_grid = g_norm.reshape(grid_size) +b_grid = b_norm.reshape(grid_size) + +# 6. Visualization +# We create 3 separate plots to see the data distribution correctly +fig, axs = plt.subplots(1, 3, figsize=(15, 5)) + +# Plot Red +im1 = axs[0].imshow(r_grid, cmap='viridis') +axs[0].set_title('Red Gain Map') +fig.colorbar(im1, ax=axs[0]) + +# Plot Green +im2 = axs[1].imshow(g_grid, cmap='viridis') +axs[1].set_title('Green Gain Map') +fig.colorbar(im2, ax=axs[1]) + +# Plot Blue +im3 = axs[2].imshow(b_grid, cmap='viridis') +axs[2].set_title('Blue Gain Map') +fig.colorbar(im3, ax=axs[2]) + +plt.suptitle(f"LSC Gain Maps (Center ~1.0, Corners > 1.0) for collor temprature {kelvin}") +plt.show() + +# 7. Prepare Texture for Export (Optional) +# Stack them for your shader: (17, 17, 3) +lsc_texture = np.dstack((r_grid, g_grid, b_grid)) +print(f"Final Texture Shape: {lsc_texture.shape}") +print(f"Sample Blue Value at Corner: {b_grid[0, 0]}")