From patchwork Tue Apr 7 22:01:06 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Kieran Bingham X-Patchwork-Id: 26486 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 2299DBDCBD for ; Tue, 7 Apr 2026 22:02:56 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id EC80C62DD2; Wed, 8 Apr 2026 00:02:46 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="Cvu7Co7p"; dkim-atps=neutral Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [IPv6:2001:4b98:dc2:55:216:3eff:fef7:d647]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 5CC7A62D66 for ; Wed, 8 Apr 2026 00:02:41 +0200 (CEST) Received: from [192.168.0.204] (ams.linuxembedded.co.uk [209.38.108.23]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id A037F1C25; Wed, 8 Apr 2026 00:01:13 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1775599273; bh=bY67C6uJP8ZsZbZQ9qGjWmYA523zk4Q6DpGuKKMIBSs=; h=From:Date:Subject:References:In-Reply-To:To:Cc:From; b=Cvu7Co7pFaVmjRwO1K5a+ZdV5/vMWFR3+10IWGIBpdApCFy1HYI/qy79L4NXhNs25 RvH4lSwByAAC0JiCFZ6T2XHA1c+N43E7h4RLqmQHoa4kFqtecy2riz3KgrK0ICQh10 Y6b6KJx8A3+0I1JRz7hgB5GhskhArC6OuVd1+FsA= From: Kieran Bingham Date: Tue, 07 Apr 2026 23:01:06 +0100 Subject: [PATCH 03/13] libcamera: software_isp: Fix black level handling in CPU ISP MIME-Version: 1.0 Message-Id: <20260407-kbingham-awb-split-v1-3-a39af3f4dc20@ideasonboard.com> References: <20260407-kbingham-awb-split-v1-0-a39af3f4dc20@ideasonboard.com> In-Reply-To: <20260407-kbingham-awb-split-v1-0-a39af3f4dc20@ideasonboard.com> To: libcamera-devel@lists.libcamera.org Cc: Kieran Bingham , Milan Zamazal X-Mailer: b4 0.14.2 X-Developer-Signature: v=1; a=ed25519-sha256; t=1775599359; l=6327; i=kieran.bingham@ideasonboard.com; s=20260204; h=from:subject:message-id; bh=J/NGwZlg7OEyKZmBQ8iQMgpXJzyjsJs2TFIf1nVIDU8=; b=lTGEh4dP6hVFLsdZ3k99gmJ0DVtzp9WZI7s4RG3EB8OPfaKZ50ZlghugbECadLtXYKlrHZVag Vfr41dYU7xFDd0m+QLv+vNLhSmxw/uHFlShdNYCDIbjSLRrPA43vPIo X-Developer-Key: i=kieran.bingham@ideasonboard.com; a=ed25519; pk=IOxS2C6nWHNjLfkDR71Iesk904i6wJDfEERqV7hDBdY= 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" From: Milan Zamazal The black level handling in CPU ISP has two flaws: - The black level is applied after white balance rather than before. - It doesn't handle black levels with different values for individual colour channels. The flaws are in both CCM and non-CCM cases. The wrong black level and white balance application order is well visible when the white balance gains are significantly different from 1.0. Then the output differs significantly from GPU ISP output, which uses the correct order. This patch changes the computations of the lookup tables in a way that fixes both the problems. Signed-off-by: Milan Zamazal --- src/libcamera/software_isp/debayer_cpu.cpp | 60 +++++++++++++++--------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/src/libcamera/software_isp/debayer_cpu.cpp b/src/libcamera/software_isp/debayer_cpu.cpp index dd0fff8714144417467bac16a1055d1185782d14..5356d6bbec11c30fa0b05659d44a91e69e2b79d0 100644 --- a/src/libcamera/software_isp/debayer_cpu.cpp +++ b/src/libcamera/software_isp/debayer_cpu.cpp @@ -879,15 +879,12 @@ void DebayerCpuThread::process4(uint32_t frame, const uint8_t *src, uint8_t *dst void DebayerCpu::updateGammaTable(const DebayerParams ¶ms) { - const RGB blackLevel = params.blackLevel; - /* Take let's say the green channel black level */ - const unsigned int blackIndex = blackLevel[1] * gammaTable_.size(); const float gamma = params.gamma; const float contrastExp = params.contrastExp; - const float divisor = gammaTable_.size() - blackIndex - 1.0; - for (unsigned int i = blackIndex; i < gammaTable_.size(); i++) { - float normalized = (i - blackIndex) / divisor; + const float divisor = gammaTable_.size() - 1.0; + for (unsigned int i = 0; i < gammaTable_.size(); i++) { + float normalized = i / divisor; /* Convert 0..2 to 0..infinity; avoid actual inifinity at tan(pi/2) */ /* Apply simple S-curve */ if (normalized < 0.5) @@ -897,14 +894,6 @@ void DebayerCpu::updateGammaTable(const DebayerParams ¶ms) gammaTable_[i] = UINT8_MAX * std::pow(normalized, gamma); } - /* - * Due to CCM operations, the table lookup may reach indices below the black - * level. Let's set the table values below black level to the minimum - * non-black value to prevent problems when the minimum value is - * significantly non-zero (for example, when the image should be all grey). - */ - std::fill(gammaTable_.begin(), gammaTable_.begin() + blackIndex, - gammaTable_[blackIndex]); } void DebayerCpu::updateLookupTables(const DebayerParams ¶ms) @@ -916,11 +905,13 @@ void DebayerCpu::updateLookupTables(const DebayerParams ¶ms) if (gammaUpdateNeeded) updateGammaTable(params); + /* Processing order: black level -> gains -> gamma */ auto matrixChanged = [](const Matrix &m1, const Matrix &m2) -> bool { return !std::equal(m1.data().begin(), m1.data().end(), m2.data().begin()); }; const unsigned int gammaTableSize = gammaTable_.size(); - const double div = static_cast(kRGBLookupSize) / gammaTableSize; + RGB blackIndex = params.blackLevel * kRGBLookupSize; + if (ccmEnabled_) { if (gammaUpdateNeeded || matrixChanged(params.combinedMatrix, params_.combinedMatrix)) { @@ -930,17 +921,21 @@ void DebayerCpu::updateLookupTables(const DebayerParams ¶ms) const unsigned int redIndex = swapRedBlueGains_ ? 2 : 0; const unsigned int greenIndex = 1; const unsigned int blueIndex = swapRedBlueGains_ ? 0 : 2; + const RGB div = + (RGB(kRGBLookupSize) - blackIndex).max(1.0) / + kRGBLookupSize; for (unsigned int i = 0; i < kRGBLookupSize; i++) { - red[i].r = std::round(i * params.combinedMatrix[redIndex][0]); - red[i].g = std::round(i * params.combinedMatrix[greenIndex][0]); - red[i].b = std::round(i * params.combinedMatrix[blueIndex][0]); - green[i].r = std::round(i * params.combinedMatrix[redIndex][1]); - green[i].g = std::round(i * params.combinedMatrix[greenIndex][1]); - green[i].b = std::round(i * params.combinedMatrix[blueIndex][1]); - blue[i].r = std::round(i * params.combinedMatrix[redIndex][2]); - blue[i].g = std::round(i * params.combinedMatrix[greenIndex][2]); - blue[i].b = std::round(i * params.combinedMatrix[blueIndex][2]); - gammaLut_[i] = gammaTable_[i / div]; + const RGB rgb = ((RGB(i) - blackIndex) / div).max(0.0); + red[i].r = std::round(rgb.r() * params.combinedMatrix[redIndex][0]); + red[i].g = std::round(rgb.g() * params.combinedMatrix[greenIndex][0]); + red[i].b = std::round(rgb.b() * params.combinedMatrix[blueIndex][0]); + green[i].r = std::round(rgb.r() * params.combinedMatrix[redIndex][1]); + green[i].g = std::round(rgb.g() * params.combinedMatrix[greenIndex][1]); + green[i].b = std::round(rgb.b() * params.combinedMatrix[blueIndex][1]); + blue[i].r = std::round(rgb.r() * params.combinedMatrix[redIndex][2]); + blue[i].g = std::round(rgb.g() * params.combinedMatrix[greenIndex][2]); + blue[i].b = std::round(rgb.b() * params.combinedMatrix[blueIndex][2]); + gammaLut_[i] = gammaTable_[i * gammaTableSize / kRGBLookupSize]; } } } else { @@ -949,12 +944,17 @@ void DebayerCpu::updateLookupTables(const DebayerParams ¶ms) auto &red = swapRedBlueGains_ ? blue_ : red_; auto &green = green_; auto &blue = swapRedBlueGains_ ? red_ : blue_; + const RGB div = + (RGB(kRGBLookupSize) - blackIndex).max(1.0) / + gammaTableSize; for (unsigned int i = 0; i < kRGBLookupSize; i++) { - /* Apply gamma after gain! */ - const RGB lutGains = (gains * i / div).min(gammaTableSize - 1); - red[i] = gammaTable_[static_cast(lutGains.r())]; - green[i] = gammaTable_[static_cast(lutGains.g())]; - blue[i] = gammaTable_[static_cast(lutGains.b())]; + const RGB lutGains = + (gains * (RGB(i) - blackIndex) / div) + .min(gammaTableSize - 1) + .max(0.0); + red[i] = gammaTable_[lutGains.r()]; + green[i] = gammaTable_[lutGains.g()]; + blue[i] = gammaTable_[lutGains.b()]; } } }