whitecodel_image_compare
Flutter image similarity — OpenCV + perceptual hashes + optional OCR via ImageCompareOptions.
Tested on Flutter 3.44.6 (FVM). Verified: Android phones-only APK ~77–81 MB; iOS device Release ~74 MB (arm64).
Install
dependencies:
whitecodel_image_compare: ^1.1.0
Then:
flutter pub get
# iOS
cd ios && pod install && cd ..
Why this package?
Single options can miss a true match or falsely accept a bad crop. Enabling multiple hash flags and combining them (package Visual weights) recovers / rejects correctly on cases where one algorithm alone is wrong.
Usage
import 'package:whitecodel_image_compare/whitecodel_image_compare.dart';
final score = await compareQueriesToReference(
refBytes,
captures,
maxImageDimension: 1024,
openCvBidirectional: false,
openCvReuseInstance: true,
ocr: true, // Latin text only
);
// Or use the production preset (1024, forward+reuse, partial OCR, offload):
final scores = await compareQueriesWithOptions(
refBytes,
captures,
ImageCompareOptions.production,
);
final passed = score >= 50; // choose your own threshold
openCv / ocr need a real Android/iOS device (or a compatible emulator). Desktop VM unit tests mark them N/A — use the example Compare Lab on device for those flags.
Packages used
| Capability | Option | Source |
|---|---|---|
| OpenCV pixel matching | openCv |
Vendored FFI (Android/iOS) |
| ML Kit OCR | ocr |
Vendored plugin (GoogleMLKit 3.2.0, Latin only) |
| Perceptual hashes | perceptualHash / differenceHash / averageHash |
image_hash |
| Image decode / resize | maxImageDimension |
image |
Native OpenCV + OCR are vendored into this plugin (no path/git deps). Pinned ML Kit avoids iOS CocoaPods conflicts with mobile_scanner 2.x.
Android setup (required)
Align your app with these (same as example/android/):
| Setting | Recommended | Where |
|---|---|---|
minSdk |
24+ | android/app/build.gradle |
compileSdk / targetSdk |
35 | android/app/build.gradle |
| Android Gradle Plugin (AGP) | 8.11.1+ | android/settings.gradle |
| Kotlin | 2.2.20+ | android/settings.gradle |
| Gradle | 8.13+ (example: 8.14.3) | gradle-wrapper.properties |
| NDK | 28.2.13676358 (or highest reported by plugins) | android/app/build.gradle |
android/settings.gradle:
plugins {
id "com.android.application" version "8.11.1" apply false
id "org.jetbrains.kotlin.android" version "2.2.20" apply false
}
android/app/build.gradle (recommended baseline):
android {
compileSdk = 35
ndkVersion = "28.2.13676358"
defaultConfig {
minSdk = 24
targetSdk = 35
}
}
If Flutter reports mismatched NDK versions across plugins, set ndkVersion to the highest one (backward compatible).
Production size — AAB vs APK vs iOS
| Release type | Fat multi-ABI size issue? | What to do |
|---|---|---|
| Android App Bundle | No — Play delivers one ABI per device | Prefer flutter build appbundle --release — no abiFilters required |
| iOS App Store / IPA | No | No Android ABI filters; device Release is arm64 |
| Android fat APK | Yes — can include x86_64 OpenCV (~50 MB) |
Filters + packaging excludes + --target-platform (below) |
Verified (example app, Flutter 3.44.6):
| Build | Result |
|---|---|
Phones-only APK (--target-platform android-arm,android-arm64 + filters + excludes) |
~77–81 MB; armeabi-v7a + arm64-v8a only |
| Fat APK without stripping | ~164 MB; still had x86_64 OpenCV |
iOS flutter build ios --release --no-codesign |
✓ ~74 MB Runner.app; arm64 only; no Android .so files |
Shrinking an Android APK (not needed for Play AAB)
Plugin can ship OpenCV for:
| ABI | ~size | Use |
|---|---|---|
armeabi-v7a |
~10 MB | 32-bit phones |
arm64-v8a |
~17 MB | 64-bit phones |
x86_64 |
~50 MB | Emulator |
ndk.abiFilters alone is often not enough. In android/app/build.gradle:
android {
defaultConfig {
ndk {
abiFilters 'armeabi-v7a', 'arm64-v8a'
}
}
packaging {
jniLibs {
excludes += ['lib/x86/**', 'lib/x86_64/**']
}
}
}
Then:
fvm flutter build apk --release --target-platform android-arm,android-arm64
# or split / arm64-only:
fvm flutter build apk --release --split-per-abi
fvm flutter build apk --release --target-platform android-arm64
Play production: use an App Bundle — Play serves the matching ABI automatically:
fvm flutter build appbundle --release
iOS setup (required)
- Deployment target 13.0+ recommended (example / plugin use 13.0)
- CocoaPods must fetch OpenCV 4.3.0 and GoogleMLKit/TextRecognition ~> 3.2.0 (network required on first
pod install) - OCR is Latin only
- Device / App Store builds are arm64 — no Android-style
abiFilters - Set a real
version:in your apppubspec.yaml(build name + number) before App Store submit - OpenCV FFI is linked via an iOS Classes forwarder (
pixelmatching_native.cpp); use a full restart after upgrading the plugin (not hot reload alone)
cd ios && pod install && cd ..
flutter build ios --release
Simulator note (important): Google ML Kit + OpenCV CocoaPods do not ship official arm64 simulator slices (still true as of mid‑2026). That breaks iOS 26+ / iPhone 16–17 Pro style simulators (arm64-only).
| Works | Does not work |
|---|---|
| Physical iPhone | iPhone 17 Pro simulator (iOS 26+, arm64) |
| Older x86_64 simulators (e.g. iPhone 15 / iOS 17.5 under Rosetta) | Expecting OpenCV/OCR on arm64-only simulators |
# Use FVM Flutter (this repo pins 3.44.6 — current stable)
fvm flutter run -d <your-iphone-id>
# OR an older x86_64 simulator from the list, e.g.:
fvm flutter run -d "iPhone 15"
App Store / device Release builds remain arm64 and are fine.
Troubleshooting
| Symptom | Fix |
|---|---|
| AGP version too low / Kotlin too old | Bump AGP 8.11.1+, Kotlin 2.2.20+, Gradle 8.13+ (see Android setup) |
| NDK mismatch across plugins | Set ndkVersion to the highest version Flutter prints |
APK ~160 MB+ with unused x86_64 |
Add abiFilters + packaging.jniLibs.excludes + --target-platform — or ship an AAB |
| OpenCV fails on x86 emulator | Include x86_64 (larger APK) or use an arm64 system image / real device |
| iOS: iPhone 17 Pro / iOS 26 simulator fails (arm64) | OpenCV/ML Kit lack arm64 simulator slices — use a physical iPhone or an older x86_64 simulator (e.g. iPhone 15 / iOS 17.5): fvm flutter run -d "iPhone 15" |
| iOS: undeclared plugin / pod errors | cd ios && rm -rf Pods Podfile.lock && pod install |
| Wrong Flutter version in IDE | This repo uses FVM 3.44.6 (.fvmrc). Run via fvm flutter …; VS Code/Cursor should use .fvm/flutter_sdk |
| SPM warning for this plugin | Expected — ML Kit has no official SPM; CocoaPods fallback still works on device |
Failed to lookup symbol 'initialize' on iOS |
Full restart / reinstall after plugin upgrade; ensure pod install picked up ios/Classes/pixelmatching_native.cpp |
| OCR wrong script / empty text | Only Latin is supported; ensure clear Latin text in the image |
openCv / ocr N/A in unit tests |
Expected on desktop — run example app on device |
Reference projects: example/android/, example/ios/.
ImageCompareOptions defaults
| Flag | Default |
|---|---|
openCv |
true |
perceptualHash |
true |
differenceHash |
true |
averageHash |
true |
ocr |
false |
skipOcrIfAverageAbove50 |
false |
ocrMatchMode |
exact |
maxImageDimension |
null |
openCvBidirectional |
true |
openCvReuseInstance |
false |
offloadVisualWork |
true |
maxQueriesPerBatch |
null |
logCompareSteps |
false |
Performance (1.1.0+) — by default, decode / resize / OpenCV / hashes run on a worker isolate so the UI thread can keep painting. ML Kit OCR always runs on the caller isolate. Use offloadVisualWork: false only for lab comparisons against legacy blocking behavior.
Batch — compareQueriesWithOptions runs all queries in one visual isolate trip when possible. Set maxQueriesPerBatch (1–10) to split very large lists into chunks.
Preset — ImageCompareOptions.production = maxImageDimension: 1024, forward OpenCV + reuse, ocr: true, ocrMatchMode: partial, offloadVisualWork: true, maxQueriesPerBatch: 5.
Hash combine (Visual) when several hash flags are on:
Visual = (pHash×0.30 + dHash×0.20 + aHash×0.15) / sum(enabled weights)
(+ OpenCV 35% when openCv: true on device)
When ocr: true and tokens overlap: Combined ≈ Visual×0.60 + OCR×0.40 (+ token boost). Same rules in exported mergeCompareWithOcr for custom pipelines.
Benchmark — real cases where alone ≠ combine
Example match threshold used below: 50 (score >= 50 → match).
Scores from:
fvm flutter test test/readme_benchmark_test.dart
Scenarios
| # | Image | Expected @50 |
|---|---|---|
| Ref | ![]() |
— |
| A Easy same | ![]() |
1 (match) |
| B Hard same (heavy crop) | ![]() |
1 (match) |
| C Bad framing (corner only) | ![]() |
0 (reject) |
Scores by ImageCompareOptions (real)
| Options | Easy same | Hard same | Bad framing |
|---|---|---|---|
perceptualHash: true only |
62.5 | 59.4 | 50.0 |
differenceHash: true only |
64.1 | 54.7 | 46.9 |
averageHash: true only |
68.8 | 43.8 | 40.6 |
Combine pHash+dHash+aHash |
64.4 | 54.3 | 46.9 |
// Alone
ImageCompareOptions(
openCv: false,
perceptualHash: true,
differenceHash: false,
averageHash: false,
maxImageDimension: 1024,
)
// Combine hashes
ImageCompareOptions(
openCv: false,
perceptualHash: true,
differenceHash: true,
averageHash: true,
ocr: false,
maxImageDimension: 1024,
openCvBidirectional: false,
openCvReuseInstance: true,
)
Result @50 — where combine helps
| Options | Easy same (want 1) | Hard same (want 1) | Bad framing (want 0) |
|---|---|---|---|
perceptualHash only |
Correct 1 |
Correct 1 |
Wrong 1 (false accept) |
differenceHash only |
Correct 1 |
Correct 1 |
Correct 0 |
averageHash only |
Correct 1 |
Wrong 0 (miss) |
Correct 0 |
| Combine p+d+a | Correct 1 |
Correct 1 |
Correct 0 |
- Hard same:
averageHashalone = 43.8 → Wrong. Combine = 54.3 → Correct. - Bad framing:
perceptualHashalone = 50.0 → Wrong. Combine = 46.9 → Correct.
xychart-beta
title "Hard same @50 — aHash alone fails, combine passes"
x-axis ["pHash", "dHash", "aHash", "Combine"]
y-axis "match" 0 --> 1
bar [1, 1, 0, 1]
xychart-beta
title "Bad framing @50 — pHash alone false accept, combine rejects"
x-axis ["pHash", "dHash", "aHash", "Combine"]
y-axis "match" 0 --> 1
bar [1, 0, 0, 0]
Dev (FVM)
fvm install 3.44.6 && fvm use 3.44.6
fvm flutter pub get && fvm flutter test
cd example && fvm flutter run
Image sources
Adapted from Wikimedia Commons (CC0 / CC BY-SA). See doc/benchmark/source_*.jpg.
License
MIT — LICENSE



