sensor op• Data kinds: images → fingerprint
• Call: import fullseye as fs; fs.ledger.sensor_fingerprint(images, denoiser: 'str' = 'wiener', sigma: 'float' = 0.02, zero_mean: 'bool' = True) -> 'np.ndarray' (to call the implementation directly, import imgforensics; imgforensics.sensor_fingerprint(images, denoiser: 'str' = 'wiener', sigma: 'float' = 0.02, zero_mean: 'bool' = True) -> 'np.ndarray'; from the registry, opsimgforensics.get("sensor_fingerprint"))
Maximum-likelihood estimate of the PRNU sensor fingerprint K from several frames (Chen et al. 2008).
K = Σ_i W_i I_i / Σ_i I_i² (W_i = I_i - denoise(I_i)). Under the imaging model I = I⁰ + I⁰·K + Θ this is the maximum-likelihood estimator of K —— PRNU shows up more strongly in brighter pixels (because it is a multiplicative defect), so it is a brightness-weighted average.
The return is (H, W) float64; with zero_mean=True (default) the row and column means are removed and it is normalized to standard deviation 1. The normalization makes the PCE of fingerprint_correlate scale-invariant; the absolute strength of the fingerprint is returned separately by fingerprint_strength_map.
images are 2 or more of the same shape. Do not resize them to match —— PRNU is the physical position of the pixel itself, so the fingerprint vanishes the moment you interpolate (a shape mismatch raises ValueError).
Measured number of images vs. separation (tests/test_imgforensics.py::test_prnu_separates_two_sensors, 128x128, PRNU strength 3%, read noise σ=0.01, default denoiser="wiener"):
====== ======================== ================= =================
Images Corr. with true K PCE same sensor PCE other sensor
====== ======================== ================= =================
2 +0.727 5295 16.5
4 +0.809 6472 14.5
8 +0.859 7246 13.1
16 +0.892 7755 -12.3
====== ======================== ================= =================
Measuring "the correlation with the true K" is what matters. Looking only at PCE, it can "look separated" even if the fingerprint captures nothing of the sensor pattern —— in fact this implementation was once in that state (docstring of _wiener_denoise). PCE measures "something that appears consistently for the same sensor", so PCE itself offers no guarantee that it is PRNU.
denoiser="wavelet" (requires PyWavelets) is consistently stronger under the same conditions: correlation +0.745 to +0.901, same-sensor PCE 6068 to 8852. The default is "wiener" so as not to make an optional dependency mandatory, not because of accuracy.
These are values on synthetic noise; on real cameras the scene content leaks into the residual, so they become smaller by orders of magnitude. This table pins down that "the implementation works correctly"; it is not a threshold for real operation.
• 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
fingerprint as input)fingerprint_correlate · fingerprint_strength_map
sensor)fingerprint_correlate · 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.