[libcamera-devel,v2,06/11] utils: libtuning: generators: Add raspberrypi output
diff mbox series

Message ID 20221022062310.2545463-7-paul.elder@ideasonboard.com
State Accepted
Headers show
Series
  • utils: tuning: Add a new tuning infrastructure
Related show

Commit Message

Paul Elder Oct. 22, 2022, 6:23 a.m. UTC
Add a generator to libtuning for writing tuning output to a json file
formatted the same way that raspberrypi's ctt formats them.

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

---
Changes in v2:
- add SPDX and copyright
- fix style
- move the 'rpi.' prefix of module names in the tuning output from here
  to into the Modules' out_name field
---
 utils/tuning/libtuning/generators/__init__.py |   5 +
 .../generators/raspberrypi_output.py          | 115 ++++++++++++++++++
 2 files changed, 120 insertions(+)
 create mode 100644 utils/tuning/libtuning/generators/raspberrypi_output.py

Comments

Laurent Pinchart Nov. 9, 2022, 10:34 a.m. UTC | #1
Hi Paul,

Thank you for the patch.

On Sat, Oct 22, 2022 at 03:23:05PM +0900, Paul Elder via libcamera-devel wrote:
> Add a generator to libtuning for writing tuning output to a json file
> formatted the same way that raspberrypi's ctt formats them.
> 
> Signed-off-by: Paul Elder <paul.elder@ideasonboard.com>
> 
> ---
> Changes in v2:
> - add SPDX and copyright
> - fix style
> - move the 'rpi.' prefix of module names in the tuning output from here
>   to into the Modules' out_name field
> ---
>  utils/tuning/libtuning/generators/__init__.py |   5 +
>  .../generators/raspberrypi_output.py          | 115 ++++++++++++++++++
>  2 files changed, 120 insertions(+)
>  create mode 100644 utils/tuning/libtuning/generators/raspberrypi_output.py
> 
> diff --git a/utils/tuning/libtuning/generators/__init__.py b/utils/tuning/libtuning/generators/__init__.py
> index e69de29b..937aff30 100644
> --- a/utils/tuning/libtuning/generators/__init__.py
> +++ b/utils/tuning/libtuning/generators/__init__.py
> @@ -0,0 +1,5 @@
> +# SPDX-License-Identifier: GPL-2.0-or-later
> +#
> +# Copyright (C) 2022, Paul Elder <paul.elder@ideasonboard.com>
> +
> +from libtuning.generators.raspberrypi_output import RaspberryPiOutput
> diff --git a/utils/tuning/libtuning/generators/raspberrypi_output.py b/utils/tuning/libtuning/generators/raspberrypi_output.py
> new file mode 100644
> index 00000000..e06aeddf
> --- /dev/null
> +++ b/utils/tuning/libtuning/generators/raspberrypi_output.py
> @@ -0,0 +1,115 @@
> +# SPDX-License-Identifier: BSD-2-Clause
> +#
> +# Copyright 2022 Raspberry Pi Ltd
> +#
> +# Script to pretty print a Raspberry Pi tuning config JSON structure in
> +# version 2.0 and later formats.
> +# (Copied from ctt_pretty_print_json.py)
> +
> +from .generator import Generator
> +
> +import json
> +from pathlib import Path
> +import textwrap
> +
> +
> +class RaspberryPiOutput(Generator):
> +    def __init__(self):
> +        super().__init__()
> +
> +    def _write(self, output_file: Path, output_dict: dict, output_order: list):
> +        # Write json dictionary to file using ctt's version 2 format
> +        out_json = {
> +            "version": 2.0,
> +            'target': 'bcm2835',
> +            "algorithms": [{f'{module.out_name}': output_dict[module]} for module in output_order]
> +        }
> +
> +        with open(output_file, 'w') as f:
> +            f.write(pretty_print(out_json))

Please move this class to the end of the file. I would also possibly
make the pretty_print function a (private) class member.

Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>

