# How to detect end points in a binarize picture

There is a picture:

When I try to use MorphologicalTransform to detect the end points

pic = Thinning@Binarize@Import["https://i.sstatic.net/K8dm3.png"];
MorphologicalTransform[pic, "EndPoints"]


It will detect five points.But four point is expected.So I wanna find it out like this,but I get nothing

ImageFilter[If[#[[2, 2]] == 1 && Total[#, 2] == 2, 1, 0] &, pic, 1]


Confuse me.But as my comprehension to ImageFilter, I think my code is right.If I wanna use ImageFilter to implement it,how to improve my code?

## UPDATE

In this particular case it is quite sufficient just to apply Pruning with 1 as the second argument:

i = Import["https://i.sstatic.net/K8dm3.png"];
ep = MorphologicalTransform[Pruning[Thinning@Binarize@i, 1], "EndPoints"];
PixelValuePositions[%, White]

{{271, 546}, {190, 471}, {694, 382}, {899, 366}}


The purpose of Pruning is to remove just one pixel which causes additional end point caught by "EndPoints":

PixelValuePositions[
ImageDifference[Thinning@Binarize@i, Pruning[Thinning@Binarize@i, 1]], White]

{{357, 341}}


One approach is to apply Pruning and then take "SkeletonEndPoints":

i = Import["https://i.sstatic.net/K8dm3.png"];
ep1 = MorphologicalTransform[Pruning[Thinning@Binarize@i, 150], "SkeletonEndPoints"]


PixelValuePositions[%, White]

{{271, 546}, {190, 471}, {694, 382}, {899, 366}}


In this particular case we can make Pruning much more efficient if we apply FillingTransform before Thinning:

ep2 = MorphologicalTransform[
Pruning[Thinning@Binarize@FillingTransform@i, 4], "SkeletonEndPoints"];

ep1 == ep2

True


Now we can ensure that we actually have found end points of the thinned image:

ppos = ImageValuePositions[ep2, White]

{{270.5, 545.5}, {189.5, 470.5}, {693.5, 381.5}, {898.5, 365.5}}

pic = Thinning@Binarize@i;
Show[ImageTrim[pic, {#}, 1], GridLines -> {Range[10], Range[10]}, ImageSize -> 100,
Method -> {"GridLinesInFront" -> True}] & /@ ppos


We can check what these pixels correspond to in the original image:

Show[ImageTrim[ReplaceImageValue[i, # -> Red], {#}, 5],
GridLines -> {Range[10], Range[10]}, ImageSize -> 100,
Method -> {"GridLinesInFront" -> True}] & /@ ppos


• I'm really don't know what the Pruning do.But it will lead to a right result. Can you give a explaination?
– yode
Mar 21, 2016 at 15:45
• @yode Citing the Documentation, "Pruning[image,n] removes branches that are at most n pixels long." In the other words, it computes lengths of branches of the skeleton an then "eats up" all branches with length no longer than n pixels. There is no explicit definition for the "length" but the principle seems to be clear. Mar 21, 2016 at 15:50
• Actually I know its usage,but I don't know its function in here.No branch be removed in here I think or I miss other something.We can show the result of Pruning.
– yode
Mar 21, 2016 at 16:05
• Oh.I just mean you first solution here ep1 = MorphologicalTransform[Pruning[Thinning@Binarize@i, 150], "SkeletonEndPoints"].In the second solution its behavior in our expectation.
– yode
Mar 21, 2016 at 16:12
• @yode Pruning here removes a tiny branch (which is the source of the trouble). In the case of ep1 this "branch" is of length one pixel as you can see with ImageDifference[Thinning@Binarize@i, Pruning[Thinning@Binarize@i, 150]], but without Pruning we get this pixel as one of the end points even with "SkeletonEndPoints" (check MorphologicalTransform[Thinning@Binarize@i, "SkeletonEndPoints"]). Mar 21, 2016 at 17:12

As Alexey noted in his follow-up question, there appears to be a bug with range-1 filters on binarised images. That means your original code actually was supposed to work. You can indeed fix it by "unbinarising" the image first:

pic = Thinning@Binarize@Import["https://i.sstatic.net/K8dm3.png"];
nonBinaryPic = Image[pic, "Real"];

filtered = ImageFilter[If[#[[2, 2]] == 1 && Total[#, 2] == 2, 1, 0] &, nonBinaryPic, 1];
Total[ImageData@filtered, 2]
(* 4. *)


You might have to re-binarise filtered after the computation if you want to end up with a binarised image.

Of course this workaround is somewhat unsatisfactory, but at least it lets you use your original approach, which I'd personally prefer to using a similar approach on the binarised image when it's not even clear why the bug exists in the first place.

• (+1) You can convert binary image to grayscale simply with Image[pic, "Real"] or Image[pic, "Real32"]. Mar 21, 2016 at 14:35
• @AlexeyPopkov Ah, thanks. That's what I was looking for. Mar 21, 2016 at 14:36

Imperfect Branch points often are being caught as "EndPoints". You may filter them out like this:

i   = Thinning@Binarize@Import@"https://i.sstatic.net/K8dm3.png";
iep = ImageMultiply[
ColorNegate@ Dilation[MorphologicalTransform[i, "SkeletonBranchPoints"], 15],
MorphologicalTransform[i, "EndPoints"]], 5];

Dilation[iep, 5] (*for visualization *)


• It is certainly a solution by branch points to filter it.But I just don't really like the 15.I think it will result a inaccurate result in some case,such as thebranch points near to the end points.But I don't very affirm my guess.
– yode
Mar 21, 2016 at 15:07

By increasing the radius in your ImageFilter from 1 to 2, I'm able to pick up the endpoints via (with Alexey Popkov's help),

ImageFilter[If[#[[3, 3]] == 1 && Total[#, 2] == 3, 1, 0] &, pic, 2]


• Strictly speaking your approach gives not the end pixels but next-to-end pixels as you can see with ppos=ImageValuePositions[ImageFilter[If[(#[[2,2]]==1||#[[-2,-2]]==1)&&Total[#,2]==2,1,0]&,pic,2],White]; Show[ImageTrim[pic,{#},2],GridLines->{Range[10],Range[10]},ImageSize->100,Method->{"GridLinesInFront"->True}]&/@ppos (output). Mar 21, 2016 at 12:41
• Thank you. I assumed there was a better way to get this, but from the OP I took it they want an answer that uses ImageFilter so I just fiddled with it until it gave the apparently right points. Mar 21, 2016 at 12:44
• Can you explain why the OP's ImageFilter approach doesn't work? I seem to understand how your method work but still don't see why it shouldn't work with radius 1. Mar 21, 2016 at 12:46
• For obtaining the correct end points one can use ImageFilter[If[(#[[3,3]]==1||#[[-3,-3]]==1)&&Total[#,2]==3,1,0]&,pic,2]. Mar 21, 2016 at 13:16
• Excellent, thank you. Actually, with that fix you don't need the Or or the second condition there. But for the life of me I can't see why ImageFilter[If[#[[3, 3]] == 1 && Total[#, 2] == 3, 1, 0] &, pic, 2] works when ImageFilter[If[#[[2, 2]] == 1 && Total[#, 2] == 2, 1, 0] &, pic, 1] fails. I put a Print[MatrixForm@#]; statement in the first and it looks like the second form should work also. Mar 21, 2016 at 13:24