From bad0c703d2211adf564f988878e71974d80b765e Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Wed, 1 Sep 2021 06:49:11 -0500 Subject: [PATCH 1/3] Move extremization, blob_LoG from Images --- Project.toml | 2 +- src/ImageFiltering.jl | 6 ++- src/compat.jl | 11 ++++ src/extrema.jl | 117 ++++++++++++++++++++++++++++++++++++++++++ test/extrema.jl | 55 ++++++++++++++++++++ test/runtests.jl | 1 + 6 files changed, 190 insertions(+), 2 deletions(-) create mode 100644 src/compat.jl create mode 100644 src/extrema.jl create mode 100644 test/extrema.jl diff --git a/Project.toml b/Project.toml index e23edad8..0efab21e 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ImageFiltering" uuid = "6a3955dd-da59-5b1f-98d4-e7296123deb5" author = ["Tim Holy ", "Jan Weidner "] -version = "0.6.22" +version = "0.7.0" [deps] CatIndices = "aafaddc9-749c-510e-ac4f-586e18779b91" diff --git a/src/ImageFiltering.jl b/src/ImageFiltering.jl index f9703a57..27a68ca5 100644 --- a/src/ImageFiltering.jl +++ b/src/ImageFiltering.jl @@ -18,7 +18,9 @@ export Kernel, KernelFactors, imfilter, imfilter!, mapwindow, mapwindow!, imgradients, padarray, centered, kernelfactors, reflect, - freqkernel, spacekernel + freqkernel, spacekernel, + findlocalminima, findlocalmaxima, + blob_LoG, BlobLoG FixedColorant{T<:Normed} = Colorant{T} StaticOffsetArray{T,N,A<:StaticArray} = OffsetArray{T,N,A} @@ -56,6 +58,7 @@ using .Algorithm: Alg, FFT, FIR, FIRTiled, IIR, Mixed Alg(r::AbstractResource{A}) where {A<:Alg} = r.settings include("utils.jl") +include("compat.jl") include("kernelfactors.jl") using .KernelFactors: TriggsSdika, IIRFilter, ReshapedOneD, iterdims, kernelfactors @@ -85,6 +88,7 @@ include("specialty.jl") include("mapwindow.jl") using .MapWindow +include("extrema.jl") function __init__() # See ComputationalResources README for explanation diff --git a/src/compat.jl b/src/compat.jl new file mode 100644 index 00000000..d22a4480 --- /dev/null +++ b/src/compat.jl @@ -0,0 +1,11 @@ +if VERSION <= v"1.0.5" + # https://github.com/JuliaLang/julia/pull/29442 + _oneunit(::CartesianIndex{N}) where {N} = _oneunit(CartesianIndex{N}) + _oneunit(::Type{CartesianIndex{N}}) where {N} = CartesianIndex(ntuple(x -> 1, Val(N))) +else + const _oneunit = Base.oneunit +end + +# Equivalent to I:J on later Julia versions +_colon(I::CartesianIndex{N}, J::CartesianIndex{N}) where N = + CartesianIndices(map((i,j) -> i:j, Tuple(I), Tuple(J))) diff --git a/src/extrema.jl b/src/extrema.jl new file mode 100644 index 00000000..4ad616eb --- /dev/null +++ b/src/extrema.jl @@ -0,0 +1,117 @@ +""" +BlobLoG stores information about the location of peaks as discovered by `blob_LoG`. +It has fields: + +- location: the location of a peak in the filtered image (a CartesianIndex) +- σ: the value of σ which lead to the largest `-LoG`-filtered amplitude at this location +- amplitude: the value of the `-LoG(σ)`-filtered image at the peak + +Note that the radius is equal to σ√2. + +See also: [`blob_LoG`](@ref). +""" +struct BlobLoG{T,S,N} + location::CartesianIndex{N} + σ::S + amplitude::T +end + +""" + blob_LoG(img, σscales; edges=(true, false, ...), σshape=(1, ...)) -> Vector{BlobLoG} + +Find "blobs" in an N-D image using the negative Lapacian of Gaussians +with the specifed vector or tuple of σ values. The algorithm searches for places +where the filtered image (for a particular σ) is at a peak compared to all +spatially- and σ-adjacent voxels, where σ is `σscales[i] * σshape` for some i. +By default, `σshape` is an ntuple of 1s. + +The optional `edges` argument controls whether peaks on the edges are +included. `edges` can be `true` or `false`, or a N+1-tuple in which +the first entry controls whether edge-σ values are eligible to serve +as peaks, and the remaining N entries control each of the N dimensions +of `img`. + +# Citation: + +Lindeberg T (1998), "Feature Detection with Automatic Scale Selection", +International Journal of Computer Vision, 30(2), 79–116. + +See also: [`BlobLoG`](@ref). +""" +function blob_LoG(img::AbstractArray{T,N}, σscales::Union{AbstractVector,Tuple}; + edges::Union{Bool,Tuple{Vararg{Bool}}}=(true, ntuple(d->false, Val(N))...), σshape=ntuple(d->1, Val(N))) where {T,N} + if edges isa Bool + edges = (edges, ntuple(d->edges,Val(N))...) + end + sigmas = sort(σscales) + img_LoG = Array{Float64}(undef, length(sigmas), size(img)...) + colons = ntuple(d->Colon(), Val(N)) + @inbounds for isigma in eachindex(sigmas) + img_LoG[isigma,colons...] = (-sigmas[isigma]) * imfilter(img, Kernel.LoG(ntuple(i->sigmas[isigma]*σshape[i],Val(N)))) + end + maxima = findlocalmaxima(img_LoG; dims=1:ndims(img_LoG), edges=edges) + [BlobLoG(CartesianIndex(tail(x.I)), sigmas[x[1]], img_LoG[x]) for x in maxima] +end + + +""" + findlocalmaxima(img; dims=coords_spatial(img), edges=true) -> Vector{CartesianIndex} + +Returns the coordinates of elements whose value is larger than all of +their immediate neighbors. `dims` is a list of dimensions to +consider. `edges` is a boolean specifying whether to include the +first and last elements of each dimension, or a tuple-of-Bool +specifying edge behavior for each dimension separately. +""" +findlocalmaxima(img::AbstractArray; dims=coords_spatial(img), edges=true) = + findlocalextrema(img, dims, edges, Base.Order.Forward) + +""" + findlocalminima(img; dims=coords_spatial(img), edges=true) -> Vector{CartesianIndex} + +Like [`findlocalmaxima`](@ref), but returns the coordinates of the smallest elements. +""" +findlocalminima(img::AbstractArray; dims=coords_spatial(img), edges=true) = + findlocalextrema(img, dims, edges, Base.Order.Reverse) + + +findlocalextrema(img::AbstractArray{T,N}, dims, edges::Bool, order) where {T,N} = findlocalextrema(img, dims, ntuple(d->edges,Val(N)), order) + +function findlocalextrema(img::AbstractArray{T,N}, dims, edges::NTuple{N,Bool}, order::Base.Order.Ordering) where {T<:Union{Gray,Number},N} + dims ⊆ 1:ndims(img) || throw(ArgumentError("invalid dims")) + extrema = Array{CartesianIndex{N}}(undef, 0) + Iedge = CartesianIndex(map(!, edges)) + R0 = CartesianIndices(img) + R = clippedinds(R0, Iedge) + I1 = _oneunit(first(R0)) + Rinterior = clippedinds(R0, I1) + iregion = CartesianIndex(ntuple(d->d ∈ dims, Val(N))) + Rregion = CartesianIndices(map((f,l)->f:l,(-iregion).I, iregion.I)) + z = zero(iregion) + for i in R + isextrema = true + img_i = img[i] + if i ∈ Rinterior + # If i is in the interior, we don't have to worry about i+j being out-of-bounds + for j in Rregion + j == z && continue + if !Base.Order.lt(order, img[i+j], img_i) + isextrema = false + break + end + end + else + for j in Rregion + (j == z || i+j ∉ R0) && continue + if !Base.Order.lt(order, img[i+j], img_i) + isextrema = false + break + end + end + end + isextrema && push!(extrema, i) + end + extrema +end + +clippedinds(Router, Iclip) = _colon(first(Router)+Iclip, last(Router)-Iclip) diff --git a/test/extrema.jl b/test/extrema.jl new file mode 100644 index 00000000..1fe987ff --- /dev/null +++ b/test/extrema.jl @@ -0,0 +1,55 @@ +@testset "extrema" begin + @testset "local extrema" begin + A = zeros(Int, 9, 9); A[[1:2;5],5].=1 + @test findlocalmaxima(A) == [CartesianIndex((5,5))] + @test findlocalmaxima(A; dims=2) == [CartesianIndex((1,5)),CartesianIndex((2,5)),CartesianIndex((5,5))] + @test findlocalmaxima(A; dims=2, edges=false) == [CartesianIndex((2,5)),CartesianIndex((5,5))] + A = zeros(Int, 9, 9, 9); A[[1:2;5],5,5].=1 + @test findlocalmaxima(A) == [CartesianIndex((5,5,5))] + @test findlocalmaxima(A; dims=2) == [CartesianIndex((1,5,5)),CartesianIndex((2,5,5)),CartesianIndex((5,5,5))] + @test findlocalmaxima(A,dims=2, edges=false) == [CartesianIndex((2,5,5)),CartesianIndex((5,5,5))] + A = zeros(Int, 9, 9); A[[1:2;5],5].=-1 + @test findlocalminima(A) == [CartesianIndex((5,5))] + end + + @testset "blob_LoG" begin + A = zeros(Int, 9, 9); A[5, 5] = 1 + blobs = blob_LoG(A, 2.0.^[0.5,0,1]) + @test length(blobs) == 1 + blob = blobs[1] + @test blob.amplitude ≈ 0.3183098861837907 + @test blob.σ === 1.0 + @test blob.location == CartesianIndex((5,5)) + @test blob_LoG(A, [1.0]) == blobs + @test blob_LoG(A, [1.0]; edges=(true, false, false)) == blobs + @test isempty(blob_LoG(A, [1.0]; edges=false)) + A = zeros(Int, 9, 9); A[1, 5] = 1 + blobs = blob_LoG(A, 2.0.^[0,0.5,1]) + A = zeros(Int, 9, 9); A[1,5] = 1 + blobs = blob_LoG(A, 2.0.^[0.5,0,1]) + @test all(b.amplitude < 1e-16 for b in blobs) + blobs = filter(b->b.amplitude > 0.1, blob_LoG(A, 2.0.^[0.5,0,1]; edges=true)) + @test length(blobs) == 1 + @test blobs[1].location == CartesianIndex((1,5)) + @test filter(b->b.amplitude > 0.1, blob_LoG(A, 2.0.^[0.5,0,1], edges=(true, true, false))) == blobs + @test isempty(blob_LoG(A, 2.0.^[0,1], edges=(false, true, false))) + blobs = blob_LoG(A, 2.0.^[0,0.5,1], edges=(true, false, true)) + @test all(b.amplitude < 1e-16 for b in blobs) + # stub test for N-dimensional blob_LoG: + A = zeros(Int, 9, 9, 9); A[5, 5, 5] = 1 + blobs = blob_LoG(A, 2.0.^[0.5, 0, 1]) + @test length(blobs) == 1 + @test blobs[1].location == CartesianIndex((5,5,5)) + # kinda anisotropic image + A = zeros(Int,9,9,9); A[5,4:6,5] .= 1; + blobs = blob_LoG(A,2 .^ [1.,0,0.5], σshape=[1.,3.,1.]) + @test length(blobs) == 1 + @test blobs[1].location == CartesianIndex((5,5,5)) + A = zeros(Int,9,9,9); A[1,1,4:6] .= 1; + blobs = filter(b->b.amplitude > 0.1, blob_LoG(A, 2.0.^[0.5,0,1], edges=true, σshape=[1.,1.,3.])) + @test length(blobs) == 1 + @test blobs[1].location == CartesianIndex((1,1,5)) + @test filter(b->b.amplitude > 0.1, blob_LoG(A, 2.0.^[0.5,0,1], edges=(true, true, true, false), σshape=[1.,1.,3.])) == blobs + @test isempty(blob_LoG(A, 2.0.^[0,1], edges=(false, true, false, false), σshape=[1.,1.,3.])) + end +end diff --git a/test/runtests.jl b/test/runtests.jl index 29d6f7c9..fd407e9c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -28,6 +28,7 @@ include("cascade.jl") include("specialty.jl") include("gradient.jl") include("mapwindow.jl") +include("extrema.jl") include("basic.jl") include("gabor.jl") From 8d854bb8e701447ec14c61abcfe97c1c27e900a7 Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Wed, 1 Sep 2021 15:39:02 -0500 Subject: [PATCH 2/3] Further API cleanups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This makes more dramatic depatures from our old implementations: - add a `show` method for `BlobLog` - encode the entire multidimensional `σ` (after multiplying by `σshape`) - add a threshold to dispose of spurious maxima by default - separate the filtering of `blob_LoG` into an independent (but non-exported) function - change the API of `findlocalmaxima` etc to be `window`-based rather than `dims`-based. This is strictly more flexible. Co-authored-by: Johnny Chen --- src/extrema.jl | 107 +++++++++++++++++++++++++++++++++++------------- test/extrema.jl | 18 ++++---- 2 files changed, 87 insertions(+), 38 deletions(-) diff --git a/src/extrema.jl b/src/extrema.jl index 4ad616eb..b75b89e9 100644 --- a/src/extrema.jl +++ b/src/extrema.jl @@ -15,9 +15,15 @@ struct BlobLoG{T,S,N} σ::S amplitude::T end +BlobLoG(; location, σ, amplitude) = BlobLoG(location, σ, amplitude) + +function Base.show(io::IO, bl::BlobLoG) + print(io, "location=", bl.location, ", σ=", bl.σ, ", amplitude=", bl.amplitude) +end + """ - blob_LoG(img, σscales; edges=(true, false, ...), σshape=(1, ...)) -> Vector{BlobLoG} + blob_LoG(img, σscales; edges::Tuple=(true, false, ...), σshape::Tuple=(1, ...), rthresh=0.001) -> Vector{BlobLoG} Find "blobs" in an N-D image using the negative Lapacian of Gaussians with the specifed vector or tuple of σ values. The algorithm searches for places @@ -31,6 +37,31 @@ the first entry controls whether edge-σ values are eligible to serve as peaks, and the remaining N entries control each of the N dimensions of `img`. +`rthresh` controls the minimum amplitude of peaks in the -LoG-filtered image, +as a fraction of `maximum(abs, img)` and the volume of the Gaussian. + +# Examples + +While most images are 2- or 3-dimensional, it will be easier to illustrate this with +a one-dimensional "image" containing two Gaussian blobs of different sizes: + +```jldoctest; setup=:(using ImageFiltering), filter=r"amplitude=.*"] +julia> σs = 2.0.^(1:6); + +julia> img = zeros(100); img[20:30] = [exp(-x^2/(2*4^2)) for x=-5:5]; img[50:80] = [exp(-x^2/(2*8^2)) for x=-15:15]; + +julia> blob_LoG(img, σs; edges=false) +2-element Vector{BlobLoG{Float64, Tuple{Float64}, 1}}: + location=CartesianIndex(25,), σ=(4.0,), amplitude=0.10453155018303673 + location=CartesianIndex(65,), σ=(8.0,), amplitude=0.046175719034527364 +``` + +The other two are centered in their corresponding "features," and the width `σ` +reflects the width of the feature itself. + +`blob_LoG` tends to work best for shapes that are "Gaussian-like" but does +generalize somewhat. + # Citation: Lindeberg T (1998), "Feature Detection with Automatic Scale Selection", @@ -38,72 +69,90 @@ International Journal of Computer Vision, 30(2), 79–116. See also: [`BlobLoG`](@ref). """ -function blob_LoG(img::AbstractArray{T,N}, σscales::Union{AbstractVector,Tuple}; - edges::Union{Bool,Tuple{Vararg{Bool}}}=(true, ntuple(d->false, Val(N))...), σshape=ntuple(d->1, Val(N))) where {T,N} +function blob_LoG(img::AbstractArray{T,N}, σscales; + edges::Union{Bool,Tuple{Bool,Vararg{Bool,N}}}=(true, ntuple(d->false, Val(N))...), + σshape::NTuple{N,Real}=ntuple(d->1, Val(N)), + rthresh::Real=1//1000) where {T<:Union{AbstractGray,Real},N} if edges isa Bool edges = (edges, ntuple(d->edges,Val(N))...) end sigmas = sort(σscales) - img_LoG = Array{Float64}(undef, length(sigmas), size(img)...) + img_LoG = multiLoG(img, sigmas, σshape) + maxima = findlocalmaxima(img_LoG; edges=edges) + # The "density" should not be much smaller than 1/volume of the Gaussian + if !iszero(rthresh) + athresh = rthresh./(sigmas.^N .* prod(σshape)) + imgmax = maximum(abs, img) + [BlobLoG(CartesianIndex(tail(x.I)), sigmas[x[1]].*σshape, img_LoG[x]) for x in maxima if img_LoG[x] > athresh[x[1]]*imgmax] + else + [BlobLoG(CartesianIndex(tail(x.I)), sigmas[x[1]].*σshape, img_LoG[x]) for x in maxima] + end +end + +function multiLoG(img::AbstractArray{T,N}, sigmas, σshape) where {T,N} + issorted(sigmas) || error("sigmas must be sorted") + img_LoG = similar(img, float(eltype(T)), (Base.OneTo(length(sigmas)), axes(img)...)) colons = ntuple(d->Colon(), Val(N)) - @inbounds for isigma in eachindex(sigmas) - img_LoG[isigma,colons...] = (-sigmas[isigma]) * imfilter(img, Kernel.LoG(ntuple(i->sigmas[isigma]*σshape[i],Val(N)))) + @inbounds for (isigma, σ) in enumerate(sigmas) + LoG_slice = @view img_LoG[isigma, colons...] + imfilter!(LoG_slice, img, Kernel.LoG(ntuple(i->σ*σshape[i], Val(N))), "reflect") + LoG_slice .*= -σ end - maxima = findlocalmaxima(img_LoG; dims=1:ndims(img_LoG), edges=edges) - [BlobLoG(CartesianIndex(tail(x.I)), sigmas[x[1]], img_LoG[x]) for x in maxima] + return img_LoG end +default_window(img) = (cs = coords_spatial(img); ntuple(d -> d ∈ cs ? 3 : 1, ndims(img))) """ - findlocalmaxima(img; dims=coords_spatial(img), edges=true) -> Vector{CartesianIndex} + findlocalmaxima(img; window=default_window(img), edges=true) -> Vector{CartesianIndex} Returns the coordinates of elements whose value is larger than all of -their immediate neighbors. `dims` is a list of dimensions to -consider. `edges` is a boolean specifying whether to include the +their immediate neighbors. `edges` is a boolean specifying whether to include the first and last elements of each dimension, or a tuple-of-Bool specifying edge behavior for each dimension separately. + +The `default_window` is 3 for each spatial dimension of `img`, and 1 otherwise, implying +that maxima are detected over nearest-neighbors in each spatial "slice" by default. """ -findlocalmaxima(img::AbstractArray; dims=coords_spatial(img), edges=true) = - findlocalextrema(img, dims, edges, Base.Order.Forward) +findlocalmaxima(img::AbstractArray; window=default_window(img), edges=true) = + findlocalextrema(>, img, window, edges) """ - findlocalminima(img; dims=coords_spatial(img), edges=true) -> Vector{CartesianIndex} + findlocalminima(img; window=default_window(img), edges=true) -> Vector{CartesianIndex} Like [`findlocalmaxima`](@ref), but returns the coordinates of the smallest elements. """ -findlocalminima(img::AbstractArray; dims=coords_spatial(img), edges=true) = - findlocalextrema(img, dims, edges, Base.Order.Reverse) +findlocalminima(img::AbstractArray; window=default_window(img), edges=true) = + findlocalextrema(<, img, window, edges) -findlocalextrema(img::AbstractArray{T,N}, dims, edges::Bool, order) where {T,N} = findlocalextrema(img, dims, ntuple(d->edges,Val(N)), order) +findlocalextrema(f, img::AbstractArray{T,N}, window, edges::Bool) where {T,N} = findlocalextrema(f, img, window, ntuple(d->edges,Val(N))) -function findlocalextrema(img::AbstractArray{T,N}, dims, edges::NTuple{N,Bool}, order::Base.Order.Ordering) where {T<:Union{Gray,Number},N} - dims ⊆ 1:ndims(img) || throw(ArgumentError("invalid dims")) - extrema = Array{CartesianIndex{N}}(undef, 0) +function findlocalextrema(f::F, img::AbstractArray{T,N}, window::Dims{N}, edges::NTuple{N,Bool}) where {F,T<:Union{Gray,Number},N} + extrema = Vector{CartesianIndex{N}}(undef, 0) Iedge = CartesianIndex(map(!, edges)) R0 = CartesianIndices(img) R = clippedinds(R0, Iedge) - I1 = _oneunit(first(R0)) - Rinterior = clippedinds(R0, I1) - iregion = CartesianIndex(ntuple(d->d ∈ dims, Val(N))) - Rregion = CartesianIndices(map((f,l)->f:l,(-iregion).I, iregion.I)) - z = zero(iregion) + halfwindow = CartesianIndex(map(x -> x >> 1, window)) + Rinterior = clippedinds(R0, halfwindow) + Rwindow = _colon(-halfwindow, halfwindow) + z = zero(halfwindow) for i in R isextrema = true img_i = img[i] if i ∈ Rinterior # If i is in the interior, we don't have to worry about i+j being out-of-bounds - for j in Rregion + for j in Rwindow j == z && continue - if !Base.Order.lt(order, img[i+j], img_i) + if !f(img_i, img[i+j]) isextrema = false break end end else - for j in Rregion + for j in Rwindow (j == z || i+j ∉ R0) && continue - if !Base.Order.lt(order, img[i+j], img_i) + if !f(img_i, img[i+j]) isextrema = false break end diff --git a/test/extrema.jl b/test/extrema.jl index 1fe987ff..93d3b491 100644 --- a/test/extrema.jl +++ b/test/extrema.jl @@ -2,12 +2,12 @@ @testset "local extrema" begin A = zeros(Int, 9, 9); A[[1:2;5],5].=1 @test findlocalmaxima(A) == [CartesianIndex((5,5))] - @test findlocalmaxima(A; dims=2) == [CartesianIndex((1,5)),CartesianIndex((2,5)),CartesianIndex((5,5))] - @test findlocalmaxima(A; dims=2, edges=false) == [CartesianIndex((2,5)),CartesianIndex((5,5))] + @test findlocalmaxima(A; window=(1,3)) == [CartesianIndex((1,5)),CartesianIndex((2,5)),CartesianIndex((5,5))] + @test findlocalmaxima(A; window=(1,3), edges=false) == [CartesianIndex((2,5)),CartesianIndex((5,5))] A = zeros(Int, 9, 9, 9); A[[1:2;5],5,5].=1 @test findlocalmaxima(A) == [CartesianIndex((5,5,5))] - @test findlocalmaxima(A; dims=2) == [CartesianIndex((1,5,5)),CartesianIndex((2,5,5)),CartesianIndex((5,5,5))] - @test findlocalmaxima(A,dims=2, edges=false) == [CartesianIndex((2,5,5)),CartesianIndex((5,5,5))] + @test findlocalmaxima(A; window=(1,3,1)) == [CartesianIndex((1,5,5)),CartesianIndex((2,5,5)),CartesianIndex((5,5,5))] + @test findlocalmaxima(A, window=(1,3,1), edges=false) == [CartesianIndex((2,5,5)),CartesianIndex((5,5,5))] A = zeros(Int, 9, 9); A[[1:2;5],5].=-1 @test findlocalminima(A) == [CartesianIndex((5,5))] end @@ -18,7 +18,7 @@ @test length(blobs) == 1 blob = blobs[1] @test blob.amplitude ≈ 0.3183098861837907 - @test blob.σ === 1.0 + @test blob.σ === (1.0, 1.0) @test blob.location == CartesianIndex((5,5)) @test blob_LoG(A, [1.0]) == blobs @test blob_LoG(A, [1.0]; edges=(true, false, false)) == blobs @@ -42,14 +42,14 @@ @test blobs[1].location == CartesianIndex((5,5,5)) # kinda anisotropic image A = zeros(Int,9,9,9); A[5,4:6,5] .= 1; - blobs = blob_LoG(A,2 .^ [1.,0,0.5], σshape=[1.,3.,1.]) + blobs = blob_LoG(A,2 .^ [1.,0,0.5], σshape=(1.,3.,1.)) @test length(blobs) == 1 @test blobs[1].location == CartesianIndex((5,5,5)) A = zeros(Int,9,9,9); A[1,1,4:6] .= 1; - blobs = filter(b->b.amplitude > 0.1, blob_LoG(A, 2.0.^[0.5,0,1], edges=true, σshape=[1.,1.,3.])) + blobs = filter(b->b.amplitude > 0.1, blob_LoG(A, 2.0.^[0.5,0,1], edges=true, σshape=(1.,1.,3.))) @test length(blobs) == 1 @test blobs[1].location == CartesianIndex((1,1,5)) - @test filter(b->b.amplitude > 0.1, blob_LoG(A, 2.0.^[0.5,0,1], edges=(true, true, true, false), σshape=[1.,1.,3.])) == blobs - @test isempty(blob_LoG(A, 2.0.^[0,1], edges=(false, true, false, false), σshape=[1.,1.,3.])) + @test filter(b->b.amplitude > 0.1, blob_LoG(A, 2.0.^[0.5,0,1], edges=(true, true, true, false), σshape=(1.,1.,3.))) == blobs + @test isempty(blob_LoG(A, 2.0.^[0,1], edges=(false, true, false, false), σshape=(1.,1.,3.))) end end From 88a9e89765b060ea8ef80f6a0b590420c82bdbeb Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Wed, 1 Sep 2021 16:55:08 -0500 Subject: [PATCH 3/3] Fix & test IO; test `rthresh` --- src/extrema.jl | 2 +- test/extrema.jl | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/extrema.jl b/src/extrema.jl index b75b89e9..0eb02b5e 100644 --- a/src/extrema.jl +++ b/src/extrema.jl @@ -18,7 +18,7 @@ end BlobLoG(; location, σ, amplitude) = BlobLoG(location, σ, amplitude) function Base.show(io::IO, bl::BlobLoG) - print(io, "location=", bl.location, ", σ=", bl.σ, ", amplitude=", bl.amplitude) + print(io, "BlobLoG(location=", bl.location, ", σ=", bl.σ, ", amplitude=", bl.amplitude, ")") end diff --git a/test/extrema.jl b/test/extrema.jl index 93d3b491..92bb20ed 100644 --- a/test/extrema.jl +++ b/test/extrema.jl @@ -20,6 +20,9 @@ @test blob.amplitude ≈ 0.3183098861837907 @test blob.σ === (1.0, 1.0) @test blob.location == CartesianIndex((5,5)) + str = sprint(print, blob) + @test occursin("σ=$((1.0, 1.0))", str) + @test eval(Meta.parse(str)) == blob @test blob_LoG(A, [1.0]) == blobs @test blob_LoG(A, [1.0]; edges=(true, false, false)) == blobs @test isempty(blob_LoG(A, [1.0]; edges=false)) @@ -51,5 +54,6 @@ @test blobs[1].location == CartesianIndex((1,1,5)) @test filter(b->b.amplitude > 0.1, blob_LoG(A, 2.0.^[0.5,0,1], edges=(true, true, true, false), σshape=(1.,1.,3.))) == blobs @test isempty(blob_LoG(A, 2.0.^[0,1], edges=(false, true, false, false), σshape=(1.,1.,3.))) + @test length(blob_LoG([zeros(10); 1.0; 0.0], [4]; edges=true, rthresh=0)) > length(blob_LoG([zeros(10); 1.0; 0.0], [4]; edges=true)) end end