> +
> +
> +class Encoder(json.JSONEncoder):
> +
> +    def __init__(self, *args, **kwargs):
> +        super().__init__(*args, **kwargs)
> +        self.indentation_level = 0
> +        self.hard_break = 120
> +        self.custom_elems = {
> +            'table': 16,
> +            'luminance_lut': 16,
> +            'ct_curve': 3,
> +            'ccm': 3,
> +            'gamma_curve': 2,
> +            'y_target': 2,
> +            'prior': 2
> +        }
> +
> +    def encode(self, o, node_key=None):
> +        if isinstance(o, (list, tuple)):
> +            # Check if we are a flat list of numbers.
> +            if not any(isinstance(el, (list, tuple, dict)) for el in o):
> +                s = ', '.join(json.dumps(el) for el in o)
> +                if node_key in self.custom_elems.keys():
> +                    # Special case handling to specify number of elements in a row for tables, ccm, etc.
> +                    self.indentation_level += 1
> +                    sl = s.split(', ')
> +                    num = self.custom_elems[node_key]
> +                    chunk = [self.indent_str + ', '.join(sl[x:x + num]) for x in range(0, len(sl), num)]
> +                    t = ',\n'.join(chunk)
> +                    self.indentation_level -= 1
> +                    output = f'\n{self.indent_str}[\n{t}\n{self.indent_str}]'
> +                elif len(s) > self.hard_break - len(self.indent_str):
> +                    # Break a long list with wraps.
> +                    self.indentation_level += 1
> +                    t = textwrap.fill(s, self.hard_break, break_long_words=False,
> +                                      initial_indent=self.indent_str, subsequent_indent=self.indent_str)
> +                    self.indentation_level -= 1
> +                    output = f'\n{self.indent_str}[\n{t}\n{self.indent_str}]'
> +                else:
> +                    # Smaller lists can remain on a single line.
> +                    output = f' [ {s} ]'
> +                return output
> +            else:
> +                # Sub-structures in the list case.
> +                self.indentation_level += 1
> +                output = [self.indent_str + self.encode(el) for el in o]
> +                self.indentation_level -= 1
> +                output = ',\n'.join(output)
> +                return f' [\n{output}\n{self.indent_str}]'
> +
> +        elif isinstance(o, dict):
> +            self.indentation_level += 1
> +            output = []
> +            for k, v in o.items():
> +                if isinstance(v, dict) and len(v) == 0:
> +                    # Empty config block special case.
> +                    output.append(self.indent_str + f'{json.dumps(k)}: {{ }}')
> +                else:
> +                    # Only linebreak if the next node is a config block.
> +                    sep = f'\n{self.indent_str}' if isinstance(v, dict) else ''
> +                    output.append(self.indent_str + f'{json.dumps(k)}:{sep}{self.encode(v, k)}')
> +            output = ',\n'.join(output)
> +            self.indentation_level -= 1
> +            return f'{{\n{output}\n{self.indent_str}}}'
> +
> +        else:
> +            return ' ' + json.dumps(o)
> +
> +    @property
> +    def indent_str(self) -> str:
> +        return ' ' * self.indentation_level * self.indent
> +
> +    def iterencode(self, o, **kwargs):
> +        return self.encode(o)
> +
> +
> +def pretty_print(in_json: dict) -> str:
> +
> +    if 'version' not in in_json or \
> +       'target' not in in_json or \
> +       'algorithms' not in in_json or \
> +       in_json['version'] < 2.0:
> +        raise RuntimeError('Incompatible JSON dictionary has been provided')
> +
> +    return json.dumps(in_json, cls=Encoder, indent=4, sort_keys=False)

Patch
diff mbox series

