Patch Detail
Show a patch.
GET /api/patches/26761/?format=api
{ "id": 26761, "url": "https://patchwork.libcamera.org/api/patches/26761/?format=api", "web_url": "https://patchwork.libcamera.org/patch/26761/", "project": { "id": 1, "url": "https://patchwork.libcamera.org/api/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": "<20260514-ov2740-tuning-v4-2-5d59b40abfef@jetm.me>", "date": "2026-05-14T20:01:49", "name": "[v4,2/2] utils: tuning: Add AIQB parser for Intel IPU6 sensors", "commit_ref": null, "pull_url": null, "state": "new", "archived": false, "hash": "356a6ae85d76de01d5724b44bb9bb08efb64f79b", "submitter": { "id": 261, "url": "https://patchwork.libcamera.org/api/people/261/?format=api", "name": "Javier Tia", "email": "floss@jetm.me" }, "delegate": null, "mbox": "https://patchwork.libcamera.org/patch/26761/mbox/", "series": [ { "id": 5948, "url": "https://patchwork.libcamera.org/api/series/5948/?format=api", "web_url": "https://patchwork.libcamera.org/project/libcamera/list/?series=5948", "date": "2026-05-14T20:01:47", "name": "Add a calibrated tuning file for the OV2740 sensor based on the Intel", "version": 4, "mbox": "https://patchwork.libcamera.org/series/5948/mbox/" } ], "comments": "https://patchwork.libcamera.org/api/patches/26761/comments/", "check": "pending", "checks": "https://patchwork.libcamera.org/api/patches/26761/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 6DA8CC32F7\n\tfor <parsemail@patchwork.libcamera.org>;\n\tThu, 14 May 2026 20:01:58 +0000 (UTC)", "from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 6550063022;\n\tThu, 14 May 2026 22:01:57 +0200 (CEST)", "from fhigh-b3-smtp.messagingengine.com\n\t(fhigh-b3-smtp.messagingengine.com [202.12.124.154])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id A110A63025\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tThu, 14 May 2026 22:01:54 +0200 (CEST)", "from phl-compute-02.internal (phl-compute-02.internal\n\t[10.202.2.42])\n\tby mailfhigh.stl.internal (Postfix) with ESMTP id C275C7A0053\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tThu, 14 May 2026 16:01:53 -0400 (EDT)", "from phl-imap-07 ([10.202.2.97])\n\tby phl-compute-02.internal (MEProxy); Thu, 14 May 2026 16:01:53 -0400", "by mailuser.phl.internal (Postfix, from userid 501)\n\tid 80EF41EA006B; Thu, 14 May 2026 16:01:53 -0400 (EDT)" ], "Authentication-Results": "lancelot.ideasonboard.com; dkim=pass (2048-bit key;\n\tunprotected) header.d=jetm.me header.i=@jetm.me header.b=\"r8QsDpEe\";\n\tdkim=pass (2048-bit key;\n\tunprotected) header.d=messagingengine.com\n\theader.i=@messagingengine.com header.b=\"lL/FSh/X\"; \n\tdkim-atps=neutral", "DKIM-Signature": [ "v=1; a=rsa-sha256; c=relaxed/relaxed; d=jetm.me; h=cc\n\t:content-transfer-encoding:content-type:content-type:date:date\n\t:from:from:in-reply-to:in-reply-to:message-id:mime-version\n\t:references:reply-to:subject:subject:to:to; s=fm2; t=1778788913;\n\tx=1778875313; bh=fZq8EJTkljr6qchHJWWzJ+vDKc1flX5PrThPvbc//r0=; b=\n\tr8QsDpEeLBfyf7CBab/HmQti1N1jFHutfmtfnjMXInLktQwz41KCEmAMlWc+jJ2W\n\tZFW5OoFL3FGKirkgmkoiLi/cs1SDuwXK0O6yLuDTQGScP/A4cXjxA1BhSnaRw6g5\n\t27xQ9MhwXPOSj4jL1VznDdOzKxLHYWqLmd7XMeVkxC5KsAAISqNuanGdix/uwAUU\n\tG3sQA/nuLrow4k96VbRoiRcvRMqebublFPheFw2CcRp5T7+LtXgzZhrE03tzSYHS\n\tmi7/LMuFvqeuf3iYivDhMPaQ7eqdeZH4MsIldT/ZjgT35CLhjaRPxYkzGIKun0sD\n\t7WFirZQnGjNt0t85k33GoQ==", "v=1; a=rsa-sha256; c=relaxed/relaxed; d=\n\tmessagingengine.com; h=cc:content-transfer-encoding:content-type\n\t:content-type:date:date:feedback-id:feedback-id:from:from\n\t:in-reply-to:in-reply-to:message-id:mime-version:references\n\t:reply-to:subject:subject:to:to:x-me-proxy:x-me-sender\n\t:x-me-sender:x-sasl-enc; s=fm3; t=1778788913; x=1778875313; bh=f\n\tZq8EJTkljr6qchHJWWzJ+vDKc1flX5PrThPvbc//r0=; b=lL/FSh/XGjQFuJzMc\n\tJ/xeNuvy2de4TxpZY9SyIBGh0907LutU+upd/jCX4DSmsZs8FVeRt3LRmCXWM+lk\n\tcwYdhzYHXNUrxJduOLfbHr+jVgFae/NbhAUrCgoFZpQyILF0x66khflTESOXcPrr\n\tH8MHT44PgS3IxMG0jvPiwUBpjWCzzqAAfKA6S6HXjRYiWTdoSK3pWOFrJgp2YZOA\n\tZf1dLstmGMT4UsB8eNheuk0th/qjLK6HqItUIAiaHrHcByGrHn+3wDs04bFe+Gwv\n\tXxaSzCX8LQ7wKRkpJAwOHZzeNw0xZGmHQM7zQCei2u+hTmo+fJ4KM+OXw3oNgxUV\n\tnYaaQ==" ], "X-ME-Sender": "<xms:MSoGapCr28IxoH_F8r6aep2R991j1hJDSz74Yir8W0augQBF2wh_NQ>\n\t<xme:MSoGaiVA-jeiXLKtgt4S23cSDePTzViU_9J2Vgv2K013w3JdFbuxNIFm7aTu7iyhR\n\tUdn-E5akX1ggcM8JWBjf1QVvgicyKix2fdZOtq1LjGDxbOfxgdBsfs>", "X-ME-Proxy-Cause": "gggruggvucftvghtrhhoucdtuddrgeefhedrtddtgdduvdekgedvucetufdoteggodetrf\n\tdotffvucfrrhhofhhilhgvmecuhfgrshhtofgrihhlpdfurfetoffkrfgpnffqhgenuceu\n\trghilhhouhhtmecufedttdenucenucfjughrpefohfffufggtgfgkffvofgjfhesthejre\n\tdtredtjeenucfhrhhomheplfgrvhhivghrucfvihgruceofhhlohhsshesjhgvthhmrdhm\n\tvgeqnecuggftrfgrthhtvghrnhephedvudeuueethfdtteelheegfeehieehleefffetke\n\tehfffggfeiieevtdeugfeinecuvehluhhsthgvrhfuihiivgepudenucfrrghrrghmpehm\n\trghilhhfrhhomhepfhhlohhsshesjhgvthhmrdhmvgdpnhgspghrtghpthhtohepuddpmh\n\thouggvpehsmhhtphhouhhtpdhrtghpthhtoheplhhisggtrghmvghrrgdquggvvhgvlhes\n\tlhhishhtshdrlhhisggtrghmvghrrgdrohhrgh", "X-ME-Proxy": "<xmx:MSoGauTQTnI-BRJmm3FMFxe5-5-m-JONC_8InPm1L0qMjHEPXH3lkg>\n\t<xmx:MSoGajtK4S1LxY8mER5j6n_GZCfBVysBGaIrBieklxYQlxQCopniaA>\n\t<xmx:MSoGaouSJit1q8PZ3BezvdrcSItdzF8evFnJSM7LRjht87Jk4yDypw>\n\t<xmx:MSoGahw05Grs5qbHiOFIF6HcvNwNu7AAhLPW0tRrlGtUdOE3MUzzKw>\n\t<xmx:MSoGaqkwFbupEgjgQ2roeUFRMi6yyHAWbzvyMZY2YT3VMslhE53a5LSo>", "Feedback-ID": "i9dde48b3:Fastmail", "X-Mailer": [ "MessagingEngine.com Webmail Interface", "b4 0.15.2" ], "From": "Javier Tia <floss@jetm.me>", "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", "Content-Type": "text/plain; charset=\"utf-8\"", "Content-Transfer-Encoding": "7bit", "Message-Id": "<20260514-ov2740-tuning-v4-2-5d59b40abfef@jetm.me>", "To": "libcamera-devel@lists.libcamera.org", "X-Developer-Signature": "v=1; a=openpgp-sha256; l=13107; i=floss@jetm.me;\n\th=from:subject:message-id;\n\tbh=zLEZJnbL/f9IDk7PLYWHX+loEL0ZBAf+qBOeFcEUo3w=; \n\tb=owEB7QES/pANAwAKAbXuwwuoZ3cfAcsmYgBqBiosdbdstxLmGNi5AfVea0/5SqXkx9y8y264d\n\tMrg4yWnAXKJAbMEAAEKAB0WIQSbE7ILzw7eI0VKk8m17sMLqGd3HwUCagYqLAAKCRC17sMLqGd3\n\tHz7KC/9sC7qQGrfbaw/V7KQuah2P6822Cs/HrYCR4yoESHjXEbj3Bj1VrZkv6NPUcgObp0D2dUI\n\tHZ8yKj916z9u0sUXUAuTSAW/mp8wZxErXps2xibccVFKfSqBVmdJ1siqI0qZRlJNwPZ9MwXFE1G\n\tzytYLzZ/Fa9JMLyqHKl3zdkQIZ+EhtHZPoKDKW820b0tahyjOOEY5IE4UrdCIsfddy5Px9P5Qr6\n\thh3ibPjT3QDcb9C7EV5+2GMpxUPaONk4jmkGNswuN0v2pishKoYTQruLsaMGzr7rNDFwhUVLBON\n\tJGWIEJBDrx5Ts78lsoG7xIEe80Ly/Ho4mrHSNqKSuDHh1n+QmNGs8KWd9wbiAgM848civRAJlY4\n\t95wKXEZ4ND0JpDo4SJ+y5h3TwrYOMjbBw0hQiWo87MEwl8NsuD+RyE9D4HFgczMZV2JzLo1vwYh\n\te0peoa0RcBhkprXsNJum/0e1oSLNXMzadDpgcZY+i6Rv3QP+iAJoernA7MWu+Ln7CtrTI=", "X-Developer-Key": "i=floss@jetm.me; a=openpgp;\n\tfpr=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": "<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": "Add a Python script to extract CCMs and AWB chromaticity limits from\nIntel AIQB binary calibration files, producing a ready-to-use\nlibcamera Simple IPA tuning YAML.\n\nAIQB is Intel's proprietary calibration format shipped with Windows\ncamera drivers for Intel IPU6 sensors. Files for Alder Lake and Tiger\nLake sensors are available in the ipu6-camera-hal repository under\nconfig/linux/ipu6ep/, or can be extracted from OEM Windows driver\ninstallers using p7zip and innoextract.\n\nThe script supports record id=25 (advanced color matrices, float\nformat with CCT in Kelvin directly) and falls back to record id=18\n(integer matrices with autodetected scale). Record id=25 is preferred\nand present in all Alder Lake AIQB files examined.\n\nTested against OV2740_CJFLE23_ADL.aiqb (Lenovo ThinkPad X1 Carbon\nGen 10, extracted from n3ace31w.exe). Other AIQB files may require\nadjustments if the record layout differs.\n\nSigned-off-by: Javier Tia <floss@jetm.me>\n---\n utils/tuning/parse_aiqb.py | 335 +++++++++++++++++++++++++++++++++++++++++++++\n 1 file changed, 335 insertions(+)", "diff": "diff --git a/utils/tuning/parse_aiqb.py b/utils/tuning/parse_aiqb.py\nnew file mode 100644\nindex 00000000..2308f967\n--- /dev/null\n+++ b/utils/tuning/parse_aiqb.py\n@@ -0,0 +1,335 @@\n+#!/usr/bin/env python3\n+# SPDX-License-Identifier: GPL-2.0-or-later\n+#\n+# Parse an Intel AIQB (CPFF) binary to extract CCMs and AWB colour gains\n+# for use in a libcamera Simple IPA tuning YAML file.\n+#\n+# Format reverse-engineered from ipu6-camera-hal headers (ia_cmc_types.h).\n+# AIQB files are available in the ipu6-camera-hal repository at\n+# config/linux/ipu6ep/, or can be extracted from OEM Windows camera\n+# driver installers using p7zip and innoextract.\n+\n+import argparse\n+import os\n+import struct\n+import sys\n+from dataclasses import dataclass\n+\n+import yaml\n+\n+# ia_mkn_record_header: size(u32), fmt_id(u8), key_id(u8), name_id(u16)\n+REC_HDR = struct.Struct('<IBBH')\n+REC_HDR_SIZE = 8\n+\n+# cmc_name_id enum values\n+CMC_GENERAL_DATA = 2\n+CMC_SENSITIVITY = 7\n+CMC_COLOR_MATRICES = 18\n+CMC_ADV_COLOR_MATRICES = 25\n+\n+# cmc_color_matrix_t (84 bytes):\n+# int32 light_src_type\n+# uint16 r_per_g, b_per_g\n+# uint16 cie_x, cie_y\n+# int32 matrix_accurate[9]\n+# int32 matrix_preferred[9]\n+COLOR_MATRIX = struct.Struct('<i HH HH 9i 9i')\n+assert COLOR_MATRIX.size == 84\n+\n+# Light source enum -> approximate CCT in Kelvin\n+LIGHT_SOURCE_CCT = {\n+ 1: 2856, # A - Incandescent/Tungsten\n+ 4: 5003, # D50\n+ 5: 5503, # D55\n+ 6: 6504, # D65\n+ 7: 7504, # D75\n+ 8: 5454, # E (equal energy)\n+ 9: 6430, # F1 daylight fluorescent\n+ 10: 4230, # F2 cool white\n+ 11: 3450, # F3 white\n+ 12: 3000, # F4 warm white\n+ 13: 6350, # F5\n+ 14: 4150, # F6\n+ 15: 6500, # F7 D65 sim\n+ 16: 5000, # F8 D50 sim\n+ 17: 4150, # F9\n+ 18: 5000, # F10\n+ 19: 4000, # F11\n+ 20: 3000, # F12\n+ 22: 2300, # HZ horizon\n+}\n+\n+# Record chain starts here in all AIQB files checked so far\n+FIRST_RECORD_OFFSET = 0x50\n+\n+\n+@dataclass\n+class ColorMatrixRecord:\n+ light_src_type: int\n+ r_per_g_raw: int\n+ b_per_g_raw: int\n+ cie_x: int\n+ cie_y: int\n+ matrix_accurate: tuple\n+ matrix_preferred: tuple\n+\n+\n+class _FlowList(list):\n+ \"\"\"YAML sequence serialised as a flow sequence (single line).\"\"\"\n+\n+\n+class _Dumper(yaml.Dumper):\n+ pass\n+\n+\n+_Dumper.add_representer(\n+ _FlowList,\n+ lambda dumper, data: dumper.represent_sequence(\n+ 'tag:yaml.org,2002:seq', data, flow_style=True\n+ ),\n+)\n+\n+\n+def walk_records(data):\n+ records = {}\n+ offset = FIRST_RECORD_OFFSET\n+ while offset + REC_HDR_SIZE <= len(data):\n+ size, fmt_id, key_id, name_id = REC_HDR.unpack_from(data, offset)\n+ if size < REC_HDR_SIZE or offset + size > len(data):\n+ break\n+ records[name_id] = (offset, size)\n+ offset += size\n+ return records\n+\n+\n+def extract_general_data(data, offset):\n+ w, h, bd, co = struct.unpack_from('<HHHH', data, offset + REC_HDR_SIZE)\n+ return {'width': w, 'height': h, 'bit_depth': bd, 'color_order': co}\n+\n+\n+def extract_sensitivity(data, offset):\n+ iso, = struct.unpack_from('<H', data, offset + REC_HDR_SIZE)\n+ return iso\n+\n+\n+def extract_color_matrices(data, offset, size):\n+ record_end = offset + size\n+ num, = struct.unpack_from('<H', data, offset + REC_HDR_SIZE)\n+ matrices = []\n+ mat_offset = offset + REC_HDR_SIZE + 2\n+ if mat_offset + num * COLOR_MATRIX.size > record_end:\n+ num = max(0, (record_end - mat_offset) // COLOR_MATRIX.size)\n+ print(f\" WARNING: record id=18 truncated, reading {num} matrices\")\n+ for i in range(num):\n+ unpacked = COLOR_MATRIX.unpack_from(data, mat_offset + i * 84)\n+ rec = ColorMatrixRecord(\n+ light_src_type=unpacked[0],\n+ r_per_g_raw=unpacked[1],\n+ b_per_g_raw=unpacked[2],\n+ cie_x=unpacked[3],\n+ cie_y=unpacked[4],\n+ matrix_accurate=unpacked[5:14],\n+ matrix_preferred=unpacked[14:23],\n+ )\n+ matrices.append({\n+ 'light_src': rec.light_src_type,\n+ 'cct': LIGHT_SOURCE_CCT.get(rec.light_src_type),\n+ 'r_per_g_raw': rec.r_per_g_raw,\n+ 'b_per_g_raw': rec.b_per_g_raw,\n+ 'matrix_raw': rec.matrix_accurate,\n+ })\n+ return matrices\n+\n+\n+def extract_advanced_color_matrices(data, offset, size):\n+ \"\"\"Parse cmc_advanced_color_matrix_correction (record id=25).\n+\n+ Layout after the 8-byte record header:\n+ uint16 num_light_srcs\n+ uint16 num_sectors\n+ uint32 hue_of_sectors[num_sectors]\n+ Per light source (24-byte cmc_acm_color_matrices_info_v101_t):\n+ uint32 src_type\n+ float r_per_g\n+ float b_per_g\n+ float cie_x\n+ float cie_y\n+ uint32 cct (Kelvin, directly)\n+ float traditional[9] (3x3 CCM, rows sum to 1.0)\n+ float advanced[num_sectors][9] (per-sector CCMs, skipped)\n+ \"\"\"\n+ record_end = offset + size\n+ pos = offset + REC_HDR_SIZE\n+ num_ls, num_sectors = struct.unpack_from('<HH', data, pos)\n+ pos += 4\n+ pos += num_sectors * 4 # skip hue_of_sectors\n+\n+ info_fmt = struct.Struct('<IffffI') # 24 bytes\n+ ccm_fmt = struct.Struct('<9f') # 36 bytes\n+ sector_skip = num_sectors * 36\n+\n+ matrices = []\n+ for _ in range(num_ls):\n+ if pos + info_fmt.size > record_end:\n+ print(\" WARNING: record id=25 truncated, stopping early\")\n+ break\n+ src_type, rg, bg, cie_x, cie_y, cct_k = info_fmt.unpack_from(data, pos)\n+ pos += 24\n+ if pos + ccm_fmt.size > record_end:\n+ print(\" WARNING: record id=25 truncated, stopping early\")\n+ break\n+ trad = ccm_fmt.unpack_from(data, pos)\n+ pos += 36\n+ matrices.append({\n+ 'light_src': src_type,\n+ 'cct': cct_k,\n+ 'r_per_g': rg,\n+ 'b_per_g': bg,\n+ 'matrix_float': trad,\n+ })\n+ if pos + sector_skip > record_end:\n+ print(\" WARNING: record id=25 truncated in advanced sectors, stopping early\")\n+ break\n+ pos += sector_skip\n+ return matrices\n+\n+\n+def guess_ccm_scale(matrices):\n+ if not matrices:\n+ return 8192\n+ for scale in (8192, 4096, 2048, 1024):\n+ errors = []\n+ for m in matrices:\n+ for row in range(3):\n+ s = sum(m['matrix_raw'][row * 3:(row + 1) * 3]) / scale\n+ errors.append(abs(s - 1.0))\n+ if max(errors) < 0.05:\n+ return scale\n+ return 8192\n+\n+\n+def main():\n+ parser = argparse.ArgumentParser(\n+ description='Parse Intel AIQB binary for libcamera Simple IPA YAML',\n+ epilog='Tested on OV2740_CJFLE23_ADL.aiqb only. Other sensors may '\n+ 'require adjustments.')\n+ parser.add_argument('aiqb', help='Path to .aiqb file')\n+ parser.add_argument('--sensor-name',\n+ help='Sensor name for YAML output (default: derived '\n+ 'from filename)')\n+ parser.add_argument('--ccm-scale', type=int, default=0,\n+ help='Integer CCM scale for record id=18 (0=autodetect)')\n+ args = parser.parse_args()\n+\n+ sensor_name = args.sensor_name or os.path.basename(args.aiqb).split('_')[0].lower()\n+ output_path = f'{sensor_name}.yaml'\n+\n+ with open(args.aiqb, 'rb') as f:\n+ data = f.read()\n+\n+ print(f\"File: {args.aiqb} ({len(data)} bytes)\")\n+\n+ records = walk_records(data)\n+ print(f\"Records found: {sorted(records.keys())}\\n\")\n+\n+ if CMC_GENERAL_DATA in records:\n+ gd = extract_general_data(data, records[CMC_GENERAL_DATA][0])\n+ print(f\"Sensor: {gd['width']}x{gd['height']}, {gd['bit_depth']}-bit, \"\n+ f\"color_order={gd['color_order']}\")\n+\n+ if CMC_SENSITIVITY in records:\n+ iso = extract_sensitivity(data, records[CMC_SENSITIVITY][0])\n+ print(f\"Base ISO: {iso}\")\n+\n+ adv_mode = False\n+ if CMC_ADV_COLOR_MATRICES in records:\n+ matrices = extract_advanced_color_matrices(data, *records[CMC_ADV_COLOR_MATRICES])\n+ adv_mode = True\n+ print(f\"\\nAdvanced color matrices (id=25, {len(matrices)} entries, float CCMs):\")\n+ elif CMC_COLOR_MATRICES in records:\n+ matrices = extract_color_matrices(data, *records[CMC_COLOR_MATRICES])\n+ print(f\"\\nColor matrices (id=18, {len(matrices)} entries):\")\n+ else:\n+ print(\"ERROR: no color_matrices record found (id=18 or id=25)\")\n+ sys.exit(1)\n+\n+ ccm_scale = args.ccm_scale or (1 if adv_mode else guess_ccm_scale(matrices))\n+\n+ valid_matrices = []\n+ for m in matrices:\n+ cct = m['cct']\n+ if not cct:\n+ continue\n+ if adv_mode:\n+ vals = list(m['matrix_float'])\n+ rg = m['r_per_g']\n+ bg = m['b_per_g']\n+ else:\n+ vals = [v / ccm_scale for v in m['matrix_raw']]\n+ rg = m['r_per_g_raw'] / 256.0\n+ bg = m['b_per_g_raw'] / 256.0\n+ row_sums = [sum(vals[r * 3:(r + 1) * 3]) for r in range(3)]\n+ if max(abs(s - 1.0) for s in row_sums) > 0.05:\n+ # Row sums deviating from 1.0 indicate a bad entry.\n+ # For id=18: the scale factor is wrong (e.g. sums near 2.0 means\n+ # ccm_scale is half the true value); use --ccm-scale to override.\n+ # For id=25: floats are stored directly; deviation means the layout\n+ # does not match ia_cmc_types.h or the entry is corrupt.\n+ print(f\" WARNING: CCT={cct}K row sums {[round(s, 4) for s in row_sums]} - skipping\")\n+ continue\n+ print(f\" CCT={cct}K R/G={rg:.4f} B/G={bg:.4f}\")\n+ valid_matrices.append((cct, vals, rg, bg))\n+\n+ if not valid_matrices:\n+ print(\"ERROR: no valid colour matrices extracted\")\n+ sys.exit(1)\n+\n+ min_rg = min(m[2] for m in valid_matrices)\n+ min_bg = min(m[3] for m in valid_matrices)\n+ max_gain_r = round((1.0 / min_rg) * 1.1, 2) if min_rg > 0 else 2.5\n+ max_gain_b = round((1.0 / min_bg) * 1.1, 2) if min_bg > 0 else 3.2\n+ print(f\"\\nSuggested AWB maxGainR={max_gain_r}, maxGainB={max_gain_b} \"\n+ f\"(from min R/G={min_rg:.4f}, min B/G={min_bg:.4f})\")\n+\n+ sorted_matrices = sorted(valid_matrices)\n+\n+ colour_gains = [\n+ {'ct': cct, 'gains': _FlowList([round(1.0 / rg, 4), round(1.0 / bg, 4)])}\n+ for cct, vals, rg, bg in sorted_matrices\n+ ]\n+\n+ ccms = [\n+ {'ct': cct, 'ccm': _FlowList([round(v, 4) for v in vals])}\n+ for cct, vals, rg, bg in sorted_matrices\n+ ]\n+\n+ aiqb_name = os.path.basename(args.aiqb)\n+ doc = {\n+ 'version': 1,\n+ 'algorithms': [\n+ {'Awb': {\n+ 'algorithm': 'grey',\n+ 'maxGainR': max_gain_r,\n+ 'maxGainB': max_gain_b,\n+ 'speed': 0.25,\n+ 'colourGains': colour_gains,\n+ }},\n+ {'Ccm': {'ccms': ccms}},\n+ {'Adjust': {'gamma': 2.2, 'contrast': 1.0, 'saturation': 1.0}},\n+ {'Agc': {}},\n+ ],\n+ }\n+\n+ with open(output_path, 'w') as f:\n+ f.write('# SPDX-License-Identifier: CC0-1.0\\n')\n+ f.write(f'# Calibrated from {aiqb_name}\\n')\n+ yaml.dump(doc, f, Dumper=_Dumper, default_flow_style=False,\n+ allow_unicode=True, sort_keys=False, version=(1, 1),\n+ explicit_start=True, explicit_end=True)\n+\n+ print(f\"\\nWrote {output_path}\")\n+ print(\"NOTE: Add a BlackLevel entry to the YAML with the sensor's black level.\")\n+\n+\n+if __name__ == '__main__':\n+ main()\n", "prefixes": [ "v4", "2/2" ] }