Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP]Add skeletonization function #13

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/ImageMorphology.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ module ImageMorphology
using ImageCore
using Images
Copy link
Member

@johnnychen94 johnnychen94 Apr 14, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's using ImageMorphology in Images.jl, and it will make package dependency complicate.

@timholy How do we deal with this problem in general? Can we directly copy Images.FeatureTransform module to ImageMorphology as temporary not-exported functions?
https:/JuliaImages/Images.jl/blob/bdfd044420fa6ffcd34760f804f0c3ce12186945/src/bwdist.jl

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@johnnychen94 another example where we find something implemented in Images.jl that could live somewhere else. We really need to reorganize concepts around and break Images.jl into smaller packages for reuse. Right now a bunch of functionality lives in the umbrella package, and that is suboptimal.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like @Deepank308 needs this function implemented and merged ASAP to continue his GSoC ImageTracking project.

@juliohm I guess we can copy these methods to FeatureTransform.jl (with comments) and remove it in the future, just like @zygmuntszpak does in https:/zygmuntszpak/ImageBinarization.jl/blob/6e0c81867eaef67b463fa5d52febfca4d0f6196a/src/integral_image.jl ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if copying/pasting helps @johnnychen94 , ideally we would start thinking more seriously about how to reorganize things. There is a lot of code living in the wrong place and we are just compromising the situation further by adopting multiple copies in submodules.

Copy link
Member

@johnnychen94 johnnychen94 Apr 17, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might need another two weeks to start the porting work. Let's see how it works then.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@johnnychen94 I don't need this function as of now in my implementations. I was just going through Skeletonization algorithm and found that Julia doesn't have it. So, I implemented and sent a PR.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, this PR might be pended until FeatureTransform being ported to a standalone module.


abstract type SkeletonizationAlgo end
struct MedialAxis <: SkeletonizationAlgo end
abstract type AbstractSkeletonizationAlgorithm end
struct MedialAxisTransform <: AbstractSkeletonizationAlgorithm end


include("dilation_and_erosion.jl")
Expand All @@ -21,7 +21,7 @@ export
bothat,
morphogradient,
morpholaplace,
MedialAxis,
MedialAxisTransform,
skeletonize,
thinning,
GuoAlgo
Expand Down
149 changes: 99 additions & 50 deletions src/skeletonization.jl
Original file line number Diff line number Diff line change
@@ -1,81 +1,130 @@
# checks if element is essential for connectivity
function compute_essential_indices()
essential_index_array = Array{Bool, 1}()
for index = 0:511
append!(essential_index_array, maximum(find_components(index)) != maximum(find_components(index & ~2^4)))
# checks if pixel is an articulation/critical node
function compute_critical_indices(num_configurations::Integer)
critical_index_array = Array{Bool, 1}()
for index = 0:num_configurations - 1
append!(critical_index_array, maximum(find_components(index)) != maximum(find_components(index & ~2^4)))
end
essential_index_array
critical_index_array
end

function find_components(index::Int64)
function find_components(index::Integer)
label_components(reshape(digits(index, base=2, pad=9), (3, 3)), trues(3, 3))
end

function corner_table_lookup(img::AbstractArray{Bool, 2}, corner_table::AbstractArray{Int, 1})
corner_score = zeros(Int, size(img) .+ 2)
# computes degree of cornerness of pixels
function corner_table_lookup(img::AbstractArray{Bool, 2}, corner_table::AbstractArray{T, 1}) where T <: Integer
corner_score = zeros(Integer, size(img) .+ 2)
score_table = [256 128 64; 32 16 8; 4 2 1]

for i = 2:size(img)[1] + 1
for j = 2:size(img)[2] + 1
if img[i - 1, j - 1]
corner_score[i - 1:i + 1, j - 1:j + 1] = corner_score[i - 1:i + 1, j - 1:j + 1] + score_table
end
end
end
end
corner_table[corner_score[2: size(img)[1] + 1, 2: size(img)[2] + 1] .+ 1]
end

function inner_skeleton_loop(img::AbstractArray{Bool, 2}, order::Array{Int, 1}, table::AbstractArray{Bool, 1})
index = findall(img)[order]
score_table = [256 128 64; 32 16 8; 4 2 1]
result = zeros(Int, size(img))
padded_image = zeros(Int, size(img) .+ 2)
padded_image[2: size(img)[1] + 1, 2: size(img)[2] + 1] = img
for i in index
result[i] = table[sum(padded_image[i[1]:i[1] + 2, i[2]:i[2] + 2] .* score_table) + 1]
padded_image[i[1] + 1, i[2] + 1] = result[i]
end
result
end

