Skip to content

Commit

Permalink
Updates API and removes DataFramesMeta from dependency
Browse files Browse the repository at this point in the history
  • Loading branch information
zygmuntszpak committed Mar 18, 2020
1 parent 690c9ca commit a054c16
Show file tree
Hide file tree
Showing 12 changed files with 253 additions and 119 deletions.
24 changes: 12 additions & 12 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
name = "ImageComponentAnalysis"
uuid = "d9b9e9a0-1569-11e9-2cb5-bbca914b0e89"
authors = ["Dr. Zygmunt L. Szpak <[email protected]>"]
version = "0.1.0"
version = "0.2.0"

[deps]
AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c"
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
DataFramesMeta = "1313f7d8-7da2-5740-9ea0-a2ca25f37964"
DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
ImageFiltering = "6a3955dd-da59-5b1f-98d4-e7296123deb5"
LeftChildRightSiblingTrees = "1d6d02ad-be62-4b6b-8a6d-2f90e265016e"
Expand All @@ -17,23 +16,24 @@ PlanarConvexHulls = "145d500b-351c-58b3-a0aa-f5d7e249d989"
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"

[compat]
DataFrames = "^0.19.4"
DataFramesMeta = "^0.5.0"
DataStructures = "^0.17.3"
ImageFiltering = "^0.6.5"
PlanarConvexHulls = "^0.3.0"
StaticArrays = "0.11.1, ^0.12.0"
julia = "1.1.0, ^1"
AbstractTrees = "0.3"
DataFrames = "0.19.4, 0.20"
DataStructures = "0.17.3, 0.17"
ImageFiltering = "0.6.5, 0.6"
LeftChildRightSiblingTrees = "0.1"
OffsetArrays = "0.11.3, 1"
Parameters = "0.12.0"
PlanarConvexHulls = "0.3.0"
StaticArrays = "0.11.1, 0.12"
julia = "1.1.0, 1"

[extras]
AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c"
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
DataFramesMeta = "1313f7d8-7da2-5740-9ea0-a2ca25f37964"
ImageCore = "a09fc81d-aa75-5fe9-8630-4744c3626534"
ImageMagick = "6218d12a-5da1-5696-b52f-db25d2ecc6d1"
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
TestImages = "5e47fb64-e119-507b-a336-dd2b206d9990"

[targets]
test = ["AbstractTrees", "ImageCore", "DataFrames", "DataFramesMeta", "Test", "TestImages", "StaticArrays"]
test = ["AbstractTrees", "ImageCore", "DataFrames", "Test", "StaticArrays"]
103 changes: 74 additions & 29 deletions src/ComponentAnalysisAPI/component_analysis.jl
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,29 @@ g = BoundingBox(area = true)
analyze_components!(measurements, components, g)
```
Most algorithms receive additional information as an argument such as `area` or
`perimeter` of `BasicMeasurement`.
You can run a sequence of analyses by passing a tuple
of the relevant algorithms. For example,
```julia
# determine the connected components and label them
components = label_components(binary_image)
# generate algorithm instances
p = Contour()
q = MinimumOrientedBoundingBox(oriented_box_aspect_ratio = false)
r = EllipseRegion(semiaxes = true)
# then pass the algorithm to `analyze_components`
measurements = analyze_components(components, tuple(p, q, r))
# or use in-place version `analyze_components!`
analyze_components!(measurements, components, tuple(p, q, r))
```
Most algorithms receive additional information as an argument, such as `area` or
`perimeter` of `BasicMeasurement`. In general, arguments are boolean flags
that signal whether or not to include a particular feature in the analysis.
```julia
# you can explicit specify whether or not you wish to report certain
Expand All @@ -46,44 +67,58 @@ For more examples, please check [`analyze_components`](@ref),
abstract type AbstractComponentAnalysisAlgorithm <: AbstractComponentAnalysis end

analyze_components!(out::AbstractDataFrame,
labels::AbstractArray{<:Integer},
f::AbstractComponentAnalysisAlgorithm,
args...; kwargs...) =
f(out, labels, args...; kwargs...)
labels::AbstractArray{<:Integer},
f::AbstractComponentAnalysisAlgorithm,
args...; kwargs...) = f(out, labels, args...; kwargs...)


analyze_components(labels::AbstractArray{<:Integer},
f::AbstractComponentAnalysisAlgorithm,
args...; kwargs...) =
analyze_components!(DataFrame(l = Base.OneTo(maximum(labels))), labels, f, args...; kwargs...)

function analyze_components(labels::AbstractArray{<:Integer},
f::AbstractComponentAnalysisAlgorithm,
args...; kwargs...)

