Show a patch.

GET /api/1.1/patches/27001/?format=api
HTTP 200 OK
Allow: GET, PUT, PATCH, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept

{
    "id": 27001,
    "url": "https://patchwork.libcamera.org/api/1.1/patches/27001/?format=api",
    "web_url": "https://patchwork.libcamera.org/patch/27001/",
    "project": {
        "id": 1,
        "url": "https://patchwork.libcamera.org/api/1.1/projects/1/?format=api",
        "name": "libcamera",
        "link_name": "libcamera",
        "list_id": "libcamera_core",
        "list_email": "libcamera-devel@lists.libcamera.org",
        "web_url": "",
        "scm_url": "",
        "webscm_url": ""
    },
    "msgid": "<20260621-kbingham-awb-saturation-v1-5-b91ea59c6cfb@ideasonboard.com>",
    "date": "2026-06-20T23:00:32",
    "name": "[5/7] libcamera: software_isp: Fix black level handling in CPU ISP",
    "commit_ref": null,
    "pull_url": null,
    "state": "new",
    "archived": false,
    "hash": "677972971157711d419021ae13efe42a69d00241",
    "submitter": {
        "id": 4,
        "url": "https://patchwork.libcamera.org/api/1.1/people/4/?format=api",
        "name": "Kieran Bingham",
        "email": "kieran.bingham@ideasonboard.com"
    },
    "delegate": null,
    "mbox": "https://patchwork.libcamera.org/patch/27001/mbox/",
    "series": [
        {
            "id": 6009,
            "url": "https://patchwork.libcamera.org/api/1.1/series/6009/?format=api",
            "web_url": "https://patchwork.libcamera.org/project/libcamera/list/?series=6009",
            "date": "2026-06-20T23:00:27",
            "name": "softisp: Fix Saturation and Black level handling",
            "version": 1,
            "mbox": "https://patchwork.libcamera.org/series/6009/mbox/"
        }
    ],
    "comments": "https://patchwork.libcamera.org/api/patches/27001/comments/",
    "check": "pending",
    "checks": "https://patchwork.libcamera.org/api/patches/27001/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 21CF8C3308\n\tfor <parsemail@patchwork.libcamera.org>;\n\tSat, 20 Jun 2026 23:00:49 +0000 (UTC)",
            "from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id E7273656EC;\n\tSun, 21 Jun 2026 01:00:44 +0200 (CEST)",
            "from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[IPv6:2001:4b98:dc2:55:216:3eff:fef7:d647])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 8867B656E2\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tSun, 21 Jun 2026 01:00:36 +0200 (CEST)",
            "from [192.168.0.240]\n\t(cpc89244-aztw30-2-0-cust6594.18-1.cable.virginm.net [86.31.185.195])\n\tby perceval.ideasonboard.com (Postfix) with ESMTPSA id C66F817D8;\n\tSun, 21 Jun 2026 00:59:59 +0200 (CEST)"
        ],
        "Authentication-Results": "lancelot.ideasonboard.com; dkim=pass (1024-bit key;\n\tunprotected) header.d=ideasonboard.com header.i=@ideasonboard.com\n\theader.b=\"V9kY6MYP\"; dkim-atps=neutral",
        "DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1781996399;\n\tbh=G2KmPxn7MDxy+1ldnSusw90wucEnlxKSCdpUja9ph6Q=;\n\th=From:Date:Subject:References:In-Reply-To:To:Cc:From;\n\tb=V9kY6MYPZZ3eaciHXMzRxocXUoTi1AuMMAGIqMbmALnMibRQpxcooa40ZUohXNBVk\n\tjWQHf4c050BqI3PXHplDOixqdY3ZkSFIPfE4oUulfWssgg1cc1xyeZ9z4XrzIjm4rJ\n\tW+OEVJmMhnCikhjj1dFlkvUKuaWnr5aWAUw+daBY=",
        "From": "Kieran Bingham <kieran.bingham@ideasonboard.com>",
        "Date": "Sun, 21 Jun 2026 00:00:32 +0100",
        "Subject": "[PATCH 5/7] libcamera: software_isp: Fix black level handling in\n\tCPU ISP",
        "MIME-Version": "1.0",
        "Content-Type": "text/plain; charset=\"utf-8\"",
        "Content-Transfer-Encoding": "7bit",
        "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 <kieran.bingham@ideasonboard.com>, \n\tMilan Zamazal <mzamazal@redhat.com>",
        "X-Mailer": "b4 0.14.2",
        "X-Developer-Signature": "v=1; a=ed25519-sha256; t=1781996434; l=6483;\n\ti=kieran.bingham@ideasonboard.com; s=20260204;\n\th=from:subject:message-id; \n\tbh=Z3taOw8EuwTfN3kX7QTe2JdvX1aHOQdk8busc/t9/sA=;\n\tb=9mzyxOpJVt6w4nRt6lwjgIoRJgFllh/xBQuzia5Bm+d/93RFs+TjNbMVjdntzqRr68Jw+OhKL\n\tX3IpGqL+PyuAO3wy3iOAlC9Q9UsRus7aENTK548y39W0m9QpE517iEi",
        "X-Developer-Key": "i=kieran.bingham@ideasonboard.com; a=ed25519;\n\tpk=IOxS2C6nWHNjLfkDR71Iesk904i6wJDfEERqV7hDBdY=",
        "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": "From: Milan Zamazal <mzamazal@redhat.com>\n\nThe 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\n[Kieran: Fix indexing from Milans' suggestion, use new clamp]\n[Kieran: Use enumerate, and div updates from Laurent's suggestion]\nSigned-off-by: Milan Zamazal <mzamazal@redhat.com>\n---\n src/libcamera/software_isp/debayer_cpu.cpp | 60 ++++++++++++++----------------\n 1 file changed, 28 insertions(+), 32 deletions(-)",
    "diff": "diff --git a/src/libcamera/software_isp/debayer_cpu.cpp b/src/libcamera/software_isp/debayer_cpu.cpp\nindex d2596d32bbcdeaaab2e2c287e3f01ae22c442884..9d6a08b3333f988570cb5d3f7cea4c117f7c2530 100644\n--- a/src/libcamera/software_isp/debayer_cpu.cpp\n+++ b/src/libcamera/software_isp/debayer_cpu.cpp\n@@ -979,32 +979,20 @@ void DebayerCpuThread::process4(uint32_t frame, const uint8_t *src, uint8_t *dst\n \n void DebayerCpu::updateGammaTable(const 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 (auto [i, value] : utils::enumerate(gammaTable_)) {\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 \t\t\tnormalized = 0.5 * std::pow(normalized / 0.5, contrastExp);\n \t\telse\n \t\t\tnormalized = 1.0 - 0.5 * std::pow((1.0 - normalized) / 0.5, contrastExp);\n-\t\tgammaTable_[i] = UINT8_MAX *\n-\t\t\t\t std::pow(normalized, gamma);\n+\t\tvalue = UINT8_MAX * 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(const DebayerParams &params)\n@@ -1016,11 +1004,15 @@ void DebayerCpu::updateLookupTables(const 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+\n+\tconst RGB<float> blackIndex = params.blackLevel * kRGBLookupSize;\n+\tconst RGB<float> div = (RGB<float>(kRGBLookupSize) - blackIndex).max(1.0);\n+\n \tif (ccmEnabled_) {\n \t\tif (gammaUpdateNeeded ||\n \t\t    matrixChanged(params.combinedMatrix, params_.combinedMatrix)) {\n@@ -1030,17 +1022,19 @@ void DebayerCpu::updateLookupTables(const 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+\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) * kRGBLookupSize / 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.r() * params.combinedMatrix[greenIndex][0]);\n+\t\t\t\tred[i].b = std::round(rgb.r() * params.combinedMatrix[blueIndex][0]);\n+\t\t\t\tgreen[i].r = std::round(rgb.g() * 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.g() * params.combinedMatrix[blueIndex][1]);\n+\t\t\t\tblue[i].r = std::round(rgb.b() * params.combinedMatrix[redIndex][2]);\n+\t\t\t\tblue[i].g = std::round(rgb.b() * params.combinedMatrix[greenIndex][2]);\n+\t\t\t\tblue[i].b = std::round(rgb.b() * params.combinedMatrix[blueIndex][2]);\n+\t\t\t\tgammaLut_[i] = gammaTable_[i * gammaTableSize / kRGBLookupSize];\n \t\t\t}\n \t\t}\n \t} else {\n@@ -1049,12 +1043,14 @@ void DebayerCpu::updateLookupTables(const 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+\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) * gammaTableSize / div)\n+\t\t\t\t\t\t.clamp(0.0, gammaTableSize - 1);\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": [
        "5/7"
    ]
}