Search code examples
julialinecontour

Distance between contours in Julia


Hi I'm using contour detection in Julia to detect contours. UPDATE: I need to find closest distance between two contours (made from broken lines).Is there function that finds the closest distance ? I'm not sure whats the best way of joining those two. The case I'm looking at is

enter image description here

so I've used the code given at link example link [https://juliaimages.org/v0.21/democards/examples/contours/contour_detection/] I've contours with n-element Vector{CartesianIndex} datatype. How do I join the line / two contours?

UPDATE: I've tried removing close contour points using the following

  sorted_contours = sort!(contours, by=x -> x[1])
    for i in eachindex(sorted_contours)
        if check_adjacent(sorted_contours[i], sorted_contours) >= 1
            sorted_contours[i] = CartesianIndex.(0, 0)
        end
    end
    deleteat!(sorted_contours,findall(sorted_contours.==CartesianIndex.(0, 0))) but from this [![enter image description here][3]][3] it improved to this only [![enter image description here][3]][3]

I want to remove these points as these are incorrect contour points. Or how could I remove the spur which causes it?


Solution

  • It's the best approach to show your effort and explain where you have the problem, and you'll get the best answers to solve it. Since this topic interested me and made me curious, I found it worthwhile, although your question doesn't meet the requirements of a good question.
    To better detect the contours, First, I perform thinning on the image to locate the contours easily. I use the following image as the input to see the results in a bigger size: enter image description here

    using Images, FileIO, ImageBinarization
    
    img = load("img.png")
    gimg = Gray.(img)
    bin = binarize(gimg, UnimodalRosin()) .> 0.5
    thinned = thinning(bin, algo=GuoAlgo())
    

    The thinned variable represents the following picture:

    julia> Gray.(thinned)
    

    enter image description here

    Now, I define my functions to find the contours:

    """
        check_adjacent(loc::CartesianIndex{2}, all_locs::Vector{CartesianIndex{2}})
    
    Return the number of adjacent locations that are in `all_locs` to `loc`.
    This function is used to determine whether a location is a corner (contour) or not.
    
    # Arguments
    - `loc::CartesianIndex{2}`: the location to check.
    - `all_locs::Vector{CartesianIndex{2}}`: all the locations that aren't black.
    
    # Returns
    - `Int`: the number of adjacent locations that are in `all_locs` to `loc`.
    """
    function check_adjacent(loc::CartesianIndex{2}, all_locs::Vector{CartesianIndex{2}})
        conditions = [
            loc - CartesianIndex(0,1) ∈ all_locs,
            loc + CartesianIndex(0,1) ∈ all_locs,
            loc - CartesianIndex(1,0) ∈ all_locs,
            loc + CartesianIndex(1,0) ∈ all_locs,
            loc - CartesianIndex(1,1) ∈ all_locs,
            loc + CartesianIndex(1,1) ∈ all_locs,
            loc - CartesianIndex(1,-1) ∈ all_locs,
            loc + CartesianIndex(1,-1) ∈ all_locs
        ]
    
        return sum(conditions)
    end
    
    """
        find_the_contour(img::BitMatrix)
    
    Return the contours of the image `img`.
    
    # Arguments
    - `img::BitMatrix`: the image to get the contours of.
    
    # Returns
    - `Vector{CartesianIndex{2}}`: the contours of the image `img`.
    
    """
    function find_the_contour(img::BitMatrix)
        img_matrix = convert(Array{Float64}, img)
        not_black = findall(!=(0.0), img_matrix)
        contours = Vector{CartesianIndex{2}}()
        for nb∈not_black
            t = check_adjacent(nb, not_black)
            t==1 && push!(contours, nb)
        end
        return contours
    end
    

    Now I use the find_the_contour function:

    julia> contours = find_the_contour(thinned)
    4-element Vector{CartesianIndex{2}}:
     CartesianIndex(161, 334)
     CartesianIndex(256, 452)
     CartesianIndex(69, 1213)
     CartesianIndex(210, 1243)
    

    How should I know this function locates the contours correctly? I define another function to highlight the contours:

    """
        highlight_contours(img, contours)
    
    Return the image `img` with the contours `contours` highlighted with red color.
    
    # Arguments
    - `img::BitMatrix`: the image to highlight the contours on.
    - `contours::Vector{CartesianIndex{2}}`: the contours to highlight.
    - `resize`::Bool=false: whether to resize the image to 512x512 or not.
    - `scale`::Union{Int64, Float64}=1: the scale to resize the image to.
    
    # Returns
    - `img_matrix`: the image `img` with the contours `contours` highlighted with red color.
    """
    function highlight_contours(img, contours; resize::Bool=false, scale::Union{Int64, Float64}=1)
        img_matrix = convert(Array{RGB, 2}, img)
        for c∈contours
            img_matrix[c] = RGB(1,0,0)
        end
    
        if resize
            img_matrix = typeof(scale)==Int64 ? imresize(img_matrix, size(img).*scale) : imresize(img_matrix, ratio=scale)
        end
        return img_matrix
    end
    

    Then I pass the thinned Matrix and contours to it:

    julia> highlited = highlight_contours(thinned, contours)
    

    If you zoom in a little bit, you find the contours with red color: enter image description here

    Now that I made sure the contours are located successfully, I go for calculating the distance between contours. I used the Euclidean distance between the CartesianIndexes of each contour! You can adjust it to match your taste:

    """
        distance_between_contours(V::Vector{T}, v::T) where T<:Tuple{Int64, Int64}
    
    Return the euclidean distance between the contour `v` and the contours in `V`.
    
    # Arguments
    - `V::Vector{T}`: the contours to check the distance to.
    - `v::T`: the contour to check the distance from.
    
    # Returns
    - `Float64`: the euclidean distance between the contour `v` and the contours in `V`.
    
    """
    function distance_between_contours(V::Vector{T}, v::T) where T<:Tuple{Int64, Int64}
        return broadcast((x, y)->sqrt(sum((x.-y).^2)), V, Ref(v))
    end
    
    """
        distance_between_contours(all_contours::Vector{CartesianIndex{2}})
    
    Return the euclidean distance between each pair of contours in `all_contours`.
    
    # Arguments
    - `all_contours::Vector{CartesianIndex{2}}`: the contours to calculate the distance between.
    
    # Returns
    - `Matrix{Float64}`: the euclidean distance between each pair of contours in `all_contours`.
    """
    function distance_between_contours(all_contours::Vector{CartesianIndex{2}})
        n = length(all_contours)
        distances = Matrix{Float64}(undef, n, n)
    
        for j in eachindex(all_contours)
            distances[:, j] .= distance_between_contours(
                Tuple.(all_contours),
                Tuple(all_contours[j])
            )
        end
    
        return distances
    end
    

    Now I pass it the contours and calculate the pairwise Euclidean distance between each:

    julia> distance_between_contours(contours)
    4×4 Matrix{Float64}:
       0.0    151.489  883.801  910.32
     151.489    0.0    783.639  792.336
     883.801  783.639    0.0    144.156
     910.32   792.336  144.156    0.0
    

    In this matrix, for example, the 151.489 value shows the distance between the first contour and the second. Remember that the first and second contours are the followings:

     CartesianIndex(161, 334)
     CartesianIndex(256, 452)
    

    I followed the same procedure for your image, and this the followings are the distances and the contours:

    julia> distance_between_contours(contours)
    6×6 Matrix{Float64}:
       0.0      27.7308  88.6397  168.6     166.676    170.188
      27.7308    0.0     60.9262  140.872   139.284    142.622
      88.6397   60.9262   0.0      80.1311   79.2465    82.0061
     168.6     140.872   80.1311    0.0      26.2488    19.2354
     166.676   139.284   79.2465   26.2488    0.0        8.06226
     170.188   142.622   82.0061   19.2354    8.06226    0.0
    
    highlited = highlight_contours(thinned, contours, resize=true, scale=2)
    

    enter image description here enter image description here

    TL;DR

    Code:

    using Images, FileIO, ImageBinarization
    
    function check_adjacent(loc::CartesianIndex{2}, all_locs::Vector{CartesianIndex{2}})
        conditions = [
            loc - CartesianIndex(0,1) ∈ all_locs,
            loc + CartesianIndex(0,1) ∈ all_locs,
            loc - CartesianIndex(1,0) ∈ all_locs,
            loc + CartesianIndex(1,0) ∈ all_locs,
            loc - CartesianIndex(1,1) ∈ all_locs,
            loc + CartesianIndex(1,1) ∈ all_locs,
            loc - CartesianIndex(1,-1) ∈ all_locs,
            loc + CartesianIndex(1,-1) ∈ all_locs
        ]
    
        return sum(conditions)
    end
    
    function find_the_contour(img::BitMatrix)
        img_matrix = convert(Array{Float64}, img)
        not_black = findall(!=(0.0), img_matrix)
        contours = Vector{CartesianIndex{2}}()
        for nb∈not_black
            t = check_adjacent(nb, not_black)
    
            t==1 && push!(contours, nb)
        end
        return contours
    end
    
    function distance_between_contours(V::Vector{T}, v::T) where T<:Tuple{Int64, Int64}
        return broadcast((x, y)->sqrt(sum((x.-y).^2)), V, Ref(v))
    end
    
    function distance_between_contours(all_contours::Vector{CartesianIndex{2}})
        n = length(all_contours)
        distances = Matrix{Float64}(undef, n, n)
    
        for j in eachindex(all_contours)
            distances[:, j] .= distance_between_contours(
                Tuple.(all_contours),
                Tuple(all_contours[j])
            )
        end
    
        return distances
    end
    
    img = load("img.png")
    gimg = Gray.(img)
    bin = binarize(gimg, UnimodalRosin()) .> 0.5
    thinned = thinning(bin, algo=GuoAlgo())
    contours = find_the_contour(thinned)
    distance_between_contours(contours)