speckle_quality — PIV 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"))

Usage

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.

Detailed usage guide

• piv_displacement family guide

References (sample data, literature)

• 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.

Runnable examples (verified samples that actually call this op)

• piv_field_analysis_tour — py -3.11 examples/piv_field_analysis_tour.py

Ops the type connects to (they accept table as input)

—

Same category (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.