jpeg_quality_estimate — IMGFORENSICS compression op

• Data kinds: image2d → table

• Call: import fullseye as fs; fs.ledger.jpeg_quality_estimate(image, max_step: 'int' = 64, n_coeff: 'int' = 21) -> 'dict' (to call the implementation directly, import imgforensics; imgforensics.jpeg_quality_estimate(image, max_step: 'int' = 64, n_coeff: 'int' = 21) -> 'dict'; from the registry, opsimgforensics.get("jpeg_quality_estimate"))

Usage

Blind estimation of the quantisation table and JPEG quality from a decoded image. table.

It does not read the file's DQT but estimates from the pixels alone (that is the point:

even for an image re-saved as PNG / pasted in, "it was originally JPEG quality N" becomes visible). It takes 8x8 block DCTs, estimates the comb spacing per coefficient (Fan & de Queiroz 2003), and from the family of standard tables built with IJG's public scaling rule selects the best-matching quality.

Returns (dict):

jpeg_compressed bool decided by how many of the n_coeff coefficients showed a comb.

Not a proof that "it is a JPEG" (see caveats below)

quality estimated quality 1..100 (None if there is no comb)

table estimated (8, 8) quantization steps (0 = could not be estimated)

n_quantized number of coefficients showing a comb / n_coeff

fit_error mean absolute difference from the standard table of the selected quality

caveats what this number cannot tell

Measured (tests/test_imgforensics.py::test_jpeg_quality_estimate_recovers_quality, 256x256 1/f^1.6 images encoded with Pillow → decoded, then estimated):

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

True Q Est. Q Comb coeffs fit_error

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

95 95 11/20 0.091

90 90 19/20 0.579

80 80 13/20 1.308

70 71 12/20 2.083

60 60 9/20 2.111

50 52 7/20 1.857

40 40 6/20 3.333

30 None 4/20 —

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

The breakdown on the low-quality side is a limit in principle. The lower the quality, the more high-frequency coefficients are quantized to 0, and the very material for reading the comb disappears from the image (the "comb coefficients" column shows how it disappears). At quality 30 there is not enough material, and it returns no estimate (quality=None). Not forcing an answer here is the design policy of this op.

And on uncompressed PNG, jpeg_compressed=False / quality=None (confirmed with 4 seeds, pinned in the same test). Not silently answering "quality 100" here is crucial; otherwise "uncompressed" and "nearly lossless JPEG" would give the same answer.

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

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

evidence_quantile

Same category (compression)

error_level_map · 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.