{"id":26700,"url":"https://patchwork.libcamera.org/api/patches/26700/?format=json","web_url":"https://patchwork.libcamera.org/patch/26700/","project":{"id":1,"url":"https://patchwork.libcamera.org/api/projects/1/?format=json","name":"libcamera","link_name":"libcamera","list_id":"libcamera_core","list_email":"libcamera-devel@lists.libcamera.org","web_url":"","scm_url":"","webscm_url":""},"msgid":"<20260508171724.201811EA006C@mailuser.phl.internal>","date":"2026-05-08T16:53:36","name":"[v2,3/3] utils: tuning: Add AIQB parser for Intel IPU6 sensors","commit_ref":null,"pull_url":null,"state":"superseded","archived":false,"hash":"5428c4d4afe922bdbdc5c30208a9d7e036aa3a50","submitter":{"id":261,"url":"https://patchwork.libcamera.org/api/people/261/?format=json","name":"Javier Tia","email":"floss@jetm.me"},"delegate":null,"mbox":"https://patchwork.libcamera.org/patch/26700/mbox/","series":[{"id":5926,"url":"https://patchwork.libcamera.org/api/series/5926/?format=json","web_url":"https://patchwork.libcamera.org/project/libcamera/list/?series=5926","date":"2026-05-08T16:51:31","name":"ipa: simple: OV2740 tuning file and swstats sumShift cleanup","version":2,"mbox":"https://patchwork.libcamera.org/series/5926/mbox/"}],"comments":"https://patchwork.libcamera.org/api/patches/26700/comments/","check":"pending","checks":"https://patchwork.libcamera.org/api/patches/26700/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 CB650BE173\n\tfor <parsemail@patchwork.libcamera.org>;\n\tFri,  8 May 2026 17:17:28 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 777AE6302B;\n\tFri,  8 May 2026 19:17:28 +0200 (CEST)","from fout-a6-smtp.messagingengine.com\n\t(fout-a6-smtp.messagingengine.com [103.168.172.149])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id E647362FE1\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri,  8 May 2026 19:17:24 +0200 (CEST)","from phl-compute-02.internal (phl-compute-02.internal\n\t[10.202.2.42])\n\tby mailfout.phl.internal (Postfix) with ESMTP id 42B95EC0073;\n\tFri,  8 May 2026 13:17:24 -0400 (EDT)","from phl-imap-07 ([10.202.2.97])\n\tby phl-compute-02.internal (MEProxy); Fri, 08 May 2026 13:17:24 -0400","by mailuser.phl.internal (Postfix, from userid 501)\n\tid 201811EA006C; Fri,  8 May 2026 13:17:24 -0400 (EDT)"],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (2048-bit key;\n\tunprotected) header.d=jetm.me header.i=@jetm.me header.b=\"UECLH6VT\";\n\tdkim=pass (2048-bit key;\n\tunprotected) header.d=messagingengine.com\n\theader.i=@messagingengine.com header.b=\"E81jVt9X\"; \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=1778260644;\n\tx=1778347044; bh=kmhZ2W1joAZD6ZezUaeHUltkj832ri3xQUgTXtRh59k=; b=\n\tUECLH6VTAGZWc6xF+2INKBSTWlkzVbpxQhvNpJFJYaZ5EHbYqNR1QlO7SpRvqlfi\n\tk06V3AK0zuVAWYSCGlDEqlr+kRz77qsDyHvHSv1TLMnok+sT9IG03pU26sep0JZH\n\t0R4sIScqEEk5a0IABFfB18dEqA7M5Wkhn4TwmgLBXYkKd4FkGWkAVaRNOUIjqJFo\n\t+Lz1rbgXbdN1YFYVU/IrtL6b8RxCyFlIoTNpBELGZOnd4U8ZYXEdJ06CW9/FCn/E\n\tMZnczcUjuXUrpUmvLv39ChRRX/pOLgJWwicPehTir0oIOHujC1875D3Amh3TneIE\n\t/uVyEB3BnYfpFo6kVHfDDg==","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=1778260644; x=\n\t1778347044; bh=kmhZ2W1joAZD6ZezUaeHUltkj832ri3xQUgTXtRh59k=; b=E\n\t81jVt9XGGSS1ssVB1vXdQqU8KjISn+ZwUgOzTO597Z/PjUe6ENbZ2iXnP8HjX2ii\n\tSyOCeiveHgiUFyCpffiuSsgV8m1cyW96/qPdqgpKMuNUQ9VWfDuF4E3bKtdE9gSl\n\tj2tnmoYGyIDwLq+/ZlYStyRwd+BM6Jn5Rk1BPn2E8ifYxXDBJS9qDVja+r0yaT0p\n\tH3OG0T5/OWZ6XbhHCskqvPwSWrtCwOMN9LiW7qUDBbm8qbhu9z9fyOpkdVH9/Z+N\n\tlsB+WDG/36p2uW/J/Iva7uz1IkhBvC2TAte+XkGi6Zhsb/wE+nnzPsUPWwAiV3Na\n\tSp+1wexfwvUElNDEZP8IA=="],"X-ME-Sender":"<xms:pBr-aT2o3cLUy0MTPxehMpHjrSFeOi8VLNL-3o3QDemjnngIcb_Rtg>\n\t<xme:pBr-ac5DUvcWJyImUuaLxZXNHwzM3uRrNQ2aVBZCfWiY-IRkBGf-Ezzg8TMAzDtDk\n\tYaNbrVN_O41BU_3ADdTb2k65woIzz13UYfVKtO3_dU5LyqnH0ouGw>","X-ME-Proxy-Cause":"gggruggvucftvghtrhhoucdtuddrgeefhedrtddtgdduuddtledvucetufdoteggodetrf\n\tdotffvucfrrhhofhhilhgvmecuhfgrshhtofgrihhlpdfurfetoffkrfgpnffqhgenuceu\n\trghilhhouhhtmecufedttdenucgopfhokfffucdluddtmdenucfjughrpefotggggffhvf\n\tffufevjghfsehtkedttdertdejnecuhfhrohhmpeflrghvihgvrhcuvfhirgcuoehflhho\n\tshhssehjvghtmhdrmhgvqeenucggtffrrghtthgvrhhnpedtudejffejkeekteelueefvd\n\tejvdeuhfefteehkeevtddvleduteekleetvdelhfenucevlhhushhtvghrufhiiigvpedu\n\tnecurfgrrhgrmhepmhgrihhlfhhrohhmpehflhhoshhssehjvghtmhdrmhgvpdhnsggprh\n\tgtphhtthhopeejpdhmohguvgepshhmthhpohhuthdprhgtphhtthhopehrohgsvghrthdr\n\tmhgruggvrhestgholhhlrggsohhrrgdrtghomhdprhgtphhtthhopegsrghrnhgrsggrsh\n\tdrphhotgiivgesihguvggrshhonhgsohgrrhgurdgtohhmpdhrtghpthhtohepkhhivghr\n\trghnrdgsihhnghhhrghmsehiuggvrghsohhnsghorghrugdrtghomhdprhgtphhtthhope\n\thlrghurhgvnhhtrdhpihhntghhrghrthesihguvggrshhonhgsohgrrhgurdgtohhmpdhr\n\ttghpthhtoheplhhisggtrghmvghrrgdquggvvhgvlheslhhishhtshdrlhhisggtrghmvg\n\thrrgdrohhrghdprhgtphhtthhopehjohhhrghnnhgvshdrghhovgguvgesohhsshdrqhhu\n\trghltghomhhmrdgtohhmpdhrtghpthhtohepmhiirghmrgiirghlsehrvgguhhgrthdrtg\n\thomh","X-ME-Proxy":"<xmx:pBr-aVd2b8PVlYKCM-W1OEAWAsYBbTFZNSzJAlW5BoqmoTba1ekl0Q>\n\t<xmx:pBr-adTsSDfyW1q04yHwk1S7NNLtYRDBkwoFxAKcGH2V1DbMPpYG7w>\n\t<xmx:pBr-aeapHa4XrubB4ObF2-CQ96zq8rFza2xlyqq0NmAAf4l7uU4L-Q>\n\t<xmx:pBr-aQf2tmuEIdNDASWyfpc_dI9ywdRlb0l-6JKolHiIyW6nq-LZrw>\n\t<xmx:pBr-aWf4x5PgNtSOtwmRHDpnOVIgvnHq5WMhMm2WVzzg1DPyha12min0>","Feedback-ID":"i9dde48b3:Fastmail","X-Mailer":"MessagingEngine.com Webmail Interface","Content-Type":"text/plain; charset=\"utf-8\"","MIME-Version":"1.0","Content-Transfer-Encoding":"8bit","From":"Javier Tia <floss@jetm.me>","To":"libcamera-devel@lists.libcamera.org","Date":"Fri, 08 May 2026 10:53:36 -0600","Subject":"[PATCH v2 3/3] utils: tuning: Add AIQB parser for Intel IPU6 sensors","Cc":"mzamazal@redhat.com, kieran.bingham@ideasonboard.com,\n\tlaurent.pinchart@ideasonboard.com, barnabas.pocze@ideasonboard.com,\n\tjohannes.goede@oss.qualcomm.com, robert.mader@collabora.com","In-Reply-To":"<177826063718.39714.13674874482653763631@jetm.me>","References":"<177826063718.39714.13674874482653763631@jetm.me>","Message-Id":"<20260508171724.201811EA006C@mailuser.phl.internal>","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 | 276 +++++++++++++++++++++++++++++++++++++\n 1 file changed, 276 insertions(+)\n create mode 100644 utils/tuning/parse_aiqb.py","diff":"diff --git a/utils/tuning/parse_aiqb.py b/utils/tuning/parse_aiqb.py\nnew file mode 100644\nindex 00000000..12948f75\n--- /dev/null\n+++ b/utils/tuning/parse_aiqb.py\n@@ -0,0 +1,276 @@\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 chromaticity\n+# limits for use in a libcamera Simple IPA tuning YAML file.\n+#\n+# Tested against OV2740_CJFLE23_ADL.aiqb (Alder Lake, Intel ipu6-camera-hal).\n+# Other sensors and AIQB versions may require adjustments to the record\n+# layout assumptions.\n+#\n+# AIQB files for Intel IPU6 sensors are available in the ipu6-camera-hal\n+# repository at config/linux/ipu6ep/. Alternatively, extract from the OEM\n+# Windows camera driver installer using p7zip and innoextract.\n+#\n+# Usage:\n+#   python3 parse_aiqb.py <sensor>.aiqb [--sensor-name <name>] \\\n+#       [--black-level <value>]\n+#\n+# The black level is NOT extracted from the AIQB. Pass --black-level with the\n+# sensor's black level in 16-bit convention (value >> 8 = 8-bit black level).\n+# Check the sensor datasheet or kernel driver for the correct value.\n+# Example: OV2740 has 0x40 at 10-bit (64 ADU), which is 4096 in 16-bit conv.\n+\n+import argparse\n+import os\n+import struct\n+import sys\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+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+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+def extract_sensitivity(data, offset):\n+    iso, = struct.unpack_from('<H', data, offset + REC_HDR_SIZE)\n+    return iso\n+\n+def extract_color_matrices(data, offset):\n+    num, = struct.unpack_from('<H', data, offset + REC_HDR_SIZE)\n+    matrices = []\n+    mat_offset = offset + REC_HDR_SIZE + 2\n+    for i in range(num):\n+        fields = COLOR_MATRIX.unpack_from(data, mat_offset + i * 84)\n+        light_src = fields[0]\n+        matrices.append({\n+            'light_src': light_src,\n+            'cct': LIGHT_SOURCE_CCT.get(light_src),\n+            'r_per_g_raw': fields[1],\n+            'b_per_g_raw': fields[2],\n+            'matrix_raw': fields[5:14],\n+        })\n+    return matrices\n+\n+def extract_advanced_color_matrices(data, offset):\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+    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 = struct.Struct('<IffffI')  # 24 bytes\n+    CCM  = struct.Struct('<9f')     # 36 bytes\n+\n+    matrices = []\n+    for _ in range(num_ls):\n+        src_type, rg, bg, cie_x, cie_y, cct_k = INFO.unpack_from(data, pos)\n+        pos += 24\n+        trad = CCM.unpack_from(data, pos)\n+        pos += 36\n+        pos += num_sectors * 36  # skip per-sector advanced CCMs\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+    return matrices\n+\n+def guess_ccm_scale(matrices):\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+def format_ccm(vals):\n+    rows = []\n+    for row in range(3):\n+        r = vals[row * 3:(row + 1) * 3]\n+        rows.append(f\"          {r[0]:8.4f}, {r[1]:8.4f}, {r[2]:8.4f}\")\n+    return '[\\n' + ',\\n'.join(rows) + ' ]'\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('--black-level', type=int, default=0,\n+                        help='Black level in 16-bit convention, e.g. 4096 for '\n+                             'OV2740 (default: 0 = unknown, emits placeholder)')\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+\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][0])\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][0])\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+            print(f\"  WARNING: CCT={cct}K row sums {[round(s, 4) for s in row_sums]}\")\n+        print(f\"  CCT={cct}K  R/G={rg:.4f}  B/G={bg:.4f}\")\n+        valid_matrices.append((cct, vals, rg, bg))\n+\n+    max_gain_r = max_gain_b = None\n+    if valid_matrices:\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+    aiqb_name = os.path.basename(args.aiqb)\n+    print(\"\\n\" + \"=\" * 60)\n+    print(f\"# {sensor_name}.yaml for libcamera Simple IPA\")\n+    print(\"# SPDX-License-Identifier: CC0-1.0\")\n+    print(f\"# Calibrated from {aiqb_name}\")\n+    print(\"%YAML 1.1\")\n+    print(\"---\")\n+    print(\"version: 1\")\n+    print(\"algorithms:\")\n+    print(\"  - BlackLevel:\")\n+    if args.black_level:\n+        print(f\"      blackLevel: {args.black_level}\")\n+    else:\n+        print(\"      blackLevel: 0  # TODO: set correct value from sensor datasheet\")\n+    print(\"  - Awb:\")\n+    print(f\"      maxGainR: {max_gain_r}\")\n+    print(f\"      maxGainB: {max_gain_b}\")\n+    print(\"      speed: 0.25\")\n+    print(\"  - Ccm:\")\n+    print(\"      ccms:\")\n+    for cct, vals, rg, bg in sorted(valid_matrices):\n+        print(f\"        - ct: {cct}\")\n+        print(f\"          ccm: {format_ccm(vals)}\")\n+    print(\"  - Adjust:\")\n+    print(\"      gamma: 2.2\")\n+    print(\"      contrast: 1.0\")\n+    print(\"      saturation: 1.0\")\n+    print(\"  - Agc:\")\n+    print(\"...\")\n+\n+if __name__ == '__main__':\n+    main()\n","prefixes":["v2","3/3"]}