Skip to content

Commit da42ffc

Browse files
committed
implement FiniteDiff submodule with new operators
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!`
1 parent bd3db5b commit da42ffc

File tree

7 files changed

+232
-15
lines changed

7 files changed

+232
-15
lines changed

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 & 6 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,7 +21,6 @@ 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")

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 ImageBase.FiniteDiff.fdiff
14+
@deprecate fdiff! ImageBase.FiniteDiff.fdiff!
15+
1116
# END 0.1 deprecation

src/diff.jl

Lines changed: 136 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,34 @@
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 (or C if it is complex value). This module provides the discrete
11+
version of gradient-related operators by viewing image arrays as functions.
12+
13+
This module provides:
14+
15+
- forward/backward difference [`fdiff`](@ref) are the Images-flavor of `Base.diff`
16+
- gradient operator [`fgradient`](@ref) and its adjoint via keyword `adjoint=true`.
17+
- divergence operator [`fdiv`](@ref) is the negative sum of the adjoint gradient operator of
18+
given vector fields.
19+
- laplacian operator [`flaplacian`](@ref) is the divergence of the gradient fields.
20+
21+
Every function in this module has its in-place version.
22+
"""
23+
FiniteDiff
24+
25+
export
26+
fdiff, fdiff!,
27+
fdiv, fdiv!,
28+
fgradient, fgradient!,
29+
flaplacian, flaplacian!
30+
31+
332
"""
433
fdiff(A::AbstractArray; dims::Int, rev=false, boundary=:periodic)
534
@@ -19,7 +48,7 @@ Take vector as an example, it computes `(A[2]-A[1], A[3]-A[2], ..., A[1]-A[end])
1948
2049
# Examples
2150
22-
```jldoctest; setup=:(using ImageBase: fdiff)
51+
```jldoctest; setup=:(using ImageBase.FiniteDiff: fdiff)
2352
julia> A = [2 4 8; 3 9 27; 4 16 64]
2453
3×3 $(Matrix{Int}):
2554
2 4 8
@@ -106,3 +135,107 @@ _fdiff_default_dims(A::AbstractVector) = 1
106135
maybe_floattype(::Type{T}) where T = T
107136
maybe_floattype(::Type{T}) where T<:FixedPoint = floattype(T)
108137
maybe_floattype(::Type{CT}) where CT<:Color = base_color_type(CT){maybe_floattype(eltype(CT))}
138+
139+
140+
"""
141+
fdiv(Vs...)
142+
143+
Compute the divergence of vector fields `Vs`.
144+
145+
See also [`fdiv!`](@ref) for the in-place version.
146+
"""
147+
function fdiv(V₁::AbstractArray{T}, Vs::AbstractArray{T}...) where T
148+
fdiv!(similar(V₁, maybe_floattype(T)), V₁, Vs...)
149+
end
150+
fdiv(Vs::Tuple) = fdiv(Vs...)
151+
152+
"""
153+
fdiv!(out, Vs...)
154+
155+
The in-place version of divergence operator [`fdiv`](@ref).
156+
"""
157+
function fdiv!(out::AbstractArray, V₁::AbstractArray, Vs::AbstractArray...)
158+
# This is the optimized version of `sum(v->fgradient(v; ajoint=true), (V₁, Vs...))`
159+
# by removing unnecessary memory allocations.
160+
all(v->axes(v) == axes(out), (V₁, Vs...)) || throw(ArgumentError("All axes of vector fields Vs and X should be the same."))
161+
162+
# TODO(johnnychen94): for better performance, we can eliminate this `tmp` allocation by fusing multiple `fdiff` in the inner loop.
163+
out .= fdiff(V₁; dims=1, rev=true, boundary=:periodic)
164+
tmp = similar(out)
165+
for i in 1:length(Vs)
166+
out .+= fdiff!(tmp, Vs[i]; dims=i+1, rev=true, boundary=:periodic)
167+
end
168+
return out
169+
end
170+
fdiv!(out::AbstractArray, Vs::Tuple) = fdiv!(out, Vs...)
171+
172+
"""
173+
flaplacian(X::AbstractArray)
174+
175+
The discrete laplacian operator, i.e., the divergence of the gradient fields of `X`.
176+
177+
See also [`flaplacian!`](@ref) for the in-place version.
178+
"""
179+
flaplacian(X::AbstractArray) = flaplacian!(similar(X, maybe_floattype(eltype(X))), X)
180+
181+
"""
182+
flaplacian!(out, X)
183+
flaplacian!(out, ∇X::Tuple, X)
184+
185+
The in-place version of the laplacian operator [`flaplacian`](@ref).
186+
187+
!!! tips Non-allocating
188+
This function will allocate a new set of memories to store the intermediate
189+
gradient fields `∇X`, if you pre-allcoate the memory for `∇X`, then this function
190+
will use it and is thus non-allcating.
191+
"""
192+
flaplacian!(out, X::AbstractArray) = fdiv!(out, fgradient(X))
193+
flaplacian!(out, ∇X::Tuple, X::AbstractArray) = fdiv!(out, fgradient!(∇X, X))
194+
195+
196+
"""
197+
fgradient(X::AbstractArray; adjoint=false) -> (∂₁X, ∂₂X, ..., ∂ₙX)
198+
199+
Computes the gradient fields of `X`. If `adjoint==true` then it computes the adjoint gradient
200+
fields.
201+
202+
Each gradient vector is computed as forward difference along specific dimension, e.g.,
203+
[`∂ᵢX = fdiff(X, dims=i)`](@ref fdiff).
204+
205+
Mathematically, the adjoint operator ∂ᵢ' of ∂ᵢ is defined as `<∂ᵢu, v> := <u, ∂ᵢ'v>`.
206+
207+
See also the in-place version [`fgradient!(X)`](@ref) to reuse the allocated memory.
208+
"""
209+
function fgradient(X::AbstractArray{T,N}; adjoint=false) where {T,N}
210+
fgradient!(ntuple(i->similar(X, maybe_floattype(T)), N), X; adjoint=adjoint)
211+
end
212+
213+
"""
214+
fgradient!(∇X::Tuple, X::AbstractArray; adjoint=false)
215+
216+
The in-place version of (adjoint) gradient operator [`fgradient`](@ref).
217+
218+
The input `∇X = (∂₁X, ∂₂X, ..., ∂ₙX)` is a tuple of arrays that are similar to `X`, i.e.,
219+
`eltype(∂ᵢX) == eltype(X)` and `axes(∂ᵢX) == axes(X)` for all `i`.
220+
"""
221+
function fgradient!(∇X::NTuple{N, <:AbstractArray}, X; adjoint=false) where N
222+
all(v->axes(v) == axes(X), ∇X) || throw(ArgumentError("All axes of vector fields ∇X and X should be the same."))
223+
for i in 1:N
224+
if adjoint
225+
# the negative adjoint of gradient operator for forward difference is the backward difference
226+
fdiff!(∇X[i], X, dims=i, rev=true)
227+
@. ∇X[i] = -∇X[i]
228+
else
229+
fdiff!(∇X[i], X, dims=i)
230+
end
231+
end
232+
return ∇X
233+
end
234+
235+
236+
237+
if VERSION < v"1.1"
238+
isnothing(x) = x === nothing
239+
end
240+
241+
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) == ImageBase.FiniteDiff.fdiff(A)
21+
out = similar(A)
22+
@test ImageBase.fdiff!(out, A) == ImageBase.FiniteDiff.fdiff!(out, A)
23+
end
1724
end

