[libcamera-devel,1/3] utils: rkisp1: Add script to extract LSC tables from Android
diff mbox series

Message ID 20230306172440.57764-2-jacopo.mondi@ideasonboard.com
State Accepted
Headers show
Series
  • ipa: rkisp1: Add script to extract LSC tables from Android
Related show

Commit Message

Jacopo Mondi March 6, 2023, 5:24 p.m. UTC
Android ship a per-sensor configuration file in .xml format.

The .xml file contains a main <matfile> node and a <sensor> sub-node
which contains an <LSC> entry. The LSC tables there contained can be
re-used for libcamera, by parsing them opportunely.

Add a script to utils/rkisp1/ to extract the LSC tables from Android
configuration file for the RkISP1 platform, modeled after the
requirements of the Rockchip closed source IQ tuning module.

Compared to the Rockchip IQ LSC module the one implemented in libcamera
is slightly simpler, and the parsing of the LSC table takes that into
account by:
- Only outputting tables for the larger found sensor resolution
- Only outputting tables for "vignetting" value == 70 (ignoring the ones
  for vignetting values of 100)

The script outputs to stdout a "LensShadingCorrection" section that
can be directly pasted in a libcamera sensor configuration file.

Signed-off-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
---
 utils/rkisp1/lsc_parse_android_config.py | 187 +++++++++++++++++++++++
 1 file changed, 187 insertions(+)
 create mode 100755 utils/rkisp1/lsc_parse_android_config.py

Comments

Laurent Pinchart March 7, 2023, 9:49 a.m. UTC | #1
Hi Jacopo,

Thank you for the patch.

On Mon, Mar 06, 2023 at 06:24:38PM +0100, Jacopo Mondi via libcamera-devel wrote:
> Android ship a per-sensor configuration file in .xml format.

s/ship/ships/

> 
> The .xml file contains a main <matfile> node and a <sensor> sub-node
> which contains an <LSC> entry. The LSC tables there contained can be
> re-used for libcamera, by parsing them opportunely.
> 
> Add a script to utils/rkisp1/ to extract the LSC tables from Android
> configuration file for the RkISP1 platform, modeled after the
> requirements of the Rockchip closed source IQ tuning module.
> 
> Compared to the Rockchip IQ LSC module the one implemented in libcamera
> is slightly simpler, and the parsing of the LSC table takes that into
> account by:
> - Only outputting tables for the larger found sensor resolution
> - Only outputting tables for "vignetting" value == 70 (ignoring the ones
>   for vignetting values of 100)
> 
> The script outputs to stdout a "LensShadingCorrection" section that
> can be directly pasted in a libcamera sensor configuration file.

Could we generate a full tuning file instead ? I would imagine that
other tuning data could be (later) extracted, it would be nice to
prepare for that.

> Signed-off-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
> ---
>  utils/rkisp1/lsc_parse_android_config.py | 187 +++++++++++++++++++++++

And we could already name the script in a more generic way, to hint that
it converts a Rockchip tuning file to a libcamera tuning file.

Do you know if the XML format is Android-specific ?

>  1 file changed, 187 insertions(+)
>  create mode 100755 utils/rkisp1/lsc_parse_android_config.py
> 
> diff --git a/utils/rkisp1/lsc_parse_android_config.py b/utils/rkisp1/lsc_parse_android_config.py
> new file mode 100755
> index 000000000000..a7c2c160319d
> --- /dev/null
> +++ b/utils/rkisp1/lsc_parse_android_config.py
> @@ -0,0 +1,187 @@
> +#!/usr/bin/env python
> +
> +# SPDX-License-Identifier: GPL-2.0-or-later
> +# Copyright (C) 2023, Jacopo Mondi - Ideas on Board Oy
> +#
> +# Parse Android .xml configuration file and extract the LSC tables.
> +#
> +# Print to standard output a "LensShadingCorrection" section, understandable by
> +# libcamera LSC algorithm, that can be pasted to the sensor configuration file.
> +
> +import argparse
> +import string
> +import sys
> +import re

Alphabetical order please.

> +import xml.etree.ElementTree as et
> +
> +
> +def sanitize(name):
> +    return re.sub(r"[\n\t\s]*", "", name)

We use single quotes for strings when there's no specific reason to do
otherwise.

