Skip to content

Commit 5687005

Browse files
implement FiniteDiffs submodule: gradient, divergence and laplacian operators (#22)
New operators: - gradient and adjoint gradient: `fgradient` - divergence: `fdiv` - laplacian: `flaplacian` To better organize the symbols, I deprecate two entrypoints: * `ImageBase.fdiff` => `ImageBase.FiniteDiff.fdiff` * `ImageBase.fdiff!` => `ImageBase.FiniteDiff.fdiff!` * disable meta quality test in old Julia versions Maintaining old compatibility becomes quite troublesome especially when we have 1.6 as the new LTS version now. * CI compatibility fix for Julia < 1.3 Co-authored-by: Tim Holy <[email protected]>
1 parent bd3db5b commit 5687005

File tree

9 files changed

+257
-35
lines changed

9 files changed

+257
-35
lines changed

.github/workflows/UnitTest.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@ jobs:
4848
${{ runner.os }}-test-
4949
${{ runner.os }}-
5050
- uses: julia-actions/julia-buildpkg@v1
51+
- name: "Compat fix for Julia < v1.3.0"
52+
if: ${{ matrix.version == '1.0' }}
53+
run: |
54+
using Pkg
55+
Pkg.add([
56+
PackageSpec(name="AbstractFFTs", version="0.5"),
57+
])
58+
shell: julia --project=. --startup=no --color=yes {0}
5159
- uses: julia-actions/julia-runtest@v1
5260
- uses: julia-actions/julia-processcoverage@v1
5361
- uses: codecov/codecov-action@v1

Project.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ julia = "1"
1414
[extras]
1515
Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595"
1616
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
17+
ImageFiltering = "6a3955dd-da59-5b1f-98d4-e7296123deb5"
1718
ImageIO = "82e4d734-157c-48bb-816b-45c225c6df19"
1819
ImageMagick = "6218d12a-5da1-5696-b52f-db25d2ecc6d1"
1920
OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881"
@@ -23,4 +24,4 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
2324
TestImages = "5e47fb64-e119-507b-a336-dd2b206d9990"
2425

2526
[targets]
26-
test = ["Aqua", "Documenter", "Test", "ImageIO", "ImageMagick", "OffsetArrays", "Statistics", "StackViews", "TestImages"]
27+
test = ["Aqua", "Documenter", "Test", "ImageFiltering", "ImageIO", "ImageMagick", "OffsetArrays", "Statistics", "StackViews", "TestImages"]

src/ImageBase.jl

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,6 @@ export
55
# originally from ImageTransformations.jl
66
restrict,
77

8-
# finite difference on one-dimension
9-
# originally from Images.jl
10-
fdiff,
11-
fdiff!,
12-
138
# basic image statistics, from Images.jl
149
minimum_finite,
1510
maximum_finite,
@@ -26,13 +21,11 @@ using Reexport
2621
using Base.Cartesian: @nloops
2722
@reexport using ImageCore
2823
using ImageCore.OffsetArrays
29-
using ImageCore.MappedArrays: of_eltype
3024

3125
include("diff.jl")
3226
include("restrict.jl")
3327
include("utils.jl")
3428
include("statistics.jl")
35-
include("compat.jl")
3629
include("deprecated.jl")
3730

3831
if VERSION >= v"1.4.2" # work around https://github.com/JuliaLang/julia/issues/34121

src/compat.jl

Lines changed: 0 additions & 9 deletions
This file was deleted.

src/deprecated.jl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,9 @@
88
@deprecate maxfinite(A; kwargs...) maximum_finite(A; kwargs...)
99
@deprecate maxabsfinite(A; kwargs...) maximum_finite(abs, A; kwargs...)
1010

11+
# These two symbols are exported by previous ImageBase versions and now organized in the
12+
# FiniteDiff submodule.
13+
@deprecate fdiff(args...; kwargs...) ImageBase.FiniteDiff.fdiff(args...; kwargs...)
14+
@deprecate fdiff!(args...; kwargs...) ImageBase.FiniteDiff.fdiff!(args...; kwargs...)
15+
1116
# END 0.1 deprecation

src/diff.jl

Lines changed: 142 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,36 @@
1-
# TODO: add keyword `shrink` to give a consistant result on Base
2-
# when this is done, then we can propose this change to upstream Base
1+
# Because there exists the `FiniteDiff.jl` package with quite different purposes,
2+
# this module is not expected to be reexported.
3+
module FiniteDiff
4+
5+
using ImageCore
6+
using ImageCore.MappedArrays: of_eltype
7+
8+
"""
9+
Although stored as an array, image can also be viewed as a function from discrete grid space
10+
Zᴺ to continuous space R if it is gray image, to C if it is complex-valued image
11+
(MRI rawdata), to Rᴺ if it is colorant image, etc.
12+
This module provides the discrete
13+
version of gradient-related operators by viewing image arrays as functions.
14+
15+
This module provides:
16+
17+
- forward/backward difference [`fdiff`](@ref) are the Images-flavor of `Base.diff`
18+
- gradient operator [`fgradient`](@ref) and its adjoint via keyword `adjoint=true`.
19+
- divergence operator [`fdiv`](@ref) computes the sum of discrete derivatives of vector
20+
fields.
21+
- laplacian operator [`flaplacian`](@ref) is the divergence of the gradient fields.
22+
23+
Every function in this module has its in-place version.
24+
"""
25+
FiniteDiff
26+
27+
export
28+
fdiff, fdiff!,
29+
fdiv, fdiv!,
30+
fgradient, fgradient!,
31+
flaplacian, flaplacian!
32+
33+
334
"""
435
fdiff(A::AbstractArray; dims::Int, rev=false, boundary=:periodic)
536
@@ -19,7 +50,7 @@ Take vector as an example, it computes `(A[2]-A[1], A[3]-A[2], ..., A[1]-A[end])
1950
2051
# Examples
2152
22-
```jldoctest; setup=:(using ImageBase: fdiff)
53+
```jldoctest; setup=:(using ImageBase.FiniteDiff: fdiff)
2354
julia> A = [2 4 8; 3 9 27; 4 16 64]
2455
3×3 $(Matrix{Int}):
2556
2 4 8
@@ -106,3 +137,111 @@ _fdiff_default_dims(A::AbstractVector) = 1
106137
maybe_floattype(::Type{T}) where T = T
107138
maybe_floattype(::Type{T}) where T<:FixedPoint = floattype(T)
108139
maybe_floattype(::Type{CT}) where CT<:Color = base_color_type(CT){maybe_floattype(eltype(CT))}
140+
141+
142+
"""
143+
fdiv(Vs...)
144+
145+
Compute the divergence of vector fields `Vs`.
146+
147+
See also [`fdiv!`](@ref) for the in-place version.
148+
"""
149+
function fdiv(V₁::AbstractArray{T}, Vs::AbstractArray{T}...) where T
150+
fdiv!(similar(V₁, maybe_floattype(T)), V₁, Vs...)
151+
end
152+
fdiv(Vs::Tuple) = fdiv(Vs...)
153+
154+
"""
155+
fdiv!(out, Vs...)
156+
157+
The in-place version of divergence operator [`fdiv`](@ref).
158+
"""
159+
function fdiv!(out::AbstractArray, V₁::AbstractArray, Vs::AbstractArray...)
160+
# This is the optimized version of `sum(v->fgradient(v; ajoint=true), (V₁, Vs...))`
161+
# by removing unnecessary memory allocations.
162+
all(v->axes(v) == axes(out), (V₁, Vs...)) || throw(ArgumentError("All axes of vector fields Vs and X should be the same."))
163+
164+
# TODO(johnnychen94): for better performance, we can eliminate this `tmp` allocation by fusing multiple `fdiff` in the inner loop.
165+
out .= fdiff(V₁; dims=1, rev=true, boundary=:periodic)
166+
tmp = similar(out)
167+
for i in 1:length(Vs)
168+
out .+= fdiff!(tmp, Vs[i]; dims=i+1, rev=true, boundary=:periodic)
169+
end
170+
return out
171+
end
172+
fdiv!(out::AbstractArray, Vs::Tuple) = fdiv!(out, Vs...)
173+
174+
"""
175+
flaplacian(X::AbstractArray)
176+
177+
The discrete laplacian operator, i.e., the divergence of the gradient fields of `X`.
178+
179+
See also [`flaplacian!`](@ref) for the in-place version.
180+
"""
181+
flaplacian(X::AbstractArray) = flaplacian!(similar(X, maybe_floattype(eltype(X))), X)
182+
183+
"""
184+
flaplacian!(out, X)
185+
flaplacian!(out, ∇X::Tuple, X)
186+
187+
The in-place version of the laplacian operator [`flaplacian`](@ref).
188+
189+
!!! tip Avoiding allocations
190+
The two-argument method will allocate memory to store the intermediate
191+
gradient fields `∇X`. If you call this repeatedly with images of consistent size and type,
192+
consider using the three-argument form with pre-allocated memory for `∇X`,
193+
which will eliminate allocation by this function.
194+
"""
195+
flaplacian!(out, X::AbstractArray) = fdiv!(out, fgradient(X))
196+
flaplacian!(out, ∇X::Tuple, X::AbstractArray) = fdiv!(out, fgradient!(∇X, X))
197+
198+
199+
"""
200+
fgradient(X::AbstractArray; adjoint=false) -> (∂₁X, ∂₂X, ..., ∂ₙX)
201+
202+
Computes the gradient fields of `X`. If `adjoint==true` then it computes the adjoint gradient
203+
fields.
204+
205+
Each gradient vector is computed as forward difference along specific dimension, e.g.,
206+
[`∂ᵢX = fdiff(X, dims=i)`](@ref fdiff).
207+
208+
Mathematically, the adjoint operator ∂ᵢ' of ∂ᵢ is defined as `<∂ᵢu, v> := <u, ∂ᵢ'v>`.
209+
210+
See also the in-place version [`fgradient!(X)`](@ref) to reuse the allocated memory.
211+
"""
212+
function fgradient(X::AbstractArray{T,N}; adjoint::Bool=false) where {T,N}
213+
fgradient!(ntuple(i->similar(X, maybe_floattype(T)), N), X; adjoint=adjoint)
214+
end
215+
216+
"""
217+
fgradient!(∇X::Tuple, X::AbstractArray; adjoint=false)
218+
219+
The in-place version of (adjoint) gradient operator [`fgradient`](@ref).
220+
221+
The input `∇X = (∂₁X, ∂₂X, ..., ∂ₙX)` is a tuple of arrays that are similar to `X`, i.e.,
222+
`eltype(∂ᵢX) == eltype(X)` and `axes(∂ᵢX) == axes(X)` for all `i`.
223+
"""
224+
function fgradient!(∇X::NTuple{N, <:AbstractArray}, X; adjoint::Bool=false) where N
225+
all(v->axes(v) == axes(X), ∇X) || throw(ArgumentError("All axes of vector fields ∇X and X should be the same."))
226+
for i in 1:N
227+
if adjoint
228+
# the negative adjoint of gradient operator for forward difference is the backward difference
229+
# see also
230+
# Getreuer, Pascal. "Rudin-Osher-Fatemi total variation denoising using split Bregman." _Image Processing On Line_ 2 (2012): 74-95.
231+
fdiff!(∇X[i], X, dims=i, rev=true)
232+
# TODO(johnnychen94): ideally we can get avoid flipping the signs for better performance.
233+
@. ∇X[i] = -∇X[i]
234+
else
235+
fdiff!(∇X[i], X, dims=i)
236+
end
237+
end
238+
return ∇X
239+
end
240+
241+
242+
243+
if VERSION < v"1.1"
244+
isnothing(x) = x === nothing
245+
end
246+
247+
end # module

test/deprecated.jl

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,11 @@
1414
@test maxabsfinite(A) == maximum_finite(abs, A)
1515
@test maxabsfinite(A, dims=1) == maximum_finite(abs, A, dims=1)
1616
end
17+
18+
@testset "fdiff entrypoints" begin
19+
A = rand(Float32, 5)
20+
@test ImageBase.fdiff(A, rev=true) == ImageBase.FiniteDiff.fdiff(A, rev=true)
21+
out = similar(A)
22+
@test ImageBase.fdiff!(out, A, rev=false) == ImageBase.FiniteDiff.fdiff!(out, A, rev=false)
23+
end
1724
end

test/diff.jl

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1+
using ImageBase.FiniteDiff
2+
# TODO(johnnychen94): remove this after we delete `Imagebase.fdiff` and `ImageBase.fdiff!` entrypoints
3+
using ImageBase.FiniteDiff: fdiff, fdiff!
4+
15
@testset "fdiff" begin
26
# Base.diff doesn't promote integer to float
3-
@test ImageBase.maybe_floattype(Int) == Int
4-
@test ImageBase.maybe_floattype(N0f8) == Float32
5-
@test ImageBase.maybe_floattype(RGB{N0f8}) == RGB{Float32}
7+
@test ImageBase.FiniteDiff.maybe_floattype(Int) == Int
8+
@test ImageBase.FiniteDiff.maybe_floattype(N0f8) == Float32
9+
@test ImageBase.FiniteDiff.maybe_floattype(RGB{N0f8}) == RGB{Float32}
610

711
@testset "API" begin
812
# fdiff! works the same as fdiff
@@ -107,3 +111,77 @@
107111
@test fdiff(A, dims=1) == fdiff(float.(A), dims=1)
108112
end
109113
end
114+
115+
@testset "fgradient" begin
116+
for T in generate_test_types([N0f8, Float32], [Gray, RGB])
117+
for sz in [(7, ), (7, 5), (7, 5, 3)]
118+
A = rand(T, sz...)
119+
120+
∇A = fgradient(A)
121+
for i in 1:length(sz)
122+
@test ∇A[i] == fdiff(A, dims=i)
123+
end
124+
∇A = map(similar, ∇A)
125+
@test fgradient!(∇A, A) == fgradient(A)
126+
127+
∇tA = fgradient(A, adjoint=true)
128+
for i in 1:length(sz)
129+
@test ∇tA[i] == .-fdiff(A, dims=i, rev=true)
130+
end
131+
∇tA = map(similar, ∇tA)
132+
@test fgradient!(∇tA, A, adjoint=true) == fgradient(A, adjoint=true)
133+
end
134+
end
135+
end
136+
137+
@testset "fdiv/flaplacian" begin
138+
ref_laplacian(X) = imfilter(X, Kernel.Laplacian(ntuple(x->true, ndims(X))), "circular")
139+
140+
X = [
141+
5 3 8 1 2 2 3
142+
5 5 1 3 3 1 1
143+
1 8 2 9 1 2 7
144+
7 3 4 5 8 1 5
145+
1 4 1 8 8 9 7
146+
]
147+
ΔX_ref = [
148+
-8 10 -26 17 6 7 3
149+
-8 -3 14 2 -5 4 12
150+
23 -21 14 -25 18 2 -19
151+
-18 11 -5 9 -17 20 2
152+
19 -8 20 -17 -5 -18 -10
153+
]
154+
ΔX = ref_laplacian(X)
155+
# Base.diff doesn't promote Int to floats so we should probably do the same for laplacian
156+
@test eltype(ΔX) == Int
157+
@test ΔX_ref == ΔX
158+
159+
for T in generate_test_types([N0f8, Float32], [Gray, RGB])
160+
for sz in [(7,), (7, 7), (7, 7, 7)]
161+
A = rand(T, sz...)
162+
∇A = fgradient(A)
163+
out = fdiv(∇A)
164+
@test out ref_laplacian(A)
165+
166+
fill!(out, zero(T))
167+
fdiv!(out, ∇A...)
168+
@test out == fdiv(∇A)
169+
end
170+
end
171+
172+
for T in generate_test_types([N0f8, Float32], [Gray, RGB])
173+
for sz in [(7,), (7, 7), (7, 7, 7)]
174+
A = rand(T, sz...)
175+
out = flaplacian(A)
176+
@test out ref_laplacian(A)
177+
178+
∇A = fgradient(A)
179+
foreach((out, ∇A...)) do x
180+
fill!(x, zero(T))
181+
end
182+
flaplacian!(out, ∇A, A)
183+
@test out == flaplacian(A)
184+
@test ∇A == fgradient(A)
185+
end
186+
end
187+
end

test/runtests.jl

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
11
using ImageBase, OffsetArrays, StackViews
2+
using ImageFiltering
23
using Test, TestImages, Aqua, Documenter
34

45
using OffsetArrays: IdentityUnitRange
56
include("testutils.jl")
67

78
@testset "ImageBase.jl" begin
8-
99
@testset "Project meta quality checks" begin
10-
# Not checking compat section for test-only dependencies
11-
Aqua.test_ambiguities(ImageBase)
12-
Aqua.test_all(ImageBase;
13-
ambiguities=false,
14-
project_extras=true,
15-
deps_compat=true,
16-
stale_deps=true,
17-
project_toml_formatting=true
18-
)
19-
if VERSION >= v"1.2"
20-
doctest(ImageBase,manual = false)
10+
if VERSION >= v"1.3"
11+
# Not checking compat section for test-only dependencies
12+
Aqua.test_ambiguities(ImageBase)
13+
Aqua.test_all(ImageBase;
14+
ambiguities=false,
15+
project_extras=true,
16+
deps_compat=true,
17+
stale_deps=true,
18+
project_toml_formatting=true
19+
)
20+
doctest(ImageBase, manual = false)
2121
end
2222
end
2323

0 commit comments

Comments
 (0)