diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 1f9e012..7872340 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -45,3 +45,20 @@ jobs: with: file: lcov.info token: ${{ secrets.CODECOV_TOKEN }} + docs: + name: Documentation + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v2 + - uses: julia-actions/setup-julia@v2 + with: + version: '1' + - name: registry_add + run: julia -e 'using Pkg; pkg"registry add General https://github.com/HolyLab/HolyLabRegistry.git"' + - uses: julia-actions/julia-buildpkg@v1 + - uses: julia-actions/julia-docdeploy@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} diff --git a/Project.toml b/Project.toml index e5d07e4..59f4449 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "RegisterMismatch" uuid = "3c0dd727-6833-5f5d-a1e8-c0d421935c74" +version = "1.0.0" authors = ["Tim Holy "] -version = "0.4.2" [deps] FFTW = "7a1cc6ca-52ef-59f5-83cd-3a7055c09341" @@ -15,21 +15,32 @@ RegisterCore = "67712758-55e7-5c3c-8e85-dda1d7758434" RegisterMismatchCommon = "abb2e897-52bf-5d28-a379-6ca321e3b878" [compat] +Aqua = "0.8" +CenterIndexedArrays = "1" +Documenter = "1" +ExplicitImports = "1" FFTW = "0.3, 1" ImageCore = "0.8.1, 0.9, 0.10" +ImageFiltering = "0.7" +Libdl = "1" MappedArrays = "0.2, 0.3, 0.4" PaddedViews = "0.5" +Printf = "1" RFFT = "0.1, 1" Reexport = "0.2, 1" RegisterCore = "0.2.1, 1" RegisterMismatchCommon = "0.2.2, 1" +Test = "1" julia = "1.10" [extras] +Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" CenterIndexedArrays = "46a7138f-0d70-54e1-8ada-fb8296f91f24" +ExplicitImports = "7d51a73a-1435-4ff3-83d9-f097790105c7" ImageFiltering = "6a3955dd-da59-5b1f-98d4-e7296123deb5" Libdl = "8f399da3-3557-5675-b5ff-fb832c97cbdb" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Test", "CenterIndexedArrays", "ImageFiltering", "Libdl"] +test = ["Aqua", "Documenter", "ExplicitImports", "Test", "CenterIndexedArrays", "ImageFiltering", "Libdl"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..0a283f2 --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +# RegisterMismatch.jl + +[![CI](https://github.com/HolyLab/RegisterMismatch.jl/actions/workflows/CI.yml/badge.svg)](https://github.com/HolyLab/RegisterMismatch.jl/actions/workflows/CI.yml) +[![Coverage](https://codecov.io/gh/HolyLab/RegisterMismatch.jl/branch/master/graph/badge.svg)](https://codecov.io/gh/HolyLab/RegisterMismatch.jl) +[![Stable docs](https://img.shields.io/badge/docs-stable-blue.svg)](https://HolyLab.github.io/RegisterMismatch.jl/stable) +[![Dev docs](https://img.shields.io/badge/docs-dev-blue.svg)](https://HolyLab.github.io/RegisterMismatch.jl/dev) + +FFT-based image mismatch computation for translation-based image registration. +This package is the CPU backend in the +[HolyLab](https://github.com/HolyLab) image registration stack; a GPU +counterpart is provided by +[RegisterMismatchCuda.jl](https://github.com/HolyLab/RegisterMismatchCuda.jl). + +## Installation + +This package is registered in the +[HolyLabRegistry](https://github.com/HolyLab/HolyLabRegistry). +Add the registry once, then install as usual: + +```julia +using Pkg +pkg"registry add General https://github.com/HolyLab/HolyLabRegistry.git" +Pkg.add("RegisterMismatch") +``` + +## Quick start + +```julia +using RegisterMismatch + +# Two images, moving shifted by (2, 3) relative to fixed +fixed = zeros(Float32, 32, 32); fixed[10:22, 10:22] .= 1f0 +moving = circshift(fixed, (2, 3)) + +# Find the optimal integer-pixel shift +shift = register_translate(fixed, moving, (5, 5)) +# CartesianIndex(2, 3) +``` + +For non-uniform or large displacements, use `mismatch_apertures` to obtain a +grid of local mismatches and recover a smooth displacement field. + +See the [documentation](https://HolyLab.github.io/RegisterMismatch.jl/stable) +for a full guide, including preprocessing (`highpass`, `nanpad`), +aperture-based registration, bias correction, and the low-level `CMStorage` +workflow for repeated computations against the same fixed image. diff --git a/docs/Project.toml b/docs/Project.toml new file mode 100644 index 0000000..5a80ec1 --- /dev/null +++ b/docs/Project.toml @@ -0,0 +1,13 @@ +[deps] +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +FFTW = "7a1cc6ca-52ef-59f5-83cd-3a7055c09341" +RegisterCore = "67712758-55e7-5c3c-8e85-dda1d7758434" +RegisterMismatch = "3c0dd727-6833-5f5d-a1e8-c0d421935c74" +RegisterMismatchCommon = "abb2e897-52bf-5d28-a379-6ca321e3b878" + +[compat] +Documenter = "1" +FFTW = "1" +RegisterCore = "1" +RegisterMismatchCommon = "1" +julia = "1.10" diff --git a/docs/make.jl b/docs/make.jl new file mode 100644 index 0000000..cb69c51 --- /dev/null +++ b/docs/make.jl @@ -0,0 +1,34 @@ +using Documenter +using RegisterMismatch +using RegisterCore +using RegisterMismatchCommon +using FFTW + +DocMeta.setdocmeta!(RegisterMismatch, :DocTestSetup, :(using RegisterMismatch, RegisterCore, FFTW); recursive = true) +DocMeta.setdocmeta!(RegisterMismatchCommon, :DocTestSetup, :(using RegisterMismatchCommon); recursive = true) +DocMeta.setdocmeta!(RegisterCore, :DocTestSetup, :(using RegisterCore); recursive = true) + +makedocs(; + modules = [RegisterMismatch, RegisterMismatchCommon, RegisterCore], + sitename = "RegisterMismatch.jl", + format = Documenter.HTML(; + prettyurls = get(ENV, "CI", "false") == "true", + canonical = "https://HolyLab.github.io/RegisterMismatch.jl", + repolink = "https://github.com/HolyLab/RegisterMismatch.jl", + ), + pages = [ + "Home" => "index.md", + "API Reference" => "api.md", + ], + checkdocs = :exports, + warnonly = false, + # upstream packages are installed (not dev'd), so Documenter cannot determine + # their git commit; disable source links to avoid MissingRemoteError + remotes = nothing, +) + +deploydocs(; + repo = "github.com/HolyLab/RegisterMismatch.jl", + devbranch = "master", + push_preview = true, +) diff --git a/docs/src/api.md b/docs/src/api.md new file mode 100644 index 0000000..367cc10 --- /dev/null +++ b/docs/src/api.md @@ -0,0 +1,103 @@ +# API Reference + +```@meta +DocTestSetup = quote + using RegisterMismatch, RegisterCore, FFTW +end +``` + +## RegisterMismatch module + +```@docs +RegisterMismatch +``` + +## Core mismatch functions + +```@docs +mismatch +mismatch! +mismatch_apertures +mismatch_apertures! +mismatch0 +``` + +## Low-level reusable workflow + +```@docs +CMStorage +fillfixed! +``` + +## Preprocessing + +```@docs +highpass +highpass! +nanpad +``` + +## Post-processing + +```@docs +correctbias! +truncatenoise! +``` + +## Utilities + +```@docs +register_translate +aperture_grid +allocate_mmarrays +aperture_range +each_point +set_FFTPROD +``` + +## Types and helpers (RegisterCore) + +```@docs +MismatchArray +NumDenom +separate +ratio +maxshift +mismatcharrays +``` + +## Indexing helpers (RegisterCore) + +```@docs +argmin_mismatch +paddedview +trimmedview +ColonFun +``` + +## Preprocessing (RegisterCore) + +```@docs +PreprocessSNF +``` + +## Type aliases (RegisterMismatchCommon) + +```@docs +DimsLike +WidthLike +``` + +## Internal helpers + +```@docs +assertsamesize +checksize_maxshift +default_aperture_width +padranges +padsize +shiftrange +tovec +RegisterMismatchCommon +RegisterCore +``` diff --git a/docs/src/index.md b/docs/src/index.md new file mode 100644 index 0000000..57a4be1 --- /dev/null +++ b/docs/src/index.md @@ -0,0 +1,173 @@ +# RegisterMismatch.jl + +```@meta +DocTestSetup = quote + using RegisterMismatch, RegisterCore, FFTW +end +``` + +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](https://github.com/HolyLab) image-analysis ecosystem. + +## Installation + +This package is registered in the +[HolyLabRegistry](https://github.com/HolyLab/HolyLabRegistry). +Add the registry once, then install as usual: + +```julia +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`](https://github.com/HolyLab/RegisterCore.jl) value so that results +from multiple apertures can be summed before dividing. The output of +[`mismatch`](@ref) 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`](@ref) 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`](@ref) removes low-frequency background before +registration. If `fixed` and `moving` have different sizes, [`nanpad`](@ref) +pads the smaller image with `NaN`s so both inputs have the same size. + +## Basic usage + +### Global registration + +```jldoctest +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`](@ref) calls [`mismatch`](@ref) internally and returns +the shift that minimises the normalised mismatch. You can also inspect the +full mismatch array: + +```jldoctest +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 + +```jldoctest +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 `MismatchArray`s, one per aperture. Pass the result +to downstream routines (e.g. from +[RegisterDeformation.jl](https://github.com/HolyLab/RegisterDeformation.jl)) +to recover a smooth displacement field. + +### Preprocessing + +Strip low-frequency background before computing mismatch: + +```jldoctest +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) +true +``` + +Pad images to the same size so that `mismatch` can accept them: + +```jldoctest +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]) +true +``` + +### Reusing 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: + +```jldoctest +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!`](@ref) imputes corrupted entries +(arising from camera-bias inhomogeneity along axes through the origin) from +their neighbours. [`truncatenoise!`](@ref) zeros out entries whose denominator +is below a noise threshold. + +```jldoctest +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. diff --git a/src/RegisterMismatch.jl b/src/RegisterMismatch.jl index 13abcf1..a7b100f 100644 --- a/src/RegisterMismatch.jl +++ b/src/RegisterMismatch.jl @@ -1,18 +1,27 @@ module RegisterMismatch -import Base: copy, eltype, isnan, ndims - -using ImageCore -using RFFT, FFTW -using RegisterCore, PaddedViews, MappedArrays -using Printf -using Reexport -@reexport using RegisterMismatchCommon +import Base: copy, eltype, ndims + +using FFTW: FFTW +using ImageCore: ImageCore, sdims +using MappedArrays: MappedArrays, of_eltype +using PaddedViews: PaddedViews, PaddedView +using Printf: Printf, @printf +using RFFT: RFFT, RCpair, plan_irfft!, plan_rfft! +using Reexport: Reexport, @reexport +using RegisterCore: RegisterCore, MismatchArray, highpass, maxshift import RegisterMismatchCommon: mismatch0, mismatch, mismatch_apertures +@reexport using RegisterMismatchCommon: DimsLike, RegisterMismatchCommon, WidthLike, + allocate_mmarrays, aperture_grid, aperture_range, + assertsamesize, checksize_maxshift, correctbias!, + default_aperture_width, each_point, nanpad, + padranges, padsize, register_translate, set_FFTPROD, + shiftrange, tovec, truncatenoise! export CMStorage, fillfixed!, + highpass, mismatch0, mismatch, mismatch!, @@ -65,33 +74,54 @@ FFTW.set_num_threads(min(Sys.CPU_THREADS, 8)) set_FFTPROD([2, 3]) mutable struct NanCorrFFTs{T <: AbstractFloat, N, RCType <: RCpair{T, N}} - I0::RCType - I1::RCType - I2::RCType + const I0::RCType + const I1::RCType + const I2::RCType end copy(x::NanCorrFFTs) = NanCorrFFTs(copy(x.I0), copy(x.I1), copy(x.I2)) """ - CMStorage{T}(undef, aperture_width, maxshift; flags=FFTW.ESTIMATE, timelimit=Inf, display=true) + CMStorage{T}(undef, aperture_width, maxshift; flags=FFTW.ESTIMATE, timelimit=Inf, display=true) -> CMStorage{T} -Prepare for FFT-based mismatch computations over domains of size `aperture_width`, computing the -mismatch up to shifts of size `maxshift`. The keyword arguments allow you to control the planning -process for the FFTs. +Pre-allocate FFT working storage for mismatch computations over apertures of size `aperture_width` +with translations up to `maxshift`. The element type `T` (e.g., `Float32` or `Float64`) must be +specified explicitly. + +`flags` is an FFTW planning flag: `FFTW.ESTIMATE` (default, instant) or `FFTW.MEASURE` / +`FFTW.PATIENT` (slower to plan but faster per call — worthwhile when the same aperture size is +reused many times). `timelimit` caps planning time in seconds. Set `display=false` to suppress +the planning progress message printed when `flags != FFTW.ESTIMATE`. + +The typical low-level workflow is: +1. Construct `CMStorage` once. +2. Call [`fillfixed!`](@ref) to load the fixed image. +3. Call [`mismatch!`](@ref) for each moving image. + +# Examples +```jldoctest +julia> cms = CMStorage{Float32}(undef, (10, 10), (3, 3)); + +julia> eltype(cms) +Float32 + +julia> ndims(cms) +2 +``` """ mutable struct CMStorage{T <: AbstractFloat, N, RCType <: RCpair{T, N}, FFT <: Function, IFFT <: Function} - aperture_width::Vector{Float64} - maxshift::Vector{Int} - getindices::Vector{UnitRange{Int}} # indices for pulling padded data, in source-coordinates - padded::Array{T, N} - fixed::NanCorrFFTs{T, N, RCType} - moving::NanCorrFFTs{T, N, RCType} - buf1::RCType - buf2::RCType + const aperture_width::Vector{Float64} + const maxshift::Vector{Int} + const getindices::Vector{UnitRange{Int}} # indices for pulling padded data, in source-coordinates + const padded::Array{T, N} + const fixed::NanCorrFFTs{T, N, RCType} + const moving::NanCorrFFTs{T, N, RCType} + const buf1::RCType + const buf2::RCType # the next two store the result of calling plan_fft! and plan_ifft! - fftfunc!::FFT - ifftfunc!::IFFT - shiftindices::Vector{Vector{Int}} # indices for performing fftshift & snipping from -maxshift:maxshift + const fftfunc!::FFT + const ifftfunc!::IFFT + const shiftindices::Vector{Vector{Int}} # indices for performing fftshift & snipping from -maxshift:maxshift end function CMStorage{T, N}(::UndefInitializer, aperture_width::NTuple{N, <:Real}, maxshift::Dims{N}; flags = FFTW.ESTIMATE, timelimit = Inf, display = true) where {T, N} @@ -126,18 +156,36 @@ CMStorage{T}(::UndefInitializer, aperture_width::NTuple{N, <:Real}, maxshift::Di eltype(cms::CMStorage{T, N}) where {T, N} = T ndims(cms::CMStorage{T, N}) where {T, N} = N +copy(cms::CMStorage) = CMStorage(copy(cms.aperture_width), copy(cms.maxshift), copy(cms.getindices), + copy(cms.padded), copy(cms.fixed), copy(cms.moving), + copy(cms.buf1), copy(cms.buf2), + cms.fftfunc!, cms.ifftfunc!, + deepcopy(cms.shiftindices)) """ - mm = mismatch([T], fixed, moving, maxshift; normalization=:intensity) + mm = mismatch([T], fixed, moving, maxshift; normalization=:intensity) -> MismatchArray + +Compute the mismatch between `fixed` and `moving` as a function of translations up to size +`maxshift`. Returns a `MismatchArray` indexed from `-maxshift[d]:maxshift[d]` in each dimension. + +The optional type parameter `T` sets the element type (default: `Float32` for integer or +fixed-point images, `eltype(fixed)` otherwise). The `normalization` keyword controls the +denominator: `:intensity` (default) normalizes by local image intensity; `:pixels` normalizes +by pixel count. + +`fixed` and `moving` must have the same size; pad with `NaN`s as needed (see [`nanpad`](@ref)). + +# Examples +```jldoctest +julia> fixed = zeros(Float32, 10, 10); fixed[3:7, 3:7] .= 1; + +julia> moving = circshift(fixed, (2, 1)); -Compute the mismatch between `fixed` and -`moving` as a function of translations (shifts) up to size `maxshift`. -Optionally specify the element-type of the mismatch arrays (default -`Float32` for Integer- or FixedPoint-valued images) and the -normalization scheme (`:intensity` or `:pixels`). +julia> mm = mismatch(fixed, moving, (3, 3)); -`fixed` and `moving` must have the same size; you can pad with -`NaN`s as needed. See `nanpad`. +julia> size(mm) +(7, 7) +``` """ function mismatch(::Type{T}, fixed::AbstractArray, moving::AbstractArray, maxshift::DimsLike; normalization = :intensity) where {T <: Real} msz = 2 .* maxshift .+ 1 @@ -151,9 +199,13 @@ function mismatch(::Type{T}, fixed::AbstractArray, moving::AbstractArray, maxshi end """ -`mismatch!(mm, cms, moving; [normalization=:intensity])` -computes the mismatch as a function of shift, storing the result in -`mm`. The `fixed` image has been prepared in `cms`, a `CMStorage` object. + mismatch!(mm, cms, moving; normalization=:intensity) -> mm + +Compute the mismatch as a function of shift, storing the result in `mm`. The `fixed` image must +have been loaded into `cms` via [`fillfixed!`](@ref) before calling this function. `cms` is a +[`CMStorage`](@ref) object. + +See also [`mismatch`](@ref) for a higher-level interface that handles setup automatically. """ function mismatch!(mm::MismatchArray, cms::CMStorage, moving::AbstractArray; normalization = :intensity) # Pad the moving snippet using any available data, including @@ -195,29 +247,37 @@ function mismatch!(mm::MismatchArray, cms::CMStorage, moving::AbstractArray; nor end """ -`mms = mismatch_apertures([T], fixed, moving, gridsize, maxshift; -[normalization=:pixels], [flags=FFTW.MEASURE], kwargs...)` computes -the mismatch between `fixed` and `moving` over a regularly-spaced grid -of aperture centers, effectively breaking the images up into -chunks. The maximum-allowed shift in any aperture is `maxshift`. - -`mms = mismatch_apertures([T], fixed, moving, aperture_centers, -aperture_width, maxshift; kwargs...)` computes the mismatch between -`fixed` and `moving` over a list of apertures of size `aperture_width` -at positions defined by `aperture_centers`. - -`fixed` and `moving` must have the same size; you can pad with `NaN`s -as needed to ensure this. You can optionally specify the real-valued -element type mm; it defaults to the element type of `fixed` and -`moving` or, for Integer- or FixedPoint-valued images, `Float32`. - -On output, `mms` will be an Array-of-MismatchArrays, with the outer -array having the same "grid" shape as `aperture_centers`. The centers -can in general be provided as an vector-of-tuples, vector-of-vectors, -or a matrix with each point in a column. If your centers are arranged -in a rectangular grid, you can use an `N`-dimensional array-of-tuples -(or array-of-vectors) or an `N+1`-dimensional array with the center -positions specified along the first dimension. See `aperture_grid`. + mms = mismatch_apertures([T], fixed, moving, gridsize, maxshift; normalization=:pixels, flags=FFTW.MEASURE, kwargs...) + mms = mismatch_apertures([T], fixed, moving, aperture_centers, aperture_width, maxshift; kwargs...) + +Compute the mismatch between `fixed` and `moving` over a grid of aperture positions. Returns an +array of `MismatchArray`s with the same shape as `aperture_centers`. + +The first form divides the image into a `gridsize` regular grid, inferring aperture centers and +widths automatically. The second form accepts explicit `aperture_centers` and `aperture_width`. +In both cases, the mismatch within each aperture is computed for translations up to `maxshift`. + +`fixed` and `moving` must have the same size; pad with `NaN`s as needed (see [`nanpad`](@ref)). +The optional type parameter `T` sets the element type of the output (default: `Float32` for +integer or fixed-point images, `eltype(fixed)` otherwise). + +The `aperture_centers` can be a vector-of-tuples, vector-of-vectors, or matrix (each point as a +column); for rectangular grids use [`aperture_grid`](@ref). + +# Examples +```jldoctest +julia> using FFTW + +julia> fixed = ones(Float32, 20, 20); moving = ones(Float32, 20, 20); + +julia> mms = mismatch_apertures(fixed, moving, (2, 2), (3, 3); flags=FFTW.ESTIMATE); + +julia> size(mms) +(2, 2) + +julia> size(mms[1, 1]) +(7, 7) +``` """ function mismatch_apertures( ::Type{T}, @@ -238,13 +298,15 @@ function mismatch_apertures( end """ -`mismatch_apertures!(mms, fixed, moving, aperture_centers, cms; -[normalization=:pixels])` computes the mismatch between `fixed` and -`moving` over a list of apertures at positions defined by -`aperture_centers`. The parameters and working storage are contained -in `cms`, a `CMStorage` object. The results are stored in `mms`, an -Array-of-MismatchArrays which must have length equal to the number of -aperture centers. + mms = mismatch_apertures!(mms, fixed, moving, aperture_centers, cms; normalization=:pixels) -> mms + +Compute the mismatch between `fixed` and `moving` over apertures at positions `aperture_centers`, +storing results in `mms`. Working storage and parameters are provided by `cms`, a +[`CMStorage`](@ref) object. `mms` must be an array of `MismatchArray`s with length equal to the +number of aperture centers. + +See also [`mismatch_apertures`](@ref) for a higher-level interface, and +[`allocate_mmarrays`](@ref) for allocating `mms`. """ function mismatch_apertures!(mms, fixed, moving, aperture_centers, cms::CMStorage{T}; normalization = :pixels) where {T} N = ndims(cms) @@ -286,6 +348,16 @@ function _fftnan!(I0, I1, I2, A::AbstractArray{T}) where {T <: Real} end end +""" + fillfixed!(cms::CMStorage, fixed) -> cms + +Load the `fixed` image into `cms`, preparing it for mismatch computations. Call this once +before calling [`mismatch!`](@ref) one or more times with different moving images. + +This is the setup step performed internally by [`mismatch`](@ref) and +[`mismatch_apertures`](@ref). Use it directly when computing multiple mismatches against the +same fixed image with different moving images. +""" function fillfixed!(cms::CMStorage{T}, fixed::AbstractArray) where {T} fill!(cms.padded, NaN) pinds = CartesianIndices(ntuple(d -> (1:size(fixed, d)) .+ cms.maxshift[d], ndims(fixed))) @@ -293,30 +365,4 @@ function fillfixed!(cms::CMStorage{T}, fixed::AbstractArray) where {T} return fftnan!(cms.fixed, cms.padded, cms.fftfunc!) end -#### Utilities - -Base.isnan(A::Array{Complex{T}}) where {T} = isnan(real(A)) | isnan(imag(A)) -function sumsq_finite(A) - s = 0.0 - for a in A - if isfinite(a) - s += a * a - end - end - if s == 0 - error("No finite values available") - end - return s -end - -### Deprecations - -function CMStorage{T}(::UndefInitializer, aperture_width::WidthLike, maxshift::WidthLike; kwargs...) where {T <: Real} - Base.depwarn("CMStorage with aperture_width::$(typeof(aperture_width)) and maxshift::$(typeof(maxshift)) is deprecated, use tuples instead", :CMStorage) - (N = length(aperture_width)) == length(maxshift) || error("Dimensionality mismatch") - return CMStorage{T, N}(undef, (aperture_width...,), (maxshift...,); kwargs...) -end - -@deprecate CMStorage(::Type{T}, aperture_width, maxshift; kwargs...) where {T} CMStorage{T}(undef, aperture_width, maxshift; kwargs...) - end diff --git a/test/runtests.jl b/test/runtests.jl index 8f9cf6a..3056108 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,9 +1,22 @@ import RegisterMismatch # we use qualified names to support simultaneous testing with RegisterMismatchCuda, which exports the same API +using Aqua +using Documenter +using ExplicitImports using ImageCore, ImageFiltering using ImageCore.OffsetArrays using CenterIndexedArrays, RegisterCore, RegisterMismatchCommon using Test, Libdl +@testset "Aqua" begin + # mismatch/mismatch_apertures are intentionally extended from RegisterMismatchCommon + Aqua.test_all(RegisterMismatch; piracies = (broken = true,)) +end + +@testset "ExplicitImports" begin + # mismatch_apertures and FFTW non-public names are intentional cross-package accesses + @test check_no_implicit_imports(RegisterMismatch) === nothing +end + let _want_cuda = isdefined(Main, :use_cuda) ? Main.use_cuda : !isempty(find_library(["libcudart", "cudart"], ["/usr/local/cuda"])) _loaded = false @@ -232,6 +245,20 @@ end B = outer[3:12, 1:8] maxshift = (3, 11) @test register_translate(A, B, maxshift) == CartesianIndex((-2, 1)) + + # Invalid normalization keyword + fixed8 = rand(Float64, 8, 8) + moving8 = rand(Float64, 8, 8) + cms8 = RM.CMStorage{Float64}(undef, (8, 8), (2, 2)) + RM.fillfixed!(cms8, fixed8) + mm8 = MismatchArray(Float64, 5, 5) + moving_padded = rand(Float64, size(cms8.padded)) + @test_throws "normalization bad not recognized" RM.mismatch!(mm8, cms8, moving_padded; normalization = :bad) + + # copy(CMStorage) exercises copy(NanCorrFFTs) + cms2 = copy(cms8) + @test cms2 isa RegisterMismatch.CMStorage{Float64} + @test cms2.fixed !== cms8.fixed end # Mismatched types @@ -256,3 +283,8 @@ for j in -mxshift[2]:mxshift[2], i in -mxshift[1]:mxshift[1] mm[i, j] = sum((fsnip .- msnip) .^ 2) / (sum(fsnip .^ 2) + sum(msnip .^ 2)) end @test num ./ denom ≈ mm + +@testset "Doctests" begin + DocMeta.setdocmeta!(RegisterMismatch, :DocTestSetup, :(using RegisterMismatch, FFTW); recursive=true) + doctest(RegisterMismatch; manual=false) +end