processFrame method
bool
processFrame({
- required DocumentCorners? quad,
- required double blurScore,
- required bool isLowLight,
- double? coverageRatio,
- double? glareRatio,
Processes an incoming camera frame with multi-signal fusion.
Enhanced v3.0: Fuses quad stability, blur, coverage, and glare signals.
quad — Detected document corners (null if no document found).
blurScore — Laplacian variance blur score (higher = sharper).
isLowLight — Whether the scene is low-light.
coverageRatio — Quad area / frame area (0.0–1.0). Optional.
glareRatio — Hot pixel fraction (0.0–1.0). Optional.
Returns true if auto capture has just been triggered.
Implementation
bool processFrame({
required DocumentCorners? quad,
required double blurScore,
required bool isLowLight,
double? coverageRatio,
double? glareRatio,
}) {
// Update cached metrics
_lastCoverageRatio = coverageRatio ?? _lastCoverageRatio;
_lastGlareRatio = glareRatio ?? _lastGlareRatio;
// Adjust blur threshold for low-light conditions
final effectiveMinBlur = isLowLight ? minBlurScore * 0.6 : minBlurScore;
// Gate 1: Valid quad check
if (quad == null || !quad.isValidQuad) {
_resetToIdle('No document detected');
return false;
}
// Gate 2: Blur check
if (blurScore < effectiveMinBlur) {
_resetToIdle('Image is blurry — hold steady');
return false;
}
// Gate 3: Coverage check
if (coverageRatio != null && coverageRatio < minCoverageRatio) {
_resetToIdle('Move closer to document');
return false;
}
// Gate 4: Glare check
if (glareRatio != null && glareRatio > maxGlareRatio) {
_qualityFeedback = 'Glare detected — adjust angle';
// Don't reset entirely for glare, just pause stabilization
_consecutiveStableCount = math.max(0, _consecutiveStableCount - 1);
_state = AutoCaptureState.detecting;
return false;
}
// Add to history
_quadHistory.add(quad);
if (_quadHistory.length > requiredStableFrames + 2) {
_quadHistory.removeAt(0);
}
if (_quadHistory.length < 2) {
_state = AutoCaptureState.detecting;
_lastStabilityScore = 0.5;
_qualityFeedback = 'Document detected';
return false;
}
// Compute stability: max corner drift between consecutive frames
final prev = _quadHistory[_quadHistory.length - 2];
final curr = quad;
final d1 = (curr.topLeft - prev.topLeft).distance;
final d2 = (curr.topRight - prev.topRight).distance;
final d3 = (curr.bottomRight - prev.bottomRight).distance;
final d4 = (curr.bottomLeft - prev.bottomLeft).distance;
final maxDrift = math.max(math.max(d1, d2), math.max(d3, d4));
_lastStabilityScore =
(1.0 - (maxDrift / (maxCornerDriftDistance * 2))).clamp(0.0, 1.0);
// Multi-signal fusion score
final fusionScore = _computeFusionScore(
stabilityScore: _lastStabilityScore,
blurScore: blurScore,
coverageRatio: _lastCoverageRatio,
glareRatio: _lastGlareRatio,
);
if (maxDrift <= maxCornerDriftDistance && fusionScore > 0.6) {
_consecutiveStableCount++;
if (_state != AutoCaptureState.stabilizing) {
_stabilizingStartTime = DateTime.now();
}
_state = AutoCaptureState.stabilizing;
_qualityFeedback =
'Hold steady... ${(progress * 100).toInt()}%';
if (_consecutiveStableCount >= requiredStableFrames) {
_state = AutoCaptureState.captured;
_qualityFeedback = 'Captured!';
return true;
}
} else {
// Gradual decay instead of hard reset (more forgiving)
_consecutiveStableCount = math.max(0, _consecutiveStableCount - 1);
_state = AutoCaptureState.detecting;
_qualityFeedback = maxDrift > maxCornerDriftDistance
? 'Hold the device steady'
: 'Adjusting...';
}
return false;
}