From patchwork Mon Oct 26 10:58:09 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Milan Zamazal X-Patchwork-Id: 28610 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id 16326C324C for ; Mon, 26 Oct 2026 10:58:24 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id D4CC068FD2; Mon, 26 Oct 2026 11:58:23 +0100 (CET) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=redhat.com header.i=@redhat.com header.b="VuXwFtz1"; dkim-atps=neutral Received: from us-smtp-delivery-124.mimecast.com (us-smtp-delivery-124.mimecast.com [170.10.129.124]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 2788068FC1 for ; Mon, 26 Oct 2026 11:58:21 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com; s=mimecast20190719; t=1793012300; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=ntOgX2ogSkdqbDcmu8JmIj1T8AElzoAhzNPEyzlRN/g=; b=VuXwFtz1kGYGkC2jIB1fD2jTYqh4cjY0COh//sPjxXYW5TudOdUZIDGXJY7EIh0YKhU2q6 dtqQ/fk//BgE8GRGR/gMinYODEUr1tk0nFyrkmG6vUoinpE7jx+cgr1NpAQhv8Q10hThb/ V9AQFpWrkmC94SQXS9eBX1ObqNHtcoo= Received: from mx-prod-mc-06.mail-002.prod.us-west-2.aws.redhat.com (ec2-35-165-154-97.us-west-2.compute.amazonaws.com [35.165.154.97]) by relay.mimecast.com with ESMTP with STARTTLS (version=TLSv1.3, cipher=TLS_AES_256_GCM_SHA384) id us-mta-513-98me2RGjPXayKAQA_Lr9Gg-1; Mon, 26 Oct 2026 06:58:18 -0400 X-MC-Unique: 98me2RGjPXayKAQA_Lr9Gg-1 X-Mimecast-MFC-AGG-ID: 98me2RGjPXayKAQA_Lr9Gg_1793012297 Received: from mx-prod-int-06.mail-002.prod.us-west-2.aws.redhat.com (mx-prod-int-06.mail-002.prod.us-west-2.aws.redhat.com [10.30.177.93]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by mx-prod-mc-06.mail-002.prod.us-west-2.aws.redhat.com (Postfix) with ESMTPS id 61AF518005BD for ; Mon, 26 Oct 2026 10:58:17 +0000 (UTC) Received: from mzamazal-thinkpadp1gen7.tpbc.com (unknown [10.43.17.75]) by mx-prod-int-06.mail-002.prod.us-west-2.aws.redhat.com (Postfix) with ESMTP id 67D531800347; Mon, 26 Oct 2026 10:58:16 +0000 (UTC) From: Milan Zamazal To: libcamera-devel@lists.libcamera.org Cc: Milan Zamazal Subject: [RFC PATCH 1/1] utils: tuning: Add softisp script Date: Mon, 26 Oct 2026 11:58:09 +0100 Message-ID: <20261026105811.15657-2-mzamazal@redhat.com> In-Reply-To: <20261026105811.15657-1-mzamazal@redhat.com> References: <20261026105811.15657-1-mzamazal@redhat.com> MIME-Version: 1.0 X-Scanned-By: MIMEDefang 3.4.1 on 10.30.177.93 X-Mimecast-Spam-Score: 0 X-Mimecast-MFC-PROC-ID: psb0cK78JLhJrlKZt7Il-FxeWgnHjYMWtQJCQaSwoVA_1793012297 X-Mimecast-Originator: redhat.com content-type: text/plain; charset="US-ASCII"; x-default=true X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" This is an initial version of the script, generating just the tuning data for Lsc algorithm. Signed-off-by: Milan Zamazal --- .../tuning/libtuning/modules/lsc/__init__.py | 3 +- utils/tuning/libtuning/modules/lsc/softisp.py | 105 ++++++++++++++++++ utils/tuning/softisp.py | 42 +++++++ 3 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 utils/tuning/libtuning/modules/lsc/softisp.py create mode 100755 utils/tuning/softisp.py diff --git a/utils/tuning/libtuning/modules/lsc/__init__.py b/utils/tuning/libtuning/modules/lsc/__init__.py index 0ba4411b4..0b7d67f0d 100644 --- a/utils/tuning/libtuning/modules/lsc/__init__.py +++ b/utils/tuning/libtuning/modules/lsc/__init__.py @@ -1,7 +1,8 @@ # SPDX-License-Identifier: GPL-2.0-or-later # -# Copyright (C) 2022, Paul Elder +# Copyright (C) 2022, 2026, Paul Elder from libtuning.modules.lsc.lsc import LSC from libtuning.modules.lsc.raspberrypi import ALSCRaspberryPi from libtuning.modules.lsc.rkisp1 import LSCRkISP1 +from libtuning.modules.lsc.softisp import LSCSoftisp diff --git a/utils/tuning/libtuning/modules/lsc/softisp.py b/utils/tuning/libtuning/modules/lsc/softisp.py new file mode 100644 index 000000000..42437083c --- /dev/null +++ b/utils/tuning/libtuning/modules/lsc/softisp.py @@ -0,0 +1,105 @@ +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (C) 2019, Raspberry Pi Ltd +# Copyright (C) 2022, Paul Elder +# +# LSC module for tuning softisp + +from .lsc import LSC + +import libtuning as lt +import libtuning.utils as utils + +from numbers import Number +import numpy as np + + +class LSCSoftisp(LSC): + hr_name = 'LSC (Softisp)' + out_name = 'Lsc' + # \todo Not sure if this is useful. Probably will remove later. + compatible = ['softisp'] + + def __init__(self, *args, **kwargs): + super().__init__(**kwargs) + + # We don't actually need anything from the config file + def validate_config(self, config: dict) -> bool: + return True + + # @return Image color temperature, flattened array of red calibration table + # (containing {sector size} elements), flattened array of red + # calibration table, flattened array of green calibration + # table, flattened array of blue calibration table + + def _do_single_lsc(self, image: lt.Image): + # Perform LSC on each colour channel independently. A future enhancement + # worth investigating would be splitting the luminance and chrominance + # LSC as done by Raspberry Pi. + cr, _ = self._lsc_single_channel(image.channels[lt.Color.R], image) + cgr, _ = self._lsc_single_channel(image.channels[lt.Color.GR], image) + cgb, _ = self._lsc_single_channel(image.channels[lt.Color.GR], image) + cb, _ = self._lsc_single_channel(image.channels[lt.Color.B], image) + cg = (cgr + cgb) / 2 + + return image.color, cr.flatten(), cg.flatten(), cb.flatten() + + # @return List of dictionaries of color temperature, red table, green + # table and blue table + + def _do_all_lsc(self, images: list) -> list: + output_list = [] + output_map_func = lt.gradient.Linear().map + + # List of colour temperatures + list_col = [] + # Associated calibration tables + list_cr = [] + list_cg = [] + list_cb = [] + for image in self._enumerate_lsc_images(images): + col, cr, cg, cb = self._do_single_lsc(image) + list_col.append(col) + list_cr.append(cr) + list_cg.append(cg) + list_cb.append(cb) + + # Convert to numpy array for data manipulation + list_col = np.array(list_col) + list_cr = np.array(list_cr) + list_cg = np.array(list_cg) + list_cb = np.array(list_cb) + + for color_temperature in sorted(set(list_col)): + # Average tables for the same colour temperature + indices = np.where(list_col == color_temperature) + color_temperature = int(color_temperature) + + tables = [] + for lis in [list_cr, list_cg, list_cb]: + table = np.mean(lis[indices], axis=0) + table = output_map_func((1, 3), (1, 255), table) + table = np.clip(table, 1, 255) + table = np.round(table).astype('int32').tolist() + tables.append(table) + + entry = { + 'ct': color_temperature, + 'r': tables[0], + 'g': tables[1], + 'b': tables[2], + } + + output_list.append(entry) + + return output_list + + def process(self, config: dict, images: list, outputs: dict) -> dict: + output = {} + + output['sets'] = self._do_all_lsc(images) + + if len(output['sets']) == 0: + return None + + return output diff --git a/utils/tuning/softisp.py b/utils/tuning/softisp.py new file mode 100755 index 000000000..cf8b21555 --- /dev/null +++ b/utils/tuning/softisp.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-or-later +# +# Copyright (C) 2022, Paul Elder +# Copyright (C) 2024, Ideas On Board +# +# Tuning script for software ISP + +import logging +import sys + +import coloredlogs +import libtuning as lt +from libtuning.generators import YamlOutput +from libtuning.modules.lsc import LSCSoftisp +from libtuning.parsers import YamlParser + +coloredlogs.install(level=logging.INFO, fmt='%(name)s %(levelname)s %(message)s') + +lsc = LSCSoftisp(debug=[lt.Debug.Plot], + sector_shape=(17, 17), + + sector_x_gradient=lt.gradient.Linear(lt.Remainder.DistributeFront), + sector_y_gradient=lt.gradient.Linear(lt.Remainder.DistributeFront), + + # This is the function that will be used to average the pixels in + # each sector. This can also be a custom function. + sector_average_function=lt.average.Mean(), + + # This is the function that will be used to smooth the color ratio + # values. This can also be a custom function. + smoothing_function=lt.smoothing.MedianBlur(3),) + +tuner = lt.Tuner('Softisp') +tuner.add([lsc]) +tuner.set_input_parser(YamlParser()) +tuner.set_output_formatter(YamlOutput()) + +tuner.set_output_order([lsc]) + +if __name__ == '__main__': + sys.exit(tuner.run(sys.argv))