RegisterMismatch.jl
RegisterMismatch computes the mismatch between two images as a function of integer translation, using FFT-based cross-correlation for efficiency. It serves as the CPU backend for image registration in the HolyLab image-analysis ecosystem.
Installation
This package is registered in the HolyLabRegistry. Add the registry once, then install as usual:
using Pkg
pkg"registry add General https://github.com/HolyLab/HolyLabRegistry.git"
Pkg.add("RegisterMismatch")Concept
Mismatch quantifies how dissimilar two images look when one is translated relative to the other. For a shift (i, j), the mismatch is
mismatch(i, j) = Σ (fixed[x, y] − moving[x+i, y+j])² / (Σ fixed² + Σ moving²)The numerator and denominator are stored separately in a NumDenom value so that results from multiple apertures can be summed before dividing. The output of mismatch is a MismatchArray indexed from −maxshift to +maxshift in each dimension; the shift where the mismatch is smallest is the best integer-pixel alignment.
For large images, or when the displacement varies across the field, use mismatch_apertures to compute local mismatches on a grid of sub-regions (apertures) and then combine or interpolate the resulting shift field.
Preprocessing
Real fluorescence images often have a non-zero background that dominates the mismatch signal. highpass removes low-frequency background before registration. If fixed and moving have different sizes, nanpad pads the smaller image with NaNs so both inputs have the same size.
Basic usage
Global registration
julia> fixed = zeros(Float32, 32, 32); fixed[10:22, 10:22] .= 1f0;
julia> moving = circshift(fixed, (2, 3));
julia> shift = register_translate(fixed, moving, (5, 5))
CartesianIndex(2, 3)register_translate calls mismatch internally and returns the shift that minimises the normalised mismatch. You can also inspect the full mismatch array:
julia> fixed = zeros(Float32, 32, 32); fixed[10:22, 10:22] .= 1f0;
julia> moving = circshift(fixed, (2, 3));
julia> mm = mismatch(fixed, moving, (5, 5));
julia> size(mm)
(11, 11)mm is indexed from −5 to +5 in each dimension; mm[2, 3] holds the mismatch at shift (2, 3).
Aperture-based (local) registration
julia> fixed = zeros(Float32, 32, 32); fixed[10:22, 10:22] .= 1f0;
julia> moving = circshift(fixed, (2, 3));
julia> mms = mismatch_apertures(fixed, moving, (2, 2), (5, 5); flags=FFTW.ESTIMATE);
julia> size(mms)
(2, 2)
julia> size(mms[1, 1])
(11, 11)mms is a 2×2 array of MismatchArrays, one per aperture. Pass the result to downstream routines (e.g. from RegisterDeformation.jl) to recover a smooth displacement field.
Preprocessing
Strip low-frequency background before computing mismatch:
julia> A = Float32[sin(0.1i + 0.2j) + 5f0 for i in 1:20, j in 1:20];
julia> Ahp = highpass(Float32, A, (3, 3));
julia> eltype(Ahp)
Float32
julia> maximum(abs, Ahp) < maximum(abs, A)
truePad images to the same size so that mismatch can accept them:
julia> A = ones(Float32, 5, 5); B = ones(Float32, 8, 7);
julia> Apad, Bpad = nanpad(A, B);
julia> size(Apad)
(8, 7)
julia> isnan(Apad[6, 1])
trueReusing FFT plans with CMStorage
When calling mismatch! many times against the same fixed image (e.g. in a multi-frame registration loop), pre-allocate the FFT working storage once and reuse it:
julia> cms = CMStorage{Float32}(undef, (16, 16), (4, 4));
julia> fixed = rand(Float32, 16, 16);
julia> fillfixed!(cms, fixed);
julia> mm = MismatchArray(Float32, 9, 9);
julia> moving_padded = rand(Float32, size(cms.padded));
julia> mm = mismatch!(mm, cms, moving_padded);
julia> size(mm)
(9, 9)Post-processing
After computing mismatch, correctbias! imputes corrupted entries (arising from camera-bias inhomogeneity along axes through the origin) from their neighbours. truncatenoise! zeros out entries whose denominator is below a noise threshold.
julia> mm = mismatch([1.0 2.0; 3.0 4.0], [1.0 2.0; 3.0 4.0], (1, 1));
julia> nd = mismatch0([1.0 2.0; 3.0 4.0], [1.0 2.0; 3.0 4.0])
NumDenom(0.0,60.0)nd.num == 0 confirms that identical images have zero mismatch at zero shift.