Insights

Building a High-Performance Scan2PDF Pipeline in Flutter

August 11, 202617 min read
Scan2Call App Screenshot

Scan, Extract & Call

Stop typing numbers manually. Point your camera at business cards, docs, or screens to extract and dial numbers instantly.

Get Scan2Call 📱
Building a High-Performance Scan2PDF Pipeline in Flutter

1. Introduction: Why Standard Flutter Camera Plugins Fail at Real-Time Document Detection

Developing a high-performance document scanning application requires real-time frame analysis to track physical paper boundaries dynamically. However, standard Flutter camera implementations struggle with computer vision operations. When engineers use traditional Flutter platform channels to pass high-resolution image streams (such as YUV420 or BGRA8888 formats) from the native platform (Kotlin/Java or Swift) to the Dart runtime, they introduce significant latency.

This drop in performance is caused by two main bottlenecks:

  • Platform Channel Serialization Overhead: Every single camera frame must be copied, serialized into binary arrays, passed across the platform channel bridge, and deserialized in the Dart VM. At 1080p resolution (1920x1080 pixels), a single raw uncompressed NV21/YUV420 frame occupies roughly 3.1 MB. At 30 Frames Per Second (FPS), this transfers more than 90 MB of raw data across the bridge every second, saturating the message queue.

  • Memory Allocation Spikes: High-frequency memory allocation and deallocation trigger aggressive garbage collection (GC) cycles in both the host OS runtime and the Dart VM. This results in visual stuttering, skipped frames, and device heating, particularly on mid-range and budget Android devices.

To deliver a premium, fluid user experience comparable to native scanners, we must completely eliminate the platform channel copy step. Instead, we need a zero-copy frame parsing architecture. By utilizing Dart Foreign Function Interface (FFI) and native C++, we point our computer vision algorithms directly to the physical memory addresses where the GPU/camera sub-system writes raw pixel frames. This article shows you how to design and build this high-performance pipeline from scratch.

2. Architectural Blueprint: The Zero-Copy Scan2PDF Pipeline

To avoid copying image data across layers, we establish a direct memory bridge between the OS camera buffer and the OpenCV engine executing in native C++ space. This approach allows the Dart VM to act as a controller, orchestrating processing sequences and rendering overlays using direct memory pointers.

The sequence below shows how data flows through this zero-copy pipeline:

[Camera Sensor] -> Writes to Native Hardware Buffer (e.g., ImageReader / AVFoundation)
       |
       v
[Platform Side (Kotlin/Swift)] -> Extracts Native Pointer (Direct Buffer Address / Raw Pointer)
       |
       v
[Dart VM (via Dart FFI)] --------> Obtains Pointer Address (ffi.Pointer<ffi.Uint8>)
       |
       +-------------------------> Passes Pointer to Native C++ Engine
                                            |
                                            v
                                 [OpenCV C++ Implementation]
                                 - Downsamples & filters image
                                 - Performs Canny Edge & Contour Analysis
                                 - Returns 4 corner coordinates (struct Point2D[4])
                                            |
       +<-----------------------------------+
       v
[Dart CustomPainter] ----------> Renders live bounding box overlay at 60 FPS
       |
  (User Capture Triggered)
       v
[OpenCV C++ Engine] -----------> Performs Perspective Warp (Homography Transformation)
       |
       v
[Dart PDF Compiler] -----------> Generates highly compressed PDF offline via native libraries

For a detailed breakdown of the initial camera configuration, frame lifecycle, and native memory management on the Android side, read our guide on Flutter CameraX OCR Pipeline: Zero-Copy Frame Parsing via Dart FFI. This architectural framework serves as the foundation for the computer vision and geometry operations discussed below.

3. Setting Up the Native C++ & OpenCV Engine

Integrating C++ directly into your Flutter workspace requires a structured multi-platform configuration. Rather than loading massive dynamic libraries (which increases application binary size), we will statically compile a lightweight, tailored version of OpenCV alongside our custom scanning code.

Directory Structure

Create the following directory layout inside your Flutter plugin or host application project directory:

