[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:10:04 CEST 2022
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')
> + 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