Skip to content

Commit

Permalink
Adds subpixel edge detection and subpixel nonmaxima suppression
Browse files Browse the repository at this point in the history
  • Loading branch information
zygmuntszpak committed Oct 29, 2020
1 parent cda9697 commit 7e2ab98
Show file tree
Hide file tree
Showing 12 changed files with 630 additions and 19 deletions.
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,4 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
TestImages = "5e47fb64-e119-507b-a336-dd2b206d9990"

[targets]
test = ["Test", "TestImages", "ReferenceTests", "FileIO", "ImageDraw", "ImageFiltering", "ImageMagick"]
test = ["Test", "TestImages", "ReferenceTests", "FileIO", "ImageDraw", "ImageFiltering", "ImageMagick"]
1 change: 1 addition & 0 deletions src/EdgeDetectionAPI/EdgeDetectionAPI.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
module EdgeDetectionAPI

using ImageCore # ColorTypes is sufficient
using StaticArrays

# TODO Relax this to all image color types
const GenericGrayImage = AbstractArray{<:Union{Number, AbstractGray}}
Expand Down
120 changes: 116 additions & 4 deletions src/EdgeDetectionAPI/edge_detection.jl
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
# usage example for package developer:
#
# import EdgeDetectionAPI: AbstractEdgeDetectionAlgorithm,
# detect_edges, detect_edges!
# detect_edges, detect_edges!,
# detect_subpixel_edges, detect_subpixel_edges!

"""
AbstractEdgeDetectionAlgorithm <: AbstractImageFilter
The root type for `ImageEdgeDetection` package.
Any concrete edge detection algorithm shall subtype it to support
[`detect_edges`](@ref) and [`detect_edges!`](@ref) APIs.
[`detect_edges`](@ref), [`detect_edges!`](@ref), [`detect_subpixel_edges`](@ref)
and [`detect_subpixel_edges!`](@ref) APIs.
# Examples
Expand All @@ -21,16 +23,37 @@ following pattern:
f = Canny()
# then pass the algorithm to `detect_edges`
img_edges, list_edges = detect_edges(img, f)
img_edges = detect_edges(img, f)
# or use in-place version `detect_edges!`
img_edges = similar(img)
list_edges = detect_edges!(img_edges, img, f)
detect_edges!(img_edges, img, f)
```
For more examples, please check [`detect_edges`](@ref),
[`detect_edges!`](@ref) and concrete algorithms.
One can also detect edges to subpixel accuracy by specifying
`SubpixelNonmaximaSuppression` as the edge thinning algorithm and using
[`detect_subpixel_edges`](@ref) or [`detect_subpixel_edges!`](@ref). The
function returns an edge image as well as a accompanying matrix of length-2
vectors which, when added to the edge image coordinates, specify the location
of an edge to subpixel precision.
```julia
# first generate an algorithm instance
f = Canny(thinning_algorithm = SubpixelNonmaximaSuppression())
# then pass the algorithm to `detect_subpixel_edges`
img_edges, subpixel_offsets = detect_subpixel_edges(img, f)
# or use in-place version `detect_edges!`
img_edges = similar(img)
subpixel_offsets = zeros(SVector{2,Float64}, axes(img))
detect_edges!(img_edges, subpixel_offsets, img, f)
```
"""
abstract type AbstractEdgeDetectionAlgorithm <: AbstractImageFilter end

Expand Down Expand Up @@ -107,6 +130,95 @@ detect_edges(img_sequence::Vector{<:AbstractArray{T}},
args...; kwargs...) where T <: Number =
detect_edges(T, img_sequence, f, args...; kwargs...)


######
detect_subpixel_edges!(out₁::Union{GenericGrayImage, AbstractArray{<:Color3}},
out₂::AbstractArray{<: StaticArray},
img,
f::AbstractEdgeDetectionAlgorithm,
args...; kwargs...) =
f(out₁, out₂, img, args...; kwargs...)

# TODO: Relax this to all color types
function detect_subpixel_edges!(img::Union{GenericGrayImage, AbstractArray{<:Color3}},
f::AbstractEdgeDetectionAlgorithm,
args...; kwargs...)
subpixel_offsets = zeros(SVector{2,Float64}, axes(img))
tmp = copy(img)
f(img, subpixel_offsets, tmp, args...; kwargs...)
return img, subpixel_offsets
end

function detect_subpixel_edges(::Type{T},
img,
f::AbstractEdgeDetectionAlgorithm,
args...; kwargs...) where T
out = similar(Array{T}, axes(img))
subpixel_offsets = zeros(SVector{2,Float64}, axes(img))
detect_subpixel_edges!(out, subpixel_offsets, img, f, args...; kwargs...)
return out, subpixel_offsets
end

function detect_subpixel_edges(::Type{T₁}, ::Type{T₂},
img,
f::AbstractEdgeDetectionAlgorithm,
args...; kwargs...) where {T₁,T₂}
out = similar(Array{T₁}, axes(img))
subpixel_offsets = zeros(SVector{2,T₂}, axes(img))
detect_subpixel_edges!(out, subpixel_offsets, img, f, args...; kwargs...)
return out, subpixel_offsets
end

detect_subpixel_edges(img::AbstractArray{T},
f::AbstractEdgeDetectionAlgorithm,
args...; kwargs...) where T <: Colorant =
detect_subpixel_edges(T, img, f, args...; kwargs...)

# Do not promote Number to Gray{<:Number}
detect_subpixel_edges(img::AbstractArray{T},
f::AbstractEdgeDetectionAlgorithm,
args...; kwargs...) where T <: Number =
detect_subpixel_edges(T, img, f, args...; kwargs...)

# TODO Handle sequences properly in a separate pull-request.

# # Handle instance where the input is a sequence of images.
# detect_subpixel_edges!(out_sequence::Vector{T},
# img_sequence,
# f::AbstractEdgeDetectionAlgorithm,
# args...; kwargs...) where T <: Union{GenericGrayImage, AbstractArray{<:Color3}} =
# f(out_sequence, img_sequence, args...; kwargs...)
#
# # TODO: Relax this to all color types
# function detect_subpixel_edges!(img_sequence::Vector{T},
# f::AbstractEdgeDetectionAlgorithm,
# args...; kwargs...) where T <: Union{GenericGrayImage, AbstractArray{<:Color3}}
# tmp = copy(img_sequence)
# f(img_sequence, tmp, args...; kwargs...)
# return img_sequence
# end

# function detect_subpixel_edges(::Type{T},
# img_sequence::Vector{<:AbstractArray},
# f::AbstractEdgeDetectionAlgorithm,
# args...; kwargs...) where T
# N = length(img_sequence)
# out_sequence = [similar(Array{T}, axes(img_sequence[n])) for n = 1:N]
# detect_subpixel_edges!(out_sequence, img_sequence, f, args...; kwargs...)
# return out_sequence
# end
#
# detect_subpixel_edges(img_sequence::Vector{<:AbstractArray{T}},
# f::AbstractEdgeDetectionAlgorithm,
# args...; kwargs...) where T <: Colorant =
# detect_subpixel_edges(T, img_sequence, f, args...; kwargs...)
#
# # Do not promote Number to Gray{<:Number}
# detect_subpixel_edges(img_sequence::Vector{<:AbstractArray{T}},
# f::AbstractEdgeDetectionAlgorithm,
# args...; kwargs...) where T <: Number =
# detect_subpixel_edges(T, img_sequence, f, args...; kwargs...)

### Docstrings

