From patchwork Sat Jun 20 23:00:32 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Kieran Bingham X-Patchwork-Id: 27001 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 21CF8C3308 for ; Sat, 20 Jun 2026 23:00:49 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id E7273656EC; Sun, 21 Jun 2026 01:00:44 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="V9kY6MYP"; 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 8867B656E2 for ; Sun, 21 Jun 2026 01:00:36 +0200 (CEST) Received: from [192.168.0.240] (cpc89244-aztw30-2-0-cust6594.18-1.cable.virginm.net [86.31.185.195]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id C66F817D8; Sun, 21 Jun 2026 00:59:59 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1781996399; bh=G2KmPxn7MDxy+1ldnSusw90wucEnlxKSCdpUja9ph6Q=; h=From:Date:Subject:References:In-Reply-To:To:Cc:From; b=V9kY6MYPZZ3eaciHXMzRxocXUoTi1AuMMAGIqMbmALnMibRQpxcooa40ZUohXNBVk jWQHf4c050BqI3PXHplDOixqdY3ZkSFIPfE4oUulfWssgg1cc1xyeZ9z4XrzIjm4rJ W+OEVJmMhnCikhjj1dFlkvUKuaWnr5aWAUw+daBY= From: Kieran Bingham Date: Sun, 21 Jun 2026 00:00:32 +0100 Subject: [PATCH 5/7] libcamera: software_isp: Fix black level handling in CPU ISP MIME-Version: 1.0 Message-Id: <20260621-kbingham-awb-saturation-v1-5-b91ea59c6cfb@ideasonboard.com> References: <20260621-kbingham-awb-saturation-v1-0-b91ea59c6cfb@ideasonboard.com> In-Reply-To: <20260621-kbingham-awb-saturation-v1-0-b91ea59c6cfb@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=1781996434; l=6483; i=kieran.bingham@ideasonboard.com; s=20260204; h=from:subject:message-id; bh=Z3taOw8EuwTfN3kX7QTe2JdvX1aHOQdk8busc/t9/sA=; b=9mzyxOpJVt6w4nRt6lwjgIoRJgFllh/xBQuzia5Bm+d/93RFs+TjNbMVjdntzqRr68Jw+OhKL X3IpGqL+PyuAO3wy3iOAlC9Q9UsRus7aENTK548y39W0m9QpE517iEi 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. [Kieran: Fix indexing from Milans' suggestion, use new clamp] [Kieran: Use enumerate, and div updates from Laurent's suggestion] Signed-off-by: Milan Zamazal Signed-off-by: Kieran Bingham --- src/libcamera/software_isp/debayer_cpu.cpp | 60 ++++++++++++++---------------- 1 file changed, 28 insertions(+), 32 deletions(-) diff --git a/src/libcamera/software_isp/debayer_cpu.cpp b/src/libcamera/software_isp/debayer_cpu.cpp index d2596d32bbcdeaaab2e2c287e3f01ae22c442884..9d6a08b3333f988570cb5d3f7cea4c117f7c2530 100644 --- a/src/libcamera/software_isp/debayer_cpu.cpp +++ b/src/libcamera/software_isp/debayer_cpu.cpp @@ -979,32 +979,20 @@ 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 (auto [i, value] : utils::enumerate(gammaTable_)) { + 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) normalized = 0.5 * std::pow(normalized / 0.5, contrastExp); else normalized = 1.0 - 0.5 * std::pow((1.0 - normalized) / 0.5, contrastExp); - gammaTable_[i] = UINT8_MAX * - std::pow(normalized, gamma); + value = 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) @@ -1016,11 +1004,15 @@ 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; + + const RGB blackIndex = params.blackLevel * kRGBLookupSize; + const RGB div = (RGB(kRGBLookupSize) - blackIndex).max(1.0); + if (ccmEnabled_) { if (gammaUpdateNeeded || matrixChanged(params.combinedMatrix, params_.combinedMatrix)) { @@ -1030,17 +1022,19 @@ 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; + 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) * kRGBLookupSize / div).max(0.0); + red[i].r = std::round(rgb.r() * params.combinedMatrix[redIndex][0]); + red[i].g = std::round(rgb.r() * params.combinedMatrix[greenIndex][0]); + red[i].b = std::round(rgb.r() * params.combinedMatrix[blueIndex][0]); + green[i].r = std::round(rgb.g() * params.combinedMatrix[redIndex][1]); + green[i].g = std::round(rgb.g() * params.combinedMatrix[greenIndex][1]); + green[i].b = std::round(rgb.g() * params.combinedMatrix[blueIndex][1]); + blue[i].r = std::round(rgb.b() * params.combinedMatrix[redIndex][2]); + blue[i].g = std::round(rgb.b() * params.combinedMatrix[greenIndex][2]); + blue[i].b = std::round(rgb.b() * params.combinedMatrix[blueIndex][2]); + gammaLut_[i] = gammaTable_[i * gammaTableSize / kRGBLookupSize]; } } } else { @@ -1049,12 +1043,14 @@ void DebayerCpu::updateLookupTables(const DebayerParams ¶ms) auto &red = swapRedBlueGains_ ? blue_ : red_; auto &green = green_; auto &blue = swapRedBlueGains_ ? red_ : blue_; + 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) * gammaTableSize / div) + .clamp(0.0, gammaTableSize - 1); + red[i] = gammaTable_[lutGains.r()]; + green[i] = gammaTable_[lutGains.g()]; + blue[i] = gammaTable_[lutGains.b()]; } } }