diff --git a/utils/tuning/libtuning/generators/__init__.py b/utils/tuning/libtuning/generators/__init__.py
index e69de29b..937aff30 100644
--- a/utils/tuning/libtuning/generators/__init__.py
+++ b/utils/tuning/libtuning/generators/__init__.py
@@ -0,0 +1,5 @@ 
+# SPDX-License-Identifier: GPL-2.0-or-later
+#
+# Copyright (C) 2022, Paul Elder <paul.elder@ideasonboard.com>
+
+from libtuning.generators.raspberrypi_output import RaspberryPiOutput
diff --git a/utils/tuning/libtuning/generators/raspberrypi_output.py b/utils/tuning/libtuning/generators/raspberrypi_output.py
new file mode 100644
index 00000000..e06aeddf
--- /dev/null
+++ b/utils/tuning/libtuning/generators/raspberrypi_output.py
@@ -0,0 +1,115 @@ 
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright 2022 Raspberry Pi Ltd
+#
+# Script to pretty print a Raspberry Pi tuning config JSON structure in
+# version 2.0 and later formats.
+# (Copied from ctt_pretty_print_json.py)
+
+from .generator import Generator
+
+import json
+from pathlib import Path
+import textwrap
+
+
+class RaspberryPiOutput(Generator):
+    def __init__(self):
+        super().__init__()
+
+    def _write(self, output_file: Path, output_dict: dict, output_order: list):
+        # Write json dictionary to file using ctt's version 2 format
+        out_json = {
+            "version": 2.0,
+            'target': 'bcm2835',
+            "algorithms": [{f'{module.out_name}': output_dict[module]} for module in output_order]
+        }
+
+        with open(output_file, 'w') as f:
+            f.write(pretty_print(out_json))
+
+
+class Encoder(json.JSONEncoder):
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.indentation_level = 0
+        self.hard_break = 120
+        self.custom_elems = {
+            'table': 16,
+            'luminance_lut': 16,
+            'ct_curve': 3,
+            'ccm': 3,
+            'gamma_curve': 2,
+            'y_target': 2,
+            'prior': 2
+        }
+
+    def encode(self, o, node_key=None):
+        if isinstance(o, (list, tuple)):
+            # Check if we are a flat list of numbers.
+            if not any(isinstance(el, (list, tuple, dict)) for el in o):
+                s = ', '.join(json.dumps(el) for el in o)
+                if node_key in self.custom_elems.keys():
+                    # Special case handling to specify number of elements in a row for tables, ccm, etc.
+                    self.indentation_level += 1
+                    sl = s.split(', ')
+                    num = self.custom_elems[node_key]
+                    chunk = [self.indent_str + ', '.join(sl[x:x + num]) for x in range(0, len(sl), num)]
+                    t = ',\n'.join(chunk)
+                    self.indentation_level -= 1
+                    output = f'\n{self.indent_str}[\n{t}\n{self.indent_str}]'
+                elif len(s) > self.hard_break - len(self.indent_str):
+                    # Break a long list with wraps.
+                    self.indentation_level += 1
+                    t = textwrap.fill(s, self.hard_break, break_long_words=False,
+                                      initial_indent=self.indent_str, subsequent_indent=self.indent_str)
+                    self.indentation_level -= 1
+                    output = f'\n{self.indent_str}[\n{t}\n{self.indent_str}]'
+                else:
+                    # Smaller lists can remain on a single line.
+                    output = f' [ {s} ]'
+                return output
+            else:
+                # Sub-structures in the list case.
+                self.indentation_level += 1
+                output = [self.indent_str + self.encode(el) for el in o]
+                self.indentation_level -= 1
+                output = ',\n'.join(output)
+                return f' [\n{output}\n{self.indent_str}]'
+
+        elif isinstance(o, dict):
+            self.indentation_level += 1
+            output = []
+            for k, v in o.items():
+                if isinstance(v, dict) and len(v) == 0:
+                    # Empty config block special case.
+                    output.append(self.indent_str + f'{json.dumps(k)}: {{ }}')
+                else:
+                    # Only linebreak if the next node is a config block.
+                    sep = f'\n{self.indent_str}' if isinstance(v, dict) else ''
+                    output.append(self.indent_str + f'{json.dumps(k)}:{sep}{self.encode(v, k)}')
+            output = ',\n'.join(output)
+            self.indentation_level -= 1
+            return f'{{\n{output}\n{self.indent_str}}}'
+
+        else:
+            return ' ' + json.dumps(o)
+
+    @property
+    def indent_str(self) -> str:
+        return ' ' * self.indentation_level * self.indent
+
+    def iterencode(self, o, **kwargs):
+        return self.encode(o)
+
+
+def pretty_print(in_json: dict) -> str:
+
+    if 'version' not in in_json or \
+       'target' not in in_json or \
+       'algorithms' not in in_json or \
+       in_json['version'] < 2.0:
+        raise RuntimeError('Incompatible JSON dictionary has been provided')
+
+    return json.dumps(in_json, cls=Encoder, indent=4, sort_keys=False)