[libcamera-devel] [PATCH] utils: rkisp1: Add script to generate CSC coefficients

paul.elder at ideasonboard.com paul.elder at ideasonboard.com
Thu Aug 25 23:11:55 CEST 2022


On Thu, Aug 25, 2022 at 04:10:04PM -0500, Paul Elder via libcamera-devel wrote:
> Hi Laurent,
> 
> On Mon, Aug 08, 2022 at 06:14:49AM +0300, Laurent Pinchart via libcamera-devel wrote:
> > This script generates fixed-point integer coefficients for the YCbCr
> > encoding 3x3 matrix. The encoding, quantization and fixed-point
> > precision can be selected through command line arguments.
> > 
> > The main purpose of the script is to generate coefficient tables to
> > extend the rkisp1 driver with support for additional YCbCr encodings,
> > but it may be useful for other purposes as well given that the rounding
> > isn't trivial.
> > 
> > The Rec. 601 full and limited range coefficients have been verified to
> > match the values currently used by the rkisp1 driver.
> > 
> > Signed-off-by: Laurent Pinchart <laurent.pinchart at ideasonboard.com>
> 
> Reviewed-by: Paul Elder <paul.elder at ideasonboard.com>
> 
> > ---
> >  utils/rkisp1/gen-csc-table.py | 204 ++++++++++++++++++++++++++++++++++
> >  1 file changed, 204 insertions(+)
> >  create mode 100755 utils/rkisp1/gen-csc-table.py
> > 
> > diff --git a/utils/rkisp1/gen-csc-table.py b/utils/rkisp1/gen-csc-table.py
> > new file mode 100755
> > index 000000000000..a966a31191ed
> > --- /dev/null
> > +++ b/utils/rkisp1/gen-csc-table.py
> > @@ -0,0 +1,204 @@
> > +#!/usr/bin/python3
> > +# SPDX-License-Identifier: GPL-2.0-or-later
> > +# Copyright (C) 2022, Ideas on Board Oy
> > +#
> > +# Generate color space conversion table coefficients with configurable
> > +# fixed-point precision
> > +
> > +import argparse
> > +import enum
> > +import sys
> > +
> > +
> > +encodings = {
> > +    'rec601': [
> > +        [  0.2990,  0.5870,  0.1140 ],
> > +        [ -0.1687, -0.3313,  0.5    ],
> > +        [  0.5,    -0.4187, -0.0813 ]
> > +    ],
> > +    'rec709': [
> > +        [  0.2126,  0.7152,  0.0722 ],
> > +        [ -0.1146, -0.3854,  0.5    ],
> > +        [  0.5,    -0.4542, -0.0458 ]
> > +    ],
> > +    'rec2020': [
> > +        [  0.2627,  0.6780,  0.0593 ],
> > +        [ -0.1396, -0.3604,  0.5    ],
> > +        [  0.5,    -0.4598, -0.0402 ]
> > +    ],
> > +    'smpte240m': [
> > +        [  0.2122,  0.7013,  0.0865 ],
> > +        [ -0.1161, -0.3839,  0.5    ],
> > +        [  0.5,    -0.4451, -0.0549 ]
> > +    ],
> > +}
> > +
> > +
> > +class Precision(object):
> > +    def __init__(self, precision):
> > +        if precision[0].upper() != 'Q':
> > +            raise RuntimeError(f'Invalid precision `{precision}`')
> > +        prec = precision[1:].split('.')
> > +        if len(prec) != 2:
> > +            raise RuntimeError(f'Invalid precision `{precision}`')
> > +
> > +        self.__prec = [int(v) for v in prec]
> > +
> > +    @property
> > +    def integer(self):
> > +        return self.__prec[0]
> > +
> > +    @property
> > +    def fractional(self):
> > +        return self.__prec[1]
> > +
> > +    @property
> > +    def total(self):
> > +        # Add 1 for the sign bit
> > +        return self.__prec[0] + self.__prec[1] + 1
> > +
> > +
> > +class Quantization(enum.Enum):
> > +    FULL = 0
> > +    LIMITED = 1
> > +
> > +
> > +def scale_coeff(coeff, quantization, luma, precision):
> > +    """Scale a coefficient to the output range dictated by the quantization and
> > +    the precision.
> > +
> > +    Parameters
> > +    ----------
> > +    coeff : float
> > +        The CSC matrix coefficient to scale
> > +    quantization : Quantization
> > +        The quantization, either FULL or LIMITED
> > +    luma : bool
> > +        True if the coefficient corresponds to a luma value, False otherwise
> > +    precision : int
> > +        The desired precision for the scaled coefficient as a number of
> > +        fractional bits
> > +    """
> > +
> > +    # Assume the input range is 8 bits. The output range is set by the
> > +    # quantization and differs between luma and chrome components for limited
> > +    # range.
> > +    in_range = 255 - 0
> > +    if quantization == Quantization.FULL:
> > +        out_range = 255 - 0
> > +    elif luma:
> > +        out_range = 235 - 16
> > +    else:
> > +        out_range = 240 - 16
> > +
> > +    return coeff * out_range / in_range * (1 << precision)
> > +
> > +
> > +def round_array(values):
> > +    """Round a list of signed floating point values to the closest integer while
> > +    preserving the (rounded) value of the sum of all elements.
> > +    """
> > +
> > +    # Calculate the rounding error as the difference between the rounded sum of
> > +    # values and the sum of rounded values. This is by definition an integer
> > +    # (positive or negative), which indicates how many values will need to be
> > +    # 'flipped' to the opposite rounding.
> > +    rounded_values = [round(value) for value in values]
> > +    sum_values = round(sum(values))
> > +    sum_error = sum_values - sum(rounded_values)
> > +
> > +    if sum_error == 0:
> > +        return rounded_values
> > +
> > +    # The next step is to distribute the error among the values, in a way that
> > +    # will minimize the relative error introduced in individual values. We
> > +    # extend the values list with the rounded value and original index for each
> > +    # element, and sort by rounding error. Then we modify the elements with the
> > +    # highest or lowest error, depending on whether the sum error is negative
> > +    # or positive.
> > +
> > +    values = [[value, round(value), index] for index, value in enumerate(values)]
> > +    values.sort(key=lambda v: v[1] - v[0])
> > +
> > +    # It could also be argued that the key for the sort order should not be the
> > +    # absolute rouding error but the relative error, as the impact of identical
> > +    # rounding errors will differ for coefficients with widely different values.
> > +    # This is a topic for further research.
> > +    #
> > +    # values.sort(key=lambda v: (v[1] - v[0]) / abs(v[0]))
> > +
> > +    if sum_error > 0:
> > +        for i in range(sum_error):
> > +            values[i][1] += 1
> > +    else:
> > +        for i in range(-sum_error):
> > +            values[len(values) - i - 1][1] -= 1
> > +
> > +    # Finally, sort back by index, make sure the total rounding error is now 0,
> > +    # and return the rounded values.
> > +    values.sort(key=lambda v: v[2])
> > +    values = [value[1] for value in values]
> > +    assert(sum(values) == sum_values)
> > +
> > +    return values
> > +
> > +
> > +def main(argv):
> > +
> > +    # Parse command line arguments.
> > +    parser = argparse.ArgumentParser(
> > +        'Generate color space conversion table coefficients with configurable '
> > +        'fixed-point precision'
> > +    )
> > +    parser.add_argument('--precision', '-p', default='Q1.7',
> > +                        help='The output fixed point precision in Q notation')

