Skip to content

Commit a83fb4f

Browse files
committed
implement FiniteDiffs 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.FiniteDiffs.fdiff` * `ImageBase.fdiff!` => `ImageBase.FiniteDiffs.fdiff!`
1 parent bd3db5b commit a83fb4f

File tree

7 files changed

+198
-15
lines changed

7 files changed

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

src/diff.jl

Lines changed: 134 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 `FiniteDiffs.jl` package with quite different purposes,
2+
# this module is not expected to be reexported.
3+
module FiniteDiffs
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+
FiniteDiffs
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.FiniteDiffs: 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,105 @@ _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(first(V₁), maybe_floattype(eltype(V₁))), V₁, Vs...)
149+
end
150+
151+
"""
152+
fdiv!(out, Vs...)
153+
154+
The in-place version of divergence operator [`fdiv`](@ref).
155+
"""
156+
function fdiv!(out::AbstractArray, V₁::AbstractArray, Vs::AbstractArray...)
157+
# This is the optimized version of `sum(v->fgradient(v; ajoint=true), (V₁, Vs...))`
158+
# by removing unnecessary memory allocations.
159+
all(v->axes(v) == axes(out), (V₁, Vs...)) || throw(ArgumentError("All axes of vector fields Vs and X should be the same."))
160+
161+
# TODO(johnnychen94): for better performance, we can eliminate this `tmp` allocation by fusing multiple `fdiff` in the inner loop.
162+
out .= fdiff(V₁; dims=1, rev=true)
163+
tmp = similar(out)
164+
for i in 1:length(Vs)
165+
out .+= fdiff!(tmp, Vs[i]; dims=i+1, rev=true)
166+
end
167+
return out
168+
end
169+
170+
"""
171+
flaplacian(X::AbstractArray)
172+
173+
The discrete laplacian operator, i.e., the divergence of the gradient fields of `X`.
174+
175+
See also [`flaplacian!`](@ref) for the in-place version.
176+
"""
177+
flaplacian(X::AbstractArray) = flaplacian!(similar(X, maybe_floattype(eltype(X))), X)
178+
179+
"""
180+
flaplacian!(out, X)
181+
flaplacian!(out, ∇X::Tuple, X)
182+
183+
The in-place version of the laplacian operator [`flaplacian`](@ref).
184+
185+
!!! tips Non-allocating
186+
This function will allocate a new set of memories to store the intermediate
187+
gradient fields `∇X`, if you pre-allcoate the memory for `∇X`, then this function
188+
will use it and is thus non-allcating.
189+
"""
190+
flaplacian!(out, X::AbstractArray) = fdiv!(out, fgradient(X)...)
191+
flaplacian!(out, ∇X::Tuple, X::AbstractArray) = fdiv!(out, fgradient!(∇X, X)...)
192+
193+
194+
"""
195+
fgradient(X::AbstractArray; adjoint=false) -> (∂₁X, ∂₂X, ..., ∂ₙX)
196+
197+
Computes the gradient fields of `X`. If `adjoint==true` then it computes the adjoint gradient
198+
fields.
199+
200+
Each gradient vector is computed as forward difference along specific dimension, e.g.,
201+
[`∂ᵢX = fdiff(X, dims=i)`](@ref fdiff).
202+
203+
Mathematically, the adjoint operator ∂ᵢ' of ∂ᵢ is defined as `<∂ᵢu, v> := <u, ∂ᵢ'v>`.
204+
205+
See also the in-place version [`fgradient!(X)`](@ref) to reuse the allocated memory.
206+
"""
207+
function fgradient(X::AbstractArray{T,N}; adjoint=false) where {T,N}
208+
fgradient!(ntuple(i->similar(X, maybe_floattype(T)), N), X; adjoint=adjoint)
209+
end
210+
211+
"""
212+
fgradient!(∇X::Tuple, X::AbstractArray; adjoint=false)
213+
214+
The in-place version of (adjoint) gradient operator [`fgradient`](@ref).
215+
216+
The input `∇X = (∂₁X, ∂₂X, ..., ∂ₙX)` is a tuple of arrays that are similar to `X`, i.e.,
217+
`eltype(∂ᵢX) == eltype(X)` and `axes(∂ᵢX) == axes(X)` for all `i`.
218+
"""
219+
function fgradient!(∇X::NTuple{N, <:AbstractArray}, X; adjoint=false) where N
220+
all(v->axes(v) == axes(X), ∇X) || throw(ArgumentError("All axes of vector fields ∇X and X should be the same."))
221+
for i in 1:N
222+
if adjoint
223+
# the negative adjoint of gradient operator for forward difference is the backward difference
224+
fdiff!(∇X[i], X, dims=i, rev=true)
225+
@. ∇X[i] = -∇X[i]
226+
else
227+
fdiff!(∇X[i], X, dims=i)
228+
end
229+
end
230+
return ∇X
231+
end
232+
233+
234+
235+
if VERSION < v"1.1"
236+
isnothing(x) = x === nothing
237+
end
238+
239+
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.FiniteDiffs.fdiff(A)
21+
out = similar(A)
22+
@test ImageBase.fdiff!(out, A) == ImageBase.FiniteDiffs.fdiff!(out, A)
23+
end
1724
end

test/diff.jl

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1+
using ImageBase.FiniteDiffs
2+
# TODO(johnnychen94): remove this after we delete `Imagebase.fdiff` and `ImageBase.fdiff!` entrypoints
3+
using ImageBase.FiniteDiffs: 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.FiniteDiffs.maybe_floattype(Int) == Int
8+
@test ImageBase.FiniteDiffs.maybe_floattype(N0f8) == Float32
9+
@test ImageBase.FiniteDiffs.maybe_floattype(RGB{N0f8}) == RGB{Float32}
610

711
@testset "API" begin
812
# fdiff! works the same as fdiff
@@ -107,3 +111,44 @@
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+
for T in generate_test_types([N0f8, Float32], [Gray, RGB])
140+
for sz in [(7,), (7, 7), (7, 7, 7)]
141+
A = rand(T, sz...)
142+
out = flaplacian(A)
143+
@test out ref_laplacian(A)
144+
145+
∇A = fgradient(A)
146+
foreach((out, ∇A...)) do x
147+
fill!(x, zero(T))
148+
end
149+
flaplacian!(out, ∇A, A)
150+
@test out == flaplacian(A)
151+
@test ∇A == fgradient(A)
152+
end
153+
end
154+
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)