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

Add Phansalkar local threshold algorithm #4734

Open
wants to merge 17 commits into
base: main
Choose a base branch
from

Conversation

kleinerELM
Copy link

@kleinerELM kleinerELM commented May 20, 2020

Applies Phansalkar local threshold to an array. Phansalkar is a modification of Sauvola technique to deal with low contrast images.

Description

I need the Phansalkar thresholding filter for low contrast SEM images. I found an implementation in ImageJ, which worked great. But I did not find any mentioning of that filter for scikit. I used threshold_sauvola() as a template.

Source paper:
Phansalskar N. et al. "Adaptive local thresholding for detection of nuclei in diversity stained cytology images.", International Conference on Communications and Signal Processing (ICCSP), pp. 218-220, 2011
DOI: 10.1109/ICCSP.2011.5739305

It should be noted, that the image has to be preprocessed using equalize_adapthist():

from skimage import data
from skimage.exposure import equalize_adapthist
image = data.moon()
image_eq = equalize_adapthist(image)
t_phansalkar = threshold_phansalkar(image_eq, window_size=15, k=0.25, p=2.0, q=10.0)
binary_image = image_eq > t_phansalkar

Checklist

For reviewers

  • Check that the PR title is short, concise, and will make sense 1 year
    later.
  • Check that new functions are imported in corresponding __init__.py.
  • Check that new features, API changes, and deprecations are mentioned in
    doc/release/release_dev.rst.

Applies Phansalkar local threshold to an array. Phansalkar is a
modification of Sauvola technique to deal with low contrast images.
@pep8speaks
Copy link

pep8speaks commented May 20, 2020

Hello @kleinerELM! Thanks for updating this PR. We checked the lines you've touched for PEP 8 issues, and found:

There are currently no PEP 8 issues detected in this Pull Request. Cheers! 🍻

Comment last updated at 2020-05-25 16:21:18 UTC

removed whitespaces, vorrected line lengths and line endings
Copy link
Member

@lagru lagru left a comment

Choose a reason for hiding this comment

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

Thank you for the contribution @kleinerELM! This looks promising and from a first look at the paper and your description this might fit with the other thresholding functions. I'll give this a more thorough review later.

skimage/filters/thresholding.py Outdated Show resolved Hide resolved
skimage/filters/thresholding.py Outdated Show resolved Hide resolved
kleinerELM and others added 2 commits May 20, 2020 18:53
format improvement

Co-authored-by: Lars Grüter <lagru@users.noreply.github.com>
Added some empty lines for readability
Copy link
Member

@rfezzani rfezzani left a comment

Choose a reason for hiding this comment

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

Thank you @kleinerELM for your contribution, I just left some comments ;)

>>> from skimage import data
>>> from skimage.exposure import equalize_adapthist
>>> image = data.moon()
>>> image_eq = equalize_adapthist(image)
Copy link
Member

Choose a reason for hiding this comment

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

Why not include this histogram equalization in the function since it seems that it is required for the method to work?

Copy link
Author

Choose a reason for hiding this comment

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

I just followed the threshold_sauvola() fuction (and most of the other thresholding functions as well). They all return a threshold mask instead of a binary image. (Is there actually any reason for that?)
Since image_eq is necessary to create the correct binary image at the end, the function should best output a binary image directly, or what do you think?

I can see one advantage not to include the histogram equalization into the thresholding function:
If you want to find the best parameters window_size, k, r, p, q for a special image set, it is much faster if you have to calculate equalize_adapthist() only once, instead every time you change a parameter.
For example:

def threshold_phansalkar_parameter_variation( image ):
    #equalize only once
    image_eq = equalize_adapthist(image)
    
    # list of parameters
    k_list = [0.2,0.25,0.3,0.35]
    p_list = [1.5,2.0,2.5,3.0]

    fig, ax = plt.subplots(len(p_list),len(k_list), figsize=(15, 6) )
    fig.subplots_adjust(hspace = .5)
    ax = ax.ravel()

    for j in range(len(p_list)):
        for i in range(len(k_list)):
            # calculate binary mask of thresh_phansalkar() without doing equalize_adapthist() every time
            thresh_phansalkar = threshold_phansalkar(image_eq, window_size=15, k=k_list[i], p=p_list[j], q=10.0)
            binary_phansalkar = image_eq > thresh_phansalkar

            ax[i+j*len(k_list)].imshow(binary_phansalkar, cmap=plt.cm.gray)
            ax[i+j*len(k_list)].set_title( "k = " + str( k_list[i] )+ ", p = " + str( (p_list[j]) ) )
            ax[i+j*len(k_list)].set_axis_off()

    plt.show()

If you think that this is too rare a usecase, then your suggestion would definitely be more user-friendly.

Copy link
Author

Choose a reason for hiding this comment

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

According to @jni the function should return a threshold map. Therefore, I think it is the best to leave the function as is. I documented the requirements of the function in the funtion itself and the example.


if r is None:
imin, imax = dtype_limits(image, clip_negative=False)
r = 0.5 * (imax - imin)
Copy link
Member

