Skip to content

Commit 90701de

Browse files
Add finite difference: fdiff and fdiff! (#11)
Co-authored-by: JKay <[email protected]>
1 parent fd17e8e commit 90701de

File tree

7 files changed

+251
-4
lines changed

7 files changed

+251
-4
lines changed

Project.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,14 @@ Reexport = "0.2, 1"
1212
julia = "1"
1313

1414
[extras]
15+
Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595"
16+
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
17+
ImageIO = "82e4d734-157c-48bb-816b-45c225c6df19"
1518
ImageMagick = "6218d12a-5da1-5696-b52f-db25d2ecc6d1"
1619
OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881"
20+
StackViews = "cae243ae-269e-4f55-b966-ac2d0dc13c15"
1721
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
1822
TestImages = "5e47fb64-e119-507b-a336-dd2b206d9990"
1923

2024
[targets]
21-
test = ["Test", "ImageMagick", "OffsetArrays", "TestImages"]
25+
test = ["Aqua", "Documenter", "Test", "ImageIO", "ImageMagick", "OffsetArrays", "StackViews", "TestImages"]

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,10 @@ This package can be seen as an experimental package inside JuliaImages:
1414
2. is very conservative to external dependencies outside JuliaImages unless there is a real need, in which case,
1515
it may just fit the first case.
1616

17+
Functions provided by this package:
18+
19+
- `restrict` for two-fold image downsample. (Originally from ImageTransformations.jl)
20+
- finite difference operator `fdiff`/`fdiff!` (Originally from Images.jl)
21+
22+
1723
[ImageCore]: https://github.com/JuliaImages/ImageCore.jl

src/ImageBase.jl

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
11
module ImageBase
22

3-
export restrict
3+
export
4+
# two-fold downsampling
5+
# originally from ImageTransformations.jl
6+
restrict,
7+
8+
# finite difference on one-dimension
9+
# originally from Images.jl
10+
fdiff,
11+
fdiff!
12+
413

514
using Reexport
615

716
using Base.Cartesian: @nloops
817
@reexport using ImageCore
918
using ImageCore.OffsetArrays
1019

20+
include("diff.jl")
1121
include("restrict.jl")
1222
include("compat.jl")
1323
include("deprecated.jl")

src/compat.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,7 @@ if VERSION < v"1.2"
33
else
44
const require_one_based_indexing = Base.require_one_based_indexing
55
end
6+
7+
if VERSION < v"1.1"
8+
isnothing(x) = x === nothing
9+
end

src/diff.jl

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
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
3+
"""
4+
fdiff(A::AbstractArray; dims::Int, rev=false, boundary=:periodic)
5+
6+
A one-dimension finite difference operator on array `A`. Unlike `Base.diff`, this function doesn't
7+
shrink the array size.
8+
9+
Take vector as an example, it computes `(A[2]-A[1], A[3]-A[2], ..., A[1]-A[end])`.
10+
11+
# Keywords
12+
13+
- `rev::Bool`
14+
If `rev==true`, then it computes the backward difference
15+
`(A[end]-A[1], A[1]-A[2], ..., A[end-1]-A[end])`.
16+
- `boundary`
17+
By default it computes periodically in the boundary, i.e., `:periodic`.
18+
In some cases, one can fill zero values with `boundary=:zero`.
19+
20+
# Examples
21+
22+
```jldoctest; setup=:(using ImageBase: fdiff)
23+
julia> A = [2 4 8; 3 9 27; 4 16 64]
24+
3×3 $(Matrix{Int}):
25+
2 4 8
26+
3 9 27
27+
4 16 64
28+
29+
julia> diff(A, dims=2) # this function exists in Base
30+
3×2 $(Matrix{Int}):
31+
2 4
32+
6 18
33+
12 48
34+
35+
julia> fdiff(A, dims=2)
36+
3×3 $(Matrix{Int}):
37+
2 4 -6
38+
6 18 -24
39+
12 48 -60
40+
41+
julia> fdiff(A, dims=2, rev=true) # reverse diff
42+
3×3 $(Matrix{Int}):
43+
-6 2 4
44+
-24 6 18
45+
-60 12 48
46+
47+
julia> fdiff(A, dims=2, boundary=:zero) # fill boundary with zeros
48+
3×3 $(Matrix{Int}):
49+
2 4 0
50+
6 18 0
51+
12 48 0
52+
```
53+
54+
See also [`fdiff!`](@ref) for the in-place version.
55+
"""
56+
fdiff(A::AbstractArray; kwargs...) = fdiff!(similar(A), A; kwargs...)
57+
58+
"""
59+
fdiff!(dst::AbstractArray, src::AbstractArray; dims::Int, rev=false, boundary=:periodic)
60+
61+
The in-place version of [`ImageBase.fdiff`](@ref)
62+
"""
63+
function fdiff!(dst::AbstractArray, src::AbstractArray;
64+
dims=_fdiff_default_dims(src),
65+
rev=false,
66+
boundary::Symbol=:periodic)
67+
isnothing(dims) && throw(UndefKeywordError(:dims))
68+
axes(dst) == axes(src) || throw(ArgumentError("axes of all input arrays should be equal. Instead they are $(axes(dst)) and $(axes(src))."))
69+
N = ndims(src)
70+
1 <= dims <= N || throw(ArgumentError("dimension $dims out of range (1:$N)"))
71+
72+
r = axes(src)
73+
r0 = ntuple(i -> i == dims ? UnitRange(first(r[i]), last(r[i]) - 1) : UnitRange(r[i]), N)
74+
r1 = ntuple(i -> i == dims ? UnitRange(first(r[i])+1, last(r[i])) : UnitRange(r[i]), N)
75+
76+
d0 = ntuple(i -> i == dims ? UnitRange(last(r[i]), last(r[i])) : UnitRange(r[i]), N)
77+
d1 = ntuple(i -> i == dims ? UnitRange(first(r[i]), first(r[i])) : UnitRange(r[i]), N)
78+
79+
if rev
80+
dst[r1...] .= view(src, r1...) .- view(src, r0...)
81+
if boundary == :periodic
82+
dst[d1...] .= view(src, d1...) .- view(src, d0...)
83+
elseif boundary == :zero
84+
dst[d1...] .= zero(eltype(dst))
85+
else
86+
throw(ArgumentError("Wrong boundary condition $boundary"))
87+
end
88+
else
89+
dst[r0...] .= view(src, r1...) .- view(src, r0...)
90+
if boundary == :periodic
91+
dst[d0...] .= view(src, d1...) .- view(src, d0...)
92+
elseif boundary == :zero
93+
dst[d0...] .= zero(eltype(dst))
94+
else
95+
throw(ArgumentError("Wrong boundary condition $boundary"))
96+
end
97+
end
98+
99+
return dst
100+
end
101+
102+
_fdiff_default_dims(A) = nothing
103+
_fdiff_default_dims(A::AbstractVector) = 1

test/diff.jl

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
@testset "fdiff" begin
2+
@testset "API" begin
3+
# fdiff! works the same as fdiff
4+
mat_in = rand(3, 3, 3)
5+
mat_out = similar(mat_in)
6+
fdiff!(mat_out, mat_in, dims = 2)
7+
@test mat_out == fdiff(mat_in, dims = 2)
8+
9+
mat_in = rand(3, 3, 3)
10+
mat_out = similar(mat_in)
11+
fdiff!(mat_out, mat_in, dims = 3, rev=true)
12+
@test mat_out == fdiff(mat_in, dims = 3, rev=true)
13+
14+
# default `dims` are only available when input is a vector
15+
v = rand(9)
16+
@test fdiff(v) == fdiff(v; dims=1)
17+
m = rand(3, 3)
18+
@test_throws UndefKeywordError fdiff(m)
19+
end
20+
21+
@testset "NumericalTests" begin
22+
a = reshape(collect(1:9), 3, 3)
23+
b_fd_1 = [1 1 1; 1 1 1; -2 -2 -2]
24+
b_fd_2 = [3 3 -6; 3 3 -6; 3 3 -6]
25+
b_bd_1 = [-2 -2 -2; 1 1 1; 1 1 1]
26+
b_bd_2 = [-6 3 3; -6 3 3; -6 3 3]
27+
out = similar(a)
28+
29+
@test fdiff(a, dims = 1) == b_fd_1
30+
@test fdiff(a, dims = 2) == b_fd_2
31+
@test fdiff(a, dims = 1, rev=true) == b_bd_1
32+
@test fdiff(a, dims = 2, rev=true) == b_bd_2
33+
fdiff!(out, a, dims = 1)
34+
@test out == b_fd_1
35+
fdiff!(out, a, dims = 2)
36+
@test out == b_fd_2
37+
fdiff!(out, a, dims = 1, rev=true)
38+
@test out == b_bd_1
39+
fdiff!(out, a, dims = 2, rev=true)
40+
@test out == b_bd_2
41+
42+
@testset "Boundary" begin
43+
# These originally lives in Images and we use it to check the compatibility
44+
forwarddiffy(u::AbstractMatrix) = [u[2:end,:]; u[end:end,:]] - u
45+
forwarddiffx(u::AbstractMatrix) = [u[:,2:end] u[:,end:end]] - u
46+
backdiffy(u::AbstractMatrix) = u - [u[1:1,:]; u[1:end-1,:]]
47+
backdiffx(u::AbstractMatrix) = u - [u[:,1:1] u[:,1:end-1]]
48+
49+
X = rand(3, 3)
50+
@test forwarddiffx(X) == fdiff(X, dims=2, boundary=:zero)
51+
@test forwarddiffy(X) == fdiff(X, dims=1, boundary=:zero)
52+
@test backdiffx(X) == fdiff(X, dims=2, rev=true, boundary=:zero)
53+
@test backdiffy(X) == fdiff(X, dims=1, rev=true, boundary=:zero)
54+
end
55+
56+
if VERSION >= v"1.1"
57+
# dims keyword for diff only exists after Julia 1.1
58+
# check numerical results with base implementation
59+
drop_last_slice(X, dims) = collect(StackView(collect(eachslice(X, dims=dims))[1:end-1]..., dims=dims))
60+
function drop_last_slice(X::AbstractVector, dims)
61+
@assert dims==1
62+
X[1:end-1]
63+
end
64+
drop_first_slice(X, dims) = collect(StackView(collect(eachslice(X, dims=dims))[2:end]..., dims=dims))
65+
function drop_first_slice(X::AbstractVector, dims)
66+
@assert dims==1
67+
X[2:end]
68+
end
69+
70+
for N in 1:3
71+
sz = ntuple(_->5, N)
72+
A = rand(sz...)
73+
A_out = similar(A)
74+
75+
for dims = 1:N
76+
out_base = diff(A; dims=dims)
77+
out = fdiff(A; dims=dims)
78+
@test out_base == drop_last_slice(out, dims)
79+
80+
out_base = .-reverse(diff(reverse(A; dims=dims); dims=dims); dims=dims)
81+
out = fdiff(A; dims=dims, rev=true)
82+
@test out_base == drop_first_slice(out, dims)
83+
end
84+
end
85+
end
86+
end
87+
88+
@testset "OffsetArrays" begin
89+
A = OffsetArray(rand(3, 3), -1, -1)
90+
A_out = fdiff(A, dims=1)
91+
@test axes(A_out) == (0:2, 0:2)
92+
@test A_out.parent == fdiff(parent(A), dims=1)
93+
94+
A = OffsetArray(rand(3, 3), -1, -1)
95+
A_out = fdiff(A, dims=1, rev=true)
96+
@test axes(A_out) == (0:2, 0:2)
97+
@test A_out.parent == fdiff(parent(A), dims=1, rev=true)
98+
end
99+
end

test/runtests.jl

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,30 @@
1-
using ImageBase, ImageCore, OffsetArrays
2-
using Test, TestImages
1+
using ImageBase, OffsetArrays, StackViews
2+
using Test, TestImages, Aqua, Documenter
33

44
using OffsetArrays: IdentityUnitRange
55

66
@testset "ImageBase.jl" begin
7+
8+
@testset "Project meta quality checks" begin
9+
# Not checking compat section for test-only dependencies
10+
ambiguity_exclude_list = [
11+
# https://github.com/JuliaDiff/ChainRulesCore.jl/pull/367#issuecomment-869071000
12+
Base.:(==),
13+
]
14+
Aqua.test_ambiguities([ImageCore, Base, Core], exclude=ambiguity_exclude_list)
15+
Aqua.test_all(ImageBase;
16+
ambiguities=false,
17+
project_extras=true,
18+
deps_compat=true,
19+
stale_deps=true,
20+
project_toml_formatting=true
21+
)
22+
if VERSION >= v"1.2"
23+
doctest(ImageBase,manual = false)
24+
end
25+
end
26+
27+
include("diff.jl")
728
include("restrict.jl")
829

930
@info "deprecations are expected"

0 commit comments

Comments
 (0)