[PATCH 1/3] utils: tuning: Add tuning scripts for mali-c55

Daniel Scally dan.scally at ideasonboard.com
Wed Mar 5 00:12:52 CET 2025


Add tuning scripts for the mali-c55 IPA. At present the only module
that is available is for lens shading correction.

Signed-off-by: Daniel Scally <dan.scally at ideasonboard.com>
---
 .../tuning/libtuning/modules/lsc/__init__.py  |   1 +
 .../tuning/libtuning/modules/lsc/mali_c55.py  | 173 ++++++++++++++++++
 utils/tuning/mali-c55.py                      |  40 ++++
 3 files changed, 214 insertions(+)
 create mode 100644 utils/tuning/libtuning/modules/lsc/mali_c55.py
 create mode 100755 utils/tuning/mali-c55.py

diff --git a/utils/tuning/libtuning/modules/lsc/__init__.py b/utils/tuning/libtuning/modules/lsc/__init__.py
index 0ba4411b..edd5ce7f 100644
--- a/utils/tuning/libtuning/modules/lsc/__init__.py
+++ b/utils/tuning/libtuning/modules/lsc/__init__.py
@@ -3,5 +3,6 @@
 # Copyright (C) 2022, Paul Elder <paul.elder at ideasonboard.com>
 
 from libtuning.modules.lsc.lsc import LSC
+from libtuning.modules.lsc.mali_c55 import LSCMaliC55
 from libtuning.modules.lsc.raspberrypi import ALSCRaspberryPi
 from libtuning.modules.lsc.rkisp1 import LSCRkISP1