> +
> +
> +def split_table(table):
> +    values = ""
> +    for v in table.text.strip(' ').split():
> +        values += v.strip('[').strip(']') + ", "
> +    return values
> +
> +
> +def print_cell(cell):
> +    lsc_template = string.Template('''        #${name} - ${illuminant}
> +        - ct: ${ct}
> +          resolution: ${res}
> +          r: [${red}]
> +          gr: [${greenr}]
> +          gb: [${greenb}]
> +          b: [${blue}]''')
> +
> +    illuminant = cell.find("illumination")
> +    ct = illuminant_to_ct(illuminant)
> +
> +    template_dict = {
> +        'name': sanitize(cell.find("name").text),
> +        'illuminant': sanitize(illuminant.text),
> +        'ct': ct,
> +        'res': sanitize(cell.find("resolution").text)
> +    }
> +
> +    red_table = cell.find("LSC_SAMPLES_red")
> +    greenr_table = cell.find("LSC_SAMPLES_greenR")
> +    greenb_table = cell.find("LSC_SAMPLES_greenB")
> +    blue_table = cell.find("LSC_SAMPLES_blue")
> +
> +    if red_table is None or greenr_table is None or greenb_table is None or blue_table is None:
> +        return
> +
> +    template_dict['red'] = split_table(red_table)
> +    template_dict['greenr'] = split_table(greenr_table)
> +    template_dict['greenb'] = split_table(greenb_table)
> +    template_dict['blue'] = split_table(blue_table)
> +
> +    return lsc_template.substitute(template_dict)
> +
> +
> +def illuminant_to_ct(illuminant):
> +    # Standard CIE Illiminants to Color Temperature in Kelvin
> +    # https://en.wikipedia.org/wiki/Standard_illuminant
> +    #
> +    # Not included (and then ignored when parsing the configuration file):
> +    # - "Horizon" == D50 == 5003
> +    # - "BW" == ?
> +    # - "PREFLASH" == ?
> +    illuminants_dict = {
> +        'A': 2856,
> +        'D50': 5003,
> +        'D65': 6504,
> +        'D75': 7504,
> +        'F11_TL84': 4000,
> +        'F2_CWF': 4230,
> +    }
> +
> +    ill_key = sanitize(illuminant.text)
> +    try:
> +        ct = illuminants_dict[ill_key]
> +    except KeyError:
> +        return None
> +
> +    return ct
> +
> +
> +# Make sure the cell is well formed and return it
> +def filter_cells(cell, res, lsc_cells):
> +    name = cell.find("name")
> +    resolution = cell.find("resolution")
> +    illumination = cell.find("illumination")
> +    vignetting = cell.find("vignetting")
> +
> +    if name is None or resolution is None or \
> +       illumination is None or vignetting is None:
> +        return
> +
> +    # Skip tables for smaller sensor resolutions
> +    if res != sanitize(resolution.text):
> +        return
> +
> +    # Skip tables for which we don't know how to translate the illuminant value
> +    ct = illuminant_to_ct(illumination)
> +    if ct is None:
> +        return
> +
> +    # Only pick tables with vignetting == 70
> +    if sanitize(vignetting.text) != "[70]":
> +        return
> +
> +    lsc_cells.append(cell)
> +
> +
> +# Get the "LSC" node
> +def find_lsc_table(root):
> +    sensor = root.find('sensor')
> +    if sensor is None:
> +        print("Failed to find \"sensor\" node in config file")
> +        raise Exception

        raise RuntimeError('Failed to find "sensor" node in config file')

and print the message in the caller. Same below.

> +
> +    lsc = sensor.find('LSC')
> +    if lsc is None:
> +        print("Filed to find \"LSC\" node in config file")
> +        raise Exception
> +
> +    return lsc
> +
> +# libcamera LSC algorithm only operates on a single resolution.
> +# Find the largest sensor mode among the ones reported in the LSC tables
> +
> +
> +def parse_max_res(cells):
> +    max_res = ""
> +    max_size = 0
> +
> +    for cell in cells:
> +        resolution = sanitize(cell.find("resolution").text)
> +        [w, h] = resolution.split('x')
> +
> +        area = int(w) * int(h)
> +        if area > max_size:
> +            max_res = resolution
> +
> +    return max_res
> +
> +
> +def main(argv):
> +    # Parse command line arguments.
> +    parser = argparse.ArgumentParser(
> +        description='Parse Android camera configuration file to extract LSC tables')
> +    parser.add_argument('--file', '-f', required=True,
> +                        help='Path to the Android .xml configuration file')

It's quite traditional in command line tools to pass the input file as a
positional argument instead of a named argument. Up to you.

> +    args = parser.parse_args(argv[1:])
> +
> +    root = et.parse(args.file).getroot()

It would be nice to catch parse errors and print a human-readable
message.

> +    try:
> +        lsc_node = find_lsc_table(root)
> +    except Exception:
> +        return 1
> +
> +    cells = lsc_node.findall("cell")
> +
> +    max_res = parse_max_res(cells)
> +    if max_res == "":
> +        return
> +
> +    lsc_cells = []
> +    for cell in cells:
> +        filter_cells(cell, max_res, lsc_cells)
> +
> +    lsc_section = '''  - LensShadingCorrection:
> +      x-size: [ 0.0625, 0.0625, 0.0625, 0.0625, 0.0625, 0.0625, 0.0625, 0.0625 ]
> +      y-size: [ 0.0625, 0.0625, 0.0625, 0.0625, 0.0625, 0.0625, 0.0625, 0.0625 ]
> +      sets:
> +'''

Given that you're generating a rkisp1 tuning file, using the YamlOutput
class from libtuning would make sense.

> +
> +    for cell in lsc_cells:
> +        lsc_section += print_cell(cell) + "\n"
> +
> +    print(lsc_section)
> +
> +
> +if __name__ == '__main__':
> +    sys.exit(main(sys.argv))
Laurent Pinchart March 7, 2023, 10:12 a.m. UTC | #2
Another comment.

