From patchwork Tue Feb 10 16:37:44 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Milan Zamazal X-Patchwork-Id: 26128 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 DB3FEBDE6B for ; Tue, 10 Feb 2026 16:37:54 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 259486218F; Tue, 10 Feb 2026 17:37:54 +0100 (CET) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=redhat.com header.i=@redhat.com header.b="CvtXnRl7"; dkim-atps=neutral Received: from us-smtp-delivery-124.mimecast.com (us-smtp-delivery-124.mimecast.com [170.10.133.124]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 0E990620FA for ; Tue, 10 Feb 2026 17:37:51 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com; s=mimecast20190719; t=1770741470; 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; bh=CX96HRjMJNQN95xKWA++JqReaLSHkAPdeihUNyzPpp4=; b=CvtXnRl76LHeiUBRScL6tLZIbRaSkMPaMNH9MCNopMMS0dQprSHbJRBxy/YNfAgt9fZwNI YoGZjyeCOTkTXWGKHT2QsIEjC+PE4ENBFoYYgKvrJ7usuiZG7dlnBvW4j+djbjOnC8BIYR qPHzZamRMxnRqGtTp+DD6oN3DaaJuGM= Received: from mx-prod-mc-08.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-360-_yDZ2TotNFuJxHpTf3sUWQ-1; Tue, 10 Feb 2026 11:37:49 -0500 X-MC-Unique: _yDZ2TotNFuJxHpTf3sUWQ-1 X-Mimecast-MFC-AGG-ID: _yDZ2TotNFuJxHpTf3sUWQ_1770741468 Received: from mx-prod-int-01.mail-002.prod.us-west-2.aws.redhat.com (mx-prod-int-01.mail-002.prod.us-west-2.aws.redhat.com [10.30.177.4]) (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-08.mail-002.prod.us-west-2.aws.redhat.com (Postfix) with ESMTPS id 7339B180047F for ; Tue, 10 Feb 2026 16:37:48 +0000 (UTC) Received: from mzamazal-thinkpadp1gen7.tpbc.com (unknown [10.45.224.213]) by mx-prod-int-01.mail-002.prod.us-west-2.aws.redhat.com (Postfix) with ESMTP id 4C59930001A5; Tue, 10 Feb 2026 16:37:46 +0000 (UTC) From: Milan Zamazal To: libcamera-devel@lists.libcamera.org Cc: Milan Zamazal Subject: [PATCH] libcamera: software_isp: Fix black level handling in CPU ISP Date: Tue, 10 Feb 2026 17:37:44 +0100 Message-ID: <20260210163744.79510-1-mzamazal@redhat.com> MIME-Version: 1.0 X-Scanned-By: MIMEDefang 3.4.1 on 10.30.177.4 X-Mimecast-Spam-Score: 0 X-Mimecast-MFC-PROC-ID: 9YfgP-z9SSEsnYpnWsVRgQTzGIll31t0xhfLyWrkNeA_1770741468 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" 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 | 63 +++++++++++----------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/src/libcamera/software_isp/debayer_cpu.cpp b/src/libcamera/software_isp/debayer_cpu.cpp index af7af0a8d..569567a49 100644 --- a/src/libcamera/software_isp/debayer_cpu.cpp +++ b/src/libcamera/software_isp/debayer_cpu.cpp @@ -752,15 +752,12 @@ void DebayerCpu::process4(uint32_t frame, const uint8_t *src, uint8_t *dst) void DebayerCpu::updateGammaTable(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) @@ -770,14 +767,6 @@ void DebayerCpu::updateGammaTable(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(DebayerParams ¶ms) @@ -789,11 +778,15 @@ void DebayerCpu::updateLookupTables(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 (swapRedBlueGains_) + blackIndex = RGB({ blackIndex.b(), blackIndex.g(), blackIndex.r() }); + if (ccmEnabled_) { if (gammaUpdateNeeded || matrixChanged(params.combinedMatrix, params_.combinedMatrix)) { @@ -803,17 +796,22 @@ void DebayerCpu::updateLookupTables(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 { @@ -822,12 +820,17 @@ void DebayerCpu::updateLookupTables(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()]; } } }