Skip to content

Commit b8a545e

Browse files
icweavercgarling
andauthored
feat: align series of images (#54)
Also re-uses photometry if available to improve performance Closes: #52 Methods now include: ```julia align_frames(img_from, img_to; ...) align_frames(img_froms, img_to; ...) # Convenience function for aligning imgs[2:end] onto imgs[1] align_frames(imgs; ...) ``` `img`s can be image data or a photometry table --------- Co-authored-by: Chris Garling <chris.t.garling@gmail.com>
1 parent 2d80cf6 commit b8a545e

2 files changed

Lines changed: 87 additions & 2 deletions

File tree

src/warp.jl

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,41 @@ function align_frames(img_from, img_to; warp_function = warp, kwargs...)
3434
tfm, _ = find_transform(img_from, img_to; kwargs...)
3535

3636
# Step 6: Apply the transform (from => to)
37-
warp_img = apply_transform(tfm, img_from, img_to; warp_function = warp)
37+
warp_img = apply_transform(tfm, img_from, img_to; warp_function)
3838

3939
return warp_img
4040
end
4141

42+
"""
43+
align_frames(img_froms::AbstractVector{<:AbstractArray}, img_to; [warp_function], kwargs...)
44+
45+
Align each image in `img_froms` to the single reference image `img_to`.
46+
47+
The photometry on `img_to` is computed once (during the alignment of the first frame) and reused for every subsequent frame, which is substantially cheaper than invoking the scalar [`align_frames`](@ref) method once per `img_from`.
48+
49+
Returns a vector of warped images, one per element of `img_froms`. Accepts the same keyword arguments as the scalar method.
50+
"""
51+
function align_frames(img_froms::AbstractVector{<:AbstractArray}, img_to; warp_function = warp, kwargs...)
52+
# Bootstrap: solve the first frame normally so we can capture `phot_to`
53+
# and reuse it (via the precomputed-Table `get_phot` dispatch) on every
54+
# subsequent frame without redoing photometry on `img_to`.
55+
tfm_first, params_ref = find_transform(first(img_froms), img_to; kwargs...)
56+
phot_to = params_ref.phot_to
57+
58+
map(enumerate(img_froms)) do (i, img_from)
59+
tfm = i == 1 ? tfm_first : first(find_transform(img_from, phot_to; kwargs...))
60+
apply_transform(tfm, img_from, img_to; warp_function)
61+
end
62+
end
63+
64+
"""
65+
align_frames(imgs::AbstractVector{<:AbstractArray}; kwargs...)
66+
67+
Convenience method that takes the first image of `imgs` as the reference (`img_to`) and aligns the remaining `length(imgs) - 1` images to it. Equivalent to `align_frames(@view(imgs[2:end]), first(imgs); kwargs...)`.
68+
"""
69+
align_frames(imgs::AbstractVector{<:AbstractArray}; kwargs...) =
70+
align_frames(@view(imgs[2:end]), first(imgs); kwargs...)
71+
4272
"""
4373
apply_transform(tfm, img_from, img_to; warp_function = warp)
4474
@@ -99,7 +129,7 @@ end
99129
Compute the transformation needed to align `img_from` onto `img_to`, assuming both images are related via a rigid
100130
(or similarity, when `scale = true`) transformation. Automatically called by [`align_frames`](@ref).
101131
102-
If `img_from` or `img_to` is instead passed as a list of (x, y) coordinates for the given sources, then the photometry step will be skipped for that image.
132+
If `img_from` or `img_to` is instead passed as a vector of `(x, y)` coordinates for the given sources, then the photometry step will be skipped for that image. This allows use of precomputed coordinate lists. A previously computed photometry [`Table`](https://typedtables.juliadata.org/stable/man/reference/#TypedTables.Table) (e.g. extracted from a prior `find_transform` result via `params.phot_to`) can also be passed in place of either image, which is useful when aligning a series of frames to a single reference image without recomputing its photometry on every call.
103133
104134
# Parameters
105135
@@ -157,3 +187,4 @@ end
157187

158188
get_phot(img::AbstractMatrix; kwargs...) = _photometry(img; kwargs...)
159189
get_phot(coords::AbstractVector; kwargs...) = Table(; xcenter = first.(coords), ycenter = last.(coords)), ()
190+
get_phot(phot::Table; kwargs...) = phot, ()

test/test-functions.jl

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,36 @@ end
3838
@test img_aligned img_to
3939
end
4040

41+
@testset "align_frames (vector input reuses phot_to)" begin
42+
using Astroalign: align_frames
43+
44+
img_to = Data.img_to
45+
img_from = Data.img_from
46+
opts = (; box_size = 1, ap_radius = 1, min_fwhm = (0.1, 0.1))
47+
48+
expected = align_frames(img_from, img_to; opts...)
49+
aligned = align_frames([img_from, img_from, img_from], img_to; opts...)
50+
51+
@test length(aligned) == 3
52+
@test all(a -> a expected, aligned)
53+
@test all(a -> a img_to, aligned)
54+
end
55+
56+
@testset "align_frames (single-arg convenience: first is reference)" begin
57+
using Astroalign: align_frames
58+
59+
img_to = Data.img_to
60+
img_from = Data.img_from
61+
opts = (; box_size = 1, ap_radius = 1, min_fwhm = (0.1, 0.1))
62+
63+
expected = align_frames([img_from, img_from], img_to; opts...)
64+
aligned = align_frames([img_to, img_from, img_from]; opts...)
65+
66+
@test length(aligned) == 2
67+
@test aligned[1] expected[1]
68+
@test aligned[2] expected[2]
69+
end
70+
4171
@testset "find_transform" begin
4272
using Astroalign: find_transform
4373

@@ -55,6 +85,30 @@ end
5585
@test tfm.translation [-3.0, 0.0]
5686
end
5787

88+
@testset "find_transform reuses precomputed phot" begin
89+
using Astroalign: find_transform
90+
91+
img_to = Data.img_to
92+
img_from = Data.img_from
93+
opts = (; box_size = 1, ap_radius = 1, min_fwhm = (0.1, 0.1))
94+
95+
tfm_ref, params_ref = find_transform(img_from, img_to; opts...)
96+
97+
# Passing the precomputed phot_to Table in place of img_to must yield the
98+
# same transform without rerunning photometry on img_to.
99+
tfm_reuse, params_reuse = find_transform(img_from, params_ref.phot_to; opts...)
100+
@test tfm_reuse.linear tfm_ref.linear
101+
@test tfm_reuse.translation tfm_ref.translation
102+
@test params_reuse.phot_to === params_ref.phot_to
103+
@test params_reuse.phot_to_params == ()
104+
105+
# Symmetric: precomputed phot_from also works.
106+
tfm_both, params_both = find_transform(params_ref.phot_from, params_ref.phot_to; opts...)
107+
@test tfm_both.linear tfm_ref.linear
108+
@test tfm_both.translation tfm_ref.translation
109+
@test params_both.phot_from_params == ()
110+
end
111+
58112
@testset "photometry" begin
59113
using Astroalign: _photometry, PSF
60114

0 commit comments

Comments
 (0)