doc_scan_lite

A lightweight Flutter FFI plugin for live document edge detection and perspective-crop. Classical computer vision only — no ML models, no Google Play Services / ML Kit dependency — targeting a native binary in the hundreds-of-KB range per ABI.

Usage

The API is layered — pick the level of control you need.

1. Prebuilt UI (drop-in)

DocScannerView owns the camera, its lifecycle, the live overlay, the capture button, an optional manual corner-adjust step, and a result preview. One callback and you have a working scanner:

DocScannerView(
  enableManualAdjust: true, // optional drag-to-fix-corners step before crop
  onCaptured: (CapturedDocument doc) {
    // doc.bytes = grayscale pixels, doc.width x doc.height (deskewed crop)
  },
)

2. Prebuilt, customized

Every visual piece is overridable while keeping the built-in camera/detection plumbing:

DocScannerView(
  onCaptured: saveDoc,
  lockedColor: Colors.tealAccent,      // overlay colors + stroke
  searchingColor: Colors.white54,
  showResultPreview: false,            // skip the built-in result screen
  controlsBuilder: (context, c) =>     // your own capture bar
      MyShutterBar(locked: c.isLocked, busy: c.isCapturing, onTap: c.capture),
  resultBuilder: (context, doc, dismiss) => MyReviewPage(doc, onDone: dismiss),
  loadingBuilder: (context) => const MySplash(),
  errorBuilder: (context, err) => MyCameraError(err),
  onError: () => toast('No document found'),   // detection/warp failed
  onCancelled: () => toast('Cancelled'),       // user backed out of adjust
)

CapturedDocument.bytes is raw single-channel grayscale (one byte per pixel, width * height long), not PNG/JPEG — the pipeline only ever reads the camera's luma plane. Use decodeCapturedDocument(doc) to get a displayable ui.Image (dispose it when done), or expand to RGB and encode with a package like image.

3. Full DIY (bring your own camera + UI)

Drive detection yourself with the low-level pieces — DocScanController + DocScannerPreview + QuadCornerEditor — when you already manage the camera or need a bespoke flow. See example/ history or the source docs; the controller exposes processFrame, currentQuad, isLocked, captureAndCropLatest, latestFrame, warpFrame, pause / resume, and onDetectionError.

For a manual corner-correction flow, snapshot controller.latestFrame, show it under a QuadCornerEditor, then finalize with controller.warpFrame(frame, adjustedQuad) — pass the same frame you showed the user, since the live stream will have moved on by the time they finish dragging.

How it works

src/ implements the detection pipeline from scratch in C:

  1. YUV420 Y-plane extraction (grayscale, no RGB conversion)
  2. 5x5 Gaussian blur
  3. Canny edge detection (Sobel + non-max suppression + hysteresis)
  4. Morphological dilate
  5. Contour tracing (Moore-neighbor border following)
  6. Douglas-Peucker polygon simplification
  7. Quad filtering (4 points, convex, area > 20% of frame)
  8. Corner ordering (TL/TR/BR/BL)
  9. Homography solve + perspective warp

Exposed via two C functions (src/doc_scan_lite.h):

bool detect_quad(const uint8_t* frame, int width, int height, Quad* out);
bool warp_perspective(const uint8_t* frame, int width, int height, Quad quad,
                       uint8_t* out_buffer, int out_width, int out_height);

lib/ wraps these with an isolate-based Dart API so detection never blocks the UI thread:

  • DocScanController — owns a persistent background isolate, throttles incoming camera frames to a target detection fps independent of the camera's preview rate, smooths corners with a moving average, and exposes currentQuad / isLocked.
  • DocScannerView — batteries-included widget that owns the camera and the full scan flow (see Usage above).
  • DocScannerPreview — camera preview widget with a live quad overlay.
  • QuadCornerEditor — manual drag-adjustment widget for correcting a bad auto-detection, pure Dart/Flutter with no native cost.
  • captureAndCrop() re-runs detection on a full-resolution frame and returns the warped/cropped image as a CapturedDocument.

Testing

  • test/run_tests.sh builds and runs the C pipeline unit tests standalone (no Flutter/FFI toolchain involved) — corner ordering, Douglas-Peucker, homography, and a full synthetic detect+warp smoke test.
  • example/ demonstrates live camera detection end-to-end. Camera streams on emulators are unreliable for this — test on a real device.

Regenerating FFI bindings

dart run ffigen --config ffigen.yaml

If ffigen's bundled libclang can't find stdbool.h on Linux, point CPATH at your system clang's resource include dir first:

CPATH="$(clang -print-resource-dir)/include" dart run ffigen --config ffigen.yaml

Non-goals

No OCR, no multi-page PDF assembly, no ML models or trained weights, no Google Play Services / ML Kit dependency anywhere in the tree.

Libraries

doc_scan_lite
A lightweight FFI plugin for live document edge detection and perspective-crop, using classical computer vision only (no ML models, no Play Services dependency).
doc_scan_lite_bindings_generated