[RFC,1/1] utils: tuning: Add softisp script
diff mbox series

Message ID 20261026105811.15657-2-mzamazal@redhat.com
State New
Headers show
Series
  • softisp tuning script for LSC
Related show

Commit Message

Milan Zamazal Oct. 26, 2026, 10:58 a.m. UTC
This is an initial version of the script, generating just the tuning
data for Lsc algorithm.

Signed-off-by: Milan Zamazal <mzamazal@redhat.com>
---
 .../tuning/libtuning/modules/lsc/__init__.py  |   3 +-
 utils/tuning/libtuning/modules/lsc/softisp.py | 105 ++++++++++++++++++
 utils/tuning/softisp.py                       |  42 +++++++
 3 files changed, 149 insertions(+), 1 deletion(-)
 create mode 100644 utils/tuning/libtuning/modules/lsc/softisp.py
 create mode 100755 utils/tuning/softisp.py

Comments

Milan Zamazal Nov. 12, 2026, 7:43 p.m. UTC | #1
Milan Zamazal <mzamazal@redhat.com> writes:

> This is an initial version of the script, generating just the tuning
> data for Lsc algorithm.
>
> Signed-off-by: Milan Zamazal <mzamazal@redhat.com>
> ---
>  .../tuning/libtuning/modules/lsc/__init__.py  |   3 +-
>  utils/tuning/libtuning/modules/lsc/softisp.py | 105 ++++++++++++++++++
>  utils/tuning/softisp.py                       |  42 +++++++
>  3 files changed, 149 insertions(+), 1 deletion(-)
>  create mode 100644 utils/tuning/libtuning/modules/lsc/softisp.py
>  create mode 100755 utils/tuning/softisp.py
>
> diff --git a/utils/tuning/libtuning/modules/lsc/__init__.py b/utils/tuning/libtuning/modules/lsc/__init__.py
> index 0ba4411b4..0b7d67f0d 100644
> --- a/utils/tuning/libtuning/modules/lsc/__init__.py
> +++ b/utils/tuning/libtuning/modules/lsc/__init__.py
> @@ -1,7 +1,8 @@
>  # SPDX-License-Identifier: GPL-2.0-or-later
>  #
> -# Copyright (C) 2022, Paul Elder <paul.elder@ideasonboard.com>
> +# Copyright (C) 2022, 2026, Paul Elder <paul.elder@ideasonboard.com>
>  
>  from libtuning.modules.lsc.lsc import LSC
>  from libtuning.modules.lsc.raspberrypi import ALSCRaspberryPi
>  from libtuning.modules.lsc.rkisp1 import LSCRkISP1
> +from libtuning.modules.lsc.softisp import LSCSoftisp
> diff --git a/utils/tuning/libtuning/modules/lsc/softisp.py b/utils/tuning/libtuning/modules/lsc/softisp.py
> new file mode 100644
> index 000000000..42437083c
> --- /dev/null
> +++ b/utils/tuning/libtuning/modules/lsc/softisp.py
> @@ -0,0 +1,105 @@
> +# SPDX-License-Identifier: BSD-2-Clause
> +#
> +# Copyright (C) 2019, Raspberry Pi Ltd
> +# Copyright (C) 2022, Paul Elder <paul.elder@ideasonboard.com>
> +#
> +# LSC module for tuning softisp
> +
> +from .lsc import LSC
> +
> +import libtuning as lt
> +import libtuning.utils as utils
> +
> +from numbers import Number
> +import numpy as np
> +
> +
> +class LSCSoftisp(LSC):
> +    hr_name = 'LSC (Softisp)'
> +    out_name = 'Lsc'
> +    # \todo Not sure if this is useful. Probably will remove later.
> +    compatible = ['softisp']
> +
> +    def __init__(self, *args, **kwargs):
> +        super().__init__(**kwargs)
> +
> +    # We don't actually need anything from the config file
> +    def validate_config(self, config: dict) -> bool:
> +        return True
> +
> +    # @return Image color temperature, flattened array of red calibration table
> +    #         (containing {sector size} elements), flattened array of red
> +    #         calibration table, flattened array of green calibration
> +    #         table, flattened array of blue calibration table
> +
> +    def _do_single_lsc(self, image: lt.Image):
> +        # Perform LSC on each colour channel independently. A future enhancement
> +        # worth investigating would be splitting the luminance and chrominance
> +        # LSC as done by Raspberry Pi.
> +        cr, _ = self._lsc_single_channel(image.channels[lt.Color.R], image)
> +        cgr, _ = self._lsc_single_channel(image.channels[lt.Color.GR], image)
> +        cgb, _ = self._lsc_single_channel(image.channels[lt.Color.GR], image)
> +        cb, _ = self._lsc_single_channel(image.channels[lt.Color.B], image)
> +        cg = (cgr + cgb) / 2
> +
> +        return image.color, cr.flatten(), cg.flatten(), cb.flatten()
> +
> +    # @return List of dictionaries of color temperature, red table, green
> +    #         table and blue table
> +
> +    def _do_all_lsc(self, images: list) -> list:
> +        output_list = []
> +        output_map_func = lt.gradient.Linear().map
> +
> +        # List of colour temperatures
> +        list_col = []
> +        # Associated calibration tables
> +        list_cr = []
> +        list_cg = []
> +        list_cb = []
> +        for image in self._enumerate_lsc_images(images):
> +            col, cr, cg, cb = self._do_single_lsc(image)
> +            list_col.append(col)
> +            list_cr.append(cr)
> +            list_cg.append(cg)
> +            list_cb.append(cb)
> +
> +        # Convert to numpy array for data manipulation
> +        list_col = np.array(list_col)
> +        list_cr = np.array(list_cr)
> +        list_cg = np.array(list_cg)
> +        list_cb = np.array(list_cb)
> +
> +        for color_temperature in sorted(set(list_col)):
> +            # Average tables for the same colour temperature
> +            indices = np.where(list_col == color_temperature)
> +            color_temperature = int(color_temperature)
> +
> +            tables = []
> +            for lis in [list_cr, list_cg, list_cb]:
> +                table = np.mean(lis[indices], axis=0)
> +                table = output_map_func((1, 3), (1, 255), table)
> +                table = np.clip(table, 1, 255)

