error_level_map — IMGFORENSICS compression op

• Data kinds: image2d → image2d

• Call: import fullseye as fs; fs.ledger.error_level_map(image, quality: 'int' = 90, normalize: 'bool' = True) -> 'np.ndarray' (to call the implementation directly, import imgforensics; imgforensics.error_level_map(image, quality: 'int' = 90, normalize: 'bool' = True) -> 'np.ndarray'; from the registry, opsimgforensics.get("error_level_map"))

Usage

ELA (error-level analysis). A map of the difference after re-compressing at a given quality. image2d.

Krawetz 2007. JPEG quantizes per 8x8 block, so a region that has been compressed once shows little error when recompressed at the same quality, whereas a region pasted / drawn in afterwards leaves a large error; that is the idea it is based on.

With normalize=True (default) it is divided by the maximum into [0, 1] (for viewing). With False it is the absolute error in raw 8 bit levels (for counting).

Without Pillow, ImportError. It does not fall back to an approximation. Imitating only the DCT quantization in numpy gives *something else*, in which the encoder's rounding, colour-space conversion and chroma subsampling are gone. Calling that ELA would mean committing yourself the "plausibly wrong" that this family sets out to crush.

What this map cannot tell (tests/test_imgforensics.py pins it in numbers; a different image pasted into the central 64x64 of a 256x256 1/f^1.6 image):

==================================== =============== =============== =========

Original image ELA of pasted ELA of backgr. Ratio

==================================== =============== =============== =========

Uncompressed (never JPEG'd) 0.1966 0.1793 1.096

Passed once through JPEG q75 0.1966 0.0401 4.898

==================================== =============== =============== =========

• On uncompressed PNG it can tell nothing. The 1.096 times above means "pasted and unpasted areas cannot be told apart". ELA is meaningful only when "the original is a JPEG", and only then does the difference become 4.898 times.

• The common claim "ELA just tracks high frequencies" does not hold for this image either —— the correlation between ELA and Sobel gradient magnitude is 0.003 (uncompressed) / 0.057 (already JPEG). ELA looks at the quantization residual of the 8x8 blocks, so it measures something other than the gradient. Even so, as the table shows, it does not separate tampering when uncompressed. It fails not "because it is a rewording of the gradient", but because there is no quantization history to serve as the reference for comparison.

• Flat regions show no error even when compressed, so they stay dark even if tampered.

References (sample data, literature)

• Sample-data catalog (download URLs / licences) — 2-D uses skimage.data (BSD/public domain) plus synthetic images; 3-D lists download URLs for real data sources (Stanford, PDS, …).

• Operator provenance and references — the sources of the research/methods this op family came from.

• The canonical algorithm (author, year) and its uses are named in the family usage guide above.

Runnable examples (verified samples that actually call this op)

• image_forensics_audit — py -3.11 examples/image_forensics_audit.py

• poc_forensics_roc — py -3.11 examples/poc_forensics_roc.py

Ops the type connects to (they accept image2d as input)

perceptual_hash · fingerprint_correlate · jpeg_quality_estimate · jpeg_ghost_map · noise_inconsistency_map · copy_move_regions · watermark_embed · watermark_extract

Same category (compression)

jpeg_quality_estimate · jpeg_ghost_map · jpeg_ghost_quality


*Provenance: imgforensics.py — IMGFORENSICS operator registry. This per-op note is generated by tools/opdocs.py md (do not hand-edit).*

© 2026 Kazufumi Furuse — Fullseye operator documentation. Licensed under Apache-2.0.