On Tue, Mar 07, 2023 at 11:49:23AM +0200, Laurent Pinchart via libcamera-devel wrote:
> Hi Jacopo,
> 
> Thank you for the patch.
> 
> On Mon, Mar 06, 2023 at 06:24:38PM +0100, Jacopo Mondi via libcamera-devel wrote:
> > Android ship a per-sensor configuration file in .xml format.
> 
> s/ship/ships/
> 
> > 
> > The .xml file contains a main <matfile> node and a <sensor> sub-node
> > which contains an <LSC> entry. The LSC tables there contained can be
> > re-used for libcamera, by parsing them opportunely.
> > 
> > Add a script to utils/rkisp1/ to extract the LSC tables from Android
> > configuration file for the RkISP1 platform, modeled after the
> > requirements of the Rockchip closed source IQ tuning module.
> > 
> > Compared to the Rockchip IQ LSC module the one implemented in libcamera
> > is slightly simpler, and the parsing of the LSC table takes that into
> > account by:
> > - Only outputting tables for the larger found sensor resolution
> > - Only outputting tables for "vignetting" value == 70 (ignoring the ones
> >   for vignetting values of 100)
> > 
> > The script outputs to stdout a "LensShadingCorrection" section that
> > can be directly pasted in a libcamera sensor configuration file.
> 
> Could we generate a full tuning file instead ? I would imagine that
> other tuning data could be (later) extracted, it would be nice to
> prepare for that.
> 
> > Signed-off-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
> > ---
> >  utils/rkisp1/lsc_parse_android_config.py | 187 +++++++++++++++++++++++
> 
> And we could already name the script in a more generic way, to hint that
> it converts a Rockchip tuning file to a libcamera tuning file.
> 
> Do you know if the XML format is Android-specific ?
> 
> >  1 file changed, 187 insertions(+)
> >  create mode 100755 utils/rkisp1/lsc_parse_android_config.py
> > 
> > diff --git a/utils/rkisp1/lsc_parse_android_config.py b/utils/rkisp1/lsc_parse_android_config.py
> > new file mode 100755
> > index 000000000000..a7c2c160319d
> > --- /dev/null
> > +++ b/utils/rkisp1/lsc_parse_android_config.py
> > @@ -0,0 +1,187 @@
> > +#!/usr/bin/env python
> > +
> > +# SPDX-License-Identifier: GPL-2.0-or-later
> > +# Copyright (C) 2023, Jacopo Mondi - Ideas on Board Oy
> > +#
> > +# Parse Android .xml configuration file and extract the LSC tables.
> > +#
> > +# Print to standard output a "LensShadingCorrection" section, understandable by
> > +# libcamera LSC algorithm, that can be pasted to the sensor configuration file.
> > +
> > +import argparse
> > +import string
> > +import sys
> > +import re
> 
> Alphabetical order please.
> 
> > +import xml.etree.ElementTree as et
> > +
> > +
> > +def sanitize(name):
> > +    return re.sub(r"[\n\t\s]*", "", name)
> 
> We use single quotes for strings when there's no specific reason to do
> otherwise.
> 
> > +
> > +
> > +def split_table(table):
> > +    values = ""
> > +    for v in table.text.strip(' ').split():
> > +        values += v.strip('[').strip(']') + ", "
> > +    return values
> > +
> > +
> > +def print_cell(cell):
> > +    lsc_template = string.Template('''        #${name} - ${illuminant}

Missing space after the #

> > +        - ct: ${ct}
> > +          resolution: ${res}
> > +          r: [${red}]
> > +          gr: [${greenr}]
> > +          gb: [${greenb}]
> > +          b: [${blue}]''')
> > +
> > +    illuminant = cell.find("illumination")
> > +    ct = illuminant_to_ct(illuminant)
> > +
> > +    template_dict = {
> > +        'name': sanitize(cell.find("name").text),
> > +        'illuminant': sanitize(illuminant.text),
> > +        'ct': ct,
> > +        'res': sanitize(cell.find("resolution").text)
> > +    }
> > +
> > +    red_table = cell.find("LSC_SAMPLES_red")
> > +    greenr_table = cell.find("LSC_SAMPLES_greenR")
> > +    greenb_table = cell.find("LSC_SAMPLES_greenB")
> > +    blue_table = cell.find("LSC_SAMPLES_blue")
> > +
> > +    if red_table is None or greenr_table is None or greenb_table is None or blue_table is None:
> > +        return
> > +
> > +    template_dict['red'] = split_table(red_table)
> > +    template_dict['greenr'] = split_table(greenr_table)
> > +    template_dict['greenb'] = split_table(greenb_table)
> > +    template_dict['blue'] = split_table(blue_table)
> > +
> > +    return lsc_template.substitute(template_dict)
> > +
> > +
> > +def illuminant_to_ct(illuminant):
> > +    # Standard CIE Illiminants to Color Temperature in Kelvin
> > +    # https://en.wikipedia.org/wiki/Standard_illuminant
> > +    #
> > +    # Not included (and then ignored when parsing the configuration file):
> > +    # - "Horizon" == D50 == 5003
> > +    # - "BW" == ?
> > +    # - "PREFLASH" == ?
> > +    illuminants_dict = {
> > +        'A': 2856,
> > +        'D50': 5003,
> > +        'D65': 6504,
> > +        'D75': 7504,
> > +        'F11_TL84': 4000,
> > +        'F2_CWF': 4230,
> > +    }
> > +
> > +    ill_key = sanitize(illuminant.text)
> > +    try:
> > +        ct = illuminants_dict[ill_key]
> > +    except KeyError:
> > +        return None
> > +
> > +    return ct
> > +
> > +
> > +# Make sure the cell is well formed and return it
> > +def filter_cells(cell, res, lsc_cells):
> > +    name = cell.find("name")
> > +    resolution = cell.find("resolution")
> > +    illumination = cell.find("illumination")
> > +    vignetting = cell.find("vignetting")
> > +
> > +    if name is None or resolution is None or \
> > +       illumination is None or vignetting is None:
> > +        return
> > +
> > +    # Skip tables for smaller sensor resolutions
> > +    if res != sanitize(resolution.text):
> > +        return
> > +
> > +    # Skip tables for which we don't know how to translate the illuminant value
> > +    ct = illuminant_to_ct(illumination)
> > +    if ct is None:
> > +        return
> > +
> > +    # Only pick tables with vignetting == 70
> > +    if sanitize(vignetting.text) != "[70]":
> > +        return
> > +
> > +    lsc_cells.append(cell)
> > +
> > +
> > +# Get the "LSC" node
> > +def find_lsc_table(root):
> > +    sensor = root.find('sensor')
> > +    if sensor is None:
> > +        print("Failed to find \"sensor\" node in config file")
> > +        raise Exception
> 
>         raise RuntimeError('Failed to find "sensor" node in config file')
> 
> and print the message in the caller. Same below.
> 
> > +
> > +    lsc = sensor.find('LSC')
> > +    if lsc is None:
> > +        print("Filed to find \"LSC\" node in config file")
> > +        raise Exception
> > +
> > +    return lsc
> > +
> > +# libcamera LSC algorithm only operates on a single resolution.
> > +# Find the largest sensor mode among the ones reported in the LSC tables
> > +
> > +
> > +def parse_max_res(cells):
> > +    max_res = ""
> > +    max_size = 0
> > +
> > +    for cell in cells:
> > +        resolution = sanitize(cell.find("resolution").text)
> > +        [w, h] = resolution.split('x')
> > +
> > +        area = int(w) * int(h)
> > +        if area > max_size:
> > +            max_res = resolution
> > +
> > +    return max_res
> > +
> > +
> > +def main(argv):
> > +    # Parse command line arguments.
> > +    parser = argparse.ArgumentParser(
> > +        description='Parse Android camera configuration file to extract LSC tables')
> > +    parser.add_argument('--file', '-f', required=True,
> > +                        help='Path to the Android .xml configuration file')
> 
> It's quite traditional in command line tools to pass the input file as a
> positional argument instead of a named argument. Up to you.
> 
> > +    args = parser.parse_args(argv[1:])
> > +
> > +    root = et.parse(args.file).getroot()
> 
> It would be nice to catch parse errors and print a human-readable
> message.
> 
> > +    try:
> > +        lsc_node = find_lsc_table(root)
> > +    except Exception:
> > +        return 1
> > +
> > +    cells = lsc_node.findall("cell")
> > +
> > +    max_res = parse_max_res(cells)
> > +    if max_res == "":
> > +        return
> > +
> > +    lsc_cells = []
> > +    for cell in cells:
> > +        filter_cells(cell, max_res, lsc_cells)
> > +
> > +    lsc_section = '''  - LensShadingCorrection:
> > +      x-size: [ 0.0625, 0.0625, 0.0625, 0.0625, 0.0625, 0.0625, 0.0625, 0.0625 ]
> > +      y-size: [ 0.0625, 0.0625, 0.0625, 0.0625, 0.0625, 0.0625, 0.0625, 0.0625 ]
> > +      sets:
> > +'''
> 
> Given that you're generating a rkisp1 tuning file, using the YamlOutput
> class from libtuning would make sense.
> 
> > +
> > +    for cell in lsc_cells:
> > +        lsc_section += print_cell(cell) + "\n"
> > +
> > +    print(lsc_section)
> > +
> > +
> > +if __name__ == '__main__':
> > +    sys.exit(main(sys.argv))
> 
> -- 
> Regards,
> 
> Laurent Pinchart
Jacopo Mondi March 7, 2023, 1:49 p.m. UTC | #3
Hi Laurent

