[libcamera-devel] [PATCH 1/3] utils: rkisp1: Add script to extract LSC tables from Android

Laurent Pinchart laurent.pinchart at ideasonboard.com
Tue Mar 7 11:12:05 CET 2023


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

-- 
Regards,

Laurent Pinchart


More information about the libcamera-devel mailing list