Choose a reason for hiding this comment

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

If the histogram equalization is integrated to the function, imax - imin can be replaced by image.ptp()

Copy link
Author

Choose a reason for hiding this comment

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

Thank you for that suggestion.
I tried it for some images. And actually r will allways be 1, if the imput image is a result of the histogram equalization.
Therefore, I would suggest skipping the calculation and set r = 1.0 in that case.

What is the advantage of image.ptp()? I tried to benchmark the function and it seems image.ptp() was a little slower on my system than the current code. But that is negligible in comparison to the thresholding itself.

Copy link
Member

Choose a reason for hiding this comment

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

I think .ptp() it's just a shorthand. I find the current version (imax - imin) slightly more readable so I'd leave it as is personally but I don't mind either way.

kleinerELM and others added 3 commits May 22, 2020 09:56
Co-authored-by: Juan Nunez-Iglesias <juan.nunez-iglesias@monash.edu>
Added 'threshold_phansalkar' to the error message in _validate_window_size() and to the description of _mean_std()
@jni
Copy link
Member

jni commented May 22, 2020

@kleinerELM btw this will need some tests, and I think it would be good to add to e.g. https://scikit-image.org/docs/dev/auto_examples/segmentation/plot_niblack_sauvola.html#sphx-glr-auto-examples-segmentation-plot-niblack-sauvola-py

It should definitely return a threshold map, not a binarised image.

@kleinerELM
Copy link
Author

kleinerELM commented May 22, 2020

@kleinerELM btw this will need some tests, and I think it would be good to add to e.g. https://scikit-image.org/docs/dev/auto_examples/segmentation/plot_niblack_sauvola.html#sphx-glr-auto-examples-segmentation-plot-niblack-sauvola-py

It should definitely return a threshold map, not a binarised image.

Thank you for your patience. I added Phansalkar local threshold to the example and fixed the missing reference in skimage/filters/__init__.py .

While I was researching how to add the test functions I stumbled across the PR #2443 . It seems there was already a discussion including the Phansalkar threshold. Phansalkar was mistyped in that discussion, which is why I did not find that issue in the fist place.
Should I continue to work on the test suite or do we drop this PR? I personally would love to see this filter in Scikit.

@sciunto sciunto changed the title added threshold_phansalkar Add Phansalkar local threshold algorithm May 25, 2020
@kleinerELM
Copy link
Author

@jni I added test functions according to similar filter functions and they seem to work properly.
Please let me know if there's anything more I can do.

Added threshold_local(), threshold_niblack(), threshold_sauvola() and threshold_phansalkar() to the try_all_threshold() function. This may help selecting the best thresold filter for an image.
'Local': thresh(threshold_local),
'Niblack': thresh(threshold_niblack),
'Sauvola': thresh(threshold_sauvola),
'Phansalkar': thresh(threshold_phansalkar)})
Copy link
Author

Choose a reason for hiding this comment

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

Even if Phansalkar will not be accepted, I think it would be helpfull, if the most available filters are included in this function.

Comment on lines 127 to 140
if ( func.__name__ == 'threshold_local' ):
# threshold_local requires the additional parameter 'block_size'
def wrapper(im):
return im > func(im, 15)
elif ( func.__name__ == 'threshold_phansalkar' ):
# threshold_phansalkar requires an equalized image
def wrapper(im):
im_eq = equalize_adapthist(im)
return im_eq > func(im_eq)
else:
# standard case
def wrapper(im):
return im > func(im)

Copy link
Author

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 you would accept this as good style, but it works for thresholding algorithms which require different input variables.

Copy link
Member

Choose a reason for hiding this comment

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

Why not check for the actual function instead of a string comparison? E.g.

if func is threshold_local:
    ...

Also the parenthesis and extra space around the conditional expressions are a bit unconventional. I'm surprised that @pep8speaks isn't bugging you about it.

Copy link
Author

Choose a reason for hiding this comment

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

Thanks for your advice, I changed it.
Actually, the parenthesis and extra space are a quirk of mine.

# standard case
def wrapper(im):
return im > func(im)

try:
wrapper.__orifunc__ = func.__orifunc__
Copy link
Member

Choose a reason for hiding this comment

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

Am I correct in assuming that this line will always raise an AttributeError? At least that's what local testing tells me. Assuming you are the author, @sciunto do you still remember the intention behind this line?

I may be a good idea to use functools.wraps instead; wraps preserves the name of the function and makes the original one accessible through the __wrapped__ attribute.

Copy link
Author

Choose a reason for hiding this comment

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

Can you make a code suggestion? I just followed the existing pattern here. I wanted to avoid breaking anything.

skimage/filters/thresholding.py Outdated Show resolved Hide resolved
skimage/filters/thresholding.py Outdated Show resolved Hide resolved
Compare functions with "is" instead of "==".
@lagru
Copy link
Member

lagru commented May 25, 2020

@kleinerELM I took the liberty to commit two small suggestions. You should be able to update your local branch with git pull origin threshold_phansalkar (in case you didn't know).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants