sensor op• Data kinds: image2d × fingerprint → table
• Call: import fullseye as fs; fs.ledger.fingerprint_correlate(image, fingerprint, denoiser: 'str' = 'wiener', sigma: 'float' = 0.02, exclude: 'int' = 11) -> 'dict' (to call the implementation directly, import imgforensics; imgforensics.fingerprint_correlate(image, fingerprint, denoiser: 'str' = 'wiener', sigma: 'float' = 0.02, exclude: 'int' = 11) -> 'dict'; from the registry, opsimgforensics.get("fingerprint_correlate"))
Match one image against a fingerprint. It returns no verdict — an evidence statistic and its caveats.
Takes the circular cross-correlation of the residual W = I - denoise(I) and the reference signal I·K via FFT, and returns the peak of the normalized cross-correlation and the PCE (peak-to-correlation energy).
Returns (a dict of the table vocabulary):
pce PCE. No threshold is bundled (see caveats below)
ncc_peak peak value of the normalized cross-correlation ([-1, 1])
peak_shift position of the peak (dy, dx). If it is not (0, 0), the position is shifted
= a clue to suspect cropping / image stabilization / resizing
n_pixels number of pixels used
caveats list (strings) of what this number cannot tell
Returning peak_shift matters: a peak other than (0, 0) is either "the same sensor but cropped" or "a chance correlation". Which of the two it is, this op cannot decide. In measurements, matching against a different sensor always peaks somewhere other than (0, 0) ((-51, -60) (31, -8) etc., scattered every time), while the same sensor always peaks at (0, 0).
Recompression kills PRNU (tests/test_imgforensics.py::test_prnu_dies_under_recompression, fingerprint built from 8 images, 128x128):
========== ========== ==========
Recompr. PCE Ratio to none
========== ========== ==========
None 7246 1.00
Quality 95 6174 0.85
Quality 90 4214 0.58
Quality 75 1412 0.19
Quality 50 530 0.07
Quality 30 207 0.03
========== ========== ==========
So a low PCE may mean not "a different camera" but "no information left". Bundling a threshold here would turn it into a tool that mechanically judges compressed images to be "a different camera".
A path that is silently wrong (measured): if an ordinary image instead of a fingerprint is passed to fingerprint, the shape matches, so no exception is raised and a finite PCE is returned. A fingerprint is (H, W) float64, so it fully satisfies the existing image2d predicate and cannot be distinguished at run time. Therefore anything "not zero-mean" is rejected at the entry with ValueError (|mean| > 0.05 * std). Measured (tests/test_imgforensics.py::test_fingerprint_gate_rejects_plain_images):
============================== ==============
What was passed |mean|/std
============================== ============== sensor_fingerprint output 4.8e-18
Ordinary natural image 4.626
Dark image (mean 0.015) 4.626
High-contrast image 1.801
============================== ==============
The ratio is scale-invariant, so dark and bright images are rejected alike (more than 1.5 orders of magnitude away from the threshold 0.05).
Still not complete: an image you zero-meaned yourself passes the gate, and a finite value PCE = -5.97 is returned (measured). That is, the run-time check can guard only one side. This is the basis of the decision "do not let the fingerprint piggyback on image2d; give it its own vocabulary", detailed in the docstring of opsimgforensics.
• 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_prnu_camera_fingerprint — py -3.11 examples/poc_prnu_camera_fingerprint.py
table as input)sensor)sensor_fingerprint · fingerprint_strength_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.