From patchwork Sun Oct 18 10:27:03 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Tobias Predel X-Patchwork-Id: 28502 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 A7649C3302 for ; Sun, 18 Oct 2026 11:25:04 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 2D60A68E64; Sun, 18 Oct 2026 13:25:02 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (2048-bit key; unprotected) header.d=gmail.com header.i=@gmail.com header.b="s2k6nMRO"; dkim-atps=neutral Received: from mail-wm1-x334.google.com (mail-wm1-x334.google.com [IPv6:2a00:1450:4864:20::334]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id B00E668E22 for ; Sun, 18 Oct 2026 12:27:05 +0200 (CEST) Received: by mail-wm1-x334.google.com with SMTP id 5b1f17b1804b1-4a1dec056ddso5262975e9.1 for ; Sun, 18 Oct 2026 03:27:05 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1792319225; x=1792924025; darn=lists.libcamera.org; h=content-transfer-encoding:mime-version:message-id:date:subject:cc :to:from:from:to:cc:subject:date:message-id:reply-to:content-type; bh=S68/vw98UyUUHWJPdHdlSwhXe/qfyalKk2Es2uaUbkA=; b=s2k6nMROp55np1/sTIw3f/29JFPSsY9qcuVX3xJWAIB1ymx5CuF+SbFehj8kjuA5XX H1b76mrs6z1oG3dz1vGBUtCCv7WeHN6mO6i+QWsQfLjhQwmDdyhChgUANSEBYFAIvfOW 3HKCCKuZfnr3PLI5YIpir3kGrBs8MXNV+1zD4OSxW2fBQrpwUccFLn7gPDxHDFwIiQqe jytIsqOTWkA2GQMTSnJ64UiKpmBU+G7/wJPgWWCRwDFDuDm1K/CrGAaUvb09T4lzoXJW g/I3hod1aYhytdKf19iqZysdbkZihvcgz5KIw8fb0CbFlEMNXMQxn++voLcyCLKqMmtm XmDg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20260707; t=1792319225; x=1792924025; h=content-transfer-encoding:mime-version:message-id:date:subject:cc :to:from:x-gm-gg:x-gm-message-state:from:to:cc:subject:date :message-id:reply-to:content-type; bh=S68/vw98UyUUHWJPdHdlSwhXe/qfyalKk2Es2uaUbkA=; b=P7mVqeOUC51sL3T5ghr/Lw2Fc9vftwBtpJaUuNh8n9q5Fo+Gv51UAeEDh4sH5rbITR uXAnRmRX7ybtqdzi+THaHCiqQf6X0BGLVu8Oa7Vsu/5iSM1aNCgcWyMbJZY6C43L42uU IQ+ZE4IYsPSBNNBrE8WsP5O9ewDCaR46LzCfK0x1ANhzbJv7JE50+SL1OjDsiSKD99mz JTNKXnYno7U/8KvdrLfQGJOMw8wgoFmtX9H6EeXA+BEGz82Z7mn3M1OWpK4YB6lJNL3q zsZDJ+Qyjy0Uj/UDm5KTtjotY0PueLyzqdJ32lAjSm7Gw6iFS6PH60Ip6u5XLV+ZX0UI loVQ== X-Gm-Message-State: AFq9FYIKhgJ5UF8MbqH1qaTV5cI5MUd8QqDZTkbRX4TvCjN6uCLxjQj6 elTCm0xilO1fHicJ1hyTjkmhPOl7IZmfbnWN4DwUgIUMMU7UYQv/6PZmdvvPDA== X-Gm-Gg: AYBFou3bJu6gmFQdelmCA5pVsZymYTjrLENaRheDwwwReywK9cuFP7mQAB1nDF76LPi WkSif4dILKWC8OX8ZP+yfGvPwpH5BT2cLtv+wFh0QsnArBNwkJTOwajqk/jqsEwebavD5Eu1DCE x9rZeyJfM1v1nh4KE1Y9GgXWAb05l/UGvtzk+1Yn2BtiXAnORqpYIi8TYdaWFdv30NXyOIRDaqI CpjLr/DE5LgyHO/WB88vysDWWtdkyf9ElREnF8kddYeEtWjxjWpkXnwKGob2H+J9qK7Y8CUtv5N /rjF1gPwLugYEFpgVosKoT6GlkKoxlqDLXodPmupvfMhpd4oMWmKLqtS+niVjw0jKvtDh9HfimJ wt0wnYScL/tBsKedw2s+a0psstD2tdbheJMrb3iQAnuklJbkBPFkLV/eCc3RDarIwysZL78z/+8 73seFVvM3livGjV+5NDXV2QFwPulSpw55TRoFcXX0oKn+pgfKEpYdiLF6Hi/h9KLyMbWW/10wz X-Received: by 2002:a05:600c:34c3:b0:4a1:df11:45a9 with SMTP id 5b1f17b1804b1-4a1df11472emr56687255e9.4.1792319225138; Sun, 18 Oct 2026 03:27:05 -0700 (PDT) Received: from archlinux ([178.197.200.30]) by smtp.gmail.com with ESMTPSA id 5b1f17b1804b1-4a1d7659805sm151726045e9.7.2026.10.18.03.27.04 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Sun, 18 Oct 2026 03:27:04 -0700 (PDT) From: Tobias Predel To: libcamera-devel@lists.libcamera.org Cc: Tobias Predel Subject: [PATCH] libcamera: software_isp: Exclude saturated pixels from AWB sums Date: Sun, 18 Oct 2026 12:27:03 +0200 Message-ID: <20261018102703.33012-1-tobias.predel@gmail.com> X-Mailer: git-send-email 2.56.0 MIME-Version: 1.0 X-Mailman-Approved-At: Sun, 18 Oct 2026 13:25:01 +0200 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 software ISP AWB computes grey world means from the colour sums of all sampled pixels. When part of the image is overexposed, the strongest raw channel, usually green, clips first, so its mean is underestimated. The resulting red and blue gains are too low and give the whole image a green cast. This is easy to trigger with a bright window or a lamp in view. Leave pixels with any colour component at or above 250/255 of full scale out of the colour sums, and count the pixels that are included, so that AWB divides by the right number. The luminance histogram still covers all pixels, as AGC needs to see the saturated ones. Tested on a Hynix Hi556 with the simple pipeline handler and a ceiling lamp in view (about 5% of the output clipped): the G/R and G/B ratios of the unclipped pixels go from 1.11 and 1.16 to 1.00 and 1.00. With no clipping in the scene, the AWB gains are unchanged. Signed-off-by: Tobias Predel Assisted-by: Claude:claude-opus-5-5 --- Notes: The change in swstats_cpu.cpp applies to both the CPU and the GPU debayer paths, as both use SwStatsCpu. I have also backported it to v0.7.2 (src/ipa/simple) and have been running it as a distribution package. This patch was written with the help of an AI coding assistant (hence the Assisted-by trailer, following the Linux kernel's coding-assistants guidelines; checkstyle doesn't know that trailer key yet). I reviewed the code and ran the tests on the hardware myself. .../internal/software_isp/swisp_stats.h | 7 +++- src/ipa/softisp/algorithms/awb.cpp | 9 +++-- src/libcamera/software_isp/swstats_cpu.cpp | 39 +++++++++++++------ 3 files changed, 39 insertions(+), 16 deletions(-) diff --git a/include/libcamera/internal/software_isp/swisp_stats.h b/include/libcamera/internal/software_isp/swisp_stats.h index d9d0d9be8..f15c8aae5 100644 --- a/include/libcamera/internal/software_isp/swisp_stats.h +++ b/include/libcamera/internal/software_isp/swisp_stats.h @@ -28,9 +28,14 @@ struct SwIspStats { */ bool valid; /** - * \brief Sums of colour channels of all the sampled pixels + * \brief Sums of colour channels of the sampled pixels that are not + * (near) saturated */ RGB sum_; + /** + * \brief Number of sampled pixels included in sum_ + */ + uint64_t sumCount_; /** * \brief Number of bins in the yHistogram */ diff --git a/src/ipa/softisp/algorithms/awb.cpp b/src/ipa/softisp/algorithms/awb.cpp index 55fd326fc..e6b750d87 100644 --- a/src/ipa/softisp/algorithms/awb.cpp +++ b/src/ipa/softisp/algorithms/awb.cpp @@ -7,7 +7,6 @@ #include "awb.h" -#include #include #include @@ -118,16 +117,18 @@ SoftIspAwbStats Awb::calculateRgbMeans(IPAContext &context, if (!stats->valid) return {}; - const SwIspStats::Histogram &histogram = stats->yHistogram; const uint8_t blackLevel = context.activeState.blc.level; + /* Saturated pixels are not included in the sums. */ + const uint64_t nPixels = stats->sumCount_; + if (!nPixels) + return {}; + /* * Black level must be subtracted to get the correct AWB ratios, they * would be off if they were computed from the whole brightness range * rather than from the sensor range. */ - const uint64_t nPixels = std::accumulate( - histogram.begin(), histogram.end(), uint64_t(0)); const uint64_t offset = blackLevel * nPixels; const uint64_t minValid = 1; diff --git a/src/libcamera/software_isp/swstats_cpu.cpp b/src/libcamera/software_isp/swstats_cpu.cpp index 7fb77ce7d..c4391a261 100644 --- a/src/libcamera/software_isp/swstats_cpu.cpp +++ b/src/libcamera/software_isp/swstats_cpu.cpp @@ -11,6 +11,8 @@ #include "libcamera/internal/software_isp/swstats_cpu.h" +#include + #include #include @@ -171,28 +173,40 @@ static constexpr unsigned int kRedYMul = 77; /* 0.299 * 256 */ static constexpr unsigned int kGreenYMul = 150; /* 0.587 * 256 */ static constexpr unsigned int kBlueYMul = 29; /* 0.114 * 256 */ +/* + * Pixels with any colour component at or above this 8-bit level are left out + * of the colour sums. Clipped pixels have lost their colour information and + * would skew the AWB ratios, typically towards the channel that clips last. + */ +static constexpr unsigned int kSaturationLevel = 250; + #define SWSTATS_START_LINE_STATS(pixel_t) \ pixel_t r, g, g2, b; \ uint64_t yVal; \ \ uint64_t sumR = 0; \ uint64_t sumG = 0; \ - uint64_t sumB = 0; - -#define SWSTATS_ACCUMULATE_LINE_STATS(div) \ - sumR += r; \ - sumG += g; \ - sumB += b; \ - \ - yVal = r * kRedYMul; \ - yVal += g * kGreenYMul; \ - yVal += b * kBlueYMul; \ + uint64_t sumB = 0; \ + uint64_t sumCount = 0; + +#define SWSTATS_ACCUMULATE_LINE_STATS(div) \ + if (std::max({ r, g, b }) < kSaturationLevel * (div)) { \ + sumR += r; \ + sumG += g; \ + sumB += b; \ + sumCount++; \ + } \ + \ + yVal = r * kRedYMul; \ + yVal += g * kGreenYMul; \ + yVal += b * kBlueYMul; \ stats.yHistogram[yVal * SwIspStats::kYHistogramSize / (256 * 256 * (div))]++; #define SWSTATS_FINISH_LINE_STATS() \ stats.sum_.r() += sumR; \ stats.sum_.g() += sumG; \ - stats.sum_.b() += sumB; + stats.sum_.b() += sumB; \ + stats.sumCount_ += sumCount; void SwStatsCpu::statsBGGR8Line0(const uint8_t *src[], SwIspStats &stats) { @@ -391,6 +405,7 @@ void SwStatsCpu::startFrame(uint32_t frame) for (auto &s : stats_) { s.sum_ = RGB({ 0, 0, 0 }); + s.sumCount_ = 0; s.yHistogram.fill(0); } } @@ -408,9 +423,11 @@ void SwStatsCpu::finishFrame(uint32_t frame, uint32_t bufferId) if (valid) { sharedStats_->sum_ = RGB({ 0, 0, 0 }); + sharedStats_->sumCount_ = 0; sharedStats_->yHistogram.fill(0); for (const auto &s : stats_) { sharedStats_->sum_ += s.sum_; + sharedStats_->sumCount_ += s.sumCount_; for (unsigned int j = 0; j < SwIspStats::kYHistogramSize; j++) sharedStats_->yHistogram[j] += s.yHistogram[j]; }