These ranges are wrong.  I think the two lines above should be:

  table = output_map_func((1, 4), (64, 256), table)
  table = np.clip(table, 64, 255)

Robert, could you please check whether you get meaningful results with
this change?

> +                table = np.round(table).astype('int32').tolist()
> +                tables.append(table)
> +
> +            entry = {
> +                'ct': color_temperature,
> +                'r': tables[0],
> +                'g': tables[1],
> +                'b': tables[2],
> +            }
> +
> +            output_list.append(entry)
> +
> +        return output_list
> +
> +    def process(self, config: dict, images: list, outputs: dict) -> dict:
> +        output = {}
> +
> +        output['sets'] = self._do_all_lsc(images)
> +
> +        if len(output['sets']) == 0:
> +            return None
> +
> +        return output
> diff --git a/utils/tuning/softisp.py b/utils/tuning/softisp.py
> new file mode 100755
> index 000000000..cf8b21555
> --- /dev/null
> +++ b/utils/tuning/softisp.py
> @@ -0,0 +1,42 @@
> +#!/usr/bin/env python3
> +# SPDX-License-Identifier: GPL-2.0-or-later
> +#
> +# Copyright (C) 2022, Paul Elder <paul.elder@ideasonboard.com>
> +# Copyright (C) 2024, Ideas On Board
> +#
> +# Tuning script for software ISP
> +
> +import logging
> +import sys
> +
> +import coloredlogs
> +import libtuning as lt
> +from libtuning.generators import YamlOutput
> +from libtuning.modules.lsc import LSCSoftisp
> +from libtuning.parsers import YamlParser
> +
> +coloredlogs.install(level=logging.INFO, fmt='%(name)s %(levelname)s %(message)s')
> +
> +lsc = LSCSoftisp(debug=[lt.Debug.Plot],
> +                 sector_shape=(17, 17),
> +
> +                 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 = lt.Tuner('Softisp')
> +tuner.add([lsc])
> +tuner.set_input_parser(YamlParser())
> +tuner.set_output_formatter(YamlOutput())
> +
> +tuner.set_output_order([lsc])
> +
> +if __name__ == '__main__':
> +    sys.exit(tuner.run(sys.argv))

Patch
diff mbox series

diff --git a/utils/tuning/libtuning/modules/lsc/__init__.py b/utils/tuning/libtuning/modules/lsc/__init__.py
index 0ba4411b4..0b7d67f0d 100644
--- a/utils/tuning/libtuning/modules/lsc/__init__.py
+++ b/utils/tuning/libtuning/modules/lsc/__init__.py
@@ -1,7 +1,8 @@ 
 # SPDX-License-Identifier: GPL-2.0-or-later
 #
-# Copyright (C) 2022, Paul Elder <paul.elder@ideasonboard.com>
+# Copyright (C) 2022, 2026, Paul Elder <paul.elder@ideasonboard.com>
 
 from libtuning.modules.lsc.lsc import LSC
 from libtuning.modules.lsc.raspberrypi import ALSCRaspberryPi
 from libtuning.modules.lsc.rkisp1 import LSCRkISP1
+from libtuning.modules.lsc.softisp import LSCSoftisp
diff --git a/utils/tuning/libtuning/modules/lsc/softisp.py b/utils/tuning/libtuning/modules/lsc/softisp.py
new file mode 100644
index 000000000..42437083c
--- /dev/null
+++ b/utils/tuning/libtuning/modules/lsc/softisp.py
@@ -0,0 +1,105 @@ 
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (C) 2019, Raspberry Pi Ltd
+# Copyright (C) 2022, Paul Elder <paul.elder@ideasonboard.com>
+#
+# LSC module for tuning softisp
+
+from .lsc import LSC
+
+import libtuning as lt
+import libtuning.utils as utils
+
+from numbers import Number
+import numpy as np
+
+
+class LSCSoftisp(LSC):
+    hr_name = 'LSC (Softisp)'
+    out_name = 'Lsc'
+    # \todo Not sure if this is useful. Probably will remove later.
+    compatible = ['softisp']
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(**kwargs)
+
+    # We don't actually need anything from the config file
+    def validate_config(self, config: dict) -> bool:
+        return True
+
+    # @return Image color temperature, flattened array of red calibration table
+    #         (containing {sector size} elements), flattened array of red
+    #         calibration table, flattened array of green calibration
+    #         table, flattened array of blue calibration table
+
+    def _do_single_lsc(self, image: lt.Image):
+        # Perform LSC on each colour channel independently. A future enhancement
+        # worth investigating would be splitting the luminance and chrominance
+        # LSC as done by Raspberry Pi.
+        cr, _ = self._lsc_single_channel(image.channels[lt.Color.R], image)
+        cgr, _ = self._lsc_single_channel(image.channels[lt.Color.GR], image)
+        cgb, _ = self._lsc_single_channel(image.channels[lt.Color.GR], image)
+        cb, _ = self._lsc_single_channel(image.channels[lt.Color.B], image)
+        cg = (cgr + cgb) / 2
+
+        return image.color, cr.flatten(), cg.flatten(), cb.flatten()
+
+    # @return List of dictionaries of color temperature, red table, green
+    #         table and blue table
+
+    def _do_all_lsc(self, images: list) -> list:
+        output_list = []
+        output_map_func = lt.gradient.Linear().map
+
+        # List of colour temperatures
+        list_col = []
+        # Associated calibration tables
+        list_cr = []
+        list_cg = []
+        list_cb = []
+        for image in self._enumerate_lsc_images(images):
+            col, cr, cg, cb = self._do_single_lsc(image)
+            list_col.append(col)
+            list_cr.append(cr)
+            list_cg.append(cg)
+            list_cb.append(cb)
+
+        # Convert to numpy array for data manipulation
+        list_col = np.array(list_col)
+        list_cr = np.array(list_cr)
+        list_cg = np.array(list_cg)
+        list_cb = np.array(list_cb)
+
+        for color_temperature in sorted(set(list_col)):
+            # Average tables for the same colour temperature
+            indices = np.where(list_col == color_temperature)
+            color_temperature = int(color_temperature)
+
+            tables = []
+            for lis in [list_cr, list_cg, list_cb]:
+                table = np.mean(lis[indices], axis=0)
+                table = output_map_func((1, 3), (1, 255), table)
+                table = np.clip(table, 1, 255)
+                table = np.round(table).astype('int32').tolist()
+                tables.append(table)
+
+            entry = {
+                'ct': color_temperature,
+                'r': tables[0],
+                'g': tables[1],
+                'b': tables[2],
+            }
+
+            output_list.append(entry)
+
+        return output_list
+
+    def process(self, config: dict, images: list, outputs: dict) -> dict:
+        output = {}
+
+        output['sets'] = self._do_all_lsc(images)
+
+        if len(output['sets']) == 0:
+            return None
+
+        return output
diff --git a/utils/tuning/softisp.py b/utils/tuning/softisp.py
new file mode 100755
index 000000000..cf8b21555
--- /dev/null
+++ b/utils/tuning/softisp.py
@@ -0,0 +1,42 @@ 
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-or-later
+#
+# Copyright (C) 2022, Paul Elder <paul.elder@ideasonboard.com>
+# Copyright (C) 2024, Ideas On Board
+#
+# Tuning script for software ISP
+
+import logging
+import sys
+
+import coloredlogs
+import libtuning as lt
+from libtuning.generators import YamlOutput
+from libtuning.modules.lsc import LSCSoftisp
+from libtuning.parsers import YamlParser
+
+coloredlogs.install(level=logging.INFO, fmt='%(name)s %(levelname)s %(message)s')
+
+lsc = LSCSoftisp(debug=[lt.Debug.Plot],
+                 sector_shape=(17, 17),
+
+                 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 = lt.Tuner('Softisp')
+tuner.add([lsc])
+tuner.set_input_parser(YamlParser())
+tuner.set_output_formatter(YamlOutput())
+
+tuner.set_output_order([lsc])
+
+if __name__ == '__main__':
+    sys.exit(tuner.run(sys.argv))