jni / skan

Python module to analyse skeleton (thin object) images
https://skeleton-analysis.org
BSD 3-Clause "New" or "Revised" License
118 stars 40 forks source link

Possible incorrect classification of branch types #92

Open theo258 opened 4 years ago

theo258 commented 4 years ago

According to the docs the branches are divided into four categories:

  1. endpoint-to-endpoint (isolated branch)
  2. junction-to-endpoint
  3. junction-to-junction
  4. isolated cycle

So when we take a look at the following picture, in the upper left corner we can see a small branch which would be considered as category 2.

test_sk | test_sk2

I'm using the following code to remove branches which are a shorter than a threshold value and are considered as 'junction-to-endpoint' (which worked well for an easier example, see #78). From what I can observe, it seems for me that not all branches belonging to category 2 are correctly classified as such. The attached picture shows my resulting image where e.g., in the upper left corner a branch which is clearly a junction-to-junction branch is removed.

import numpy as np
from skan import csr
from skimage import io
import matplotlib.pyplot as plt

# loading image
sk = io.imread('https://user-images.githubusercontent.com/60721219/90833952-67b11e00-e349-11ea-9e4b-c2ea9ae8c426.png').astype(bool)

# object for all properties of the graph
graph_class = csr.Skeleton(sk)

# get statistics for each branch, especially relevant the length and type of branch here
stats = csr.branch_statistics(graph_class.graph)

# remove all branches which are shorter than the threshold value and a tip-junction (endpoint to junction) branch
thres_min_size = 10
for ii in range(np.size(stats, axis=0)):
    if stats[ii,2] < thres_min_size and stats[ii,3] == 1:
        # remove the branch
        for jj in range(np.size(graph_class.path_coordinates(ii), axis=0)):
            sk[int(graph_class.path_coordinates(ii)[jj,0]), int(graph_class.path_coordinates(ii)[jj,1])] = False

# show resulting image
plt.figure()
plt.imshow(sk)

Resulting image:

result_sk | result_sk2

So my question is if I have overseen something or is this maybe for some reason expected behavior? I'm using version Skan version: 0.8 installed from conda.

jni commented 4 years ago

@theo258 apologies again for the delay in responding. Very cool code that would make a great contribution to skan!

The issue is actually explained in #71 and #72: the path ordering in Skeleton and in the base csgraph code does not match. So rather than use csr.branch_statistics(graph_class.graph), what you need is csr.summarize(graph_class), then grab the 'branch-type' column.

Here is my modified code:

import numpy as np
from skan import csr
from skimage import io, morphology
import matplotlib.pyplot as plt

# loading image
sk = io.imread(
    'https://user-images.githubusercontent.com/60721219/'
    '90833952-67b11e00-e349-11ea-9e4b-c2ea9ae8c426.png'
).astype(bool)
sk_copy = sk.copy()  # in case we want to look at original ;)

# object for all properties of the graph
graph_class = csr.Skeleton(sk)

# get statistics for each branch, especially relevant the length
# and type of branch here
stats = csr.summarize(graph_class)

# remove all branches which are shorter than the threshold value and
# a tip-junction (endpoint to junction) branch
thres_min_size = 10
for ii in range(stats.shape[0]):
    if (stats.loc[ii, 'branch-distance'] < thres_min_size
            and stats.loc[ii, 'branch-type'] == 1):
        # grab NumPy indexing coordinates, ignoring endpoints
        integer_coords = tuple(
            graph_class.path_coordinates(ii)[1:-1].T.astype(int)
        )
        # remove the branch
        sk[integer_coords] = False

# remove isolated endpoints
skclean = morphology.remove_small_objects(sk, min_size=2, connectivity=2)

# show resulting image
plt.figure()
plt.imshow(skclean)
jni commented 4 years ago

I'll leave this open as a reminder to document the discrepancy between the old and new interfaces, but hopefully this lets you move on with your code @theo258!