function skeletonize(img::AbstractArray{Bool}, algo::SkeletonizationAlgo)
skeletonize_impl(img, algo)
# traverse foreground and reduce image to skeleton
function inner_skeleton_loop(img::AbstractArray{Bool, 2}, order::AbstractArray{T, 1}, table::AbstractArray{Bool, 1}) where T <: Integer
index = findall(img)[order]
score_table = [256 128 64; 32 16 8; 4 2 1]
result = falses(size(img))
padded_image = padarray(img, Fill(0, (1, 1), (1, 1)))
for i in index
result[i] = table[sum(padded_image[i[1] - 1:i[1] + 1, i[2] - 1:i[2] + 1] .* score_table) + 1]
padded_image[i[1], i[2]] = result[i]
end
result
end

function skeletonize_impl(img::AbstractArray{Bool, 2}, algo::MedialAxis)
"""
```
skeletonize(img, MedialAxisTransform())
```

Returns Medial axis of binary image that matches the dimensions of the input binary image.

# Details
Skeletonization is a process for reducing foreground regions in a binary image to a
skeletal remnant that largely preserves the extent and connectivity of the original
region while throwing away most of the original foreground pixels.

Steps of the algorithm :
- A look up table is built to check whether a pixel is to be removed or not. The pixel
is removed if it has more than one neighbour and if removing it doesn't change the connectedness.
- Distance transform is computed as well as degree of cornerness.
- Foreground pixels are traversed as per distance transform and cornerness.

# Arguments
The img parameter needs to be a two-dimensional array of bool type where true represents
foreground and false represents background.

# Example

Compute the skeleton of rectangular object.
```julia

using Images, ImageMorphology

img = Bool.([0 0 0 0 0
0 1 1 1 0
0 1 1 1 0
0 1 1 1 0
0 1 1 1 0
0 0 0 0 0])

skeleton = skeletonize(img, MedialAxisTransform())

skeleton = [0 0 0 0 0
0 1 0 1 0
0 0 1 0 0
0 0 1 0 0
0 1 0 1 0
0 0 0 0 0]
```

# References
[1] http://homepages.inf.ed.ac.uk/rbf/HIPR2/skeleton.htm
Copy link
Member

@johnnychen94 johnnychen94 Apr 15, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a title to the reference item for clarity

also here are some thoughts on simplifying documentation of function with multiple methods.

JuliaImages/Images.jl#790 (comment)
JuliaImages/ImageBinarization.jl#25

It'll be great to hear thoughts from you.

Further documentation work can be done after codes being stable.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noted. I will keep that in my mind if any other skeletonization algorithm will be implemented in future?

[2] H. Blum in 1967, and in 1968 by L. Calabi and W. E. Hartnett.
"""
function skeletonize(img::AbstractArray{Bool, 2}, algo::MedialAxisTransform)

# check if array has only 0s
if sum(img) == 0
if all(.~img)
return img
end

# check if array has only 1s
if sum(img) == length(img)
img[1, 1:end] = false
img[1:end - 1, end] = false
img[end, 1:end - 1] = false
return convert(Array{Bool}, .~img)
img[1, 1:end] .= false
img[1:end - 1, end] .= false
img[end, 1:end - 1] .= false
return .~img
end

# possible number of configurations of 3x3 kernel
num_configurations = 512

# build look-up table
patterns = [0:1:511;]
patterns = [0:1:num_configurations - 1;]
num_pixels_in_patterns_kernel = sum.(digits.(patterns, base=2))
center_is_foreground = convert(Array{Bool, 1}, patterns .& 2^4 .> 0)
table = (center_is_foreground .& compute_essential_indices() .| (num_pixels_in_patterns_kernel .< 3))
table = convert(Array{Bool, 1}, table)

# store distance transform
distance = distance_transform(feature_transform(.~img))

# corners handling
corner_table = 9 .- num_pixels_in_patterns_kernel
corner_score = corner_table_lookup(img, corner_table)

# generate order for traversing the shape
dist_corner_pair = CartesianIndex.(round.(Int, distance[img] .* 10), corner_score[img])
first_sort = sortperm(dist_corner_pair, by=x->x[2])
second_sort = sortperm(dist_corner_pair[first_sort], by=x->x[1])
order = first_sort[second_sort]

convert(Array{Bool, 2}, inner_skeleton_loop(img, order, table))
center_is_foreground = patterns .& 2^4 .> 0
table = (center_is_foreground .& compute_critical_indices(num_configurations) .| (num_pixels_in_patterns_kernel .< 3))

# store distance transform
distance = distance_transform(feature_transform(.~img))

# corners handling
corner_table = 9 .- num_pixels_in_patterns_kernel
corner_score = corner_table_lookup(img, corner_table)

# generate order for traversing the shape
dist_corner_pair = CartesianIndex.(round.(Integer, distance[img] .* 10), corner_score[img])
first_sort = sortperm(dist_corner_pair, by=x->x[2])
second_sort = sortperm(dist_corner_pair[first_sort], by=x->x[1])
traversal_order = first_sort[second_sort]

inner_skeleton_loop(img, traversal_order, table)
end
2 changes: 1 addition & 1 deletion test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,6 @@ using Test
0 1 0 1 0
0 0 0 0 0])

@test skeletonize(img, MedialAxis()) == ans
@test skeletonize(img, MedialAxisTransform()) == ans
end
end