From 48cedac3c3b5aaf48e42e1574f57eddf3005d00d Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Mon, 17 Aug 2026 03:24:56 +0200 Subject: [PATCH] libcamera: stabilize AWB with a white point locus clamp + damping (r8) The softISP AWB is pure gray world, so panning swings ColourGains with the frame average (white wall renders green when the frame is warm-dominated, dark desk renders red when cool-dominated) and the CCM, interpolated over a CT estimated from those gains, amplifies and quantizes the swings. Patch 0015 (ours, original work) adds optional per-sensor Awb tuning to the simple IPA: a piecewise-linear locus of calibrated illuminant white points in gain space that the gray-world estimate is clamped to (clampMargin), a per-stats-frame EMA on the applied gains (damping) with a fast-converge path for persistent illuminant changes, and CT interpolated from the applied gains' position along the locus - on the same scale as the tuning file's ccms table - instead of the generic estimateCCT(). Tuning files without whitePoints keep the previous behaviour exactly, so every other sensor is unaffected. imx896.yaml gains the locus from the stock tuning blob's 10-point per-illuminant AWB calibration (white points and their exactly reciprocal gain triplets, cross-validated): the 7 on-locus points as nodes, the 3 fluorescent points admitted via clampMargin 0.35. The 5000 K label is the blob's own proven anchor; 2856 K (A) is locked by the fluorescent trio projecting onto the 2856-5000 chord at ~3960 K (TL84); the rest are geometry-derived approximations. Co-Authored-By: Claude Fable 5 --- ...dd-optional-white-point-locus-clamp-.patch | 408 ++++++++++++++++++ aports/temp/libcamera/APKBUILD | 6 +- aports/temp/libcamera/imx896.yaml | 25 ++ 3 files changed, 437 insertions(+), 2 deletions(-) create mode 100644 aports/temp/libcamera/0015-ipa-simple-awb-Add-optional-white-point-locus-clamp-.patch diff --git a/aports/temp/libcamera/0015-ipa-simple-awb-Add-optional-white-point-locus-clamp-.patch b/aports/temp/libcamera/0015-ipa-simple-awb-Add-optional-white-point-locus-clamp-.patch new file mode 100644 index 0000000..3659a8c --- /dev/null +++ b/aports/temp/libcamera/0015-ipa-simple-awb-Add-optional-white-point-locus-clamp-.patch @@ -0,0 +1,408 @@ +From 62fc9d05460aa0ad5e1e4c8f81ae966796c066c9 Mon Sep 17 00:00:00 2001 +From: Jorijn van der Graaf +Date: Mon, 17 Aug 2026 03:23:30 +0200 +Subject: [PATCH] ipa: simple: awb: Add optional white point locus clamp and + damping + +The simple IPA's AWB is pure gray world: every stats frame the applied +gains become the frame-average G/R and G/B ratios of that frame. Two +artifacts follow on real hardware. Panning swings the gains with the +scene composition - a warm-dominated frame renders a white wall green, +a cool-dominated one renders a dark desk red - and the CCM, which is +interpolated over a colour temperature estimated from those same gains, +amplifies and quantizes each swing. + +Add optional per-sensor tuning for the Awb algorithm: + + - Awb: + whitePoints: # ordered by strictly ascending ct + - { ct: 2856, gains: [ 1.378, 2.769 ] } # [ R gain, B gain ] + - { ct: 5000, gains: [ 2.037, 1.809 ] } + clampMargin: 0.35 # max distance from the locus in gain space + damping: 0.8 # per-stats-frame EMA weight of the old gains + +whitePoints defines a piecewise-linear locus of calibrated illuminant +white points in (R gain, B gain) space. The gray-world estimate is +computed as before, then clamped to within clampMargin of the locus: +scene colours can no longer pull the white balance arbitrarily far from +colours a real illuminant would produce, while off-locus (greenish, +e.g. fluorescent) illuminants within the margin still get corrected. +The applied gains follow the clamped estimate through a per-stats-frame +exponential moving average so composition changes fade in smoothly; a +large difference persisting for several stats frames is a real +illuminant change and halves the damping so scene cuts converge quickly +instead of crawling. + +The colour temperature is no longer estimated with the generic +estimateCCT() matrix but interpolated from the applied gains' position +along the locus, so the Ccm algorithm always picks a matrix consistent +with the applied white balance, on the same ct scale the tuning file's +ccms table uses. + +A tuning file without whitePoints keeps the previous behaviour exactly. + +Signed-off-by: Jorijn van der Graaf +--- + src/ipa/simple/algorithms/awb.cpp | 240 ++++++++++++++++++++++++++++-- + src/ipa/simple/algorithms/awb.h | 30 ++++ + 2 files changed, 257 insertions(+), 13 deletions(-) + +diff --git a/src/ipa/simple/algorithms/awb.cpp b/src/ipa/simple/algorithms/awb.cpp +index 05155c8..83dcece 100644 +--- a/src/ipa/simple/algorithms/awb.cpp ++++ b/src/ipa/simple/algorithms/awb.cpp +@@ -1,34 +1,160 @@ + /* SPDX-License-Identifier: LGPL-2.1-or-later */ + /* + * Copyright (C) 2024-2026 Red Hat Inc. ++ * Copyright (C) 2026 Jorijn van der Graaf + * + * Auto white balance + */ + + #include "awb.h" + ++#include ++#include ++#include + #include ++#include + #include + + #include + + #include + ++#include "libcamera/internal/value_node.h" ++#include "libcamera/internal/vector.h" ++ + #include "libipa/colours.h" + #include "simple/ipa_context.h" + ++namespace { ++ ++constexpr double kDefaultClampMargin = 0.35; ++constexpr double kDefaultDamping = 0.8; ++ ++/* ++ * When the damped gains stay farther than kFastConvergeDistance (in gain ++ * space) from the clamped gray-world target for more than ++ * kFastConvergeFrames consecutive stats frames, the difference is a real ++ * illuminant change rather than scene-composition noise: halve the damping ++ * so the change converges quickly instead of crawling. ++ */ ++constexpr double kFastConvergeDistance = 0.15; ++constexpr unsigned int kFastConvergeFrames = 3; ++ ++/* Start from the white point closest to daylight. */ ++constexpr double kInitialCT = 5000.0; ++ ++} /* namespace */ ++ + namespace libcamera { + + LOG_DEFINE_CATEGORY(IPASoftAwb) + + namespace ipa::soft::algorithms { + ++/* ++ * Optional per-sensor tuning: ++ * ++ * - Awb: ++ * whitePoints: # ordered by strictly ascending ct ++ * - { ct: 2856, gains: [ 1.378, 2.769 ] } # [ R gain, B gain ] ++ * - { ct: 5000, gains: [ 2.037, 1.809 ] } ++ * clampMargin: 0.35 # max distance from the locus in gain space ++ * damping: 0.8 # per-stats-frame EMA weight of the old gains ++ * ++ * whitePoints defines a piecewise-linear locus of calibrated illuminant ++ * white points in (R gain, B gain) space. The gray-world estimate is ++ * clamped to within clampMargin of the locus, bounding how far scene ++ * colours can pull the white balance while still allowing off-locus ++ * (greenish, e.g. fluorescent) illuminants. The applied gains follow the ++ * clamped estimate through an exponential moving average, and the colour ++ * temperature fed to the Ccm algorithm is interpolated from the applied ++ * gains' position along the locus. Without whitePoints the plain ++ * gray-world algorithm runs unchanged. ++ */ ++int Awb::init([[maybe_unused]] IPAContext &context, const ValueNode &tuningData) ++{ ++ const ValueNode &wps = tuningData["whitePoints"]; ++ if (wps.isEmpty()) ++ return 0; ++ ++ if (!wps.isList() || wps.size() < 2) { ++ LOG(IPASoftAwb, Error) ++ << "whitePoints must be a list of at least two points"; ++ return -EINVAL; ++ } ++ ++ for (const ValueNode &node : wps.asList()) { ++ std::optional ct = node["ct"].get(); ++ std::optional> gains = ++ node["gains"].get>(); ++ if (!ct || !gains || (*gains)[0] <= 0.0 || (*gains)[1] <= 0.0) { ++ LOG(IPASoftAwb, Error) ++ << "whitePoints entries need a ct and positive gains [ R, B ]"; ++ whitePoints_.clear(); ++ return -EINVAL; ++ } ++ if (!whitePoints_.empty()) { ++ const WhitePoint &prev = whitePoints_.back(); ++ if (*ct <= prev.ct) { ++ LOG(IPASoftAwb, Error) ++ << "whitePoints must be ordered by strictly ascending ct"; ++ whitePoints_.clear(); ++ return -EINVAL; ++ } ++ if ((*gains)[0] == prev.rGain && (*gains)[1] == prev.bGain) { ++ LOG(IPASoftAwb, Error) ++ << "consecutive whitePoints must differ in gains"; ++ whitePoints_.clear(); ++ return -EINVAL; ++ } ++ } ++ whitePoints_.push_back({ *ct, (*gains)[0], (*gains)[1] }); ++ } ++ ++ clampMargin_ = tuningData["clampMargin"].get(kDefaultClampMargin); ++ damping_ = tuningData["damping"].get(kDefaultDamping); ++ if (clampMargin_ <= 0.0 || damping_ < 0.0 || damping_ >= 1.0) { ++ LOG(IPASoftAwb, Error) ++ << "clampMargin must be positive and damping within [0, 1)"; ++ whitePoints_.clear(); ++ return -EINVAL; ++ } ++ ++ LOG(IPASoftAwb, Info) ++ << "White point locus: " << whitePoints_.size() << " points, " ++ << whitePoints_.front().ct << "-" << whitePoints_.back().ct ++ << " K, clamp margin " << clampMargin_ ++ << ", damping " << damping_; ++ ++ return 0; ++} ++ + int Awb::configure(IPAContext &context, + [[maybe_unused]] const IPAConfigInfo &configInfo) + { + auto &gains = context.activeState.awb.gains; + gains = { { 1.0, 1.0, 1.0 } }; + ++ fastFrames_ = 0; ++ ++ if (whitePoints_.empty()) ++ return 0; ++ ++ /* ++ * Start from the calibrated white point closest to daylight rather ++ * than from unity gains, so the first frames are roughly plausible ++ * while the loop converges. ++ */ ++ const WhitePoint *initial = &whitePoints_.front(); ++ for (const WhitePoint &wp : whitePoints_) { ++ if (std::abs(wp.ct - kInitialCT) < std::abs(initial->ct - kInitialCT)) ++ initial = ℘ ++ } ++ gains = { { static_cast(initial->rGain), 1.0, ++ static_cast(initial->bGain) } }; ++ context.activeState.awb.temperatureK = ++ static_cast(std::lround(initial->ct)); ++ + return 0; + } + +@@ -43,6 +169,39 @@ void Awb::prepare(IPAContext &context, + params->gains = gains; + } + ++/* ++ * Find the closest point on the piecewise-linear white point locus, and ++ * the colour temperature interpolated along the containing segment. ++ */ ++Awb::LocusPosition Awb::closestOnLocus(double rGain, double bGain) const ++{ ++ LocusPosition best{}; ++ double bestDist2 = std::numeric_limits::max(); ++ ++ for (unsigned int i = 0; i + 1 < whitePoints_.size(); i++) { ++ const WhitePoint &p0 = whitePoints_[i]; ++ const WhitePoint &p1 = whitePoints_[i + 1]; ++ const double dr = p1.rGain - p0.rGain; ++ const double db = p1.bGain - p0.bGain; ++ const double len2 = dr * dr + db * db; ++ double t = ((rGain - p0.rGain) * dr + (bGain - p0.bGain) * db) / len2; ++ t = std::clamp(t, 0.0, 1.0); ++ const double qr = p0.rGain + t * dr; ++ const double qb = p0.bGain + t * db; ++ const double dist2 = (rGain - qr) * (rGain - qr) + ++ (bGain - qb) * (bGain - qb); ++ if (dist2 < bestDist2) { ++ bestDist2 = dist2; ++ best.rGain = qr; ++ best.bGain = qb; ++ best.ct = p0.ct + t * (p1.ct - p0.ct); ++ } ++ } ++ ++ best.distance = std::sqrt(bestDist2); ++ return best; ++} ++ + void Awb::process(IPAContext &context, + [[maybe_unused]] const uint32_t frame, + IPAFrameContext &frameContext, +@@ -73,24 +232,79 @@ void Awb::process(IPAContext &context, + */ + const RGB sum = stats->sum_.max(offset + minValid) - offset; + ++ auto &gains = context.activeState.awb.gains; ++ ++ if (whitePoints_.empty()) { ++ /* ++ * Calculate red and blue gains for AWB. ++ * Clamp max gain at 4.0, this also avoids 0 division. ++ */ ++ gains = { { ++ sum.r() <= sum.g() / 4 ? 4.0f : static_cast(sum.g()) / sum.r(), ++ 1.0, ++ sum.b() <= sum.g() / 4 ? 4.0f : static_cast(sum.g()) / sum.b(), ++ } }; ++ ++ RGB rgbGains{ { 1 / gains.r(), 1 / gains.g(), 1 / gains.b() } }; ++ context.activeState.awb.temperatureK = estimateCCT(rgbGains); ++ metadata.set(controls::ColourTemperature, ++ context.activeState.awb.temperatureK); ++ ++ LOG(IPASoftAwb, Debug) ++ << "gain R/B: " << gains << "; temperature: " ++ << context.activeState.awb.temperatureK; ++ ++ return; ++ } ++ + /* +- * Calculate red and blue gains for AWB. +- * Clamp max gain at 4.0, this also avoids 0 division. ++ * Gray-world estimate. The sums are at least minValid, so the ++ * divisions are safe; the locus clamp below bounds the result. + */ +- auto &gains = context.activeState.awb.gains; +- gains = { { +- sum.r() <= sum.g() / 4 ? 4.0f : static_cast(sum.g()) / sum.r(), +- 1.0, +- sum.b() <= sum.g() / 4 ? 4.0f : static_cast(sum.g()) / sum.b(), +- } }; ++ double rTarget = static_cast(sum.g()) / sum.r(); ++ double bTarget = static_cast(sum.g()) / sum.b(); ++ ++ /* Clamp the estimate to within clampMargin_ of the locus. */ ++ const LocusPosition projection = closestOnLocus(rTarget, bTarget); ++ if (projection.distance > clampMargin_) { ++ const double f = clampMargin_ / projection.distance; ++ rTarget = projection.rGain + (rTarget - projection.rGain) * f; ++ bTarget = projection.bGain + (bTarget - projection.bGain) * f; ++ } + +- RGB rgbGains{ { 1 / gains.r(), 1 / gains.g(), 1 / gains.b() } }; +- context.activeState.awb.temperatureK = estimateCCT(rgbGains); +- metadata.set(controls::ColourTemperature, context.activeState.awb.temperatureK); ++ /* ++ * Damp the applied gains toward the clamped target so that changes ++ * of the frame composition (e.g. panning) do not swing the colours, ++ * converging faster when the difference is large and persistent. ++ */ ++ const double err = std::hypot(rTarget - gains.r(), bTarget - gains.b()); ++ if (err > kFastConvergeDistance) ++ fastFrames_++; ++ else ++ fastFrames_ = 0; ++ double damping = damping_; ++ if (fastFrames_ > kFastConvergeFrames) ++ damping /= 2.0; ++ ++ const double rNew = damping * gains.r() + (1.0 - damping) * rTarget; ++ const double bNew = damping * gains.b() + (1.0 - damping) * bTarget; ++ gains = { { static_cast(rNew), 1.0, static_cast(bNew) } }; ++ ++ /* ++ * The colour temperature the Ccm algorithm consumes follows from ++ * where the applied gains sit along the locus, so the matrix always ++ * matches the applied white balance. ++ */ ++ const LocusPosition applied = closestOnLocus(rNew, bNew); ++ context.activeState.awb.temperatureK = ++ static_cast(std::lround(applied.ct)); ++ metadata.set(controls::ColourTemperature, ++ context.activeState.awb.temperatureK); + + LOG(IPASoftAwb, Debug) +- << "gain R/B: " << gains << "; temperature: " +- << context.activeState.awb.temperatureK; ++ << "gain R/B: " << gains ++ << "; locus distance: " << projection.distance ++ << "; temperature: " << context.activeState.awb.temperatureK; + } + + REGISTER_IPA_ALGORITHM(Awb, "Awb") +diff --git a/src/ipa/simple/algorithms/awb.h b/src/ipa/simple/algorithms/awb.h +index ad993f3..ef4e4f3 100644 +--- a/src/ipa/simple/algorithms/awb.h ++++ b/src/ipa/simple/algorithms/awb.h +@@ -1,12 +1,15 @@ + /* SPDX-License-Identifier: LGPL-2.1-or-later */ + /* + * Copyright (C) 2024-2025 Red Hat Inc. ++ * Copyright (C) 2026 Jorijn van der Graaf + * + * Auto white balance + */ + + #pragma once + ++#include ++ + #include "algorithm.h" + + namespace libcamera { +@@ -19,6 +22,7 @@ public: + Awb() = default; + ~Awb() = default; + ++ int init(IPAContext &context, const ValueNode &tuningData) override; + int configure(IPAContext &context, const IPAConfigInfo &configInfo) override; + void prepare(IPAContext &context, + const uint32_t frame, +@@ -29,6 +33,32 @@ public: + IPAFrameContext &frameContext, + const SwIspStats *stats, + ControlList &metadata) override; ++ ++private: ++ struct WhitePoint { ++ double ct; ++ double rGain; ++ double bGain; ++ }; ++ ++ struct LocusPosition { ++ double rGain; ++ double bGain; ++ double ct; ++ double distance; ++ }; ++ ++ LocusPosition closestOnLocus(double rGain, double bGain) const; ++ ++ /* ++ * Calibrated white point locus in (R gain, B gain) space, ordered by ++ * strictly ascending colour temperature. Empty when the tuning file ++ * provides no white points; the plain gray-world algorithm runs then. ++ */ ++ std::vector whitePoints_; ++ double clampMargin_ = 0.0; ++ double damping_ = 0.0; ++ unsigned int fastFrames_ = 0; + }; + + } /* namespace ipa::soft::algorithms */ diff --git a/aports/temp/libcamera/APKBUILD b/aports/temp/libcamera/APKBUILD index c7abddf..0ccacc5 100644 --- a/aports/temp/libcamera/APKBUILD +++ b/aports/temp/libcamera/APKBUILD @@ -3,7 +3,7 @@ maintainer="Robert Mader " pkgname=libcamera _pkgver=0.7.2 pkgver=9999$_pkgver -pkgrel=7 +pkgrel=8 pkgdesc="Linux camera framework" url="https://libcamera.org/" arch="all" @@ -72,6 +72,7 @@ source="https://gitlab.freedesktop.org/camera/libcamera/-/archive/v$_pkgver/libc 0012-libcamera-software_isp-af-Harden-the-lens-control-ha.patch 0013-libcamera-software_isp-af-Add-AfMode-and-focus-witho.patch 0014-libcamera-software_isp-af-Re-baseline-the-scene-refe.patch + 0015-ipa-simple-awb-Add-optional-white-point-locus-clamp-.patch qcam.desktop $_tuning_files " @@ -199,6 +200,7 @@ c8ddc64ab943d9b215f4c997f2629e4403744c0ddb5de88cd42d384506a27c0762191eddd4d13559 8734e27966bd1bc5a4b0324a3d8a3ee4ab371f6d1536500b2bb44d49bde874c3fd6f244312332226c352e4716ba12fb72bf51e64b9e97cf6fa646aa6dfef3277 0012-libcamera-software_isp-af-Harden-the-lens-control-ha.patch dce81c2863ff6b9374115325f4af14ac5d396a6e2aee12f87d55a47d1d0bda860ae2af5a9d6885b126e20a12b7f3f40e49a2b9d738f94c04177bff6149da64e1 0013-libcamera-software_isp-af-Add-AfMode-and-focus-witho.patch 18ed6a03fbe3bcd750fac280831d1e62f2a2e35f76880535f2dc16ff196308512535e077ab0d8a923ad241090713edb53d1777e0dd6e5dcc10de495db1277052 0014-libcamera-software_isp-af-Re-baseline-the-scene-refe.patch +b01deba75c093ad4312ffa829ddbe5d745f2dfb199a3f00cdd52a8cb972fbe58bfb7cd94fa825b30d65047ac0c371ecf3ec0d0e356f090119d11086552f514be 0015-ipa-simple-awb-Add-optional-white-point-locus-clamp-.patch 22167a4eceb6d1b40b0b7c45fdf116c71684f5340de7f767535cb8e160ad9d2ae0f00cb3d461f73a344520a48a4641cf46226841d78bee06bfbfd2a91337f754 qcam.desktop 2ee566653b17d565d2c0de67cbe6ebad25df5aac6051cad78e6bae52016fa2ca0247e964d735ac776b3e0bd447ead9c4fcb26cd7629228cdba3ceb91b46f378f hi846.yaml 55dab9dcb9b1982143b9d63e4e51138c5bffd7c8f4a8e4e122750b3ad9d475446956bc297d9f564819ade4be75a575d06633a064e14f8ba3ef77986783c5f067 imx355.yaml @@ -207,7 +209,7 @@ dce81c2863ff6b9374115325f4af14ac5d396a6e2aee12f87d55a47d1d0bda860ae2af5a9d6885b1 55dab9dcb9b1982143b9d63e4e51138c5bffd7c8f4a8e4e122750b3ad9d475446956bc297d9f564819ade4be75a575d06633a064e14f8ba3ef77986783c5f067 imx376.yaml 2ee566653b17d565d2c0de67cbe6ebad25df5aac6051cad78e6bae52016fa2ca0247e964d735ac776b3e0bd447ead9c4fcb26cd7629228cdba3ceb91b46f378f imx519.yaml 55dab9dcb9b1982143b9d63e4e51138c5bffd7c8f4a8e4e122750b3ad9d475446956bc297d9f564819ade4be75a575d06633a064e14f8ba3ef77986783c5f067 imx858.yaml -cbb6204657fdb4bc35e9ee686200900291c8bb74678d3f19f8ed30120e5d939836534f8724726c58e8b7aa2eb99e99edace96350b11472fbbf4878cc1025c3f5 imx896.yaml +918bc5fc2035ba32af617a9f00a98dda1b8272cab04d7cea12bdf26b7e0d9811a8c8c2061191e00174d01a1ad991a3327bf418ce89dfb3610d06787f14389fa0 imx896.yaml 2ee566653b17d565d2c0de67cbe6ebad25df5aac6051cad78e6bae52016fa2ca0247e964d735ac776b3e0bd447ead9c4fcb26cd7629228cdba3ceb91b46f378f s5k3l6xx.yaml d59020e62520ee622282c85ce7bcdfe453c99becf48cc1005c5f8a4bb8b2511b19c89f38fb3c994e7c6a79d1f84e2ed5f5a09b4c5bdc1dc4a44b071dbccf0d44 s5kjn1.yaml " diff --git a/aports/temp/libcamera/imx896.yaml b/aports/temp/libcamera/imx896.yaml index 34305cd..73b0d96 100644 --- a/aports/temp/libcamera/imx896.yaml +++ b/aports/temp/libcamera/imx896.yaml @@ -19,6 +19,21 @@ # dynamic estimate ran at 48, leaving a 16-code residual the CCM amplified # into colour casts in dark regions. # +# Awb whitePoints: the 10-point per-illuminant AWB calibration from the +# same blob (white points @0x1c69ed, gain triplets @0x1c2dba — exact +# reciprocals of each other, cross-validated to float precision). The +# locus below carries the 7 on-locus points as (R gain, B gain), warm to +# cool. The 3 fluorescent points (idx 4-6) are deliberately NOT locus +# nodes: they sit ~0.33 gain units off the A-D50 chord (green side) and +# would zigzag the locus; clampMargin 0.35 admits them instead. CT +# labels: 5000 K is PROVEN (the blob's default gain + CCT anchor at +# index 3); 2856 K (illuminant A) is locked by two independent checks — +# sensor-typical A ratios AND the fluorescent trio projecting onto the +# 2856-5000 chord at ~3960 K, i.e. exactly TL84; the remaining labels +# are geometry-derived approximations (the blob stores no CCT list). +# Same ct scale as the ccms table below. damping = per-stats-frame EMA +# (stats run every 4th frame), ~1 s settling at 30 fps. +# # PROVENANCE / LICENSE: the matrices are numeric calibration data read out # of the proprietary vendor blob (measured spectral response of this # sensor, not authored expression). Shipping decided 2026-08-17 by the @@ -32,6 +47,16 @@ algorithms: - BlackLevel: blackLevel: 4096 - Awb: + whitePoints: + - { ct: 1900, gains: [ 0.878997, 4.771635 ] } + - { ct: 2300, gains: [ 1.119141, 3.340533 ] } + - { ct: 2856, gains: [ 1.377957, 2.769483 ] } + - { ct: 5000, gains: [ 2.037341, 1.809127 ] } + - { ct: 6504, gains: [ 2.367133, 1.574040 ] } + - { ct: 7504, gains: [ 2.741927, 1.357219 ] } + - { ct: 9500, gains: [ 4.508486, 1.012164 ] } + clampMargin: 0.35 + damping: 0.8 - Ccm: ccms: - ct: 2800