Per-platform colour-border sensitivity (Teams violet, Meet glow)

Cross-platform research (Grant) flagged that the colour-border cue differs by app;
checking the real brand colours against the detector found a concrete bug: the
global 0.5 saturation threshold MISSES Teams' violet ring (#6264A7 ≈ 0.41, light
variants ~0.27) entirely and Meet's lighter blue glow (#8ab4f8 ≈ 0.44). Those
adapters would have detected nothing.

- FrameSampler.saturatedPoints: add a tunable threshold + optional hue-band gate
  (degrees) so a lowered threshold doesn't pick up warm video.
- GridCallAnalyzer.Config: colorSaturation / colorMinBrightness / colorHueRange,
  plumbed to the colour-border path (defaults preserve prior behaviour).
- MeetAdapter sat→0.35 (catch the glow); TeamsAdapter sat→0.22 + hue 215–275°
  (catch the faint violet, reject other colours); ZoomAdapter sat 0.45 + hue
  40–150° (vivid green/yellow). Values are first-pass pending real-fixture
  calibration; the hue gate is the main calibration lever.

Tests: Teams now detects the faint violet ring and rejects a green one; Meet/Zoom
vivid cases still pass. 27/27 XCTest.
This commit is contained in:
Grant Gilliam
2026-06-06 10:51:12 -05:00
parent 3785f6bdd0
commit 63cf3026ff
6 changed files with 72 additions and 8 deletions
+24 -4
View File
@@ -92,9 +92,14 @@ struct FrameSampler {
return points
}
/// Grid-sampled pixel positions (top-left origin) that are strongly saturated
/// AND bright enough to be a UI highlight i.e. a coloured speaking ring/border.
func saturatedPoints(threshold: Double = 0.5, minBrightness: Double = 60, gridStep: Int = 6) -> [CGPoint] {
/// Grid-sampled pixel positions (top-left origin) that are saturated AND bright
/// enough to be a UI highlight i.e. a coloured speaking ring/border. `threshold`
/// is the minimum saturation; lower it for muted accent rings (Teams violet,
/// Meet's light-blue glow sit below the 0.5 default). `hueRange`, when set,
/// additionally restricts to a hue band (degrees, 0360) so a low threshold
/// doesn't pick up warm video the per-platform calibration lever.
func saturatedPoints(threshold: Double = 0.5, minBrightness: Double = 60,
hueRange: ClosedRange<Double>? = nil, gridStep: Int = 6) -> [CGPoint] {
var points: [CGPoint] = []
var y = 0
while y < height {
@@ -104,11 +109,26 @@ struct FrameSampler {
let r = Double(pixels[i]), g = Double(pixels[i + 1]), b = Double(pixels[i + 2])
let mx = max(r, g, b), mn = min(r, g, b)
let sat = mx > 0 ? (mx - mn) / mx : 0
if sat > threshold && mx > minBrightness { points.append(CGPoint(x: x, y: y)) }
if sat > threshold && mx > minBrightness,
hueRange == nil || hueRange!.contains(Self.hueDegrees(r, g, b, mx, mn)) {
points.append(CGPoint(x: x, y: y))
}
x += gridStep
}
y += gridStep
}
return points
}
/// HSV hue in degrees (0360) from RGB and its precomputed max/min channels.
private static func hueDegrees(_ r: Double, _ g: Double, _ b: Double, _ mx: Double, _ mn: Double) -> Double {
let d = mx - mn
guard d > 0 else { return 0 }
let h: Double
if mx == r { h = (g - b) / d }
else if mx == g { h = 2 + (b - r) / d }
else { h = 4 + (r - g) / d }
let deg = h * 60
return deg < 0 ? deg + 360 : deg
}
}