Image Quality Guard

Image Quality Guard
Detect blurry, glare-affected, poorly lit, and low-contrast images before they enter your upload, scanning, or recognition workflow.

pub.flutter-io.cn version GitHub stars Web Demo Flutter Dart


๐ŸŽฏ 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

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-based sharpnessScore described 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:

  1. Normalize the scale โ€” Sharpness is always measured on at most 1280 px (the scale every blurThreshold is calibrated for), even with fullResolution. Area-averaged downsampling also suppresses sensor noise.

  2. Apply a Laplacian filter โ€” For every non-border pixel, compute the edge response:

    response = 4 ร— center โˆ’ up โˆ’ down โˆ’ left โˆ’ right
    

    This 4-neighbour Laplacian highlights rapid intensity changes (edges). Its variance is high for crisp edges and low when edges are smoothed out.

  3. 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.

  4. Pick the detailed regions โ€” sharpnessScore is 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 than minInformativeTiles informative tiles, the whole-image variance is used instead.

  5. Decide โ€” The image is blurry when sharpnessScore < blurThreshold (isOutOfFocus), or, when minSharpTileRatio is 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:

  1. Each pixel is converted to grayscale using the Rec. 601 luma formula: 0.299R + 0.587G + 0.114B
  2. The average of all luminance values is calculated on a 0โ€“255 scale
  3. The result is classified:
    • Below minBrightness โ†’ Too dark ๐ŸŒ‘
    • Above maxBrightness โ†’ Too bright โ˜€๏ธ
    • Within range โ†’ Optimal โœ…

๐ŸŽจ Contrast Measurement

Contrast is the population standard deviation of pixel luminance values:

  1. All luminance samples are analyzed in a single pass
  2. Standard deviation is computed incrementally using Welford's algorithm
  3. 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 maxAnalysisDimension to 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.