Oh wait, I was going to mention; should you mention here that the Q
notation m bit does not include the bit for the sign?


Paul

> > +    parser.add_argument('--quantization', '-q', choices=['full', 'limited'],
> > +                        default='limited', help='Quantization range')
> > +    parser.add_argument('encoding', choices=encodings.keys(), help='YCbCr encoding')
> > +    args = parser.parse_args(argv[1:])
> > +
> > +    try:
> > +        precision = Precision(args.precision)
> > +    except Exception:
> > +        print(f'Invalid precision `{args.precision}`')
> > +        return 1
> > +
> > +    encoding = encodings[args.encoding]
> > +    quantization = Quantization[args.quantization.upper()]
> > +
> > +    # Scale and round the encoding coefficients based on the precision and
> > +    # quantization range.
> > +    luma = True
> > +    scaled_coeffs = []
> > +    for line in encoding:
> > +        line = [scale_coeff(coeff, quantization, luma, precision.fractional) for coeff in line]
> > +        scaled_coeffs.append(line)
> > +        luma = False
> > +
> > +    rounded_coeffs = []
> > +    for line in scaled_coeffs:
> > +        line = round_array(line)
> > +
> > +        # Convert coefficients to the number of bits selected by the precision.
> > +        # Negative values will be turned into positive integers using 2's
> > +        # complement.
> > +        line = [coeff & ((1 << precision.total) - 1) for coeff in line]
> > +        rounded_coeffs.append(line)
> > +
> > +    # Print the result as C code.
> > +    nbits = 1 << (precision.total - 1).bit_length()
> > +    nbytes = nbits // 4
> > +    print(f'static const u{nbits} rgb2yuv_{args.encoding}_{quantization.name.lower()}_coeffs[] = {{')
> > +
> > +    for line in rounded_coeffs:
> > +        line = [f'0x{coeff:0{nbytes}x}' for coeff in line]
> > +
> > +        print(f'\t{", ".join(line)},')
> > +
> > +    print('};')
> > +
> > +    return 0
> > +
> > +
> > +if __name__ == '__main__':
> > +    sys.exit(main(sys.argv))


More information about the libcamera-devel mailing list