Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 0 additions & 11 deletions .claude/freshen-package-status

This file was deleted.

26 changes: 26 additions & 0 deletions .github/workflows/Documenter.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Documenter
on:
push:
branches:
- master
tags: '*'
pull_request:

jobs:
build:
permissions:
contents: write
statuses: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: julia-actions/setup-julia@v2
with:
version: '1'
- name: Add HolyLab registry
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 }}
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ Manifest.toml
Manifest-v*.toml
test/Manifest.toml
.vscode/
docs/build/
docs/Manifest.toml
12 changes: 11 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,32 +21,42 @@ Rotations = "6038ab10-8711-5258-84ad-4b1120ba62dc"
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"

[compat]
Aqua = "0.8"
AxisArrays = "0.3, 0.4"
CenterIndexedArrays = "1"
CoordinateTransformations = "0.5, 0.6"
Distributions = "0.20, 0.21, 0.22, 0.23, 0.24, 0.25"
Documenter = "1"
ExplicitImports = "1"
ImageCore = "0.10"
ImageFiltering = "0.7"
ImageMagick = "0.7, 1"
ImageMetadata = "0.9"
ImageTransformations = "0.10"
Interpolations = "0.12, 0.13, 0.14, 0.15"
LinearAlgebra = "1"
MappedArrays = "0.2, 0.3, 0.4"
OffsetArrays = "0.11, 1"
PaddedViews = "0.4, 0.5"
QuadDIRECT = "0.1"
Random = "1"
RegisterCore = "1"
RegisterDeformation = "1"
RegisterMismatch = "1"
RegisterMismatchCommon = "1"
Rotations = "0.12, 0.13, 1"
StaticArrays = "0.11, 0.12, 1"
Test = "1"
TestImages = "0.5, 0.6, 1"
Unitful = "0.17, 0.18, 1"
julia = "1.10"

[extras]
Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595"
AxisArrays = "39de3d68-74b9-583c-8d2d-e117c070f3a9"
Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
ExplicitImports = "7d51a73a-1435-4ff3-83d9-f097790105c7"
ImageMagick = "6218d12a-5da1-5696-b52f-db25d2ecc6d1"
ImageMetadata = "bc367c6b-8a6b-528e-b4bd-a4b897500b49"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
Expand All @@ -57,4 +67,4 @@ TestImages = "5e47fb64-e119-507b-a336-dd2b206d9990"
Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d"

[targets]
test = ["Test", "ImageMagick", "TestImages", "Random", "AxisArrays", "ImageMetadata", "Unitful", "Distributions", "LinearAlgebra", "RegisterMismatch"]
test = ["Aqua", "Documenter", "ExplicitImports", "Test", "ImageMagick", "TestImages", "Random", "AxisArrays", "ImageMetadata", "Unitful", "Distributions", "LinearAlgebra", "RegisterMismatch"]
57 changes: 47 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,60 @@
# RegisterQD

