[1/3] utils: tuning: Add tuning scripts for mali-c55
diff mbox series

Message ID 20250304231254.10588-2-dan.scally@ideasonboard.com
State New
Headers show
Series
  • Add Mali-C55 Tuning Script
Related show

Commit Message

Dan Scally March 4, 2025, 11:12 p.m. UTC
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@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

Comments

Kieran Bingham July 21, 2025, 6:47 p.m. UTC | #1
Quoting Daniel Scally (2025-03-04 23:12:52)
> Add tuning scripts for the mali-c55 IPA. At present the only module
> that is available is for lens shading correction.
> 

As a new module - I don't think this can cause regressions - so as long
as it runs ...

Acked-by: Kieran Bingham <kieran.bingham@ideasonboard.com>

> Signed-off-by: Daniel Scally <dan.scally@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@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
>
Paul Elder July 22, 2025, 10:16 a.m. UTC | #2
Quoting Daniel Scally (2025-03-05 08:12:52)
> 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@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@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()
> +               )

This looks exactly the same as rkisp1's... I wonder if it's worth pulling this
out into the LSC class. At least _lsc_single_channel is deduplicated :)

In any case, out of scope for this series; just me mumbling to myself.

> +
> +       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.

rkisp1 doesn't look relevant to mali-c55 :)


With that removed (or changed to fit), looks good to me.

Reviewed-by: Paul Elder <paul.elder@ideasonboard.com>

> +          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
>
Dan Scally July 25, 2025, 12:01 p.m. UTC | #3
Hi Paul

On 22/07/2025 11:16, Paul Elder wrote:
> Quoting Daniel Scally (2025-03-05 08:12:52)
>> 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@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@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()
>> +               )
> This looks exactly the same as rkisp1's... I wonder if it's worth pulling this
> out into the LSC class. At least _lsc_single_channel is deduplicated :)
>
> In any case, out of scope for this series; just me mumbling to myself.
>
>> +
>> +       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.
> rkisp1 doesn't look relevant to mali-c55 :)

Oops, I'll fix that
>
>
> With that removed (or changed to fit), looks good to me.
>
> Reviewed-by: Paul Elder <paul.elder@ideasonboard.com>


Thanks!

>
>> +          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
>>

Patch
diff mbox series

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@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))