solid op• Data kinds: image2d → table
• Call: import fullseye as fs; fs.ledger.speckle_quality(img: 'Any') -> 'dict[str, float]' (to call the implementation directly, import dic; dic.speckle_quality(img: 'Any') -> 'dict[str, float]'; from the registry, opspiv.get("speckle_quality"))
Returns 4 numbers indicating whether a captured speckle pattern is suitable for DIC.
Exists in neither pivops nor fullseye (speckle_filter is SAR speckle-noise removal, a different thing). An entry point for judging on the spot during capture "can this pattern be measured".
Arguments
img speckle image (2-D). The luminance scale does not matter, but coverage
is counted with a relative threshold based on the minimum and maximum within the image.
The return value is a dict:
mig mean intensity gradient sqrt(mean(Ix² + Iy²)).
The DIC quality index of Pan et al. The lower bound of the displacement variance is
set by σ_noise / (mig √N), so if this
is small, nothing you do can measure it.
grad_rms sqrt(mean(Ix²)) in the x direction only.
coverage fraction of pixels exceeding 0.2*(max-min) + min.
mean_blob_diameter_px an estimate of the mean speckle diameter [px] (see below).
★ mean_blob_diameter_px is an estimate, not a measurement
The radius R½ at which the radial average of the autocorrelation of the mean-subtracted image falls to 0.5 is found by linear
interpolation, and diameter = √2 · R½ is returned. The basis for √2: for a field of randomly scattered
Gaussian speckles of 1σ radius r, the autocorrelation is a Gaussian with 1σ = r√2, so
R½ = 2r√(ln2), while the FWHM of a speckle is 2r√(2ln2); the ratio is exactly √2.
In other words, it matches the FWHM only under the assumption that speckles are Gaussian and their positions uncorrelated.
Measured (left: analytic speckle, 1σ radius varied with the count adjusted to keep coverage equal.
right: piv_synth_particles; diameter_px is 2σ, so the FWHM is
1.1774 × diameter_px):
1σ r FWHM est. ratio| d_px FWHM est. ratio cov mig
0.6 1.41 1.36 0.97 | 1.5 1.77 1.80 1.02 0.043 0.1351
1.0 2.35 2.39 1.01 | 2.5 2.94 2.93 1.00 0.106 0.1676
1.6 3.77 3.77 1.00 | 4.0 4.71 4.69 1.00 0.159 0.1775
2.5 5.89 5.77 0.98 | 6.0 7.06 7.01 0.99 0.286 0.1764
4.0 9.42 9.18 0.97 |
Within 3 % for Gaussian speckles. Real speckle (printed, sprayed) is
not Gaussian, so this table confirms that the conversion arithmetic is right;
it is not the accuracy on real images. What it is biased by:
• if the speckle shape is not Gaussian, the √2 conversion itself is off.
• low-frequency unevenness (illumination gradient) lifts the tail of the autocorrelation and the result comes out too large. Apply a high-pass filter first.
• the autocorrelation is circular (FFT), so a periodic background disturbs it.
• the representative radius of a ring is the mean radius within the ring. Using the bin index mixes the diagonal √2 into the radius-1 ring, and at 1σ=0.6 px the diameter came out 0.80 times (2 tenths too small).
★ MIG is an index for a lower cut-off, not an objective function to maximise
In the left table above, mig grows the finer the speckles (0.1127 at 1σ=0.6 versus
0.0384 at 1σ=4.0, 2.9 times). Yet speckle of diameter 1.4 px is under-
sampled, and in the measurement of section 9 of examples/poc_dic_strain.py both bias and scatter
are worst. Maximising MIG picks speckle that cannot be measured.
For the particle images of the right table it plateaus at d=4.0 and drops slightly at d=6.0 ——
the same index is monotonic or not depending on how the input is made, so
use it to compare images taken the same way, not as an absolute value.
★ The coverage threshold is not Otsu
It is the fixed threshold 0.2*(max-min) + min. Otsu assumes a bimodal distribution, but
the intensity distribution of speckle is often unimodal because speckles overlap, and with Otsu
the threshold moves from image to image, making comparison impossible. A fixed one at least lets
images of the same scale be compared.
★ MIG has a unit
"luminance / px". Passing 8-bit integers as-is versus normalising to [0, 1] first makes
a factor of 255 difference. It can only be compared between images of the same scale.
fail-closed
• not 2-D / smaller than 8x8 / contains NaN / Inf.
• the image is completely uniform (max == min) —— not a single speckle.
• the autocorrelation does not fall below 0.5 (speckles larger than the field of view) —— only in this case is mean_blob_diameter_px nan. The other 3 are returned.
• piv_displacement family guide
• 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.
• piv_field_analysis_tour — py -3.11 examples/piv_field_analysis_tour.py
table as input)—
solid)strain_from_displacement · correlation_quality
*Provenance: dic.py — PIV 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.