On Tue, Mar 07, 2023 at 11:49:23AM +0200, Laurent Pinchart via libcamera-devel wrote:
> Hi Jacopo,
>
> Thank you for the patch.
>
> On Mon, Mar 06, 2023 at 06:24:38PM +0100, Jacopo Mondi via libcamera-devel wrote:
> > Android ship a per-sensor configuration file in .xml format.
>
> s/ship/ships/
>
> >
> > The .xml file contains a main <matfile> node and a <sensor> sub-node
> > which contains an <LSC> entry. The LSC tables there contained can be
> > re-used for libcamera, by parsing them opportunely.
> >
> > Add a script to utils/rkisp1/ to extract the LSC tables from Android
> > configuration file for the RkISP1 platform, modeled after the
> > requirements of the Rockchip closed source IQ tuning module.
> >
> > Compared to the Rockchip IQ LSC module the one implemented in libcamera
> > is slightly simpler, and the parsing of the LSC table takes that into
> > account by:
> > - Only outputting tables for the larger found sensor resolution
> > - Only outputting tables for "vignetting" value == 70 (ignoring the ones
> >   for vignetting values of 100)
> >
> > The script outputs to stdout a "LensShadingCorrection" section that
> > can be directly pasted in a libcamera sensor configuration file.
>
> Could we generate a full tuning file instead ? I would imagine that
> other tuning data could be (later) extracted, it would be nice to
> prepare for that.
>

I'm not sure about this.. how should the other algorithms already
described in a config file be handled ? are we going to overwrite
everything ?

> > Signed-off-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
> > ---
> >  utils/rkisp1/lsc_parse_android_config.py | 187 +++++++++++++++++++++++
>
> And we could already name the script in a more generic way, to hint that
> it converts a Rockchip tuning file to a libcamera tuning file.
>
> Do you know if the XML format is Android-specific ?

I presume is specific to what I think is the Rockchip 3A closed source library.
The IPU3 in example, uses a different (binary) format for the sensor
tuning paramters. So I guess is the closed source 3A module that
defines the format.

So maybe yes, "android" should not be part of the name

lsc_IQ_to_libcamera.py ?

