From patchwork Mon Jan 26 10:42:54 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Rick ten Wolde X-Patchwork-Id: 25964 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 792CBC32E7 for ; Mon, 26 Jan 2026 10:48:18 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id E5B4C61FD1; Mon, 26 Jan 2026 11:48:16 +0100 (CET) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (2048-bit key; unprotected) header.d=gmail.com header.i=@gmail.com header.b="BiVQyINT"; dkim-atps=neutral Received: from mail-ej1-x62b.google.com (mail-ej1-x62b.google.com [IPv6:2a00:1450:4864:20::62b]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 3399C61FD3 for ; Mon, 26 Jan 2026 11:43:08 +0100 (CET) Received: by mail-ej1-x62b.google.com with SMTP id a640c23a62f3a-b8850aa5b56so622017866b.2 for ; Mon, 26 Jan 2026 02:43:08 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20230601; t=1769424187; x=1770028987; darn=lists.libcamera.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to; bh=UGMhLAQJP7biuVIZMSesOWnaMhbq21WkJYjcWc6nhwQ=; b=BiVQyINTRtuMoSFKmFBtaVpfqeZqz5rQ2dsi5KveDjpb6HbnvFC6nY4p9Gp7Ky1FNn aNoxO989p3Uj8w5HI8hZFe5/iymYryWIJkwnxpxHhieoHI3pY74Q3oRHZqyV8igzWEWh /RJPfhpxy9RnOBcrFAdLDSJVYQcuLWxZ8JxcznD9y5BRB+cys5R108IyOiIL989l7u8A FnnW8eGa5M4BOrcZo7NX3+tuTFZzZB9QIXZrzK0aENkx5GrAFKzfVEmO3RlRgS73slHE 70/Osve22Q0mM20lWvMQpuaBLIWC0lK/p1aT9olgut+Oh18G2cyzB4aI6oHtBayUehK/ 5N/Q== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1769424187; x=1770028987; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to; bh=UGMhLAQJP7biuVIZMSesOWnaMhbq21WkJYjcWc6nhwQ=; b=GRjrO6DEpuWyK6wPf6Vy3Z1uiqL3tQwHmT5Id1MKtgZjmy2sDCTcLNDYTfQQvKCoPM Se5WWomiDym4F6eftqqMCpFU2hUdsNkVSgLbmwAu+4no7Nsm9pd8WfKBKnl7J1olAa8V +7ylBK32OZ3ECWmCAAubLjmkwhJI92pGxYc+LfzFNLSy6my6kAyiujL7vzPvVrDuhQAA dsiVUBHyeEhJ/5kcooY3jH057pIR1y59RpJn+KcZTG0GAlpoKIvUEjpzlBy7YjN+QnBH 4EXVMdJgOgnLExZF6KouqNb3PcVW+3VwKQXt2AWFLAJv39aUAdJ1bGfjRnbDZs+FcDMf o9RQ== X-Gm-Message-State: AOJu0YxRH0bogJ1fMlXyM3OrS/UDU7cZnp7MK+x73eLUl22zaGHp0qFW /Hp5pHCxCZfRWuL6Si5mIY+uEvqkdBx7SsgyYk0UJfHAa5wnEuvF4P55+cNrPw== X-Gm-Gg: AZuq6aKkFGrITgDbsYMZL0+WXnfr8juzpyaHNIrYRiN9M4XW4zmPSOss7JfqcjKhT8r oSbfsHQAAyozfIRm7iSi3h9a2QYpJRiX9Dg67iw9scMcCSN00GPDbSALWU5y1jR0pgxp4GZrPhL X99er6sn02sPsvkr69ZvsSUglmmUek6dv0Pdd8jedmRwN7Y/UWDDlejtNIHiGaY6gdYreGIxR8p TAgpgpnOPrY2MvWnyl2QTXnsTM+V/lGqCmV3rAPJpFgbp0vey57G+sTmsJPYv7kMXyhsBcELj+A NQOUO2Glx8MCHqBGq6shoyn8PQiIumIxCMPst7L6HiRpLbY3HmLTJp+bFKYvRI0Fc7EFABmQxtc MtJx7xXIrtkUoDs+Q6aQlcg7sH5YBM+iLC539Ot/Fnio5A2F8VLCf6didyvdRg7u/GtK+NF/8j0 hFUtp87gTOXFcbmmqwprn/QuKIOb/7B2/zt1KPl/0sEiaY9VAFpo0Je9tkgjuh7m4= X-Received: by 2002:a17:907:7b85:b0:b88:2848:dec8 with SMTP id a640c23a62f3a-b8d2e8330ddmr324052666b.54.1769424187157; Mon, 26 Jan 2026 02:43:07 -0800 (PST) Received: from castortop.wolde.loc (195-240-110-192.fixed.kpn.net. [195.240.110.192]) by smtp.gmail.com with ESMTPSA id a640c23a62f3a-b885b7661f7sm599220366b.54.2026.01.26.02.43.06 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Mon, 26 Jan 2026 02:43:06 -0800 (PST) From: Rick ten Wolde To: libcamera-devel@lists.libcamera.org Cc: xander.c.pronk@gmail.com, derekgielen@outlook.com, 22012540@student.hhs.nl, rick.w.ten.wolde@gmail.com, johannes.goede@oss.qualcomm.com, Aron Dosti 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 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: 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-authored-by: Aron Dosti Signed-off-by: Aron Dosti Signed-off-by: Derek Gielen --- 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 00000000..4f07aa1e --- /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 00000000..67b3a041 --- /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]}")