# Handle instance where the input is several component analysis algorithms.
# analyze_components!(out::AbstractDataFrame,
# labels::AbstractArray{<:Integer},
# f::Tuple{AbstractComponentAnalysisAlgorithm ,Vararg{AbstractComponentAnalysisAlgorithm}},
# args...; kwargs...) =
# f(out, labels, args...; kwargs...) # TODO rethink this...
out = DataFrame(l = Base.OneTo(maximum(labels)))
analyze_components!(out, labels, f, args...; kwargs...)
return out
end


# function analyze_components(labels::AbstractArray{<:Integer},
# f::Tuple{AbstractComponentAnalysisAlgorithm ,Vararg{AbstractComponentAnalysisAlgorithm}},
# args...; kwargs...)
# analyze_components!(DataFrame(l = Base.OneTo(maximum(labels))), labels, f, args...; kwargs...)
# end

# Handle instance where the input is several component analysis algorithms.
function analyze_components!(out::AbstractDataFrame,
labels::AbstractArray{<:Integer},
fs::Tuple{AbstractComponentAnalysisAlgorithm ,Vararg{AbstractComponentAnalysisAlgorithm}},
args...; kwargs...)
for f in fs
f(out, labels, args...; kwargs...)
end
return nothing
end


function analyze_components(labels::AbstractArray{<:Integer},
f::Tuple{AbstractComponentAnalysisAlgorithm ,Vararg{AbstractComponentAnalysisAlgorithm}},
args...; kwargs...)
df = DataFrame(l = Base.OneTo(maximum(labels)))
analyze_components!(df, labels, f, args...; kwargs...)
return df
end

### Docstrings