>
> >  1 file changed, 187 insertions(+)
> >  create mode 100755 utils/rkisp1/lsc_parse_android_config.py
> >
> > diff --git a/utils/rkisp1/lsc_parse_android_config.py b/utils/rkisp1/lsc_parse_android_config.py
> > new file mode 100755
> > index 000000000000..a7c2c160319d
> > --- /dev/null
> > +++ b/utils/rkisp1/lsc_parse_android_config.py
> > @@ -0,0 +1,187 @@
> > +#!/usr/bin/env python
> > +
> > +# SPDX-License-Identifier: GPL-2.0-or-later
> > +# Copyright (C) 2023, Jacopo Mondi - Ideas on Board Oy
> > +#
> > +# Parse Android .xml configuration file and extract the LSC tables.
> > +#
> > +# Print to standard output a "LensShadingCorrection" section, understandable by
> > +# libcamera LSC algorithm, that can be pasted to the sensor configuration file.
> > +
> > +import argparse
> > +import string
> > +import sys
> > +import re
>
> Alphabetical order please.
>
> > +import xml.etree.ElementTree as et
> > +
> > +
> > +def sanitize(name):
> > +    return re.sub(r"[\n\t\s]*", "", name)
>
> We use single quotes for strings when there's no specific reason to do
> otherwise.
>
> > +
> > +
> > +def split_table(table):
> > +    values = ""
> > +    for v in table.text.strip(' ').split():
> > +        values += v.strip('[').strip(']') + ", "
> > +    return values
> > +
> > +
> > +def print_cell(cell):
> > +    lsc_template = string.Template('''        #${name} - ${illuminant}
> > +        - ct: ${ct}
> > +          resolution: ${res}
> > +          r: [${red}]
> > +          gr: [${greenr}]
> > +          gb: [${greenb}]
> > +          b: [${blue}]''')
> > +
> > +    illuminant = cell.find("illumination")
> > +    ct = illuminant_to_ct(illuminant)
> > +
> > +    template_dict = {
> > +        'name': sanitize(cell.find("name").text),
> > +        'illuminant': sanitize(illuminant.text),
> > +        'ct': ct,
> > +        'res': sanitize(cell.find("resolution").text)
> > +    }
> > +
> > +    red_table = cell.find("LSC_SAMPLES_red")
> > +    greenr_table = cell.find("LSC_SAMPLES_greenR")
> > +    greenb_table = cell.find("LSC_SAMPLES_greenB")
> > +    blue_table = cell.find("LSC_SAMPLES_blue")
> > +
> > +    if red_table is None or greenr_table is None or greenb_table is None or blue_table is None:
> > +        return
> > +
> > +    template_dict['red'] = split_table(red_table)
> > +    template_dict['greenr'] = split_table(greenr_table)
> > +    template_dict['greenb'] = split_table(greenb_table)
> > +    template_dict['blue'] = split_table(blue_table)
> > +
> > +    return lsc_template.substitute(template_dict)
> > +
> > +
> > +def illuminant_to_ct(illuminant):
> > +    # Standard CIE Illiminants to Color Temperature in Kelvin
> > +    # https://en.wikipedia.org/wiki/Standard_illuminant
> > +    #
> > +    # Not included (and then ignored when parsing the configuration file):
> > +    # - "Horizon" == D50 == 5003
> > +    # - "BW" == ?
> > +    # - "PREFLASH" == ?
> > +    illuminants_dict = {
> > +        'A': 2856,
> > +        'D50': 5003,
> > +        'D65': 6504,
> > +        'D75': 7504,
> > +        'F11_TL84': 4000,
> > +        'F2_CWF': 4230,
> > +    }
> > +
> > +    ill_key = sanitize(illuminant.text)
> > +    try:
> > +        ct = illuminants_dict[ill_key]
> > +    except KeyError:
> > +        return None
> > +
> > +    return ct
> > +
> > +
> > +# Make sure the cell is well formed and return it
> > +def filter_cells(cell, res, lsc_cells):
> > +    name = cell.find("name")
> > +    resolution = cell.find("resolution")
> > +    illumination = cell.find("illumination")
> > +    vignetting = cell.find("vignetting")
> > +
> > +    if name is None or resolution is None or \
> > +       illumination is None or vignetting is None:
> > +        return
> > +
> > +    # Skip tables for smaller sensor resolutions
> > +    if res != sanitize(resolution.text):
> > +        return
> > +
> > +    # Skip tables for which we don't know how to translate the illuminant value
> > +    ct = illuminant_to_ct(illumination)
> > +    if ct is None:
> > +        return
> > +
> > +    # Only pick tables with vignetting == 70
> > +    if sanitize(vignetting.text) != "[70]":
> > +        return
> > +
> > +    lsc_cells.append(cell)
> > +
> > +
> > +# Get the "LSC" node
> > +def find_lsc_table(root):
> > +    sensor = root.find('sensor')
> > +    if sensor is None:
> > +        print("Failed to find \"sensor\" node in config file")
> > +        raise Exception
>
>         raise RuntimeError('Failed to find "sensor" node in config file')
>
> and print the message in the caller. Same below.
>
> > +
> > +    lsc = sensor.find('LSC')
> > +    if lsc is None:
> > +        print("Filed to find \"LSC\" node in config file")
> > +        raise Exception
> > +
> > +    return lsc
> > +
> > +# libcamera LSC algorithm only operates on a single resolution.
> > +# Find the largest sensor mode among the ones reported in the LSC tables
> > +
> > +
> > +def parse_max_res(cells):
> > +    max_res = ""
> > +    max_size = 0
> > +
> > +    for cell in cells:
> > +        resolution = sanitize(cell.find("resolution").text)
> > +        [w, h] = resolution.split('x')
> > +
> > +        area = int(w) * int(h)
> > +        if area > max_size:
> > +            max_res = resolution
> > +
> > +    return max_res
> > +
> > +
> > +def main(argv):
> > +    # Parse command line arguments.
> > +    parser = argparse.ArgumentParser(
> > +        description='Parse Android camera configuration file to extract LSC tables')
> > +    parser.add_argument('--file', '-f', required=True,
> > +                        help='Path to the Android .xml configuration file')
>
> It's quite traditional in command line tools to pass the input file as a
> positional argument instead of a named argument. Up to you.
>
> > +    args = parser.parse_args(argv[1:])
> > +
> > +    root = et.parse(args.file).getroot()
>
> It would be nice to catch parse errors and print a human-readable
> message.
>
> > +    try:
> > +        lsc_node = find_lsc_table(root)
> > +    except Exception:
> > +        return 1
> > +
> > +    cells = lsc_node.findall("cell")
> > +
> > +    max_res = parse_max_res(cells)
> > +    if max_res == "":
> > +        return
> > +
> > +    lsc_cells = []
> > +    for cell in cells:
> > +        filter_cells(cell, max_res, lsc_cells)
> > +
> > +    lsc_section = '''  - LensShadingCorrection:
> > +      x-size: [ 0.0625, 0.0625, 0.0625, 0.0625, 0.0625, 0.0625, 0.0625, 0.0625 ]
> > +      y-size: [ 0.0625, 0.0625, 0.0625, 0.0625, 0.0625, 0.0625, 0.0625, 0.0625 ]
> > +      sets:
> > +'''
>
> Given that you're generating a rkisp1 tuning file, using the YamlOutput
> class from libtuning would make sense.
>

If we get to add more algorithms yes, it might make sense

> > +
> > +    for cell in lsc_cells:
> > +        lsc_section += print_cell(cell) + "\n"
> > +
> > +    print(lsc_section)
> > +
> > +
> > +if __name__ == '__main__':
> > +    sys.exit(main(sys.argv))
>
> --
> Regards,
>
> Laurent Pinchart
Laurent Pinchart March 7, 2023, 2:15 p.m. UTC | #4
Hi Jacopo,

On Tue, Mar 07, 2023 at 02:49:37PM +0100, Jacopo Mondi wrote:
> On Tue, Mar 07, 2023 at 11:49:23AM +0200, Laurent Pinchart via libcamera-devel wrote:
> > On Mon, Mar 06, 2023 at 06:24:38PM +0100, Jacopo Mondi via libcamera-devel wrote:
> > > Android ship a per-sensor configuration file in .xml format.
> >
> > s/ship/ships/
> >
> > > The .xml file contains a main <matfile> node and a <sensor> sub-node
> > > which contains an <LSC> entry. The LSC tables there contained can be
> > > re-used for libcamera, by parsing them opportunely.
> > >
> > > Add a script to utils/rkisp1/ to extract the LSC tables from Android
> > > configuration file for the RkISP1 platform, modeled after the
> > > requirements of the Rockchip closed source IQ tuning module.
> > >
> > > Compared to the Rockchip IQ LSC module the one implemented in libcamera
> > > is slightly simpler, and the parsing of the LSC table takes that into
> > > account by:
> > > - Only outputting tables for the larger found sensor resolution
> > > - Only outputting tables for "vignetting" value == 70 (ignoring the ones
> > >   for vignetting values of 100)
> > >
> > > The script outputs to stdout a "LensShadingCorrection" section that
> > > can be directly pasted in a libcamera sensor configuration file.
> >
> > Could we generate a full tuning file instead ? I would imagine that
> > other tuning data could be (later) extracted, it would be nice to
> > prepare for that.
> 
> I'm not sure about this.. how should the other algorithms already
> described in a config file be handled ? are we going to overwrite
> everything ?

I'd say the tool should output a full tuning file, skipping algorithms
that it doesn't handle, and then a human can pick individual algorithms
from the output file and copy&paste them to the target tuning file.

> > > Signed-off-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
> > > ---
> > >  utils/rkisp1/lsc_parse_android_config.py | 187 +++++++++++++++++++++++
> >
> > And we could already name the script in a more generic way, to hint that
> > it converts a Rockchip tuning file to a libcamera tuning file.
> >
> > Do you know if the XML format is Android-specific ?
> 
> I presume is specific to what I think is the Rockchip 3A closed source library.
> The IPU3 in example, uses a different (binary) format for the sensor
> tuning paramters. So I guess is the closed source 3A module that
> defines the format.

That's my guess too.

https://gitlab.com/firefly-linux/external/camera_engine_rkaiq/-/tree/rk3588/firefly
may provide some interesting information.

> So maybe yes, "android" should not be part of the name
> 
> lsc_IQ_to_libcamera.py ?

I'd go for rkisp1_aiq_to_libcamera.py (or with dashes instead of
underscores).

