processFrame method

bool processFrame({
  1. required DocumentCorners? quad,
  2. required double blurScore,
  3. required bool isLowLight,
  4. double? coverageRatio,
  5. 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;
}