diff --git a/utils/tuning/libtuning/modules/lsc/mali_c55.py b/utils/tuning/libtuning/modules/lsc/mali_c55.py
new file mode 100644
index 00000000..7d85aee9
--- /dev/null
+++ b/utils/tuning/libtuning/modules/lsc/mali_c55.py
@@ -0,0 +1,173 @@
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (C) 2019, Raspberry Pi Ltd
+# Copyright (C) 2024, Ideas on Board oy
+#
+# mali-c55.py - LSC module for tuning mali-c55
+
+from .lsc import LSC
+
+import libtuning as lt
+
+import numpy as np
+
+class LSCMaliC55(LSC):
+	out_name = 'Lsc'
+	name = "LSCMaliC55"
+
+	def __init__(self, *args, **kwargs):
+		super().__init__(**kwargs)
+
+	def validate_config(self, config: dict) -> bool:
+		# There's nothing we need in config
+		return True
+
+	def _do_single_lsc(self, image: lt.Image):
+		"""
+		Generate lens shading tables of gain values for a single image
+
+		This function generates a set of lens shading tables - one for
+		each colour channel. The table values are gain values, expressed
+		generically (I.E. not in the IPA module's format)
+
+		Returns a tuple of colour temperature, array of red gain values,
+		array of blue gain values, array of red's green gain values and
+		array of blue's green gain values.
+		"""
+		cgr, _ = self._lsc_single_channel(image.channels[lt.Color.GR],
+						   image)
+		cgb, _ = self._lsc_single_channel(image.channels[lt.Color.GB],
+						   image)
+		cr, _ = self._lsc_single_channel(image.channels[lt.Color.R],
+						 image)
+		cb, _ = self._lsc_single_channel(image.channels[lt.Color.B],
+						 image)
+
+		return (
+			image.color,
+			cr.flatten(),
+			cb.flatten(),
+			cgr.flatten(),
+			cgb.flatten()
+		)
+
+	def _do_all_lsc(self, images: list) -> dict:
+		"""
+		Generate lens shading tables in the IPA's format
+
+		This function generates lens shading tables from the list of
+		images. A set (one per colour channel) of tables is generated
+		for each image, and then the sets from all images which were
+		generated from the same colour temperature are averaged together
+		and transformed into the format required by the IPA module.
+
+		Returns a list of dictionaries containing colour temperature,
+		red lens shading table, green lens shading table and blue lens
+		shading table.
+		"""
+
+		output_map_func = lt.gradient.Linear().map
+
+		list_ct = []
+
+		list_cr = []
+		list_cb = []
+		list_cgr = []
+		list_cgb = []
+
+		for image in images:
+			ct, cr, cg, cgr, cgb = self._do_single_lsc(image)
+
+			list_ct.append(ct)
+			list_cr.append(cr)
+			list_cb.append(cg)
+			list_cgr.append(cgr)
+			list_cgb.append(cgb)
+
+		list_ct = np.array(list_ct)
+		list_cr = np.array(list_cr)
+		list_cb = np.array(list_cb)
+		list_cgr = np.array(list_cgr)
+		list_cgb = np.array(list_cgb)
+		list_cg = (list_cgr + list_cgb) / 2
+
+		# We need to map the gains into the IPA-specific values to pass
+		# to the ISP. For the mali-c55 the values are always in the
+		# range [0..255] but the min/max that those values represent
+		# depend on the mesh scale parameter, so we'll need to choose
+		# what that should be and use the gain-range it represents as
+		# the domain for output_map_func().
+		#
+		# For convenient reference, the possible mesh scale values are
+		# as follows (taken from include/linux/mali-c55-config.h)
+		#
+		#	- 0 = 0-2x gain
+		#	- 1 = 0-4x gain
+		#	- 2 = 0-8x gain
+		#	- 3 = 0-16x gain
+		#	- 4 = 1-2x gain
+		#	- 5 = 1-3x gain
+		#	- 6 = 1-5x gain
+		#	- 7 = 1-9x gain
+		#
+		# We want to use the scale with the smallest range that still
+		# covers the minimum and maximum value we want to set...but this
+		# process at present hard-codes a minimum gain of 1.0, so the
+		# first 4 scales are out right away. We'll just consider the
+		# minimum as 1.0 for now and if we ever need more than 9.0 gain
+		# we'll have to fix this - shout about that if so.
+
+		max_gain = np.max([list_cr, list_cgr, list_cgb, list_cb])
+		if (max_gain > 9.0):
+			print("WARNING: Maximum gain restricted artificially to 9.0")
+
+		mesh_scales = {
+			4: (1.0, 2.0),
+			5: (1.0, 3.0),
+			6: (1.0, 5.0),
+			7: (1.0, 9.0)
+		}
+
+		for i in mesh_scales.keys():
+			if max_gain <= mesh_scales[i][1]:
+				break
+
+		mesh_scale = i
+
+		output_list = []
+		for ct in sorted(set(list_ct)):
+			indices = np.where(list_ct == ct)
+			ct = int(ct)
+
+			tables = []
+			for lists in [list_cr, list_cg, list_cb]:
+				table = np.mean(lists[indices], axis=0)
+
+				table = output_map_func(
+					(
+						mesh_scales[mesh_scale][0],
+						mesh_scales[mesh_scale][1] - 0.001
+					),
+					(0, 255),
+					table
+				)
+				table = np.round(table).astype('uint8').tolist()
+				tables.append(table)
+
+			entry = {
+				'ct': ct,
+				'r': tables[0],
+				'g': tables[1],
+				'b': tables[2],
+			}
+
+			output_list.append(entry)
+
+		return {
+			'meshScale': mesh_scale,
+			'sets': output_list
+		}
+
+	def process(self, config: dict, images: list, outputs: dict) -> dict:
+		return self._do_all_lsc(images)
+
diff --git a/utils/tuning/mali-c55.py b/utils/tuning/mali-c55.py
new file mode 100755
index 00000000..01535366
--- /dev/null
+++ b/utils/tuning/mali-c55.py
@@ -0,0 +1,40 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-or-later
+#
+# Copyright (C) 2024, Ideas on Board oy
+#
+# mali-c55.py - Tuning script for mali-c55
+
+import sys
+
+import libtuning as lt
+from libtuning.parsers import YamlParser
+from libtuning.generators import YamlOutput
+from libtuning.modules.lsc import LSCMaliC55
+
+tuner = lt.Tuner('MaliC55')
+tuner.add(LSCMaliC55(
+          debug=[lt.Debug.Plot],
+          # This is for the actual LSC tuning, and is part of the base LSC
+          # module. rkisp1's table sector sizes (16x16 programmed as mirrored
+          # 8x8) are separate, and is hardcoded in its specific LSC tuning
+          # module.
+          sector_shape=(32, 32),
+
+          sector_x_gradient=lt.gradient.Linear(lt.Remainder.DistributeFront),
+          sector_y_gradient=lt.gradient.Linear(lt.Remainder.DistributeFront),
+
+          # This is the function that will be used to average the pixels in
+          # each sector. This can also be a custom function.
+          sector_average_function=lt.average.Mean(),
+
+          # This is the function that will be used to smooth the color ratio
+          # values.  This can also be a custom function.
+          smoothing_function=lt.smoothing.MedianBlur(3),
+          ))
+tuner.set_input_parser(YamlParser())
+tuner.set_output_formatter(YamlOutput())
+tuner.set_output_order([LSCMaliC55])
+
+if __name__ == '__main__':
+    sys.exit(tuner.run(sys.argv))
-- 
2.34.1



More information about the libcamera-devel mailing list