> > >  1 file changed, 187 insertions(+)
> > >  create mode 100755 utils/rkisp1/lsc_parse_android_config.py
> > >
> > > diff --git a/utils/rkisp1/lsc_parse_android_config.py b/utils/rkisp1/lsc_parse_android_config.py
> > > new file mode 100755
> > > index 000000000000..a7c2c160319d
> > > --- /dev/null
> > > +++ b/utils/rkisp1/lsc_parse_android_config.py
> > > @@ -0,0 +1,187 @@
> > > +#!/usr/bin/env python
> > > +
> > > +# SPDX-License-Identifier: GPL-2.0-or-later
> > > +# Copyright (C) 2023, Jacopo Mondi - Ideas on Board Oy
> > > +#
> > > +# Parse Android .xml configuration file and extract the LSC tables.
> > > +#
> > > +# Print to standard output a "LensShadingCorrection" section, understandable by
> > > +# libcamera LSC algorithm, that can be pasted to the sensor configuration file.
> > > +
> > > +import argparse
> > > +import string
> > > +import sys
> > > +import re
> >
> > Alphabetical order please.
> >
> > > +import xml.etree.ElementTree as et
> > > +
> > > +
> > > +def sanitize(name):
> > > +    return re.sub(r"[\n\t\s]*", "", name)
> >
> > We use single quotes for strings when there's no specific reason to do
> > otherwise.
> >
> > > +
> > > +
> > > +def split_table(table):
> > > +    values = ""
> > > +    for v in table.text.strip(' ').split():
> > > +        values += v.strip('[').strip(']') + ", "
> > > +    return values
> > > +
> > > +
> > > +def print_cell(cell):
> > > +    lsc_template = string.Template('''        #${name} - ${illuminant}
> > > +        - ct: ${ct}
> > > +          resolution: ${res}
> > > +          r: [${red}]
> > > +          gr: [${greenr}]
> > > +          gb: [${greenb}]
> > > +          b: [${blue}]''')
> > > +
> > > +    illuminant = cell.find("illumination")
> > > +    ct = illuminant_to_ct(illuminant)
> > > +
> > > +    template_dict = {
> > > +        'name': sanitize(cell.find("name").text),
> > > +        'illuminant': sanitize(illuminant.text),
> > > +        'ct': ct,
> > > +        'res': sanitize(cell.find("resolution").text)
> > > +    }
> > > +
> > > +    red_table = cell.find("LSC_SAMPLES_red")
> > > +    greenr_table = cell.find("LSC_SAMPLES_greenR")
> > > +    greenb_table = cell.find("LSC_SAMPLES_greenB")
> > > +    blue_table = cell.find("LSC_SAMPLES_blue")
> > > +
> > > +    if red_table is None or greenr_table is None or greenb_table is None or blue_table is None:
> > > +        return
> > > +
> > > +    template_dict['red'] = split_table(red_table)
> > > +    template_dict['greenr'] = split_table(greenr_table)
> > > +    template_dict['greenb'] = split_table(greenb_table)
> > > +    template_dict['blue'] = split_table(blue_table)
> > > +
> > > +    return lsc_template.substitute(template_dict)
> > > +
> > > +
> > > +def illuminant_to_ct(illuminant):
> > > +    # Standard CIE Illiminants to Color Temperature in Kelvin
> > > +    # https://en.wikipedia.org/wiki/Standard_illuminant
> > > +    #
> > > +    # Not included (and then ignored when parsing the configuration file):
> > > +    # - "Horizon" == D50 == 5003
> > > +    # - "BW" == ?
> > > +    # - "PREFLASH" == ?
> > > +    illuminants_dict = {
> > > +        'A': 2856,
> > > +        'D50': 5003,
> > > +        'D65': 6504,
> > > +        'D75': 7504,
> > > +        'F11_TL84': 4000,
> > > +        'F2_CWF': 4230,
> > > +    }
> > > +
> > > +    ill_key = sanitize(illuminant.text)
> > > +    try:
> > > +        ct = illuminants_dict[ill_key]
> > > +    except KeyError:
> > > +        return None
> > > +
> > > +    return ct
> > > +
> > > +
> > > +# Make sure the cell is well formed and return it
> > > +def filter_cells(cell, res, lsc_cells):
> > > +    name = cell.find("name")
> > > +    resolution = cell.find("resolution")
> > > +    illumination = cell.find("illumination")
> > > +    vignetting = cell.find("vignetting")
> > > +
> > > +    if name is None or resolution is None or \
> > > +       illumination is None or vignetting is None:
> > > +        return
> > > +
> > > +    # Skip tables for smaller sensor resolutions
> > > +    if res != sanitize(resolution.text):
> > > +        return
> > > +
> > > +    # Skip tables for which we don't know how to translate the illuminant value
> > > +    ct = illuminant_to_ct(illumination)
> > > +    if ct is None:
> > > +        return
> > > +
> > > +    # Only pick tables with vignetting == 70
> > > +    if sanitize(vignetting.text) != "[70]":
> > > +        return
> > > +
> > > +    lsc_cells.append(cell)
> > > +
> > > +
> > > +# Get the "LSC" node
> > > +def find_lsc_table(root):
> > > +    sensor = root.find('sensor')
> > > +    if sensor is None:
> > > +        print("Failed to find \"sensor\" node in config file")
> > > +        raise Exception
> >
> >         raise RuntimeError('Failed to find "sensor" node in config file')
> >
> > and print the message in the caller. Same below.
> >
> > > +
> > > +    lsc = sensor.find('LSC')
> > > +    if lsc is None:
> > > +        print("Filed to find \"LSC\" node in config file")
> > > +        raise Exception
> > > +
> > > +    return lsc
> > > +
> > > +# libcamera LSC algorithm only operates on a single resolution.
> > > +# Find the largest sensor mode among the ones reported in the LSC tables
> > > +
> > > +
> > > +def parse_max_res(cells):
> > > +    max_res = ""
> > > +    max_size = 0
> > > +
> > > +    for cell in cells:
> > > +        resolution = sanitize(cell.find("resolution").text)
> > > +        [w, h] = resolution.split('x')
> > > +
> > > +        area = int(w) * int(h)
> > > +        if area > max_size:
> > > +            max_res = resolution
> > > +
> > > +    return max_res
> > > +
> > > +
> > > +def main(argv):
> > > +    # Parse command line arguments.
> > > +    parser = argparse.ArgumentParser(
> > > +        description='Parse Android camera configuration file to extract LSC tables')
> > > +    parser.add_argument('--file', '-f', required=True,
> > > +                        help='Path to the Android .xml configuration file')
> >
> > It's quite traditional in command line tools to pass the input file as a
> > positional argument instead of a named argument. Up to you.
> >
> > > +    args = parser.parse_args(argv[1:])
> > > +
> > > +    root = et.parse(args.file).getroot()
> >
> > It would be nice to catch parse errors and print a human-readable
> > message.
> >
> > > +    try:
> > > +        lsc_node = find_lsc_table(root)
> > > +    except Exception:
> > > +        return 1
> > > +
> > > +    cells = lsc_node.findall("cell")
> > > +
> > > +    max_res = parse_max_res(cells)
> > > +    if max_res == "":
> > > +        return
> > > +
> > > +    lsc_cells = []
> > > +    for cell in cells:
> > > +        filter_cells(cell, max_res, lsc_cells)
> > > +
> > > +    lsc_section = '''  - LensShadingCorrection:
> > > +      x-size: [ 0.0625, 0.0625, 0.0625, 0.0625, 0.0625, 0.0625, 0.0625, 0.0625 ]
> > > +      y-size: [ 0.0625, 0.0625, 0.0625, 0.0625, 0.0625, 0.0625, 0.0625, 0.0625 ]
> > > +      sets:
> > > +'''
> >
> > Given that you're generating a rkisp1 tuning file, using the YamlOutput
> > class from libtuning would make sense.
> 
> If we get to add more algorithms yes, it might make sense
> 
> > > +
> > > +    for cell in lsc_cells:
> > > +        lsc_section += print_cell(cell) + "\n"
> > > +
> > > +    print(lsc_section)
> > > +
> > > +
> > > +if __name__ == '__main__':
> > > +    sys.exit(main(sys.argv))

Patch
diff mbox series

