Patch Detail
Show a patch.
GET /api/patches/23614/?format=api
{ "id": 23614, "url": "https://patchwork.libcamera.org/api/patches/23614/?format=api", "web_url": "https://patchwork.libcamera.org/patch/23614/", "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": "<20250620124452.557855-7-naush@raspberrypi.com>", "date": "2025-06-20T12:42:27", "name": "[v1,6/8] ipa: rpi: controller: Autofocus to use AWB statistics; re-trigger", "commit_ref": null, "pull_url": null, "state": "accepted", "archived": false, "hash": "d15ceb720ffb1269858f93365334bddc3ecca7b2", "submitter": { "id": 34, "url": "https://patchwork.libcamera.org/api/people/34/?format=api", "name": "Naushir Patuck", "email": "naush@raspberrypi.com" }, "delegate": null, "mbox": "https://patchwork.libcamera.org/patch/23614/mbox/", "series": [ { "id": 5235, "url": "https://patchwork.libcamera.org/api/series/5235/?format=api", "web_url": "https://patchwork.libcamera.org/project/libcamera/list/?series=5235", "date": "2025-06-20T12:42:21", "name": "Raspberry Pi: AF improvements", "version": 1, "mbox": "https://patchwork.libcamera.org/series/5235/mbox/" } ], "comments": "https://patchwork.libcamera.org/api/patches/23614/comments/", "check": "pending", "checks": "https://patchwork.libcamera.org/api/patches/23614/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 4D322C3240\n\tfor <parsemail@patchwork.libcamera.org>;\n\tFri, 20 Jun 2025 12:45:16 +0000 (UTC)", "from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id AA28B68DFA;\n\tFri, 20 Jun 2025 14:45:12 +0200 (CEST)", "from mail-wr1-x42e.google.com (mail-wr1-x42e.google.com\n\t[IPv6:2a00:1450:4864:20::42e])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id C184E68DED\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri, 20 Jun 2025 14:45:01 +0200 (CEST)", "by mail-wr1-x42e.google.com with SMTP id\n\tffacd0b85a97d-3a4ee391e6fso297752f8f.3\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri, 20 Jun 2025 05:45:01 -0700 (PDT)", "from NAUSH-P-DELL.pitowers.org ([93.93.133.154])\n\tby smtp.gmail.com with ESMTPSA id\n\t5b1f17b1804b1-45361461375sm41561525e9.14.2025.06.20.05.45.00\n\t(version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);\n\tFri, 20 Jun 2025 05:45:00 -0700 (PDT)" ], "Authentication-Results": "lancelot.ideasonboard.com; dkim=pass (2048-bit key;\n\tunprotected) header.d=raspberrypi.com header.i=@raspberrypi.com\n\theader.b=\"SQ/7t7SK\"; dkim-atps=neutral", "DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=raspberrypi.com; s=google; t=1750423501; x=1751028301;\n\tdarn=lists.libcamera.org; \n\th=content-transfer-encoding:mime-version:references:in-reply-to\n\t:message-id:date:subject:cc:to:from:from:to:cc:subject:date\n\t:message-id:reply-to;\n\tbh=z+/M/GBxeweYftAicR2QQxTn1xxyvAvg8zZbDwaDW2Q=;\n\tb=SQ/7t7SKAV74rV0MR/0zGnP4sJJpllJR5BnJxXxv+uZgvt373AwMclCCEG3lmGUtIh\n\tkmQjyJ7xtM/rstcsox4OYdzdr6X/gg7STSHG45cTcFyp0MoznLKpdohVGRKZttplFzuL\n\t4SHc9j3MNsiSKDt245+cC8TMSbRPOFL33Syb3bGLNM6FMtvKnByiQUk6rQSVDk5aNton\n\tzlZXwrEHlnNx+1pJMA67NzdRYYJSTtUZH02Lm8HRhJmrd4eIcPqYssfuaxuOBWCjyEd4\n\tUMswuWqC4EIQTY/9S2h3l1LoRyGSRMQUXPSdOpwIUA/qBSqW8zkuKkxoqst/c465TGoW\n\tsT1A==", "X-Google-DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=1e100.net; s=20230601; t=1750423501; x=1751028301;\n\th=content-transfer-encoding:mime-version:references:in-reply-to\n\t:message-id:date:subject:cc:to:from:x-gm-message-state:from:to:cc\n\t:subject:date:message-id:reply-to;\n\tbh=z+/M/GBxeweYftAicR2QQxTn1xxyvAvg8zZbDwaDW2Q=;\n\tb=eQUYYAA1fzQIXMP2zAagz5tShOkdrbWZWdkv6YRvLDgp1Vk1aHUEdIWT6d99hgynJP\n\t4e4kvDdnvovnqUh4pwDKcsKRkb2iO62dr6LTgm9vxG5mZkZ+x09O+tPOIzGG6sRPu/61\n\tuRTOwi5QHYXdfXkjBDdYOCgViBvAvjve9GQvq2EZ7fCA1x28lFDkfLLsibGnLHGQNjQd\n\tDG+Z8u05j0gBtxYNFfcnOeKgVZt/2+OOC2FH6hxZyVmUsIHeo57akBjhh9GIrgg3KSDf\n\tESmAt64OH1etCSzCdYiq42+Ggj24l6G1uez2QfJrq0PeqD25jib6nmNdsz8BiMtXQbxt\n\t8n0w==", "X-Gm-Message-State": "AOJu0YywpngUNXT3g8XytItsqNZSIdD9QnH+DEqyxWdY3O7ig7uyObRA\n\t7dB5yo5KTUtfQGqFCdHGhegsvf0dWBry8MLyL2KokOXVOv+FMVP8T8YWEmDDK+bKAm/cxd3ezKK\n\tefip1", "X-Gm-Gg": "ASbGncs/byOPcnxMbxPHeycFVRb9iYNuHOxjEJVCr8eyPfaV2HnzfsXR/ZCA9dcWpUu\n\t73BdBhcFFWhyaswLKxgceRRHy7CiIhKa7as5jeVQ8YwHmTJRrf4jYRAHLDrWeoO7xdsuaEQJBAS\n\tvNktaAXOc0VeWI0CWAA23YMGnV7G4M/qZ/bmx+WyxkxyfWwtvEx/H8O3LfcRZUb3fJ7+A4YjZm2\n\t641isboi4ggRSIiW4hAekiaLZ5PXu6MliMwyUgwquvFkCC3+SfPOgmThWvIw5P41lutadwVBilw\n\tPRe6TqMxtqPx7nF7QNjNssMfnYILtVFPAMCoL0BtC7RohMCIQ+H3oEmYwsdxtVG0ydrv6zgtbsn\n\thgnfmmwoYYm2b5P4Y", "X-Google-Smtp-Source": "AGHT+IE3h2D0ZR2HRJzay5YgOmCdgR+Fd9/p+PBUBGYKTUvnhJh3Ji4KAQUKLPn+Y7oqt7G182+P2Q==", "X-Received": "by 2002:a05:6000:400f:b0:3a4:f912:86af with SMTP id\n\tffacd0b85a97d-3a6d11931e8mr897526f8f.2.1750423500791; \n\tFri, 20 Jun 2025 05:45:00 -0700 (PDT)", "From": "Naushir Patuck <naush@raspberrypi.com>", "To": "libcamera-devel@lists.libcamera.org", "Cc": "Nick Hollinghurst <nick.hollinghurst@raspberrypi.com>,\n\tNaushir Patuck <naush@raspberrypi.com>", "Subject": "[PATCH v1 6/8] ipa: rpi: controller: Autofocus to use AWB\n\tstatistics; re-trigger", "Date": "Fri, 20 Jun 2025 13:42:27 +0100", "Message-ID": "<20250620124452.557855-7-naush@raspberrypi.com>", "X-Mailer": "git-send-email 2.43.0", "In-Reply-To": "<20250620124452.557855-1-naush@raspberrypi.com>", "References": "<20250620124452.557855-1-naush@raspberrypi.com>", "MIME-Version": "1.0", "Content-Transfer-Encoding": "8bit", "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": "From: Nick Hollinghurst <nick.hollinghurst@raspberrypi.com>\n\nAnalyse AWB statistics: used both for scene change detection\nand to detect IR lighting (when a flag is set in the tuning file).\n\nOption to suppress PDAF altogether when IR lighting is detected.\n\nRather than being based solely on PDAF \"dropout\", allow a scan to\nbe (re-)triggered whenever the scene changes and then stabilizes,\nbased on contrast and average RGB statistics within the AF window.\n\nSigned-off-by: Nick Hollinghurst <nick.hollinghurst@raspberrypi.com>\nSigned-off-by: Naushir Patuck <naush@raspberrypi.com>\nReviewed-by: Naushir Patuck <naush@raspberrypi.com>\n---\n src/ipa/rpi/controller/rpi/af.cpp | 137 +++++++++++++++++++++++++++---\n src/ipa/rpi/controller/rpi/af.h | 37 +++++---\n 2 files changed, 149 insertions(+), 25 deletions(-)", "diff": "diff --git a/src/ipa/rpi/controller/rpi/af.cpp b/src/ipa/rpi/controller/rpi/af.cpp\nindex ecc0fc4175a7..4396420a0277 100644\n--- a/src/ipa/rpi/controller/rpi/af.cpp\n+++ b/src/ipa/rpi/controller/rpi/af.cpp\n@@ -46,6 +46,8 @@ Af::SpeedDependentParams::SpeedDependentParams()\n \t: stepCoarse(1.0),\n \t stepFine(0.25),\n \t contrastRatio(0.75),\n+\t retriggerRatio(0.75),\n+\t retriggerDelay(10),\n \t pdafGain(-0.02),\n \t pdafSquelch(0.125),\n \t maxSlew(2.0),\n@@ -60,6 +62,7 @@ Af::CfgParams::CfgParams()\n \t confThresh(16),\n \t confClip(512),\n \t skipFrames(5),\n+\t checkForIR(false),\n \t map()\n {\n }\n@@ -87,6 +90,8 @@ void Af::SpeedDependentParams::read(const libcamera::YamlObject ¶ms)\n \treadNumber<double>(stepCoarse, params, \"step_coarse\");\n \treadNumber<double>(stepFine, params, \"step_fine\");\n \treadNumber<double>(contrastRatio, params, \"contrast_ratio\");\n+\treadNumber<double>(retriggerRatio, params, \"retrigger_ratio\");\n+\treadNumber<uint32_t>(retriggerDelay, params, \"retrigger_delay\");\n \treadNumber<double>(pdafGain, params, \"pdaf_gain\");\n \treadNumber<double>(pdafSquelch, params, \"pdaf_squelch\");\n \treadNumber<double>(maxSlew, params, \"max_slew\");\n@@ -137,6 +142,7 @@ int Af::CfgParams::read(const libcamera::YamlObject ¶ms)\n \treadNumber<uint32_t>(confThresh, params, \"conf_thresh\");\n \treadNumber<uint32_t>(confClip, params, \"conf_clip\");\n \treadNumber<uint32_t>(skipFrames, params, \"skip_frames\");\n+\treadNumber<bool>(checkForIR, params, \"check_for_ir\");\n \n \tif (params.contains(\"map\"))\n \t\tmap = params[\"map\"].get<ipa::Pwl>(ipa::Pwl{});\n@@ -176,29 +182,37 @@ Af::Af(Controller *controller)\n \t useWindows_(false),\n \t phaseWeights_(),\n \t contrastWeights_(),\n+\t awbWeights_(),\n \t scanState_(ScanState::Idle),\n \t initted_(false),\n+\t irFlag_(false),\n \t ftarget_(-1.0),\n \t fsmooth_(-1.0),\n \t prevContrast_(0.0),\n+\t oldSceneContrast_(0.0),\n+\t prevAverage_{ 0.0, 0.0, 0.0 },\n+\t oldSceneAverage_{ 0.0, 0.0, 0.0 },\n \t prevPhase_(0.0),\n \t skipCount_(0),\n \t stepCount_(0),\n \t dropCount_(0),\n \t sameSignCount_(0),\n+\t sceneChangeCount_(0),\n \t scanMaxContrast_(0.0),\n \t scanMinContrast_(1.0e9),\n \t scanData_(),\n \t reportState_(AfState::Idle)\n {\n \t/*\n-\t * Reserve space for data, to reduce memory fragmentation. It's too early\n-\t * to query the size of the PDAF (from camera) and Contrast (from ISP)\n-\t * statistics, but these are plausible upper bounds.\n+\t * Reserve space for data structures, to reduce memory fragmentation.\n+\t * It's too early to query the size of the PDAF sensor data, so guess.\n \t */\n+\twindows_.reserve(1);\n \tphaseWeights_.w.reserve(16 * 12);\n \tcontrastWeights_.w.reserve(getHardwareConfig().focusRegions.width *\n \t\t\t\t getHardwareConfig().focusRegions.height);\n+\tcontrastWeights_.w.reserve(getHardwareConfig().awbRegions.width *\n+\t\t\t\t getHardwareConfig().awbRegions.height);\n \tscanData_.reserve(32);\n }\n \n@@ -309,6 +323,7 @@ void Af::invalidateWeights()\n {\n \tphaseWeights_.sum = 0;\n \tcontrastWeights_.sum = 0;\n+\tawbWeights_.sum = 0;\n }\n \n bool Af::getPhase(PdafRegions const ®ions, double &phase, double &conf)\n@@ -365,6 +380,54 @@ double Af::getContrast(const FocusRegions &focusStats)\n \treturn (contrastWeights_.sum > 0) ? ((double)sumWc / (double)contrastWeights_.sum) : 0.0;\n }\n \n+/*\n+ * Get the average R, G, B values in AF window[s] (from AWB statistics).\n+ * Optionally, check if all of {R,G,B} are within 4:5 of each other\n+ * across more than 50% of the counted area and within the AF window:\n+ * for an RGB sensor this strongly suggests that IR lighting is in use.\n+ */\n+\n+bool Af::getAverageAndTestIr(const RgbyRegions &awbStats, double rgb[3])\n+{\n+\tlibcamera::Size size = awbStats.size();\n+\tif (size.height != awbWeights_.rows ||\n+\t size.width != awbWeights_.cols || awbWeights_.sum == 0) {\n+\t\tLOG(RPiAf, Debug) << \"Recompute RGB weights \" << size.width << 'x' << size.height;\n+\t\tcomputeWeights(&awbWeights_, size.height, size.width);\n+\t}\n+\n+\tuint64_t sr = 0, sg = 0, sb = 0, sw = 1;\n+\tuint64_t greyCount = 0, allCount = 0;\n+\tfor (unsigned i = 0; i < awbStats.numRegions(); ++i) {\n+\t\tuint64_t r = awbStats.get(i).val.rSum;\n+\t\tuint64_t g = awbStats.get(i).val.gSum;\n+\t\tuint64_t b = awbStats.get(i).val.bSum;\n+\t\tuint64_t w = awbWeights_.w[i];\n+\t\tif (w) {\n+\t\t\tsw += w;\n+\t\t\tsr += w * r;\n+\t\t\tsg += w * g;\n+\t\t\tsb += w * b;\n+\t\t}\n+\t\tif (cfg_.checkForIR) {\n+\t\t\tif (4 * r < 5 * b && 4 * b < 5 * r &&\n+\t\t\t 4 * r < 5 * g && 4 * g < 5 * r &&\n+\t\t\t 4 * b < 5 * g && 4 * g < 5 * b)\n+\t\t\t\tgreyCount += awbStats.get(i).counted;\n+\t\t\tallCount += awbStats.get(i).counted;\n+\t\t}\n+\t}\n+\n+\trgb[0] = sr / (double)sw;\n+\trgb[1] = sg / (double)sw;\n+\trgb[2] = sb / (double)sw;\n+\n+\treturn (cfg_.checkForIR && 2 * greyCount > allCount &&\n+\t\t4 * sr < 5 * sb && 4 * sb < 5 * sr &&\n+\t\t4 * sr < 5 * sg && 4 * sg < 5 * sr &&\n+\t\t4 * sb < 5 * sg && 4 * sg < 5 * sb);\n+}\n+\n void Af::doPDAF(double phase, double conf)\n {\n \t/* Apply loop gain */\n@@ -473,6 +536,8 @@ void Af::doScan(double contrast, double phase, double conf)\n \tif (scanData_.empty() || contrast > scanMaxContrast_) {\n \t\tscanMaxContrast_ = contrast;\n \t\tscanMaxIndex_ = scanData_.size();\n+\t\tif (scanState_ != ScanState::Fine)\n+\t\t\tstd::copy(prevAverage_, prevAverage_ + 3, oldSceneAverage_);\n \t}\n \tif (contrast < scanMinContrast_)\n \t\tscanMinContrast_ = contrast;\n@@ -523,27 +588,63 @@ void Af::doAF(double contrast, double phase, double conf)\n \t\tsameSignCount_++;\n \tprevPhase_ = phase;\n \n+\tif (mode_ == AfModeManual)\n+\t\treturn; /* nothing to do */\n+\n \tif (scanState_ == ScanState::Pdaf) {\n \t\t/*\n \t\t * Use PDAF closed-loop control whenever available, in both CAF\n \t\t * mode and (for a limited number of iterations) when triggered.\n-\t\t * If PDAF fails (due to poor contrast, noise or large defocus),\n-\t\t * fall back to a CDAF-based scan. To avoid \"nuisance\" scans,\n-\t\t * scan only after a number of frames with low PDAF confidence.\n+\t\t * If PDAF fails (due to poor contrast, noise or large defocus)\n+\t\t * for at least dropoutFrames, fall back to a CDAF-based scan\n+\t\t * immediately (in triggered-auto) or on scene change (in CAF).\n \t\t */\n-\t\tif (conf > (dropCount_ ? 1.0 : 0.25) * cfg_.confEpsilon) {\n+\t\tif (conf >= cfg_.confEpsilon) {\n \t\t\tif (mode_ == AfModeAuto || sameSignCount_ >= 3)\n \t\t\t\tdoPDAF(phase, conf);\n \t\t\tif (stepCount_ > 0)\n \t\t\t\tstepCount_--;\n \t\t\telse if (mode_ != AfModeContinuous)\n \t\t\t\tscanState_ = ScanState::Idle;\n+\t\t\toldSceneContrast_ = contrast;\n+\t\t\tstd::copy(prevAverage_, prevAverage_ + 3, oldSceneAverage_);\n+\t\t\tsceneChangeCount_ = 0;\n \t\t\tdropCount_ = 0;\n-\t\t} else if (++dropCount_ == cfg_.speeds[speed_].dropoutFrames)\n+\t\t\treturn;\n+\t\t} else {\n+\t\t\tdropCount_++;\n+\t\t\tif (dropCount_ < cfg_.speeds[speed_].dropoutFrames)\n+\t\t\t\treturn;\n+\t\t\tif (mode_ != AfModeContinuous) {\n+\t\t\t\tstartProgrammedScan();\n+\t\t\t\treturn;\n+\t\t\t}\n+\t\t\t/* else fall through to waiting for a scene change */\n+\t\t}\n+\t}\n+\tif (scanState_ < ScanState::Coarse && mode_ == AfModeContinuous) {\n+\t\t/*\n+\t\t * In CAF mode, not in a scan, and PDAF is unavailable.\n+\t\t * Wait for a scene change, followed by stability.\n+\t\t */\n+\t\tif (contrast + 1.0 < cfg_.speeds[speed_].retriggerRatio * oldSceneContrast_ ||\n+\t\t oldSceneContrast_ + 1.0 < cfg_.speeds[speed_].retriggerRatio * contrast ||\n+\t\t prevAverage_[0] + 1.0 < cfg_.speeds[speed_].retriggerRatio * oldSceneAverage_[0] ||\n+\t\t oldSceneAverage_[0] + 1.0 < cfg_.speeds[speed_].retriggerRatio * prevAverage_[0] ||\n+\t\t prevAverage_[1] + 1.0 < cfg_.speeds[speed_].retriggerRatio * oldSceneAverage_[1] ||\n+\t\t oldSceneAverage_[1] + 1.0 < cfg_.speeds[speed_].retriggerRatio * prevAverage_[1] ||\n+\t\t prevAverage_[2] + 1.0 < cfg_.speeds[speed_].retriggerRatio * oldSceneAverage_[2] ||\n+\t\t oldSceneAverage_[2] + 1.0 < cfg_.speeds[speed_].retriggerRatio * prevAverage_[2]) {\n+\t\t\toldSceneContrast_ = contrast;\n+\t\t\tstd::copy(prevAverage_, prevAverage_ + 3, oldSceneAverage_);\n+\t\t\tsceneChangeCount_ = 1;\n+\t\t} else if (sceneChangeCount_)\n+\t\t\tsceneChangeCount_++;\n+\t\tif (sceneChangeCount_ >= cfg_.speeds[speed_].retriggerDelay)\n \t\t\tstartProgrammedScan();\n \t} else if (scanState_ >= ScanState::Coarse && fsmooth_ == ftarget_) {\n \t\t/*\n-\t\t * Scanning sequence. This means PDAF has become unavailable.\n+\t\t * CDAF-based scanning sequence.\n \t\t * Allow a delay between steps for CDAF FoM statistics to be\n \t\t * updated, and a \"settling time\" at the end of the sequence.\n \t\t * [A coarse or fine scan can be abandoned if two PDAF samples\n@@ -562,11 +663,14 @@ void Af::doAF(double contrast, double phase, double conf)\n \t\t\t\tscanState_ = ScanState::Pdaf;\n \t\t\telse\n \t\t\t\tscanState_ = ScanState::Idle;\n+\t\t\tdropCount_ = 0;\n+\t\t\tsceneChangeCount_ = 0;\n+\t\t\toldSceneContrast_ = std::max(scanMaxContrast_, prevContrast_);\n \t\t\tscanData_.clear();\n \t\t} else if (conf >= cfg_.confThresh && earlyTerminationByPhase(phase)) {\n+\t\t\tstd::copy(prevAverage_, prevAverage_ + 3, oldSceneAverage_);\n \t\t\tscanState_ = ScanState::Settle;\n-\t\t\tstepCount_ = (mode_ == AfModeContinuous) ? 0\n-\t\t\t\t\t\t\t\t : cfg_.speeds[speed_].stepFrames;\n+\t\t\tstepCount_ = (mode_ == AfModeContinuous) ? 0 : cfg_.speeds[speed_].stepFrames;\n \t\t} else\n \t\t\tdoScan(contrast, phase, conf);\n \t}\n@@ -596,7 +700,8 @@ void Af::updateLensPosition()\n void Af::startAF()\n {\n \t/* Use PDAF if the tuning file allows it; else CDAF. */\n-\tif (cfg_.speeds[speed_].dropoutFrames > 0 &&\n+\tif (cfg_.speeds[speed_].pdafGain != 0.0 &&\n+\t cfg_.speeds[speed_].dropoutFrames > 0 &&\n \t (mode_ == AfModeContinuous || cfg_.speeds[speed_].pdafFrames > 0)) {\n \t\tif (!initted_) {\n \t\t\tftarget_ = cfg_.ranges[range_].focusDefault;\n@@ -606,6 +711,8 @@ void Af::startAF()\n \t\tscanState_ = ScanState::Pdaf;\n \t\tscanData_.clear();\n \t\tdropCount_ = 0;\n+\t\toldSceneContrast_ = 0.0;\n+\t\tsceneChangeCount_ = 0;\n \t\treportState_ = AfState::Scanning;\n \t} else\n \t\tstartProgrammedScan();\n@@ -656,7 +763,7 @@ void Af::prepare(Metadata *imageMetadata)\n \t\tuint32_t oldSt = stepCount_;\n \t\tif (imageMetadata->get(\"pdaf.regions\", regions) == 0)\n \t\t\tgetPhase(regions, phase, conf);\n-\t\tdoAF(prevContrast_, phase, conf);\n+\t\tdoAF(prevContrast_, phase, irFlag_ ? 0 : conf);\n \t\tupdateLensPosition();\n \t\tLOG(RPiAf, Debug) << std::fixed << std::setprecision(2)\n \t\t\t\t << static_cast<unsigned int>(reportState_)\n@@ -666,7 +773,8 @@ void Af::prepare(Metadata *imageMetadata)\n \t\t\t\t << \" ft\" << oldFt << \"->\" << ftarget_\n \t\t\t\t << \" fs\" << oldFs << \"->\" << fsmooth_\n \t\t\t\t << \" cont=\" << (int)prevContrast_\n-\t\t\t\t << \" phase=\" << (int)phase << \" conf=\" << (int)conf;\n+\t\t\t\t << \" phase=\" << (int)phase << \" conf=\" << (int)conf\n+\t\t\t\t << (irFlag_ ? \" IR\" : \"\");\n \t}\n \n \t/* Report status and produce new lens setting */\n@@ -690,6 +798,7 @@ void Af::process(StatisticsPtr &stats, [[maybe_unused]] Metadata *imageMetadata)\n {\n \t(void)imageMetadata;\n \tprevContrast_ = getContrast(stats->focusRegions);\n+\tirFlag_ = getAverageAndTestIr(stats->awbRegions, prevAverage_);\n }\n \n /* Controls */\ndiff --git a/src/ipa/rpi/controller/rpi/af.h b/src/ipa/rpi/controller/rpi/af.h\nindex b06a3a16fab5..e1700f998f29 100644\n--- a/src/ipa/rpi/controller/rpi/af.h\n+++ b/src/ipa/rpi/controller/rpi/af.h\n@@ -15,20 +15,28 @@\n /*\n * This algorithm implements a hybrid of CDAF and PDAF, favouring PDAF.\n *\n- * Whenever PDAF is available, it is used in a continuous feedback loop.\n- * When triggered in auto mode, we simply enable AF for a limited number\n- * of frames (it may terminate early if the delta becomes small enough).\n+ * Whenever PDAF is available (and reports sufficiently high confidence),\n+ * it is used for continuous feedback control of the lens position. When\n+ * triggered in Auto mode, we enable the loop for a limited number of frames\n+ * (it may terminate sooner if the phase becomes small). In CAF mode, the\n+ * PDAF loop runs continuously. Very small lens movements are suppressed.\n *\n * When PDAF confidence is low (due e.g. to low contrast or extreme defocus)\n * or PDAF data are absent, fall back to CDAF with a programmed scan pattern.\n- * A coarse and fine scan are performed, using ISP's CDAF focus FoM to\n- * estimate the lens position with peak contrast. This is slower due to\n- * extra latency in the ISP, and requires a settling time between steps.\n+ * A coarse and fine scan are performed, using the ISP's CDAF contrast FoM\n+ * to estimate the lens position with peak contrast. (This is slower due to\n+ * extra latency in the ISP, and requires a settling time between steps.)\n+ * The scan may terminate early if PDAF recovers and allows the zero-phase\n+ * lens position to be interpolated.\n *\n- * Some hysteresis is applied to the switch between PDAF and CDAF, to avoid\n- * \"nuisance\" scans. During each interval where PDAF is not working, only\n- * ONE scan will be performed; CAF cannot track objects using CDAF alone.\n+ * In CAF mode, the fallback to a CDAF scan is triggered when PDAF fails to\n+ * report high confidence and a configurable number of frames have elapsed\n+ * since the last image change since either PDAF was working or a previous\n+ * scan found peak contrast. Image changes are detected using both contrast\n+ * and AWB statistics (within the AF window[s]).\n *\n+ * IR lighting can interfere with the correct operation of PDAF, so we\n+ * optionally try to detect it (from AWB statistics).\n */\n \n namespace RPiController {\n@@ -85,6 +93,8 @@ private:\n \t\tdouble stepCoarse;\t\t/* used for scans */\n \t\tdouble stepFine;\t\t/* used for scans */\n \t\tdouble contrastRatio;\t\t/* used for scan termination and reporting */\n+\t\tdouble retriggerRatio; /* contrast and RGB ratio for re-triggering */\n+\t\tuint32_t retriggerDelay; /* frames of stability before re-triggering */\n \t\tdouble pdafGain;\t\t/* coefficient for PDAF feedback loop */\n \t\tdouble pdafSquelch;\t\t/* PDAF stability parameter (device-specific) */\n \t\tdouble maxSlew;\t\t\t/* limit for lens movement per frame */\n@@ -103,6 +113,7 @@ private:\n \t\tuint32_t confThresh;\t \t/* PDAF confidence cell min (sensor-specific) */\n \t\tuint32_t confClip;\t \t/* PDAF confidence cell max (sensor-specific) */\n \t\tuint32_t skipFrames;\t \t/* frames to skip at start or modeswitch */\n+\t\tbool checkForIR; /* Set this if PDAF is unreliable in IR light */\n \t\tlibcamera::ipa::Pwl map; \t/* converts dioptres -> lens driver position */\n \n \t\tCfgParams();\n@@ -131,6 +142,7 @@ private:\n \tvoid invalidateWeights();\n \tbool getPhase(PdafRegions const ®ions, double &phase, double &conf);\n \tdouble getContrast(const FocusRegions &focusStats);\n+\tbool getAverageAndTestIr(const RgbyRegions &awbStats, double rgb[3]);\n \tvoid doPDAF(double phase, double conf);\n \tbool earlyTerminationByPhase(double phase);\n \tdouble findPeak(unsigned index) const;\n@@ -152,15 +164,18 @@ private:\n \tbool useWindows_;\n \tRegionWeights phaseWeights_;\n \tRegionWeights contrastWeights_;\n+\tRegionWeights awbWeights_;\n \n \t/* Working state. */\n \tScanState scanState_;\n-\tbool initted_;\n+\tbool initted_, irFlag_;\n \tdouble ftarget_, fsmooth_;\n-\tdouble prevContrast_;\n+\tdouble prevContrast_, oldSceneContrast_;\n+\tdouble prevAverage_[3], oldSceneAverage_[3];\n \tdouble prevPhase_;\n \tunsigned skipCount_, stepCount_, dropCount_;\n \tunsigned sameSignCount_;\n+\tunsigned sceneChangeCount_;\n \tunsigned scanMaxIndex_;\n \tdouble scanMaxContrast_, scanMinContrast_;\n \tstd::vector<ScanRecord> scanData_;\n", "prefixes": [ "v1", "6/8" ] }