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"))
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.
• 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.
• image_forensics_audit — py -3.11 examples/image_forensics_audit.py
table as input)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.