compression op• Data kinds: images → image2d
• Call: import fullseye as fs; fs.ledger.jpeg_ghost_quality(ghosts, qualities=None) -> 'np.ndarray' (to call the implementation directly, import imgforensics; imgforensics.jpeg_ghost_quality(ghosts, qualities=None) -> 'np.ndarray'; from the registry, opsimgforensics.get("jpeg_ghost_quality"))
A series of ghost maps → a map of the quality at which each pixel's residual is smallest. image2d.
If qualities is omitted, the default of jpeg_ghost_map (40..95 step 5) is assumed —— if the number of maps does not match, ValueError (it does not return a map whose indices and qualities are out of step).
The return is an (H, W) array holding the quality itself as pixel values, so the value range is not [0, 1]. Normalize it for display (this op returns numbers, not a picture).
★ If the composite is saved even once, this argmin is unusable. Measured on 2026-09-06 on a 128x128 image with a 32x32 patch pasted, background q92 and material q60:
==================== ================== ================
Condition Mode q of pasted Mode q of backgr.
==================== ================== ================
Not re-saved 60 (correct) 95
Re-saved at q95 95 (= backgr.) 95
==================== ================== ================
The reason is that the sweep residual decreases monotonically with quality —— if the last saved quality is at the top end of the sweep, argmin sticks there for every pixel (measured: the mean residual after saving drops in a straight line, 230.7 → 5.9e-02). Real-world tampering always goes through a save, so on real images this readout becomes a constant map. The existing test test_jpeg_ghost_finds_the_pasted_quality passes only because the composite is never saved (it is a correct unit test in itself, and the implementation of argmin is not wrong —— the way of reading it out does not fit the field).
What to use instead: the valley depth of the residual curve along the sweep (the local dip). Taking the per-pixel ROC in the same experiment, the argmin readout had AUC 0.500 (same as random), while the valley-depth readout had AUC 0.997 / detection rate 0.944 at 1 % false positives (examples/poc_forensics_roc.py). Even so, re-saving the whole image at q60 drops it to 0.475 —— "JPEG ghosts are weak against post-processing" is the correct summary.
• 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
• poc_forensics_roc — py -3.11 examples/poc_forensics_roc.py
image2d as input)perceptual_hash · fingerprint_correlate · error_level_map · jpeg_quality_estimate · jpeg_ghost_map · noise_inconsistency_map · copy_move_regions · watermark_embed
compression)error_level_map · jpeg_quality_estimate · jpeg_ghost_map
*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.