Image Quality Guard
Detect blurry, glare-affected, poorly lit, and low-contrast images before they enter your upload, scanning, or recognition workflow.
๐ฏ What is Image Quality Guard?
image_quality_guard is a platform-independent Dart package for Flutter and Dart applications. It analyzes image bytes to detect quality issues โ blur, glare, brightness, and contrast โ before images enter your upload, scanning, or recognition pipeline.
No native runtime dependencies. Analysis runs entirely in Dart, with optional background isolate support for smooth UI performance.
๐ Key Capabilities
| Capability | Description |
|---|---|
| ๐ Blur Detection | Tile-based Laplacian variance that scores the detailed regions (text, print) of the image |
| โจ Glare Detection | Flags blown-out reflections, e.g. on laminated ID cards |
| ๐ก Brightness Analysis | Classifies images as too dark, optimal, or too bright |
| ๐จ Contrast Measurement | Luminance standard deviation for depth and clarity |
| โ One-Call Validation | Combined pass/fail with a single analyze() call |
| โ๏ธ 8 Presets | Mobile, card, NID, document, photo, relaxed, strict, and full-resolution configurations |
| ๐๏ธ Custom Thresholds | Fine-tune every metric for your specific use case |
| ๐ Background Isolate | Non-blocking analysis for large images |
| ๐ Web Support | Runs on Flutter web โ desktop, mobile, and browser |
| ๐ฆ Encoded & Decoded Input | Accept raw bytes or pre-decoded image package objects |
๐ฆ Installation
flutter pub add image_quality_guard
import 'package:image_quality_guard/image_quality_guard.dart';
๐ Quick Start
Main API: ImageQualityGuard (recommended)
Pass encoded image bytes from a camera, gallery picker, file, or network response. Analysis runs on a background isolate โ your UI stays responsive:
import 'dart:typed_data';
import 'package:image_quality_guard/image_quality_guard.dart';
final result = await ImageQualityGuard.analyze(imageBytes);
if (result.isValid) {
// โ
Image quality is acceptable โ proceed with upload or processing
} else {
// โ Issues detected
for (final issue in result.issues) {
print(issue);
}
}
โ ๏ธ Invalid or unsupported image bytes throw an
ImageDecodeException.
Alternative API: ImageQualityValidator
A convenience wrapper that returns QualityResult with typed sub-results:
final validator = ImageQualityValidator();
final result = await validator.validate(imageBytes);
if (result.isValid) {
// Proceed with the upload or recognition flow.
} else {
print(result.errorMessage);
}
๐ท๏ธ Presets
Choose a preset matching your capture scenario:
final result = await ImageQualityGuard.analyze(
imageBytes,
config: ImageQualityConfig.nidCapture,
);
| Preset | Intended Use | Blur | Min Bright | Max Bright | Min Contrast | Max Glare |
|---|---|---|---|---|---|---|
๐ชช ImageQualityConfig.cardScanning |
IDs, bank cards, licenses | 80 | 35 | 230 | 40 | 3% |
๐ ImageQualityConfig.nidCapture |
National ID cards (calibrate on your devices) | 80 | 55 | 225 | 25 | 3% |
๐ ImageQualityConfig.documentScanning |
Forms, receipts, printed text | 120 | 45 | 235 | 30 | 3% |
๐ท ImageQualityConfig.photoCapture |
High-quality photo capture | 200 | 30 | 235 | 45 | off |
๐ ImageQualityConfig.relaxed |
Challenging lighting / low-quality cameras | 50 | 25 | 240 | 30 | off |
โ๏ธ ImageQualityConfig.strict |
Strict quality requirements | 250 | 50 | 200 | 65 | off |
๐ฑ ImageQualityConfig.mobile |
Balanced defaults for everyday images | 100 | 40 | 220 | 50 | off |
๐ผ๏ธ ImageQualityConfig.fullResolution |
mobile thresholds, brightness/contrast/glare on every pixel |
100 | 40 | 220 | 50 | off |
documentScanning also requires 55% (and strict 65%) of the detailed image tiles to be sharp, so the page must fill most of the frame. Use cardScanning or nidCapture for ID cards photographed on a table.
๐๏ธ Custom Thresholds
Override individual metrics for application-specific needs:
final result = await ImageQualityGuard.analyze(
imageBytes,
config: const ImageQualityConfig(
blurThreshold: 150,
minBrightness: 50,
maxBrightness: 210,
minContrast: 60,
maxGlareRatio: 0.03,
),
);
| Parameter | Description | Range | Default |
|---|---|---|---|
blurThreshold |
Minimum sharpnessScore; higher = sharper image required |
1โโ | 100.0 |
minBrightness |
Below = too dark (0โ255 luminance) | 0โ255 | 40.0 |
maxBrightness |
Above = too bright/overexposed (0โ255) | 0โ255 | 220.0 |
minContrast |
Higher = more luminance variation required | 0โโ | 50.0 |
maxGlareRatio |
Max fraction of blown-out pixels (luminance โฅ 250); 1.0 = off |
0โ1 | 1.0 |
maxAnalysisDimension |
Longest side for brightness/contrast/glare (px); 0 = full res. Sharpness is always measured at โค 1280 px, the scale blurThreshold is calibrated for |
0โโ | 1280 |
Advanced sharpness settings
| Parameter | Description | Default |
|---|---|---|
tileRows / tileColumns |
Grid used for tile-based sharpness | 4 ร 4 |
minTileContrast |
Tiles with a lower luminance std. deviation are ignored as blank | 8.0 |
minInformativeTiles |
Fewer informative tiles โ whole-image Laplacian is used instead | 4 |
minSharpTileRatio |
Minimum share of informative tiles that must reach blurThreshold; 0 = off |
0.0 |
minTenengradScore |
Optional Sobel/Tenengrad edge-strength gate; 0 = off |
0.0 |
denoiseBeforeSharpness |
3ร3 Gaussian smoothing before measuring; lowers scores ~5โ10ร, so lower blurThreshold too |
false |
๐ก Tip: Quality thresholds are heuristics. Calibrate them with representative images from your target devices and environments.
๐ Individual Checks
Run specific checks independently:
final validator = ImageQualityValidator();
// ๐ Blur check
final blur = validator.checkBlur(imageBytes);
print('Variance: ${blur.variance}, Sharp: ${!blur.isBlurry}');
// ๐ก Brightness check
final brightness = validator.checkBrightness(imageBytes);
print('Level: ${brightness.level}, Avg: ${brightness.averageBrightness}');
// ๐จ Contrast check
final contrast = validator.checkContrast(imageBytes);
print('Score: ${contrast.contrastScore}, Good: ${contrast.hasGoodContrast}');
The individual checks measure the classic whole-image Laplacian variance.
ImageQualityGuard.analyze()uses the tile-basedsharpnessScoredescribed below.
For images already decoded with package:image, use the *FromImage variants to avoid re-decoding:
final blur = validator.checkBlurFromImage(decodedImage);
final brightness = validator.checkBrightnessFromImage(decodedImage);
final contrast = validator.checkContrastFromImage(decodedImage);
๐ Result Data
ImageQualityResult (from ImageQualityGuard)
| Property | Type | Description |
|---|---|---|
isValid |
bool |
All checks passed |
sharpnessScore |
double |
Tile-based sharpness used for the blur decision (higher = sharper) |
isBlurry |
bool |
isOutOfFocus or hasLowSharpCoverage |
isOutOfFocus |
bool |
sharpnessScore (or the optional Tenengrad gate) is below the threshold |
hasLowSharpCoverage |
bool |
Too few tiles are sharp (minSharpTileRatio) โ usually the subject does not fill the frame |
sharpTileRatio |
double |
Share of informative tiles reaching blurThreshold (0โ1) |
blurScore |
double |
Raw whole-image Laplacian variance (legacy, diagnostic) |
tenengradScore |
double |
Sobel gradient edge strength (diagnostic) |
brightness |
double |
Average luminance (0โ255) |
contrast |
double |
Luminance standard deviation |
glareRatio / hasGlare |
double / bool |
Share of blown-out pixels and whether it exceeds maxGlareRatio |
originalWidth / originalHeight |
int |
Decoded resolution |
analyzedWidth / analyzedHeight |
int |
Resolution fed to detectors |
processingTimeMs |
int |
Analysis time in ms |
issues |
List<String> |
Human-readable failed checks |
wasDownsampled |
bool |
Whether the image was resized |
QualityResult (from ImageQualityValidator)
| Property | Type | Description |
|---|---|---|
isValid |
bool |
All checks passed |
issues |
List<String> |
All detected issues |
errorMessage |
String? |
First issue, or null when valid |
blurResult |
BlurResult |
Variance, confidence, threshold, blur status |
brightnessResult |
BrightnessResult |
Average brightness, thresholds, classification |
contrastResult |
ContrastResult |
Contrast score, threshold, pass status |
๐๏ธ Architecture
Flutter UI isolate Background isolate
โโโโโโโโโโโโโโโโโโโโโโโ bytes โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ImageQualityGuard โโโโโโโโโโโโถ โ 1. Decode image โ
โ .analyze(bytes) โ โ 2. Downsample* โ
โ โ โ 3. Sharpness (tiles) โ
โ โ result โ 4. Brightness + contrast โ
โ ImageQualityResult โโโโโโโโโโโโ โ 5. Glare โ
โโโโโโโโโโโโโโโโโโโโโโโ (map) โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
*Only when image exceeds maxAnalysisDimension (default 1280px).
๐งฎ How It Works
All checks share the same pipeline: decode โ convert to grayscale โ measure. The image is converted to a flat 8-bit luminance buffer (one byte per pixel) and every metric is computed from that buffer alone.
Processing Pipeline
Raw Image Bytes
โ
โผ
โโโโโโโโโโโโโโโโโโโโ
โ Decode (image โ โ img.Image (RGBA)
โ package) โ
โโโโโโโโโโฌโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Luminance Extraction โ โ Uint8List (0โ255, one byte/pixel)
โ Rec. 601 luma: โ 0.299R + 0.587G + 0.114B
โ 0.299R + 0.587G + 0.114Bโ
โโโโโโโโโโฌโโโโโโโโโโโโโโโโโโ
โ
โโโโโโผโโโโโโโโโโโโโฌโโโโโโโโโโโโโ
โ โ โ โ
โผ โผ โผ โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Blur โโBrightnessโโ Contrast โโ Glare โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
๐ Blur Detection โ Tile-Based Laplacian Variance
Blur is measured with the Laplacian variance method, applied per region so that the parts of the image that actually carry detail decide the result:
-
Normalize the scale โ Sharpness is always measured on at most 1280 px (the scale every
blurThresholdis calibrated for), even withfullResolution. Area-averaged downsampling also suppresses sensor noise. -
Apply a Laplacian filter โ For every non-border pixel, compute the edge response:
response = 4 ร center โ up โ down โ left โ rightThis 4-neighbour Laplacian highlights rapid intensity changes (edges). Its variance is high for crisp edges and low when edges are smoothed out.
-
Score tiles โ The image is split into a 4ร4 grid. Tiles whose luminance standard deviation is below
minTileContrast(blank areas) are ignored; the Laplacian variance of every remaining informative tile is computed with Welford's online algorithm. -
Pick the detailed regions โ
sharpnessScoreis the 80th percentile of the tile scores. In a card or document photo most tiles show the table, plain card surface or a portrait, which are smooth even when the photo is perfectly focused; the percentile follows the tiles holding text and fine print, while still needing several sharp tiles so one noisy tile cannot pass a blurry photo. With fewer thanminInformativeTilesinformative tiles, the whole-image variance is used instead. -
Decide โ The image is blurry when
sharpnessScore < blurThreshold(isOutOfFocus), or, whenminSharpTileRatiois set, when too small a share of the tiles is sharp (hasLowSharpCoverage).
Sharp card on a table (16 tile scores, sorted):
10 11 13 13 15 16 17 20 38 39 46 75 | 1003 1074 1618 2139
โโโโ table, plain card, portrait โโโ โโ text lines โโโ
80th percentile โ sharpnessScore โ 1003 โ
(NID threshold 80)
Same card, out of focus:
2 2 2 2 2 3 3 4 4 5 6 10 | 30 35 65 83
80th percentile โ sharpnessScore โ 30 โ
Why Laplacian? The Laplacian is isotropic (detects edges in all directions equally) and is widely used in camera auto-focus systems, making it a natural choice for general-purpose sharpness detection.
โจ Glare Detection
Glare is the share of blown-out pixels (luminance โฅ 250). A reflection on a laminated card wipes out the text underneath while the average brightness stays normal, so it cannot be caught by maxBrightness. When glareRatio exceeds maxGlareRatio (3% for the card, NID and document presets), the image is rejected with a glare issue.
๐ก Brightness Analysis
Brightness is the mean (average) luminance across all pixels:
- Each pixel is converted to grayscale using the Rec. 601 luma formula:
0.299R + 0.587G + 0.114B - The average of all luminance values is calculated on a 0โ255 scale
- The result is classified:
- Below
minBrightnessโ Too dark ๐ - Above
maxBrightnessโ Too bright โ๏ธ - Within range โ Optimal โ
- Below
๐จ Contrast Measurement
Contrast is the population standard deviation of pixel luminance values:
- All luminance samples are analyzed in a single pass
- Standard deviation is computed incrementally using Welford's algorithm
- Classification:
- Low standard deviation โ Flat histogram, poor contrast (washed out)
- High standard deviation โ Wide tonal range, good contrast (vivid)
๐ง Memory Efficiency
Brightness, contrast, and every sharpness tile share a key optimization: Welford's online algorithm accumulates mean and variance incrementally. The package never materializes a per-pixel list of intermediate values, so memory stays constant regardless of image size.
๐ฑ Example App
An interactive Flutter example with camera/gallery input, every preset, custom threshold sliders, image preview, and a focused results view:
cd example
flutter run
๐ Web Demo
The example app supports Flutter web. Visit the live demo to explore all features interactively in your browser:
๐ Web Demo
The web version includes image preview, quality profile selection, custom threshold sliders, and real-time results visualization.
Deployment
The web app is automatically deployed to GitHub Pages on every push to main via .github/workflows/deploy.yml.
The workflow uses GitHub's official Pages actions (configure-pages, upload-pages-artifact, deploy-pages) and auto-enables GitHub Pages on the repository.
To deploy manually:
cd example
flutter build web --base-href /image_quality_guard/ --release
Then upload example/build/web/ to GitHub Pages (Settings โ Pages โ Source โ GitHub Actions).
โก Performance
- Analysis runs locally and synchronously after image decoding.
- Very large images can take noticeable time โ resize camera images or use
maxAnalysisDimensionto auto-downsample. ImageQualityGuard.analyze()runs everything on a background isolate, keeping your UI smooth.- Images are decoded once and reused across all checks.
๐ ๏ธ Error Handling
All errors extend ImageQualityException:
try {
final result = await ImageQualityGuard.analyze(imageBytes);
} on ImageDecodeException {
// โ Unsupported or corrupted file
} on ImageAnalysisException {
// โ Unexpected analysis failure
} on ImageIsolateException {
// โ Background isolate could not start
}
When using ImageQualityValidator, decode errors are thrown as ArgumentError for backward compatibility.
๐ License
BSD 3-Clause License โ see LICENSE.
Libraries
- image_quality_guard
- A Flutter/Dart package to detect blur, glare, brightness, and contrast issues in images with configurable thresholds.