From patchwork Wed Sep 9 08:13:54 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: "D. Manresa" X-Patchwork-Id: 28216 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 779DAC3272 for ; Wed, 9 Sep 2026 08:13:49 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 3EAE76864E; Wed, 9 Sep 2026 10:13:48 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (2048-bit key; unprotected) header.d=gmail.com header.i=@gmail.com header.b="V039V6fr"; dkim-atps=neutral Received: from mail-wr1-x42b.google.com (mail-wr1-x42b.google.com [IPv6:2a00:1450:4864:20::42b]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 402F2685E3 for ; Wed, 9 Sep 2026 10:13:45 +0200 (CEST) Received: by mail-wr1-x42b.google.com with SMTP id ffacd0b85a97d-48436216a98so3831697f8f.0 for ; Wed, 09 Sep 2026 01:13:45 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1788941625; x=1789546425; 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=5lfZtWrrCDhLNwm6hZqQitjfc2cJ25IwZv7nK+UEb4M=; b=V039V6friM4GJXuka5lBjdcoR99FmBYBC3AsLckV+EQnaNFrc6YnnjlhdvYYVNHtq4 BPkg00y95J0Niu6aE5kAWZ2X/Pix1vRTYruQuBOIYl5Sduxx7GNrV1sVeuh9epdQ4ibk 4RjsHyxgd37X+YBvxngzffRLjrXTpQiA2yvQONVynn2SIvP+T+wYWA+WMqOcAJaiIncE rRdI3VIZeLPshGVYwYJHHb+YL8jfDLq+2FclLg+8bBWp1RSlJWz2o8mp1Dbd5KI0FPD0 M0HFaRFwsuRp/bn0rjJidaVaZ/lsCsfb8B9rPOsXs/mtyiHR1BpE/9nLkV31AUYgoxsy GuuA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1788941625; x=1789546425; 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=5lfZtWrrCDhLNwm6hZqQitjfc2cJ25IwZv7nK+UEb4M=; b=ot784Ev8JNoMNa+QCYQ0t7u4+vHIJXzseb6+ZIpHXL9BtSatITplT6VwLZvBlxM8+/ WXocYcjZLkLOQFAmJFXMMSX0KAHbt+GJJec5h7eYOVmXBk12K4sPh7HZhZUfFlqGUeft nzCLgTKZClm+Gj6vVVpYCR8AtUc76TRnG5yAYAYensLZyegwKnu0KfMqTMrdFMXcaNzd pFgAyYAOrDJdCcBqw07q/sQgHP3jJ1zpDzv2XZbl0lxaopqszgm/QyMlNWrRZLhoFYIR 2C8t++uh0IBaJl/EtJeSPDFBNcfVw9jtyCqaEfZsUTS57i9p+G1325fE8nu9nbAbHGb9 QUXQ== X-Gm-Message-State: AFuF++mPRPtPgo2lacW5dX1gT8lw66L+LImHbGyOOyCteCxnGep7wVDN 5iZjHLiOhc6Dq8Qi7IdLH8v+NXxUhgjqftVrF3o4cE4zWuTv57pddnPEioxhdYQzNQ== X-Gm-Gg: AYBFou3v+Jzr0uf/z3e3ydOz9HfjQuIVAnxYP9W2PNGirvkVkuW2U4Nere5d47jZq+r /g7nb8CQ+cwsYD0knV+wULcqXoSASN0df+q3cp9S+AfhezfewG0C88EjTPW9DdO7fs6EqCCchFo vMPj/QpbH6CXtTqneWRghANuk6NI59FkQpAga7opGzKUj/Qn/Oj3McUrZx4HTU7x4yXSEfQdQz6 v6dybX3osNIQx0VrHrh+vJWM3ZFAFDAB/+eHY4+yvJfibDPxLfrIpGf95JdmwEJCwk3tGQRZE8K zDCfIb9bX14SPe/rtbKk2IlBFbaDnjvgEcm80o2KUtDdnP8INE2OulzvHfKwVuyJl/O6BjRU1l3 MRtPLcAa+s8nCqyV5PmN5BOvk0lluTDSL25wEh54nHKHEAQZMJGR0kKoltq9j64T65dBjAahpEi dpWMH4CLbGsJf5ZcFzGyHCPeoIeOuoAwoKWbsoFtFcYGLHdt49BlCq1/CrQxkOqh6qV3DxeW5Yo /Gq4S+IlMEfCxMr4XKy7x37+s6Jj44GAJkJRItpFi8MV/3oJmyBs5TkZVY= X-Received: by 2002:a05:600c:4505:b0:49c:dca2:ac47 with SMTP id 5b1f17b1804b1-49cf7fe607cmr314951475e9.2.1788941624260; Wed, 09 Sep 2026 01:13:44 -0700 (PDT) Received: from latitude7275 (84.124.213.91.dyn.user.ono.com. [84.124.213.91]) by smtp.gmail.com with ESMTPSA id 5b1f17b1804b1-49d04fe7f9dsm355283515e9.0.2026.09.09.01.13.43 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 09 Sep 2026 01:13:43 -0700 (PDT) From: "D. Manresa" To: libcamera-devel@lists.libcamera.org Cc: Daniel Scally , =?utf-8?q?Barnab=C3=A1s_P?= =?utf-8?b?xZFjemU=?= Subject: [PATCH v2 1/2] ipa: ipu3: Add a Saturation algorithm Date: Wed, 9 Sep 2026 10:13:54 +0200 Message-ID: <20260909081355.28471-2-dmanresa@gmail.com> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20260909081355.28471-1-dmanresa@gmail.com> References: <20260909070146.18825-1-dmanresa@gmail.com> <20260909081355.28471-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 | 202 +++++++++++++++++++++++++ src/ipa/ipu3/algorithms/saturation.h | 43 ++++++ src/ipa/ipu3/data/uncalibrated.yaml | 1 + src/ipa/ipu3/ipa_context.h | 9 ++ 5 files changed, 256 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..8fb3c23 --- /dev/null +++ b/src/ipa/ipu3/algorithms/saturation.cpp @@ -0,0 +1,202 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2026, D. Manresa + * + * IPU3 Saturation control + */ + +#include "saturation.h" + +#include +#include + +#include + +#include + +#include "libcamera/internal/value_node.h" + +#include "libipa/fixedpoint.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 float kMinSaturation = 0.0f; +constexpr float kMaxSaturation = 2.0f; + +/* clang-format off */ +/* imgu_css_tcc_gain_pcwl_lut from the ImgU driver (258 entries) */ +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) */ +const struct ipu3_uapi_yuvp2_tcc_r_sqr_lut_static_config kDefaultRSqr = { { + 32, 44, 64, 92, 128, 180, 256, 364, 512, 628, 724, 808, 888, + 956, 1024, 1088, 1144, 1200, 1256, 1304, 1356, 1404, 1448, 0 +} }; +/* clang-format on */ + +} /* namespace */ + +/** + * \copydoc libcamera::ipa::Algorithm::init + */ +int Saturation::init(IPAContext &context, const ValueNode &tuningData) +{ + defaultSaturation_ = std::clamp(tuningData["saturation"].get(1.0f), + kMinSaturation, kMaxSaturation); + + context.ctrlMap[&controls::Saturation] = + ControlInfo(kMinSaturation, kMaxSaturation, 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 float saturation = frameContext.saturation.value; + struct ipu3_uapi_yuvp2_tcc_static_config &tcc = params->acc_param.tcc; + + /* + * Everything but the gain table replicates the configuration that the + * ImgU driver programs when the block is left untouched, see + * imgu_css_cfg_acc(). + */ + 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); + + tcc.r_sqr_lut = kDefaultRSqr; + + /* The gain is u12.0. */ + for (unsigned int i = 0; i < IPU3_UAPI_YUVP2_TCC_GAIN_PCWL_LUT_ELEMENTS; i++) + tcc.gain_pcwl.entries[i] = + UQ<12, 0>(saturation * kDefaultGainPcwl[i]).quantized(); + + 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..0abe32a --- /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: + float defaultSaturation_ = 1.0f; +}; + +} /* 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..3d01a02 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 { + float 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 { + float value; + bool update; + } saturation; }; struct IPAContext { From patchwork Wed Sep 9 08:13:55 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: "D. Manresa" X-Patchwork-Id: 28217 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 A59A9C3272 for ; Wed, 9 Sep 2026 08:13:50 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id F335368646; Wed, 9 Sep 2026 10:13:48 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (2048-bit key; unprotected) header.d=gmail.com header.i=@gmail.com header.b="TABqYMCU"; dkim-atps=neutral Received: from mail-wm1-x331.google.com (mail-wm1-x331.google.com [IPv6:2a00:1450:4864:20::331]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 0C4BD685E3 for ; Wed, 9 Sep 2026 10:13:46 +0200 (CEST) Received: by mail-wm1-x331.google.com with SMTP id 5b1f17b1804b1-4995b0343c1so77847845e9.3 for ; Wed, 09 Sep 2026 01:13:46 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1788941625; x=1789546425; 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=P4di1sGlNqYFQlGCPuPKwU8aCwqhqoKZ3M4ca6yidPs=; b=TABqYMCUpsaUDECise7YMeaQ+SFCjsxgxxXc49DbxGk012TIdPaWIZgXJdADjMheg9 aW0M07ioBWGfIEzwsYrIUj0jpKiP6uH+9APGJiBKmEv9/l9sZChiiX8ycAbiTfu67Tya z+IOmthrPEiE8jnanxy4DT7zFPUdOqx6ef7cZbVMMdM/iiqnbY1szCLDQvjOQSl2Lxfh yYxKUng+jMcg9KsFYmn3rAnDrA1oTr/4RHWdcYBkBxuH602uNLplSUKzaa8y8enxqtnZ +oNFfAoVyqgRZ93m4DFmr9Mx1Z4HK4PsqdD3e2I3we8Ls1TWFyaJCuzPK/JgUX0RPG9m tyMQ== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1788941625; x=1789546425; 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=P4di1sGlNqYFQlGCPuPKwU8aCwqhqoKZ3M4ca6yidPs=; b=HDCafAOJoxdE1PxsfFN1GtiW6XD3SzUBGyNuFiHRCNbpupcF7LuNRdnE804CLaXkju tWGUZRZlh68x1Jol1a2MKyKCL3dCenuqA4z0TDQ142brb2SriBfS4LfYOc7kCx0vaTyB d3ozPIcloCqF8Ae2PGPx/y2tx4/Azyf023uBjdBxGGQZ2vEQ0Zfhqatzb2yypHtCA8Sk EUB3Em6prksA42KKOHNGX2lnnNwH5D/gLCGFkTzORKS7M+6xYhCGP2LkvwIvxQchhGnt bly7KHNGKEGRreI1JNZxudxwaQgynXlhAnUrtvbBgorf5+BdUNTqrVHhbVV7d4kQ5wdz +POw== X-Gm-Message-State: AFuF++m3nqswCbVZZr8oGa1rEZHD2WQX2bAaYJowuhZnblfVKIm5AER/ N4lIpACB/7jgo7EWan9/w41JvLHQW2QCy9qMfVrxm73GJ9dCVaJzwrG8jymMotd/1Q== X-Gm-Gg: AYBFou203xo4Ri7kMApdMzV10FnQn6kMaVEitECe9Px1/RiZqzvMd9Z6GuXMrgon0i8 J3QddoRWAGghcgEQz8LABzDFu2E8gNDBwPIe0JGfHoTK5kvoYhM+TDhHr3ew0xUfr/XIK5dJNqa kXdbXUHo/yHp6Oqde7cpNeT4aIy4jsawcggz6o+PZqOrURYe6N0+kBsONclMdED83VQDJ6xB0Vz djxFkm7IrppZPfeDJ0bwkAdePVg7y0Xw5HcIlEmx7BPsL7VD7fnI6ULb0bL+AviuUUbjPnBwpkx y+EWnP1W93W7y3IkUJ0eav8M1kBdCRmnDNS2eCmUK7mWqjE6Bnq/nbi2QEYF4+/AXuHJeZiD9Y1 ONTNQ8ysSXtkNoOo1TWJBKTS0ezMpR7DvGAhUussRTYkR+k2lnqpcEvhcwjCruK0odDqcmvQBH6 /fb5AgQNE4xDp7/IzmdxDqlTZkSKgOAGpNnq0kVkW9VD7V0VC7nAxIGb+SOkSkAlNfz5bObIFfJ Q37ER5ymLcQDqYrjp6sL7QZnl1hcBXVkYcYlVJRqCo4k23VrzTC5XMj+8o= X-Received: by 2002:a05:600c:3b18:b0:49c:fa21:1c8a with SMTP id 5b1f17b1804b1-49cfa211d97mr285291965e9.31.1788941625118; Wed, 09 Sep 2026 01:13:45 -0700 (PDT) Received: from latitude7275 (84.124.213.91.dyn.user.ono.com. [84.124.213.91]) by smtp.gmail.com with ESMTPSA id 5b1f17b1804b1-49d04fe7f9dsm355283515e9.0.2026.09.09.01.13.44 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 09 Sep 2026 01:13:44 -0700 (PDT) From: "D. Manresa" To: libcamera-devel@lists.libcamera.org Cc: Daniel Scally , =?utf-8?q?Barnab=C3=A1s_P?= =?utf-8?b?xZFjemU=?= Subject: [PATCH v2 2/2] ipa: ipu3: Add a Sharpness algorithm Date: Wed, 9 Sep 2026 10:13:55 +0200 Message-ID: <20260909081355.28471-3-dmanresa@gmail.com> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20260909081355.28471-1-dmanresa@gmail.com> References: <20260909070146.18825-1-dmanresa@gmail.com> <20260909081355.28471-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 | 205 ++++++++++++++++++++++++++ src/ipa/ipu3/algorithms/sharpness.h | 43 ++++++ src/ipa/ipu3/data/uncalibrated.yaml | 1 + src/ipa/ipu3/ipa_context.h | 9 ++ 5 files changed, 259 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..afdc29f --- /dev/null +++ b/src/ipa/ipu3/algorithms/sharpness.cpp @@ -0,0 +1,205 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2026, D. Manresa + * + * IPU3 Sharpness control + */ + +#include "sharpness.h" + +#include + +#include + +#include + +#include "libcamera/internal/value_node.h" + +#include "libipa/fixedpoint.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 float kMinSharpness = 0.0f; +/* The unsharp mask amount saturates at 511, 9 times the default of 56. */ +constexpr float kMaxSharpness = 9.0f; + +/* clang-format off */ +/* imgu_css_iefd_defaults from the ImgU driver (ipu3-tables.c) */ +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 */ + +} /* namespace */ + +/** + * \copydoc libcamera::ipa::Algorithm::init + */ +int Sharpness::init(IPAContext &context, const ValueNode &tuningData) +{ + defaultSharpness_ = std::clamp(tuningData["sharpness"].get(1.0f), + kMinSharpness, kMaxSharpness); + + context.ctrlMap[&controls::Sharpness] = + ControlInfo(kMinSharpness, kMaxSharpness, 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 float sharpness = frameContext.sharpness.value; + struct ipu3_uapi_yuvp1_iefd_config &iefd = params->acc_param.iefd; + + iefd = kIefdDefaults; + + /* + * Scale the sharpening parameters of the default configuration. The + * unsharp mask amount is u4.5, the direct sharpening weight is u1.6 + * limited to 1.0, and the overshoot limits are u13.0. + */ + const struct ipu3_uapi_yuvp1_iefd_shrp_cfg &def = kIefdDefaults.sharp; + struct ipu3_uapi_yuvp1_iefd_shrp_cfg &sharp = iefd.sharp; + + const UQ<4, 5> unsharpAmount{ static_cast(def.unshrp_cfg.unsharp_amount) }; + const UQ<1, 6> dirSharp{ static_cast(def.far_w.dir_shrp) }; + + sharp.unshrp_cfg.unsharp_amount = UQ<4, 5>(sharpness * unsharpAmount.value()).quantized(); + sharp.far_w.dir_shrp = UQ<1, 6>(std::min(sharpness * dirSharp.value(), 1.0f)).quantized(); + sharp.cfg.nega_lmt_txt = UQ<13, 0>(sharpness * def.cfg.nega_lmt_txt).quantized(); + sharp.cfg.posi_lmt_txt = UQ<13, 0>(sharpness * def.cfg.posi_lmt_txt).quantized(); + sharp.cfg.nega_lmt_dir = UQ<13, 0>(sharpness * def.cfg.nega_lmt_dir).quantized(); + sharp.cfg.posi_lmt_dir = UQ<13, 0>(sharpness * def.cfg.posi_lmt_dir).quantized(); + + 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..79ae822 --- /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: + float defaultSharpness_ = 1.0f; +}; + +} /* 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 3d01a02..dfc5596 100644 --- a/src/ipa/ipu3/ipa_context.h +++ b/src/ipa/ipu3/ipa_context.h @@ -59,6 +59,10 @@ struct IPAActiveState { struct { float value; } saturation; + + struct { + float value; + } sharpness; }; struct IPAFrameContext : public FrameContext { @@ -77,6 +81,11 @@ struct IPAFrameContext : public FrameContext { float value; bool update; } saturation; + + struct { + float value; + bool update; + } sharpness; }; struct IPAContext {