[{"id":38858,"web_url":"https://patchwork.libcamera.org/comment/38858/","msgid":"<855x4ut46t.fsf@mzamazal-thinkpadp1gen7.tpbc.csb>","date":"2026-05-11T14:24:10","subject":"Re: [PATCH v2 10/10] test: ipa: libipa: Add CCM row-sum validation\n\ttest","submitter":{"id":177,"url":"https://patchwork.libcamera.org/api/people/177/","name":"Milan Zamazal","email":"mzamazal@redhat.com"},"content":"d3vv3 <devve.3@gmail.com> writes:\n\n> Verify that each row of a colour correction matrix sums to 1.0\n> (luminance preservation property). Tests cover:\n> - identity and known-bad inline matrices\n> - a real OV01A10 D65 calibrated CCM\n> - parsing a multi-entry CCM YAML table via Interpolator<Matrix>\n> - detection of a bad entry in YAML\n\nI'm not sure I get the purpose of the test -- it looks like it tests\nmostly stuff defined in the test itself.  What libcamera functionality\nrelated to row sums is it supposed to test?\n\n> Tolerance is 5e-4 to accommodate 4-decimal-place rounding in tuning files.\n>\n> Signed-off-by: d3vv3 <devve.3@gmail.com>\n> ---\n>  test/ipa/libipa/ccm.cpp     | 158 ++++++++++++++++++++++++++++++++++++\n>  test/ipa/libipa/meson.build |   1 +\n>  2 files changed, 159 insertions(+)\n>  create mode 100644 test/ipa/libipa/ccm.cpp\n>\n> diff --git a/test/ipa/libipa/ccm.cpp b/test/ipa/libipa/ccm.cpp\n> new file mode 100644\n> index 00000000..efc0035a\n> --- /dev/null\n> +++ b/test/ipa/libipa/ccm.cpp\n> @@ -0,0 +1,158 @@\n> +/* SPDX-License-Identifier: GPL-2.0-or-later */\n> +/*\n> + * Copyright (C) 2024-2026, Red Hat Inc.\n> + *\n> + * CCM matrix row-sum validation tests\n> + *\n> + * Each row of a colour correction matrix must sum to 1.0 (luminance\n> + * preservation).  This test verifies that property for inline matrix data\n> + * and for matrices parsed from a YAML CCM table.\n> + */\n> +\n> +#include \"../../../src/ipa/libipa/interpolator.h\"\n> +\n> +#include <cmath>\n> +#include <iostream>\n> +#include <string>\n> +#include <unistd.h>\n> +\n> +#include \"libcamera/base/file.h\"\n> +#include \"libcamera/internal/matrix.h\"\n> +#include \"libcamera/internal/yaml_parser.h\"\n> +\n> +#include \"test.h\"\n> +\n> +using namespace std;\n> +using namespace libcamera;\n> +using namespace ipa;\n> +\n> +/* Tolerance for floating-point row-sum comparison.\n> + * CCM values in tuning files are typically given to 4 decimal places,\n> + * which can introduce up to ~0.5e-3 rounding error per row. */\n> +static constexpr float kRowSumTolerance = 5e-4f;\n> +\n> +#define ASSERT_TRUE(cond)                             \\\n> +\tdo {                                          \\\n> +\t\tif (!(cond)) {                        \\\n> +\t\t\tcerr << \"FAIL: \" #cond \"\\n\";  \\\n> +\t\t\treturn TestFail;              \\\n> +\t\t}                                     \\\n> +\t} while (0)\n> +\n> +static bool allRowsSumToOne(const Matrix<float, 3, 3> &m)\n> +{\n> +\tfor (unsigned int row = 0; row < 3; row++) {\n> +\t\tfloat sum = 0.0f;\n> +\t\tfor (unsigned int col = 0; col < 3; col++)\n> +\t\t\tsum += m[row][col];\n> +\t\tif (std::abs(sum - 1.0f) > kRowSumTolerance)\n> +\t\t\treturn false;\n> +\t}\n> +\treturn true;\n> +}\n> +\n> +class CcmRowSumTest : public Test\n> +{\n> +protected:\n> +\tbool writeTempYaml(const std::string &content, std::string &filename)\n> +\t{\n> +\t\tfilename = \"/tmp/libcamera.ccm.test.XXXXXX\";\n> +\t\tint fd = mkstemp(&filename.front());\n> +\t\tif (fd == -1)\n> +\t\t\treturn false;\n> +\t\tssize_t ret = write(fd, content.c_str(), content.size());\n> +\t\tclose(fd);\n> +\t\treturn ret == static_cast<ssize_t>(content.size());\n> +\t}\n> +\n> +\tstd::unique_ptr<ValueNode> parseYaml(const std::string &content)\n> +\t{\n> +\t\tstd::string filename;\n> +\t\tif (!writeTempYaml(content, filename))\n> +\t\t\treturn nullptr;\n> +\n> +\t\tFile file{ filename };\n> +\t\tif (!file.open(File::OpenModeFlag::ReadOnly))\n> +\t\t\treturn nullptr;\n> +\n> +\t\tauto root = YamlParser::parse(file);\n> +\t\tunlink(filename.c_str());\n> +\t\treturn root;\n> +\t}\n> +\n> +\tint run()\n> +\t{\n> +\t\t/* --- 1. Known-good identity matrix --- */\n> +\t\tMatrix<float, 3, 3> identity{ { 1, 0, 0,\n> +\t\t\t\t\t\t0, 1, 0,\n> +\t\t\t\t\t\t0, 0, 1 } };\n> +\t\tASSERT_TRUE(allRowsSumToOne(identity));\n> +\n> +\t\t/* --- 2. Known-bad matrix (rows do not sum to 1) --- */\n> +\t\tMatrix<float, 3, 3> bad{ { 2, 0, 0,\n> +\t\t\t\t\t   0, 1, 0,\n> +\t\t\t\t\t   0, 0, 1 } };\n> +\t\tASSERT_TRUE(!allRowsSumToOne(bad));\n> +\n> +\t\t/* --- 3. Typical calibrated CCM (D65, OV01A10) --- */\n> +\t\tMatrix<float, 3, 3> d65{ {  1.8163f, -0.7062f, -0.1100f,\n> +\t\t\t\t\t   -0.1640f,  1.5736f, -0.4096f,\n> +\t\t\t\t\t   -0.0084f, -0.8294f,  1.8378f } };\n> +\t\tASSERT_TRUE(allRowsSumToOne(d65));\n> +\n> +\t\t/* --- 4. Parse a valid CCM table from YAML and validate --- */\n> +\t\tconst std::string validYaml =\n> +\t\t\t\"- ct: 2856\\n\"\n> +\t\t\t\"  ccm: [  1.1248,  0.2210, -0.3458,\\n\"\n> +\t\t\t\"         -0.4616,  1.7736, -0.3120,\\n\"\n> +\t\t\t\"         -0.4342, -0.9348,  2.3690 ]\\n\"\n> +\t\t\t\"- ct: 6500\\n\"\n> +\t\t\t\"  ccm: [  1.8163, -0.7062, -0.1100,\\n\"\n> +\t\t\t\"         -0.1640,  1.5736, -0.4096,\\n\"\n> +\t\t\t\"         -0.0084, -0.8294,  1.8378 ]\\n\"\n> +\t\t\t\"- ct: 7500\\n\"\n> +\t\t\t\"  ccm: [  1.8953, -0.7980, -0.0973,\\n\"\n> +\t\t\t\"         -0.1539,  1.6001, -0.4462,\\n\"\n> +\t\t\t\"         -0.0101, -0.7800,  1.7902 ]\\n\";\n> +\n> +\t\tauto root = parseYaml(validYaml);\n> +\t\tASSERT_TRUE(root);\n> +\n> +\t\tInterpolator<Matrix<float, 3, 3>> interp;\n> +\t\tASSERT_TRUE(interp.readYaml(*root, \"ct\", \"ccm\") == 0);\n> +\t\tASSERT_TRUE(interp.data().size() == 3);\n> +\n> +\t\tfor (const auto &[ct, m] : interp.data()) {\n> +\t\t\tif (!allRowsSumToOne(m)) {\n> +\t\t\t\tcerr << \"CCM at ct=\" << ct\n> +\t\t\t\t     << \" has a row that does not sum to 1.0\\n\";\n> +\t\t\t\treturn TestFail;\n> +\t\t\t}\n> +\t\t}\n> +\n> +\t\t/* --- 5. Detect a bad entry in YAML --- */\n> +\t\tconst std::string badYaml =\n> +\t\t\t\"- ct: 5000\\n\"\n> +\t\t\t\"  ccm: [  2.0000, -0.7062, -0.1100,\\n\"\n> +\t\t\t\"         -0.1640,  1.5736, -0.4096,\\n\"\n> +\t\t\t\"         -0.0084, -0.8294,  1.8378 ]\\n\";\n> +\n> +\t\tauto badRoot = parseYaml(badYaml);\n> +\t\tASSERT_TRUE(badRoot);\n> +\n> +\t\tInterpolator<Matrix<float, 3, 3>> badInterp;\n> +\t\tASSERT_TRUE(badInterp.readYaml(*badRoot, \"ct\", \"ccm\") == 0);\n> +\n> +\t\tfor (const auto &[ct, m] : badInterp.data()) {\n> +\t\t\tif (allRowsSumToOne(m)) {\n> +\t\t\t\tcerr << \"Expected bad CCM at ct=\" << ct\n> +\t\t\t\t     << \" to fail row-sum check, but it passed\\n\";\n> +\t\t\t\treturn TestFail;\n> +\t\t\t}\n> +\t\t}\n> +\n> +\t\treturn TestPass;\n> +\t}\n> +};\n> +\n> +TEST_REGISTER(CcmRowSumTest)\n> diff --git a/test/ipa/libipa/meson.build b/test/ipa/libipa/meson.build\n> index c3e25587..8c36800c 100644\n> --- a/test/ipa/libipa/meson.build\n> +++ b/test/ipa/libipa/meson.build\n> @@ -1,6 +1,7 @@\n>  # SPDX-License-Identifier: CC0-1.0\n>  \n>  libipa_test = [\n> +    {'name': 'ccm', 'sources': ['ccm.cpp']},\n>      {'name': 'fixedpoint', 'sources': ['fixedpoint.cpp']},\n>      {'name': 'histogram', 'sources': ['histogram.cpp']},\n>      {'name': 'interpolator', 'sources': ['interpolator.cpp']},","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 5CB16BDCBD\n\tfor <parsemail@patchwork.libcamera.org>;\n\tMon, 11 May 2026 14:24:24 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 4ABAE6301E;\n\tMon, 11 May 2026 16:24:23 +0200 (CEST)","from us-smtp-delivery-124.mimecast.com\n\t(us-smtp-delivery-124.mimecast.com [170.10.129.124])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 0F6AF62DC4\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tMon, 11 May 2026 16:24:21 +0200 (CEST)","from mail-wm1-f69.google.com (mail-wm1-f69.google.com\n\t[209.85.128.69]) by relay.mimecast.com with ESMTP with STARTTLS\n\t(version=TLSv1.3, cipher=TLS_AES_256_GCM_SHA384) id\n\tus-mta-655-YUHDbcM4O6CMOW3UWOnqQg-1; Mon, 11 May 2026 10:24:15 -0400","by mail-wm1-f69.google.com with SMTP id\n\t5b1f17b1804b1-488c0120047so26438595e9.0\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tMon, 11 May 2026 07:24:13 -0700 (PDT)","from mzamazal-thinkpadp1gen7.tpbc.csb\n\t(ip-77-48-47-4.net.vodafone.cz. [77.48.47.4])\n\tby smtp.gmail.com with ESMTPSA id\n\t5b1f17b1804b1-48e6fff9ab8sm198333875e9.2.2026.05.11.07.24.10\n\t(version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);\n\tMon, 11 May 2026 07:24:11 -0700 (PDT)"],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (1024-bit key;\n\tunprotected) header.d=redhat.com header.i=@redhat.com\n\theader.b=\"AEY/BEit\"; dkim-atps=neutral","DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com;\n\ts=mimecast20190719; t=1778509460;\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\tin-reply-to:in-reply-to:references:references;\n\tbh=1JVa/S6oQIfhzmsmOQqXstL+H8/NHNJVcqVXzzlPJTo=;\n\tb=AEY/BEit8K341zudOFkeKlKy0bT5NztvBP5pMasgN9x4PEBSygOAzdbN+J12QHxUKXJi4I\n\thiZhXSw4NIXCMmLVn0CeWuwZcfMhXaBDoznhIZXw7WrelEcjHIcpkJkDrYJJ/Dz9XIJzIM\n\tsOrkC1N3fjbYLDkZ/ojvi6iVfjWk8YY=","X-MC-Unique":"YUHDbcM4O6CMOW3UWOnqQg-1","X-Mimecast-MFC-AGG-ID":"YUHDbcM4O6CMOW3UWOnqQg_1778509453","X-Google-DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=1e100.net; s=20251104; t=1778509452; x=1779114252;\n\th=mime-version:user-agent:message-id:date:references:in-reply-to\n\t:subject:cc:to:from:x-gm-gg:x-gm-message-state:from:to:cc:subject\n\t:date:message-id:reply-to;\n\tbh=1JVa/S6oQIfhzmsmOQqXstL+H8/NHNJVcqVXzzlPJTo=;\n\tb=C1P7VFT7zgewTkZE8WuD9t+0pDoIgXpfhTZrw/CRtMRWtMxQV1nsd4N2Z/39c+Qqvy\n\t74gC6GGA2K/7d8ZiZAGkTkx+pXmApI4fhPbC9b+GeFyh2CC05z9+EQhd94ULeUmUL0l+\n\tlgdOuNnwNYCqPzRxU/bE7Wc4YtkqssyisJnNUKMFBtD8b1AOf1Ko2CGcgoK/6o+EawCe\n\tXbDvIu1q5ypbURznDOhXePHZQp1uU8yxKkpb1Jmb+qpzq7JM9t+x0xcTlLL1GHAz7d/C\n\tcYRnwJjdlm+Vvycnz+EBENr7NOhMK0l+Mn/wS2BgfR/mvV/5/sK+6NcIOivnT+JHD1fg\n\tI8yA==","X-Gm-Message-State":"AOJu0YyWHSP5BM/0ve4KXEzqpVR203HIlqghzSHIw1zapOvRmbOUwJ1Q\n\tpujeUEGireNgqmgd/FlKEaEVp4xrEguQinbSJITQ+mf7mFZb53I5rTKaIgQ+UfaM/IKvT/Ty8Uf\n\tlZcf1BPauoW/OldwMwCsu4SU6icO7UUsl/LRqwnDrwQF5BSZn8Em5Puet6AMVUaSqThveyAJJjz\n\tQcfuVsJklYKZmtA4XeL7nnK4OKtUv7uvWMuIObS8GEKnb9qwrt4EeMu0KIVnE=","X-Gm-Gg":"Acq92OFSh7I0tuexjjxpSYdjSKeoGSj0lml+22B5TAxNtCDu3AMk9ovQ6qSdkZnRbHA\n\tp4QWO5RrVf/edODN1ya4KM/ttt0EKHebbpTPhuewJ8AK7WtZddoEgDJHVIA30wfsgLkra6NIk4w\n\tB7DyYYR983RRnBnIc1fU1puWhTU91Iuo+GAPR+QdrAcrk0PszRwBZhGXgIDC7fXwXsH8vlPmfbW\n\tsUHhoVAqqiIhX0LYQBE2Kx3Mtw7jXH4ZEIn6601N4temu33NXsICrD9hzmQeEbtq5hLOByxG5v9\n\tCE1XNap5+MIMase5JW0Kon94bT+ld5KMQUOWu+Szu9XV5EJkr21no4z/7s10ocFlTxLXWMb448Y\n\tBSwo7x5uvmEjjFccU3OsOxoUNONGtg8Ok0bz+mIFi4Te35I9//Lx8FgpcAI0HDxL+CqIhINDb3m\n\titFCKA06aAug==","X-Received":["by 2002:a05:600c:64c5:b0:488:b241:2c5f with SMTP id\n\t5b1f17b1804b1-48e51f4584fmr388774435e9.26.1778509452356; \n\tMon, 11 May 2026 07:24:12 -0700 (PDT)","by 2002:a05:600c:64c5:b0:488:b241:2c5f with SMTP id\n\t5b1f17b1804b1-48e51f4584fmr388773805e9.26.1778509451782; \n\tMon, 11 May 2026 07:24:11 -0700 (PDT)"],"From":"Milan Zamazal <mzamazal@redhat.com>","To":"d3vv3 <devve.3@gmail.com>","Cc":"libcamera-devel@lists.libcamera.org","Subject":"Re: [PATCH v2 10/10] test: ipa: libipa: Add CCM row-sum validation\n\ttest","In-Reply-To":"<20260506230722.1041596-11-devve.3@gmail.com> (d3vv3's message\n\tof \"Thu, 7 May 2026 01:07:22 +0200\")","References":"<20260506230722.1041596-1-devve.3@gmail.com>\n\t<20260506230722.1041596-11-devve.3@gmail.com>","Date":"Mon, 11 May 2026 16:24:10 +0200","Message-ID":"<855x4ut46t.fsf@mzamazal-thinkpadp1gen7.tpbc.csb>","User-Agent":"Gnus/5.13 (Gnus v5.13)","MIME-Version":"1.0","X-Mimecast-Spam-Score":"0","X-Mimecast-MFC-PROC-ID":"xegtDFylBGnrGHVhzRUJhTb3Sk8gOFPHRHpOxmDanCk_1778509453","X-Mimecast-Originator":"redhat.com","Content-Type":"text/plain","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>"}}]