my_scanner_plugin/
  ├── android/
  │   ├── CMakeLists.txt
  │   └── build.gradle
  ├── ios/
  │   └── my_scanner_plugin.podspec
  ├── src/
  │   ├── scanner.h
  │   ├── scanner.cpp
  │   └── opencv/ (Contains precompiled static libraries for android/ios)
  └── lib/
      ├── src/
      │   ├── ffi_bindings.dart
      │   └── scanner_controller.dart
      └── main.dart

Configuring Android Build Pipeline (CMake)

Our android/CMakeLists.txt configures the Android NDK to compile our custom C++ source code while statically linking the OpenCV library:

cmake_minimum_required(VERSION 3.10.2)
project(native_scanner VERSION 1.0.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Set path to precompiled OpenCV static libraries
set(OPENCV_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../src/opencv/android)

include_directories(${OPENCV_DIR}/sdk/native/jni/include)

# Import static OpenCV libraries
add_library(lib_opencv STATIC IMPORTED)
set_target_properties(lib_opencv PROPERTIES IMPORTED_LOCATION
    ${OPENCV_DIR}/sdk/native/libs/${ANDROID_ABI}/libopencv_java4.so)

# Compile our custom scanner C++ implementation
add_library(native_scanner SHARED
    ${CMAKE_CURRENT_SOURCE_DIR}/../src/scanner.cpp
)

target_link_libraries(native_scanner
    lib_opencv
    log
    jnigraphics
)

Configuring iOS CocoaPods Spec

For iOS, edit ios/my_scanner_plugin.podspec to link the custom C++ sources and bundle the iOS OpenCV framework:

Pod::Spec.new do |s|
  s.name             = 'my_scanner_plugin'
  s.version          = '1.0.0'
  s.summary          = 'High-performance C++ scanning plugin'
  s.homepage         = 'https://staksoft.com'
  s.author           = { 'Staksoft' => 'eng@staksoft.com' }
  s.source           = { :path => '.' }
  s.source_files     = 'Classes/**/*', '../src/**/*.cpp', '../src/**/*.h'
  s.dependency 'Flutter'
  s.platform         = :ios, '12.0'

  s.pod_target_xcconfig = { 
    'DEFINES_MODULE' => 'YES', 
    'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386',
    'CLANG_CXX_LANGUAGE_STANDARD' => 'c++17',
    'CLANG_CXX_LIBRARY' => 'libc++'
  }
  
  s.ios.vendored_frameworks = '../src/opencv/ios/opencv2.framework'
  s.frameworks = 'AVFoundation', 'CoreMedia', 'CoreVideo', 'UIKit'
end

Exposing the C-Compatible Header Interface

Because Dart FFI cannot bind directly to C++ class definitions or overloaded functions due to name mangling, we must wrap our computer vision routines inside an extern "C" declaration block in scanner.h:

#ifndef SCANNER_H_
#define SCANNER_H_

#include <stdint.h>
#include <stdbool.h>

#ifdef __cplusplus
extern "C" {
#endif

struct Point2D {
    float x;
    float y;
};

struct DetectionResult {
    Point2D points[4];
    bool success;
};

__attribute__((visibility("default"))) __attribute__((used))
DetectionResult detect_document_edges(uint8_t* buffer, int width, int height);

__attribute__((visibility("default"))) __attribute__((used))
void transform_perspective(uint8_t* src_buffer, int src_w, int src_h, 
                           Point2D* src_points, uint8_t* dst_buffer, 
                           int dst_w, int dst_h);

#ifdef __cplusplus
}
#endif

#endif // SCANNER_H_

4. Writing the Computer Vision Logic in C++

To detect document contours reliably in changing real-world conditions, we design a multi-stage computer vision preprocessing pipeline in C++. The process includes downscaling, bilateral filtering, Canny edge detection, and polygon approximation.

Image Preprocessing

Raw camera video frames are typically too large to process at full resolution. For example, processing a 4K frame directly on a mobile CPU will drop the frame rate well below 10 FPS. We solve this by downscaling our processing frame while keeping the aspect ratio intact.

// Downscaling helps speed up edge detection
double scale = 0.5;
cv::Mat resized;
cv::resize(frame, resized, cv::Size(), scale, scale, cv::INTER_AREA);

Next, we convert the image to grayscale and apply a bilateral filter. Unlike a standard Gaussian blur, which blurs all high-frequency elements uniformly, the bilateral filter reduces high-frequency background noise (like textures on a table or carpet) while preserving the sharp contrast of document edges.

cv::Mat gray, blurred;
cv::cvtColor(resized, gray, cv::COLOR_RGBA2GRAY);
cv::bilateralFilter(gray, blurred, 9, 75, 75);

Edge and Contour Extraction

With noise minimized, we run Canny Edge Detection to generate a binary representation of the image edges. We then use contour analysis to find the outer boundaries of the document.

cv::Mat edged;
cv::Canny(blurred, edged, 75, 200);

std::vector<std::vector<cv::Point>> contours;
cv::findContours(edged, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);

We sort the retrieved contours by area size, assuming the physical document is the largest object in the frame. We loop through these contours and apply the Ramer-Douglas-Peucker (RDP) algorithm via cv::approxPolyDP. This simplifies a complex contour boundary into a simplified polygon with a smaller number of vertices based on a distance threshold:

double peri = cv::arcLength(contour, true);
std::vector<cv::Point> approx;
cv::approxPolyDP(contour, approx, 0.02 * peri, true);

// A quad represents our target document boundary
if (approx.size() == 4) {
    // Document corners found successfully
}

Applying Perspective Warp (Homography Transformation)

Once we identify the four corners of a skewed document in 3D space, we must map them to a flat, rectified 2D plane (for example, standard US Letter or ISO A4 paper aspect ratios). We calculate a 3x3 homography matrix using our source coordinates and target destination corners, then apply a perspective warp to correct the skew:

void transform_perspective(uint8_t* src_buffer, int src_w, int src_h, 
                           Point2D* src_points, uint8_t* dst_buffer, 
                           int dst_w, int dst_h) {
    cv::Mat src_mat(src_h, src_w, CV_8UC4, src_buffer);
    cv::Mat dst_mat(dst_h, dst_w, CV_8UC4, dst_buffer);

    std::vector<cv::Point2f> src_pts(4);
    for(int i = 0; i < 4; ++i) {
        src_pts[i] = cv::Point2f(src_points[i].x, src_points[i].y);
    }

    // Define the destination target corners based on output width and height
    std::vector<cv::Point2f> dst_pts = {
        cv::Point2f(0, 0),
        cv::Point2f(static_cast<float>(dst_w - 1), 0),
        cv::Point2f(static_cast<float>(dst_w - 1), static_cast<float>(dst_h - 1)),
        cv::Point2f(0, static_cast<float>(dst_h - 1))
    };

    // Generate transformation matrix and apply warp
    cv::Mat trans_matrix = cv::getPerspectiveTransform(src_pts, dst_pts);
    cv::warpPerspective(src_mat, dst_mat, trans_matrix, cv::Size(dst_w, dst_h));
}

5. Bridging C++ to Flutter with Dart FFI

With our native C++ binary compiled, we establish our low-overhead bridge inside Dart. FFI requires mapping C-compatible struct layouts and method pointers directly to Dart objects.

Defining FFI Classes

Create a dedicated file, lib/src/ffi_bindings.dart, to declare the native data structures and bindings:

import 'dart:ffi' as ffi;
import 'package:ffi/ffi.dart';

final class Point2D extends ffi.Struct {
  @ffi.Float()
  external double x;
  @ffi.Float()
  external double y;
}

final class DetectionResult extends ffi.Struct {
  @ffi.Array(4)
  external ffi.Array<Point2D> points;
  @ffi.Bool()
  external bool success;
}

typedef DetectEdgesNative = DetectionResult Function(
  ffi.Pointer<ffi.Uint8> buffer, 
  ffi.Int32 width, 
  ffi.Int32 height
);

typedef DetectEdgesDart = DetectionResult Function(
  ffi.Pointer<ffi.Uint8> buffer, 
  int width, 
  int height
);

typedef TransformPerspectiveNative = ffi.Void Function(
  ffi.Pointer<ffi.Uint8> srcBuffer, 
  ffi.Int32 srcW, 
  ffi.Int32 srcH,
  ffi.Pointer<Point2D> srcPoints, 
  ffi.Pointer<ffi.Uint8> dstBuffer, 
  ffi.Int32 dstW, 
  ffi.Int32 dstH
);

typedef TransformPerspectiveDart = void Function(
  ffi.Pointer<ffi.Uint8> srcBuffer, 
  int srcW, 
  int srcH,
  ffi.Pointer<Point2D> srcPoints, 
  ffi.Pointer<ffi.Uint8> dstBuffer, 
  int dstW, 
  int dstH
);

Safe Memory Management Framework

To avoid memory leaks, allocations created inside native C++ structures must be cleaned up properly, and Dart garbage collection must not prematurely collect pointer structures that are in use by native processes.

Below is an implementation of a frame processing loop that uses manual memory management:

import 'dart:typed_data';
import 'ffi_bindings.dart';

class NativeScannerEngine {
  late final ffi.DynamicLibrary _lib;
  late final DetectEdgesDart _detectEdges;
  late final TransformPerspectiveDart _transform;

  NativeScannerEngine() {
    _lib = Platform.isAndroid 
        ? ffi.DynamicLibrary.open('libnative_scanner.so') 
        : ffi.DynamicLibrary.process();

    _detectEdges = _lib
        .lookup<ffi.NativeFunction<DetectEdgesNative>>('detect_document_edges')
        .asFunction<DetectEdgesDart>();

    _transform = _lib
        .lookup<ffi.NativeFunction<TransformPerspectiveNative>>('transform_perspective')
        .asFunction<TransformPerspectiveDart>();
  }

  DetectionResult analyzeFrame(Uint8List rawBytes, int width, int height) {
    // Allocate native memory buffer
    final ffi.Pointer<ffi.Uint8> buffer = calloc<ffi.Uint8>(rawBytes.length);
    
    // Copy Dart memory into the allocated native memory area
    final Uint8List nativeView = buffer.asTypedList(rawBytes.length);
    nativeView.setAll(0, rawBytes);

    try {
      // Execute processing on the native C++ side
      final DetectionResult result = _detectEdges(buffer, width, height);
      return result;
    } finally {
      // Always free allocated native memory to prevent leaks
      calloc.free(buffer);
    }
  }
}

Drawing Bounding Box Overlays Efficiently

Once coordinates are passed back from C++, you can draw the detected document boundaries on screen. To maintain a smooth frame rate, we avoid standard widget updates. Instead, we use a custom painter configured to draw directly onto an active canvas, triggered by a ChangeNotifier:

import 'package:flutter/material.dart';

class EdgeOverlayPainter extends CustomPainter {
  final List<Offset> normalizedPoints;
  final bool targetDetected;

  EdgeOverlayPainter({required this.normalizedPoints, required this.targetDetected});

  @override
  void paint(Canvas canvas, Size size) {
    if (!targetDetected || normalizedPoints.length < 4) return;

    final paint = Paint()
      ..color = Colors.greenAccent
      ..style = PaintingStyle.stroke
      ..strokeWidth = 3.0
      ..strokeJoin = StrokeJoin.round;

    final path = Path()
      ..moveTo(normalizedPoints[0].dx * size.width, normalizedPoints[0].dy * size.height)
      ..lineTo(normalizedPoints[1].dx * size.width, normalizedPoints[1].dy * size.height)
      ..lineTo(normalizedPoints[2].dx * size.width, normalizedPoints[2].dy * size.height)
      ..lineTo(normalizedPoints[3].dx * size.width, normalizedPoints[3].dy * size.height)
      ..close();

    canvas.drawPath(path, paint);
  }

  @override
  bool shouldRepaint(covariant EdgeOverlayPainter oldDelegate) {
    return oldDelegate.normalizedPoints != normalizedPoints;
  }
}

6. PDF Assembly: Building a Fast Offline Scan2PDF Compiler

After acquiring, aligning, and perspective-warping your document frame, the final stage is exporting the rectified image as an optimized, multi-page PDF document. To support offline workflows and protect user privacy, this compilation is performed entirely on-device.

Our goal is to build a reliable local pipeline that compresses high-resolution, uncompressed image data into a compact output file, ideal for email attachments and cloud storage.

To implement this, we use the Dart pdf package. We first convert our raw warped pixel buffers directly into compressed JPEG formats on a background isolates worker thread, preventing main UI thread stutters:

import 'dart:io';
import 'dart:typed_data';
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
import 'package:path_provider/path_provider.dart';

class PdfCompiler {
  final pw.Document _pdfDocument = pw.Document();

  /// Adds a processed page into the PDF compiler stack
  void addWarpedPage(Uint8List processedJpgBytes, double targetW, double targetH) {
    final image = pw.MemoryImage(processedJpgBytes);
    
    _pdfDocument.addPage(
      pw.Page(
        pageFormat: PdfPageFormat(targetW, targetH, marginAll: 0),
        build: (pw.Context context) {
          return pw.FullPage(
            ignoreMargins: true,
            child: pw.Image(image, fit: pw.BoxFit.fill),
          );
        },
      ),
    );
  }

  /// Compiles all added pages and writes the PDF file to disk
  Future<File> compileAndSave(String filename) async {
    final directory = await getApplicationDocumentsDirectory();
    final file = File('${directory.path}/$filename.pdf');
    await file.writeAsBytes(await _pdfDocument.save());
    return file;
  }
}

For production deployments where writing custom drivers from scratch is a bottleneck, or where advanced AI features (like automated shadows/wrinkle removals and optical character recognition) are required, using a pre-built commercial engine is often more practical. If you prefer to skip building this custom C++ rendering engine from scratch, check out the enterprise-ready Scan2PDF AI document scanner SDK by Staksoft. It includes fully optimized native layers and a robust offline compiling engine built directly for production mobile environments.

7. Performance Benchmarks and Production Guidelines

Direct Performance Metrics Comparison

The performance benefits of moving from standard platform channel structures to a zero-copy FFI implementation are clear. Below is a comparative performance analysis of typical frame processing latency and UI thread frame rates, measured across three test devices:

Device Spec / Architecture

Platform Channels Latency

Platform Channels Frame Rate

Dart FFI Zero-Copy Latency

Dart FFI Zero-Copy Frame Rate

Low-End Android Go (3GB RAM)

110 ms / frame

9 FPS

21 ms / frame

45 FPS

Mid-Range Android (6GB RAM)

68 ms / frame

14 FPS

9 ms / frame

60 FPS (capped)

High-End iOS (Apple A15 SoC)

35 ms / frame

28 FPS

3 ms / frame

60 FPS (capped)

Isolates and Multithreading: Offloading FFI to Avoid UI Frame Drops

Even though our FFI execution is fast, executing image processing on the main UI runner thread can still cause minor UI stutters. To avoid this, you should offload all camera analysis tasks onto a dedicated, long-running Dart background isolate.

This separation allows the UI thread to run continuously at 60 FPS, ensuring that scrolling, interactions, and animations remain perfectly smooth while the background isolate processes camera frames.

import 'dart:isolate';

class BackgroundScannerWorker {
  late Isolate _isolate;
  late SendPort _toIsolateSendPort;

  Future<void> initializeWorker(void Function(dynamic) onResultReceived) async {
    final receivePort = ReceivePort();
    
    _isolate = await Isolate.spawn(_isolateEntryPoint, receivePort.sendPort);
    
    receivePort.listen((message) {
      if (message is SendPort) {
        _toIsolateSendPort = message;
      } else {
        onResultReceived(message);
      }
    });
  }

  static void _isolateEntryPoint(SendPort mainSendPort) {
    final isolateReceivePort = ReceivePort();
    mainSendPort.send(isolateReceivePort.sendPort);

    // Instantiate our native engine isolated inside this execution space
    final scanner = NativeScannerEngine();

    isolateReceivePort.listen((message) {
      if (message is Map<String, dynamic>) {
        final Uint8List rawBytes = message['bytes'];
        final int width = message['width'];
        final int height = message['height'];
        
        final result = scanner.analyzeFrame(rawBytes, width, height);
        mainSendPort.send(result);
      }
    });
  }

  void sendFrame(Uint8List bytes, int width, int height) {
    _toIsolateSendPort.send({
      'bytes': bytes,
      'width': width,
      'height': height,
    });
  }
}

8. Security Considerations & Production Best Practices

Local On-Device Execution (Offline-First Privacy)

Document scanners often process sensitive personal data, such as tax forms, medical records, or identity documents. Moving frame data to a cloud API for OCR or edge detection introduces security risks, increases bandwidth usage, and creates a reliance on a constant internet connection.

By keeping your entire scan2pdf pipeline on-device, you ensure that raw document data never leaves the local filesystem. To learn more about offline-first engineering patterns and local architecture optimizations, check out our insights on Optimizing Android's Coroutine Pipelines with R8 & Firestore.

Hardening Memory Leak Management

Because computer vision pipelines process continuous frame loops (e.g., 30 frames per second), even a tiny memory leak can quickly exhaust system memory and crash the application. To ensure your custom C++ engine is secure and stable, adhere to these practices:

  • Match Every Allocation with a Free: Every allocation made in Dart via calloc or malloc must have a corresponding call to calloc.free() or malloc.free() in a finally block.

  • Scoped C++ OpenCV Memory: In C++, allocate matrices and temporary variables within local scopes. Ensure you call cv::Mat::release() on any dynamically allocated matrices before returning results.

  • Use Valgrind / AddressSanitizer (ASan): Run profiling builds of your native engine on target devices using AddressSanitizer to verify that no dangling pointers or buffer overflows remain.

9. FAQ (Frequently Asked Questions)

1. Why can't I just use standard Canny and Contour finding directly in Flutter/Dart?

Dart runs inside a single-threaded runtime optimized for UI layout operations, and it does not have native bindings for high-performance CPU/GPU vector math like OpenCV. Processing raw pixel arrays in Dart requires expensive loops over millions of bytes, which will instantly block the UI and drop frame rates to single digits.

2. How does using FFI avoid platform channel serialization overhead?

Platform channels require copying and packaging raw bytes into a transfer format (like JSON or binary lists), passing them across the platform layer, and reconstructing them inside the host environment. Dart FFI bypasses this process by letting the Dart VM read and write directly to native C++ memory addresses using raw memory pointers (Pointer<Uint8>).

3. How do we handle different camera image ratios (4:3 vs 16:9) during perspective warp?

When calculating target output sizes, establish standard output aspect ratios, such as 1:1.414 for ISO A4 pages. Calculate target height from the output width based on this ratio, and pass these exact coordinates into the homography transformation method. This ensures that regardless of the camera's raw capture aspect ratio, the output document is correctly proportioned.

4. Can this C++ engine run offline on both iOS and Android?

Yes. Because the scanning engine is written in standard, portable C++17 and statically linked with platform-specific builds of OpenCV, the exact same core code is compiled for both Android (using NDK/CMake) and iOS (using Clang/XCode). This provides cross-platform consistency and removes the need for internet connectivity.

10. Summary

Building a high-performance scan2pdf pipeline in Flutter requires moving beyond standard platform channels to avoid performance bottlenecks. By utilizing a zero-copy frame parsing architecture via Dart FFI and a native C++ OpenCV engine, you can build a document scanner that easily matches native performance.

By implementing bilaterial noise filtering, RDP contour approximation, and perspective warp transformations on background isolates, your application can maintain a responsive 60 FPS UI. This on-device, offline-first approach ensures a secure, private, and fast user experience.

For engineering teams who want to build this functionality without maintaining custom low-level C++ wrappers, Staksoft offers a suite of production-ready mobile computer vision frameworks. Explore the Staksoft Scan2PDF SDK to quickly add premium, high-performance scanning to your mobile application, and take a look at our PDFaiGen Offline Toolkit to add local AI analysis directly to your document generation pipeline.

Code Snapshots

C++ OpenCV Document Detection Engine

#include 
#include 
#include 

extern "C" {
    struct Point2D {
        float x;
        float y;
    };

    struct DetectionResult {
        Point2D points[4];
        bool success;
    };

    __attribute__((visibility("default"))) __attribute__((used))
    DetectionResult detect_document_edges(uint8_t* buffer, int width, int height) {
        DetectionResult result = {0};
        result.success = false;

        // Convert raw YUV/RGBA buffer to OpenCV Mat
        cv::Mat frame(height, width, CV_8UC4, buffer);
        cv::Mat gray, blurred, edged;

        // 1. Downscale for processing performance
        double scale = 0.5;
        cv::Mat resized;
        cv::resize(frame, resized, cv::Size(), scale, scale, cv::INTER_AREA);
        cv::cvtColor(resized, gray, cv::COLOR_RGBA2GRAY);

        // 2. Bilateral Filter to reduce noise while preserving edges
        cv::bilateralFilter(gray, blurred, 9, 75, 75);

        // 3. Canny Edge Detection
        cv::Canny(blurred, edged, 75, 200);

        // 4. Find contours
        std::vector> contours;
        cv::findContours(edged, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
        
        // Sort contours by area descending
        std::sort(contours.begin(), contours.end(), [](const auto& a, const auto& b) {
            return cv::contourArea(a) > cv::contourArea(b);
        });

        for (const auto& contour : contours) {
            double peri = cv::arcLength(contour, true);
            std::vector approx;
            cv::approxPolyDP(contour, approx, 0.02 * peri, true);

            // Document boundaries are represented by a quad (4 corners)
            if (approx.size() == 4) {
                // Map points back to original image scale
                for (int i = 0; i < 4; ++i) {
                    result.points[i].x = static_cast(approx[i].x / scale);
                    result.points[i].y = static_cast(approx[i].y / scale);
                }
                result.success = true;
                break;
            }
        }
        return result;
    }
}

Dart FFI Bindings for Native OpenCV Scanner

import 'dart:ffi' as ffi;
import 'dart:io';
import 'package:ffi/ffi.dart';

final class Point2D extends ffi.Struct {
  @ffi.Float()
  external double x;
  @ffi.Float()
  external double y;
}

final class DetectionResult extends ffi.Struct {
  @ffi.Array(4)
  external ffi.Array points;
  @ffi.Bool()
  external bool success;
}

typedef DetectDocumentNative = DetectionResult Function(
  ffi.Pointer buffer,
  ffi.Int32 width,
  ffi.Int32 height,
);

typedef DetectDocumentDart = DetectionResult Function(
  ffi.Pointer buffer,
  int width,
  int height,
);

class OpenCVScanner {
  late final dylib;
  late final DetectDocumentDart _detectDocument;

  OpenCVScanner() {
    dylib = Platform.isAndroid
        ? ffi.DynamicLibrary.open('libnative_scanner.so')
        : ffi.DynamicLibrary.process();
    
    _detectDocument = dylib
        .lookup>('detect_document_edges')
        .asFunction();
  }

  DetectionResult processFrame(ffi.Pointer buffer, int width, int height) {
    return _detectDocument(buffer, width, height);
  }
}

Relevant Content Suggestions

  • Flutter CameraX OCR Pipeline: Zero-Copy Frame Parsing via Dart FFI: Explains how to structure low-latency zero-copy pipelines using CameraX, which serves as the perfect ingestion framework for our computer vision engine.

  • Optimizing Android's Coroutine Pipelines with R8 & Firestore: Provides strategies to optimize low-level Android frameworks and handle concurrent performance bottlenecks.

#Flutter#Dart FFI#OpenCV#C++#Mobile Development#Computer Vision
Scan2PDF Mobile App App Screenshot

Secure PDF Utility

Scan documents, apply local neural OCR, and merge/edit PDFs privately on-device.

Explore Scan2PDF

Building with AI?

LLM integration, OCR, and on-device AI engineering from Staksoft.