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:
@@ -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, 0…360) 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 (0…360) 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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user