{"id":26128,"url":"https://patchwork.libcamera.org/api/patches/26128/?format=json","web_url":"https://patchwork.libcamera.org/patch/26128/","project":{"id":1,"url":"https://patchwork.libcamera.org/api/projects/1/?format=json","name":"libcamera","link_name":"libcamera","list_id":"libcamera_core","list_email":"libcamera-devel@lists.libcamera.org","web_url":"","scm_url":"","webscm_url":""},"msgid":"<20260210163744.79510-1-mzamazal@redhat.com>","date":"2026-02-10T16:37:44","name":"libcamera: software_isp: Fix black level handling in CPU ISP","commit_ref":null,"pull_url":null,"state":"superseded","archived":false,"hash":"36beebaeafaadc5703f2fbefaf0e4836a7f95085","submitter":{"id":177,"url":"https://patchwork.libcamera.org/api/people/177/?format=json","name":"Milan Zamazal","email":"mzamazal@redhat.com"},"delegate":null,"mbox":"https://patchwork.libcamera.org/patch/26128/mbox/","series":[{"id":5782,"url":"https://patchwork.libcamera.org/api/series/5782/?format=json","web_url":"https://patchwork.libcamera.org/project/libcamera/list/?series=5782","date":"2026-02-10T16:37:44","name":"libcamera: software_isp: Fix black level handling in CPU ISP","version":1,"mbox":"https://patchwork.libcamera.org/series/5782/mbox/"}],"comments":"https://patchwork.libcamera.org/api/patches/26128/comments/","check":"pending","checks":"https://patchwork.libcamera.org/api/patches/26128/checks/","tags":{},"headers":{"Return-Path":"<libcamera-devel-bounces@lists.libcamera.org>","X-Original-To":"parsemail@patchwork.libcamera.org","Delivered-To":"parsemail@patchwork.libcamera.org","Received":["from lancelot.ideasonboard.com (lancelot.ideasonboard.com\n\t[92.243.16.209])\n\tby patchwork.libcamera.org (Postfix) with ESMTPS id DB3FEBDE6B\n\tfor <parsemail@patchwork.libcamera.org>;\n\tTue, 10 Feb 2026 16:37:54 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 259486218F;\n\tTue, 10 Feb 2026 17:37:54 +0100 (CET)","from us-smtp-delivery-124.mimecast.com\n\t(us-smtp-delivery-124.mimecast.com [170.10.133.124])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 0E990620FA\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tTue, 10 Feb 2026 17:37:51 +0100 (CET)","from mx-prod-mc-08.mail-002.prod.us-west-2.aws.redhat.com\n\t(ec2-35-165-154-97.us-west-2.compute.amazonaws.com [35.165.154.97])\n\tby relay.mimecast.com with ESMTP with STARTTLS (version=TLSv1.3,\n\tcipher=TLS_AES_256_GCM_SHA384) id us-mta-360-_yDZ2TotNFuJxHpTf3sUWQ-1;\n\tTue, 10 Feb 2026 11:37:49 -0500","from mx-prod-int-01.mail-002.prod.us-west-2.aws.redhat.com\n\t(mx-prod-int-01.mail-002.prod.us-west-2.aws.redhat.com [10.30.177.4])\n\t(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)\n\tkey-exchange X25519 server-signature RSA-PSS (2048 bits)\n\tserver-digest SHA256) (No client certificate requested)\n\tby mx-prod-mc-08.mail-002.prod.us-west-2.aws.redhat.com (Postfix)\n\twith ESMTPS\n\tid 7339B180047F for <libcamera-devel@lists.libcamera.org>;\n\tTue, 10 Feb 2026 16:37:48 +0000 (UTC)","from mzamazal-thinkpadp1gen7.tpbc.com (unknown [10.45.224.213])\n\tby mx-prod-int-01.mail-002.prod.us-west-2.aws.redhat.com (Postfix)\n\twith ESMTP id 4C59930001A5; Tue, 10 Feb 2026 16:37:46 +0000 (UTC)"],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (1024-bit key;\n\tunprotected) header.d=redhat.com header.i=@redhat.com\n\theader.b=\"CvtXnRl7\"; dkim-atps=neutral","DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com;\n\ts=mimecast20190719; t=1770741470;\n\th=from:from:reply-to:subject:subject:date:date:message-id:message-id:\n\tto:to:cc:cc:mime-version:mime-version:content-type:content-type:\n\tcontent-transfer-encoding:content-transfer-encoding;\n\tbh=CX96HRjMJNQN95xKWA++JqReaLSHkAPdeihUNyzPpp4=;\n\tb=CvtXnRl76LHeiUBRScL6tLZIbRaSkMPaMNH9MCNopMMS0dQprSHbJRBxy/YNfAgt9fZwNI\n\tYoGZjyeCOTkTXWGKHT2QsIEjC+PE4ENBFoYYgKvrJ7usuiZG7dlnBvW4j+djbjOnC8BIYR\n\tqPHzZamRMxnRqGtTp+DD6oN3DaaJuGM=","X-MC-Unique":"_yDZ2TotNFuJxHpTf3sUWQ-1","X-Mimecast-MFC-AGG-ID":"_yDZ2TotNFuJxHpTf3sUWQ_1770741468","From":"Milan Zamazal <mzamazal@redhat.com>","To":"libcamera-devel@lists.libcamera.org","Cc":"Milan Zamazal <mzamazal@redhat.com>","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-Transfer-Encoding":"8bit","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":"<libcamera-devel.lists.libcamera.org>","List-Unsubscribe":"<https://lists.libcamera.org/options/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=unsubscribe>","List-Archive":"<https://lists.libcamera.org/pipermail/libcamera-devel/>","List-Post":"<mailto:libcamera-devel@lists.libcamera.org>","List-Help":"<mailto:libcamera-devel-request@lists.libcamera.org?subject=help>","List-Subscribe":"<https://lists.libcamera.org/listinfo/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=subscribe>","Errors-To":"libcamera-devel-bounces@lists.libcamera.org","Sender":"\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>"},"content":"The black level handling in CPU ISP has two flaws:\n\n- The black level is applied after white balance rather than before.\n\n- It doesn't handle black levels with different values for individual\n  colour channels.\n\nThe flaws are in both CCM and non-CCM cases.  The wrong black level and\nwhite balance application order is well visible when the white balance\ngains are significantly different from 1.0.  Then the output differs\nsignificantly from GPU ISP output, which uses the correct order.\n\nThis patch changes the computations of the lookup tables in a way that\nfixes both the problems.\n\nSigned-off-by: Milan Zamazal <mzamazal@redhat.com>\n---\n src/libcamera/software_isp/debayer_cpu.cpp | 63 +++++++++++-----------\n 1 file changed, 33 insertions(+), 30 deletions(-)","diff":"diff --git a/src/libcamera/software_isp/debayer_cpu.cpp b/src/libcamera/software_isp/debayer_cpu.cpp\nindex af7af0a8d..569567a49 100644\n--- a/src/libcamera/software_isp/debayer_cpu.cpp\n+++ b/src/libcamera/software_isp/debayer_cpu.cpp\n@@ -752,15 +752,12 @@ void DebayerCpu::process4(uint32_t frame, const uint8_t *src, uint8_t *dst)\n \n void DebayerCpu::updateGammaTable(DebayerParams &params)\n {\n-\tconst RGB<float> blackLevel = params.blackLevel;\n-\t/* Take let's say the green channel black level */\n-\tconst unsigned int blackIndex = blackLevel[1] * gammaTable_.size();\n \tconst float gamma = params.gamma;\n \tconst float contrastExp = params.contrastExp;\n \n-\tconst float divisor = gammaTable_.size() - blackIndex - 1.0;\n-\tfor (unsigned int i = blackIndex; i < gammaTable_.size(); i++) {\n-\t\tfloat normalized = (i - blackIndex) / divisor;\n+\tconst float divisor = gammaTable_.size() - 1.0;\n+\tfor (unsigned int i = 0; i < gammaTable_.size(); i++) {\n+\t\tfloat normalized = i / divisor;\n \t\t/* Convert 0..2 to 0..infinity; avoid actual inifinity at tan(pi/2) */\n \t\t/* Apply simple S-curve */\n \t\tif (normalized < 0.5)\n@@ -770,14 +767,6 @@ void DebayerCpu::updateGammaTable(DebayerParams &params)\n \t\tgammaTable_[i] = UINT8_MAX *\n \t\t\t\t std::pow(normalized, gamma);\n \t}\n-\t/*\n-\t * Due to CCM operations, the table lookup may reach indices below the black\n-\t * level. Let's set the table values below black level to the minimum\n-\t * non-black value to prevent problems when the minimum value is\n-\t * significantly non-zero (for example, when the image should be all grey).\n-\t */\n-\tstd::fill(gammaTable_.begin(), gammaTable_.begin() + blackIndex,\n-\t\t  gammaTable_[blackIndex]);\n }\n \n void DebayerCpu::updateLookupTables(DebayerParams &params)\n@@ -789,11 +778,15 @@ void DebayerCpu::updateLookupTables(DebayerParams &params)\n \tif (gammaUpdateNeeded)\n \t\tupdateGammaTable(params);\n \n+\t/* Processing order: black level -> gains -> gamma */\n \tauto matrixChanged = [](const Matrix<float, 3, 3> &m1, const Matrix<float, 3, 3> &m2) -> bool {\n \t\treturn !std::equal(m1.data().begin(), m1.data().end(), m2.data().begin());\n \t};\n \tconst unsigned int gammaTableSize = gammaTable_.size();\n-\tconst double div = static_cast<double>(kRGBLookupSize) / gammaTableSize;\n+\tRGB<float> blackIndex = params.blackLevel * kRGBLookupSize;\n+\tif (swapRedBlueGains_)\n+\t\tblackIndex = RGB<float>({ blackIndex.b(), blackIndex.g(), blackIndex.r() });\n+\n \tif (ccmEnabled_) {\n \t\tif (gammaUpdateNeeded ||\n \t\t    matrixChanged(params.combinedMatrix, params_.combinedMatrix)) {\n@@ -803,17 +796,22 @@ void DebayerCpu::updateLookupTables(DebayerParams &params)\n \t\t\tconst unsigned int redIndex = swapRedBlueGains_ ? 2 : 0;\n \t\t\tconst unsigned int greenIndex = 1;\n \t\t\tconst unsigned int blueIndex = swapRedBlueGains_ ? 0 : 2;\n+\t\t\tconst RGB<float> div =\n+\t\t\t\t(RGB<float>(kRGBLookupSize) - blackIndex).max(1.0) /\n+\t\t\t\tkRGBLookupSize;\n \t\t\tfor (unsigned int i = 0; i < kRGBLookupSize; i++) {\n-\t\t\t\tred[i].r = std::round(i * params.combinedMatrix[redIndex][0]);\n-\t\t\t\tred[i].g = std::round(i * params.combinedMatrix[greenIndex][0]);\n-\t\t\t\tred[i].b = std::round(i * params.combinedMatrix[blueIndex][0]);\n-\t\t\t\tgreen[i].r = std::round(i * params.combinedMatrix[redIndex][1]);\n-\t\t\t\tgreen[i].g = std::round(i * params.combinedMatrix[greenIndex][1]);\n-\t\t\t\tgreen[i].b = std::round(i * params.combinedMatrix[blueIndex][1]);\n-\t\t\t\tblue[i].r = std::round(i * params.combinedMatrix[redIndex][2]);\n-\t\t\t\tblue[i].g = std::round(i * params.combinedMatrix[greenIndex][2]);\n-\t\t\t\tblue[i].b = std::round(i * params.combinedMatrix[blueIndex][2]);\n-\t\t\t\tgammaLut_[i] = gammaTable_[i / div];\n+\t\t\t\tconst RGB<float> rgb = ((RGB<float>(i) - blackIndex) / div).max(0.0);\n+\t\t\t\tred[i].r = std::round(rgb.r() * params.combinedMatrix[redIndex][0]);\n+\t\t\t\tred[i].g = std::round(rgb.g() * params.combinedMatrix[greenIndex][0]);\n+\t\t\t\tred[i].b = std::round(rgb.b() * params.combinedMatrix[blueIndex][0]);\n+\t\t\t\tgreen[i].r = std::round(rgb.r() * params.combinedMatrix[redIndex][1]);\n+\t\t\t\tgreen[i].g = std::round(rgb.g() * params.combinedMatrix[greenIndex][1]);\n+\t\t\t\tgreen[i].b = std::round(rgb.b() * params.combinedMatrix[blueIndex][1]);\n+\t\t\t\tblue[i].r = std::round(rgb.r() * params.combinedMatrix[redIndex][2]);\n+\t\t\t\tblue[i].g = std::round(rgb.g() * params.combinedMatrix[greenIndex][2]);\n+\t\t\t\tblue[i].b = std::round(rgb.b() * params.combinedMatrix[blueIndex][2]);\n+\t\t\t\t;\n+\t\t\t\tgammaLut_[i] = gammaTable_[i * gammaTableSize / kRGBLookupSize];\n \t\t\t}\n \t\t}\n \t} else {\n@@ -822,12 +820,17 @@ void DebayerCpu::updateLookupTables(DebayerParams &params)\n \t\t\tauto &red = swapRedBlueGains_ ? blue_ : red_;\n \t\t\tauto &green = green_;\n \t\t\tauto &blue = swapRedBlueGains_ ? red_ : blue_;\n+\t\t\tconst RGB<float> div =\n+\t\t\t\t(RGB<float>(kRGBLookupSize) - blackIndex).max(1.0) /\n+\t\t\t\tgammaTableSize;\n \t\t\tfor (unsigned int i = 0; i < kRGBLookupSize; i++) {\n-\t\t\t\t/* Apply gamma after gain! */\n-\t\t\t\tconst RGB<float> lutGains = (gains * i / div).min(gammaTableSize - 1);\n-\t\t\t\tred[i] = gammaTable_[static_cast<unsigned int>(lutGains.r())];\n-\t\t\t\tgreen[i] = gammaTable_[static_cast<unsigned int>(lutGains.g())];\n-\t\t\t\tblue[i] = gammaTable_[static_cast<unsigned int>(lutGains.b())];\n+\t\t\t\tconst RGB<float> lutGains =\n+\t\t\t\t\t(gains * (RGB<float>(i) - blackIndex) / div)\n+\t\t\t\t\t\t.min(gammaTableSize - 1)\n+\t\t\t\t\t\t.max(0.0);\n+\t\t\t\tred[i] = gammaTable_[lutGains.r()];\n+\t\t\t\tgreen[i] = gammaTable_[lutGains.g()];\n+\t\t\t\tblue[i] = gammaTable_[lutGains.b()];\n \t\t\t}\n \t\t}\n \t}\n","prefixes":[]}