test/diff.jl

Lines changed: 80 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,76 @@
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+
@test eltype(ΔX) == Int
156+
@test ΔX_ref == ΔX
157+
158+
for T in generate_test_types([N0f8, Float32], [Gray, RGB])
159+
for sz in [(7,), (7, 7), (7, 7, 7)]
160+
A = rand(T, sz...)
161+
∇A = fgradient(A)
162+
out = fdiv(∇A)
163+
@test out ref_laplacian(A)
164+
165+
fill!(out, zero(T))
166+
fdiv!(out, ∇A...)
167+
@test out == fdiv(∇A)
168+
end
169+
end
170+
171+
for T in generate_test_types([N0f8, Float32], [Gray, RGB])
172+
for sz in [(7,), (7, 7), (7, 7, 7)]
173+
A = rand(T, sz...)
174+
out = flaplacian(A)
175+
@test out ref_laplacian(A)
176+
177+
∇A = fgradient(A)
178+
foreach((out, ∇A...)) do x
179+
fill!(x, zero(T))
180+
end
181+
flaplacian!(out, ∇A, A)
182+
@test out == flaplacian(A)
183+
@test ∇A == fgradient(A)
184+
end
185+
end
186+
end

test/runtests.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
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
1010
# Not checking compat section for test-only dependencies
1111
Aqua.test_ambiguities(ImageBase)
@@ -17,7 +17,7 @@ include("testutils.jl")
1717
project_toml_formatting=true
1818
)
1919
if VERSION >= v"1.2"
20-
doctest(ImageBase,manual = false)
20+
doctest(ImageBase, manual = false)
2121
end
2222
end
2323

0 commit comments

Comments
 (0)