-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
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
base: main
Are you sure you want to change the base?
Conversation
Applies Phansalkar local threshold to an array. Phansalkar is a modification of Sauvola technique to deal with low contrast images.
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
There was a problem hiding this 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.
format improvement Co-authored-by: Lars Grüter <lagru@users.noreply.github.com>
Added some empty lines for readability
There was a problem hiding this 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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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()
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
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()
@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 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. |
@jni I added test functions according to similar filter functions and they seem to work properly. |
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)}) |
There was a problem hiding this comment.
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.
skimage/filters/thresholding.py
Outdated
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) | ||
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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__ |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Compare functions with "is" instead of "==".
@kleinerELM I took the liberty to commit two small suggestions. You should be able to update your local branch with |
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():
Checklist
./benchmarks
, if your changes aren't covered by anexisting benchmark
./doc/examples
(new features only)For reviewers
later.
__init__.py
.doc/release/release_dev.rst
.