diff --git a/utils/rkisp1/lsc_parse_android_config.py b/utils/rkisp1/lsc_parse_android_config.py
new file mode 100755
index 000000000000..a7c2c160319d
--- /dev/null
+++ b/utils/rkisp1/lsc_parse_android_config.py
@@ -0,0 +1,187 @@ 
+#!/usr/bin/env python
+
+# SPDX-License-Identifier: GPL-2.0-or-later
+# Copyright (C) 2023, Jacopo Mondi - Ideas on Board Oy
+#
+# Parse Android .xml configuration file and extract the LSC tables.
+#
+# Print to standard output a "LensShadingCorrection" section, understandable by
+# libcamera LSC algorithm, that can be pasted to the sensor configuration file.
+
+import argparse
+import string
+import sys
+import re
+import xml.etree.ElementTree as et
+
+
+def sanitize(name):
+    return re.sub(r"[\n\t\s]*", "", name)
+
+
+def split_table(table):
+    values = ""
+    for v in table.text.strip(' ').split():
+        values += v.strip('[').strip(']') + ", "
+    return values
+
+
+def print_cell(cell):
+    lsc_template = string.Template('''        #${name} - ${illuminant}
+        - ct: ${ct}
+          resolution: ${res}
+          r: [${red}]
+          gr: [${greenr}]
+          gb: [${greenb}]
+          b: [${blue}]''')
+
+    illuminant = cell.find("illumination")
+    ct = illuminant_to_ct(illuminant)
+
+    template_dict = {
+        'name': sanitize(cell.find("name").text),
+        'illuminant': sanitize(illuminant.text),
+        'ct': ct,
+        'res': sanitize(cell.find("resolution").text)
+    }
+
+    red_table = cell.find("LSC_SAMPLES_red")
+    greenr_table = cell.find("LSC_SAMPLES_greenR")
+    greenb_table = cell.find("LSC_SAMPLES_greenB")
+    blue_table = cell.find("LSC_SAMPLES_blue")
+
+    if red_table is None or greenr_table is None or greenb_table is None or blue_table is None:
+        return
+
+    template_dict['red'] = split_table(red_table)
+    template_dict['greenr'] = split_table(greenr_table)
+    template_dict['greenb'] = split_table(greenb_table)
+    template_dict['blue'] = split_table(blue_table)
+
+    return lsc_template.substitute(template_dict)
+
+
+def illuminant_to_ct(illuminant):
+    # Standard CIE Illiminants to Color Temperature in Kelvin
+    # https://en.wikipedia.org/wiki/Standard_illuminant
+    #
+    # Not included (and then ignored when parsing the configuration file):
+    # - "Horizon" == D50 == 5003
+    # - "BW" == ?
+    # - "PREFLASH" == ?
+    illuminants_dict = {
+        'A': 2856,
+        'D50': 5003,
+        'D65': 6504,
+        'D75': 7504,
+        'F11_TL84': 4000,
+        'F2_CWF': 4230,
+    }
+
+    ill_key = sanitize(illuminant.text)
+    try:
+        ct = illuminants_dict[ill_key]
+    except KeyError:
+        return None
+
+    return ct
+
+
+# Make sure the cell is well formed and return it
+def filter_cells(cell, res, lsc_cells):
+    name = cell.find("name")
+    resolution = cell.find("resolution")
+    illumination = cell.find("illumination")
+    vignetting = cell.find("vignetting")
+
+    if name is None or resolution is None or \
+       illumination is None or vignetting is None:
+        return
+
+    # Skip tables for smaller sensor resolutions
+    if res != sanitize(resolution.text):
+        return
+
+    # Skip tables for which we don't know how to translate the illuminant value
+    ct = illuminant_to_ct(illumination)
+    if ct is None:
+        return
+
+    # Only pick tables with vignetting == 70
+    if sanitize(vignetting.text) != "[70]":
+        return
+
+    lsc_cells.append(cell)
+
+
+# Get the "LSC" node
+def find_lsc_table(root):
+    sensor = root.find('sensor')
+    if sensor is None:
+        print("Failed to find \"sensor\" node in config file")
+        raise Exception
+
+    lsc = sensor.find('LSC')
+    if lsc is None:
+        print("Filed to find \"LSC\" node in config file")
+        raise Exception
+
+    return lsc
+
+# libcamera LSC algorithm only operates on a single resolution.
+# Find the largest sensor mode among the ones reported in the LSC tables
+
+
+def parse_max_res(cells):
+    max_res = ""
+    max_size = 0
+
+    for cell in cells:
+        resolution = sanitize(cell.find("resolution").text)
+        [w, h] = resolution.split('x')
+
+        area = int(w) * int(h)
+        if area > max_size:
+            max_res = resolution
+
+    return max_res
+
+
+def main(argv):
+    # Parse command line arguments.
+    parser = argparse.ArgumentParser(
+        description='Parse Android camera configuration file to extract LSC tables')
+    parser.add_argument('--file', '-f', required=True,
+                        help='Path to the Android .xml configuration file')
+    args = parser.parse_args(argv[1:])
+
+    root = et.parse(args.file).getroot()
+    try:
+        lsc_node = find_lsc_table(root)
+    except Exception:
+        return 1
+
+    cells = lsc_node.findall("cell")
+
+    max_res = parse_max_res(cells)
+    if max_res == "":
+        return
+
+    lsc_cells = []
+    for cell in cells:
+        filter_cells(cell, max_res, lsc_cells)
+
+    lsc_section = '''  - LensShadingCorrection:
+      x-size: [ 0.0625, 0.0625, 0.0625, 0.0625, 0.0625, 0.0625, 0.0625, 0.0625 ]
+      y-size: [ 0.0625, 0.0625, 0.0625, 0.0625, 0.0625, 0.0625, 0.0625, 0.0625 ]
+      sets:
+'''
+
+    for cell in lsc_cells:
+        lsc_section += print_cell(cell) + "\n"
+
+    print(lsc_section)
+
+
+if __name__ == '__main__':
+    sys.exit(main(sys.argv))