Patch Detail
Show a patch.
GET /api/patches/26716/?format=api
{ "id": 26716, "url": "https://patchwork.libcamera.org/api/patches/26716/?format=api", "web_url": "https://patchwork.libcamera.org/patch/26716/", "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": "<20260511-ov2740-tuning-v3-3-8ff3e57c7368@jetm.me>", "date": "2026-05-11T18:16:03", "name": "[v3,3/3] utils: tuning: Add AIQB parser for Intel IPU6 sensors", "commit_ref": null, "pull_url": null, "state": "superseded", "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/26716/mbox/", "series": [ { "id": 5933, "url": "https://patchwork.libcamera.org/api/series/5933/?format=api", "web_url": "https://patchwork.libcamera.org/project/libcamera/list/?series=5933", "date": "2026-05-11T18:16:00", "name": "ipa: simple: Add OV2740 tuning + AIQB parser", "version": 3, "mbox": "https://patchwork.libcamera.org/series/5933/mbox/" } ], "comments": "https://patchwork.libcamera.org/api/patches/26716/comments/", "check": "pending", "checks": "https://patchwork.libcamera.org/api/patches/26716/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 25581BDCBD\n\tfor <parsemail@patchwork.libcamera.org>;\n\tMon, 11 May 2026 18:16:15 +0000 (UTC)", "from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id B024563020;\n\tMon, 11 May 2026 20:16:13 +0200 (CEST)", "from fout-b1-smtp.messagingengine.com\n\t(fout-b1-smtp.messagingengine.com [202.12.124.144])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id A69566302C\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tMon, 11 May 2026 20:16:09 +0200 (CEST)", "from phl-compute-02.internal (phl-compute-02.internal\n\t[10.202.2.42])\n\tby mailfout.stl.internal (Postfix) with ESMTP id A0E181D00134;\n\tMon, 11 May 2026 14:16:08 -0400 (EDT)", "from phl-imap-07 ([10.202.2.97])\n\tby phl-compute-02.internal (MEProxy); Mon, 11 May 2026 14:16:08 -0400", "by mailuser.phl.internal (Postfix, from userid 501)\n\tid 5EB601EA006B; Mon, 11 May 2026 14:16:08 -0400 (EDT)" ], "Authentication-Results": "lancelot.ideasonboard.com; dkim=pass (2048-bit key;\n\tunprotected) header.d=jetm.me header.i=@jetm.me header.b=\"viGNj2F9\";\n\tdkim=pass (2048-bit key;\n\tunprotected) header.d=messagingengine.com\n\theader.i=@messagingengine.com header.b=\"nI9jbXty\"; \n\tdkim-atps=neutral", "DKIM-Signature": [ "v=1; a=rsa-sha256; c=relaxed/relaxed; d=jetm.me; h=cc: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=1778523368;\n\tx=1778609768; bh=fZq8EJTkljr6qchHJWWzJ+vDKc1flX5PrThPvbc//r0=; b=\n\tviGNj2F9lHb5uzPz3yUB5+yugfuCcD4k+mSazLfOuBWvqS5p/4x3OUkMYCh3iHQG\n\tyT3VwVfDyl1FjhZuTnw37DslqSIMrsGu/cPlhUNOy7ZD3GPgBu6ak7NLHoUl4L0B\n\tYat3AzoIZFaXbU8kEtrBHgE9rJZR3i1kR+/gR87Ec7kE1tc7O4rbCkupvfj6VcOD\n\t9fQtTWIExj/HSSEhuf6HOxOD/oJuX6F5+1nVgDa1UAqZ3fQtbGgF+QWE8Jw1qv+5\n\tAklOFRB9+jFTW07Js/zPlrzYnz45iUf6GC7RmfCDERhCkgzkF5PPHQFfMFCApsro\n\tjZCSPflS5wRoKmULYgbc4g==", "v=1; a=rsa-sha256; c=relaxed/relaxed; d=\n\tmessagingengine.com; h=cc:cc:content-transfer-encoding\n\t:content-type:content-type:date:date:feedback-id:feedback-id\n\t:from:from:in-reply-to:in-reply-to:message-id:mime-version\n\t:references:reply-to:subject:subject:to:to:x-me-proxy\n\t:x-me-sender:x-me-sender:x-sasl-enc; s=fm3; t=1778523368; x=\n\t1778609768; bh=fZq8EJTkljr6qchHJWWzJ+vDKc1flX5PrThPvbc//r0=; b=n\n\tI9jbXtyXLUHKf/EIg+2Om0HfylrreZoRe7RsxAZNcwX8JyZvecROshBJUP2FfNN2\n\tdCW1u1w7gfruhY7Z7sNaFncQiSRU8vvszSOdmkQgPhHLQ2STYpWzGDAo4LNplH9x\n\tw2IODrhtTbN7XQ8prBRKUWOZS1rN2zgY/oOIxNdG6Q+HH/+cjfvEINRZ7Ecu+Gyj\n\ttgeZuAnmykbBrZPSfbWne1CovDCY+fjxC4BIbP9gidfvKAYoGCTdTyf/XqRq03A9\n\t2fx355MSAJlbN1PTncfn9rHDl6crB4Lq2nbBYenqKRpO7lscwEOS5LKtQzaPIaOH\n\tdKJGDrEH2hTTPub1DXpJg==" ], "X-ME-Sender": "<xms:6BwCaqCRF5mnJT3sP-YsmRcekimqqLgWpAZR8PXu2ftUf-6c0-4DAQ>\n\t<xme:6BwCavWU7TbHU_4G9aM_3nF5C2lCh25XdHSSCgELhjcyxfzal_CWXmZZiupunhDSQ\n\tcej2ybhOd3c9wfypZ-O2vj4GECPK7YSP22l6L5Sq7w86ZDBGZDY9gE>", "X-ME-Proxy-Cause": "gggruggvucftvghtrhhoucdtuddrgeefhedrtddtgdduudeliedvucetufdoteggodetrf\n\tdotffvucfrrhhofhhilhgvmecuhfgrshhtofgrihhlpdfurfetoffkrfgpnffqhgenuceu\n\trghilhhouhhtmecufedttdenucesvcftvggtihhpihgvnhhtshculddquddttddmnecujf\n\tgurhepoffhfffugggtgffkvfevofgjfhesthejredtredtjeenucfhrhhomheplfgrvhhi\n\tvghrucfvihgruceofhhlohhsshesjhgvthhmrdhmvgeqnecuggftrfgrthhtvghrnhepvd\n\tdtjeeiheeijedtveeujeevvdekjeeuveekleeijeekfeejhfefveeiffffvdeinecuvehl\n\tuhhsthgvrhfuihiivgeptdenucfrrghrrghmpehmrghilhhfrhhomhepfhhlohhsshesjh\n\tgvthhmrdhmvgdpnhgspghrtghpthhtohepjedpmhhouggvpehsmhhtphhouhhtpdhrtghp\n\tthhtoheprhhosggvrhhtrdhmrgguvghrsegtohhllhgrsghorhgrrdgtohhmpdhrtghpth\n\thtohepsggrrhhnrggsrghsrdhpohgtiigvsehiuggvrghsohhnsghorghrugdrtghomhdp\n\trhgtphhtthhopehkihgvrhgrnhdrsghinhhghhgrmhesihguvggrshhonhgsohgrrhgurd\n\tgtohhmpdhrtghpthhtoheplhgruhhrvghnthdrphhinhgthhgrrhhtsehiuggvrghsohhn\n\tsghorghrugdrtghomhdprhgtphhtthhopehlihgstggrmhgvrhgrqdguvghvvghlsehlih\n\thsthhsrdhlihgstggrmhgvrhgrrdhorhhgpdhrtghpthhtohepjhhohhgrnhhnvghsrdhg\n\tohgvuggvsehoshhsrdhquhgrlhgtohhmmhdrtghomhdprhgtphhtthhopehmiigrmhgrii\n\tgrlhesrhgvughhrghtrdgtohhm", "X-ME-Proxy": "<xmx:6BwCavpLYBFxWyjPQRpBtX7yJRlGQeWeQZ2KHPGVeD9nVqJLWrzzYA>\n\t<xmx:6BwCajtVCBP6J83qX15RmcwGfx1X-k_lCFlZJAq09MXBKZCsVQtaHg>\n\t<xmx:6BwCasGnXcdZdS-b_ydAnBFSmPrsp0RQrIgTnumCouJEv3_tpfPw3A>\n\t<xmx:6BwCakbO7Jk4Sf_aBzTc7auSljVcKZ3eRse1qi6vyvEmysQxLK43Tg>\n\t<xmx:6BwCamr6LJWCM5AHdJX2jOhTW3HcRzYH6TTqU4vxHBlFNaWpqzjoAPWr>", "Feedback-ID": "i9dde48b3:Fastmail", "X-Mailer": [ "MessagingEngine.com Webmail Interface", "b4 0.15.2" ], "From": "Javier Tia <floss@jetm.me>", "Date": "Mon, 11 May 2026 12:16:03 -0600", "Subject": "[PATCH v3 3/3] 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": "<20260511-ov2740-tuning-v3-3-8ff3e57c7368@jetm.me>", "To": "libcamera-devel@lists.libcamera.org", "Cc": "Kieran Bingham <kieran.bingham@ideasonboard.com>, Laurent Pinchart\n\t<laurent.pinchart@ideasonboard.com>, =?utf-8?b?QmFybmFiw6FzIFDFkWN6?=\n\t=?utf-8?q?e?= <barnabas.pocze@ideasonboard.com>,\n\tMilan Zamazal <mzamazal@redhat.com>, Robert Mader\n\t<robert.mader@collabora.com>, Hans de Goede\n\t<johannes.goede@oss.qualcomm.com>", "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/pANAwAKAbXuwwuoZ3cfAcsmYgBqAhzgVVSsu5Kp0AQ4KZhGdx/5fom+OENXDpTqI\n\tIKLRA4DRZiJAbMEAAEKAB0WIQSbE7ILzw7eI0VKk8m17sMLqGd3HwUCagIc4AAKCRC17sMLqGd3\n\tHxgOC/9xh9cNDSpA6ywkE0Sv+wIAk74QIyo051nqYGzCvbVzBUPYPbkHbjkFuGUwkIXtgUCyHTE\n\tgZkJjrAff93oucxjjtxbOzeHwE1nWS7QW6A3EMMqPf7ObwSo7qVogmFSzQCq02gh+wTLklI+unq\n\txjD4IJ8EeJ2la1Xj4qp39xAx2MWK9v9UVHnFKPSMbyQF5LanhapjBjk0HZjPWlK0bX1Xf3qXv1i\n\tWHpoIgqHvXiysUDsnL3649mtKJlQU5PG6i+dMlZAt/WBza/l+1u6xG8P9DdcOXEHE11pqfTWaEu\n\t+z4J3Pr0r9QCDyYPoIqQbO2ouC6ZPUKk7pEHfV3LTF55D6mxz8S8xO165B0ZKpWbf34yMH/yajN\n\t5ySAMFM0Xk+ha4zf9xhspko2r04wfZckjvAxCOpHM04u5JITEWb/Bzmas4PWblhACJj71UpFWog\n\ttr/f4xn/X4Sth9L7kLPToAZo3779cUkHDOZ0Al/z+JH0hrvVxIeMJJltU0T80VID/UEco=", "X-Developer-Key": "i=floss@jetm.me; a=openpgp;\n\tfpr=9B13B20BCF0EDE23454A93C9B5EEC30BA867771F", "In-Reply-To": "<20260511-ov2740-tuning-v3-0-8ff3e57c7368@jetm.me>", "References": "<20260511-ov2740-tuning-v3-0-8ff3e57c7368@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": [ "v3", "3/3" ] }