[![CI](https://github.com/HolyLab/RegisterQD.jl/actions/workflows/CI.yml/badge.svg)](https://github.com/HolyLab/RegisterQD.jl/actions/workflows/CI.yml)[![codecov](https://codecov.io/gh/HolyLab/RegisterQD.jl/branch/master/graph/badge.svg)](https://codecov.io/gh/HolyLab/RegisterQD.jl)
[![CI](https://github.com/HolyLab/RegisterQD.jl/actions/workflows/CI.yml/badge.svg)](https://github.com/HolyLab/RegisterQD.jl/actions/workflows/CI.yml)
[![codecov](https://codecov.io/gh/HolyLab/RegisterQD.jl/branch/master/graph/badge.svg)](https://codecov.io/gh/HolyLab/RegisterQD.jl)
[![Stable docs](https://img.shields.io/badge/docs-stable-blue.svg)](https://HolyLab.github.io/RegisterQD.jl/stable/)
[![Dev docs](https://img.shields.io/badge/docs-dev-blue.svg)](https://HolyLab.github.io/RegisterQD.jl/dev/)
[![Aqua QA](https://juliatesting.github.io/Aqua.jl/dev/assets/badge.svg)](https://github.com/JuliaTesting/Aqua.jl)

RegisterQD performs image registration using the global optimization routine [QuadDIRECT](https://github.com/timholy/QuadDIRECT.jl).
Unlike many other registration packages, this is not "greedy" descent based on an initial guess---it attempts to find the globally-optimal alignment of your images.
Unlike many other registration packages, this is not "greedy" descent based on an initial guessit attempts to find the globally-optimal alignment of your images.

To use this package, users must choose between using either the CPU or the GPU. For CPU processing, you must manually load the [RegisterMismatch package](https://github.com/HolyLab/RegisterMismatch.jl): `using RegisterMismatch, RegisterQD`. For GPU processing, you should instead load the [RegisterMismatchCuda package](https://github.com/HolyLab/RegisterMismatchCuda.jl): `using RegisterMismatchCuda, RegisterQD`. *Note that loading both mismatch packages in the same session will cause method conflicts.* Both mismatch packages are registered in the publicly-available [HolyLabRegistry](https://github.com/HolyLab/HolyLabRegistry), and users are advised to add that registry.
In the current absense of Github resources for GPU code, "gpu_test.jl" should be run on your personal machine as required.
## Installation

RegisterQD and its dependencies live in the [HolyLab registry](https://github.com/HolyLab/HolyLabRegistry).
Add the registry once, then install:

```julia
using Pkg
pkg"registry add https://github.com/HolyLab/HolyLabRegistry.git"
Pkg.add("RegisterQD")
```

You also need a mismatch backend. For CPU processing, load [RegisterMismatch](https://github.com/HolyLab/RegisterMismatch.jl):

```julia
Pkg.add("RegisterMismatch")
```

For GPU processing, use [RegisterMismatchCuda](https://github.com/HolyLab/RegisterMismatchCuda.jl) instead.
*Do not load both in the same session — they conflict.*

## Quick start

```julia
using RegisterMismatch, RegisterQD

fixed = Float64.(reshape(1:25, 5, 5))
moving = circshift(fixed, (2, 1)) # known shift: 2 rows, 1 column

tform, mm = qd_translate(fixed, moving, (3, 3))
# tform.translation == [2.0, 1.0]
# mm == 0.0
```

## Registration functions

This package exports the following registration functions:
- `qd_translate`: register images by shifting one with respect to another (translations only)
- `qd_rigid`: register images using rotations and translations
- `qd_affine`: register images using arbitrary affine transformations

In general, using more degrees of freedom allows you to solve harder optimization problems, but also makes it harder to find the global optimum. Your best strategy is to permit no more degrees of freedom than needed to solve the problem.
In general, using more degrees of freedom allows you to solve harder optimization problems, but also makes it harder to find the global optimum.
Use no more degrees of freedom than your problem requires.

See the help on these functions for details about how to call them.
## Anisotropic sampling

Another important feature of this package is that it supports images that were sampled anisotropically. This is particularly common for three-dimensional biomedical imaging, where MRI and optical microscopy typically have one axis sampled at lower resolution.
A rotation (from a rigid transformation) in physical space needs to be modified before applying it to an anisotropically-sampled image; see `arrayscale` and `getSD` for more information.
This package supports images sampled anisotropically, which is common in 3-D biomedical imaging (e.g. MRI, optical sections where the axial resolution differs from the in-plane resolution).
Pass `SD = diagm(voxelspacing)` to the registration functions to account for non-uniform spacing.
See [`arrayscale`](https://HolyLab.github.io/RegisterQD.jl/stable/api/#RegisterQD.arrayscale) and [`getSD`](https://HolyLab.github.io/RegisterQD.jl/stable/api/#RegisterQD.getSD) for details, and the [User Guide](https://HolyLab.github.io/RegisterQD.jl/stable/guide/) for a full explanation.

**NOTE**: see NEWS.md for information about a recent breaking change.
**NOTE**: see NEWS.md for information about recent breaking changes.
7 changes: 7 additions & 0 deletions docs/Project.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[deps]
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
RegisterQD = "ac24ea0c-1830-11e9-18d4-81f172323054"

[compat]
Documenter = "1"
julia = "1.10"
25 changes: 25 additions & 0 deletions docs/make.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using RegisterQD
using Documenter

makedocs(;
modules = [RegisterQD],
authors = "HolyLab",
sitename = "RegisterQD.jl",
format = Documenter.HTML(;
prettyurls = get(ENV, "CI", "false") == "true",
canonical = "https://HolyLab.github.io/RegisterQD.jl",
),
pages = [
"Home" => "index.md",
"User Guide" => "guide.md",
"API Reference" => "api.md",
],
checkdocs = :exports,
doctest = :none, # doctests are run in the test suite via doctest(RegisterQD; manual=false)
)

deploydocs(;
repo = "github.com/HolyLab/RegisterQD.jl",
devbranch = "master",
push_preview = true,
)
24 changes: 24 additions & 0 deletions docs/src/api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# API Reference

## Registration functions

```@docs
qd_translate
qd_rigid
qd_affine
```

## Rotation grid search

```@docs
grid_rotations
rotation_gridsearch
```

## Utilities

```@docs
arrayscale
getSD
qsmooth
```
140 changes: 140 additions & 0 deletions docs/src/guide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# User Guide

## How registration works

Image registration is the problem of finding a geometric transformation that
aligns one image (`moving`) to another (`fixed`). RegisterQD measures how well
two images are aligned using a *mismatch* value — roughly, the sum-of-squared
differences after normalising for intensity — and minimises it with the
[QuadDIRECT](https://github.com/timholy/QuadDIRECT.jl) global optimiser.

Because QuadDIRECT searches the entire parameter space rather than following a
local gradient, it can find the correct alignment even without a good starting
guess. The trade-off is that it is slower than greedy methods; providing a good
`initial_tfm` when one is available can speed things up significantly.

## Mismatch backend

The mismatch computation is implemented in a separate *backend* package that must
be loaded before calling any registration function:

| Use case | Package to load |
|----------|----------------|
| CPU | [RegisterMismatch](https://github.comlyHolyLab/RegisterMismatch.jl) |
| GPU (CUDA) | [RegisterMismatchCuda](https://github.com/HolyLab/RegisterMismatchCuda.jl) |

Load exactly one backend — loading both in the same session causes method
conflicts.

## Choosing a registration function

| Function | Degrees of freedom | When to use |
|---|---|---|
| [`qd_translate`](@ref) | N translations | images are related by a pure shift |
| [`qd_rigid`](@ref) | N translations + rotation(s) | images share the same shape but may be rotated |
| [`qd_affine`](@ref) | N translations + N² linear | images may have scaling or shear in addition to rotation |

Start with the fewest degrees of freedom that your problem requires.
More degrees of freedom make the global search harder and increase the risk of
finding a degenerate (low-overlap) solution.

## Anisotropic sampling and the `SD` parameter

Three-dimensional biomedical images (MRI, optical sections) are often sampled at
different resolutions along different axes — for example, 0.5 mm in-plane but
2 mm axially. A physical-space rotation of such a volume does not look like a
rotation in array-index space.

The `SD` ("spatial displacements") parameter encodes the physical spacing. Its
columns are the physical displacements corresponding to one array-element step
along each axis:

```julia
# 2-D example: dim 2 sampled 4× coarser than dim 1
SD = [1.0 0.0;
0.0 4.0]

tform, mm = qd_rigid(fixed, moving, mxshift, mxrot; SD)
```

If your image carries axis metadata (e.g. from
[AxisArrays.jl](https://github.com/JuliaArrays/AxisArrays.jl)), use
[`getSD`](@ref) to extract `SD` automatically:

```julia
SD = getSD(fixed)
tform, mm = qd_rigid(fixed, moving, mxshift, mxrot; SD)
```

The returned transformation is always in *physical* coordinates. Before warping
the array, convert it with [`arrayscale`](@ref):

```julia
itfm = arrayscale(tform, SD)
warped = warp(moving, itfm)
```

## Centre of rotation

Rotations are defined around the *origin of coordinates*, which for a plain
Julia array is index `(1,1,…)`. For most images, rotation around the image
centre is more natural. Call `centered` (from ImageTransformations) on both
images to shift the origin to the centre before registering:

```julia
using ImageTransformations: centered

cfixed = centered(fixed)
cmoving = centered(moving)

tform, mm = qd_rigid(collect(cfixed), collect(float(cmoving)), mxshift, mxrot)
```

Remember to call `centered` again when applying the result to a new image, or
re-encode the transformation with a different origin using
`recenter(tform, newctr)`.

## Initial guess

If you already have an approximate transformation, pass it as `initial_tfm` to
warm-start the search:

```julia
guess = Translation(1.0, 0.0)
tform, mm = qd_translate(fixed, moving, mxshift; initial_tfm=guess)
```

## Pre-smoothing with `qsmooth`

[`qsmooth`](@ref) smooths an image with a quadratic B-spline kernel and returns
an interpolant that can be warped cheaply. Pre-smoothing `fixed` and passing
`presmoothed=true` reduces the mismatch evaluation cost:

```julia
fixed_s = qsmooth(fixed)
tform, mm = qd_translate(fixed_s, moving, mxshift; presmoothed=true)
```

Do **not** smooth `moving`.

## Overlap threshold (`thresh`)

`thresh` prevents the optimiser from "aligning" images by sliding one entirely
out of view. The default is 10 % of the sum-of-squared intensity of `fixed`
for translations and rigid transforms, and 50 % for affine transforms (because
the extra degrees of freedom make degenerate solutions more tempting). Increase
`thresh` if you see unexpected results.

## Coarse rotation search

For large rotations where QuadDIRECT may not converge, use
[`rotation_gridsearch`](@ref) to identify a good starting rotation, then refine
with [`qd_rigid`](@ref):

```julia
best_rot, _ = rotation_gridsearch(fixed, moving, mxshift, maxradians, gridsz)
tform, mm = qd_rigid(fixed, moving, mxshift, mxrot; initial_tfm=best_rot)
```

[`grid_rotations`](@ref) generates the grid of candidate rotations used
internally by `rotation_gridsearch` and can also be used standalone.
Loading
Loading