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"))
• GPU: none (CPU reference implementation only)
Blind estimation of the quantisation table and JPEG quality from a decoded image. table.
Instead of reading the DQT of the file, estimate it from pixels only (that's the point, Even if the image is resaved/pasted to PNG, the originally JPEG quality N'' is displayed. visible). Take an 8x8 block DCT and estimate the comb spacing for each coefficient to (Fan & de Queiroz 2003), from a family of standard tables created using IJG's public scaling rules. Choose the quality that best suits you.
Return (dict):
jpeg_compressed Judging by how many n_coeff coefficients were combed
bool. This is not proof that it is a JPEG (caveats below)
quality Estimated quality 1..100 (None if there is no comb) table Estimated quantization step of (8, 8) (0 = not estimated) n_quantized raw number of combed coefficients excluding DC n_tested = n_coeff - 1 fit_error Mean absolute difference from the selected quality standard table caveats What this number cannot say
Actual measurement (tests/test_imgforensics.py::test_jpeg_quality_estimate_recovers_quality, Encode 256x256 1/f^1.6 image with Pillow → decode and estimate):
======= ========= =============== ============ True Q estimate Q combed coefficient 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 — ======= ========= =============== ============
It is a theoretical limit that it collapses on the low quality side. As the quality decreases, the high frequency coefficient becomes 0. quantized, and the comb reading material itself disappears from the image (the column of combed coefficients'' is How it disappears). At quality 30, there is not enough material and does not return an estimate(quality=None). The design policy of this op is not to force an answer here.
and jpeg_compressed=False / quality=None for uncompressed PNG. (Confirmed with 4 seeds, fixed in the same test). The best thing you can do is not just say “quality 100”. In this case, "uncompressed" and "virtually undegraded JPEG" will be 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.