"""
Expand Down
81 changes: 75 additions & 6 deletions src/EdgeDetectionAPI/edge_thinning.jl
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
# usage example for package developer:
#
# import EdgeDetectionAPI: AbstractEdgeThinningAlgorithm,
# thin_edges, thin_edges!
# thin_edges, thin_edges!,
# thin_subpixel_edges, thin_subpixel_edges!

"""
AbstractEdgeThinningAlgorithm <: AbstractImageFilter
A root type for `ImageEdgeDetection` package.
Any concrete edge thinning algorithm shall subtype it to support
[`thin_edges`](@ref) and [`thin_edges!`](@ref) APIs.
[`thin_edges`](@ref), [`thin_edges!`](@ref), [`thin_subpixel_edges`](@ref) and
[`thin_subpixel_edges!`](@ref) APIs.
# Examples
Expand All @@ -36,6 +38,13 @@ thin_edges!(thinned_edges, mag, g₁, g₂, f)
For more examples, please check [`thin_edges`](@ref),
[`thin_edges!`](@ref) and concrete algorithms.
One can also perform non-maxima suppression to subpixel precision using
[`thin_subpixel_edges`](@ref) and [`thin_subpixel_edges!`](@ref).
The function returns an edge image as well as a accompanying matrix of length-2
vectors which, when added to the edge image coordinates, specify the location
of an edge to subpixel precision.
"""
abstract type AbstractEdgeThinningAlgorithm <: AbstractImageFilter end

Expand All @@ -51,17 +60,36 @@ function thin_edges(::Type{T},
g₂::AbstractArray,
f::AbstractEdgeThinningAlgorithm,
args...; kwargs...) where T
out = zeros(T, axes(mag))
detect_edges!(out, mag, g₁, g₂, f, args...; kwargs...)
return out
out = zeros(T, axes(mag))
return f(out, mag, g₁, g₂, args...; kwargs...)
end

thin_edges!(out::AbstractArray, mag::AbstractArray, g₁::AbstractArray, g₂::AbstractArray,
f::AbstractEdgeThinningAlgorithm,
args...; kwargs...) =
f(out, mag, g₁, g₂, args...; kwargs...)
f(out, mag, g₁, g₂, args...; kwargs...)


thin_subpixel_edges(mag::AbstractArray, g₁::AbstractArray, g₂::AbstractArray,
f::AbstractEdgeThinningAlgorithm,
args...; kwargs...) =
f(zeros(eltype(mag), axes(mag)), zeros(SVector{2,Float64}, axes(mag)), mag, g₁, g₂, args... ; kwargs...)

thin_subpixel_edges!(out₁::AbstractArray, out₂::Matrix{<:AbstractArray}, mag::AbstractArray, g₁::AbstractArray, g₂::AbstractArray,
f::AbstractEdgeThinningAlgorithm,
args...; kwargs...) =
f(out₁, out₂, mag, g₁, g₂, args...; kwargs...)

function thin_subpixel_edges(::Type{T₁}, ::Type{T₂},
mag::AbstractArray,
g₁::AbstractArray,
g₂::AbstractArray,
f::AbstractEdgeThinningAlgorithm,
args...; kwargs...) where {T₁, T₂}
out₁ = zeros(T₁, axes(mag))
out₂ = zeros(T₂, axes(mag))
return f(out₁, out₂, mag, g₁, g₂, args...; kwargs...)
end
### Docstrings

"""
Expand Down Expand Up @@ -144,3 +172,44 @@ thinned_edges_float32 = thin_edges(Gray{Float32}, mag, g₁, g₂, f)
See also [`thin_edges!`](@ref) for in-place edge thinning.
"""
thin_edges


# """
# thin_subpixel_edges!([out₁, out₂,] mag, g₁, g₂, f::AbstractEdgeThinningAlgorithm, args...; kwargs...)
#
# Isolate local maxima of gradient magnitude `mag` along the local gradient
# direction. The arguments `g₁` and `g₂` represent the gradient in the first
# spatial dimension (y), and the second spatial dimension (x), respectively.
#
# # Output
#
# If `out₁` and `out₂` are specified, they will be changed in place. Otherwise `mag` will be changed in place.
#
# # Examples
#
# Just simply pass an algorithm to `thin_edges!`:
#
# ```julia
# using TestImages, ImageFiltering
# img = Gray.(testimage("mandril"))
#
# # Gradient in the first and second spatial dimension
# g₁, g₂ = imgradients(img, KernelFactors.scharr)
#
# # Gradient magnitude
# mag = hypot.(g₁, g₂)
#
# thinned_edges = zeros(eltype(mag), axes(mag))
# thin_edges!(thinned_edges, mag, g₁, g₂, f)
# ```
#
# For cases you just want to change `mag` in place, you don't necessarily need to manually
# allocate `thinned_edges`; just use the convenient method:
#
# ```julia
# thin_edges!(mag, g₁, g₂, f)
# ```
#
# See also: [`thin_edges`](@ref)
# """
# thin_subpixel_edges!
15 changes: 12 additions & 3 deletions src/ImageEdgeDetection.jl
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@ using StatsBase
# TODO: port EdgeDetectionAPI to ImagesAPI
include("EdgeDetectionAPI/EdgeDetectionAPI.jl")
import .EdgeDetectionAPI: AbstractEdgeDetectionAlgorithm,
detect_edges, detect_edges!
detect_edges, detect_edges!,
detect_subpixel_edges, detect_subpixel_edges!

import .EdgeDetectionAPI: AbstractEdgeThinningAlgorithm,
thin_edges, thin_edges!
thin_edges, thin_edges!,
thin_subpixel_edges, thin_subpixel_edges!

# TODO Relax this to all image color types
const GenericGrayImage = AbstractArray{<:Union{Number, AbstractGray}}

Expand All @@ -33,16 +36,22 @@ struct Percentile{T} <: Real
end

include("algorithms/nonmaxima_suppression.jl")
include("algorithms/subpixel_nonmaxima_suppression.jl")
include("algorithms/canny.jl")

export
# main types and functions
Canny,
NonmaximaSuppression,
SubpixelNonmaximaSuppression,
Percentile,
thin_edges,
thin_edges!,
thin_subpixel_edges,
thin_subpixel_edges!,
detect_edges,
detect_edges!
detect_edges!,
detect_subpixel_edges,
detect_subpixel_edges!

end # module
Loading

0 comments on commit 7e2ab98

Please sign in to comment.