From b09915d2403b1a6fb21a0eb76bfe368ac5b3ed4c Mon Sep 17 00:00:00 2001 From: Dae Woo Kim Date: Fri, 8 May 2026 14:44:00 -0500 Subject: [PATCH 1/9] Add Aqua.jl quality checks; fix compat entries and isnan piracy - Add Aqua as test dependency with all-pass checks (piracy broken=true for mismatch/mismatch_apertures, which intentionally extend RegisterMismatchCommon) - Add missing compat entries: Aqua, CenterIndexedArrays, ImageFiltering, Libdl, Printf, Test - Remove Base.isnan piracy for Array{Complex{T}} (method was unused) Co-Authored-By: Claude Sonnet 4.6 --- Project.toml | 11 +++++++++-- src/RegisterMismatch.jl | 3 +-- test/runtests.jl | 6 ++++++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/Project.toml b/Project.toml index e5d07e4..ccae80b 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "RegisterMismatch" uuid = "3c0dd727-6833-5f5d-a1e8-c0d421935c74" -authors = ["Tim Holy "] version = "0.4.2" +authors = ["Tim Holy "] [deps] FFTW = "7a1cc6ca-52ef-59f5-83cd-3a7055c09341" @@ -15,21 +15,28 @@ RegisterCore = "67712758-55e7-5c3c-8e85-dda1d7758434" RegisterMismatchCommon = "abb2e897-52bf-5d28-a379-6ca321e3b878" [compat] +Aqua = "0.8" +CenterIndexedArrays = "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" CenterIndexedArrays = "46a7138f-0d70-54e1-8ada-fb8296f91f24" 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", "Test", "CenterIndexedArrays", "ImageFiltering", "Libdl"] diff --git a/src/RegisterMismatch.jl b/src/RegisterMismatch.jl index 13abcf1..5009775 100644 --- a/src/RegisterMismatch.jl +++ b/src/RegisterMismatch.jl @@ -1,6 +1,6 @@ module RegisterMismatch -import Base: copy, eltype, isnan, ndims +import Base: copy, eltype, ndims using ImageCore using RFFT, FFTW @@ -295,7 +295,6 @@ 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 diff --git a/test/runtests.jl b/test/runtests.jl index 8f9cf6a..1ac6713 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,9 +1,15 @@ import RegisterMismatch # we use qualified names to support simultaneous testing with RegisterMismatchCuda, which exports the same API +using Aqua 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 + let _want_cuda = isdefined(Main, :use_cuda) ? Main.use_cuda : !isempty(find_library(["libcudart", "cudart"], ["/usr/local/cuda"])) _loaded = false From 4f867ebae8914c33e03a58f9fbe11d6ac6773779 Mon Sep 17 00:00:00 2001 From: Dae Woo Kim Date: Tue, 12 May 2026 13:29:09 -0500 Subject: [PATCH 2/9] Remove deprecated CMStorage constructors The vector-argument and old-style Type{T} constructors for CMStorage were replaced by tuple-based constructors in a prior release. Co-Authored-By: Claude Sonnet 4.6 --- .claude/freshen-package-status | 11 +++++++++++ src/RegisterMismatch.jl | 10 ---------- 2 files changed, 11 insertions(+), 10 deletions(-) create mode 100644 .claude/freshen-package-status diff --git a/.claude/freshen-package-status b/.claude/freshen-package-status new file mode 100644 index 0000000..1bfeca4 --- /dev/null +++ b/.claude/freshen-package-status @@ -0,0 +1,11 @@ +DONE: design review +DONE: API review +DONE: update .gitignore +DONE: format with runic +DONE: add Aqua.jl +DONE: remove deprecations +TODO: add ExplicitImports.jl +TODO: limit struct mutability +TODO: improve test coverage +TODO: add and improve docstrings +TODO: add or improve documentation diff --git a/src/RegisterMismatch.jl b/src/RegisterMismatch.jl index 5009775..69b7a66 100644 --- a/src/RegisterMismatch.jl +++ b/src/RegisterMismatch.jl @@ -308,14 +308,4 @@ function sumsq_finite(A) 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 From e716aeae7715f3118708cba9c8395ac902491743 Mon Sep 17 00:00:00 2001 From: Dae Woo Kim Date: Tue, 12 May 2026 13:36:05 -0500 Subject: [PATCH 3/9] Add ExplicitImports.jl: convert implicit to explicit imports Replace bare `using X` statements with explicit `using X: X, name, ...` forms for all dependencies. Add ExplicitImports as a test dependency with a regression check via `check_no_implicit_imports`. Co-Authored-By: Claude Sonnet 4.6 --- .claude/freshen-package-status | 2 +- Project.toml | 4 +++- src/RegisterMismatch.jl | 20 ++++++++++++++------ test/runtests.jl | 6 ++++++ 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/.claude/freshen-package-status b/.claude/freshen-package-status index 1bfeca4..fe6b783 100644 --- a/.claude/freshen-package-status +++ b/.claude/freshen-package-status @@ -4,7 +4,7 @@ DONE: update .gitignore DONE: format with runic DONE: add Aqua.jl DONE: remove deprecations -TODO: add ExplicitImports.jl +DONE: add ExplicitImports.jl TODO: limit struct mutability TODO: improve test coverage TODO: add and improve docstrings diff --git a/Project.toml b/Project.toml index ccae80b..aeca8df 100644 --- a/Project.toml +++ b/Project.toml @@ -17,6 +17,7 @@ RegisterMismatchCommon = "abb2e897-52bf-5d28-a379-6ca321e3b878" [compat] Aqua = "0.8" CenterIndexedArrays = "1" +ExplicitImports = "1" FFTW = "0.3, 1" ImageCore = "0.8.1, 0.9, 0.10" ImageFiltering = "0.7" @@ -34,9 +35,10 @@ julia = "1.10" [extras] Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" 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 = ["Aqua", "Test", "CenterIndexedArrays", "ImageFiltering", "Libdl"] +test = ["Aqua", "ExplicitImports", "Test", "CenterIndexedArrays", "ImageFiltering", "Libdl"] diff --git a/src/RegisterMismatch.jl b/src/RegisterMismatch.jl index 69b7a66..5ed9f2e 100644 --- a/src/RegisterMismatch.jl +++ b/src/RegisterMismatch.jl @@ -2,13 +2,21 @@ module RegisterMismatch import Base: copy, eltype, ndims -using ImageCore -using RFFT, FFTW -using RegisterCore, PaddedViews, MappedArrays -using Printf -using Reexport -@reexport using RegisterMismatchCommon +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, 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, diff --git a/test/runtests.jl b/test/runtests.jl index 1ac6713..3ebd1e4 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,5 +1,6 @@ import RegisterMismatch # we use qualified names to support simultaneous testing with RegisterMismatchCuda, which exports the same API using Aqua +using ExplicitImports using ImageCore, ImageFiltering using ImageCore.OffsetArrays using CenterIndexedArrays, RegisterCore, RegisterMismatchCommon @@ -10,6 +11,11 @@ using Test, Libdl 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 From b80ddbf86e0293286c15b4e137c6f2087dae9ad8 Mon Sep 17 00:00:00 2001 From: Dae Woo Kim Date: Tue, 12 May 2026 13:38:22 -0500 Subject: [PATCH 4/9] Mark all NanCorrFFTs and CMStorage fields as const No fields are ever rebound after construction; const makes the immutability explicit and enables compiler optimizations. Co-Authored-By: Claude Sonnet 4.6 --- .claude/freshen-package-status | 2 +- src/RegisterMismatch.jl | 28 ++++++++++++++-------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.claude/freshen-package-status b/.claude/freshen-package-status index fe6b783..3cec0b0 100644 --- a/.claude/freshen-package-status +++ b/.claude/freshen-package-status @@ -5,7 +5,7 @@ DONE: format with runic DONE: add Aqua.jl DONE: remove deprecations DONE: add ExplicitImports.jl -TODO: limit struct mutability +DONE: limit struct mutability TODO: improve test coverage TODO: add and improve docstrings TODO: add or improve documentation diff --git a/src/RegisterMismatch.jl b/src/RegisterMismatch.jl index 5ed9f2e..d8659be 100644 --- a/src/RegisterMismatch.jl +++ b/src/RegisterMismatch.jl @@ -73,9 +73,9 @@ 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)) @@ -88,18 +88,18 @@ mismatch up to shifts of size `maxshift`. The keyword arguments allow you to co process for the FFTs. """ 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} From e2cd4b1306a5003b9b9a9aed71749099f641a5b9 Mon Sep 17 00:00:00 2001 From: Dae Woo Kim Date: Tue, 12 May 2026 13:53:03 -0500 Subject: [PATCH 5/9] Fix highpass export, add copy(CMStorage), improve test coverage to 100% - Export highpass (from RegisterCore) which was advertised in the module docstring and tested but never actually re-exported - Add copy(::CMStorage) which deep-copies the internal NanCorrFFTs fields - Delete dead sumsq_finite function (defined but never called) - Add tests for invalid normalization error path and copy(CMStorage) Co-Authored-By: Claude Sonnet 4.6 --- .claude/freshen-package-status | 2 +- src/RegisterMismatch.jl | 23 +++++++---------------- test/runtests.jl | 14 ++++++++++++++ 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/.claude/freshen-package-status b/.claude/freshen-package-status index 3cec0b0..7707179 100644 --- a/.claude/freshen-package-status +++ b/.claude/freshen-package-status @@ -6,6 +6,6 @@ DONE: add Aqua.jl DONE: remove deprecations DONE: add ExplicitImports.jl DONE: limit struct mutability -TODO: improve test coverage +DONE: improve test coverage TODO: add and improve docstrings TODO: add or improve documentation diff --git a/src/RegisterMismatch.jl b/src/RegisterMismatch.jl index d8659be..a4aad54 100644 --- a/src/RegisterMismatch.jl +++ b/src/RegisterMismatch.jl @@ -9,7 +9,7 @@ using PaddedViews: PaddedViews, PaddedView using Printf: Printf, @printf using RFFT: RFFT, RCpair, plan_irfft!, plan_rfft! using Reexport: Reexport, @reexport -using RegisterCore: RegisterCore, MismatchArray, maxshift +using RegisterCore: RegisterCore, MismatchArray, highpass, maxshift import RegisterMismatchCommon: mismatch0, mismatch, mismatch_apertures @reexport using RegisterMismatchCommon: DimsLike, RegisterMismatchCommon, WidthLike, allocate_mmarrays, aperture_grid, aperture_range, @@ -21,6 +21,7 @@ import RegisterMismatchCommon: mismatch0, mismatch, mismatch_apertures export CMStorage, fillfixed!, + highpass, mismatch0, mismatch, mismatch!, @@ -134,6 +135,11 @@ 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) @@ -301,19 +307,4 @@ function fillfixed!(cms::CMStorage{T}, fixed::AbstractArray) where {T} return fftnan!(cms.fixed, cms.padded, cms.fftfunc!) end -#### Utilities - -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 - end diff --git a/test/runtests.jl b/test/runtests.jl index 3ebd1e4..4a77ad0 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -244,6 +244,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 From c7b515acb2de1b311247d80fcc0d5e3f66e495f0 Mon Sep 17 00:00:00 2001 From: Dae Woo Kim Date: Tue, 12 May 2026 14:09:32 -0500 Subject: [PATCH 6/9] Add and improve docstrings; add Documenter doctest coverage - Write docstring for `fillfixed!` (was exported but undocumented) - Improve `CMStorage`, `mismatch`, `mismatch!`, `mismatch_apertures`, and `mismatch_apertures!`: fix signature style (4-space indent, imperative mood), add return type annotations, add cross-references, and add `jldoctest` examples to the three primary entry points - Add `Documenter` as a test dependency and wire up `doctest()` in `runtests.jl` Co-Authored-By: Claude Sonnet 4.6 --- .claude/freshen-package-status | 2 +- Project.toml | 4 +- src/RegisterMismatch.jl | 148 +++++++++++++++++++++++---------- test/runtests.jl | 6 ++ 4 files changed, 113 insertions(+), 47 deletions(-) diff --git a/.claude/freshen-package-status b/.claude/freshen-package-status index 7707179..b2bf205 100644 --- a/.claude/freshen-package-status +++ b/.claude/freshen-package-status @@ -7,5 +7,5 @@ DONE: remove deprecations DONE: add ExplicitImports.jl DONE: limit struct mutability DONE: improve test coverage -TODO: add and improve docstrings +DONE: add and improve docstrings TODO: add or improve documentation diff --git a/Project.toml b/Project.toml index aeca8df..d33b266 100644 --- a/Project.toml +++ b/Project.toml @@ -17,6 +17,7 @@ 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" @@ -34,6 +35,7 @@ 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" @@ -41,4 +43,4 @@ Libdl = "8f399da3-3557-5675-b5ff-fb832c97cbdb" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Aqua", "ExplicitImports", "Test", "CenterIndexedArrays", "ImageFiltering", "Libdl"] +test = ["Aqua", "Documenter", "ExplicitImports", "Test", "CenterIndexedArrays", "ImageFiltering", "Libdl"] diff --git a/src/RegisterMismatch.jl b/src/RegisterMismatch.jl index a4aad54..a7b100f 100644 --- a/src/RegisterMismatch.jl +++ b/src/RegisterMismatch.jl @@ -82,11 +82,32 @@ 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} const aperture_width::Vector{Float64} @@ -142,16 +163,29 @@ copy(cms::CMStorage) = CMStorage(copy(cms.aperture_width), copy(cms.maxshift), c 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. -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`). +`fixed` and `moving` must have the same size; pad with `NaN`s as needed (see [`nanpad`](@ref)). -`fixed` and `moving` must have the same size; you can pad with -`NaN`s as needed. See `nanpad`. +# Examples +```jldoctest +julia> fixed = zeros(Float32, 10, 10); fixed[3:7, 3:7] .= 1; + +julia> moving = circshift(fixed, (2, 1)); + +julia> mm = mismatch(fixed, moving, (3, 3)); + +julia> size(mm) +(7, 7) +``` """ function mismatch(::Type{T}, fixed::AbstractArray, moving::AbstractArray, maxshift::DimsLike; normalization = :intensity) where {T <: Real} msz = 2 .* maxshift .+ 1 @@ -165,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 @@ -209,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}, @@ -252,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) @@ -300,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))) diff --git a/test/runtests.jl b/test/runtests.jl index 4a77ad0..3056108 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,5 +1,6 @@ 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 @@ -282,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 From 4366c2d9dc8134e8a66ad15189ed4450634aa49b Mon Sep 17 00:00:00 2001 From: Dae Woo Kim Date: Tue, 12 May 2026 14:51:36 -0500 Subject: [PATCH 7/9] Add README and Documenter docs with CI deploy step - README.md: badges (CI, coverage, docs), installation note for HolyLabRegistry, quick-start example - docs/: make.jl, Project.toml, src/index.md (concept overview + 7 jldoctests), src/api.md (full API reference via @docs blocks) - CI.yml: add docs job using julia-docdeploy - makedocs uses modules=[RegisterMismatch, RegisterMismatchCommon, RegisterCore] with checkdocs=:exports and remotes=nothing (upstream pkgs have no git history) Co-Authored-By: Claude Sonnet 4.6 --- .claude/freshen-package-status | 2 +- .github/workflows/CI.yml | 17 ++++ README.md | 46 +++++++++ docs/Project.toml | 13 +++ docs/make.jl | 34 +++++++ docs/src/api.md | 103 ++++++++++++++++++++ docs/src/index.md | 173 +++++++++++++++++++++++++++++++++ 7 files changed, 387 insertions(+), 1 deletion(-) create mode 100644 README.md create mode 100644 docs/Project.toml create mode 100644 docs/make.jl create mode 100644 docs/src/api.md create mode 100644 docs/src/index.md diff --git a/.claude/freshen-package-status b/.claude/freshen-package-status index b2bf205..9e5e151 100644 --- a/.claude/freshen-package-status +++ b/.claude/freshen-package-status @@ -8,4 +8,4 @@ DONE: add ExplicitImports.jl DONE: limit struct mutability DONE: improve test coverage DONE: add and improve docstrings -TODO: add or improve documentation +DONE: add or improve documentation 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/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. From f1073d55da8cfc5cc8aa658c4bd7f6c5a97d060a Mon Sep 17 00:00:00 2001 From: Dae Woo Kim Date: Tue, 12 May 2026 14:51:51 -0500 Subject: [PATCH 8/9] Remove freshen-package-status (all steps complete) Co-Authored-By: Claude Sonnet 4.6 --- .claude/freshen-package-status | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 .claude/freshen-package-status diff --git a/.claude/freshen-package-status b/.claude/freshen-package-status deleted file mode 100644 index 9e5e151..0000000 --- a/.claude/freshen-package-status +++ /dev/null @@ -1,11 +0,0 @@ -DONE: design review -DONE: API review -DONE: update .gitignore -DONE: format with runic -DONE: add Aqua.jl -DONE: remove deprecations -DONE: add ExplicitImports.jl -DONE: limit struct mutability -DONE: improve test coverage -DONE: add and improve docstrings -DONE: add or improve documentation From da8d54a2449129fea9e096555963aca7b6281b80 Mon Sep 17 00:00:00 2001 From: Dae Woo Kim Date: Tue, 12 May 2026 14:54:42 -0500 Subject: [PATCH 9/9] Bump version to 1.0.0 Co-Authored-By: Claude Sonnet 4.6 --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index d33b266..59f4449 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "RegisterMismatch" uuid = "3c0dd727-6833-5f5d-a1e8-c0d421935c74" -version = "0.4.2" +version = "1.0.0" authors = ["Tim Holy "] [deps]