From patchwork Thu May 14 20:01:48 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Javier Tia X-Patchwork-Id: 26760 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 1281EBDCBD for ; Thu, 14 May 2026 20:01:57 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 8C95863024; Thu, 14 May 2026 22:01:54 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (2048-bit key; unprotected) header.d=jetm.me header.i=@jetm.me header.b="bb7C8DJz"; dkim=pass (2048-bit key; unprotected) header.d=messagingengine.com header.i=@messagingengine.com header.b="mEd5Avje"; dkim-atps=neutral Received: from fhigh-b3-smtp.messagingengine.com (fhigh-b3-smtp.messagingengine.com [202.12.124.154]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id CFF7662FE8 for ; Thu, 14 May 2026 22:01:52 +0200 (CEST) Received: from phl-compute-02.internal (phl-compute-02.internal [10.202.2.42]) by mailfhigh.stl.internal (Postfix) with ESMTP id ED4307A006C for ; Thu, 14 May 2026 16:01:51 -0400 (EDT) Received: from phl-imap-07 ([10.202.2.97]) by phl-compute-02.internal (MEProxy); Thu, 14 May 2026 16:01:52 -0400 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=jetm.me; h=cc :content-transfer-encoding:content-type:content-type:date:date :from:from:in-reply-to:in-reply-to:message-id:mime-version :references:reply-to:subject:subject:to:to; s=fm2; t=1778788911; x=1778875311; bh=2gWSqYkwBitTNhg3SSRLFDPdQLDS0MxbIfQ7KDluudQ=; b= bb7C8DJz5Mvx3kbBzwSSV6EEyRKK9fwfAnkHD5yBHeZ3cL6B9nJ7blsj14JXSGUV Pg5SqVU5clgExYwT/li6xjLWi7dwX5xK6soO8fF71iD8erNXqFuW0q2YAdhzraBN v9VHV6MU/LTd+CNEXw8SwGX7a5OQG+xD7dd4DJMTo82xr/y0tlPjhTw/G4A8b5hJ B2Za4Es/G/HLJ17ul8QMQMDocA0kJZpqdh8fwhYbSRLpnvMTXvP1U8DRlz96xK1U iGwkZjYEeuhvHnlqxlHC4dWHQUYUG+1Oa3GpVDV+zgtMHaqLZgr5BuiUZNIdR4o4 3ztpxLFx09bjH6JknyOc+Q== DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d= messagingengine.com; h=cc:content-transfer-encoding:content-type :content-type:date:date:feedback-id:feedback-id:from:from :in-reply-to:in-reply-to:message-id:mime-version:references :reply-to:subject:subject:to:to:x-me-proxy:x-me-sender :x-me-sender:x-sasl-enc; s=fm3; t=1778788911; x=1778875311; bh=2 gWSqYkwBitTNhg3SSRLFDPdQLDS0MxbIfQ7KDluudQ=; b=mEd5AvjeQM2ZBkOmS PTAxUraFeSoyIZxdeBriMk/ViEHBg1qxBE2jHQ/+8Glb+ZvNOqJVzlKSttPbiZs5 +uObsB33dp23TPlBGvs1tmmgbSN92BIFped8plv0/ktR8axggSnuUspfz3Z2wvHs YZ5aGjSx9YTVrVcGC/AvuNq0Zf3zgwpGifb+Ze9iA65nmAUEALknjNezY6UIXwD1 So+51blIKS+GqH/PWFKP7X+2QcTzUAj/lGhczcTwtxCuY5lzZly+1u/pOOA8by8x KGLPMz1WMc5ygiDmfQt4ko8/Hj+htWeoWPZls3jbXsAlHuOpU6X1kTwZAG5wfE0i MVmzw== X-ME-Sender: X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgeefhedrtddtgdduvdekgedvucetufdoteggodetrf dotffvucfrrhhofhhilhgvmecuhfgrshhtofgrihhlpdfurfetoffkrfgpnffqhgenuceu rghilhhouhhtmecufedttdenucenucfjughrpefohfffufggtgfgkffvofgjfhesthejre dtredtjeenucfhrhhomheplfgrvhhivghrucfvihgruceofhhlohhsshesjhgvthhmrdhm vgeqnecuggftrfgrthhtvghrnhephedvudeuueethfdtteelheegfeehieehleefffetke ehfffggfeiieevtdeugfeinecuvehluhhsthgvrhfuihiivgeptdenucfrrghrrghmpehm rghilhhfrhhomhepfhhlohhsshesjhgvthhmrdhmvgdpnhgspghrtghpthhtohepuddpmh houggvpehsmhhtphhouhhtpdhrtghpthhtoheplhhisggtrghmvghrrgdquggvvhgvlhes lhhishhtshdrlhhisggtrghmvghrrgdrohhrgh X-ME-Proxy: Feedback-ID: i9dde48b3:Fastmail Received: by mailuser.phl.internal (Postfix, from userid 501) id A7A0B1EA006B; Thu, 14 May 2026 16:01:51 -0400 (EDT) X-Mailer: MessagingEngine.com Webmail Interface From: Javier Tia Date: Thu, 14 May 2026 14:01:48 -0600 Subject: [PATCH v4 1/2] ipa: simple: data: Add OV2740 tuning file MIME-Version: 1.0 Message-Id: <20260514-ov2740-tuning-v4-1-5d59b40abfef@jetm.me> To: libcamera-devel@lists.libcamera.org X-Mailer: b4 0.15.2 X-Developer-Signature: v=1; a=openpgp-sha256; l=3811; i=floss@jetm.me; h=from:subject:message-id; bh=PF2gc+FYa/uT/H4EVGsAYS/w7sQZDONq0fZ6ceMKq0g=; b=owEB7QES/pANAwAKAbXuwwuoZ3cfAcsmYgBqBiorOW7KgQ9PVMX/wlbEL9q6DsngHdIIPHrFc asUOVofrd2JAbMEAAEKAB0WIQSbE7ILzw7eI0VKk8m17sMLqGd3HwUCagYqKwAKCRC17sMLqGd3 H1FCDACe8k4jjd89qQLkMN9W72m9voxKr/jrFdGDxHTDs9SQ96FRDFSylLL2KNUeQoWOtUgPixZ kvg94IZLeZKSvcO5aKplG0k2QCnBFM/k6sDccAvJlYEbPzrGHMEOqBoEKxSH1vsHEPWNZvxYW0S FuubYl4zG5QgWjGKUfMMNcG83wqp7YoxvowoKma0uXZjC02D0WIDu7fuPIuXFEcsNAeILPbEFsN Hrjc0W13K/wxec0f9ZlouexNYL0EytFa9YNcl0PN/Vv4oPRA9GCvZnHExHZucNdCdhPMwoKAWRo wt6oLWLzHRPzicmkQ85EM/KKaQiU9a2vrZ6jD9NXZufXIm1p971naFmeCxkIOaHTjIetr5hIgyB muHLEdIWPFeDqk2NI2005gsbaleIqW7KsiJEQk4dTFKk3MfKFDRl3cyWfbwpIv0+/b443WHNCBL GPvaG9rE5xAB9WGHGHuyJgMuGbkQtBFYPQyRW8Kd7SEk4oaR/KNhdqkXjbESq/2VC6W38= X-Developer-Key: i=floss@jetm.me; a=openpgp; fpr=9B13B20BCF0EDE23454A93C9B5EEC30BA867771F In-Reply-To: <20260514-ov2740-tuning-v4-0-5d59b40abfef@jetm.me> References: <20260514-ov2740-tuning-v4-0-5d59b40abfef@jetm.me> 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" Add a tuning file for the OV2740 sensor calibrated from the Intel IPU6 AIQB binary (OV2740_CJFLE23_ADL.aiqb) shipped with the Lenovo ThinkPad X1 Carbon Gen 10 (Alder Lake, JP2 module, Chicony CJFLE23 camera). Black level is 0x40 at 10-bit (64 ADU), stored as the 16-bit value 4096 per the BlackLevel algorithm convention (value >> 8 = 16 at 8-bit scale). AWB gain limits are derived from the minimum R/G and B/G chromaticities across the 8 calibrated illuminants (2319 K to 6302 K), with a 10% headroom margin: maxGainR=2.49, maxGainB=3.07. Eight CCMs are extracted from the AIQB advanced color matrix records (record id=25, float format), covering illuminants from 2319 K (incandescent) to 6302 K (daylight). Signed-off-by: Javier Tia --- src/ipa/simple/data/meson.build | 1 + src/ipa/simple/data/ov2740.yaml | 72 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/src/ipa/simple/data/meson.build b/src/ipa/simple/data/meson.build index 92795ee4..e3e4de74 100644 --- a/src/ipa/simple/data/meson.build +++ b/src/ipa/simple/data/meson.build @@ -1,6 +1,7 @@ # SPDX-License-Identifier: CC0-1.0 conf_files = files([ + 'ov2740.yaml', 'uncalibrated.yaml', ]) diff --git a/src/ipa/simple/data/ov2740.yaml b/src/ipa/simple/data/ov2740.yaml new file mode 100644 index 00000000..0704143a --- /dev/null +++ b/src/ipa/simple/data/ov2740.yaml @@ -0,0 +1,72 @@ +# SPDX-License-Identifier: CC0-1.0 +# Calibrated from OV2740_CJFLE23_ADL.aiqb (Lenovo JP2 module, IPU6 ADL) +%YAML 1.1 +--- +version: 1 +algorithms: + - BlackLevel: + blackLevel: 4096 + - Awb: + algorithm: grey + maxGainR: 2.49 + maxGainB: 3.07 + speed: 0.25 + # Forward-compatible with the AwbGrey colourGains interpolation in + # libcamera patch series 5874. Has no effect until that series merges. + colourGains: + - ct: 2319 + gains: [1.0103, 2.7953] + - ct: 2854 + gains: [1.2614, 2.3815] + - ct: 2884 + gains: [1.3996, 2.4229] + - ct: 3239 + gains: [1.5648, 2.2331] + - ct: 3865 + gains: [1.7861, 1.8856] + - ct: 4136 + gains: [1.9005, 1.8955] + - ct: 4939 + gains: [1.9603, 1.6439] + - ct: 6302 + gains: [2.2597, 1.4038] + - Ccm: + ccms: + - ct: 2319 + ccm: [ 1.5938, -0.1714, -0.4224, + -0.6134, 1.9612, -0.3478, + -0.4710, -1.8500, 3.3210 ] + - ct: 2854 + ccm: [ 1.6119, -0.3132, -0.2987, + -0.4418, 1.8227, -0.3809, + -0.1017, -1.3958, 2.4975 ] + - ct: 2884 + ccm: [ 1.7739, -0.6655, -0.1085, + -0.4113, 1.6619, -0.2506, + -0.0150, -1.1661, 2.1811 ] + - ct: 3239 + ccm: [ 1.8298, -0.6636, -0.1662, + -0.4086, 1.7373, -0.3287, + -0.0500, -1.0836, 2.1335 ] + - ct: 3865 + ccm: [ 1.8836, -0.7430, -0.1406, + -0.3653, 1.7000, -0.3348, + -0.0542, -0.8442, 1.8984 ] + - ct: 4136 + ccm: [ 1.9043, -0.8348, -0.0695, + -0.3241, 1.6389, -0.3148, + 0.0262, -0.9593, 1.9332 ] + - ct: 4939 + ccm: [ 1.6371, -0.4490, -0.1881, + -0.2675, 1.6494, -0.3819, + -0.0245, -0.7782, 1.8026 ] + - ct: 6302 + ccm: [ 1.6401, -0.4418, -0.1984, + -0.2360, 1.7191, -0.4832, + -0.0248, -0.7221, 1.7469 ] + - Adjust: + gamma: 2.2 + contrast: 1.0 + saturation: 1.0 + - Agc: +... From patchwork Thu May 14 20:01:49 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Javier Tia X-Patchwork-Id: 26761 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 6DA8CC32F7 for ; Thu, 14 May 2026 20:01:58 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 6550063022; Thu, 14 May 2026 22:01:57 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (2048-bit key; unprotected) header.d=jetm.me header.i=@jetm.me header.b="r8QsDpEe"; dkim=pass (2048-bit key; unprotected) header.d=messagingengine.com header.i=@messagingengine.com header.b="lL/FSh/X"; dkim-atps=neutral Received: from fhigh-b3-smtp.messagingengine.com (fhigh-b3-smtp.messagingengine.com [202.12.124.154]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id A110A63025 for ; Thu, 14 May 2026 22:01:54 +0200 (CEST) Received: from phl-compute-02.internal (phl-compute-02.internal [10.202.2.42]) by mailfhigh.stl.internal (Postfix) with ESMTP id C275C7A0053 for ; Thu, 14 May 2026 16:01:53 -0400 (EDT) Received: from phl-imap-07 ([10.202.2.97]) by phl-compute-02.internal (MEProxy); Thu, 14 May 2026 16:01:53 -0400 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=jetm.me; h=cc :content-transfer-encoding:content-type:content-type:date:date :from:from:in-reply-to:in-reply-to:message-id:mime-version :references:reply-to:subject:subject:to:to; s=fm2; t=1778788913; x=1778875313; bh=fZq8EJTkljr6qchHJWWzJ+vDKc1flX5PrThPvbc//r0=; b= r8QsDpEeLBfyf7CBab/HmQti1N1jFHutfmtfnjMXInLktQwz41KCEmAMlWc+jJ2W ZFW5OoFL3FGKirkgmkoiLi/cs1SDuwXK0O6yLuDTQGScP/A4cXjxA1BhSnaRw6g5 27xQ9MhwXPOSj4jL1VznDdOzKxLHYWqLmd7XMeVkxC5KsAAISqNuanGdix/uwAUU G3sQA/nuLrow4k96VbRoiRcvRMqebublFPheFw2CcRp5T7+LtXgzZhrE03tzSYHS mi7/LMuFvqeuf3iYivDhMPaQ7eqdeZH4MsIldT/ZjgT35CLhjaRPxYkzGIKun0sD 7WFirZQnGjNt0t85k33GoQ== DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d= messagingengine.com; h=cc:content-transfer-encoding:content-type :content-type:date:date:feedback-id:feedback-id:from:from :in-reply-to:in-reply-to:message-id:mime-version:references :reply-to:subject:subject:to:to:x-me-proxy:x-me-sender :x-me-sender:x-sasl-enc; s=fm3; t=1778788913; x=1778875313; bh=f Zq8EJTkljr6qchHJWWzJ+vDKc1flX5PrThPvbc//r0=; b=lL/FSh/XGjQFuJzMc J/xeNuvy2de4TxpZY9SyIBGh0907LutU+upd/jCX4DSmsZs8FVeRt3LRmCXWM+lk cwYdhzYHXNUrxJduOLfbHr+jVgFae/NbhAUrCgoFZpQyILF0x66khflTESOXcPrr H8MHT44PgS3IxMG0jvPiwUBpjWCzzqAAfKA6S6HXjRYiWTdoSK3pWOFrJgp2YZOA Zf1dLstmGMT4UsB8eNheuk0th/qjLK6HqItUIAiaHrHcByGrHn+3wDs04bFe+Gwv XxaSzCX8LQ7wKRkpJAwOHZzeNw0xZGmHQM7zQCei2u+hTmo+fJ4KM+OXw3oNgxUV nYaaQ== X-ME-Sender: X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgeefhedrtddtgdduvdekgedvucetufdoteggodetrf dotffvucfrrhhofhhilhgvmecuhfgrshhtofgrihhlpdfurfetoffkrfgpnffqhgenuceu rghilhhouhhtmecufedttdenucenucfjughrpefohfffufggtgfgkffvofgjfhesthejre dtredtjeenucfhrhhomheplfgrvhhivghrucfvihgruceofhhlohhsshesjhgvthhmrdhm vgeqnecuggftrfgrthhtvghrnhephedvudeuueethfdtteelheegfeehieehleefffetke ehfffggfeiieevtdeugfeinecuvehluhhsthgvrhfuihiivgepudenucfrrghrrghmpehm rghilhhfrhhomhepfhhlohhsshesjhgvthhmrdhmvgdpnhgspghrtghpthhtohepuddpmh houggvpehsmhhtphhouhhtpdhrtghpthhtoheplhhisggtrghmvghrrgdquggvvhgvlhes lhhishhtshdrlhhisggtrghmvghrrgdrohhrgh X-ME-Proxy: Feedback-ID: i9dde48b3:Fastmail Received: by mailuser.phl.internal (Postfix, from userid 501) id 80EF41EA006B; Thu, 14 May 2026 16:01:53 -0400 (EDT) X-Mailer: MessagingEngine.com Webmail Interface From: Javier Tia Date: Thu, 14 May 2026 14:01:49 -0600 Subject: [PATCH v4 2/2] utils: tuning: Add AIQB parser for Intel IPU6 sensors MIME-Version: 1.0 Message-Id: <20260514-ov2740-tuning-v4-2-5d59b40abfef@jetm.me> To: libcamera-devel@lists.libcamera.org X-Mailer: b4 0.15.2 X-Developer-Signature: v=1; a=openpgp-sha256; l=13107; i=floss@jetm.me; h=from:subject:message-id; bh=zLEZJnbL/f9IDk7PLYWHX+loEL0ZBAf+qBOeFcEUo3w=; b=owEB7QES/pANAwAKAbXuwwuoZ3cfAcsmYgBqBiosdbdstxLmGNi5AfVea0/5SqXkx9y8y264d Mrg4yWnAXKJAbMEAAEKAB0WIQSbE7ILzw7eI0VKk8m17sMLqGd3HwUCagYqLAAKCRC17sMLqGd3 Hz7KC/9sC7qQGrfbaw/V7KQuah2P6822Cs/HrYCR4yoESHjXEbj3Bj1VrZkv6NPUcgObp0D2dUI HZ8yKj916z9u0sUXUAuTSAW/mp8wZxErXps2xibccVFKfSqBVmdJ1siqI0qZRlJNwPZ9MwXFE1G zytYLzZ/Fa9JMLyqHKl3zdkQIZ+EhtHZPoKDKW820b0tahyjOOEY5IE4UrdCIsfddy5Px9P5Qr6 hh3ibPjT3QDcb9C7EV5+2GMpxUPaONk4jmkGNswuN0v2pishKoYTQruLsaMGzr7rNDFwhUVLBON JGWIEJBDrx5Ts78lsoG7xIEe80Ly/Ho4mrHSNqKSuDHh1n+QmNGs8KWd9wbiAgM848civRAJlY4 95wKXEZ4ND0JpDo4SJ+y5h3TwrYOMjbBw0hQiWo87MEwl8NsuD+RyE9D4HFgczMZV2JzLo1vwYh e0peoa0RcBhkprXsNJum/0e1oSLNXMzadDpgcZY+i6Rv3QP+iAJoernA7MWu+Ln7CtrTI= X-Developer-Key: i=floss@jetm.me; a=openpgp; fpr=9B13B20BCF0EDE23454A93C9B5EEC30BA867771F In-Reply-To: <20260514-ov2740-tuning-v4-0-5d59b40abfef@jetm.me> References: <20260514-ov2740-tuning-v4-0-5d59b40abfef@jetm.me> 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" Add a Python script to extract CCMs and AWB chromaticity limits from Intel AIQB binary calibration files, producing a ready-to-use libcamera Simple IPA tuning YAML. AIQB is Intel's proprietary calibration format shipped with Windows camera drivers for Intel IPU6 sensors. Files for Alder Lake and Tiger Lake sensors are available in the ipu6-camera-hal repository under config/linux/ipu6ep/, or can be extracted from OEM Windows driver installers using p7zip and innoextract. The script supports record id=25 (advanced color matrices, float format with CCT in Kelvin directly) and falls back to record id=18 (integer matrices with autodetected scale). Record id=25 is preferred and present in all Alder Lake AIQB files examined. Tested against OV2740_CJFLE23_ADL.aiqb (Lenovo ThinkPad X1 Carbon Gen 10, extracted from n3ace31w.exe). Other AIQB files may require adjustments if the record layout differs. Signed-off-by: Javier Tia --- utils/tuning/parse_aiqb.py | 335 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 335 insertions(+) diff --git a/utils/tuning/parse_aiqb.py b/utils/tuning/parse_aiqb.py new file mode 100644 index 00000000..2308f967 --- /dev/null +++ b/utils/tuning/parse_aiqb.py @@ -0,0 +1,335 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-or-later +# +# Parse an Intel AIQB (CPFF) binary to extract CCMs and AWB colour gains +# for use in a libcamera Simple IPA tuning YAML file. +# +# Format reverse-engineered from ipu6-camera-hal headers (ia_cmc_types.h). +# AIQB files are available in the ipu6-camera-hal repository at +# config/linux/ipu6ep/, or can be extracted from OEM Windows camera +# driver installers using p7zip and innoextract. + +import argparse +import os +import struct +import sys +from dataclasses import dataclass + +import yaml + +# ia_mkn_record_header: size(u32), fmt_id(u8), key_id(u8), name_id(u16) +REC_HDR = struct.Struct(' approximate CCT in Kelvin +LIGHT_SOURCE_CCT = { + 1: 2856, # A - Incandescent/Tungsten + 4: 5003, # D50 + 5: 5503, # D55 + 6: 6504, # D65 + 7: 7504, # D75 + 8: 5454, # E (equal energy) + 9: 6430, # F1 daylight fluorescent + 10: 4230, # F2 cool white + 11: 3450, # F3 white + 12: 3000, # F4 warm white + 13: 6350, # F5 + 14: 4150, # F6 + 15: 6500, # F7 D65 sim + 16: 5000, # F8 D50 sim + 17: 4150, # F9 + 18: 5000, # F10 + 19: 4000, # F11 + 20: 3000, # F12 + 22: 2300, # HZ horizon +} + +# Record chain starts here in all AIQB files checked so far +FIRST_RECORD_OFFSET = 0x50 + + +@dataclass +class ColorMatrixRecord: + light_src_type: int + r_per_g_raw: int + b_per_g_raw: int + cie_x: int + cie_y: int + matrix_accurate: tuple + matrix_preferred: tuple + + +class _FlowList(list): + """YAML sequence serialised as a flow sequence (single line).""" + + +class _Dumper(yaml.Dumper): + pass + + +_Dumper.add_representer( + _FlowList, + lambda dumper, data: dumper.represent_sequence( + 'tag:yaml.org,2002:seq', data, flow_style=True + ), +) + + +def walk_records(data): + records = {} + offset = FIRST_RECORD_OFFSET + while offset + REC_HDR_SIZE <= len(data): + size, fmt_id, key_id, name_id = REC_HDR.unpack_from(data, offset) + if size < REC_HDR_SIZE or offset + size > len(data): + break + records[name_id] = (offset, size) + offset += size + return records + + +def extract_general_data(data, offset): + w, h, bd, co = struct.unpack_from(' record_end: + num = max(0, (record_end - mat_offset) // COLOR_MATRIX.size) + print(f" WARNING: record id=18 truncated, reading {num} matrices") + for i in range(num): + unpacked = COLOR_MATRIX.unpack_from(data, mat_offset + i * 84) + rec = ColorMatrixRecord( + light_src_type=unpacked[0], + r_per_g_raw=unpacked[1], + b_per_g_raw=unpacked[2], + cie_x=unpacked[3], + cie_y=unpacked[4], + matrix_accurate=unpacked[5:14], + matrix_preferred=unpacked[14:23], + ) + matrices.append({ + 'light_src': rec.light_src_type, + 'cct': LIGHT_SOURCE_CCT.get(rec.light_src_type), + 'r_per_g_raw': rec.r_per_g_raw, + 'b_per_g_raw': rec.b_per_g_raw, + 'matrix_raw': rec.matrix_accurate, + }) + return matrices + + +def extract_advanced_color_matrices(data, offset, size): + """Parse cmc_advanced_color_matrix_correction (record id=25). + + Layout after the 8-byte record header: + uint16 num_light_srcs + uint16 num_sectors + uint32 hue_of_sectors[num_sectors] + Per light source (24-byte cmc_acm_color_matrices_info_v101_t): + uint32 src_type + float r_per_g + float b_per_g + float cie_x + float cie_y + uint32 cct (Kelvin, directly) + float traditional[9] (3x3 CCM, rows sum to 1.0) + float advanced[num_sectors][9] (per-sector CCMs, skipped) + """ + record_end = offset + size + pos = offset + REC_HDR_SIZE + num_ls, num_sectors = struct.unpack_from(' record_end: + print(" WARNING: record id=25 truncated, stopping early") + break + src_type, rg, bg, cie_x, cie_y, cct_k = info_fmt.unpack_from(data, pos) + pos += 24 + if pos + ccm_fmt.size > record_end: + print(" WARNING: record id=25 truncated, stopping early") + break + trad = ccm_fmt.unpack_from(data, pos) + pos += 36 + matrices.append({ + 'light_src': src_type, + 'cct': cct_k, + 'r_per_g': rg, + 'b_per_g': bg, + 'matrix_float': trad, + }) + if pos + sector_skip > record_end: + print(" WARNING: record id=25 truncated in advanced sectors, stopping early") + break + pos += sector_skip + return matrices + + +def guess_ccm_scale(matrices): + if not matrices: + return 8192 + for scale in (8192, 4096, 2048, 1024): + errors = [] + for m in matrices: + for row in range(3): + s = sum(m['matrix_raw'][row * 3:(row + 1) * 3]) / scale + errors.append(abs(s - 1.0)) + if max(errors) < 0.05: + return scale + return 8192 + + +def main(): + parser = argparse.ArgumentParser( + description='Parse Intel AIQB binary for libcamera Simple IPA YAML', + epilog='Tested on OV2740_CJFLE23_ADL.aiqb only. Other sensors may ' + 'require adjustments.') + parser.add_argument('aiqb', help='Path to .aiqb file') + parser.add_argument('--sensor-name', + help='Sensor name for YAML output (default: derived ' + 'from filename)') + parser.add_argument('--ccm-scale', type=int, default=0, + help='Integer CCM scale for record id=18 (0=autodetect)') + args = parser.parse_args() + + sensor_name = args.sensor_name or os.path.basename(args.aiqb).split('_')[0].lower() + output_path = f'{sensor_name}.yaml' + + with open(args.aiqb, 'rb') as f: + data = f.read() + + print(f"File: {args.aiqb} ({len(data)} bytes)") + + records = walk_records(data) + print(f"Records found: {sorted(records.keys())}\n") + + if CMC_GENERAL_DATA in records: + gd = extract_general_data(data, records[CMC_GENERAL_DATA][0]) + print(f"Sensor: {gd['width']}x{gd['height']}, {gd['bit_depth']}-bit, " + f"color_order={gd['color_order']}") + + if CMC_SENSITIVITY in records: + iso = extract_sensitivity(data, records[CMC_SENSITIVITY][0]) + print(f"Base ISO: {iso}") + + adv_mode = False + if CMC_ADV_COLOR_MATRICES in records: + matrices = extract_advanced_color_matrices(data, *records[CMC_ADV_COLOR_MATRICES]) + adv_mode = True + print(f"\nAdvanced color matrices (id=25, {len(matrices)} entries, float CCMs):") + elif CMC_COLOR_MATRICES in records: + matrices = extract_color_matrices(data, *records[CMC_COLOR_MATRICES]) + print(f"\nColor matrices (id=18, {len(matrices)} entries):") + else: + print("ERROR: no color_matrices record found (id=18 or id=25)") + sys.exit(1) + + ccm_scale = args.ccm_scale or (1 if adv_mode else guess_ccm_scale(matrices)) + + valid_matrices = [] + for m in matrices: + cct = m['cct'] + if not cct: + continue + if adv_mode: + vals = list(m['matrix_float']) + rg = m['r_per_g'] + bg = m['b_per_g'] + else: + vals = [v / ccm_scale for v in m['matrix_raw']] + rg = m['r_per_g_raw'] / 256.0 + bg = m['b_per_g_raw'] / 256.0 + row_sums = [sum(vals[r * 3:(r + 1) * 3]) for r in range(3)] + if max(abs(s - 1.0) for s in row_sums) > 0.05: + # Row sums deviating from 1.0 indicate a bad entry. + # For id=18: the scale factor is wrong (e.g. sums near 2.0 means + # ccm_scale is half the true value); use --ccm-scale to override. + # For id=25: floats are stored directly; deviation means the layout + # does not match ia_cmc_types.h or the entry is corrupt. + print(f" WARNING: CCT={cct}K row sums {[round(s, 4) for s in row_sums]} - skipping") + continue + print(f" CCT={cct}K R/G={rg:.4f} B/G={bg:.4f}") + valid_matrices.append((cct, vals, rg, bg)) + + if not valid_matrices: + print("ERROR: no valid colour matrices extracted") + sys.exit(1) + + min_rg = min(m[2] for m in valid_matrices) + min_bg = min(m[3] for m in valid_matrices) + max_gain_r = round((1.0 / min_rg) * 1.1, 2) if min_rg > 0 else 2.5 + max_gain_b = round((1.0 / min_bg) * 1.1, 2) if min_bg > 0 else 3.2 + print(f"\nSuggested AWB maxGainR={max_gain_r}, maxGainB={max_gain_b} " + f"(from min R/G={min_rg:.4f}, min B/G={min_bg:.4f})") + + sorted_matrices = sorted(valid_matrices) + + colour_gains = [ + {'ct': cct, 'gains': _FlowList([round(1.0 / rg, 4), round(1.0 / bg, 4)])} + for cct, vals, rg, bg in sorted_matrices + ] + + ccms = [ + {'ct': cct, 'ccm': _FlowList([round(v, 4) for v in vals])} + for cct, vals, rg, bg in sorted_matrices + ] + + aiqb_name = os.path.basename(args.aiqb) + doc = { + 'version': 1, + 'algorithms': [ + {'Awb': { + 'algorithm': 'grey', + 'maxGainR': max_gain_r, + 'maxGainB': max_gain_b, + 'speed': 0.25, + 'colourGains': colour_gains, + }}, + {'Ccm': {'ccms': ccms}}, + {'Adjust': {'gamma': 2.2, 'contrast': 1.0, 'saturation': 1.0}}, + {'Agc': {}}, + ], + } + + with open(output_path, 'w') as f: + f.write('# SPDX-License-Identifier: CC0-1.0\n') + f.write(f'# Calibrated from {aiqb_name}\n') + yaml.dump(doc, f, Dumper=_Dumper, default_flow_style=False, + allow_unicode=True, sort_keys=False, version=(1, 1), + explicit_start=True, explicit_end=True) + + print(f"\nWrote {output_path}") + print("NOTE: Add a BlackLevel entry to the YAML with the sensor's black level.") + + +if __name__ == '__main__': + main()