[RFC,v2,8/8] utils/tuning: Add LSC scripts
diff mbox series

Message ID 20260420183949.110548-9-mzamazal@redhat.com
State New
Headers show
Series
  • LSC for SoftISP simple pipeline
Related show

Commit Message

Milan Zamazal April 20, 2026, 6:39 p.m. UTC
From: Derek Gielen <derekgielen@outlook.com>

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 <aron-dosti@hotmail.com>
Signed-off-by: Aron Dosti <aron-dosti@hotmail.com>
Signed-off-by: Derek Gielen <derekgielen@outlook.com>
Signed-off-by: Milan Zamazal <mzamazal@redhat.com>
---
 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

Comments

Milan Zamazal April 20, 2026, 6:56 p.m. UTC | #1
Milan Zamazal <mzamazal@redhat.com> writes:

> From: Derek Gielen <derekgielen@outlook.com>
>
> 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 <aron-dosti@hotmail.com>
> Signed-off-by: Aron Dosti <aron-dosti@hotmail.com>
> Signed-off-by: Derek Gielen <derekgielen@outlook.com>
> Signed-off-by: Milan Zamazal <mzamazal@redhat.com>
> ---
>  utils/tuning/exportTuningToLscShader.py | 120 ++++++++++++++++++++++++
>  utils/tuning/generate_lsc_map_plot.py   |  76 +++++++++++++++

These files are missing licence information.  They are separate works so
it's not clear what licence applies.  Could you please clarify?

(If you have no idea and the files are not derivative works of something
else, I can see the other Python scripts in utils/tuning/ use
GPL-2.0-or-later, so it might be a good choice.)

>  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]}")

Patch
diff mbox series

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]}")