"""
analyze_components!(dataframe::AbstractDataFrame, components::AbstractArray{<:Integer}, f::AbstractComponentAnalysisAlgorithm, args...; kwargs...)
analyze_components!(dataframe::AbstractDataFrame, components::AbstractArray{<:Integer}, fs::Tuple{AbstractComponentAnalysisAlgorithm, Vararg{AbstractComponentAnalysisAlgorithm}}, args...; kwargs...)
Analyze connected components using component analysis algorithm `f` and store
the results in a `DataFrame`.
Analyze connected components using component analysis algorithm `f` or sequence
of algorithms specified in a tuple `fs`, and store the results in a `DataFrame`.
# Output
The `DataFrame` will be changed in place and its columns will store the measurements
that algorithm `f` computes.
The information about the components is stored in a `DataFrame`; each
row number contains information corresponding to a particular connected component.
The `DataFrame` will be changed in place and its columns will store the
measurements that algorithm `f` or algorithms `fs` computes.
# Examples
Expand All @@ -93,6 +128,9 @@ Just simply pass an algorithm to `analyze_components!`:
df = DataFrame()
f = BasicMeasurement()
analyze_components!(df, components, f)
fs = tuple(RegionEllipse(), Contour())
analyze_components!(df, components, fs)
```
See also: [`analyze_components`](@ref)
Expand All @@ -101,13 +139,17 @@ analyze_components!

"""
analyze_components(components::AbstractArray{<:Integer}, f::AbstractComponentAnalysisAlgorithm, args...; kwargs...)
analyze_components(components::AbstractArray{<:Integer}, f::Tuple{AbstractComponentAnalysisAlgorithm, Vararg{AbstractComponentAnalysisAlgorithm}}, args...; kwargs...)
Analyze connected components using algorithm `f`.
Analyze connected components using algorithm `f` or sequence
of algorithms specified in a tuple `fs`, and store the results in a `DataFrame`.
# Output
The information about the components is stored in a `DataFrame`; each
row number contains information corresponding to a particular connected component.
The information about the components is stored in a `DataFrame`; each row number
contains information corresponding to a particular connected component. The
columns of the `DataFrame` will store the measurements that algorithm `f` or
algorithms `fs` computes.
# Examples
Expand All @@ -117,9 +159,12 @@ to `analyze_component`.
```julia
f = BasicMeasurement()
analyze_components = analyze_component(components, f)
fs = tuple(RegionEllipse(), Contour())
analyze_components!(df, components, fs)
```
This reads as "`analyze_components` of connected `components` using
The first example reads as "`analyze_components` of connected `components` using
algorithm `f`".
See also [`analyze_components!`](@ref) for appending information about connected
Expand Down
1 change: 0 additions & 1 deletion src/ImageComponentAnalysis.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ using AbstractTrees
# Used in generic_labelling.jl to allow @nexprs macros.
using Base.Cartesian
using DataFrames
using DataFramesMeta
using ImageFiltering: padarray, Fill
using LeftChildRightSiblingTrees
using LinearAlgebra
Expand Down
19 changes: 11 additions & 8 deletions src/algorithms/basic_measurement.jl
Original file line number Diff line number Diff line change
Expand Up @@ -46,25 +46,28 @@ function(f::BasicMeasurement)(df::AbstractDataFrame, labels::AbstractArray{<:Int
present_symbols = names(df)
required_symbols = [Symbol("Q₀"), Symbol("Q₁"), Symbol("Q₂"), Symbol("Q₃"), Symbol("Q₄"), Symbol("Qₓ")]
has_bitcodes = all(map( x-> x in present_symbols, required_symbols))
out = (!has_bitcodes) ? fill_properties(f, append_bitcodes(df, labels, nrow(df))) : fill_properties(f, df)
!(has_bitcodes) ? append_bitcodes!(df, labels, nrow(df)) : nothing
fill_properties!(df, f)
return nothing
end

function fill_properties(property::BasicMeasurement, df₀::AbstractDataFrame)
df₁ = property.area ? compute_area(df) : df₀
df₂ = property.perimeter ? compute_perimeter(df) : df₁
function fill_properties!(df::AbstractDataFrame, property::BasicMeasurement)
property.area ? compute_area!(df) : nothing
property.perimeter ? compute_perimeter!(df) : nothing
end

function compute_area(df::AbstractDataFrame)
function compute_area!(df::AbstractDataFrame)
# Equation 18.2-8a
# Pratt, William K., Digital Image Processing, New York, John Wiley & Sons, Inc., 1991, p. 629.
@transform(df, area = ((1/4)*:Q₁ + (1/2)*:Q₂ + (7/8)*:Q₃ + :Q₄ +(3/4)*:Qₓ))
df[!, :area] = ((1/4)*df.Q₁ + (1/2)*df.Q₂ + (7/8)*df.Q₃ + df.Q₄ +(3/4)*df.Qₓ)
end

function compute_perimeter(df::AbstractDataFrame)
function compute_perimeter!(df::AbstractDataFrame)
# perimiter₀ and perimter₁ are given by equations 18.2-8b and 18.2-7a in [1].
# perimeter₂ = (:Q₂ + (:Q₁ +:Q₃)/sqrt(2)) is given by equation (32) in [2]
# and is equivalent to perimiter₀.
# [1] Pratt, William K., Digital Image Processing, New York, John Wiley & Sons, Inc., 1991, p. 629.
# [2] S. B. Gray, “Local Properties of Binary Images in Two Dimensions,” IEEE Transactions on Computers, vol. C–20, no. 5, pp. 551–561, May 1971. https://doi.org/10.1109/t-c.1971.223289
@transform(df, perimeter₀ = (:Q₂ + (1/sqrt(2))*(:Q₁ + :Q₃ + 2*:Qₓ)), perimeter₁ = (:Q₁ + :Q₂ + :Q₃ + 2*:Qₓ))
df[!, :perimeter₀] = df.Q₂ + (1/sqrt(2))*(df.Q₁ + df.Q₃ + 2*df.Qₓ)
df[!, :perimeter₁] = df.Q₁ + df.Q₂ + df.Q₃ + 2*df.Qₓ
end
19 changes: 11 additions & 8 deletions src/algorithms/basic_topology.jl
Original file line number Diff line number Diff line change
Expand Up @@ -43,20 +43,23 @@ function(f::BasicTopology)(df::AbstractDataFrame, labels::AbstractArray{<:Intege
present_symbols = names(df)
required_symbols = [Symbol("Q₀"), Symbol("Q₁"), Symbol("Q₂"), Symbol("Q₃"), Symbol("Q₄"), Symbol("Qₓ")]
has_bitcodes = all(map( x-> x in present_symbols, required_symbols))
out = (!has_bitcodes) ? fill_properties(f, append_bitcodes(df, labels, nrow(df))) : fill_properties(f, df)
!(has_bitcodes) ? append_bitcodes!(df, labels, nrow(df)) : nothing
fill_properties!(df, f)
return nothing
end

function fill_properties(property::BasicTopology, df₀::AbstractDataFrame)
df₁ = property.holes ? determine_holes(df) : df₀
df₂ = property.euler_number ? compute_euler_number(df) : df₁
function fill_properties!(df::AbstractDataFrame, property::BasicTopology)
property.holes ? determine_holes!(df) : nothing
property.euler_number ? compute_euler_number!(df) : nothing
end

function compute_euler_number(df::AbstractDataFrame)
@transform(df, euler₄ = (1/4).*(:Q₁ .- :Q₃ .+ (2 .* :Qₓ)), euler₈ = (1/4).*(:Q₁ .- :Q₃ .- (2 .* :Qₓ)))
function compute_euler_number!(df::AbstractDataFrame)
df[!, :euler₄] = (1/4).*(df.Q₁ .- df.Q₃ .+ (2 .* df.Qₓ))
df[!, :euler₈] = (1/4).*(df.Q₁ .- df.Q₃ .- (2 .* df.Qₓ))
end

function determine_holes(df::AbstractDataFrame)
function determine_holes!(df::AbstractDataFrame)
# Number of holes equals the number of connected components (i.e. 1) minus
# the Euler number.
@transform(df, holes = 1 .- (1/4).*(:Q₁ .- :Q₃ .- (2 .* :Qₓ)))
df[!, :holes] = 1 .- (1/4).*(df.Q₁ .- df.Q₃ .- (2 .* df.Qₓ))
end
10 changes: 8 additions & 2 deletions src/algorithms/bitcodes.jl
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
function append_bitcodes(df::AbstractDataFrame, labels::AbstractArray{<:Integer}, N::Int)
function append_bitcodes!(df::AbstractDataFrame, labels::AbstractArray{<:Integer}, N::Int)
# Stores counts of Bit Quad patterns for each component.
𝓠₀ = zeros(Int, N)
𝓠₁ = zeros(Int, N)
Expand All @@ -25,7 +25,13 @@ function append_bitcodes(df::AbstractDataFrame, labels::AbstractArray{<:Integer}
end
end
end
@transform(df, Q₀ = 𝓠₀, Q₁ = 𝓠₁, Q₂ = 𝓠₂, Q₃ = 𝓠₃, Q₄ = 𝓠₄, Qₓ = 𝓠ₓ)
df[!, :Q₀] = 𝓠₀
df[!, :Q₁] = 𝓠₁
df[!, :Q₂] = 𝓠₂
df[!, :Q₃] = 𝓠₃
df[!, :Q₄] = 𝓠₄
df[!, :Qₓ] = 𝓠ₓ
return nothing
end


Expand Down
19 changes: 10 additions & 9 deletions src/algorithms/bounding_box.jl
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,14 @@ Base.@kwdef struct BoundingBox <: AbstractComponentAnalysisAlgorithm
end

function(f::BoundingBox)(df::AbstractDataFrame, labels::AbstractArray{<:Integer})
out = measure_feature(f, df, labels)
measure_feature!(df, labels, f)
return nothing
end

function measure_feature(property::BoundingBox, df::AbstractDataFrame, labels::AbstractArray)
function measure_feature!(df::AbstractDataFrame, labels::AbstractArray, property::BoundingBox)
N = maximum(labels)
init = StepRange(typemax(Int),-1,-typemax(Int))
coords = [(init, init) for i = 1:N]

for i in CartesianIndices(labels)
l = labels[i]
if l != 0
Expand All @@ -48,15 +48,16 @@ function measure_feature(property::BoundingBox, df::AbstractDataFrame, labels::A
coords[l] = rs, cs
end
end
coords_matrix = reshape(reinterpret(StepRange{Int,Int},coords),(2,N))
df₁ = @transform(df, box_indices = coords)
fill_properties(property, df₁)
df[!, :box_indices] = coords
fill_properties!(df, property)
return nothing
end

function fill_properties(property::BoundingBox, df₀::AbstractDataFrame)
df₁ = property.box_area ? compute_box_area(df) : df₀
function fill_properties!(df::AbstractDataFrame, property::BoundingBox)
property.box_area ? compute_box_area(df) : nothing
end

function compute_box_area(df::AbstractDataFrame)
@transform(df, box_area = length.(first.(:box_indices)) .* length.(last.(:box_indices)))
df[!, :box_area] = length.(first.(df.box_indices)) .* length.(last.(df.box_indices))
return nothing
end
14 changes: 6 additions & 8 deletions src/algorithms/contour_topology.jl
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ measurements = analyze_components(components, Contour())
```
# Reference
1. S. Suzuki and K. Abe, “Topological structural analysis of digitized binary images by border following,” Computer Vision, Graphics, and Image Processing, vol. 29, no. 3, p. 396, Mar. 1985.
"""
struct Contour <: AbstractComponentAnalysisAlgorithm
end
Expand All @@ -48,20 +50,16 @@ end

function(f::Contour)(df::AbstractDataFrame, labels::AbstractArray{<:Integer})
N = maximum(labels)

df₁ = DataFrame(l = df.l,
outer_contour = [Vector{CartesianIndex{2}}() for n = 1:N],
hole_contour = [Vector{CartesianIndex{2}}() for n = 1:N])

df[!, :outer_contour] = [Vector{CartesianIndex{2}}() for n = 1:N]
df[!, :hole_contour] = [Vector{CartesianIndex{2}}() for n = 1:N]
tree = establish_contour_hierarchy(labels)
for i in PostOrderDFS(tree)
@unpack id, is_outer, pixels = i.data
if id != 0
is_outer ? df[id,:outer_contour] = pixels : df[id,:hole_contour] = pixels
is_outer ? df[id,:outer_contour] = pixels : df[id,:hole_contour] = pixels
end
end
df₂ = join(df, df₁, on = :l)
return df₂
return nothing
end


Expand Down
Loading

0 comments on commit a054c16

Please sign in to comment.