From 7e2a9657631684104238ed2b0c5ef15b55c830ab Mon Sep 17 00:00:00 2001 From: Zygmunt Szpak Date: Mon, 5 Dec 2022 21:27:22 +1030 Subject: [PATCH] Adds support for specifying kernels using StaticArrays Fixes https://github.com/JuliaImages/ImageFiltering.jl/issues/252 --- Project.toml | 2 +- src/border.jl | 19 +++++++ src/imfilter.jl | 4 +- test/2d.jl | 17 +++++++ test/nd.jl | 131 ++++++++++++++++++++++++++---------------------- 5 files changed, 111 insertions(+), 62 deletions(-) diff --git a/Project.toml b/Project.toml index 219b745f..650ab70d 100644 --- a/Project.toml +++ b/Project.toml @@ -30,7 +30,7 @@ ImageCore = "0.9" OffsetArrays = "1.9" Reexport = "1.1" StaticArrays = "0.10, 0.11, 0.12, 1.0" -TiledIteration = "0.2, 0.3, 0.4" +TiledIteration = "0.2, 0.3" julia = "1" [extras] diff --git a/src/border.jl b/src/border.jl index e12f1c39..914929dc 100644 --- a/src/border.jl +++ b/src/border.jl @@ -997,6 +997,25 @@ expand(inds::Indices, kernel) = expand(inds, calculate_padding(kernel)) expand(inds::Indices, pad::Indices) = firsttype(map_copytail(expand, inds, pad)) expand(ind::AbstractUnitRange, pad::AbstractUnitRange) = typeof(ind)(first(ind)+first(pad):last(ind)+last(pad)) expand(ind::Base.OneTo, pad::AbstractUnitRange) = expand(UnitRange(ind), pad) +expand(ind::SOneTo{T₁}, pad::AbstractUnitRange) where {T₁} = represent_unit_range_with_SOneTo(ind, pad) +expand(ind::OffsetArrays.IdOffsetRange{T₁, SOneTo{T₂}}, pad::AbstractUnitRange) where {T₁, T₂} = determine_offset_with_SOneTo(ind, pad) + +function represent_unit_range_with_SOneTo(ind::SOneTo{T₁}, pad::AbstractUnitRange) where {T₁} + lo = first(ind)+first(pad) + hi = last(ind)+last(pad) + interval = 1:length(lo:hi) + T = typeof(T₁) + I = typeof(SOneTo(length(interval))) + return OffsetArrays.IdOffsetRange{T, I}(interval, lo - 1) +end + +function determine_offset_with_SOneTo(ind::OffsetArrays.IdOffsetRange{T₁, SOneTo{T₂}}, pad::AbstractUnitRange) where {T₁, T₂} + lo = first(ind)+first(pad) + hi = last(ind)+last(pad) + interval = 1:length(lo:hi) + I = typeof(SOneTo(length(interval))) + return OffsetArrays.IdOffsetRange{T₁, I}(interval, lo - 1) +end """ shrink(inds::Indices, kernel) diff --git a/src/imfilter.jl b/src/imfilter.jl index 9ec00494..ebcf1494 100644 --- a/src/imfilter.jl +++ b/src/imfilter.jl @@ -502,8 +502,8 @@ See also: [`Pad`](@ref), [`padarray`](@ref), [`Inner`](@ref), [`NA`](@ref) and ## Choices for `alg` -The `alg` parameter allows you to choose the particular algorithm: `Algorithm.FIR()` -(finite impulse response, aka traditional digital filtering) or `Algorithm.FFT()` +The `alg` parameter allows you to choose the particular algorithm: `FIR()` +(finite impulse response, aka traditional digital filtering) or `FFT()` (Fourier-based filtering). If no choice is specified, one will be chosen based on the size of the image and kernel in a way that strives to deliver good performance. Alternatively you can use a custom filter type, like diff --git a/test/2d.jl b/test/2d.jl index 37d8fd29..9a7effb8 100644 --- a/test/2d.jl +++ b/test/2d.jl @@ -341,3 +341,20 @@ end @test_throws DimensionMismatch imfilter(CPU1(), A, kern, Fill(0, (0,1))) @test_throws DimensionMismatch imfilter(CPU1(), A, kern, Fill(0, (0,0))) end + +@testset "Static Arrays (issue #252)" begin + img = rand(RGB{Float32}, 9, 9) + k₁ = [0.2f0 -0.5f0 0.3f0] + k₂ = SMatrix{1, 3, Float32}(k₁) + border_styles = ["replicate", "circular", "reflect", "symmetric"] + for border_style in border_styles + A = imfilter(img, (k₁', k₁), border_style, ImageFiltering.Algorithm.FIR()) + B = imfilter(img, (k₂', k₂), border_style, ImageFiltering.Algorithm.FIR()) + @test A ≈ B + k₁ = centered(k₁) + k₂ = centered(k₂) + A = imfilter(img, (k₁', k₁), border_style, ImageFiltering.Algorithm.FIR()) + B = imfilter(img, (k₂', k₂), border_style, ImageFiltering.Algorithm.FIR()) + @test A ≈ B + end +end \ No newline at end of file diff --git a/test/nd.jl b/test/nd.jl index 8842743f..684bb030 100644 --- a/test/nd.jl +++ b/test/nd.jl @@ -13,38 +13,42 @@ Base.zero(::Type{WrappedFloat}) = WrappedFloat(0.0) @testset "1d" begin img = 1:8 - # Exercise all the different ways to call imfilter - kern = centered([1/3,1/3,1/3]) - imgf = imfilter(img, kern) - r = CPU1(Algorithm.FIR()) - @test imfilter(Float64, img, kern) == imgf - @test imfilter(Float64, img, (kern,)) == imgf - @test imfilter(Float64, img, kern, "replicate") == imgf - @test imfilter(Float64, img, (kern,), "replicate") == imgf - @test_throws ArgumentError imfilter(Float64, img, (kern,), "inner") - @test_throws ArgumentError imfilter(Float64, img, (kern,), "nonsense") - @test imfilter(Float64, img, kern, "replicate", Algorithm.FIR()) == imgf - @test imfilter(Float64, img, (kern,), "replicate", Algorithm.FIR()) == imgf - @test imfilter(r, img, kern) == imgf - @test imfilter(r, img, kern) == imgf - @test imfilter(r, Float64, img, kern) == imgf - @test imfilter(r, Float64, img, (kern,)) == imgf - @test imfilter(r, img, kern, "replicate") == imgf - @test imfilter(r, Float64, img, kern, "replicate") == imgf - @test imfilter(r, img, (kern,), "replicate") == imgf - @test imfilter(r, Float64, img, (kern,), "replicate") == imgf - @test_throws MethodError imfilter(r, img, (kern,), "replicate", Algorithm.FIR()) - @test_throws MethodError imfilter(r, Float64, img, (kern,), "replicate", Algorithm.FIR()) - out = similar(imgf) - @test imfilter!(out, img, kern) == imgf - @test imfilter!(out, img, (kern,)) == imgf - @test imfilter!(out, img, (kern,), "replicate") == imgf - @test imfilter!(out, img, (kern,), "replicate", Algorithm.FIR()) == imgf - @test imfilter!(r, out, img, kern) == imgf - @test imfilter!(r, out, img, (kern,)) == imgf - @test imfilter!(r, out, img, kern, "replicate") == imgf - @test imfilter!(r, out, img, (kern,), "replicate") == imgf - @test_throws MethodError imfilter!(r, out, img, (kern,), "replicate", Algorithm.FIR()) + array_types = [Vector, SVector{3}] + values = [1/3, 1/3, 1/3] + for array_type in array_types + # Exercise all the different ways to call imfilter + kern = centered(array_type(values)) + imgf = imfilter(img, kern) + r = CPU1(Algorithm.FIR()) + @test imfilter(Float64, img, kern) == imgf + @test imfilter(Float64, img, (kern,)) == imgf + @test imfilter(Float64, img, kern, "replicate") == imgf + @test imfilter(Float64, img, (kern,), "replicate") == imgf + @test_throws ArgumentError imfilter(Float64, img, (kern,), "inner") + @test_throws ArgumentError imfilter(Float64, img, (kern,), "nonsense") + @test imfilter(Float64, img, kern, "replicate", Algorithm.FIR()) == imgf + @test imfilter(Float64, img, (kern,), "replicate", Algorithm.FIR()) == imgf + @test imfilter(r, img, kern) == imgf + @test imfilter(r, img, kern) == imgf + @test imfilter(r, Float64, img, kern) == imgf + @test imfilter(r, Float64, img, (kern,)) == imgf + @test imfilter(r, img, kern, "replicate") == imgf + @test imfilter(r, Float64, img, kern, "replicate") == imgf + @test imfilter(r, img, (kern,), "replicate") == imgf + @test imfilter(r, Float64, img, (kern,), "replicate") == imgf + @test_throws MethodError imfilter(r, img, (kern,), "replicate", Algorithm.FIR()) + @test_throws MethodError imfilter(r, Float64, img, (kern,), "replicate", Algorithm.FIR()) + out = similar(imgf) + @test imfilter!(out, img, kern) == imgf + @test imfilter!(out, img, (kern,)) == imgf + @test imfilter!(out, img, (kern,), "replicate") == imgf + @test imfilter!(out, img, (kern,), "replicate", Algorithm.FIR()) == imgf + @test imfilter!(r, out, img, kern) == imgf + @test imfilter!(r, out, img, (kern,)) == imgf + @test imfilter!(r, out, img, kern, "replicate") == imgf + @test imfilter!(r, out, img, (kern,), "replicate") == imgf + @test_throws MethodError imfilter!(r, out, img, (kern,), "replicate", Algorithm.FIR()) + end # Element-type widening (issue #17) v = fill(0xff, 10) @@ -56,24 +60,29 @@ Base.zero(::Type{WrappedFloat}) = WrappedFloat(0.0) @test all(x->x==0x0002fa03, vout) # Cascades don't result in out-of-bounds values - k1 = centered([0.25, 0.5, 0.25]) - k2 = OffsetArray([0.5, 0.5], 1:2) - casc = imfilter(img, (k1, k2, k1)) - A0 = padarray(img, Pad(:replicate, (2,), (4,))) - A1 = imfilter(A0, k1, Inner()) - @test A1 ≈ OffsetArray([1.0,1.25,2.0,3.0,4.0,5.0,6.0,7.0,7.75,8.0,8.0,8.0], 0:11) - A2 = imfilter(A1, k2, Inner()) - @test A2 ≈ OffsetArray([1.625,2.5,3.5,4.5,5.5,6.5,7.375,7.875,8.0,8.0], 0:9) - A3 = imfilter(A2, k1, Inner()) - @test casc ≈ A3 - @test size(casc) == size(img) - # copy! kernels, presence/order doesn't matter - kc = centered([1]) - @test ImageFiltering.iscopy(kc) - @test imfilter(img, (k1,)) ≈ [1.25,2.0,3.0,4.0,5.0,6.0,7.0,7.75] - @test imfilter(img, (kc, k1)) ≈ [1.25,2.0,3.0,4.0,5.0,6.0,7.0,7.75] - @test imfilter(img, (k1, kc)) ≈ [1.25,2.0,3.0,4.0,5.0,6.0,7.0,7.75] - + array_types₁ = [Vector, SVector{3}] + array_types₂ = [Vector, SVector{2}] + for (array_type₁, array_type₂) in zip(array_types₁, array_types₂) + values1 = [0.25, 0.5, 0.25] + values2 = [0.5, 0.5] + k1 = centered(array_type₁(values1)) + k2 = OffsetArray(array_type₂(values2), 1:2) + casc = imfilter(img, (k1, k2, k1)) + A0 = padarray(img, Pad(:replicate, (2,), (4,))) + A1 = imfilter(A0, k1, Inner()) + @test A1 ≈ OffsetArray([1.0,1.25,2.0,3.0,4.0,5.0,6.0,7.0,7.75,8.0,8.0,8.0], 0:11) + A2 = imfilter(A1, k2, Inner()) + @test A2 ≈ OffsetArray([1.625,2.5,3.5,4.5,5.5,6.5,7.375,7.875,8.0,8.0], 0:9) + A3 = imfilter(A2, k1, Inner()) + @test casc ≈ A3 + @test size(casc) == size(img) + # copy! kernels, presence/order doesn't matter + kc = centered([1]) + @test ImageFiltering.iscopy(kc) + @test imfilter(img, (k1,)) ≈ [1.25,2.0,3.0,4.0,5.0,6.0,7.0,7.75] + @test imfilter(img, (kc, k1)) ≈ [1.25,2.0,3.0,4.0,5.0,6.0,7.0,7.75] + @test imfilter(img, (k1, kc)) ≈ [1.25,2.0,3.0,4.0,5.0,6.0,7.0,7.75] + end # FFT without padding img = collect(1:8) img[1] = img[8] = 0 @@ -81,15 +90,19 @@ Base.zero(::Type{WrappedFloat}) = WrappedFloat(0.0) @test out ≈ imfilter(img, kern) # Inputs that have non-1 axes - img = OffsetArray(zeros(11), -5:5) - img[0] = 1 - for border in ("replicate", "circular", "symmetric", "reflect", - Fill(zero(eltype(img))), Inner(1), NA()) - imgf = imfilter(img, centered([0.25, 0.5, 0.25]), border) - @test imgf[-1] == imgf[1] == 0.25 - @test imgf[0] == 0.5 - inds = axes(imgf,1) - @test all(x->x==0, imgf[first(inds):-2]) && all(x->x==0, imgf[2:last(inds)]) + array_types = [Vector, MVector{11}] + values = zeros(11) + for array_type in array_types + img = OffsetArray(array_type(values), -5:5) + img[0] = 1 + for border in ("replicate", "circular", "symmetric", "reflect", + Fill(zero(eltype(img))), Inner(1), NA()) + imgf = imfilter(img, centered([0.25, 0.5, 0.25]), border) + @test imgf[-1] == imgf[1] == 0.25 + @test imgf[0] == 0.5 + inds = axes(imgf,1) + @test all(x->x==0, imgf[first(inds):-2]) && all(x->x==0, imgf[2:last(inds)]) + end end # Non-finite input