From patchwork Wed Sep 9 07:01:45 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: "D. Manresa" X-Patchwork-Id: 28212 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 5EB6DC3351 for ; Wed, 9 Sep 2026 07:01:51 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 6751F6863F; Wed, 9 Sep 2026 09:01:49 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (2048-bit key; unprotected) header.d=gmail.com header.i=@gmail.com header.b="LOoKU//D"; dkim-atps=neutral Received: from mail-wr1-x429.google.com (mail-wr1-x429.google.com [IPv6:2a00:1450:4864:20::429]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 37161685E3 for ; Wed, 9 Sep 2026 09:01:47 +0200 (CEST) Received: by mail-wr1-x429.google.com with SMTP id ffacd0b85a97d-48441a2ba1bso3963385f8f.1 for ; Wed, 09 Sep 2026 00:01:47 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1788937307; x=1789542107; darn=lists.libcamera.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to:content-type; bh=vIq1VDmi0VpwWIBWALWPFGBAYaXrn8kNGAgkOoVzVyc=; b=LOoKU//DNArTH9ZbNVPJFMxq0UUMBoRhUOsKv0EW3iYJhAolBomtLYjYj7m3TmwNqz KK+cjezjlMEcQq3lURHpVfuRpZ6sdx7HwZf5qCNTVngf0rGiP2HMjPAxbipx/kvYaaDe tYNEXMHpbj2DdAkTxA5wETLHQW/+/OMNUKpJjw5fUq4SWAtVMBTcxlWsxF0O9aXb+MEv 7XHqdFMksPZJbb1Xs8uQrB0mnDZjIDkjV9YdGcn4OxA9pGozK3Ni0urDfHQXnBCPhcmb eyE3Zz/xQf6olky05/McCLp7haSke6SvVhbgbC4dbEtHRrkvVMAA2KJ/qUfrN1AuQwgI Fyog== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1788937307; x=1789542107; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to:content-type; bh=vIq1VDmi0VpwWIBWALWPFGBAYaXrn8kNGAgkOoVzVyc=; b=e7DKAroX8PtaZhQDITng/wuniZATgL3DmWUA7hPzlUg3HOr1dDl9qd9V7rKD13zGFS YFZgE3e8LVAuDg+oSi9YCW+RaCraGpAH2vRIE6ltJfnPFbQZ+d/NVZOKR8irWTkQYZFu LIsipNvFxQ5+gm3r4vL6a2O+Mvoxc8Efe3EjkkiCiOcT/IBAjTA/0SH+QL+5kdC5qYRP w5hsK8A3dmIjts7qsFGrauguzEVp0rJoWT2kfIimmOMmzkiIZlNYibXPbmjW7patzIMh XVObhug7IOUwjB7INVcpzGhEDdAhA2dQBpJ6J4gZN35TYMaNiud9BDoT/U2DHaPtUVvM LnAA== X-Gm-Message-State: AFuF++nn59h0F1MshmbhXx7qUnMVso+hXTRb3h83JKLmmNCpm9ISM8rV N0uEgeLFy/cq7YIxwq5dZLEao1+L+5lEkIzBHQyO+X5Wl0XdvKfb2sLW1GjBMaKfTA== X-Gm-Gg: AYBFou1Aj9jEr1/GV5cxiSqZlW1auaa1mDCuzR5WOIOKHFxjC13Tj6QpPGmzC+96gw8 MNN2vob9hAzJ+hZmao/PCqsajTpgpVbZrYYIE9WdhP2o0g3OsyzEWuakXNQx0i/7UbnOb5CwB0z lxraTY6sSvkQF4kJusBA+8x2gZQ3G7NUqPPgUsV2WFlA7Wjp0jjm12ygyQ0xazsE0Fm6PK/zpI+ SQfG66nz/Ts5/iJe6O5B7xAwBwWRUJU1uc3fCjOWgK1+6NT+WaLG6OkF2ei7aTvbe3ak0bEZmT2 1nc8bafD83Q3O7TgCtO1gwdL6Nfoo6/EWmVKwHh2Kzaho5phpSefcLLLZn7BfJoPaxF242NiWUY q74MKbwCUH4w1yIwIP7BXTlmkmsSX7a7SCZSGk+PzupkB3eFhYZNr77+3R/NEY77omL4SmbI2rf bHsFIkE4pyq57hdRolKPuqe47+Ye9IcVHXxu75G2U2ZaSKnlc53MbWNjX6a+5HidMRVEJvnocwu fPM3QmssK1CtmcJnRxMTmE1PI6/vsYAGtlrnf3Zm859uZs2LGEZRcT14zM= X-Received: by 2002:a05:6000:1847:b0:485:a517:62cc with SMTP id ffacd0b85a97d-485a5176e8amr10534960f8f.2.1788937306109; Wed, 09 Sep 2026 00:01:46 -0700 (PDT) Received: from latitude7275 (84.124.213.91.dyn.user.ono.com. [84.124.213.91]) by smtp.gmail.com with ESMTPSA id ffacd0b85a97d-48594172546sm33523472f8f.15.2026.09.09.00.01.45 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 09 Sep 2026 00:01:45 -0700 (PDT) From: "D. Manresa" To: libcamera-devel@lists.libcamera.org Cc: Daniel Scally Subject: [PATCH 1/2] ipa: ipu3: Add a Saturation algorithm Date: Wed, 9 Sep 2026 09:01:45 +0200 Message-ID: <20260909070146.18825-2-dmanresa@gmail.com> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20260909070146.18825-1-dmanresa@gmail.com> References: <20260909070146.18825-1-dmanresa@gmail.com> MIME-Version: 1.0 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" The Total Color Correction (TCC) block of the ImgU applies a chroma gain that depends on the pixel luminance. When the block is not programmed the ImgU driver enables it with a gain table that boosts the chroma by up to 1.5 in the mid-tones, and there is currently no way to change that. Add a Saturation algorithm that programs the TCC block with the driver defaults and scales the gain table by a saturation factor: 1.0 keeps the driver behaviour, 0.0 gives a greyscale image and larger values increase the saturation. The default comes from the optional 'saturation' tuning parameter and can be changed at runtime with the Saturation control, which is also reported in the metadata. Enable the algorithm in the uncalibrated tuning file; with the default value the output is unchanged. Measured on a Dell Latitude 7275 (OV5670, 1280x720 NV12, same scene): the mean chroma |U-128|+|V-128| is 0.00, 7.97 and 15.83 for saturation 0.0, 1.0 and 2.0. Signed-off-by: D. Manresa --- src/ipa/ipu3/algorithms/meson.build | 1 + src/ipa/ipu3/algorithms/saturation.cpp | 199 +++++++++++++++++++++++++ src/ipa/ipu3/algorithms/saturation.h | 43 ++++++ src/ipa/ipu3/data/uncalibrated.yaml | 1 + src/ipa/ipu3/ipa_context.h | 9 ++ 5 files changed, 253 insertions(+) create mode 100644 src/ipa/ipu3/algorithms/saturation.cpp create mode 100644 src/ipa/ipu3/algorithms/saturation.h diff --git a/src/ipa/ipu3/algorithms/meson.build b/src/ipa/ipu3/algorithms/meson.build index 70177f5..833583e 100644 --- a/src/ipa/ipu3/algorithms/meson.build +++ b/src/ipa/ipu3/algorithms/meson.build @@ -7,5 +7,6 @@ ipu3_ipa_algorithms = files([ 'blc.cpp', 'ccm.cpp', 'lsc.cpp', + 'saturation.cpp', 'tone_mapping.cpp', ]) diff --git a/src/ipa/ipu3/algorithms/saturation.cpp b/src/ipa/ipu3/algorithms/saturation.cpp new file mode 100644 index 0000000..479291d --- /dev/null +++ b/src/ipa/ipu3/algorithms/saturation.cpp @@ -0,0 +1,199 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2026, D. Manresa + * + * IPU3 Saturation control + */ + +#include "saturation.h" + +#include +#include +#include + +#include + +#include + +#include "libcamera/internal/value_node.h" + +/** + * \file saturation.h + */ + +namespace libcamera { + +namespace ipa::ipu3::algorithms { + +/** + * \class Saturation + * \brief Control of the colour saturation through the ImgU TCC block + * + * The Total Color Correction (TCC) block of the ImgU applies a chroma gain + * that depends on the pixel luminance. When the block is not programmed, the + * ImgU driver enables it with a gain look-up table that boosts the chroma by + * up to 1.5 in the mid-tones. This algorithm programs the block with the same + * default configuration and scales the gain table by the requested + * saturation, so that a value of 1.0 reproduces the driver defaults, 0.0 + * gives a greyscale image and larger values increase the saturation. + * + * The default saturation is taken from the optional \a saturation tuning + * parameter (1.0 if absent) and can be changed at runtime with the + * controls::Saturation control. + */ + +LOG_DEFINE_CATEGORY(IPU3Saturation) + +namespace { + +constexpr double kMinSaturation = 0.0; +constexpr double kMaxSaturation = 2.0; +constexpr uint16_t kMaxGain = 4095; + +/* imgu_css_tcc_gain_pcwl_lut from the ImgU driver (258 entries) */ +static const uint16_t kDefaultGainPcwl[IPU3_UAPI_YUVP2_TCC_GAIN_PCWL_LUT_ELEMENTS] = { + 1024, 1032, 1040, 1048, 1057, 1065, 1073, 1081, 1089, 1097, 1105, 1113, + 1122, 1130, 1138, 1146, 1154, 1162, 1170, 1178, 1187, 1195, 1203, 1211, + 1219, 1227, 1235, 1243, 1252, 1260, 1268, 1276, 1284, 1292, 1300, 1308, + 1317, 1325, 1333, 1341, 1349, 1357, 1365, 1373, 1382, 1390, 1398, 1406, + 1414, 1422, 1430, 1438, 1447, 1455, 1463, 1471, 1479, 1487, 1495, 1503, + 1512, 1520, 1528, 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, + 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, + 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, + 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, + 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, + 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, + 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, + 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, + 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, + 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, + 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, 1536, + 1536, 1536, 1528, 1520, 1512, 1503, 1495, 1487, 1479, 1471, 1463, 1455, + 1447, 1438, 1430, 1422, 1414, 1406, 1398, 1390, 1382, 1373, 1365, 1357, + 1349, 1341, 1333, 1325, 1317, 1308, 1300, 1292, 1284, 1276, 1268, 1260, + 1252, 1243, 1235, 1227, 1219, 1211, 1203, 1195, 1187, 1178, 1170, 1162, + 1154, 1146, 1138, 1130, 1122, 1113, 1105, 1097, 1089, 1081, 1073, 1065, + 1057, 1048, 1040, 1032, 1024 +}; + +/* imgu_css_tcc_r_sqr_lut from the ImgU driver (24 entries, last one 0) */ +static const int16_t kDefaultRSqr[IPU3_UAPI_YUVP2_TCC_R_SQR_LUT_ELEMENTS] = { + 32, 44, 64, 92, 128, 180, 256, 364, 512, 628, 724, 808, 888, + 956, 1024, 1088, 1144, 1200, 1256, 1304, 1356, 1404, 1448, 0 +}; + +} /* namespace */ + +/** + * \copydoc libcamera::ipa::Algorithm::init + */ +int Saturation::init(IPAContext &context, const ValueNode &tuningData) +{ + defaultSaturation_ = std::clamp(tuningData["saturation"].get(1.0), + kMinSaturation, kMaxSaturation); + + context.ctrlMap[&controls::Saturation] = + ControlInfo(static_cast(kMinSaturation), + static_cast(kMaxSaturation), + static_cast(defaultSaturation_)); + + return 0; +} + +/** + * \copydoc libcamera::ipa::Algorithm::configure + */ +int Saturation::configure(IPAContext &context, + [[maybe_unused]] const IPAConfigInfo &configInfo) +{ + context.activeState.saturation.value = defaultSaturation_; + + return 0; +} + +/** + * \copydoc libcamera::ipa::Algorithm::queueRequest + */ +void Saturation::queueRequest(IPAContext &context, const uint32_t frame, + IPAFrameContext &frameContext, + const ControlList &controls) +{ + auto &saturation = context.activeState.saturation; + + frameContext.saturation.update = frame == 0; + + const auto &value = controls.get(controls::Saturation); + if (value) { + saturation.value = std::clamp(*value, kMinSaturation, + kMaxSaturation); + frameContext.saturation.update = true; + LOG(IPU3Saturation, Debug) << "Set saturation to " << saturation.value; + } + + frameContext.saturation.value = saturation.value; +} + +/** + * \copydoc libcamera::ipa::Algorithm::prepare + * + * Program the TCC block with the ImgU driver defaults, scaling the chroma gain + * table by the saturation of the frame. + */ +void Saturation::prepare([[maybe_unused]] IPAContext &context, + [[maybe_unused]] const uint32_t frame, + IPAFrameContext &frameContext, + ipu3_uapi_params *params) +{ + if (!frameContext.saturation.update) + return; + + const double saturation = frameContext.saturation.value; + struct ipu3_uapi_yuvp2_tcc_static_config &tcc = params->acc_param.tcc; + + memset(&tcc, 0, sizeof(tcc)); + + tcc.gen_control.en = 1; + tcc.gen_control.blend_shift = 3; + tcc.gen_control.gain_according_to_y_only = 1; + tcc.gen_control.gamma = 8; + tcc.gen_control.delta = 0; + + for (unsigned int i = 0; i < IPU3_UAPI_YUVP2_TCC_MACC_TABLE_ELEMENTS; i++) { + tcc.macc_table.entries[i].a = 1024; + tcc.macc_table.entries[i].b = 0; + tcc.macc_table.entries[i].c = 0; + tcc.macc_table.entries[i].d = 1024; + } + + tcc.inv_y_lut.entries[6] = 1023; + for (unsigned int i = 7; i < IPU3_UAPI_YUVP2_TCC_INV_Y_LUT_ELEMENTS; i++) + tcc.inv_y_lut.entries[i] = 1024 >> (i - 6); + + for (unsigned int i = 0; i < IPU3_UAPI_YUVP2_TCC_GAIN_PCWL_LUT_ELEMENTS; i++) { + long gain = std::lround(saturation * kDefaultGainPcwl[i]); + tcc.gain_pcwl.entries[i] = std::clamp(gain, 0, kMaxGain); + } + + for (unsigned int i = 0; i < IPU3_UAPI_YUVP2_TCC_R_SQR_LUT_ELEMENTS; i++) + tcc.r_sqr_lut.entries[i] = kDefaultRSqr[i]; + + params->use.acc_tcc = 1; +} + +/** + * \copydoc libcamera::ipa::Algorithm::process + */ +void Saturation::process([[maybe_unused]] IPAContext &context, + [[maybe_unused]] const uint32_t frame, + IPAFrameContext &frameContext, + [[maybe_unused]] const ipu3_uapi_stats_3a *stats, + ControlList &metadata) +{ + metadata.set(controls::Saturation, frameContext.saturation.value); +} + +REGISTER_IPA_ALGORITHM(Saturation, "Saturation") + +} /* namespace ipa::ipu3::algorithms */ + +} /* namespace libcamera */ diff --git a/src/ipa/ipu3/algorithms/saturation.h b/src/ipa/ipu3/algorithms/saturation.h new file mode 100644 index 0000000..6ce245a --- /dev/null +++ b/src/ipa/ipu3/algorithms/saturation.h @@ -0,0 +1,43 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2026, D. Manresa + * + * IPU3 Saturation control + */ + +#pragma once + +#include + +#include "algorithm.h" + +namespace libcamera { + +namespace ipa::ipu3::algorithms { + +class Saturation : public Algorithm +{ +public: + Saturation() = default; + ~Saturation() = default; + + int init(IPAContext &context, const ValueNode &tuningData) override; + int configure(IPAContext &context, const IPAConfigInfo &configInfo) override; + void queueRequest(IPAContext &context, const uint32_t frame, + IPAFrameContext &frameContext, + const ControlList &controls) override; + void prepare(IPAContext &context, const uint32_t frame, + IPAFrameContext &frameContext, + ipu3_uapi_params *params) override; + void process(IPAContext &context, const uint32_t frame, + IPAFrameContext &frameContext, + const ipu3_uapi_stats_3a *stats, + ControlList &metadata) override; + +private: + double defaultSaturation_ = 1.0; +}; + +} /* namespace ipa::ipu3::algorithms */ + +} /* namespace libcamera */ diff --git a/src/ipa/ipu3/data/uncalibrated.yaml b/src/ipa/ipu3/data/uncalibrated.yaml index 415ca45..b7ab0d7 100644 --- a/src/ipa/ipu3/data/uncalibrated.yaml +++ b/src/ipa/ipu3/data/uncalibrated.yaml @@ -9,4 +9,5 @@ algorithms: - BlackLevelCorrection: - Ccm: - ToneMapping: + - Saturation: ... diff --git a/src/ipa/ipu3/ipa_context.h b/src/ipa/ipu3/ipa_context.h index fde628c..76147e3 100644 --- a/src/ipa/ipu3/ipa_context.h +++ b/src/ipa/ipu3/ipa_context.h @@ -55,6 +55,10 @@ struct IPAActiveState { ipa::ccm::ActiveState ccm; ipa::gamma::ActiveState gamma; ipa::lsc::ActiveState lsc; + + struct { + double value; + } saturation; }; struct IPAFrameContext : public FrameContext { @@ -68,6 +72,11 @@ struct IPAFrameContext : public FrameContext { ipa::ccm::FrameContext ccm; ipa::gamma::FrameContext gamma; ipa::lsc::FrameContext lsc; + + struct { + double value; + bool update; + } saturation; }; struct IPAContext { From patchwork Wed Sep 9 07:01:46 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: "D. Manresa" X-Patchwork-Id: 28213 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 25337C3272 for ; Wed, 9 Sep 2026 07:01:53 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 9DAB868637; Wed, 9 Sep 2026 09:01:51 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (2048-bit key; unprotected) header.d=gmail.com header.i=@gmail.com header.b="U283D7c4"; dkim-atps=neutral Received: from mail-wr1-x435.google.com (mail-wr1-x435.google.com [IPv6:2a00:1450:4864:20::435]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 405126863D for ; Wed, 9 Sep 2026 09:01:48 +0200 (CEST) Received: by mail-wr1-x435.google.com with SMTP id ffacd0b85a97d-4843f205a5bso3471029f8f.1 for ; Wed, 09 Sep 2026 00:01:48 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1788937307; x=1789542107; darn=lists.libcamera.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to:content-type; bh=8CImbtHLXUaMPFd2IhtB5hXgkV61KoJCzLcfGIp+Hmw=; b=U283D7c4Ui5DrKdpDmIRsM5zujcHmgj3Bl1Zvv4bkRyF/vcrZHSkFrRS8Ha33xnfUv azde7Z+rd8CbcJrrlqqu+EDZT9nPI45tpJR/9O/KoMKihrr4kbUc4FpERE1Vk+539VYs ve/oHDmgkrsMaWHPfTbCzGWNYnr/bEIb9Lq95lpjeW2xOiUsbZfziytXbIICP+VHKDqe FM5hmx9Ie0owILKAwaliNiXADunoHGzS2sQNW9+IUHKfDDiY8EyX7EYqmW1OFyVX24Qo nC7j9UK8F/UWLSteJ5lpKWBuEwVir0jcTE21gJFz9rZ/bR6t6CZiLhj0mImcv/ghxz4I 5BTw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1788937307; x=1789542107; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to:content-type; bh=8CImbtHLXUaMPFd2IhtB5hXgkV61KoJCzLcfGIp+Hmw=; b=G2Dz2UZ6tDgqyXu250HF4X/sNgKhstK04lSdA/S6dph6qs8PK0dmF8+tZ1gCEfoHIB OwXUwB0ukBXCOYBi008omuBSOXllqtWhiW1Qy+//2DDy41e+8bNSR27UrPBABImgARCe Q4g9TC7/g1suaJE1QprKv70EBNmdE/4wptpk8ARkIn6tFD1934/BDTcZVyfIzP6Iei7e IRcuR52L4uN5Rdkm5pwXKIrQqbxUT6jYegKxYMfqGxkq7G7d7Sdb2yytgkDw50UEw9P4 PtK99FnCBlv+UY2mD79el8iKqODm7HGq8q68BpVczJc1n0EBb+coF3pJtGn1PlZfB3mQ QIgQ== X-Gm-Message-State: AFuF++nbCMR+nW/eDB40RCRoAJAgTHqKuw4NrW+NYUjorN1jG7s1kgt+ luSpi1kUMBS3zHICy8944Oo8iXUhfv10wAMecVNKthba3VEe0axtBKFWdggF9zakvA== X-Gm-Gg: AYBFou2IpxXIboVQByf0okHvwwoELjEvvAyzdU8dYLJopiF/dT3+xtTw4RVt12mACsu FuMNbVhJE0XJ3+XZU/F+8Wd4m74llgtGQgS+EkNBdcNl2/vOD8heDDXi4flut82Zr5wNVXIKJGz tXXNtexQjXsJ3FGifGiultElKk9eqQ2SvbumfNHct5XTCP+ouivCjgeU+tZ22YN9KAuJ7BLK0Gv 5uR8h6adet8xJzUTjtBta3zzSpdrR9Hdr/YH4eds8liezy1u8rmV2xGhPJBtZIABKs92S7kV/FP 45b0BIH8xS3OONfJgJy29llsgE+uke+t9coOur4oQLnM99GW+Wq+5B75bvQlv0QMJQ8X98O5Bdg ig7n1lenrk6cJSywoWVc2OUJbrRIJ72ZYCbgKGc7a9zzBRQlSPVlePkiqQucm5TuVz/CaEgHYFv zx4SKqhW/i2ZUEi24CSXTU0tzRmwxQcr3PQklDGsit/LULtqVntDENi5u89voqysuftoHCKTIgQ 8xCs4dL7Z3Vt+SidNUji+xphP6ccnrOfsp9L4W+xvi1I3mEMFDVBfs56jg= X-Received: by 2002:a05:6000:4816:b0:485:9bbc:ac72 with SMTP id ffacd0b85a97d-4859bbcb102mr17871546f8f.42.1788937307213; Wed, 09 Sep 2026 00:01:47 -0700 (PDT) Received: from latitude7275 (84.124.213.91.dyn.user.ono.com. [84.124.213.91]) by smtp.gmail.com with ESMTPSA id ffacd0b85a97d-48594172546sm33523472f8f.15.2026.09.09.00.01.46 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 09 Sep 2026 00:01:46 -0700 (PDT) From: "D. Manresa" To: libcamera-devel@lists.libcamera.org Cc: Daniel Scally Subject: [PATCH 2/2] ipa: ipu3: Add a Sharpness algorithm Date: Wed, 9 Sep 2026 09:01:46 +0200 Message-ID: <20260909070146.18825-3-dmanresa@gmail.com> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20260909070146.18825-1-dmanresa@gmail.com> References: <20260909070146.18825-1-dmanresa@gmail.com> MIME-Version: 1.0 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" The Image Enhancement Filter Directed (IEFD) block of the ImgU combines denoising with directional and unsharp-mask sharpening. It is the sharpening block that the ImgU firmware binaries actually run: none of them enables the Y_EE_NR edge enhancement accelerator, so programming that block has no effect. When IEFD is not programmed the driver enables it with a moderate default sharpening, and there is currently no way to change it. Add a Sharpness algorithm that programs the IEFD block with the driver defaults and scales the sharpening parameters (unsharp mask amount, directional sharpening weight and overshoot limits) by a sharpness factor: 1.0 keeps the driver behaviour, 0.0 disables the sharpening while keeping the denoising, and values up to 9.0 (where the unsharp mask amount saturates) sharpen more. The default comes from the optional 'sharpness' tuning parameter and can be changed at runtime with the Sharpness control, which is also reported in the metadata. Enable the algorithm in the uncalibrated tuning file; with the default value the output is unchanged. Measured on a Dell Latitude 7275 (OV5670, 1280x720 NV12, same scene): the standard deviation of the Laplacian of Y is 2.24, 2.47, 2.81 and 3.43 for sharpness 0.0, 1.0, 4.0 and 9.0, with the mean luminance and chroma unchanged. Signed-off-by: D. Manresa --- src/ipa/ipu3/algorithms/meson.build | 1 + src/ipa/ipu3/algorithms/sharpness.cpp | 208 ++++++++++++++++++++++++++ src/ipa/ipu3/algorithms/sharpness.h | 43 ++++++ src/ipa/ipu3/data/uncalibrated.yaml | 1 + src/ipa/ipu3/ipa_context.h | 9 ++ 5 files changed, 262 insertions(+) create mode 100644 src/ipa/ipu3/algorithms/sharpness.cpp create mode 100644 src/ipa/ipu3/algorithms/sharpness.h diff --git a/src/ipa/ipu3/algorithms/meson.build b/src/ipa/ipu3/algorithms/meson.build index 833583e..fd97330 100644 --- a/src/ipa/ipu3/algorithms/meson.build +++ b/src/ipa/ipu3/algorithms/meson.build @@ -8,5 +8,6 @@ ipu3_ipa_algorithms = files([ 'ccm.cpp', 'lsc.cpp', 'saturation.cpp', + 'sharpness.cpp', 'tone_mapping.cpp', ]) diff --git a/src/ipa/ipu3/algorithms/sharpness.cpp b/src/ipa/ipu3/algorithms/sharpness.cpp new file mode 100644 index 0000000..da26351 --- /dev/null +++ b/src/ipa/ipu3/algorithms/sharpness.cpp @@ -0,0 +1,208 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2026, D. Manresa + * + * IPU3 Sharpness control + */ + +#include "sharpness.h" + +#include +#include + +#include + +#include + +#include "libcamera/internal/value_node.h" + +/** + * \file sharpness.h + */ + +namespace libcamera { + +namespace ipa::ipu3::algorithms { + +/** + * \class Sharpness + * \brief Control of the sharpening through the ImgU IEFD block + * + * The Image Enhancement Filter Directed (IEFD) block of the ImgU combines + * denoising with a directional and an unsharp-mask sharpening. It is the + * sharpening block that the ImgU firmware binaries actually run: the Y_EE_NR + * edge enhancement block is never enabled by any of them, so programming it + * has no effect. + * + * When the IEFD block is not programmed the ImgU driver enables it with a + * moderate default sharpening. This algorithm programs the block with the + * same default configuration and scales the sharpening strength, that is the + * unsharp mask amount, the directional sharpening weight and the overshoot + * limits, by the requested sharpness. A value of 1.0 reproduces the driver + * defaults, 0.0 disables the sharpening while keeping the denoising active, + * and larger values sharpen more. + * + * The default sharpness is taken from the optional \a sharpness tuning + * parameter (1.0 if absent) and can be changed at runtime with the + * controls::Sharpness control. + */ + +LOG_DEFINE_CATEGORY(IPU3Sharpness) + +namespace { + +constexpr double kMinSharpness = 0.0; +/* The unsharp mask amount saturates at 511, 9 times the default of 56. */ +constexpr double kMaxSharpness = 9.0; + +/* clang-format off */ +/* imgu_css_iefd_defaults from the ImgU driver (ipu3-tables.c), reserved fields as 0 */ +static const struct ipu3_uapi_yuvp1_iefd_config kIefdDefaults = { + /* units */ { + /* cu_1 */ { 0, 150, 7, 0 }, + /* cu_ed */ { 7, 110, 244, 0, 307, 409, 511, 0, + 184, 255, 255, 0, 0, 0, 0, + 7, 81, 255, 0, 255, 255, 0 }, + /* cu_3 */ { 148, 251, 10, 0 }, + /* cu_5 */ { 25, 70, 501, 0, 32, 0 }, + /* cu_6 */ { 32, 63, 183, 0, 397, + 33, 0, 0, 0, + 0, 64, 0, 64, 0 }, + /* cu_7 */ { 200, 303, 10, 0 }, + /* cu_unsharp */ { 10, 64, 110, 0, 511, + 66, 12, 0, 0, + 0, 56, 0, 64, 0 }, + /* cu_radial */ { 6, 203, 255, 255, 255, 255, 0, + 84, 444, 397, 288, 300, 0, + 4, 69, 207, 0, 369, 448, 0 }, + /* cu_vssnlm */ { 61, 100, 25, 0 } + }, + /* config */ { 45, 0, 0, 0, 16, 0, 45, 0 }, + /* control */ { 1, 1, 1, 1, 1, 0 }, + /* sharp */ { { 50, 0, 511, 0, 50, 0, 50, 0 }, + { 64, 0, 0, 0, 0, 0 }, + { 56, 0, 56, 0 } }, + /* unsharp */ { { 36, 17, 8, 0 }, + { 13, 7, 3, 0 } }, + /* rad */ { { -2104, 0, -1559, 0 }, + { 4426816, 0 }, + { 2430481, 0 }, + { 6, 0, 79, 0 }, + { 64, 0, 0, 0 }, + { 1, 0, 2, 0, 0, 0, 0, 0 }, + { 40, 0, 62, 0 } }, + /* vsslnm */ { { 16, 32, 64, 0 }, + { 1, 0, 2, 0, 8, 0 } }, +}; +/* clang-format on */ + +uint32_t scaleU32(uint32_t value, double factor, uint32_t max) +{ + long scaled = std::lround(value * factor); + return std::clamp(scaled, 0, max); +} + +} /* namespace */ + +/** + * \copydoc libcamera::ipa::Algorithm::init + */ +int Sharpness::init(IPAContext &context, const ValueNode &tuningData) +{ + defaultSharpness_ = std::clamp(tuningData["sharpness"].get(1.0), + kMinSharpness, kMaxSharpness); + + context.ctrlMap[&controls::Sharpness] = + ControlInfo(static_cast(kMinSharpness), + static_cast(kMaxSharpness), + static_cast(defaultSharpness_)); + + return 0; +} + +/** + * \copydoc libcamera::ipa::Algorithm::configure + */ +int Sharpness::configure(IPAContext &context, + [[maybe_unused]] const IPAConfigInfo &configInfo) +{ + context.activeState.sharpness.value = defaultSharpness_; + + return 0; +} + +/** + * \copydoc libcamera::ipa::Algorithm::queueRequest + */ +void Sharpness::queueRequest(IPAContext &context, const uint32_t frame, + IPAFrameContext &frameContext, + const ControlList &controls) +{ + auto &sharpness = context.activeState.sharpness; + + frameContext.sharpness.update = frame == 0; + + const auto &value = controls.get(controls::Sharpness); + if (value) { + sharpness.value = std::clamp(*value, kMinSharpness, + kMaxSharpness); + frameContext.sharpness.update = true; + LOG(IPU3Sharpness, Debug) << "Set sharpness to " << sharpness.value; + } + + frameContext.sharpness.value = sharpness.value; +} + +/** + * \copydoc libcamera::ipa::Algorithm::prepare + * + * Program the IEFD block with the ImgU driver defaults, scaling the + * sharpening parameters by the sharpness of the frame. + */ +void Sharpness::prepare([[maybe_unused]] IPAContext &context, + [[maybe_unused]] const uint32_t frame, + IPAFrameContext &frameContext, + ipu3_uapi_params *params) +{ + if (!frameContext.sharpness.update) + return; + + const double sharpness = frameContext.sharpness.value; + struct ipu3_uapi_yuvp1_iefd_config &iefd = params->acc_param.iefd; + + iefd = kIefdDefaults; + + struct ipu3_uapi_yuvp1_iefd_shrp_cfg &sharp = iefd.sharp; + sharp.unshrp_cfg.unsharp_amount = + scaleU32(kIefdDefaults.sharp.unshrp_cfg.unsharp_amount, sharpness, 511); + sharp.far_w.dir_shrp = + scaleU32(kIefdDefaults.sharp.far_w.dir_shrp, sharpness, 64); + sharp.cfg.nega_lmt_txt = + scaleU32(kIefdDefaults.sharp.cfg.nega_lmt_txt, sharpness, 8191); + sharp.cfg.posi_lmt_txt = + scaleU32(kIefdDefaults.sharp.cfg.posi_lmt_txt, sharpness, 8191); + sharp.cfg.nega_lmt_dir = + scaleU32(kIefdDefaults.sharp.cfg.nega_lmt_dir, sharpness, 8191); + sharp.cfg.posi_lmt_dir = + scaleU32(kIefdDefaults.sharp.cfg.posi_lmt_dir, sharpness, 8191); + + params->use.acc_iefd = 1; +} + +/** + * \copydoc libcamera::ipa::Algorithm::process + */ +void Sharpness::process([[maybe_unused]] IPAContext &context, + [[maybe_unused]] const uint32_t frame, + IPAFrameContext &frameContext, + [[maybe_unused]] const ipu3_uapi_stats_3a *stats, + ControlList &metadata) +{ + metadata.set(controls::Sharpness, frameContext.sharpness.value); +} + +REGISTER_IPA_ALGORITHM(Sharpness, "Sharpness") + +} /* namespace ipa::ipu3::algorithms */ + +} /* namespace libcamera */ diff --git a/src/ipa/ipu3/algorithms/sharpness.h b/src/ipa/ipu3/algorithms/sharpness.h new file mode 100644 index 0000000..900fbf7 --- /dev/null +++ b/src/ipa/ipu3/algorithms/sharpness.h @@ -0,0 +1,43 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2026, D. Manresa + * + * IPU3 Sharpness control + */ + +#pragma once + +#include + +#include "algorithm.h" + +namespace libcamera { + +namespace ipa::ipu3::algorithms { + +class Sharpness : public Algorithm +{ +public: + Sharpness() = default; + ~Sharpness() = default; + + int init(IPAContext &context, const ValueNode &tuningData) override; + int configure(IPAContext &context, const IPAConfigInfo &configInfo) override; + void queueRequest(IPAContext &context, const uint32_t frame, + IPAFrameContext &frameContext, + const ControlList &controls) override; + void prepare(IPAContext &context, const uint32_t frame, + IPAFrameContext &frameContext, + ipu3_uapi_params *params) override; + void process(IPAContext &context, const uint32_t frame, + IPAFrameContext &frameContext, + const ipu3_uapi_stats_3a *stats, + ControlList &metadata) override; + +private: + double defaultSharpness_ = 1.0; +}; + +} /* namespace ipa::ipu3::algorithms */ + +} /* namespace libcamera */ diff --git a/src/ipa/ipu3/data/uncalibrated.yaml b/src/ipa/ipu3/data/uncalibrated.yaml index b7ab0d7..930d4ed 100644 --- a/src/ipa/ipu3/data/uncalibrated.yaml +++ b/src/ipa/ipu3/data/uncalibrated.yaml @@ -10,4 +10,5 @@ algorithms: - Ccm: - ToneMapping: - Saturation: + - Sharpness: ... diff --git a/src/ipa/ipu3/ipa_context.h b/src/ipa/ipu3/ipa_context.h index 76147e3..860655e 100644 --- a/src/ipa/ipu3/ipa_context.h +++ b/src/ipa/ipu3/ipa_context.h @@ -59,6 +59,10 @@ struct IPAActiveState { struct { double value; } saturation; + + struct { + double value; + } sharpness; }; struct IPAFrameContext : public FrameContext { @@ -77,6 +81,11 @@ struct IPAFrameContext : public FrameContext { double value; bool update; } saturation; + + struct { + double value; + bool update; + } sharpness; }; struct IPAContext {