From e421cea33122a735ad4f7fb0bacfcca12a4e00b0 Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Tue, 12 Jan 2016 16:20:32 +0000 Subject: [PATCH 01/39] ENH: start adding a python version of Skeletonize3D code --- skimage/morphology/data/3/pngs/1.png | Bin 0 -> 251 bytes skimage/morphology/data/3/pngs/2.png | Bin 0 -> 248 bytes skimage/morphology/data/3/pngs/3.png | Bin 0 -> 184 bytes skimage/morphology/data/3/pngs/5.png | Bin 0 -> 178 bytes skimage/morphology/data/3/pngs/6.png | Bin 0 -> 178 bytes skimage/morphology/data/3/pngs/7.png | Bin 0 -> 176 bytes skimage/morphology/data/3/stack.tif | Bin 0 -> 15187 bytes skimage/morphology/data/3/stack_fiji.tif | Bin 0 -> 15187 bytes skimage/morphology/data/4/1_fiji.png | Bin 0 -> 94 bytes skimage/morphology/data/4/pngs/1.png | Bin 0 -> 149 bytes skimage/morphology/data/4/pngs/2.png | Bin 0 -> 137 bytes skimage/morphology/data/4/pngs/3.png | Bin 0 -> 137 bytes skimage/morphology/data/4/pngs/4.png | Bin 0 -> 137 bytes skimage/morphology/data/4/pngs/5.png | Bin 0 -> 137 bytes skimage/morphology/data/4/pngs/6.png | Bin 0 -> 137 bytes skimage/morphology/data/4/stack.tif | Bin 0 -> 1795 bytes skimage/morphology/data/4/stack_fiji.tif | Bin 0 -> 1795 bytes skimage/morphology/data/cross.png | Bin 0 -> 149 bytes skimage/morphology/data/cross.txt | 24 + skimage/morphology/data/cross_fiji.txt | 24 + skimage/morphology/data/loop.png | Bin 0 -> 184 bytes skimage/morphology/data/loop.txt | 24 + skimage/morphology/data/loop_fiji.txt | 24 + skimage/morphology/data/strip.png | Bin 0 -> 131 bytes skimage/morphology/data/strip.txt | 12 + skimage/morphology/data/strip_fiji.txt | 12 + skimage/morphology/data/two-hole.png | Bin 0 -> 176 bytes skimage/morphology/data/two-hole.txt | 16 + skimage/morphology/data/two-hole_fiji.txt | 16 + skimage/morphology/skel.py | 678 ++++++++++++++++++++++ skimage/morphology/test_simple_2d.py | 84 +++ skimage/morphology/trivial.py | 73 +++ 32 files changed, 987 insertions(+) create mode 100644 skimage/morphology/data/3/pngs/1.png create mode 100644 skimage/morphology/data/3/pngs/2.png create mode 100644 skimage/morphology/data/3/pngs/3.png create mode 100644 skimage/morphology/data/3/pngs/5.png create mode 100644 skimage/morphology/data/3/pngs/6.png create mode 100644 skimage/morphology/data/3/pngs/7.png create mode 100644 skimage/morphology/data/3/stack.tif create mode 100644 skimage/morphology/data/3/stack_fiji.tif create mode 100644 skimage/morphology/data/4/1_fiji.png create mode 100644 skimage/morphology/data/4/pngs/1.png create mode 100644 skimage/morphology/data/4/pngs/2.png create mode 100644 skimage/morphology/data/4/pngs/3.png create mode 100644 skimage/morphology/data/4/pngs/4.png create mode 100644 skimage/morphology/data/4/pngs/5.png create mode 100644 skimage/morphology/data/4/pngs/6.png create mode 100644 skimage/morphology/data/4/stack.tif create mode 100644 skimage/morphology/data/4/stack_fiji.tif create mode 100644 skimage/morphology/data/cross.png create mode 100644 skimage/morphology/data/cross.txt create mode 100644 skimage/morphology/data/cross_fiji.txt create mode 100644 skimage/morphology/data/loop.png create mode 100644 skimage/morphology/data/loop.txt create mode 100644 skimage/morphology/data/loop_fiji.txt create mode 100644 skimage/morphology/data/strip.png create mode 100644 skimage/morphology/data/strip.txt create mode 100644 skimage/morphology/data/strip_fiji.txt create mode 100644 skimage/morphology/data/two-hole.png create mode 100644 skimage/morphology/data/two-hole.txt create mode 100644 skimage/morphology/data/two-hole_fiji.txt create mode 100644 skimage/morphology/skel.py create mode 100644 skimage/morphology/test_simple_2d.py create mode 100644 skimage/morphology/trivial.py diff --git a/skimage/morphology/data/3/pngs/1.png b/skimage/morphology/data/3/pngs/1.png new file mode 100644 index 0000000000000000000000000000000000000000..da940f11a60ff0c8e90ff61a0ffe6d000ad573b8 GIT binary patch literal 251 zcmeAS@N?(olHy`uVBq!ia0vp^Wz#oq6^RZsftubPXO7TIfX ztPA1UWge4v-0y4ChsE0$%~+UycHNec%re7EU*!JC@n^f{dnamtv)-}P)7C%q{dB+L t&vTmHo|WlNlYC~ua5+EwNq+q`_CJqpTPD6SUI}y;gQu&X%Q~loCIGAdUiSb1 literal 0 HcmV?d00001 diff --git a/skimage/morphology/data/3/pngs/2.png b/skimage/morphology/data/3/pngs/2.png new file mode 100644 index 0000000000000000000000000000000000000000..ada13a9ee12f81c9f75611f195dc52270cc6b870 GIT binary patch literal 248 zcmeAS@N?(olHy`uVBq!ia0vp^WU`533hbv~qyzDus+}e6r`e^mMz<_;bE@$^Jho6WudFkV@fBMZ= qmFxaV&D$8h#<}GGMyvn7XY)7c+0A7P650TC6oaR$pUXO@geCy`E?md} literal 0 HcmV?d00001 diff --git a/skimage/morphology/data/3/pngs/3.png b/skimage/morphology/data/3/pngs/3.png new file mode 100644 index 0000000000000000000000000000000000000000..00ee7d1f35fd2a8a5d415ba1be23eaf78328f066 GIT binary patch literal 184 zcmeAS@N?(olHy`uVBq!ia0vp^WviwG%p$wj`elF{r5}E)ar#q|w literal 0 HcmV?d00001 diff --git a/skimage/morphology/data/3/pngs/5.png b/skimage/morphology/data/3/pngs/5.png new file mode 100644 index 0000000000000000000000000000000000000000..8378036e08c6dbb5b5c81e1cea2d8ef961825c35 GIT binary patch literal 178 zcmeAS@N?(olHy`uVBq!ia0vp^W;vq2^c)t; zo-ml*xR5U|i>YPW{{wXlE}NZSSfz2~9{gTe~DWM4f6qGbC literal 0 HcmV?d00001 diff --git a/skimage/morphology/data/3/pngs/6.png b/skimage/morphology/data/3/pngs/6.png new file mode 100644 index 0000000000000000000000000000000000000000..3c6051aedc05befee077d00896d023f62c02f212 GIT binary patch literal 178 zcmeAS@N?(olHy`uVBq!ia0vp^W;vq2^c)t; zo-n#QPqFbQiHHKJLoIHLyB|Li=AOZM<1`P0zAv}Z Unsf7B0gYquboFyt=akR{0A9>Cy8r+H literal 0 HcmV?d00001 diff --git a/skimage/morphology/data/3/pngs/7.png b/skimage/morphology/data/3/pngs/7.png new file mode 100644 index 0000000000000000000000000000000000000000..61eb70015833d281f406b02d04e2fd2c0a720c23 GIT binary patch literal 176 zcmeAS@N?(olHy`uVBq!ia0vp^W;vq2^c)t; zo-n#QPqFbQiHHKJLp^SbyB|Li=H__C%wX_^ThTyN RrT}OdgQu&X%Q~loCIHSVGu!|G literal 0 HcmV?d00001 diff --git a/skimage/morphology/data/3/stack.tif b/skimage/morphology/data/3/stack.tif new file mode 100644 index 0000000000000000000000000000000000000000..28752135debf71383648d205de0b17e81f45646a GIT binary patch literal 15187 zcmeI3%}x|S5XXB^5jV!f81>}UgCdFs{7AUNCVTea2@MjGRY-`BqTYNHuOvKx2M->7 z4Ym5Kx~iwAr)B_yu}F6K*T=7_yJmJ~A=%l?+%v{3nXBfPxd^I(mpw$i-ZZF}n+0>h zg_j^_F=$9`u9*e!--p+i=6cL;9yB*e&aI%AA^&#Fxw3im$?40$9v^*n{Wn4XH0Ifl zt#{kIJJXHTm9!_Zhf&%(;o+j|y{tQ@<%g2KR#N;IUv6M?xFGg~S?7>K; zpG0s}u?UeQ0p?N&Vx`A2Cp8PL`mqRQ=Qg-$d8!k|xhh5>(4<}s^Rck|5=QEv3{V`J zH8JK(B{M0c2$w~ZiHcCjVNld8Vgf3wC)-!Me(IDJBmt0aYupvA?ntFs4eG=kM9yBe zoOalI>b?BVLRA7-nKfW$sG1QXs=!oR#er~+t)^)3{Q7oCfrJrfzqD9?mS*S#bOI-u zfO~%qsF2>z(zhrlg#8MT;D%77`tBnPvXcmo>QEv?k_4DbA&8Y8%be6KwCcwqlnpJo zX?dy>#knd*A<(2=4fC-$>jFmVpbStPnl&-zOC>WYqzIQqlZlE@$YD^_EMfvGt0&u6 zyMF4F6(j+WZfo2XtnNsqSq@cjCANP&bAXTP*qf0kzG1kUpW+{Hg|f#D4^kYYEU0qmF+*6!c6*5Ws`IFWT5 zb;B5So3TSpcDB2CNSpC=@|Mfl8wz{vRsk{7_p>sBqc4cc_*B}sEeTmi7pL7 zc?m~cz4M?&1fklWqdF=6#{#QS{cO(*f`u#L5p?&6ze)8hMZ-D)oq$e2C(xe2nA@~t z$OEEXwdnC!F;sH`RttB;Mp7Ns)P3p`$gT!KA`eVoxQ2IHlG*$KK1at!b(gY@_|E0#s2g#Qjjd}hG pzK3|~U#YzDRoMOeS#-a1N!Ig%pX-K&JzFt literal 0 HcmV?d00001 diff --git a/skimage/morphology/data/3/stack_fiji.tif b/skimage/morphology/data/3/stack_fiji.tif new file mode 100644 index 0000000000000000000000000000000000000000..9373767547c7d86a4192e6072677d61b4daaa679 GIT binary patch literal 15187 zcmeI3%SyvQ6oyagJ%}KpuH3rtQg5_gpnF^B+J&nswG^TiijSi1d=s~V58%RuOJBo! zk~U*5lXhloLR45TdPj}Ry zLlGT`s34-Eh>k_X4RC;f7J&{|yQk{WagT1X|9g-$TBl?XBSkx6PKsnE*Yfl5tGq?IsqFq8FB^_eoHkk~PC!ULByo0RX; zPMA$di#sHpmnMb84dNBj=Q*T_s>HSvsfrDKv%ifhRw7BTN>9-3;hRZ1A_W2+6gko~VPY9BL|YIoFu00@8p2uwHu ze2h%^4Ib6yMIOA+Ffm|s00ck)1VCW){}hIMG~NUf|EDGLT`4jg??vP4K>!3m z00ck)1VA7|0{9-4A#7NI00<11fIa+|8tn&_ml}yqFZn&hp}$hO@~Y_mnTGqujGFt| UMH=p#RYmuYG~D+d)!d1G0j7(b{r~^~ literal 0 HcmV?d00001 diff --git a/skimage/morphology/data/4/1_fiji.png b/skimage/morphology/data/4/1_fiji.png new file mode 100644 index 0000000000000000000000000000000000000000..03a7b79a4570ba4c6a89d04c3bc351c88bfcb1e3 GIT binary patch literal 94 zcmeAS@N?(olHy`uVBq!ia0vp^JRr;gBp8b2n5}`7s;7%%NX4z>gaiEllnwtYSNxy& qBw)pVo*6bRH|p6^l>hxdo^Q^ewny;2b@lEEAdQ}`elF{r5}E)*&K%YN literal 0 HcmV?d00001 diff --git a/skimage/morphology/data/4/pngs/1.png b/skimage/morphology/data/4/pngs/1.png new file mode 100644 index 0000000000000000000000000000000000000000..d03660d2d80e785af86148e963b0466643aab2a4 GIT binary patch literal 149 zcmeAS@N?(olHy`uVBq!ia0vp^JRr=-3?y@QpIZW?n2Vh}LpV4%Za?&Y0OYa-_=LCu z>Hq)#FTFYcE|A4p;1OBOz`%C|gc+x5^GO2*)jVAsLpWw8CnO{%BqmHqPS}v2;9+z4 o!vjW+sWu54><=i^H!$)rOjh7HzImO`Y@iMXPgg&ebxsLQ0NPY4f&c&j literal 0 HcmV?d00001 diff --git a/skimage/morphology/data/4/pngs/2.png b/skimage/morphology/data/4/pngs/2.png new file mode 100644 index 0000000000000000000000000000000000000000..efa5c10492ba48e3074e296370deb5054d498853 GIT binary patch literal 137 zcmeAS@N?(olHy`uVBq!ia0vp^JRr=-3?y@QpIZW?n2Vh}LpV4%Za?&Y0OYa-_=LCu z>Hq)#FTFYcE|A4p;1OBOz`%C|gc+x5^GO2*r9536LpWw8CnTgbG_#1cvGefo?~oLd bn83)Oe~D?bz!bqIpehDWS3j3^P6Hq)#FTFYcE|A4p;1OBOz`%C|gc+x5^GO2*r9536LpWw8CnO{{FthWuG4t^7OGpYy bOkiZtn!psGcy_^QpehDWS3j3^P6Hq)#FTFYcE|A4p;1OBOz`%C|gc+x5^GO2*r9536LpWw8CnO{{FthWuG4t^7OGpYy bOkiZtn!psGcy_^QpehDWS3j3^P6Hq)#FTFYcE|A4p;1OBOz`%C|gc+x5^GO2*r9536LpWw8CnO{{FthWuG4t^7OGpYy bOkiZtn!psGcy_^QpehDWS3j3^P6Hq)#FTFYcE|A4p;1OBOz`%C|gc+x5^GO2*r9536LpWw8CnO{{FthWuG4t^7OGpYy bOkiZtn!psGcy_^QpehDWS3j3^P6%#Pp_QBq#U3e8-pQ(9zy{` z9z!~V5u9ZVXPLlRrf`-SoCP!h1^kEe{sRG&17ZRJhzBEq9H5Yu25LT#nU3X(J<`3=cw@UR+n4h3&GWil|dnZk7ze}9$Z0xkKojI3C+goX3`(NU0XJ7yT2!@p& literal 0 HcmV?d00001 diff --git a/skimage/morphology/data/4/stack_fiji.tif b/skimage/morphology/data/4/stack_fiji.tif new file mode 100644 index 0000000000000000000000000000000000000000..c0d182fd31f51f6b1ae14876343a46bd4cf834dd GIT binary patch literal 1795 zcmebEWzb?^VBlcjVfe?u0%S1)F^GV%c^Day*h~z}U}+AZ8a5~!sF;zDfe9$C4aD;p z1!3wLbs0sG)QCaZJV5p0Ff|Mlj!grz*@1db9-9uf-vh{g3UorRXKrG8s+X;yo~c19 zS0;#4Y-`3%#Pp_QBq#U3e8-pQ(9zy{` z9z!~V5u9ZVXPLlRrf`-SoCP!hC-@JS`Tw5*#sSkX4p;#u2O}(d(|4_WO0;q$*)78&qol`;+03>EBBLDyZ literal 0 HcmV?d00001 diff --git a/skimage/morphology/data/cross.txt b/skimage/morphology/data/cross.txt new file mode 100644 index 00000000..2118f7f1 --- /dev/null +++ b/skimage/morphology/data/cross.txt @@ -0,0 +1,24 @@ +0 0 0 0 0 255 255 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 255 255 255 255 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 255 255 255 255 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 255 255 255 255 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 255 255 255 255 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 255 255 255 255 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 0 0 0 +0 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 0 0 0 +0 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 0 0 +0 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 0 0 +0 0 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 0 0 +0 0 0 0 0 255 255 255 255 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 255 255 255 255 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 255 255 255 255 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 255 255 255 255 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 255 255 255 255 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 255 255 255 255 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 255 255 255 255 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 255 255 255 255 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 255 255 255 255 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 255 255 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 diff --git a/skimage/morphology/data/cross_fiji.txt b/skimage/morphology/data/cross_fiji.txt new file mode 100644 index 00000000..c4ea69a9 --- /dev/null +++ b/skimage/morphology/data/cross_fiji.txt @@ -0,0 +1,24 @@ +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 255 255 255 255 0 255 255 255 255 255 255 255 255 255 255 255 255 0 0 0 0 +0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 diff --git a/skimage/morphology/data/loop.png b/skimage/morphology/data/loop.png new file mode 100644 index 0000000000000000000000000000000000000000..05fdc25955801664638367107253540ee3885375 GIT binary patch literal 184 zcmeAS@N?(olHy`uVBq!ia0vp^l0YoM$P6S+tB1s;*b3=DinK$vl=HlH+5(9_e!F@$4gaza9aMw$crH~xkr^#UKy zGkDZ62=(wcuzYUhcX;%mS&Hr0V_t{OYK5cq4KqIS_Z@W!H)#L> literal 0 HcmV?d00001 diff --git a/skimage/morphology/data/loop.txt b/skimage/morphology/data/loop.txt new file mode 100644 index 00000000..277e1b47 --- /dev/null +++ b/skimage/morphology/data/loop.txt @@ -0,0 +1,24 @@ +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 255 255 255 255 255 255 255 255 255 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 255 255 255 255 255 255 255 255 255 255 255 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 255 255 255 255 255 255 255 0 0 0 255 255 0 0 0 0 0 +0 0 0 0 0 0 0 0 255 255 255 255 0 0 0 0 0 0 255 255 0 0 0 0 0 +0 0 0 0 0 0 0 255 255 255 255 0 0 0 0 0 0 0 255 255 0 0 0 0 0 +0 0 0 0 0 0 255 255 255 255 0 0 0 0 0 0 255 255 255 255 0 0 0 0 0 +0 0 0 0 0 0 255 255 255 255 0 0 0 0 0 255 255 255 255 0 0 0 0 0 0 +0 0 0 0 0 0 0 255 255 255 0 0 0 255 255 255 255 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 255 255 0 255 255 255 255 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 255 255 255 255 255 255 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 255 255 255 255 255 255 255 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 255 255 255 255 255 255 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 255 255 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 255 255 255 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 255 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 diff --git a/skimage/morphology/data/loop_fiji.txt b/skimage/morphology/data/loop_fiji.txt new file mode 100644 index 00000000..4d38bfac --- /dev/null +++ b/skimage/morphology/data/loop_fiji.txt @@ -0,0 +1,24 @@ +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 0 255 255 255 255 255 255 255 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 255 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 255 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 255 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 255 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 255 255 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 255 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 255 0 0 0 0 255 255 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 255 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 255 255 255 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 diff --git a/skimage/morphology/data/strip.png b/skimage/morphology/data/strip.png new file mode 100644 index 0000000000000000000000000000000000000000..74e9b3e49c0730a12cbf27c91e9dd825acd61822 GIT binary patch literal 131 zcmeAS@N?(olHy`uVBq!ia0vp^+(693$P6U=(y!hHQq09po*^6@9Je3(KLB#s0(?ST z|Ns9FWQHEPTnD5$3p^r=85sDEfH31!Z9ZwBpopi7V+hC0|22Vm7w!=Se(5=Wj}+%v){v!G=Hi!Tzopr0FyU3r~m)} literal 0 HcmV?d00001 diff --git a/skimage/morphology/data/two-hole.txt b/skimage/morphology/data/two-hole.txt new file mode 100644 index 00000000..680e7beb --- /dev/null +++ b/skimage/morphology/data/two-hole.txt @@ -0,0 +1,16 @@ +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 255 255 255 255 255 0 0 0 0 0 +0 0 0 0 0 255 255 255 255 255 255 255 255 255 0 255 0 +0 0 0 255 255 255 255 255 255 255 255 255 255 255 255 255 0 +0 0 255 255 255 255 0 0 255 255 255 0 0 255 255 255 0 +0 0 255 255 255 0 0 0 255 255 255 0 0 255 255 255 0 +0 0 0 255 255 0 0 0 255 255 255 0 0 255 255 255 0 +0 0 0 255 255 255 0 0 255 255 255 0 0 255 255 255 0 +0 0 0 0 255 255 0 0 255 255 255 0 0 255 255 255 0 +0 0 0 0 255 255 255 255 255 255 255 255 255 255 255 255 0 +0 0 0 0 0 255 255 255 255 255 255 255 255 255 255 255 0 +0 0 0 0 0 255 255 255 255 255 255 255 255 255 255 255 0 +0 0 0 0 0 0 0 255 255 255 255 255 255 255 0 0 0 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 diff --git a/skimage/morphology/data/two-hole_fiji.txt b/skimage/morphology/data/two-hole_fiji.txt new file mode 100644 index 00000000..6c16d3f7 --- /dev/null +++ b/skimage/morphology/data/two-hole_fiji.txt @@ -0,0 +1,16 @@ +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 255 255 0 255 255 0 0 0 0 0 +0 0 0 0 0 255 255 0 0 255 0 0 255 0 0 0 0 +0 0 0 0 255 0 0 0 0 255 0 0 0 255 0 0 0 +0 0 0 255 0 0 0 0 0 255 0 0 0 0 255 0 0 +0 0 0 255 0 0 0 0 0 255 0 0 0 0 255 0 0 +0 0 0 0 255 0 0 0 0 255 0 0 0 0 255 0 0 +0 0 0 0 255 0 0 0 0 255 0 0 0 0 255 0 0 +0 0 0 0 0 255 0 0 0 255 0 0 0 0 255 0 0 +0 0 0 0 0 0 255 255 0 255 0 0 0 255 0 0 0 +0 0 0 0 0 0 0 0 255 0 255 255 255 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 diff --git a/skimage/morphology/skel.py b/skimage/morphology/skel.py new file mode 100644 index 00000000..e53c7ee6 --- /dev/null +++ b/skimage/morphology/skel.py @@ -0,0 +1,678 @@ +from __future__ import division, print_function, absolute_import + +import numpy as np + + +def _prepare_image(img_in): + """Convert to a binary image, pad the it w/ zeros, and ensure it's 3D. + """ + if img_in.ndim < 2 or img_in.ndim > 3: + raise ValueError('expect 2D, got ndim = %s' % img_in.ndim) + + img = img_in.copy() + + if img.ndim == 2: + img = img.reshape((1,) + img.shape) + + # normalize to binary + img[img != 0] = 1 + + # pad w/ zeros to simplify dealing w/ neighborhood of a pixel + img_o = np.zeros(tuple(s + 2 for s in img.shape), + dtype=np.uint8) + img_o[1:-1, 1:-1, 1:-1] = img.astype(np.uint8) + + return img_o + + +def _postprocess_image(img_o): + """Clip the image (padding is an implementation detail), convert to b/w. + """ + img_oo = img_o[1:-1, 1:-1, 1:-1] + img_oo = img_oo.squeeze() + img_oo *= 255 + return img_oo + + +def get_neighborhood(img, p, r, c): + """Get the neighborhood of a pixel. + + Assume zero boundary conditions. Image is already padded, so no + out-of-bounds checking. + """ + neighborhood = np.zeros(27, dtype=np.uint8) + + neighborhood[0] = img[p-1, r-1, c-1] + neighborhood[1] = img[p-1, r, c-1] + neighborhood[2] = img[p-1, r+1, c-1] + + neighborhood[ 3] = img[p-1, r-1, c] + neighborhood[ 4] = img[p-1, r, c] + neighborhood[ 5] = img[p-1, r+1, c] + + neighborhood[ 6] = img[p-1, r-1, c+1] + neighborhood[ 7] = img[p-1, r, c+1] + neighborhood[ 8] = img[p-1, r+1, c+1] + + neighborhood[ 9] = img[p, r-1, c-1] + neighborhood[10] = img[p, r, c-1] + neighborhood[11] = img[p, r+1, c-1] + + neighborhood[12] = img[p, r-1, c] + neighborhood[13] = img[p, r, c] + neighborhood[14] = img[p, r+1, c] + + neighborhood[15] = img[p, r-1, c+1] + neighborhood[16] = img[p, r, c+1] + neighborhood[17] = img[p, r+1, c+1] + + neighborhood[18] = img[p+1, r-1, c-1] + neighborhood[19] = img[p+1, r, c-1] + neighborhood[20] = img[p+1, r+1, c-1] + + neighborhood[21] = img[p+1, r-1, c] + neighborhood[22] = img[p+1, r, c] + neighborhood[23] = img[p+1, r+1, c] + + neighborhood[24] = img[p+1, r-1, c+1] + neighborhood[25] = img[p+1, r, c+1] + neighborhood[26] = img[p+1, r+1, c+1] + + return neighborhood + + +###### look-up tables +def fill_numpoints_LUT(n=256): + p = int(np.log2(n) + 1) + return np.sum(np.arange(n)[:, None] & (1 << np.arange(p)) != 0, axis=1) + +NUMPOINTS_LUT = fill_numpoints_LUT() + + +def fill_Euler_LUT(): + LUT = np.zeros(256, dtype=int) + + LUT[1] = 1 + LUT[3] = -1 + LUT[5] = -1 + LUT[7] = 1 + LUT[9] = -3 + LUT[11] = -1 + LUT[13] = -1 + LUT[15] = 1 + LUT[17] = -1 + LUT[19] = 1 + LUT[21] = 1 + LUT[23] = -1 + LUT[25] = 3 + LUT[27] = 1 + LUT[29] = 1 + LUT[31] = -1 + LUT[33] = -3 + LUT[35] = -1 + LUT[37] = 3 + LUT[39] = 1 + LUT[41] = 1 + LUT[43] = -1 + LUT[45] = 3 + LUT[47] = 1 + LUT[49] = -1 + LUT[51] = 1 + + LUT[53] = 1 + LUT[55] = -1 + LUT[57] = 3 + LUT[59] = 1 + LUT[61] = 1 + LUT[63] = -1 + LUT[65] = -3 + LUT[67] = 3 + LUT[69] = -1 + LUT[71] = 1 + LUT[73] = 1 + LUT[75] = 3 + LUT[77] = -1 + LUT[79] = 1 + LUT[81] = -1 + LUT[83] = 1 + LUT[85] = 1 + LUT[87] = -1 + LUT[89] = 3 + LUT[91] = 1 + LUT[93] = 1 + LUT[95] = -1 + LUT[97] = 1 + LUT[99] = 3 + LUT[101] = 3 + LUT[103] = 1 + + LUT[105] = 5 + LUT[107] = 3 + LUT[109] = 3 + LUT[111] = 1 + LUT[113] = -1 + LUT[115] = 1 + LUT[117] = 1 + LUT[119] = -1 + LUT[121] = 3 + LUT[123] = 1 + LUT[125] = 1 + LUT[127] = -1 + LUT[129] = -7 + LUT[131] = -1 + LUT[133] = -1 + LUT[135] = 1 + LUT[137] = -3 + LUT[139] = -1 + LUT[141] = -1 + LUT[143] = 1 + LUT[145] = -1 + LUT[147] = 1 + LUT[149] = 1 + LUT[151] = -1 + LUT[153] = 3 + LUT[155] = 1 + + LUT[157] = 1 + LUT[159] = -1 + LUT[161] = -3 + LUT[163] = -1 + LUT[165] = 3 + LUT[167] = 1 + LUT[169] = 1 + LUT[171] = -1 + LUT[173] = 3 + LUT[175] = 1 + LUT[177] = -1 + LUT[179] = 1 + LUT[181] = 1 + LUT[183] = -1 + LUT[185] = 3 + LUT[187] = 1 + LUT[189] = 1 + LUT[191] = -1 + LUT[193] = -3 + LUT[195] = 3 + LUT[197] = -1 + LUT[199] = 1 + LUT[201] = 1 + LUT[203] = 3 + LUT[205] = -1 + LUT[207] = 1 + + LUT[209] = -1 + LUT[211] = 1 + LUT[213] = 1 + LUT[215] = -1 + LUT[217] = 3 + LUT[219] = 1 + LUT[221] = 1 + LUT[223] = -1 + LUT[225] = 1 + LUT[227] = 3 + LUT[229] = 3 + LUT[231] = 1 + LUT[233] = 5 + LUT[235] = 3 + LUT[237] = 3 + LUT[239] = 1 + LUT[241] = -1 + LUT[243] = 1 + LUT[245] = 1 + LUT[247] = -1 + LUT[249] = 3 + LUT[251] = 1 + LUT[253] = 1 + LUT[255] = -1 + return LUT + +LUT = fill_Euler_LUT() + + +### Octants (indexOctantXXX functions) +OCTANTS = tuple(range(8)) +NEB, NWB, SEB, SWB, NEU, NWU, SEU, SWU = OCTANTS + +neib_idx = np.empty((8, 7), dtype=int) +neib_idx[NEB, ...] = [2, 1, 11, 10, 5, 4, 14] +neib_idx[NWB, ...] = [0, 9, 3, 12, 1, 10, 4] +neib_idx[SEB, ...] = [8, 7, 17, 16, 5, 4, 14] +neib_idx[SWB, ...] = [6, 15, 7, 16, 3, 12, 4] +neib_idx[NEU, ...] = [20, 23, 19, 22, 11, 14, 10] +neib_idx[NWU, ...] = [18, 21, 9, 12, 19, 22, 10] +neib_idx[SEU, ...] = [26, 23, 17, 14, 25, 22, 16] +neib_idx[SWU, ...] = [24, 25, 15, 16, 21, 22, 12] + +def index_octants(octant, neighbors): + n = 1 + for j, idx in enumerate(neib_idx[octant]): + if neighbors[idx] == 1: + n |= 2**(7 - j) + return n + + +def is_surfacepoint(neighbors, points_LUT): + for octant in OCTANTS: + n = index_octants(octabt, neighbors) + if n not in (240, 165, 170) and points_LUT[n] > 2: + return False + return True + + +def is_Euler_invariant(neighbors): + """Check if a point is Euler invariant. + + Calculate Euler characteristc for each octant and sum up. + + Parameters + ---------- + neighbors : ndarray, shape (27,) + neighbors of a point + + Returns + ------- + bool + + """ + euler_char = 0 + for octant in OCTANTS: + n = index_octants(octant, neighbors) + euler_char += LUT[n] + return euler_char == 0 + + +def is_simple_point(neighbors): + """Check is a point is a Simple Point. + + This method is named 'N(v)_labeling' in [Lee94]. + Outputs the number of connected objects in a neighborhood of a point + after this point would have been removed. + + Parameters + ---------- + neighbors : ndarray, shape(27,) + neighbors of the point + + Returns + ------- + bool + Whether the point is simple or not. + + """ + # copy neighbors for labeling + # ignore center pixel (i=13) when counting (see [Lee94]) + cube = np.r_[neighbors[:13], neighbors[14:]] + + # set initial label + label = 2 + + # for all point in the neighborhood + for i in range(26): + if cube[i] == 1: + # voxel has not been labeled yet + # start recursion with any octant that contains the point i + if i in (0, 1, 3, 4, 9, 10, 12): + octree_labeling(1, label, cube) + elif i in (2, 5, 11, 13): + octree_labeling(2, label, cube) + elif i in (6, 7, 14, 15): + octree_labeling(3, label, cube) + elif i in (8, 16): + octree_labeling(4, label, cube) + elif i in (17, 18, 20, 21): + octree_labeling(5, label, cube) + elif i in (19, 22): + octree_labeling(6, label, cube) + elif i in (23, 24): + octree_labeling(7, label, cube) + elif i == 25: + octree_labeling(8, label, cube) + else: + raise ValueError("Never be here. i = %s" % i) + label += 1 + if label - 2 >= 2: + return False + return True + + +def octree_labeling(octant, label, cube): + """This is a recursive method that calculates the number of connected + components in the 3D neighborhood after the center pixel would + have been removed. + + Parameters + ---------- + octant : int + octant index + label : int + the current label of the center point + cube : ndarray, shape(26,) + local neighborhood of the point + + """ + # check if there are points in the octant with value 1 + if octant == 1: + # set points in this octant to current label + # and recursive labeling of adjacent octants + if cube[0] == 1: + cube[0] = label + if cube[1] == 1: + cube[1] = label + octree_labeling(2, label, cube) + if cube[3] == 1: + cube[3] = label + octree_labeling(3, label, cube) + if cube[4] == 1: + cube[4] = label + octree_labeling(2, label, cube) + octree_labeling(3, label, cube) + octree_labeling(4, label, cube) + if cube[9] == 1: + cube[9] = label + octree_labeling(5, label, cube) + if cube[10] == 1: + cube[10] = label + octree_labeling(2, label, cube) + octree_labeling(5, label, cube) + octree_labeling(6, label, cube) + if cube[12] == 1: + cube[12] = label + octree_labeling(3, label, cube) + octree_labeling(5, label, cube) + octree_labeling(7, label, cube) + + if octant == 2: + if cube[1] == 1: + cube[1] = label + octree_labeling(1, label, cube) + if cube[4] == 1: + cube[4] = label + octree_labeling(1, label, cube) + octree_labeling(3, label, cube) + octree_labeling(4, label, cube) + if cube[10] == 1: + cube[10] = label + octree_labeling(1, label, cube) + octree_labeling(5, label, cube) + octree_labeling(6, label, cube) + if cube[2] == 1: + cube[2] = label + if cube[5] == 1: + cube[5] = label + octree_labeling(4, label, cube) + if cube[11] == 1: + cube[11] = label + octree_labeling(6, label, cube) + if cube[13] == 1: + cube[13] = label + octree_labeling(4, label, cube) + octree_labeling(6, label, cube) + octree_labeling(8, label, cube) + + if octant ==3: + if cube[3] == 1: + cube[3] = label + octree_labeling(1, label, cube) + if cube[4] == 1: + cube[4] = label + octree_labeling(1, label, cube) + octree_labeling(2, label, cube) + octree_labeling(4, label, cube) + if cube[12] == 1: + cube[12] = label + octree_labeling(1, label, cube) + octree_labeling(5, label, cube) + octree_labeling(7, label, cube) + if cube[6] == 1: + cube[6] = label + if cube[7] == 1: + cube[7] = label + octree_labeling(4, label, cube) + if cube[14] == 1: + cube[14] = label + octree_labeling(7, label, cube) + if cube[15] == 1: + cube[15] = label + octree_labeling(4, label, cube) + octree_labeling(7, label, cube) + octree_labeling(8, label, cube) + + if octant == 4: + if cube[4] == 1: + cube[4] = label + octree_labeling(1, label, cube) + octree_labeling(2, label, cube) + octree_labeling(3, label, cube) + if cube[5] == 1: + cube[5] = label + octree_labeling(2, label, cube) + if cube[13] == 1: + cube[13] = label + octree_labeling(2, label, cube) + octree_labeling(6, label, cube) + octree_labeling(8, label, cube) + if cube[7] == 1: + cube[7] = label + octree_labeling(3, label, cube) + if cube[15] == 1: + cube[15] = label + octree_labeling(3, label, cube) + octree_labeling(7, label, cube) + octree_labeling(8, label, cube) + if cube[8] == 1: + cube[8] = label + if cube[16] == 1: + cube[16] = label + octree_labeling(8, label, cube) + + if octant == 5: + if cube[9] == 1: + cube[9] = label + octree_labeling(1, label, cube) + if cube[10] == 1: + cube[10] = label + octree_labeling(1, label, cube) + octree_labeling(2, label, cube) + octree_labeling(6, label, cube) + if cube[12] == 1: + cube[12] = label + octree_labeling(1, label, cube) + octree_labeling(3, label, cube) + octree_labeling(7, label, cube) + if cube[17] == 1: + cube[17] = label + if cube[18] == 1: + cube[18] = label + octree_labeling(6, label, cube) + if cube[20] == 1: + cube[20] = label + octree_labeling(7, label, cube) + if cube[21] == 1: + cube[21] = label + octree_labeling(6, label, cube) + octree_labeling(7, label, cube) + octree_labeling(8, label, cube) + + if octant == 6: + if cube[10] == 1: + cube[10] = label + octree_labeling(1, label, cube) + octree_labeling(2, label, cube) + octree_labeling(5, label, cube) + if cube[11] == 1: + cube[11] = label + octree_labeling(2, label, cube) + if cube[13] == 1: + cube[13] = label + octree_labeling(2, label, cube) + octree_labeling(4, label, cube) + octree_labeling(8, label, cube) + if cube[18] == 1: + cube[18] = label + octree_labeling(5, label, cube) + if cube[21] == 1: + cube[21] = label + octree_labeling(5, label, cube) + octree_labeling(7, label, cube) + octree_labeling(8, label, cube) + if cube[19] == 1: + cube[19] = label + if cube[22] == 1: + cube[22] = label + octree_labeling(8, label, cube) + + if octant == 7: + if cube[12] == 1: + cube[12] = label + octree_labeling(1, label, cube) + octree_labeling(3, label, cube) + octree_labeling(5, label, cube) + if cube[14] == 1: + cube[14] = label + octree_labeling(3, label, cube) + if cube[15] == 1: + cube[15] = label + octree_labeling(3, label, cube) + octree_labeling(4, label, cube) + octree_labeling(8, label, cube) + if cube[20] == 1: + cube[20] = label + octree_labeling(5, label, cube) + if cube[21] == 1: + cube[21] = label + octree_labeling(5, label, cube) + octree_labeling(6, label, cube) + octree_labeling(8, label, cube) + if cube[23] == 1: + cube[23] = label + if cube[24] == 1: + cube[24] = label + octree_labeling(8, label, cube) + + if octant == 8: + if cube[13] == 1: + cube[13] = label + octree_labeling(2, label, cube) + octree_labeling(4, label, cube) + octree_labeling(6, label, cube) + if cube[15] == 1: + cube[15] = label + octree_labeling(3, label, cube) + octree_labeling(4, label, cube) + octree_labeling(7, label, cube) + if cube[16] == 1: + cube[16] = label + octree_labeling(4, label, cube) + if cube[21] == 1: + cube[21] = label + octree_labeling(5, label, cube) + octree_labeling(6, label, cube) + octree_labeling(7, label, cube) + if cube[22] == 1: + cube[22] = label + octree_labeling(6, label, cube) + if cube[24] == 1: + cube[24] = label + octree_labeling(7, label, cube) + if cube[25] == 1: + cube[25] = label + + +def _loop_through(img, curr_border): + """Inner loop of compute_thin_image. + + return simple_border_points as a list to be rechecked sequentially. + """ + # loop through the image + # NB: each loop is from 1 to size-1: img is padded from all sides + simple_border_points = [] + + ### XXX: 2D images + ### if the original is 2D, img.shape[0] == 3, the algorithm removes too much + ### because all points are considered 'boundary' in the 3rd direction. + ### Hence just bail out + if img.shape[0] == 3 and curr_border in (5, 6): + print("skipping curr_border = ", curr_border) + return [] + + for p in range(1, img.shape[0] - 1): + for r in range(1, img.shape[1] - 1): + for c in range(1, img.shape[2] - 1): + + # check if pixel is foreground + if img[p, r, c] != 1: + continue + + is_border_pt = (curr_border == 1 and img[p, r, c-1] <= 0 or #N + curr_border == 2 and img[p, r, c+1] <= 0 or #S + curr_border == 3 and img[p, r+1, c] <= 0 or #E + curr_border == 4 and img[p, r-1, c] <= 0 or #W + curr_border == 5 and img[p+1, r, c] <= 0 or #U + curr_border == 6 and img[p-1, r, c] <= 0) #B + if not is_border_pt: + # current point is not deletable + continue + + neighborhood = get_neighborhood(img, p, r, c) + + # check if (p, r, c) is an endpoint. An endpoint has exactly + # one neighbor in the 26-neighborhood. + # The center pixel is counted, thus r.h.s. is 2 + if neighborhood.sum() == 2: + continue + + # check if point is Euler invariant (condition 1 in [Lee94]) + # if it is not, it's not deletable + if not is_Euler_invariant(neighborhood): + continue + + # check if point is simple (i.e., deletion does not + # change connectivity in the 3x3x3 neighborhood) + # this are conditions 2 and 3 in [Lee94] + if not is_simple_point(neighborhood): + continue + + # ok, add (p, r, c) to the list of simple border points + simple_border_points.append((p, r, c)) + return simple_border_points + + +def compute_thin_image(img_in): + + ### prepare + img = _prepare_image(img_in) + + ### compute + unchanged_borders = 0 + + # loop through the image several times until there is no change for all + # the six border types + while unchanged_borders < 6: + unchanged_borders = 0 + for curr_border in (4, 3, 2, 1, 5, 6): + + simple_border_points = _loop_through(img, curr_border) + print(curr_border, " : ", simple_border_points, '\n') + + # sequential re-checking to preserve connectivity when deleting + # in a parallel way + no_change = True + for pt in simple_border_points: + p, r, c = pt + neighb = get_neighborhood(img, p, r, c) + if is_simple_point(neighb): + img[p, r, c] = 0 + no_change = False + else: + print(" *** ", pt, is_simple_point(neighb)) + + if no_change: + unchanged_borders += 1 + simple_border_points = [] + + img = _postprocess_image(img) + return img + + +if __name__ == "__main__": + pass diff --git a/skimage/morphology/test_simple_2d.py b/skimage/morphology/test_simple_2d.py new file mode 100644 index 00000000..c4a5cd2b --- /dev/null +++ b/skimage/morphology/test_simple_2d.py @@ -0,0 +1,84 @@ +from __future__ import division, print_function, absolute_import + +import numpy as np +from numpy.testing import assert_equal + +from skimage import io + +from skel import compute_thin_image + +import matplotlib.pyplot as plt +import matplotlib.ticker as ticker + +# nose test generators: + +# 2D images +def test_simple_2d_images(): + for fname in ("strip", "loop", "cross", "two-hole"): + yield check_skel, fname + +# trivial 3D images +def test_simple_3d(): + for fname in ['3/stack', '4/stack']: + yield check_skel_3d, fname + + +def check_skel(fname, viz=False): + # compute the thin image and compare the result to that of ImageJ + img = np.loadtxt('data/' + fname + '.txt', dtype=np.uint8) + + if viz: + ax = _viz(img, **dict(marker='s', color='b', s=99, alpha=0.2)) + + # compute + img1_2d = compute_thin_image(img) + + if viz: + ax = _viz(img1_2d, ax, **dict(marker='o', color='r', + s=80, alpha=0.7, label='us')) + + # compare to FIJI + img_f = np.loadtxt('data/' + fname + '_fiji.txt', dtype=np.uint8) + + if not viz: + # actually compare images + assert_equal(img1_2d, img_f) + else: + ax = _viz(img_f, ax, **dict(marker='o', color='g', s=45, label='fiji')) + + ax.legend() + ax.grid(True) + + def yformatter(val, pos): + return int(img.shape[1] - val + 1) + def xformatter(val, pos): + return int(val + 1) + ax.xaxis.set_major_formatter(ticker.FuncFormatter(xformatter)) + ax.yaxis.set_major_formatter(ticker.FuncFormatter(yformatter)) + + plt.show() + + +def _viz(img, ax=None, **kwds): + if ax is None: + import matplotlib.pyplot as plt + fix, ax = plt.subplots() + + x, y = np.nonzero(img) + ax.scatter(y, img.shape[1] - x, **kwds) + return ax + + +def check_skel_3d(fname): + img = io.imread('data/' + fname + '.tif') + img_f = io.imread('data/' + fname + '_fiji.tif') + + img_s = compute_thin_image(img) + assert_equal(img_s, img_f) + + +if __name__ == "__main__": + import sys + if len(sys.argv) < 2: + sys.exit("Expect an image name from the data/ directory.") + check_skel(sys.argv[1], True) diff --git a/skimage/morphology/trivial.py b/skimage/morphology/trivial.py new file mode 100644 index 00000000..46656d93 --- /dev/null +++ b/skimage/morphology/trivial.py @@ -0,0 +1,73 @@ +import numpy as np +import matplotlib.pyplot as plt + +from skel import prepare_image, compute_thin_image + + +def get_rhombus(n=64, L=22, width=7): + img = np.zeros((n, n), dtype=np.int8) + + x = np.arange(L, dtype=int) + y = L - x + + for w in range(width): + img[x + n//2, y + n//2 + w] = 1 + img[-x + n//2, y + n//2 + w] = 1 + img[x + n//2, -y + n//2 + w] = 1 + img[-x + n//2, -y + n//2 + w] = 1 + + return img + +def get_strip(): + img = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=np.uint8) + return img + + +def get_square(n=64, L=16, width=4): + img = np.zeros((n, n), dtype=np.int8) + + x = np.arange(-L//2 + 1, L//2) + for w in range(width): + img[n//2 + x, n//2 + L//2 + w] = 1 + img[n//2 + x, n//2 - L//2 - w] = 1 + + img[n//2 + L//2 - w, n//2 + x] = 1 + img[n//2 + x, n//2 - L//2 - w] = 1 + + return img + + +def get_loop(): + img = np.loadtxt('Untitled.txt', dtype=np.uint8) + return img + + +if __name__ == "__main__": + +## img = get_rhombus() +## img = get_strip() + img = get_loop() + + x, y = np.nonzero(img) + plt.scatter(x, y, marker='s', color='b', s=40, alpha=0.3) + + # skeletonize + img1 = prepare_image(img) + img1 = compute_thin_image(img1) + + img1_2d = img1[1, 1:, 1:] + x, y = np.nonzero(img1_2d) + + plt.scatter(x, y, marker='o', color='r') + plt.show() From a85c9ddd3cd53e0855b2db2ccbd683f6be93fe95 Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Tue, 2 Feb 2016 14:26:33 +0000 Subject: [PATCH 02/39] MAINT: move new skeletonize3d files to more appropriate places --- skimage/morphology/__init__.py | 1 + .../morphology/{ => tests}/data/3/pngs/1.png | Bin .../morphology/{ => tests}/data/3/pngs/2.png | Bin .../morphology/{ => tests}/data/3/pngs/3.png | Bin .../morphology/{ => tests}/data/3/pngs/5.png | Bin .../morphology/{ => tests}/data/3/pngs/6.png | Bin .../morphology/{ => tests}/data/3/pngs/7.png | Bin .../morphology/{ => tests}/data/3/stack.tif | Bin .../{ => tests}/data/3/stack_fiji.tif | Bin .../morphology/{ => tests}/data/4/1_fiji.png | Bin .../morphology/{ => tests}/data/4/pngs/1.png | Bin .../morphology/{ => tests}/data/4/pngs/2.png | Bin .../morphology/{ => tests}/data/4/pngs/3.png | Bin .../morphology/{ => tests}/data/4/pngs/4.png | Bin .../morphology/{ => tests}/data/4/pngs/5.png | Bin .../morphology/{ => tests}/data/4/pngs/6.png | Bin .../morphology/{ => tests}/data/4/stack.tif | Bin .../{ => tests}/data/4/stack_fiji.tif | Bin skimage/morphology/{ => tests}/data/cross.png | Bin skimage/morphology/{ => tests}/data/cross.txt | 0 .../{ => tests}/data/cross_fiji.txt | 0 skimage/morphology/{ => tests}/data/loop.png | Bin skimage/morphology/{ => tests}/data/loop.txt | 0 .../morphology/{ => tests}/data/loop_fiji.txt | 0 skimage/morphology/{ => tests}/data/strip.png | Bin skimage/morphology/{ => tests}/data/strip.txt | 0 .../{ => tests}/data/strip_fiji.txt | 0 .../morphology/{ => tests}/data/two-hole.png | Bin .../morphology/{ => tests}/data/two-hole.txt | 0 .../{ => tests}/data/two-hole_fiji.txt | 0 .../test_skeletonize_3d.py} | 22 ++++++++++++++---- 31 files changed, 18 insertions(+), 5 deletions(-) rename skimage/morphology/{ => tests}/data/3/pngs/1.png (100%) rename skimage/morphology/{ => tests}/data/3/pngs/2.png (100%) rename skimage/morphology/{ => tests}/data/3/pngs/3.png (100%) rename skimage/morphology/{ => tests}/data/3/pngs/5.png (100%) rename skimage/morphology/{ => tests}/data/3/pngs/6.png (100%) rename skimage/morphology/{ => tests}/data/3/pngs/7.png (100%) rename skimage/morphology/{ => tests}/data/3/stack.tif (100%) rename skimage/morphology/{ => tests}/data/3/stack_fiji.tif (100%) rename skimage/morphology/{ => tests}/data/4/1_fiji.png (100%) rename skimage/morphology/{ => tests}/data/4/pngs/1.png (100%) rename skimage/morphology/{ => tests}/data/4/pngs/2.png (100%) rename skimage/morphology/{ => tests}/data/4/pngs/3.png (100%) rename skimage/morphology/{ => tests}/data/4/pngs/4.png (100%) rename skimage/morphology/{ => tests}/data/4/pngs/5.png (100%) rename skimage/morphology/{ => tests}/data/4/pngs/6.png (100%) rename skimage/morphology/{ => tests}/data/4/stack.tif (100%) rename skimage/morphology/{ => tests}/data/4/stack_fiji.tif (100%) rename skimage/morphology/{ => tests}/data/cross.png (100%) rename skimage/morphology/{ => tests}/data/cross.txt (100%) rename skimage/morphology/{ => tests}/data/cross_fiji.txt (100%) rename skimage/morphology/{ => tests}/data/loop.png (100%) rename skimage/morphology/{ => tests}/data/loop.txt (100%) rename skimage/morphology/{ => tests}/data/loop_fiji.txt (100%) rename skimage/morphology/{ => tests}/data/strip.png (100%) rename skimage/morphology/{ => tests}/data/strip.txt (100%) rename skimage/morphology/{ => tests}/data/strip_fiji.txt (100%) rename skimage/morphology/{ => tests}/data/two-hole.png (100%) rename skimage/morphology/{ => tests}/data/two-hole.txt (100%) rename skimage/morphology/{ => tests}/data/two-hole_fiji.txt (100%) rename skimage/morphology/{test_simple_2d.py => tests/test_skeletonize_3d.py} (75%) diff --git a/skimage/morphology/__init__.py b/skimage/morphology/__init__.py index a313f4ca..7d294911 100644 --- a/skimage/morphology/__init__.py +++ b/skimage/morphology/__init__.py @@ -6,6 +6,7 @@ from .selem import (square, rectangle, diamond, disk, cube, octahedron, ball, octagon, star) from .watershed import watershed from ._skeletonize import skeletonize, medial_axis +from .skel import compute_thin_image as compute_thin_image from .convex_hull import convex_hull_image, convex_hull_object from .greyreconstruct import reconstruction from .misc import remove_small_objects, remove_small_holes diff --git a/skimage/morphology/data/3/pngs/1.png b/skimage/morphology/tests/data/3/pngs/1.png similarity index 100% rename from skimage/morphology/data/3/pngs/1.png rename to skimage/morphology/tests/data/3/pngs/1.png diff --git a/skimage/morphology/data/3/pngs/2.png b/skimage/morphology/tests/data/3/pngs/2.png similarity index 100% rename from skimage/morphology/data/3/pngs/2.png rename to skimage/morphology/tests/data/3/pngs/2.png diff --git a/skimage/morphology/data/3/pngs/3.png b/skimage/morphology/tests/data/3/pngs/3.png similarity index 100% rename from skimage/morphology/data/3/pngs/3.png rename to skimage/morphology/tests/data/3/pngs/3.png diff --git a/skimage/morphology/data/3/pngs/5.png b/skimage/morphology/tests/data/3/pngs/5.png similarity index 100% rename from skimage/morphology/data/3/pngs/5.png rename to skimage/morphology/tests/data/3/pngs/5.png diff --git a/skimage/morphology/data/3/pngs/6.png b/skimage/morphology/tests/data/3/pngs/6.png similarity index 100% rename from skimage/morphology/data/3/pngs/6.png rename to skimage/morphology/tests/data/3/pngs/6.png diff --git a/skimage/morphology/data/3/pngs/7.png b/skimage/morphology/tests/data/3/pngs/7.png similarity index 100% rename from skimage/morphology/data/3/pngs/7.png rename to skimage/morphology/tests/data/3/pngs/7.png diff --git a/skimage/morphology/data/3/stack.tif b/skimage/morphology/tests/data/3/stack.tif similarity index 100% rename from skimage/morphology/data/3/stack.tif rename to skimage/morphology/tests/data/3/stack.tif diff --git a/skimage/morphology/data/3/stack_fiji.tif b/skimage/morphology/tests/data/3/stack_fiji.tif similarity index 100% rename from skimage/morphology/data/3/stack_fiji.tif rename to skimage/morphology/tests/data/3/stack_fiji.tif diff --git a/skimage/morphology/data/4/1_fiji.png b/skimage/morphology/tests/data/4/1_fiji.png similarity index 100% rename from skimage/morphology/data/4/1_fiji.png rename to skimage/morphology/tests/data/4/1_fiji.png diff --git a/skimage/morphology/data/4/pngs/1.png b/skimage/morphology/tests/data/4/pngs/1.png similarity index 100% rename from skimage/morphology/data/4/pngs/1.png rename to skimage/morphology/tests/data/4/pngs/1.png diff --git a/skimage/morphology/data/4/pngs/2.png b/skimage/morphology/tests/data/4/pngs/2.png similarity index 100% rename from skimage/morphology/data/4/pngs/2.png rename to skimage/morphology/tests/data/4/pngs/2.png diff --git a/skimage/morphology/data/4/pngs/3.png b/skimage/morphology/tests/data/4/pngs/3.png similarity index 100% rename from skimage/morphology/data/4/pngs/3.png rename to skimage/morphology/tests/data/4/pngs/3.png diff --git a/skimage/morphology/data/4/pngs/4.png b/skimage/morphology/tests/data/4/pngs/4.png similarity index 100% rename from skimage/morphology/data/4/pngs/4.png rename to skimage/morphology/tests/data/4/pngs/4.png diff --git a/skimage/morphology/data/4/pngs/5.png b/skimage/morphology/tests/data/4/pngs/5.png similarity index 100% rename from skimage/morphology/data/4/pngs/5.png rename to skimage/morphology/tests/data/4/pngs/5.png diff --git a/skimage/morphology/data/4/pngs/6.png b/skimage/morphology/tests/data/4/pngs/6.png similarity index 100% rename from skimage/morphology/data/4/pngs/6.png rename to skimage/morphology/tests/data/4/pngs/6.png diff --git a/skimage/morphology/data/4/stack.tif b/skimage/morphology/tests/data/4/stack.tif similarity index 100% rename from skimage/morphology/data/4/stack.tif rename to skimage/morphology/tests/data/4/stack.tif diff --git a/skimage/morphology/data/4/stack_fiji.tif b/skimage/morphology/tests/data/4/stack_fiji.tif similarity index 100% rename from skimage/morphology/data/4/stack_fiji.tif rename to skimage/morphology/tests/data/4/stack_fiji.tif diff --git a/skimage/morphology/data/cross.png b/skimage/morphology/tests/data/cross.png similarity index 100% rename from skimage/morphology/data/cross.png rename to skimage/morphology/tests/data/cross.png diff --git a/skimage/morphology/data/cross.txt b/skimage/morphology/tests/data/cross.txt similarity index 100% rename from skimage/morphology/data/cross.txt rename to skimage/morphology/tests/data/cross.txt diff --git a/skimage/morphology/data/cross_fiji.txt b/skimage/morphology/tests/data/cross_fiji.txt similarity index 100% rename from skimage/morphology/data/cross_fiji.txt rename to skimage/morphology/tests/data/cross_fiji.txt diff --git a/skimage/morphology/data/loop.png b/skimage/morphology/tests/data/loop.png similarity index 100% rename from skimage/morphology/data/loop.png rename to skimage/morphology/tests/data/loop.png diff --git a/skimage/morphology/data/loop.txt b/skimage/morphology/tests/data/loop.txt similarity index 100% rename from skimage/morphology/data/loop.txt rename to skimage/morphology/tests/data/loop.txt diff --git a/skimage/morphology/data/loop_fiji.txt b/skimage/morphology/tests/data/loop_fiji.txt similarity index 100% rename from skimage/morphology/data/loop_fiji.txt rename to skimage/morphology/tests/data/loop_fiji.txt diff --git a/skimage/morphology/data/strip.png b/skimage/morphology/tests/data/strip.png similarity index 100% rename from skimage/morphology/data/strip.png rename to skimage/morphology/tests/data/strip.png diff --git a/skimage/morphology/data/strip.txt b/skimage/morphology/tests/data/strip.txt similarity index 100% rename from skimage/morphology/data/strip.txt rename to skimage/morphology/tests/data/strip.txt diff --git a/skimage/morphology/data/strip_fiji.txt b/skimage/morphology/tests/data/strip_fiji.txt similarity index 100% rename from skimage/morphology/data/strip_fiji.txt rename to skimage/morphology/tests/data/strip_fiji.txt diff --git a/skimage/morphology/data/two-hole.png b/skimage/morphology/tests/data/two-hole.png similarity index 100% rename from skimage/morphology/data/two-hole.png rename to skimage/morphology/tests/data/two-hole.png diff --git a/skimage/morphology/data/two-hole.txt b/skimage/morphology/tests/data/two-hole.txt similarity index 100% rename from skimage/morphology/data/two-hole.txt rename to skimage/morphology/tests/data/two-hole.txt diff --git a/skimage/morphology/data/two-hole_fiji.txt b/skimage/morphology/tests/data/two-hole_fiji.txt similarity index 100% rename from skimage/morphology/data/two-hole_fiji.txt rename to skimage/morphology/tests/data/two-hole_fiji.txt diff --git a/skimage/morphology/test_simple_2d.py b/skimage/morphology/tests/test_skeletonize_3d.py similarity index 75% rename from skimage/morphology/test_simple_2d.py rename to skimage/morphology/tests/test_skeletonize_3d.py index c4a5cd2b..4bd48af7 100644 --- a/skimage/morphology/test_simple_2d.py +++ b/skimage/morphology/tests/test_skeletonize_3d.py @@ -1,11 +1,14 @@ from __future__ import division, print_function, absolute_import +import os + import numpy as np from numpy.testing import assert_equal +import skimage from skimage import io -from skel import compute_thin_image +from skimage.morphology import compute_thin_image import matplotlib.pyplot as plt import matplotlib.ticker as ticker @@ -23,9 +26,18 @@ def test_simple_3d(): yield check_skel_3d, fname +def get_data_path(): + # XXX this is a bad temp hack + return os.path.join(os.path.split(skimage.__file__)[0], + 'morphology', + 'tests', + 'data') + + def check_skel(fname, viz=False): # compute the thin image and compare the result to that of ImageJ - img = np.loadtxt('data/' + fname + '.txt', dtype=np.uint8) + + img = np.loadtxt(os.path.join(get_data_path(), fname+'.txt'), dtype=np.uint8) if viz: ax = _viz(img, **dict(marker='s', color='b', s=99, alpha=0.2)) @@ -38,7 +50,7 @@ def check_skel(fname, viz=False): s=80, alpha=0.7, label='us')) # compare to FIJI - img_f = np.loadtxt('data/' + fname + '_fiji.txt', dtype=np.uint8) + img_f = np.loadtxt(os.path.join(get_data_path(), fname+'_fiji.txt'), dtype=np.uint8) if not viz: # actually compare images @@ -70,8 +82,8 @@ def _viz(img, ax=None, **kwds): def check_skel_3d(fname): - img = io.imread('data/' + fname + '.tif') - img_f = io.imread('data/' + fname + '_fiji.tif') + img = io.imread(os.path.join(get_data_path(), fname+'.tif')) + img_f = io.imread(os.path.join(get_data_path(), fname+'_fiji.tif')) img_s = compute_thin_image(img) assert_equal(img_s, img_f) From 06d5a91f57dc7ab4c92c251e253a64ee018bdabf Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Tue, 26 Jan 2016 19:48:08 +0000 Subject: [PATCH 03/39] MAINT: skel3d: move computations to cython --- skimage/morphology/_skel.pyx | 638 +++++++++++++++++++++++++++++++++++ skimage/morphology/setup.py | 3 + skimage/morphology/skel.py | 637 +--------------------------------- 3 files changed, 644 insertions(+), 634 deletions(-) create mode 100644 skimage/morphology/_skel.pyx diff --git a/skimage/morphology/_skel.pyx b/skimage/morphology/_skel.pyx new file mode 100644 index 00000000..3c24bca1 --- /dev/null +++ b/skimage/morphology/_skel.pyx @@ -0,0 +1,638 @@ +from __future__ import division, print_function, absolute_import + +import numpy as np + + +def get_neighborhood(img, p, r, c): + """Get the neighborhood of a pixel. + + Assume zero boundary conditions. Image is already padded, so no + out-of-bounds checking. + """ + neighborhood = np.zeros(27, dtype=np.uint8) + + neighborhood[0] = img[p-1, r-1, c-1] + neighborhood[1] = img[p-1, r, c-1] + neighborhood[2] = img[p-1, r+1, c-1] + + neighborhood[ 3] = img[p-1, r-1, c] + neighborhood[ 4] = img[p-1, r, c] + neighborhood[ 5] = img[p-1, r+1, c] + + neighborhood[ 6] = img[p-1, r-1, c+1] + neighborhood[ 7] = img[p-1, r, c+1] + neighborhood[ 8] = img[p-1, r+1, c+1] + + neighborhood[ 9] = img[p, r-1, c-1] + neighborhood[10] = img[p, r, c-1] + neighborhood[11] = img[p, r+1, c-1] + + neighborhood[12] = img[p, r-1, c] + neighborhood[13] = img[p, r, c] + neighborhood[14] = img[p, r+1, c] + + neighborhood[15] = img[p, r-1, c+1] + neighborhood[16] = img[p, r, c+1] + neighborhood[17] = img[p, r+1, c+1] + + neighborhood[18] = img[p+1, r-1, c-1] + neighborhood[19] = img[p+1, r, c-1] + neighborhood[20] = img[p+1, r+1, c-1] + + neighborhood[21] = img[p+1, r-1, c] + neighborhood[22] = img[p+1, r, c] + neighborhood[23] = img[p+1, r+1, c] + + neighborhood[24] = img[p+1, r-1, c+1] + neighborhood[25] = img[p+1, r, c+1] + neighborhood[26] = img[p+1, r+1, c+1] + + return neighborhood + + +###### look-up tables +def fill_numpoints_LUT(n=256): + p = int(np.log2(n) + 1) + return np.sum(np.arange(n)[:, None] & (1 << np.arange(p)) != 0, axis=1) + +NUMPOINTS_LUT = fill_numpoints_LUT() + + +def fill_Euler_LUT(): + LUT = np.zeros(256, dtype=int) + + LUT[1] = 1 + LUT[3] = -1 + LUT[5] = -1 + LUT[7] = 1 + LUT[9] = -3 + LUT[11] = -1 + LUT[13] = -1 + LUT[15] = 1 + LUT[17] = -1 + LUT[19] = 1 + LUT[21] = 1 + LUT[23] = -1 + LUT[25] = 3 + LUT[27] = 1 + LUT[29] = 1 + LUT[31] = -1 + LUT[33] = -3 + LUT[35] = -1 + LUT[37] = 3 + LUT[39] = 1 + LUT[41] = 1 + LUT[43] = -1 + LUT[45] = 3 + LUT[47] = 1 + LUT[49] = -1 + LUT[51] = 1 + + LUT[53] = 1 + LUT[55] = -1 + LUT[57] = 3 + LUT[59] = 1 + LUT[61] = 1 + LUT[63] = -1 + LUT[65] = -3 + LUT[67] = 3 + LUT[69] = -1 + LUT[71] = 1 + LUT[73] = 1 + LUT[75] = 3 + LUT[77] = -1 + LUT[79] = 1 + LUT[81] = -1 + LUT[83] = 1 + LUT[85] = 1 + LUT[87] = -1 + LUT[89] = 3 + LUT[91] = 1 + LUT[93] = 1 + LUT[95] = -1 + LUT[97] = 1 + LUT[99] = 3 + LUT[101] = 3 + LUT[103] = 1 + + LUT[105] = 5 + LUT[107] = 3 + LUT[109] = 3 + LUT[111] = 1 + LUT[113] = -1 + LUT[115] = 1 + LUT[117] = 1 + LUT[119] = -1 + LUT[121] = 3 + LUT[123] = 1 + LUT[125] = 1 + LUT[127] = -1 + LUT[129] = -7 + LUT[131] = -1 + LUT[133] = -1 + LUT[135] = 1 + LUT[137] = -3 + LUT[139] = -1 + LUT[141] = -1 + LUT[143] = 1 + LUT[145] = -1 + LUT[147] = 1 + LUT[149] = 1 + LUT[151] = -1 + LUT[153] = 3 + LUT[155] = 1 + + LUT[157] = 1 + LUT[159] = -1 + LUT[161] = -3 + LUT[163] = -1 + LUT[165] = 3 + LUT[167] = 1 + LUT[169] = 1 + LUT[171] = -1 + LUT[173] = 3 + LUT[175] = 1 + LUT[177] = -1 + LUT[179] = 1 + LUT[181] = 1 + LUT[183] = -1 + LUT[185] = 3 + LUT[187] = 1 + LUT[189] = 1 + LUT[191] = -1 + LUT[193] = -3 + LUT[195] = 3 + LUT[197] = -1 + LUT[199] = 1 + LUT[201] = 1 + LUT[203] = 3 + LUT[205] = -1 + LUT[207] = 1 + + LUT[209] = -1 + LUT[211] = 1 + LUT[213] = 1 + LUT[215] = -1 + LUT[217] = 3 + LUT[219] = 1 + LUT[221] = 1 + LUT[223] = -1 + LUT[225] = 1 + LUT[227] = 3 + LUT[229] = 3 + LUT[231] = 1 + LUT[233] = 5 + LUT[235] = 3 + LUT[237] = 3 + LUT[239] = 1 + LUT[241] = -1 + LUT[243] = 1 + LUT[245] = 1 + LUT[247] = -1 + LUT[249] = 3 + LUT[251] = 1 + LUT[253] = 1 + LUT[255] = -1 + return LUT + +LUT = fill_Euler_LUT() + + +### Octants (indexOctantXXX functions) +OCTANTS = tuple(range(8)) +NEB, NWB, SEB, SWB, NEU, NWU, SEU, SWU = OCTANTS + +neib_idx = np.empty((8, 7), dtype=int) +neib_idx[NEB, ...] = [2, 1, 11, 10, 5, 4, 14] +neib_idx[NWB, ...] = [0, 9, 3, 12, 1, 10, 4] +neib_idx[SEB, ...] = [8, 7, 17, 16, 5, 4, 14] +neib_idx[SWB, ...] = [6, 15, 7, 16, 3, 12, 4] +neib_idx[NEU, ...] = [20, 23, 19, 22, 11, 14, 10] +neib_idx[NWU, ...] = [18, 21, 9, 12, 19, 22, 10] +neib_idx[SEU, ...] = [26, 23, 17, 14, 25, 22, 16] +neib_idx[SWU, ...] = [24, 25, 15, 16, 21, 22, 12] + +def index_octants(octant, neighbors): + n = 1 + for j, idx in enumerate(neib_idx[octant]): + if neighbors[idx] == 1: + n |= 2**(7 - j) + return n + + +def is_surfacepoint(neighbors, points_LUT): + for octant in OCTANTS: + n = index_octants(octant, neighbors) + if n not in (240, 165, 170) and points_LUT[n] > 2: + return False + return True + + +def is_Euler_invariant(neighbors): + """Check if a point is Euler invariant. + + Calculate Euler characteristc for each octant and sum up. + + Parameters + ---------- + neighbors : ndarray, shape (27,) + neighbors of a point + + Returns + ------- + bool + + """ + euler_char = 0 + for octant in OCTANTS: + n = index_octants(octant, neighbors) + euler_char += LUT[n] + return euler_char == 0 + + +def is_simple_point(neighbors): + """Check is a point is a Simple Point. + + This method is named 'N(v)_labeling' in [Lee94]. + Outputs the number of connected objects in a neighborhood of a point + after this point would have been removed. + + Parameters + ---------- + neighbors : ndarray, shape(27,) + neighbors of the point + + Returns + ------- + bool + Whether the point is simple or not. + + """ + # copy neighbors for labeling + # ignore center pixel (i=13) when counting (see [Lee94]) + cube = np.r_[neighbors[:13], neighbors[14:]] + + # set initial label + label = 2 + + # for all point in the neighborhood + for i in range(26): + if cube[i] == 1: + # voxel has not been labeled yet + # start recursion with any octant that contains the point i + if i in (0, 1, 3, 4, 9, 10, 12): + octree_labeling(1, label, cube) + elif i in (2, 5, 11, 13): + octree_labeling(2, label, cube) + elif i in (6, 7, 14, 15): + octree_labeling(3, label, cube) + elif i in (8, 16): + octree_labeling(4, label, cube) + elif i in (17, 18, 20, 21): + octree_labeling(5, label, cube) + elif i in (19, 22): + octree_labeling(6, label, cube) + elif i in (23, 24): + octree_labeling(7, label, cube) + elif i == 25: + octree_labeling(8, label, cube) + else: + raise ValueError("Never be here. i = %s" % i) + label += 1 + if label - 2 >= 2: + return False + return True + + +def octree_labeling(octant, label, cube): + """This is a recursive method that calculates the number of connected + components in the 3D neighborhood after the center pixel would + have been removed. + + Parameters + ---------- + octant : int + octant index + label : int + the current label of the center point + cube : ndarray, shape(26,) + local neighborhood of the point + + """ + # check if there are points in the octant with value 1 + if octant == 1: + # set points in this octant to current label + # and recursive labeling of adjacent octants + if cube[0] == 1: + cube[0] = label + if cube[1] == 1: + cube[1] = label + octree_labeling(2, label, cube) + if cube[3] == 1: + cube[3] = label + octree_labeling(3, label, cube) + if cube[4] == 1: + cube[4] = label + octree_labeling(2, label, cube) + octree_labeling(3, label, cube) + octree_labeling(4, label, cube) + if cube[9] == 1: + cube[9] = label + octree_labeling(5, label, cube) + if cube[10] == 1: + cube[10] = label + octree_labeling(2, label, cube) + octree_labeling(5, label, cube) + octree_labeling(6, label, cube) + if cube[12] == 1: + cube[12] = label + octree_labeling(3, label, cube) + octree_labeling(5, label, cube) + octree_labeling(7, label, cube) + + if octant == 2: + if cube[1] == 1: + cube[1] = label + octree_labeling(1, label, cube) + if cube[4] == 1: + cube[4] = label + octree_labeling(1, label, cube) + octree_labeling(3, label, cube) + octree_labeling(4, label, cube) + if cube[10] == 1: + cube[10] = label + octree_labeling(1, label, cube) + octree_labeling(5, label, cube) + octree_labeling(6, label, cube) + if cube[2] == 1: + cube[2] = label + if cube[5] == 1: + cube[5] = label + octree_labeling(4, label, cube) + if cube[11] == 1: + cube[11] = label + octree_labeling(6, label, cube) + if cube[13] == 1: + cube[13] = label + octree_labeling(4, label, cube) + octree_labeling(6, label, cube) + octree_labeling(8, label, cube) + + if octant ==3: + if cube[3] == 1: + cube[3] = label + octree_labeling(1, label, cube) + if cube[4] == 1: + cube[4] = label + octree_labeling(1, label, cube) + octree_labeling(2, label, cube) + octree_labeling(4, label, cube) + if cube[12] == 1: + cube[12] = label + octree_labeling(1, label, cube) + octree_labeling(5, label, cube) + octree_labeling(7, label, cube) + if cube[6] == 1: + cube[6] = label + if cube[7] == 1: + cube[7] = label + octree_labeling(4, label, cube) + if cube[14] == 1: + cube[14] = label + octree_labeling(7, label, cube) + if cube[15] == 1: + cube[15] = label + octree_labeling(4, label, cube) + octree_labeling(7, label, cube) + octree_labeling(8, label, cube) + + if octant == 4: + if cube[4] == 1: + cube[4] = label + octree_labeling(1, label, cube) + octree_labeling(2, label, cube) + octree_labeling(3, label, cube) + if cube[5] == 1: + cube[5] = label + octree_labeling(2, label, cube) + if cube[13] == 1: + cube[13] = label + octree_labeling(2, label, cube) + octree_labeling(6, label, cube) + octree_labeling(8, label, cube) + if cube[7] == 1: + cube[7] = label + octree_labeling(3, label, cube) + if cube[15] == 1: + cube[15] = label + octree_labeling(3, label, cube) + octree_labeling(7, label, cube) + octree_labeling(8, label, cube) + if cube[8] == 1: + cube[8] = label + if cube[16] == 1: + cube[16] = label + octree_labeling(8, label, cube) + + if octant == 5: + if cube[9] == 1: + cube[9] = label + octree_labeling(1, label, cube) + if cube[10] == 1: + cube[10] = label + octree_labeling(1, label, cube) + octree_labeling(2, label, cube) + octree_labeling(6, label, cube) + if cube[12] == 1: + cube[12] = label + octree_labeling(1, label, cube) + octree_labeling(3, label, cube) + octree_labeling(7, label, cube) + if cube[17] == 1: + cube[17] = label + if cube[18] == 1: + cube[18] = label + octree_labeling(6, label, cube) + if cube[20] == 1: + cube[20] = label + octree_labeling(7, label, cube) + if cube[21] == 1: + cube[21] = label + octree_labeling(6, label, cube) + octree_labeling(7, label, cube) + octree_labeling(8, label, cube) + + if octant == 6: + if cube[10] == 1: + cube[10] = label + octree_labeling(1, label, cube) + octree_labeling(2, label, cube) + octree_labeling(5, label, cube) + if cube[11] == 1: + cube[11] = label + octree_labeling(2, label, cube) + if cube[13] == 1: + cube[13] = label + octree_labeling(2, label, cube) + octree_labeling(4, label, cube) + octree_labeling(8, label, cube) + if cube[18] == 1: + cube[18] = label + octree_labeling(5, label, cube) + if cube[21] == 1: + cube[21] = label + octree_labeling(5, label, cube) + octree_labeling(7, label, cube) + octree_labeling(8, label, cube) + if cube[19] == 1: + cube[19] = label + if cube[22] == 1: + cube[22] = label + octree_labeling(8, label, cube) + + if octant == 7: + if cube[12] == 1: + cube[12] = label + octree_labeling(1, label, cube) + octree_labeling(3, label, cube) + octree_labeling(5, label, cube) + if cube[14] == 1: + cube[14] = label + octree_labeling(3, label, cube) + if cube[15] == 1: + cube[15] = label + octree_labeling(3, label, cube) + octree_labeling(4, label, cube) + octree_labeling(8, label, cube) + if cube[20] == 1: + cube[20] = label + octree_labeling(5, label, cube) + if cube[21] == 1: + cube[21] = label + octree_labeling(5, label, cube) + octree_labeling(6, label, cube) + octree_labeling(8, label, cube) + if cube[23] == 1: + cube[23] = label + if cube[24] == 1: + cube[24] = label + octree_labeling(8, label, cube) + + if octant == 8: + if cube[13] == 1: + cube[13] = label + octree_labeling(2, label, cube) + octree_labeling(4, label, cube) + octree_labeling(6, label, cube) + if cube[15] == 1: + cube[15] = label + octree_labeling(3, label, cube) + octree_labeling(4, label, cube) + octree_labeling(7, label, cube) + if cube[16] == 1: + cube[16] = label + octree_labeling(4, label, cube) + if cube[21] == 1: + cube[21] = label + octree_labeling(5, label, cube) + octree_labeling(6, label, cube) + octree_labeling(7, label, cube) + if cube[22] == 1: + cube[22] = label + octree_labeling(6, label, cube) + if cube[24] == 1: + cube[24] = label + octree_labeling(7, label, cube) + if cube[25] == 1: + cube[25] = label + + +def _loop_through(img, curr_border): + """Inner loop of compute_thin_image. + + return simple_border_points as a list to be rechecked sequentially. + """ + # loop through the image + # NB: each loop is from 1 to size-1: img is padded from all sides + simple_border_points = [] + + ### XXX: 2D images + ### if the original is 2D, img.shape[0] == 3, the algorithm removes too much + ### because all points are considered 'boundary' in the 3rd direction. + ### Hence just bail out + if img.shape[0] == 3 and curr_border in (5, 6): + print("skipping curr_border = ", curr_border) + return [] + + for p in range(1, img.shape[0] - 1): + for r in range(1, img.shape[1] - 1): + for c in range(1, img.shape[2] - 1): + + # check if pixel is foreground + if img[p, r, c] != 1: + continue + + is_border_pt = (curr_border == 1 and img[p, r, c-1] <= 0 or #N + curr_border == 2 and img[p, r, c+1] <= 0 or #S + curr_border == 3 and img[p, r+1, c] <= 0 or #E + curr_border == 4 and img[p, r-1, c] <= 0 or #W + curr_border == 5 and img[p+1, r, c] <= 0 or #U + curr_border == 6 and img[p-1, r, c] <= 0) #B + if not is_border_pt: + # current point is not deletable + continue + + neighborhood = get_neighborhood(img, p, r, c) + + # check if (p, r, c) is an endpoint. An endpoint has exactly + # one neighbor in the 26-neighborhood. + # The center pixel is counted, thus r.h.s. is 2 + if neighborhood.sum() == 2: + continue + + # check if point is Euler invariant (condition 1 in [Lee94]) + # if it is not, it's not deletable + if not is_Euler_invariant(neighborhood): + continue + + # check if point is simple (i.e., deletion does not + # change connectivity in the 3x3x3 neighborhood) + # this are conditions 2 and 3 in [Lee94] + if not is_simple_point(neighborhood): + continue + + # ok, add (p, r, c) to the list of simple border points + simple_border_points.append((p, r, c)) + return simple_border_points + + +def _compute_thin_image(img): + ### compute + unchanged_borders = 0 + + # loop through the image several times until there is no change for all + # the six border types + while unchanged_borders < 6: + unchanged_borders = 0 + for curr_border in (4, 3, 2, 1, 5, 6): + + simple_border_points = _loop_through(img, curr_border) + print(curr_border, " : ", simple_border_points, '\n') + + # sequential re-checking to preserve connectivity when deleting + # in a parallel way + no_change = True + for pt in simple_border_points: + p, r, c = pt + neighb = get_neighborhood(img, p, r, c) + if is_simple_point(neighb): + img[p, r, c] = 0 + no_change = False + else: + print(" *** ", pt, is_simple_point(neighb)) + + if no_change: + unchanged_borders += 1 + simple_border_points = [] + + return img diff --git a/skimage/morphology/setup.py b/skimage/morphology/setup.py index dbbcad8b..5a26ac52 100644 --- a/skimage/morphology/setup.py +++ b/skimage/morphology/setup.py @@ -16,6 +16,7 @@ def configuration(parent_package='', top_path=None): cython(['_skeletonize_cy.pyx'], working_path=base_path) cython(['_convex_hull.pyx'], working_path=base_path) cython(['_greyreconstruct.pyx'], working_path=base_path) + cython(['_skel.pyx'], working_path=base_path) config.add_extension('_watershed', sources=['_watershed.c'], include_dirs=[get_numpy_include_dirs()]) @@ -25,6 +26,8 @@ def configuration(parent_package='', top_path=None): include_dirs=[get_numpy_include_dirs()]) config.add_extension('_greyreconstruct', sources=['_greyreconstruct.c'], include_dirs=[get_numpy_include_dirs()]) + config.add_extension('_skel', sources=['_skel.c'], + include_dirs=[get_numpy_include_dirs()]) return config diff --git a/skimage/morphology/skel.py b/skimage/morphology/skel.py index e53c7ee6..5f8d9fad 100644 --- a/skimage/morphology/skel.py +++ b/skimage/morphology/skel.py @@ -2,6 +2,8 @@ from __future__ import division, print_function, absolute_import import numpy as np +from ._skel import _compute_thin_image + def _prepare_image(img_in): """Convert to a binary image, pad the it w/ zeros, and ensure it's 3D. @@ -34,642 +36,9 @@ def _postprocess_image(img_o): return img_oo -def get_neighborhood(img, p, r, c): - """Get the neighborhood of a pixel. - - Assume zero boundary conditions. Image is already padded, so no - out-of-bounds checking. - """ - neighborhood = np.zeros(27, dtype=np.uint8) - - neighborhood[0] = img[p-1, r-1, c-1] - neighborhood[1] = img[p-1, r, c-1] - neighborhood[2] = img[p-1, r+1, c-1] - - neighborhood[ 3] = img[p-1, r-1, c] - neighborhood[ 4] = img[p-1, r, c] - neighborhood[ 5] = img[p-1, r+1, c] - - neighborhood[ 6] = img[p-1, r-1, c+1] - neighborhood[ 7] = img[p-1, r, c+1] - neighborhood[ 8] = img[p-1, r+1, c+1] - - neighborhood[ 9] = img[p, r-1, c-1] - neighborhood[10] = img[p, r, c-1] - neighborhood[11] = img[p, r+1, c-1] - - neighborhood[12] = img[p, r-1, c] - neighborhood[13] = img[p, r, c] - neighborhood[14] = img[p, r+1, c] - - neighborhood[15] = img[p, r-1, c+1] - neighborhood[16] = img[p, r, c+1] - neighborhood[17] = img[p, r+1, c+1] - - neighborhood[18] = img[p+1, r-1, c-1] - neighborhood[19] = img[p+1, r, c-1] - neighborhood[20] = img[p+1, r+1, c-1] - - neighborhood[21] = img[p+1, r-1, c] - neighborhood[22] = img[p+1, r, c] - neighborhood[23] = img[p+1, r+1, c] - - neighborhood[24] = img[p+1, r-1, c+1] - neighborhood[25] = img[p+1, r, c+1] - neighborhood[26] = img[p+1, r+1, c+1] - - return neighborhood - - -###### look-up tables -def fill_numpoints_LUT(n=256): - p = int(np.log2(n) + 1) - return np.sum(np.arange(n)[:, None] & (1 << np.arange(p)) != 0, axis=1) - -NUMPOINTS_LUT = fill_numpoints_LUT() - - -def fill_Euler_LUT(): - LUT = np.zeros(256, dtype=int) - - LUT[1] = 1 - LUT[3] = -1 - LUT[5] = -1 - LUT[7] = 1 - LUT[9] = -3 - LUT[11] = -1 - LUT[13] = -1 - LUT[15] = 1 - LUT[17] = -1 - LUT[19] = 1 - LUT[21] = 1 - LUT[23] = -1 - LUT[25] = 3 - LUT[27] = 1 - LUT[29] = 1 - LUT[31] = -1 - LUT[33] = -3 - LUT[35] = -1 - LUT[37] = 3 - LUT[39] = 1 - LUT[41] = 1 - LUT[43] = -1 - LUT[45] = 3 - LUT[47] = 1 - LUT[49] = -1 - LUT[51] = 1 - - LUT[53] = 1 - LUT[55] = -1 - LUT[57] = 3 - LUT[59] = 1 - LUT[61] = 1 - LUT[63] = -1 - LUT[65] = -3 - LUT[67] = 3 - LUT[69] = -1 - LUT[71] = 1 - LUT[73] = 1 - LUT[75] = 3 - LUT[77] = -1 - LUT[79] = 1 - LUT[81] = -1 - LUT[83] = 1 - LUT[85] = 1 - LUT[87] = -1 - LUT[89] = 3 - LUT[91] = 1 - LUT[93] = 1 - LUT[95] = -1 - LUT[97] = 1 - LUT[99] = 3 - LUT[101] = 3 - LUT[103] = 1 - - LUT[105] = 5 - LUT[107] = 3 - LUT[109] = 3 - LUT[111] = 1 - LUT[113] = -1 - LUT[115] = 1 - LUT[117] = 1 - LUT[119] = -1 - LUT[121] = 3 - LUT[123] = 1 - LUT[125] = 1 - LUT[127] = -1 - LUT[129] = -7 - LUT[131] = -1 - LUT[133] = -1 - LUT[135] = 1 - LUT[137] = -3 - LUT[139] = -1 - LUT[141] = -1 - LUT[143] = 1 - LUT[145] = -1 - LUT[147] = 1 - LUT[149] = 1 - LUT[151] = -1 - LUT[153] = 3 - LUT[155] = 1 - - LUT[157] = 1 - LUT[159] = -1 - LUT[161] = -3 - LUT[163] = -1 - LUT[165] = 3 - LUT[167] = 1 - LUT[169] = 1 - LUT[171] = -1 - LUT[173] = 3 - LUT[175] = 1 - LUT[177] = -1 - LUT[179] = 1 - LUT[181] = 1 - LUT[183] = -1 - LUT[185] = 3 - LUT[187] = 1 - LUT[189] = 1 - LUT[191] = -1 - LUT[193] = -3 - LUT[195] = 3 - LUT[197] = -1 - LUT[199] = 1 - LUT[201] = 1 - LUT[203] = 3 - LUT[205] = -1 - LUT[207] = 1 - - LUT[209] = -1 - LUT[211] = 1 - LUT[213] = 1 - LUT[215] = -1 - LUT[217] = 3 - LUT[219] = 1 - LUT[221] = 1 - LUT[223] = -1 - LUT[225] = 1 - LUT[227] = 3 - LUT[229] = 3 - LUT[231] = 1 - LUT[233] = 5 - LUT[235] = 3 - LUT[237] = 3 - LUT[239] = 1 - LUT[241] = -1 - LUT[243] = 1 - LUT[245] = 1 - LUT[247] = -1 - LUT[249] = 3 - LUT[251] = 1 - LUT[253] = 1 - LUT[255] = -1 - return LUT - -LUT = fill_Euler_LUT() - - -### Octants (indexOctantXXX functions) -OCTANTS = tuple(range(8)) -NEB, NWB, SEB, SWB, NEU, NWU, SEU, SWU = OCTANTS - -neib_idx = np.empty((8, 7), dtype=int) -neib_idx[NEB, ...] = [2, 1, 11, 10, 5, 4, 14] -neib_idx[NWB, ...] = [0, 9, 3, 12, 1, 10, 4] -neib_idx[SEB, ...] = [8, 7, 17, 16, 5, 4, 14] -neib_idx[SWB, ...] = [6, 15, 7, 16, 3, 12, 4] -neib_idx[NEU, ...] = [20, 23, 19, 22, 11, 14, 10] -neib_idx[NWU, ...] = [18, 21, 9, 12, 19, 22, 10] -neib_idx[SEU, ...] = [26, 23, 17, 14, 25, 22, 16] -neib_idx[SWU, ...] = [24, 25, 15, 16, 21, 22, 12] - -def index_octants(octant, neighbors): - n = 1 - for j, idx in enumerate(neib_idx[octant]): - if neighbors[idx] == 1: - n |= 2**(7 - j) - return n - - -def is_surfacepoint(neighbors, points_LUT): - for octant in OCTANTS: - n = index_octants(octabt, neighbors) - if n not in (240, 165, 170) and points_LUT[n] > 2: - return False - return True - - -def is_Euler_invariant(neighbors): - """Check if a point is Euler invariant. - - Calculate Euler characteristc for each octant and sum up. - - Parameters - ---------- - neighbors : ndarray, shape (27,) - neighbors of a point - - Returns - ------- - bool - - """ - euler_char = 0 - for octant in OCTANTS: - n = index_octants(octant, neighbors) - euler_char += LUT[n] - return euler_char == 0 - - -def is_simple_point(neighbors): - """Check is a point is a Simple Point. - - This method is named 'N(v)_labeling' in [Lee94]. - Outputs the number of connected objects in a neighborhood of a point - after this point would have been removed. - - Parameters - ---------- - neighbors : ndarray, shape(27,) - neighbors of the point - - Returns - ------- - bool - Whether the point is simple or not. - - """ - # copy neighbors for labeling - # ignore center pixel (i=13) when counting (see [Lee94]) - cube = np.r_[neighbors[:13], neighbors[14:]] - - # set initial label - label = 2 - - # for all point in the neighborhood - for i in range(26): - if cube[i] == 1: - # voxel has not been labeled yet - # start recursion with any octant that contains the point i - if i in (0, 1, 3, 4, 9, 10, 12): - octree_labeling(1, label, cube) - elif i in (2, 5, 11, 13): - octree_labeling(2, label, cube) - elif i in (6, 7, 14, 15): - octree_labeling(3, label, cube) - elif i in (8, 16): - octree_labeling(4, label, cube) - elif i in (17, 18, 20, 21): - octree_labeling(5, label, cube) - elif i in (19, 22): - octree_labeling(6, label, cube) - elif i in (23, 24): - octree_labeling(7, label, cube) - elif i == 25: - octree_labeling(8, label, cube) - else: - raise ValueError("Never be here. i = %s" % i) - label += 1 - if label - 2 >= 2: - return False - return True - - -def octree_labeling(octant, label, cube): - """This is a recursive method that calculates the number of connected - components in the 3D neighborhood after the center pixel would - have been removed. - - Parameters - ---------- - octant : int - octant index - label : int - the current label of the center point - cube : ndarray, shape(26,) - local neighborhood of the point - - """ - # check if there are points in the octant with value 1 - if octant == 1: - # set points in this octant to current label - # and recursive labeling of adjacent octants - if cube[0] == 1: - cube[0] = label - if cube[1] == 1: - cube[1] = label - octree_labeling(2, label, cube) - if cube[3] == 1: - cube[3] = label - octree_labeling(3, label, cube) - if cube[4] == 1: - cube[4] = label - octree_labeling(2, label, cube) - octree_labeling(3, label, cube) - octree_labeling(4, label, cube) - if cube[9] == 1: - cube[9] = label - octree_labeling(5, label, cube) - if cube[10] == 1: - cube[10] = label - octree_labeling(2, label, cube) - octree_labeling(5, label, cube) - octree_labeling(6, label, cube) - if cube[12] == 1: - cube[12] = label - octree_labeling(3, label, cube) - octree_labeling(5, label, cube) - octree_labeling(7, label, cube) - - if octant == 2: - if cube[1] == 1: - cube[1] = label - octree_labeling(1, label, cube) - if cube[4] == 1: - cube[4] = label - octree_labeling(1, label, cube) - octree_labeling(3, label, cube) - octree_labeling(4, label, cube) - if cube[10] == 1: - cube[10] = label - octree_labeling(1, label, cube) - octree_labeling(5, label, cube) - octree_labeling(6, label, cube) - if cube[2] == 1: - cube[2] = label - if cube[5] == 1: - cube[5] = label - octree_labeling(4, label, cube) - if cube[11] == 1: - cube[11] = label - octree_labeling(6, label, cube) - if cube[13] == 1: - cube[13] = label - octree_labeling(4, label, cube) - octree_labeling(6, label, cube) - octree_labeling(8, label, cube) - - if octant ==3: - if cube[3] == 1: - cube[3] = label - octree_labeling(1, label, cube) - if cube[4] == 1: - cube[4] = label - octree_labeling(1, label, cube) - octree_labeling(2, label, cube) - octree_labeling(4, label, cube) - if cube[12] == 1: - cube[12] = label - octree_labeling(1, label, cube) - octree_labeling(5, label, cube) - octree_labeling(7, label, cube) - if cube[6] == 1: - cube[6] = label - if cube[7] == 1: - cube[7] = label - octree_labeling(4, label, cube) - if cube[14] == 1: - cube[14] = label - octree_labeling(7, label, cube) - if cube[15] == 1: - cube[15] = label - octree_labeling(4, label, cube) - octree_labeling(7, label, cube) - octree_labeling(8, label, cube) - - if octant == 4: - if cube[4] == 1: - cube[4] = label - octree_labeling(1, label, cube) - octree_labeling(2, label, cube) - octree_labeling(3, label, cube) - if cube[5] == 1: - cube[5] = label - octree_labeling(2, label, cube) - if cube[13] == 1: - cube[13] = label - octree_labeling(2, label, cube) - octree_labeling(6, label, cube) - octree_labeling(8, label, cube) - if cube[7] == 1: - cube[7] = label - octree_labeling(3, label, cube) - if cube[15] == 1: - cube[15] = label - octree_labeling(3, label, cube) - octree_labeling(7, label, cube) - octree_labeling(8, label, cube) - if cube[8] == 1: - cube[8] = label - if cube[16] == 1: - cube[16] = label - octree_labeling(8, label, cube) - - if octant == 5: - if cube[9] == 1: - cube[9] = label - octree_labeling(1, label, cube) - if cube[10] == 1: - cube[10] = label - octree_labeling(1, label, cube) - octree_labeling(2, label, cube) - octree_labeling(6, label, cube) - if cube[12] == 1: - cube[12] = label - octree_labeling(1, label, cube) - octree_labeling(3, label, cube) - octree_labeling(7, label, cube) - if cube[17] == 1: - cube[17] = label - if cube[18] == 1: - cube[18] = label - octree_labeling(6, label, cube) - if cube[20] == 1: - cube[20] = label - octree_labeling(7, label, cube) - if cube[21] == 1: - cube[21] = label - octree_labeling(6, label, cube) - octree_labeling(7, label, cube) - octree_labeling(8, label, cube) - - if octant == 6: - if cube[10] == 1: - cube[10] = label - octree_labeling(1, label, cube) - octree_labeling(2, label, cube) - octree_labeling(5, label, cube) - if cube[11] == 1: - cube[11] = label - octree_labeling(2, label, cube) - if cube[13] == 1: - cube[13] = label - octree_labeling(2, label, cube) - octree_labeling(4, label, cube) - octree_labeling(8, label, cube) - if cube[18] == 1: - cube[18] = label - octree_labeling(5, label, cube) - if cube[21] == 1: - cube[21] = label - octree_labeling(5, label, cube) - octree_labeling(7, label, cube) - octree_labeling(8, label, cube) - if cube[19] == 1: - cube[19] = label - if cube[22] == 1: - cube[22] = label - octree_labeling(8, label, cube) - - if octant == 7: - if cube[12] == 1: - cube[12] = label - octree_labeling(1, label, cube) - octree_labeling(3, label, cube) - octree_labeling(5, label, cube) - if cube[14] == 1: - cube[14] = label - octree_labeling(3, label, cube) - if cube[15] == 1: - cube[15] = label - octree_labeling(3, label, cube) - octree_labeling(4, label, cube) - octree_labeling(8, label, cube) - if cube[20] == 1: - cube[20] = label - octree_labeling(5, label, cube) - if cube[21] == 1: - cube[21] = label - octree_labeling(5, label, cube) - octree_labeling(6, label, cube) - octree_labeling(8, label, cube) - if cube[23] == 1: - cube[23] = label - if cube[24] == 1: - cube[24] = label - octree_labeling(8, label, cube) - - if octant == 8: - if cube[13] == 1: - cube[13] = label - octree_labeling(2, label, cube) - octree_labeling(4, label, cube) - octree_labeling(6, label, cube) - if cube[15] == 1: - cube[15] = label - octree_labeling(3, label, cube) - octree_labeling(4, label, cube) - octree_labeling(7, label, cube) - if cube[16] == 1: - cube[16] = label - octree_labeling(4, label, cube) - if cube[21] == 1: - cube[21] = label - octree_labeling(5, label, cube) - octree_labeling(6, label, cube) - octree_labeling(7, label, cube) - if cube[22] == 1: - cube[22] = label - octree_labeling(6, label, cube) - if cube[24] == 1: - cube[24] = label - octree_labeling(7, label, cube) - if cube[25] == 1: - cube[25] = label - - -def _loop_through(img, curr_border): - """Inner loop of compute_thin_image. - - return simple_border_points as a list to be rechecked sequentially. - """ - # loop through the image - # NB: each loop is from 1 to size-1: img is padded from all sides - simple_border_points = [] - - ### XXX: 2D images - ### if the original is 2D, img.shape[0] == 3, the algorithm removes too much - ### because all points are considered 'boundary' in the 3rd direction. - ### Hence just bail out - if img.shape[0] == 3 and curr_border in (5, 6): - print("skipping curr_border = ", curr_border) - return [] - - for p in range(1, img.shape[0] - 1): - for r in range(1, img.shape[1] - 1): - for c in range(1, img.shape[2] - 1): - - # check if pixel is foreground - if img[p, r, c] != 1: - continue - - is_border_pt = (curr_border == 1 and img[p, r, c-1] <= 0 or #N - curr_border == 2 and img[p, r, c+1] <= 0 or #S - curr_border == 3 and img[p, r+1, c] <= 0 or #E - curr_border == 4 and img[p, r-1, c] <= 0 or #W - curr_border == 5 and img[p+1, r, c] <= 0 or #U - curr_border == 6 and img[p-1, r, c] <= 0) #B - if not is_border_pt: - # current point is not deletable - continue - - neighborhood = get_neighborhood(img, p, r, c) - - # check if (p, r, c) is an endpoint. An endpoint has exactly - # one neighbor in the 26-neighborhood. - # The center pixel is counted, thus r.h.s. is 2 - if neighborhood.sum() == 2: - continue - - # check if point is Euler invariant (condition 1 in [Lee94]) - # if it is not, it's not deletable - if not is_Euler_invariant(neighborhood): - continue - - # check if point is simple (i.e., deletion does not - # change connectivity in the 3x3x3 neighborhood) - # this are conditions 2 and 3 in [Lee94] - if not is_simple_point(neighborhood): - continue - - # ok, add (p, r, c) to the list of simple border points - simple_border_points.append((p, r, c)) - return simple_border_points - - def compute_thin_image(img_in): - - ### prepare img = _prepare_image(img_in) - - ### compute - unchanged_borders = 0 - - # loop through the image several times until there is no change for all - # the six border types - while unchanged_borders < 6: - unchanged_borders = 0 - for curr_border in (4, 3, 2, 1, 5, 6): - - simple_border_points = _loop_through(img, curr_border) - print(curr_border, " : ", simple_border_points, '\n') - - # sequential re-checking to preserve connectivity when deleting - # in a parallel way - no_change = True - for pt in simple_border_points: - p, r, c = pt - neighb = get_neighborhood(img, p, r, c) - if is_simple_point(neighb): - img[p, r, c] = 0 - no_change = False - else: - print(" *** ", pt, is_simple_point(neighb)) - - if no_change: - unchanged_borders += 1 - simple_border_points = [] - + img = _compute_thin_image(img) img = _postprocess_image(img) return img From f1176a5319607fcb649bdf1e74d275f911b9f054 Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Tue, 26 Jan 2016 20:05:16 +0000 Subject: [PATCH 04/39] MAINT: skel3d: add static typing to obvious places --- skimage/morphology/_skel.pyx | 41 ++++++++++++++++++++++++------------ skimage/morphology/skel.py | 2 -- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/skimage/morphology/_skel.pyx b/skimage/morphology/_skel.pyx index 3c24bca1..84b59f2d 100644 --- a/skimage/morphology/_skel.pyx +++ b/skimage/morphology/_skel.pyx @@ -1,9 +1,11 @@ from __future__ import division, print_function, absolute_import import numpy as np +from numpy cimport npy_intp, npy_uint8 +ctypedef npy_uint8[:, :, ::1] img_type -def get_neighborhood(img, p, r, c): +def get_neighborhood(img_type img, npy_intp p, npy_intp r, npy_intp c): """Get the neighborhood of a pixel. Assume zero boundary conditions. Image is already padded, so no @@ -228,7 +230,7 @@ def is_surfacepoint(neighbors, points_LUT): return True -def is_Euler_invariant(neighbors): +cdef bint is_Euler_invariant(neighbors): """Check if a point is Euler invariant. Calculate Euler characteristc for each octant and sum up. @@ -243,14 +245,15 @@ def is_Euler_invariant(neighbors): bool """ - euler_char = 0 - for octant in OCTANTS: + cdef int octant, n + cdef int euler_char = 0 + for octant in xrange(8): n = index_octants(octant, neighbors) euler_char += LUT[n] return euler_char == 0 -def is_simple_point(neighbors): +cdef bint is_simple_point(neighbors): """Check is a point is a Simple Point. This method is named 'N(v)_labeling' in [Lee94]. @@ -272,8 +275,10 @@ def is_simple_point(neighbors): # ignore center pixel (i=13) when counting (see [Lee94]) cube = np.r_[neighbors[:13], neighbors[14:]] + cdef int i + # set initial label - label = 2 + cdef int label = 2 # for all point in the neighborhood for i in range(26): @@ -304,7 +309,7 @@ def is_simple_point(neighbors): return True -def octree_labeling(octant, label, cube): +cdef void octree_labeling(int octant, int label, cube): """This is a recursive method that calculates the number of connected components in the 3D neighborhood after the center pixel would have been removed. @@ -547,19 +552,23 @@ def octree_labeling(octant, label, cube): cube[25] = label -def _loop_through(img, curr_border): +cdef list _loop_through(img_type img, + int curr_border): """Inner loop of compute_thin_image. return simple_border_points as a list to be rechecked sequentially. """ + cdef: + list simple_border_points = [] + npy_intp p, r, c + bint is_border_pt + # loop through the image # NB: each loop is from 1 to size-1: img is padded from all sides - simple_border_points = [] - ### XXX: 2D images ### if the original is 2D, img.shape[0] == 3, the algorithm removes too much ### because all points are considered 'boundary' in the 3rd direction. - ### Hence just bail out + ### Hence just bail out. if img.shape[0] == 3 and curr_border in (5, 6): print("skipping curr_border = ", curr_border) return [] @@ -607,14 +616,18 @@ def _loop_through(img, curr_border): def _compute_thin_image(img): - ### compute - unchanged_borders = 0 + + cdef: + int unchanged_borders = 0, curr_border + npy_intp p, r, c + bint no_change + list simple_border_points # loop through the image several times until there is no change for all # the six border types while unchanged_borders < 6: unchanged_borders = 0 - for curr_border in (4, 3, 2, 1, 5, 6): + for curr_border in [4, 3, 2, 1, 5, 6]: simple_border_points = _loop_through(img, curr_border) print(curr_border, " : ", simple_border_points, '\n') diff --git a/skimage/morphology/skel.py b/skimage/morphology/skel.py index 5f8d9fad..eff1e56c 100644 --- a/skimage/morphology/skel.py +++ b/skimage/morphology/skel.py @@ -12,7 +12,6 @@ def _prepare_image(img_in): raise ValueError('expect 2D, got ndim = %s' % img_in.ndim) img = img_in.copy() - if img.ndim == 2: img = img.reshape((1,) + img.shape) @@ -23,7 +22,6 @@ def _prepare_image(img_in): img_o = np.zeros(tuple(s + 2 for s in img.shape), dtype=np.uint8) img_o[1:-1, 1:-1, 1:-1] = img.astype(np.uint8) - return img_o From 53f48cb6c05fe6f67d1be580b9f7f9fcd6880170 Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Tue, 26 Jan 2016 21:27:47 +0000 Subject: [PATCH 05/39] MAINT: skel3d: only allocate and type neighborhood once; switch off bounds checking --- skimage/morphology/_skel.pyx | 38 +++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/skimage/morphology/_skel.pyx b/skimage/morphology/_skel.pyx index 84b59f2d..5b8b04d5 100644 --- a/skimage/morphology/_skel.pyx +++ b/skimage/morphology/_skel.pyx @@ -2,17 +2,22 @@ from __future__ import division, print_function, absolute_import import numpy as np from numpy cimport npy_intp, npy_uint8 +cimport cython ctypedef npy_uint8[:, :, ::1] img_type +ctypedef npy_uint8[::1] neighb_type -def get_neighborhood(img_type img, npy_intp p, npy_intp r, npy_intp c): + +@cython.boundscheck(False) +@cython.wraparound(False) +cdef void get_neighborhood(img_type img, + npy_intp p, npy_intp r, npy_intp c, + neighb_type neighborhood): """Get the neighborhood of a pixel. Assume zero boundary conditions. Image is already padded, so no out-of-bounds checking. """ - neighborhood = np.zeros(27, dtype=np.uint8) - neighborhood[0] = img[p-1, r-1, c-1] neighborhood[1] = img[p-1, r, c-1] neighborhood[2] = img[p-1, r+1, c-1] @@ -49,8 +54,6 @@ def get_neighborhood(img_type img, npy_intp p, npy_intp r, npy_intp c): neighborhood[25] = img[p+1, r, c+1] neighborhood[26] = img[p+1, r+1, c+1] - return neighborhood - ###### look-up tables def fill_numpoints_LUT(n=256): @@ -214,7 +217,7 @@ neib_idx[NWU, ...] = [18, 21, 9, 12, 19, 22, 10] neib_idx[SEU, ...] = [26, 23, 17, 14, 25, 22, 16] neib_idx[SWU, ...] = [24, 25, 15, 16, 21, 22, 12] -def index_octants(octant, neighbors): +def index_octants(octant, neighb_type neighbors): n = 1 for j, idx in enumerate(neib_idx[octant]): if neighbors[idx] == 1: @@ -230,7 +233,7 @@ def is_surfacepoint(neighbors, points_LUT): return True -cdef bint is_Euler_invariant(neighbors): +cdef bint is_Euler_invariant(neighb_type neighbors): """Check if a point is Euler invariant. Calculate Euler characteristc for each octant and sum up. @@ -253,7 +256,9 @@ cdef bint is_Euler_invariant(neighbors): return euler_char == 0 -cdef bint is_simple_point(neighbors): +@cython.boundscheck(False) +@cython.wraparound(False) +cdef bint is_simple_point(neighb_type neighbors): """Check is a point is a Simple Point. This method is named 'N(v)_labeling' in [Lee94]. @@ -273,8 +278,7 @@ cdef bint is_simple_point(neighbors): """ # copy neighbors for labeling # ignore center pixel (i=13) when counting (see [Lee94]) - cube = np.r_[neighbors[:13], neighbors[14:]] - + cdef neighb_type cube = np.r_[neighbors[:13], neighbors[14:]] cdef int i # set initial label @@ -309,7 +313,9 @@ cdef bint is_simple_point(neighbors): return True -cdef void octree_labeling(int octant, int label, cube): +@cython.boundscheck(False) +@cython.wraparound(False) +cdef void octree_labeling(int octant, int label, neighb_type cube): """This is a recursive method that calculates the number of connected components in the 3D neighborhood after the center pixel would have been removed. @@ -552,6 +558,8 @@ cdef void octree_labeling(int octant, int label, cube): cube[25] = label +@cython.boundscheck(False) +@cython.wraparound(False) cdef list _loop_through(img_type img, int curr_border): """Inner loop of compute_thin_image. @@ -560,6 +568,7 @@ cdef list _loop_through(img_type img, """ cdef: list simple_border_points = [] + neighb_type neighborhood = np.zeros(27, dtype=np.uint8) npy_intp p, r, c bint is_border_pt @@ -591,12 +600,12 @@ cdef list _loop_through(img_type img, # current point is not deletable continue - neighborhood = get_neighborhood(img, p, r, c) + get_neighborhood(img, p, r, c, neighborhood) # check if (p, r, c) is an endpoint. An endpoint has exactly # one neighbor in the 26-neighborhood. # The center pixel is counted, thus r.h.s. is 2 - if neighborhood.sum() == 2: + if np.sum(neighborhood) == 2: continue # check if point is Euler invariant (condition 1 in [Lee94]) @@ -622,6 +631,7 @@ def _compute_thin_image(img): npy_intp p, r, c bint no_change list simple_border_points + neighb_type neighb = np.zeros(27, dtype=np.uint8) # loop through the image several times until there is no change for all # the six border types @@ -637,7 +647,7 @@ def _compute_thin_image(img): no_change = True for pt in simple_border_points: p, r, c = pt - neighb = get_neighborhood(img, p, r, c) + get_neighborhood(img, p, r, c, neighb) if is_simple_point(neighb): img[p, r, c] = 0 no_change = False From 32865d2bd949e2cc180b47e3b5937149a14c7103 Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Wed, 27 Jan 2016 08:34:18 +0000 Subject: [PATCH 06/39] MAINT: skel3d: statically type index_octants --- skimage/morphology/_skel.pyx | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/skimage/morphology/_skel.pyx b/skimage/morphology/_skel.pyx index 5b8b04d5..cafe6006 100644 --- a/skimage/morphology/_skel.pyx +++ b/skimage/morphology/_skel.pyx @@ -207,21 +207,29 @@ LUT = fill_Euler_LUT() OCTANTS = tuple(range(8)) NEB, NWB, SEB, SWB, NEU, NWU, SEU, SWU = OCTANTS -neib_idx = np.empty((8, 7), dtype=int) -neib_idx[NEB, ...] = [2, 1, 11, 10, 5, 4, 14] -neib_idx[NWB, ...] = [0, 9, 3, 12, 1, 10, 4] -neib_idx[SEB, ...] = [8, 7, 17, 16, 5, 4, 14] -neib_idx[SWB, ...] = [6, 15, 7, 16, 3, 12, 4] -neib_idx[NEU, ...] = [20, 23, 19, 22, 11, 14, 10] -neib_idx[NWU, ...] = [18, 21, 9, 12, 19, 22, 10] -neib_idx[SEU, ...] = [26, 23, 17, 14, 25, 22, 16] -neib_idx[SWU, ...] = [24, 25, 15, 16, 21, 22, 12] +_neib_idx = np.empty((8, 7), dtype=np.int32) +_neib_idx[NEB, ...] = [2, 1, 11, 10, 5, 4, 14] +_neib_idx[NWB, ...] = [0, 9, 3, 12, 1, 10, 4] +_neib_idx[SEB, ...] = [8, 7, 17, 16, 5, 4, 14] +_neib_idx[SWB, ...] = [6, 15, 7, 16, 3, 12, 4] +_neib_idx[NEU, ...] = [20, 23, 19, 22, 11, 14, 10] +_neib_idx[NWU, ...] = [18, 21, 9, 12, 19, 22, 10] +_neib_idx[SEU, ...] = [26, 23, 17, 14, 25, 22, 16] +_neib_idx[SWU, ...] = [24, 25, 15, 16, 21, 22, 12] +cdef int[:, ::1] neib_idx = _neib_idx -def index_octants(octant, neighb_type neighbors): - n = 1 - for j, idx in enumerate(neib_idx[octant]): +@cython.boundscheck(False) +@cython.wraparound(False) +@cython.cdivision(True) +cdef int index_octants(int octant, + neighb_type neighbors, + int[:, ::1] neib_idx=neib_idx): + # XXX: early binding or just a normal argument for neib_idx? + cdef int n = 1, j, idx + for j in range(7): + idx = neib_idx[octant, j] if neighbors[idx] == 1: - n |= 2**(7 - j) + n |= 2 ** (7 - j) # XXX hardcode powers? return n From c9ed06ad9225b8a60f84d15099dfd2e455952d8e Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Wed, 27 Jan 2016 08:40:48 +0000 Subject: [PATCH 07/39] MAINT: skel3d: statically type Euler LUT --- skimage/morphology/_skel.pyx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/skimage/morphology/_skel.pyx b/skimage/morphology/_skel.pyx index cafe6006..6eedc407 100644 --- a/skimage/morphology/_skel.pyx +++ b/skimage/morphology/_skel.pyx @@ -64,7 +64,7 @@ NUMPOINTS_LUT = fill_numpoints_LUT() def fill_Euler_LUT(): - LUT = np.zeros(256, dtype=int) + LUT = np.zeros(256, dtype=np.intc) LUT[1] = 1 LUT[3] = -1 @@ -200,14 +200,14 @@ def fill_Euler_LUT(): LUT[255] = -1 return LUT -LUT = fill_Euler_LUT() +cdef int[::] LUT = fill_Euler_LUT() ### Octants (indexOctantXXX functions) OCTANTS = tuple(range(8)) NEB, NWB, SEB, SWB, NEU, NWU, SEU, SWU = OCTANTS -_neib_idx = np.empty((8, 7), dtype=np.int32) +_neib_idx = np.empty((8, 7), dtype=np.intc) _neib_idx[NEB, ...] = [2, 1, 11, 10, 5, 4, 14] _neib_idx[NWB, ...] = [0, 9, 3, 12, 1, 10, 4] _neib_idx[SEB, ...] = [8, 7, 17, 16, 5, 4, 14] @@ -241,6 +241,8 @@ def is_surfacepoint(neighbors, points_LUT): return True +@cython.boundscheck(False) +@cython.wraparound(False) cdef bint is_Euler_invariant(neighb_type neighbors): """Check if a point is Euler invariant. From 01e1f04b287fc2db146eb0c0b7830bab9fe52c5a Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Tue, 2 Feb 2016 15:35:55 +0000 Subject: [PATCH 08/39] TST: skel3d: add a Bat Cochlea sample from FIJI collections --- skimage/morphology/skel.py | 2 +- .../tests/data/bat/bat-cochlea-volume.tif | Bin 0 -> 2138695 bytes .../data/bat/bat-cochlea-volume_fiji.tif | Bin 0 -> 2138695 bytes .../morphology/tests/test_skeletonize_3d.py | 5 +++++ 4 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 skimage/morphology/tests/data/bat/bat-cochlea-volume.tif create mode 100644 skimage/morphology/tests/data/bat/bat-cochlea-volume_fiji.tif diff --git a/skimage/morphology/skel.py b/skimage/morphology/skel.py index eff1e56c..2da3979e 100644 --- a/skimage/morphology/skel.py +++ b/skimage/morphology/skel.py @@ -36,7 +36,7 @@ def _postprocess_image(img_o): def compute_thin_image(img_in): img = _prepare_image(img_in) - img = _compute_thin_image(img) + img = np.asarray(_compute_thin_image(img)) img = _postprocess_image(img) return img diff --git a/skimage/morphology/tests/data/bat/bat-cochlea-volume.tif b/skimage/morphology/tests/data/bat/bat-cochlea-volume.tif new file mode 100644 index 0000000000000000000000000000000000000000..0613f1e71e92c83930770a2dd8cf8dc5a4ccc5be GIT binary patch literal 2138695 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 literal 0 HcmV?d00001 diff --git a/skimage/morphology/tests/data/bat/bat-cochlea-volume_fiji.tif b/skimage/morphology/tests/data/bat/bat-cochlea-volume_fiji.tif new file mode 100644 index 0000000000000000000000000000000000000000..e6ce0122c41feae592b5141247c70d7256b0f186 GIT binary patch literal 2138695 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 T(;v!^e!_`gO-i5rrMmwILrd19 literal 0 HcmV?d00001 diff --git a/skimage/morphology/tests/test_skeletonize_3d.py b/skimage/morphology/tests/test_skeletonize_3d.py index 4bd48af7..985a5bde 100644 --- a/skimage/morphology/tests/test_skeletonize_3d.py +++ b/skimage/morphology/tests/test_skeletonize_3d.py @@ -25,6 +25,11 @@ def test_simple_3d(): for fname in ['3/stack', '4/stack']: yield check_skel_3d, fname +# 'slow' test: Bat Cochlea from FIJI collections. +def test_large(): + for fname in ['bat/bat-cochlea-volume',]: + yield check_skel_3d, fname + def get_data_path(): # XXX this is a bad temp hack From b7889b2c4f398f292610ead86eb514ff4e08d820 Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Wed, 27 Jan 2016 11:07:08 +0000 Subject: [PATCH 09/39] MAINT: avoid doing np.sum in the hot loop (you gotta be kidding me, x2 speed-up) --- skimage/morphology/_skel.pyx | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/skimage/morphology/_skel.pyx b/skimage/morphology/_skel.pyx index 6eedc407..a500fb66 100644 --- a/skimage/morphology/_skel.pyx +++ b/skimage/morphology/_skel.pyx @@ -241,6 +241,18 @@ def is_surfacepoint(neighbors, points_LUT): return True +@cython.boundscheck(False) +@cython.wraparound(False) +cdef inline bint is_endpoint(neighb_type neighbors): + """An endpoint has exactly one neighbor in the 26-neighborhood. + """ + # The center pixel is counted, thus r.h.s. is 2 + cdef int s = 0, j + for j in range(27): + s += neighbors[j] + return s == 2 + + @cython.boundscheck(False) @cython.wraparound(False) cdef bint is_Euler_invariant(neighb_type neighbors): @@ -612,14 +624,12 @@ cdef list _loop_through(img_type img, get_neighborhood(img, p, r, c, neighborhood) - # check if (p, r, c) is an endpoint. An endpoint has exactly - # one neighbor in the 26-neighborhood. - # The center pixel is counted, thus r.h.s. is 2 - if np.sum(neighborhood) == 2: + # check if (p, r, c) is an endpoint: endpoints are not deletable. + if is_endpoint(neighborhood): continue - # check if point is Euler invariant (condition 1 in [Lee94]) - # if it is not, it's not deletable + # check if point is Euler invariant (condition 1 in [Lee94]): + # if it is not, it's not deletable. if not is_Euler_invariant(neighborhood): continue From 2f0d0cc18cc651299fe9cc0febd3521180f87b89 Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Wed, 27 Jan 2016 12:41:08 +0000 Subject: [PATCH 10/39] MAINT: avoid incrurring np.r_ overhead in is_simple_point (yet another x1.6) --- skimage/morphology/_skel.pyx | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/skimage/morphology/_skel.pyx b/skimage/morphology/_skel.pyx index a500fb66..24b2c9c4 100644 --- a/skimage/morphology/_skel.pyx +++ b/skimage/morphology/_skel.pyx @@ -4,15 +4,14 @@ import numpy as np from numpy cimport npy_intp, npy_uint8 cimport cython -ctypedef npy_uint8[:, :, ::1] img_type -ctypedef npy_uint8[::1] neighb_type +ctypedef npy_uint8 pixel_type @cython.boundscheck(False) @cython.wraparound(False) -cdef void get_neighborhood(img_type img, +cdef void get_neighborhood(pixel_type[:, :, ::1] img, npy_intp p, npy_intp r, npy_intp c, - neighb_type neighborhood): + pixel_type[::1] neighborhood): """Get the neighborhood of a pixel. Assume zero boundary conditions. Image is already padded, so no @@ -222,7 +221,7 @@ cdef int[:, ::1] neib_idx = _neib_idx @cython.wraparound(False) @cython.cdivision(True) cdef int index_octants(int octant, - neighb_type neighbors, + pixel_type[::1] neighbors, int[:, ::1] neib_idx=neib_idx): # XXX: early binding or just a normal argument for neib_idx? cdef int n = 1, j, idx @@ -243,7 +242,7 @@ def is_surfacepoint(neighbors, points_LUT): @cython.boundscheck(False) @cython.wraparound(False) -cdef inline bint is_endpoint(neighb_type neighbors): +cdef inline bint is_endpoint(pixel_type[::1] neighbors): """An endpoint has exactly one neighbor in the 26-neighborhood. """ # The center pixel is counted, thus r.h.s. is 2 @@ -255,7 +254,7 @@ cdef inline bint is_endpoint(neighb_type neighbors): @cython.boundscheck(False) @cython.wraparound(False) -cdef bint is_Euler_invariant(neighb_type neighbors): +cdef bint is_Euler_invariant(pixel_type[::1] neighbors): """Check if a point is Euler invariant. Calculate Euler characteristc for each octant and sum up. @@ -280,7 +279,7 @@ cdef bint is_Euler_invariant(neighb_type neighbors): @cython.boundscheck(False) @cython.wraparound(False) -cdef bint is_simple_point(neighb_type neighbors): +cdef bint is_simple_point(pixel_type[::1] neighbors): """Check is a point is a Simple Point. This method is named 'N(v)_labeling' in [Lee94]. @@ -300,11 +299,14 @@ cdef bint is_simple_point(neighb_type neighbors): """ # copy neighbors for labeling # ignore center pixel (i=13) when counting (see [Lee94]) - cdef neighb_type cube = np.r_[neighbors[:13], neighbors[14:]] - cdef int i + cdef: + pixel_type a_cube[26] + pixel_type[::1] cube = a_cube + cube[:13] = neighbors[:13] + cube[13:] = neighbors[14:] # set initial label - cdef int label = 2 + cdef int label = 2, i # for all point in the neighborhood for i in range(26): @@ -337,7 +339,7 @@ cdef bint is_simple_point(neighb_type neighbors): @cython.boundscheck(False) @cython.wraparound(False) -cdef void octree_labeling(int octant, int label, neighb_type cube): +cdef void octree_labeling(int octant, int label, pixel_type[::1] cube): """This is a recursive method that calculates the number of connected components in the 3D neighborhood after the center pixel would have been removed. @@ -582,7 +584,7 @@ cdef void octree_labeling(int octant, int label, neighb_type cube): @cython.boundscheck(False) @cython.wraparound(False) -cdef list _loop_through(img_type img, +cdef list _loop_through(pixel_type[:, :, ::1] img, int curr_border): """Inner loop of compute_thin_image. @@ -590,7 +592,7 @@ cdef list _loop_through(img_type img, """ cdef: list simple_border_points = [] - neighb_type neighborhood = np.zeros(27, dtype=np.uint8) + pixel_type[::1] neighborhood = np.zeros(27, dtype=np.uint8) npy_intp p, r, c bint is_border_pt @@ -651,7 +653,7 @@ def _compute_thin_image(img): npy_intp p, r, c bint no_change list simple_border_points - neighb_type neighb = np.zeros(27, dtype=np.uint8) + pixel_type[::1] neighb = np.zeros(27, dtype=np.uint8) # loop through the image several times until there is no change for all # the six border types From c60b6b05cfcc27177c758427c6da044ab76e1ad8 Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Wed, 27 Jan 2016 12:53:14 +0000 Subject: [PATCH 11/39] MAINT: statically type `img` in toplevel _compute_thin_image --- skimage/morphology/_skel.pyx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/skimage/morphology/_skel.pyx b/skimage/morphology/_skel.pyx index 24b2c9c4..f576d10e 100644 --- a/skimage/morphology/_skel.pyx +++ b/skimage/morphology/_skel.pyx @@ -646,7 +646,9 @@ cdef list _loop_through(pixel_type[:, :, ::1] img, return simple_border_points -def _compute_thin_image(img): +@cython.boundscheck(False) +@cython.wraparound(False) +def _compute_thin_image(pixel_type[:, :, ::1] img not None): cdef: int unchanged_borders = 0, curr_border @@ -680,4 +682,4 @@ def _compute_thin_image(img): unchanged_borders += 1 simple_border_points = [] - return img + return np.asarray(img) From 689980124dff04b53935442f6a352df3c1c22462 Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Wed, 27 Jan 2016 14:11:10 +0000 Subject: [PATCH 12/39] MAINT: skel3d: simplify dealing with 2D images --- skimage/morphology/_skel.pyx | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/skimage/morphology/_skel.pyx b/skimage/morphology/_skel.pyx index f576d10e..b598f3db 100644 --- a/skimage/morphology/_skel.pyx +++ b/skimage/morphology/_skel.pyx @@ -598,14 +598,6 @@ cdef list _loop_through(pixel_type[:, :, ::1] img, # loop through the image # NB: each loop is from 1 to size-1: img is padded from all sides - - ### if the original is 2D, img.shape[0] == 3, the algorithm removes too much - ### because all points are considered 'boundary' in the 3rd direction. - ### Hence just bail out. - if img.shape[0] == 3 and curr_border in (5, 6): - print("skipping curr_border = ", curr_border) - return [] - for p in range(1, img.shape[0] - 1): for r in range(1, img.shape[1] - 1): for c in range(1, img.shape[2] - 1): @@ -651,17 +643,27 @@ cdef list _loop_through(pixel_type[:, :, ::1] img, def _compute_thin_image(pixel_type[:, :, ::1] img not None): cdef: - int unchanged_borders = 0, curr_border + int unchanged_borders = 0, curr_border, num_borders + int borders[6] npy_intp p, r, c bint no_change list simple_border_points pixel_type[::1] neighb = np.zeros(27, dtype=np.uint8) + borders[:] = [4, 3, 2, 1, 5, 6] + + # no need to worry about the z direction if the original image is 2D. + if img.shape[0] == 3: + num_borders = 4 + else: + num_borders = 6 + # loop through the image several times until there is no change for all # the six border types - while unchanged_borders < 6: + while unchanged_borders < num_borders: unchanged_borders = 0 - for curr_border in [4, 3, 2, 1, 5, 6]: + for j in range(num_borders): + curr_border = borders[j] simple_border_points = _loop_through(img, curr_border) print(curr_border, " : ", simple_border_points, '\n') @@ -676,7 +678,7 @@ def _compute_thin_image(pixel_type[:, :, ::1] img not None): img[p, r, c] = 0 no_change = False else: - print(" *** ", pt, is_simple_point(neighb)) + print(" *** ", pt, " is not simple.") if no_change: unchanged_borders += 1 From 88a9bcc874bfe8ce7dc1a93653236bf17592a3c7 Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Tue, 2 Feb 2016 15:52:39 +0000 Subject: [PATCH 13/39] DOC: skel3d: add basic docstrings --- skimage/morphology/_skel.pyx | 81 ++++++++++++++++++++++++++++++++---- skimage/morphology/skel.py | 3 ++ 2 files changed, 77 insertions(+), 7 deletions(-) diff --git a/skimage/morphology/_skel.pyx b/skimage/morphology/_skel.pyx index b598f3db..937b8a8e 100644 --- a/skimage/morphology/_skel.pyx +++ b/skimage/morphology/_skel.pyx @@ -1,3 +1,34 @@ +""" +This is an implementation of the 2D/3D thinning algorithm +of [Lee94] of binary images, based on [IAC15]. + +The original Java code [IAC15] carries the following message: + + * This work is an implementation by Ignacio Arganda-Carreras of the + * 3D thinning algorithm from Lee et al. "Building skeleton models via 3-D + * medial surface/axis thinning algorithms. Computer Vision, Graphics, and + * Image Processing, 56(6):462–478, 1994." Based on the ITK version from + * Hanno Homann http://hdl.handle.net/1926/1292 + *

+ * More information at Skeletonize3D homepage: + * http://fiji.sc/Skeletonize3D + * + * @version 1.0 11/13/2015 (unique BSD licensed version for scikit-image) + * @author Ignacio Arganda-Carreras (iargandacarreras at gmail.com) + +Porting to Cython was done by Evgeni Burovski (evgeny.burovskiy@gmail.com). + +References +---------- + +.. [Lee94] Lee et al, Building skeleton models via 3-D medial surface/axis + thinning algorithms. Computer Vision, Graphics, and Image Processing, + 56(6):462–478, 1994 + +.. [IAC15] Ignacio Arganda-Carreras, 2015. Skeletonize3D plugin for ImageJ(C). + http://fiji.sc/Skeletonize3D + +""" from __future__ import division, print_function, absolute_import import numpy as np @@ -14,8 +45,12 @@ cdef void get_neighborhood(pixel_type[:, :, ::1] img, pixel_type[::1] neighborhood): """Get the neighborhood of a pixel. - Assume zero boundary conditions. Image is already padded, so no - out-of-bounds checking. + Assume zero boundary conditions. + Image is already padded, so no out-of-bounds checking. + + For the numbering of points see Fig. 1a. of [Lee94], where the numbers + do *not* include the center point itself. OTOH, this numbering below + includes it as number 13. The latter is consistent with [IAC15]. """ neighborhood[0] = img[p-1, r-1, c-1] neighborhood[1] = img[p-1, r, c-1] @@ -63,6 +98,10 @@ NUMPOINTS_LUT = fill_numpoints_LUT() def fill_Euler_LUT(): + """ Look-up table for preserving Euler characteristic. + + This is column $\delta G_{26}$ of Table 2 of [Lee94]. + """ LUT = np.zeros(256, dtype=np.intc) LUT[1] = 1 @@ -217,6 +256,7 @@ _neib_idx[SEU, ...] = [26, 23, 17, 14, 25, 22, 16] _neib_idx[SWU, ...] = [24, 25, 15, 16, 21, 22, 12] cdef int[:, ::1] neib_idx = _neib_idx + @cython.boundscheck(False) @cython.wraparound(False) @cython.cdivision(True) @@ -266,7 +306,7 @@ cdef bint is_Euler_invariant(pixel_type[::1] neighbors): Returns ------- - bool + bool (C bool, that is) """ cdef int octant, n @@ -282,7 +322,7 @@ cdef bint is_Euler_invariant(pixel_type[::1] neighbors): cdef bint is_simple_point(pixel_type[::1] neighbors): """Check is a point is a Simple Point. - This method is named 'N(v)_labeling' in [Lee94]. + This method is named "N(v)_labeling" in [Lee94]. Outputs the number of connected objects in a neighborhood of a point after this point would have been removed. @@ -344,6 +384,8 @@ cdef void octree_labeling(int octant, int label, pixel_type[::1] cube): components in the 3D neighborhood after the center pixel would have been removed. + See Figs. 6 and 7 of [Lee94] for the values of indices. + Parameters ---------- octant : int @@ -588,8 +630,17 @@ cdef list _loop_through(pixel_type[:, :, ::1] img, int curr_border): """Inner loop of compute_thin_image. - return simple_border_points as a list to be rechecked sequentially. + The algorithm of [Lee94] proceeds in two steps: (1) six directions are + checked for simple border points to remove, and (2) these candidates are + sequentially rechecked, see Sec 3 of [Lee94] for rationale and discussion. + + This routine implements the first step above: it loops over the image + for a given direction and assembles candidates for removal. + """ + # This routine looks like it could be nogil, but actually it cannot be, + # because of `simple_border_points` being a python list which is being + # mutated. cdef: list simple_border_points = [] pixel_type[::1] neighborhood = np.zeros(27, dtype=np.uint8) @@ -618,7 +669,7 @@ cdef list _loop_through(pixel_type[:, :, ::1] img, get_neighborhood(img, p, r, c, neighborhood) - # check if (p, r, c) is an endpoint: endpoints are not deletable. + # check if (p, r, c) is an endpoint (then it's not deletable.) if is_endpoint(neighborhood): continue @@ -641,7 +692,24 @@ cdef list _loop_through(pixel_type[:, :, ::1] img, @cython.boundscheck(False) @cython.wraparound(False) def _compute_thin_image(pixel_type[:, :, ::1] img not None): + """Compute a thin image. + Loop through the image multiple times, removing "simple" points, i.e. + those point which can be removed without changing local connectivity in the + 3x3x3 neighborhood of a point. + + This routine implements the two-pass algorthim of [Lee94]. Namely, + for each of the six border types (positive and negative x-, y- and z-), + the algorithm first collects all possibly deletable points, and then + performs a sequential rechecking. + + The input, `img`, is assumed to be a 3D binary image in the + (p, r, c) format [i.e., C ordered array], filled by zeros (background) and + ones. Furthermore, `img` is assumed to be padded by zeros from all + directions --- this way the zero boundary conditions are authomatic + and there is need to guard against out-of-bounds access. + + """ cdef: int unchanged_borders = 0, curr_border, num_borders int borders[6] @@ -649,7 +717,6 @@ def _compute_thin_image(pixel_type[:, :, ::1] img not None): bint no_change list simple_border_points pixel_type[::1] neighb = np.zeros(27, dtype=np.uint8) - borders[:] = [4, 3, 2, 1, 5, 6] # no need to worry about the z direction if the original image is 2D. diff --git a/skimage/morphology/skel.py b/skimage/morphology/skel.py index 2da3979e..5af021b9 100644 --- a/skimage/morphology/skel.py +++ b/skimage/morphology/skel.py @@ -27,6 +27,7 @@ def _prepare_image(img_in): def _postprocess_image(img_o): """Clip the image (padding is an implementation detail), convert to b/w. + If the original was 2D, convert back to 2D. """ img_oo = img_o[1:-1, 1:-1, 1:-1] img_oo = img_oo.squeeze() @@ -35,6 +36,8 @@ def _postprocess_image(img_o): def compute_thin_image(img_in): + """Compute the thin image. + """ img = _prepare_image(img_in) img = np.asarray(_compute_thin_image(img)) img = _postprocess_image(img) From 42ba24deb3ae88307c0ec3d22b6877919ae054e1 Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Thu, 28 Jan 2016 17:41:42 +0000 Subject: [PATCH 14/39] MAINT: skel3d: don't print stuff --- skimage/morphology/_skel.pyx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/skimage/morphology/_skel.pyx b/skimage/morphology/_skel.pyx index 937b8a8e..8b3e37c3 100644 --- a/skimage/morphology/_skel.pyx +++ b/skimage/morphology/_skel.pyx @@ -733,7 +733,7 @@ def _compute_thin_image(pixel_type[:, :, ::1] img not None): curr_border = borders[j] simple_border_points = _loop_through(img, curr_border) - print(curr_border, " : ", simple_border_points, '\n') + ## print(curr_border, " : ", simple_border_points, '\n') # sequential re-checking to preserve connectivity when deleting # in a parallel way @@ -745,7 +745,8 @@ def _compute_thin_image(pixel_type[:, :, ::1] img not None): img[p, r, c] = 0 no_change = False else: - print(" *** ", pt, " is not simple.") + pass + ## print(" *** ", pt, " is not simple.") if no_change: unchanged_borders += 1 From f82dddd475d3051f1c5f6d24f2abcffc5f794012 Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Thu, 28 Jan 2016 19:36:01 +0000 Subject: [PATCH 15/39] MAINT: stop slicing memoryviews in is_simple_point. Use plain C arrays for neighbors[27] + memcpy when copying the `cube` for labeling. Apparently, slicing memoryviews *is* expensive (cf Jake vdP'2012). --- skimage/morphology/_skel.pyx | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/skimage/morphology/_skel.pyx b/skimage/morphology/_skel.pyx index 8b3e37c3..9ca43e80 100644 --- a/skimage/morphology/_skel.pyx +++ b/skimage/morphology/_skel.pyx @@ -31,6 +31,8 @@ References """ from __future__ import division, print_function, absolute_import +from libc.string cimport memcpy + import numpy as np from numpy cimport npy_intp, npy_uint8 cimport cython @@ -42,7 +44,7 @@ ctypedef npy_uint8 pixel_type @cython.wraparound(False) cdef void get_neighborhood(pixel_type[:, :, ::1] img, npy_intp p, npy_intp r, npy_intp c, - pixel_type[::1] neighborhood): + pixel_type neighborhood[]): """Get the neighborhood of a pixel. Assume zero boundary conditions. @@ -261,7 +263,7 @@ cdef int[:, ::1] neib_idx = _neib_idx @cython.wraparound(False) @cython.cdivision(True) cdef int index_octants(int octant, - pixel_type[::1] neighbors, + pixel_type neighbors[], int[:, ::1] neib_idx=neib_idx): # XXX: early binding or just a normal argument for neib_idx? cdef int n = 1, j, idx @@ -282,7 +284,7 @@ def is_surfacepoint(neighbors, points_LUT): @cython.boundscheck(False) @cython.wraparound(False) -cdef inline bint is_endpoint(pixel_type[::1] neighbors): +cdef inline bint is_endpoint(pixel_type neighbors[]): """An endpoint has exactly one neighbor in the 26-neighborhood. """ # The center pixel is counted, thus r.h.s. is 2 @@ -294,7 +296,7 @@ cdef inline bint is_endpoint(pixel_type[::1] neighbors): @cython.boundscheck(False) @cython.wraparound(False) -cdef bint is_Euler_invariant(pixel_type[::1] neighbors): +cdef bint is_Euler_invariant(pixel_type neighbors[]): """Check if a point is Euler invariant. Calculate Euler characteristc for each octant and sum up. @@ -319,7 +321,7 @@ cdef bint is_Euler_invariant(pixel_type[::1] neighbors): @cython.boundscheck(False) @cython.wraparound(False) -cdef bint is_simple_point(pixel_type[::1] neighbors): +cdef bint is_simple_point(pixel_type neighbors[]): """Check is a point is a Simple Point. This method is named "N(v)_labeling" in [Lee94]. @@ -339,11 +341,9 @@ cdef bint is_simple_point(pixel_type[::1] neighbors): """ # copy neighbors for labeling # ignore center pixel (i=13) when counting (see [Lee94]) - cdef: - pixel_type a_cube[26] - pixel_type[::1] cube = a_cube - cube[:13] = neighbors[:13] - cube[13:] = neighbors[14:] + cdef pixel_type cube[26] + memcpy(cube, neighbors, 13*sizeof(pixel_type)) + memcpy(cube+13, neighbors+14, 13*sizeof(pixel_type)) # set initial label cdef int label = 2, i @@ -379,7 +379,7 @@ cdef bint is_simple_point(pixel_type[::1] neighbors): @cython.boundscheck(False) @cython.wraparound(False) -cdef void octree_labeling(int octant, int label, pixel_type[::1] cube): +cdef void octree_labeling(int octant, int label, pixel_type cube[]): """This is a recursive method that calculates the number of connected components in the 3D neighborhood after the center pixel would have been removed. @@ -643,7 +643,7 @@ cdef list _loop_through(pixel_type[:, :, ::1] img, # mutated. cdef: list simple_border_points = [] - pixel_type[::1] neighborhood = np.zeros(27, dtype=np.uint8) + pixel_type neighborhood[27] npy_intp p, r, c bint is_border_pt @@ -716,7 +716,7 @@ def _compute_thin_image(pixel_type[:, :, ::1] img not None): npy_intp p, r, c bint no_change list simple_border_points - pixel_type[::1] neighb = np.zeros(27, dtype=np.uint8) + pixel_type neighb[27] borders[:] = [4, 3, 2, 1, 5, 6] # no need to worry about the z direction if the original image is 2D. From dc6cf19fecbd64406033472141a8852d5d58a9cf Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Thu, 28 Jan 2016 20:45:26 +0000 Subject: [PATCH 16/39] STY: move main functions to the top of the file --- skimage/morphology/_skel.pyx | 262 +++++++++++++++++------------------ 1 file changed, 131 insertions(+), 131 deletions(-) diff --git a/skimage/morphology/_skel.pyx b/skimage/morphology/_skel.pyx index 9ca43e80..8ef752b9 100644 --- a/skimage/morphology/_skel.pyx +++ b/skimage/morphology/_skel.pyx @@ -40,6 +40,137 @@ cimport cython ctypedef npy_uint8 pixel_type +@cython.boundscheck(False) +@cython.wraparound(False) +def _compute_thin_image(pixel_type[:, :, ::1] img not None): + """Compute a thin image. + + Loop through the image multiple times, removing "simple" points, i.e. + those point which can be removed without changing local connectivity in the + 3x3x3 neighborhood of a point. + + This routine implements the two-pass algorthim of [Lee94]. Namely, + for each of the six border types (positive and negative x-, y- and z-), + the algorithm first collects all possibly deletable points, and then + performs a sequential rechecking. + + The input, `img`, is assumed to be a 3D binary image in the + (p, r, c) format [i.e., C ordered array], filled by zeros (background) and + ones. Furthermore, `img` is assumed to be padded by zeros from all + directions --- this way the zero boundary conditions are authomatic + and there is need to guard against out-of-bounds access. + + """ + cdef: + int unchanged_borders = 0, curr_border, num_borders + int borders[6] + npy_intp p, r, c + bint no_change + list simple_border_points + pixel_type neighb[27] + borders[:] = [4, 3, 2, 1, 5, 6] + + # no need to worry about the z direction if the original image is 2D. + if img.shape[0] == 3: + num_borders = 4 + else: + num_borders = 6 + + # loop through the image several times until there is no change for all + # the six border types + while unchanged_borders < num_borders: + unchanged_borders = 0 + for j in range(num_borders): + curr_border = borders[j] + + simple_border_points = _loop_through(img, curr_border) + ## print(curr_border, " : ", simple_border_points, '\n') + + # sequential re-checking to preserve connectivity when deleting + # in a parallel way + no_change = True + for pt in simple_border_points: + p, r, c = pt + get_neighborhood(img, p, r, c, neighb) + if is_simple_point(neighb): + img[p, r, c] = 0 + no_change = False + else: + pass + ## print(" *** ", pt, " is not simple.") + + if no_change: + unchanged_borders += 1 + simple_border_points = [] + + return np.asarray(img) + + +@cython.boundscheck(False) +@cython.wraparound(False) +cdef list _loop_through(pixel_type[:, :, ::1] img, + int curr_border): + """Inner loop of compute_thin_image. + + The algorithm of [Lee94] proceeds in two steps: (1) six directions are + checked for simple border points to remove, and (2) these candidates are + sequentially rechecked, see Sec 3 of [Lee94] for rationale and discussion. + + This routine implements the first step above: it loops over the image + for a given direction and assembles candidates for removal. + + """ + # This routine looks like it could be nogil, but actually it cannot be, + # because of `simple_border_points` being a python list which is being + # mutated. + cdef: + list simple_border_points = [] + pixel_type neighborhood[27] + npy_intp p, r, c + bint is_border_pt + + # loop through the image + # NB: each loop is from 1 to size-1: img is padded from all sides + for p in range(1, img.shape[0] - 1): + for r in range(1, img.shape[1] - 1): + for c in range(1, img.shape[2] - 1): + + # check if pixel is foreground + if img[p, r, c] != 1: + continue + + is_border_pt = (curr_border == 1 and img[p, r, c-1] <= 0 or #N + curr_border == 2 and img[p, r, c+1] <= 0 or #S + curr_border == 3 and img[p, r+1, c] <= 0 or #E + curr_border == 4 and img[p, r-1, c] <= 0 or #W + curr_border == 5 and img[p+1, r, c] <= 0 or #U + curr_border == 6 and img[p-1, r, c] <= 0) #B + if not is_border_pt: + # current point is not deletable + continue + + get_neighborhood(img, p, r, c, neighborhood) + + # check if (p, r, c) is an endpoint (then it's not deletable.) + if is_endpoint(neighborhood): + continue + + # check if point is Euler invariant (condition 1 in [Lee94]): + # if it is not, it's not deletable. + if not is_Euler_invariant(neighborhood): + continue + + # check if point is simple (i.e., deletion does not + # change connectivity in the 3x3x3 neighborhood) + # this are conditions 2 and 3 in [Lee94] + if not is_simple_point(neighborhood): + continue + + # ok, add (p, r, c) to the list of simple border points + simple_border_points.append((p, r, c)) + return simple_border_points + + @cython.boundscheck(False) @cython.wraparound(False) cdef void get_neighborhood(pixel_type[:, :, ::1] img, @@ -622,134 +753,3 @@ cdef void octree_labeling(int octant, int label, pixel_type cube[]): octree_labeling(7, label, cube) if cube[25] == 1: cube[25] = label - - -@cython.boundscheck(False) -@cython.wraparound(False) -cdef list _loop_through(pixel_type[:, :, ::1] img, - int curr_border): - """Inner loop of compute_thin_image. - - The algorithm of [Lee94] proceeds in two steps: (1) six directions are - checked for simple border points to remove, and (2) these candidates are - sequentially rechecked, see Sec 3 of [Lee94] for rationale and discussion. - - This routine implements the first step above: it loops over the image - for a given direction and assembles candidates for removal. - - """ - # This routine looks like it could be nogil, but actually it cannot be, - # because of `simple_border_points` being a python list which is being - # mutated. - cdef: - list simple_border_points = [] - pixel_type neighborhood[27] - npy_intp p, r, c - bint is_border_pt - - # loop through the image - # NB: each loop is from 1 to size-1: img is padded from all sides - for p in range(1, img.shape[0] - 1): - for r in range(1, img.shape[1] - 1): - for c in range(1, img.shape[2] - 1): - - # check if pixel is foreground - if img[p, r, c] != 1: - continue - - is_border_pt = (curr_border == 1 and img[p, r, c-1] <= 0 or #N - curr_border == 2 and img[p, r, c+1] <= 0 or #S - curr_border == 3 and img[p, r+1, c] <= 0 or #E - curr_border == 4 and img[p, r-1, c] <= 0 or #W - curr_border == 5 and img[p+1, r, c] <= 0 or #U - curr_border == 6 and img[p-1, r, c] <= 0) #B - if not is_border_pt: - # current point is not deletable - continue - - get_neighborhood(img, p, r, c, neighborhood) - - # check if (p, r, c) is an endpoint (then it's not deletable.) - if is_endpoint(neighborhood): - continue - - # check if point is Euler invariant (condition 1 in [Lee94]): - # if it is not, it's not deletable. - if not is_Euler_invariant(neighborhood): - continue - - # check if point is simple (i.e., deletion does not - # change connectivity in the 3x3x3 neighborhood) - # this are conditions 2 and 3 in [Lee94] - if not is_simple_point(neighborhood): - continue - - # ok, add (p, r, c) to the list of simple border points - simple_border_points.append((p, r, c)) - return simple_border_points - - -@cython.boundscheck(False) -@cython.wraparound(False) -def _compute_thin_image(pixel_type[:, :, ::1] img not None): - """Compute a thin image. - - Loop through the image multiple times, removing "simple" points, i.e. - those point which can be removed without changing local connectivity in the - 3x3x3 neighborhood of a point. - - This routine implements the two-pass algorthim of [Lee94]. Namely, - for each of the six border types (positive and negative x-, y- and z-), - the algorithm first collects all possibly deletable points, and then - performs a sequential rechecking. - - The input, `img`, is assumed to be a 3D binary image in the - (p, r, c) format [i.e., C ordered array], filled by zeros (background) and - ones. Furthermore, `img` is assumed to be padded by zeros from all - directions --- this way the zero boundary conditions are authomatic - and there is need to guard against out-of-bounds access. - - """ - cdef: - int unchanged_borders = 0, curr_border, num_borders - int borders[6] - npy_intp p, r, c - bint no_change - list simple_border_points - pixel_type neighb[27] - borders[:] = [4, 3, 2, 1, 5, 6] - - # no need to worry about the z direction if the original image is 2D. - if img.shape[0] == 3: - num_borders = 4 - else: - num_borders = 6 - - # loop through the image several times until there is no change for all - # the six border types - while unchanged_borders < num_borders: - unchanged_borders = 0 - for j in range(num_borders): - curr_border = borders[j] - - simple_border_points = _loop_through(img, curr_border) - ## print(curr_border, " : ", simple_border_points, '\n') - - # sequential re-checking to preserve connectivity when deleting - # in a parallel way - no_change = True - for pt in simple_border_points: - p, r, c = pt - get_neighborhood(img, p, r, c, neighb) - if is_simple_point(neighb): - img[p, r, c] = 0 - no_change = False - else: - pass - ## print(" *** ", pt, " is not simple.") - - if no_change: - unchanged_borders += 1 - simple_border_points = [] - - return np.asarray(img) From ecf744542e8ddc13473e91a81dacc458ef771d19 Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Thu, 28 Jan 2016 20:57:12 +0000 Subject: [PATCH 17/39] MAINT: remove decorators which have no effect anyway --- skimage/morphology/_skel.pyx | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/skimage/morphology/_skel.pyx b/skimage/morphology/_skel.pyx index 8ef752b9..53696032 100644 --- a/skimage/morphology/_skel.pyx +++ b/skimage/morphology/_skel.pyx @@ -413,8 +413,6 @@ def is_surfacepoint(neighbors, points_LUT): return True -@cython.boundscheck(False) -@cython.wraparound(False) cdef inline bint is_endpoint(pixel_type neighbors[]): """An endpoint has exactly one neighbor in the 26-neighborhood. """ @@ -434,7 +432,7 @@ cdef bint is_Euler_invariant(pixel_type neighbors[]): Parameters ---------- - neighbors : ndarray, shape (27,) + neighbors : uint8 C array, shape (27,) neighbors of a point Returns @@ -442,16 +440,13 @@ cdef bint is_Euler_invariant(pixel_type neighbors[]): bool (C bool, that is) """ - cdef int octant, n - cdef int euler_char = 0 - for octant in xrange(8): + cdef int octant, n, euler_char = 0 + for octant in range(8): n = index_octants(octant, neighbors) euler_char += LUT[n] return euler_char == 0 -@cython.boundscheck(False) -@cython.wraparound(False) cdef bint is_simple_point(pixel_type neighbors[]): """Check is a point is a Simple Point. @@ -461,7 +456,7 @@ cdef bint is_simple_point(pixel_type neighbors[]): Parameters ---------- - neighbors : ndarray, shape(27,) + neighbors : uint8 C array, shape(27,) neighbors of the point Returns @@ -523,7 +518,7 @@ cdef void octree_labeling(int octant, int label, pixel_type cube[]): octant index label : int the current label of the center point - cube : ndarray, shape(26,) + cube : uint8 C array, shape(26,) local neighborhood of the point """ From 5fa50a67651d4e4167553f5be115fc95460ac2c6 Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Wed, 27 Jan 2016 15:53:20 +0000 Subject: [PATCH 18/39] MAINT: skel3d: prune dead code (eg, is_surface_point) --- skimage/morphology/_skel.pyx | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/skimage/morphology/_skel.pyx b/skimage/morphology/_skel.pyx index 53696032..bb03a16b 100644 --- a/skimage/morphology/_skel.pyx +++ b/skimage/morphology/_skel.pyx @@ -223,13 +223,6 @@ cdef void get_neighborhood(pixel_type[:, :, ::1] img, ###### look-up tables -def fill_numpoints_LUT(n=256): - p = int(np.log2(n) + 1) - return np.sum(np.arange(n)[:, None] & (1 << np.arange(p)) != 0, axis=1) - -NUMPOINTS_LUT = fill_numpoints_LUT() - - def fill_Euler_LUT(): """ Look-up table for preserving Euler characteristic. @@ -405,14 +398,6 @@ cdef int index_octants(int octant, return n -def is_surfacepoint(neighbors, points_LUT): - for octant in OCTANTS: - n = index_octants(octant, neighbors) - if n not in (240, 165, 170) and points_LUT[n] > 2: - return False - return True - - cdef inline bint is_endpoint(pixel_type neighbors[]): """An endpoint has exactly one neighbor in the 26-neighborhood. """ From 456a5252c9ef4282e7f6949e70872758d3ef1343 Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Thu, 28 Jan 2016 22:39:51 +0000 Subject: [PATCH 19/39] STY: skel3d: clarify the status of module-level LUTs. Fill in at the module level, then rebind in compute_thin_image, and pass around as arguments to relevant functions. --- skimage/morphology/_skel.pyx | 56 ++++++++++++++++++++++-------------- 1 file changed, 35 insertions(+), 21 deletions(-) diff --git a/skimage/morphology/_skel.pyx b/skimage/morphology/_skel.pyx index bb03a16b..8cd79133 100644 --- a/skimage/morphology/_skel.pyx +++ b/skimage/morphology/_skel.pyx @@ -129,6 +129,11 @@ cdef list _loop_through(pixel_type[:, :, ::1] img, npy_intp p, r, c bint is_border_pt + # rebind global names to avoid lookup. Both tables are filled in + # at import time. + int[::1] Euler_LUT = LUT + cdef int[:, ::1] neighb_idx = NEIGHB_IDX + # loop through the image # NB: each loop is from 1 to size-1: img is padded from all sides for p in range(1, img.shape[0] - 1): @@ -157,7 +162,7 @@ cdef list _loop_through(pixel_type[:, :, ::1] img, # check if point is Euler invariant (condition 1 in [Lee94]): # if it is not, it's not deletable. - if not is_Euler_invariant(neighborhood): + if not is_Euler_invariant(neighborhood, Euler_LUT, neighb_idx): continue # check if point is simple (i.e., deletion does not @@ -363,24 +368,28 @@ def fill_Euler_LUT(): LUT[253] = 1 LUT[255] = -1 return LUT - -cdef int[::] LUT = fill_Euler_LUT() +cdef int[::1] LUT = fill_Euler_LUT() ### Octants (indexOctantXXX functions) -OCTANTS = tuple(range(8)) -NEB, NWB, SEB, SWB, NEU, NWU, SEU, SWU = OCTANTS +def fill_neighbor_idx(): + """Fill the look-up table for indexing octants for computing the Euler + characteristics. -_neib_idx = np.empty((8, 7), dtype=np.intc) -_neib_idx[NEB, ...] = [2, 1, 11, 10, 5, 4, 14] -_neib_idx[NWB, ...] = [0, 9, 3, 12, 1, 10, 4] -_neib_idx[SEB, ...] = [8, 7, 17, 16, 5, 4, 14] -_neib_idx[SWB, ...] = [6, 15, 7, 16, 3, 12, 4] -_neib_idx[NEU, ...] = [20, 23, 19, 22, 11, 14, 10] -_neib_idx[NWU, ...] = [18, 21, 9, 12, 19, 22, 10] -_neib_idx[SEU, ...] = [26, 23, 17, 14, 25, 22, 16] -_neib_idx[SWU, ...] = [24, 25, 15, 16, 21, 22, 12] -cdef int[:, ::1] neib_idx = _neib_idx + See index_octants and is_Euler_invariant routines below. + """ + NEB, NWB, SEB, SWB, NEU, NWU, SEU, SWU = tuple(range(8)) + _neighb_idx = np.empty((8, 7), dtype=np.intc) + _neighb_idx[NEB, ...] = [2, 1, 11, 10, 5, 4, 14] + _neighb_idx[NWB, ...] = [0, 9, 3, 12, 1, 10, 4] + _neighb_idx[SEB, ...] = [8, 7, 17, 16, 5, 4, 14] + _neighb_idx[SWB, ...] = [6, 15, 7, 16, 3, 12, 4] + _neighb_idx[NEU, ...] = [20, 23, 19, 22, 11, 14, 10] + _neighb_idx[NWU, ...] = [18, 21, 9, 12, 19, 22, 10] + _neighb_idx[SEU, ...] = [26, 23, 17, 14, 25, 22, 16] + _neighb_idx[SWU, ...] = [24, 25, 15, 16, 21, 22, 12] + return _neighb_idx +cdef int[:, ::1] NEIGHB_IDX = fill_neighbor_idx() @cython.boundscheck(False) @@ -388,8 +397,7 @@ cdef int[:, ::1] neib_idx = _neib_idx @cython.cdivision(True) cdef int index_octants(int octant, pixel_type neighbors[], - int[:, ::1] neib_idx=neib_idx): - # XXX: early binding or just a normal argument for neib_idx? + int[:, ::1] neib_idx): cdef int n = 1, j, idx for j in range(7): idx = neib_idx[octant, j] @@ -410,15 +418,21 @@ cdef inline bint is_endpoint(pixel_type neighbors[]): @cython.boundscheck(False) @cython.wraparound(False) -cdef bint is_Euler_invariant(pixel_type neighbors[]): +cdef bint is_Euler_invariant(pixel_type neighbors[], + int[::1] lut, + int[:, ::1] neighb_idx): """Check if a point is Euler invariant. Calculate Euler characteristc for each octant and sum up. Parameters ---------- - neighbors : uint8 C array, shape (27,) + neighbors neighbors of a point + lut + The look-up table for preserving the Euler characteristic. + neighb_idx + The look-up table for indexing octants. Returns ------- @@ -427,8 +441,8 @@ cdef bint is_Euler_invariant(pixel_type neighbors[]): """ cdef int octant, n, euler_char = 0 for octant in range(8): - n = index_octants(octant, neighbors) - euler_char += LUT[n] + n = index_octants(octant, neighbors, neighb_idx) + euler_char += lut[n] return euler_char == 0 From 85f46c6fe81f6a42af1a332f659e0526c8072dd6 Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Sat, 30 Jan 2016 14:01:38 +0000 Subject: [PATCH 20/39] MAINT: skel3d: address review comments * simplify filling in the neighb_idx LUT * underscore references * preserve the color range of the original image --- skimage/morphology/_skel.pyx | 58 ++++++++++++---------------- skimage/morphology/skel.py | 12 +++--- skimage/morphology/trivial.py | 73 ----------------------------------- 3 files changed, 32 insertions(+), 111 deletions(-) delete mode 100644 skimage/morphology/trivial.py diff --git a/skimage/morphology/_skel.pyx b/skimage/morphology/_skel.pyx index 8cd79133..49d69e9e 100644 --- a/skimage/morphology/_skel.pyx +++ b/skimage/morphology/_skel.pyx @@ -1,8 +1,8 @@ """ This is an implementation of the 2D/3D thinning algorithm -of [Lee94] of binary images, based on [IAC15]. +of [Lee94]_ of binary images, based on [IAC15]_. -The original Java code [IAC15] carries the following message: +The original Java code [IAC15]_ carries the following message: * This work is an implementation by Ignacio Arganda-Carreras of the * 3D thinning algorithm from Lee et al. "Building skeleton models via 3-D @@ -49,7 +49,7 @@ def _compute_thin_image(pixel_type[:, :, ::1] img not None): those point which can be removed without changing local connectivity in the 3x3x3 neighborhood of a point. - This routine implements the two-pass algorthim of [Lee94]. Namely, + This routine implements the two-pass algorthim of [Lee94]_. Namely, for each of the six border types (positive and negative x-, y- and z-), the algorithm first collects all possibly deletable points, and then performs a sequential rechecking. @@ -112,9 +112,9 @@ cdef list _loop_through(pixel_type[:, :, ::1] img, int curr_border): """Inner loop of compute_thin_image. - The algorithm of [Lee94] proceeds in two steps: (1) six directions are + The algorithm of [Lee94]_ proceeds in two steps: (1) six directions are checked for simple border points to remove, and (2) these candidates are - sequentially rechecked, see Sec 3 of [Lee94] for rationale and discussion. + sequentially rechecked, see Sec 3 of [Lee94]_ for rationale and discussion. This routine implements the first step above: it loops over the image for a given direction and assembles candidates for removal. @@ -160,14 +160,14 @@ cdef list _loop_through(pixel_type[:, :, ::1] img, if is_endpoint(neighborhood): continue - # check if point is Euler invariant (condition 1 in [Lee94]): + # check if point is Euler invariant (condition 1 in [Lee94]_): # if it is not, it's not deletable. if not is_Euler_invariant(neighborhood, Euler_LUT, neighb_idx): continue # check if point is simple (i.e., deletion does not # change connectivity in the 3x3x3 neighborhood) - # this are conditions 2 and 3 in [Lee94] + # this are conditions 2 and 3 in [Lee94]_ if not is_simple_point(neighborhood): continue @@ -186,9 +186,9 @@ cdef void get_neighborhood(pixel_type[:, :, ::1] img, Assume zero boundary conditions. Image is already padded, so no out-of-bounds checking. - For the numbering of points see Fig. 1a. of [Lee94], where the numbers + For the numbering of points see Fig. 1a. of [Lee94]_, where the numbers do *not* include the center point itself. OTOH, this numbering below - includes it as number 13. The latter is consistent with [IAC15]. + includes it as number 13. The latter is consistent with [IAC15]_. """ neighborhood[0] = img[p-1, r-1, c-1] neighborhood[1] = img[p-1, r, c-1] @@ -231,7 +231,7 @@ cdef void get_neighborhood(pixel_type[:, :, ::1] img, def fill_Euler_LUT(): """ Look-up table for preserving Euler characteristic. - This is column $\delta G_{26}$ of Table 2 of [Lee94]. + This is column $\delta G_{26}$ of Table 2 of [Lee94]_. """ LUT = np.zeros(256, dtype=np.intc) @@ -371,25 +371,17 @@ def fill_Euler_LUT(): cdef int[::1] LUT = fill_Euler_LUT() -### Octants (indexOctantXXX functions) -def fill_neighbor_idx(): - """Fill the look-up table for indexing octants for computing the Euler - characteristics. - - See index_octants and is_Euler_invariant routines below. - """ - NEB, NWB, SEB, SWB, NEU, NWU, SEU, SWU = tuple(range(8)) - _neighb_idx = np.empty((8, 7), dtype=np.intc) - _neighb_idx[NEB, ...] = [2, 1, 11, 10, 5, 4, 14] - _neighb_idx[NWB, ...] = [0, 9, 3, 12, 1, 10, 4] - _neighb_idx[SEB, ...] = [8, 7, 17, 16, 5, 4, 14] - _neighb_idx[SWB, ...] = [6, 15, 7, 16, 3, 12, 4] - _neighb_idx[NEU, ...] = [20, 23, 19, 22, 11, 14, 10] - _neighb_idx[NWU, ...] = [18, 21, 9, 12, 19, 22, 10] - _neighb_idx[SEU, ...] = [26, 23, 17, 14, 25, 22, 16] - _neighb_idx[SWU, ...] = [24, 25, 15, 16, 21, 22, 12] - return _neighb_idx -cdef int[:, ::1] NEIGHB_IDX = fill_neighbor_idx() +# Fill the look-up table for indexing octants for computing the Euler +# characteristic. See index_octants and is_Euler_invariant routines below. +cdef int[:, ::1] NEIGHB_IDX = np.array([[2, 1, 11, 10, 5, 4, 14], # NEB + [0, 9, 3, 12, 1, 10, 4], # NWB + [8, 7, 17, 16, 5, 4, 14], # SEB + [6, 15, 7, 16, 3, 12, 4], # SWB + [20, 23, 19, 22, 11, 14, 10], # NEU + [18, 21, 9, 12, 19, 22, 10], # NWU + [26, 23, 17, 14, 25, 22, 16], # SEU + [24, 25, 15, 16, 21, 22, 12], # SWU + ], dtype=np.intc) @cython.boundscheck(False) @@ -402,7 +394,7 @@ cdef int index_octants(int octant, for j in range(7): idx = neib_idx[octant, j] if neighbors[idx] == 1: - n |= 2 ** (7 - j) # XXX hardcode powers? + n |= 1 << (7 - j) # XXX hardcode powers? return n @@ -449,7 +441,7 @@ cdef bint is_Euler_invariant(pixel_type neighbors[], cdef bint is_simple_point(pixel_type neighbors[]): """Check is a point is a Simple Point. - This method is named "N(v)_labeling" in [Lee94]. + This method is named "N(v)_labeling" in [Lee94]_. Outputs the number of connected objects in a neighborhood of a point after this point would have been removed. @@ -465,7 +457,7 @@ cdef bint is_simple_point(pixel_type neighbors[]): """ # copy neighbors for labeling - # ignore center pixel (i=13) when counting (see [Lee94]) + # ignore center pixel (i=13) when counting (see [Lee94]_) cdef pixel_type cube[26] memcpy(cube, neighbors, 13*sizeof(pixel_type)) memcpy(cube+13, neighbors+14, 13*sizeof(pixel_type)) @@ -509,7 +501,7 @@ cdef void octree_labeling(int octant, int label, pixel_type cube[]): components in the 3D neighborhood after the center pixel would have been removed. - See Figs. 6 and 7 of [Lee94] for the values of indices. + See Figs. 6 and 7 of [Lee94]_ for the values of indices. Parameters ---------- diff --git a/skimage/morphology/skel.py b/skimage/morphology/skel.py index 5af021b9..672a1f76 100644 --- a/skimage/morphology/skel.py +++ b/skimage/morphology/skel.py @@ -15,6 +15,8 @@ def _prepare_image(img_in): if img.ndim == 2: img = img.reshape((1,) + img.shape) + intensity = img.max() + # normalize to binary img[img != 0] = 1 @@ -22,25 +24,25 @@ def _prepare_image(img_in): img_o = np.zeros(tuple(s + 2 for s in img.shape), dtype=np.uint8) img_o[1:-1, 1:-1, 1:-1] = img.astype(np.uint8) - return img_o + return img_o, intensity -def _postprocess_image(img_o): +def _postprocess_image(img_o, intensity): """Clip the image (padding is an implementation detail), convert to b/w. If the original was 2D, convert back to 2D. """ img_oo = img_o[1:-1, 1:-1, 1:-1] img_oo = img_oo.squeeze() - img_oo *= 255 + img_oo *= intensity return img_oo def compute_thin_image(img_in): """Compute the thin image. """ - img = _prepare_image(img_in) + img, intensity = _prepare_image(img_in) img = np.asarray(_compute_thin_image(img)) - img = _postprocess_image(img) + img = _postprocess_image(img, intensity) return img diff --git a/skimage/morphology/trivial.py b/skimage/morphology/trivial.py deleted file mode 100644 index 46656d93..00000000 --- a/skimage/morphology/trivial.py +++ /dev/null @@ -1,73 +0,0 @@ -import numpy as np -import matplotlib.pyplot as plt - -from skel import prepare_image, compute_thin_image - - -def get_rhombus(n=64, L=22, width=7): - img = np.zeros((n, n), dtype=np.int8) - - x = np.arange(L, dtype=int) - y = L - x - - for w in range(width): - img[x + n//2, y + n//2 + w] = 1 - img[-x + n//2, y + n//2 + w] = 1 - img[x + n//2, -y + n//2 + w] = 1 - img[-x + n//2, -y + n//2 + w] = 1 - - return img - -def get_strip(): - img = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=np.uint8) - return img - - -def get_square(n=64, L=16, width=4): - img = np.zeros((n, n), dtype=np.int8) - - x = np.arange(-L//2 + 1, L//2) - for w in range(width): - img[n//2 + x, n//2 + L//2 + w] = 1 - img[n//2 + x, n//2 - L//2 - w] = 1 - - img[n//2 + L//2 - w, n//2 + x] = 1 - img[n//2 + x, n//2 - L//2 - w] = 1 - - return img - - -def get_loop(): - img = np.loadtxt('Untitled.txt', dtype=np.uint8) - return img - - -if __name__ == "__main__": - -## img = get_rhombus() -## img = get_strip() - img = get_loop() - - x, y = np.nonzero(img) - plt.scatter(x, y, marker='s', color='b', s=40, alpha=0.3) - - # skeletonize - img1 = prepare_image(img) - img1 = compute_thin_image(img1) - - img1_2d = img1[1, 1:, 1:] - x, y = np.nonzero(img1_2d) - - plt.scatter(x, y, marker='o', color='r') - plt.show() From c4c95ae4bc84f69e963404830907d388b664fceb Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Sun, 31 Jan 2016 13:15:35 +0000 Subject: [PATCH 21/39] STY: reformat the filling-in of the Euler LUT --- skimage/morphology/_skel.pyx | 146 +++-------------------------------- 1 file changed, 11 insertions(+), 135 deletions(-) diff --git a/skimage/morphology/_skel.pyx b/skimage/morphology/_skel.pyx index 49d69e9e..273384e1 100644 --- a/skimage/morphology/_skel.pyx +++ b/skimage/morphology/_skel.pyx @@ -34,7 +34,7 @@ from __future__ import division, print_function, absolute_import from libc.string cimport memcpy import numpy as np -from numpy cimport npy_intp, npy_uint8 +from numpy cimport npy_intp, npy_uint8, ndarray cimport cython ctypedef npy_uint8 pixel_type @@ -233,140 +233,16 @@ def fill_Euler_LUT(): This is column $\delta G_{26}$ of Table 2 of [Lee94]_. """ - LUT = np.zeros(256, dtype=np.intc) - - LUT[1] = 1 - LUT[3] = -1 - LUT[5] = -1 - LUT[7] = 1 - LUT[9] = -3 - LUT[11] = -1 - LUT[13] = -1 - LUT[15] = 1 - LUT[17] = -1 - LUT[19] = 1 - LUT[21] = 1 - LUT[23] = -1 - LUT[25] = 3 - LUT[27] = 1 - LUT[29] = 1 - LUT[31] = -1 - LUT[33] = -3 - LUT[35] = -1 - LUT[37] = 3 - LUT[39] = 1 - LUT[41] = 1 - LUT[43] = -1 - LUT[45] = 3 - LUT[47] = 1 - LUT[49] = -1 - LUT[51] = 1 - - LUT[53] = 1 - LUT[55] = -1 - LUT[57] = 3 - LUT[59] = 1 - LUT[61] = 1 - LUT[63] = -1 - LUT[65] = -3 - LUT[67] = 3 - LUT[69] = -1 - LUT[71] = 1 - LUT[73] = 1 - LUT[75] = 3 - LUT[77] = -1 - LUT[79] = 1 - LUT[81] = -1 - LUT[83] = 1 - LUT[85] = 1 - LUT[87] = -1 - LUT[89] = 3 - LUT[91] = 1 - LUT[93] = 1 - LUT[95] = -1 - LUT[97] = 1 - LUT[99] = 3 - LUT[101] = 3 - LUT[103] = 1 - - LUT[105] = 5 - LUT[107] = 3 - LUT[109] = 3 - LUT[111] = 1 - LUT[113] = -1 - LUT[115] = 1 - LUT[117] = 1 - LUT[119] = -1 - LUT[121] = 3 - LUT[123] = 1 - LUT[125] = 1 - LUT[127] = -1 - LUT[129] = -7 - LUT[131] = -1 - LUT[133] = -1 - LUT[135] = 1 - LUT[137] = -3 - LUT[139] = -1 - LUT[141] = -1 - LUT[143] = 1 - LUT[145] = -1 - LUT[147] = 1 - LUT[149] = 1 - LUT[151] = -1 - LUT[153] = 3 - LUT[155] = 1 - - LUT[157] = 1 - LUT[159] = -1 - LUT[161] = -3 - LUT[163] = -1 - LUT[165] = 3 - LUT[167] = 1 - LUT[169] = 1 - LUT[171] = -1 - LUT[173] = 3 - LUT[175] = 1 - LUT[177] = -1 - LUT[179] = 1 - LUT[181] = 1 - LUT[183] = -1 - LUT[185] = 3 - LUT[187] = 1 - LUT[189] = 1 - LUT[191] = -1 - LUT[193] = -3 - LUT[195] = 3 - LUT[197] = -1 - LUT[199] = 1 - LUT[201] = 1 - LUT[203] = 3 - LUT[205] = -1 - LUT[207] = 1 - - LUT[209] = -1 - LUT[211] = 1 - LUT[213] = 1 - LUT[215] = -1 - LUT[217] = 3 - LUT[219] = 1 - LUT[221] = 1 - LUT[223] = -1 - LUT[225] = 1 - LUT[227] = 3 - LUT[229] = 3 - LUT[231] = 1 - LUT[233] = 5 - LUT[235] = 3 - LUT[237] = 3 - LUT[239] = 1 - LUT[241] = -1 - LUT[243] = 1 - LUT[245] = 1 - LUT[247] = -1 - LUT[249] = 3 - LUT[251] = 1 - LUT[253] = 1 - LUT[255] = -1 + cdef int arr[128] + arr[:] = [1, -1, -1, 1, -3, -1, -1, 1, -1, 1, 1, -1, 3, 1, 1, -1, -3, -1, + 3, 1, 1, -1, 3, 1, -1, 1, 1, -1, 3, 1, 1, -1, -3, 3, -1, 1, 1, + 3, -1, 1, -1, 1, 1, -1, 3, 1, 1, -1, 1, 3, 3, 1, 5, 3, 3, 1, + -1, 1, 1, -1, 3, 1, 1, -1, -7, -1, -1, 1, -3, -1, -1, 1, -1, + 1, 1, -1, 3, 1, 1, -1, -3, -1, 3, 1, 1, -1, 3, 1, -1, 1, 1, + -1, 3, 1, 1, -1, -3, 3, -1, 1, 1, 3, -1, 1, -1, 1, 1, -1, 3, + 1, 1, -1, 1, 3, 3, 1, 5, 3, 3, 1, -1, 1, 1, -1, 3, 1, 1, -1] + cdef ndarray LUT = np.zeros(256, dtype=np.intc) + LUT[1::2] = arr return LUT cdef int[::1] LUT = fill_Euler_LUT() From 3bbb766f4efe7db3d82e9200fce45552d7b6c2c3 Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Tue, 2 Feb 2016 16:40:39 +0000 Subject: [PATCH 22/39] MAINT: skel3d: rename _skel.pyx to _skel.pyx.in (no templating yet) --- skimage/morphology/{_skel.pyx => _skel.pyx.in} | 0 skimage/morphology/setup.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename skimage/morphology/{_skel.pyx => _skel.pyx.in} (100%) diff --git a/skimage/morphology/_skel.pyx b/skimage/morphology/_skel.pyx.in similarity index 100% rename from skimage/morphology/_skel.pyx rename to skimage/morphology/_skel.pyx.in diff --git a/skimage/morphology/setup.py b/skimage/morphology/setup.py index 5a26ac52..6d572ec6 100644 --- a/skimage/morphology/setup.py +++ b/skimage/morphology/setup.py @@ -16,7 +16,7 @@ def configuration(parent_package='', top_path=None): cython(['_skeletonize_cy.pyx'], working_path=base_path) cython(['_convex_hull.pyx'], working_path=base_path) cython(['_greyreconstruct.pyx'], working_path=base_path) - cython(['_skel.pyx'], working_path=base_path) + cython(['_skel.pyx.in'], working_path=base_path) config.add_extension('_watershed', sources=['_watershed.c'], include_dirs=[get_numpy_include_dirs()]) From de912c4f7b44a2d35bf4f2849391dadded789862 Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Sun, 31 Jan 2016 17:19:49 +0000 Subject: [PATCH 23/39] STY: generate `octree_labeling` routine via tempita at build time --- skimage/morphology/_skel.pyx.in | 272 ++++++-------------------------- 1 file changed, 47 insertions(+), 225 deletions(-) diff --git a/skimage/morphology/_skel.pyx.in b/skimage/morphology/_skel.pyx.in index 273384e1..e212c35d 100644 --- a/skimage/morphology/_skel.pyx.in +++ b/skimage/morphology/_skel.pyx.in @@ -370,6 +370,37 @@ cdef bint is_simple_point(pixel_type neighbors[]): return True +# Octree structure for labeling in `octree_labeling` routine below. +# NB: this is only available at build time, and is used by Tempita templating. +{{py: +_octree = [ + # octant 1 + ([0, 1, 3, 4, 9, 10, 12], + [[], [2], [3], [2, 3, 4], [5], [2, 5, 6], [3, 5, 7]]), + # octant 2 + ([1, 4, 10, 2, 5, 11, 13], + [[1], [1, 3, 4], [1, 5, 6], [], [4], [6], [4, 6, 8]]), + # octant 3 + ([3, 4, 12, 6, 7, 14, 15], + [[1], [1, 2, 4], [1, 5, 7], [], [4], [7], [4, 7, 8]]), + # octant 4 + ([4, 5, 13, 7, 15, 8, 16], + [[1, 2, 3], [2], [2, 6, 8], [3], [3, 7, 8], [], [8]]), + # octant 5 + ([9, 10, 12, 17, 18, 20, 21], + [[1], [1, 2, 6], [1, 3, 7], [], [6], [7], [6, 7, 8]]), + # octant 6 + ([10, 11, 13, 18, 21, 19, 22], + [[1, 2, 5], [2], [2, 4, 8], [5], [5, 7, 8], [], [8]]), + # octant 7 + ([12, 14, 15, 20, 21, 23, 24], + [[1, 3, 5], [3], [3, 4, 8], [5], [5, 6, 8], [], [8]]), + # octant 8 + ([13, 15, 16, 21, 22, 24, 25], + [[2, 4, 6], [3, 4, 7], [4], [5, 6, 7], [6], [7], []]) +] +}} + @cython.boundscheck(False) @cython.wraparound(False) cdef void octree_labeling(int octant, int label, pixel_type cube[]): @@ -389,229 +420,20 @@ cdef void octree_labeling(int octant, int label, pixel_type cube[]): local neighborhood of the point """ - # check if there are points in the octant with value 1 - if octant == 1: - # set points in this octant to current label - # and recursive labeling of adjacent octants - if cube[0] == 1: - cube[0] = label - if cube[1] == 1: - cube[1] = label - octree_labeling(2, label, cube) - if cube[3] == 1: - cube[3] = label - octree_labeling(3, label, cube) - if cube[4] == 1: - cube[4] = label - octree_labeling(2, label, cube) - octree_labeling(3, label, cube) - octree_labeling(4, label, cube) - if cube[9] == 1: - cube[9] = label - octree_labeling(5, label, cube) - if cube[10] == 1: - cube[10] = label - octree_labeling(2, label, cube) - octree_labeling(5, label, cube) - octree_labeling(6, label, cube) - if cube[12] == 1: - cube[12] = label - octree_labeling(3, label, cube) - octree_labeling(5, label, cube) - octree_labeling(7, label, cube) + # This routine checks if there are points in the octant with value 1 + # Then sets points in this octant to current label + # and recursive labeling of adjacent octants. + # + # Below, leading underscore means build-time variables. + {{for _oct in range(1, 9)}} - if octant == 2: - if cube[1] == 1: - cube[1] = label - octree_labeling(1, label, cube) - if cube[4] == 1: - cube[4] = label - octree_labeling(1, label, cube) - octree_labeling(3, label, cube) - octree_labeling(4, label, cube) - if cube[10] == 1: - cube[10] = label - octree_labeling(1, label, cube) - octree_labeling(5, label, cube) - octree_labeling(6, label, cube) - if cube[2] == 1: - cube[2] = label - if cube[5] == 1: - cube[5] = label - octree_labeling(4, label, cube) - if cube[11] == 1: - cube[11] = label - octree_labeling(6, label, cube) - if cube[13] == 1: - cube[13] = label - octree_labeling(4, label, cube) - octree_labeling(6, label, cube) - octree_labeling(8, label, cube) - - if octant ==3: - if cube[3] == 1: - cube[3] = label - octree_labeling(1, label, cube) - if cube[4] == 1: - cube[4] = label - octree_labeling(1, label, cube) - octree_labeling(2, label, cube) - octree_labeling(4, label, cube) - if cube[12] == 1: - cube[12] = label - octree_labeling(1, label, cube) - octree_labeling(5, label, cube) - octree_labeling(7, label, cube) - if cube[6] == 1: - cube[6] = label - if cube[7] == 1: - cube[7] = label - octree_labeling(4, label, cube) - if cube[14] == 1: - cube[14] = label - octree_labeling(7, label, cube) - if cube[15] == 1: - cube[15] = label - octree_labeling(4, label, cube) - octree_labeling(7, label, cube) - octree_labeling(8, label, cube) - - if octant == 4: - if cube[4] == 1: - cube[4] = label - octree_labeling(1, label, cube) - octree_labeling(2, label, cube) - octree_labeling(3, label, cube) - if cube[5] == 1: - cube[5] = label - octree_labeling(2, label, cube) - if cube[13] == 1: - cube[13] = label - octree_labeling(2, label, cube) - octree_labeling(6, label, cube) - octree_labeling(8, label, cube) - if cube[7] == 1: - cube[7] = label - octree_labeling(3, label, cube) - if cube[15] == 1: - cube[15] = label - octree_labeling(3, label, cube) - octree_labeling(7, label, cube) - octree_labeling(8, label, cube) - if cube[8] == 1: - cube[8] = label - if cube[16] == 1: - cube[16] = label - octree_labeling(8, label, cube) - - if octant == 5: - if cube[9] == 1: - cube[9] = label - octree_labeling(1, label, cube) - if cube[10] == 1: - cube[10] = label - octree_labeling(1, label, cube) - octree_labeling(2, label, cube) - octree_labeling(6, label, cube) - if cube[12] == 1: - cube[12] = label - octree_labeling(1, label, cube) - octree_labeling(3, label, cube) - octree_labeling(7, label, cube) - if cube[17] == 1: - cube[17] = label - if cube[18] == 1: - cube[18] = label - octree_labeling(6, label, cube) - if cube[20] == 1: - cube[20] = label - octree_labeling(7, label, cube) - if cube[21] == 1: - cube[21] = label - octree_labeling(6, label, cube) - octree_labeling(7, label, cube) - octree_labeling(8, label, cube) - - if octant == 6: - if cube[10] == 1: - cube[10] = label - octree_labeling(1, label, cube) - octree_labeling(2, label, cube) - octree_labeling(5, label, cube) - if cube[11] == 1: - cube[11] = label - octree_labeling(2, label, cube) - if cube[13] == 1: - cube[13] = label - octree_labeling(2, label, cube) - octree_labeling(4, label, cube) - octree_labeling(8, label, cube) - if cube[18] == 1: - cube[18] = label - octree_labeling(5, label, cube) - if cube[21] == 1: - cube[21] = label - octree_labeling(5, label, cube) - octree_labeling(7, label, cube) - octree_labeling(8, label, cube) - if cube[19] == 1: - cube[19] = label - if cube[22] == 1: - cube[22] = label - octree_labeling(8, label, cube) - - if octant == 7: - if cube[12] == 1: - cube[12] = label - octree_labeling(1, label, cube) - octree_labeling(3, label, cube) - octree_labeling(5, label, cube) - if cube[14] == 1: - cube[14] = label - octree_labeling(3, label, cube) - if cube[15] == 1: - cube[15] = label - octree_labeling(3, label, cube) - octree_labeling(4, label, cube) - octree_labeling(8, label, cube) - if cube[20] == 1: - cube[20] = label - octree_labeling(5, label, cube) - if cube[21] == 1: - cube[21] = label - octree_labeling(5, label, cube) - octree_labeling(6, label, cube) - octree_labeling(8, label, cube) - if cube[23] == 1: - cube[23] = label - if cube[24] == 1: - cube[24] = label - octree_labeling(8, label, cube) - - if octant == 8: - if cube[13] == 1: - cube[13] = label - octree_labeling(2, label, cube) - octree_labeling(4, label, cube) - octree_labeling(6, label, cube) - if cube[15] == 1: - cube[15] = label - octree_labeling(3, label, cube) - octree_labeling(4, label, cube) - octree_labeling(7, label, cube) - if cube[16] == 1: - cube[16] = label - octree_labeling(4, label, cube) - if cube[21] == 1: - cube[21] = label - octree_labeling(5, label, cube) - octree_labeling(6, label, cube) - octree_labeling(7, label, cube) - if cube[22] == 1: - cube[22] = label - octree_labeling(6, label, cube) - if cube[24] == 1: - cube[24] = label - octree_labeling(7, label, cube) - if cube[25] == 1: - cube[25] = label + if octant == {{_oct}}: + {{py: _indices, _list_octants = _octree[_oct-1]}} + {{for _idx, _new_octants in zip(_indices, _list_octants)}} + if cube[{{_idx}}] == 1: + cube[{{_idx}}] = label + {{for _new_octant in _new_octants}} + octree_labeling({{_new_octant}}, label, cube) + {{endfor}} + {{endfor}} + {{endfor}} From a07dd7cb9e355e7662249957247b5b3b48d082c9 Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Mon, 1 Feb 2016 13:27:34 +0000 Subject: [PATCH 24/39] MAINT: skel3d: minor sty/perf fixes * remove commented out code * statically type tuple of ints --- skimage/morphology/_skel.pyx.in | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/skimage/morphology/_skel.pyx.in b/skimage/morphology/_skel.pyx.in index e212c35d..5f8a9450 100644 --- a/skimage/morphology/_skel.pyx.in +++ b/skimage/morphology/_skel.pyx.in @@ -66,7 +66,11 @@ def _compute_thin_image(pixel_type[:, :, ::1] img not None): int borders[6] npy_intp p, r, c bint no_change + list simple_border_points + Py_ssize_t num_simple_points, i, j + (npy_intp, npy_intp, npy_intp) point + pixel_type neighb[27] borders[:] = [4, 3, 2, 1, 5, 6] @@ -84,24 +88,21 @@ def _compute_thin_image(pixel_type[:, :, ::1] img not None): curr_border = borders[j] simple_border_points = _loop_through(img, curr_border) - ## print(curr_border, " : ", simple_border_points, '\n') # sequential re-checking to preserve connectivity when deleting # in a parallel way no_change = True - for pt in simple_border_points: - p, r, c = pt + num_border_points = len(simple_border_points) + for i in range(num_border_points): + point = simple_border_points[i] + p, r, c = point get_neighborhood(img, p, r, c, neighb) if is_simple_point(neighb): img[p, r, c] = 0 no_change = False - else: - pass - ## print(" *** ", pt, " is not simple.") if no_change: unchanged_borders += 1 - simple_border_points = [] return np.asarray(img) @@ -128,6 +129,7 @@ cdef list _loop_through(pixel_type[:, :, ::1] img, pixel_type neighborhood[27] npy_intp p, r, c bint is_border_pt + (npy_intp, npy_intp, npy_intp) point # rebind global names to avoid lookup. Both tables are filled in # at import time. @@ -172,7 +174,8 @@ cdef list _loop_through(pixel_type[:, :, ::1] img, continue # ok, add (p, r, c) to the list of simple border points - simple_border_points.append((p, r, c)) + point = p, r, c + simple_border_points.append(point) return simple_border_points @@ -362,8 +365,6 @@ cdef bint is_simple_point(pixel_type neighbors[]): octree_labeling(7, label, cube) elif i == 25: octree_labeling(8, label, cube) - else: - raise ValueError("Never be here. i = %s" % i) label += 1 if label - 2 >= 2: return False From 99de44744a9e724f641a2f797ecc00df9c39ab0e Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Mon, 1 Feb 2016 14:11:55 +0000 Subject: [PATCH 25/39] ENH: skel3d: unroll and inline `index_octants` into is_euler_invariant 13.7/7.3 performance boost on the lobster dataset, huh --- skimage/morphology/_skel.pyx.in | 83 +++++++++++++++------------------ 1 file changed, 38 insertions(+), 45 deletions(-) diff --git a/skimage/morphology/_skel.pyx.in b/skimage/morphology/_skel.pyx.in index 5f8a9450..56e6abda 100644 --- a/skimage/morphology/_skel.pyx.in +++ b/skimage/morphology/_skel.pyx.in @@ -131,10 +131,9 @@ cdef list _loop_through(pixel_type[:, :, ::1] img, bint is_border_pt (npy_intp, npy_intp, npy_intp) point - # rebind global names to avoid lookup. Both tables are filled in + # rebind a global name to avoid lookup. The table is filled in # at import time. int[::1] Euler_LUT = LUT - cdef int[:, ::1] neighb_idx = NEIGHB_IDX # loop through the image # NB: each loop is from 1 to size-1: img is padded from all sides @@ -164,7 +163,7 @@ cdef list _loop_through(pixel_type[:, :, ::1] img, # check if point is Euler invariant (condition 1 in [Lee94]_): # if it is not, it's not deletable. - if not is_Euler_invariant(neighborhood, Euler_LUT, neighb_idx): + if not is_Euler_invariant(neighborhood, Euler_LUT): continue # check if point is simple (i.e., deletion does not @@ -251,47 +250,24 @@ cdef int[::1] LUT = fill_Euler_LUT() # Fill the look-up table for indexing octants for computing the Euler -# characteristic. See index_octants and is_Euler_invariant routines below. -cdef int[:, ::1] NEIGHB_IDX = np.array([[2, 1, 11, 10, 5, 4, 14], # NEB - [0, 9, 3, 12, 1, 10, 4], # NWB - [8, 7, 17, 16, 5, 4, 14], # SEB - [6, 15, 7, 16, 3, 12, 4], # SWB - [20, 23, 19, 22, 11, 14, 10], # NEU - [18, 21, 9, 12, 19, 22, 10], # NWU - [26, 23, 17, 14, 25, 22, 16], # SEU - [24, 25, 15, 16, 21, 22, 12], # SWU - ], dtype=np.intc) - - -@cython.boundscheck(False) -@cython.wraparound(False) -@cython.cdivision(True) -cdef int index_octants(int octant, - pixel_type neighbors[], - int[:, ::1] neib_idx): - cdef int n = 1, j, idx - for j in range(7): - idx = neib_idx[octant, j] - if neighbors[idx] == 1: - n |= 1 << (7 - j) # XXX hardcode powers? - return n - - -cdef inline bint is_endpoint(pixel_type neighbors[]): - """An endpoint has exactly one neighbor in the 26-neighborhood. - """ - # The center pixel is counted, thus r.h.s. is 2 - cdef int s = 0, j - for j in range(27): - s += neighbors[j] - return s == 2 +# characteristic. See is_Euler_invariant routine below. +{{py: +_neighb_idx = [[2, 1, 11, 10, 5, 4, 14], # NEB + [0, 9, 3, 12, 1, 10, 4], # NWB + [8, 7, 17, 16, 5, 4, 14], # SEB + [6, 15, 7, 16, 3, 12, 4], # SWB + [20, 23, 19, 22, 11, 14, 10], # NEU + [18, 21, 9, 12, 19, 22, 10], # NWU + [26, 23, 17, 14, 25, 22, 16], # SEU + [24, 25, 15, 16, 21, 22, 12], # SWU + ] +}} @cython.boundscheck(False) @cython.wraparound(False) cdef bint is_Euler_invariant(pixel_type neighbors[], - int[::1] lut, - int[:, ::1] neighb_idx): + int[::1] lut): """Check if a point is Euler invariant. Calculate Euler characteristc for each octant and sum up. @@ -302,21 +278,38 @@ cdef bint is_Euler_invariant(pixel_type neighbors[], neighbors of a point lut The look-up table for preserving the Euler characteristic. - neighb_idx - The look-up table for indexing octants. Returns ------- bool (C bool, that is) """ - cdef int octant, n, euler_char = 0 - for octant in range(8): - n = index_octants(octant, neighbors, neighb_idx) - euler_char += lut[n] + cdef int n, euler_char = 0 + {{for _octant in range(8)}} + + # octant {{_octant}}: + n = 1 + {{for _j in range(7):}} + {{py: _idx = _neighb_idx[_octant][_j]}} + if neighbors[{{_idx}}] == 1: + n |= {{1 << (7 - _j)}} + + {{endfor}} + euler_char += lut[n] + {{endfor}} return euler_char == 0 +cdef inline bint is_endpoint(pixel_type neighbors[]): + """An endpoint has exactly one neighbor in the 26-neighborhood. + """ + # The center pixel is counted, thus r.h.s. is 2 + cdef int s = 0, j + for j in range(27): + s += neighbors[j] + return s == 2 + + cdef bint is_simple_point(pixel_type neighbors[]): """Check is a point is a Simple Point. From 212c3d15ce2fb6aad44a6c9a6a68f690d4e2d34c Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Tue, 2 Feb 2016 17:29:56 +0000 Subject: [PATCH 26/39] BLD: add the extension to bento.info, fix names --- bento.info | 3 +++ skimage/morphology/__init__.py | 3 ++- skimage/morphology/{skel.py => _skeletonize_3d.py} | 4 ++-- .../morphology/{_skel.pyx.in => _skeletonize_3d_cy.pyx.in} | 0 skimage/morphology/setup.py | 4 ++-- 5 files changed, 9 insertions(+), 5 deletions(-) rename skimage/morphology/{skel.py => _skeletonize_3d.py} (93%) rename skimage/morphology/{_skel.pyx.in => _skeletonize_3d_cy.pyx.in} (100%) diff --git a/bento.info b/bento.info index 1d7f3a28..5d89ddc9 100644 --- a/bento.info +++ b/bento.info @@ -119,6 +119,9 @@ Library: Extension: skimage.morphology._skeletonize_cy Sources: skimage/morphology/_skeletonize_cy.pyx + Extension: skimage.morphology._skeletonize_3d_cy + Sources: + skimage/morphology/_skeletonize_3d_cy.pyx Extension: skimage.transform._radon_transform Sources: skimage/transform/_radon_transform.pyx diff --git a/skimage/morphology/__init__.py b/skimage/morphology/__init__.py index 7d294911..4579794f 100644 --- a/skimage/morphology/__init__.py +++ b/skimage/morphology/__init__.py @@ -6,7 +6,7 @@ from .selem import (square, rectangle, diamond, disk, cube, octahedron, ball, octagon, star) from .watershed import watershed from ._skeletonize import skeletonize, medial_axis -from .skel import compute_thin_image as compute_thin_image +from ._skeletonize_3d import skeletonize_3d as compute_thin_image from .convex_hull import convex_hull_image, convex_hull_object from .greyreconstruct import reconstruction from .misc import remove_small_objects, remove_small_holes @@ -35,6 +35,7 @@ __all__ = ['binary_erosion', 'label', 'watershed', 'skeletonize', + 'skeletonize_3d', 'medial_axis', 'convex_hull_image', 'convex_hull_object', diff --git a/skimage/morphology/skel.py b/skimage/morphology/_skeletonize_3d.py similarity index 93% rename from skimage/morphology/skel.py rename to skimage/morphology/_skeletonize_3d.py index 672a1f76..f92590fb 100644 --- a/skimage/morphology/skel.py +++ b/skimage/morphology/_skeletonize_3d.py @@ -2,7 +2,7 @@ from __future__ import division, print_function, absolute_import import numpy as np -from ._skel import _compute_thin_image +from ._skeletonize_3d_cy import _compute_thin_image def _prepare_image(img_in): @@ -37,7 +37,7 @@ def _postprocess_image(img_o, intensity): return img_oo -def compute_thin_image(img_in): +def skeletonize_3d(img_in): """Compute the thin image. """ img, intensity = _prepare_image(img_in) diff --git a/skimage/morphology/_skel.pyx.in b/skimage/morphology/_skeletonize_3d_cy.pyx.in similarity index 100% rename from skimage/morphology/_skel.pyx.in rename to skimage/morphology/_skeletonize_3d_cy.pyx.in diff --git a/skimage/morphology/setup.py b/skimage/morphology/setup.py index 6d572ec6..a1945d91 100644 --- a/skimage/morphology/setup.py +++ b/skimage/morphology/setup.py @@ -16,7 +16,7 @@ def configuration(parent_package='', top_path=None): cython(['_skeletonize_cy.pyx'], working_path=base_path) cython(['_convex_hull.pyx'], working_path=base_path) cython(['_greyreconstruct.pyx'], working_path=base_path) - cython(['_skel.pyx.in'], working_path=base_path) + cython(['_skeletonize_3d_cy.pyx.in'], working_path=base_path) config.add_extension('_watershed', sources=['_watershed.c'], include_dirs=[get_numpy_include_dirs()]) @@ -26,7 +26,7 @@ def configuration(parent_package='', top_path=None): include_dirs=[get_numpy_include_dirs()]) config.add_extension('_greyreconstruct', sources=['_greyreconstruct.c'], include_dirs=[get_numpy_include_dirs()]) - config.add_extension('_skel', sources=['_skel.c'], + config.add_extension('_skeletonize_3d_cy', sources=['_skeletonize_3d_cy.c'], include_dirs=[get_numpy_include_dirs()]) return config From 27b5dbca48dc85da2f4cbd658009a840e5685b03 Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Tue, 2 Feb 2016 23:47:09 +0000 Subject: [PATCH 27/39] MAINT: skel3d: address review comments Tidy up the python wrapper a little. --- skimage/morphology/_skeletonize_3d.py | 71 +++++++++++-------- .../morphology/tests/test_skeletonize_3d.py | 66 ++++------------- 2 files changed, 55 insertions(+), 82 deletions(-) diff --git a/skimage/morphology/_skeletonize_3d.py b/skimage/morphology/_skeletonize_3d.py index f92590fb..fe0bf86a 100644 --- a/skimage/morphology/_skeletonize_3d.py +++ b/skimage/morphology/_skeletonize_3d.py @@ -1,50 +1,59 @@ from __future__ import division, print_function, absolute_import import numpy as np - from ._skeletonize_3d_cy import _compute_thin_image -def _prepare_image(img_in): - """Convert to a binary image, pad the it w/ zeros, and ensure it's 3D. +def skeletonize_3d(img_in): + """Compute the skeleton of a binary image. + + Thinning is used to reduce each connected component in a binary image + to a single-pixel wide skeleton. + + Parameters + ---------- + image : ndarray, 2D or 3D + A binary image containing the objects to be skeletonized. Zeros + represent background, nonzero values are foreground. + + Returns + ------- + skeleton : ndarray + The thinned image. + + See also + -------- + skeletonize, medial_axis + + References + ---------- + .. [Lee94] Lee et al, Building skeleton models via 3-D medial surface/axis + thinning algorithms. Computer Vision, Graphics, and Image Processing, + 56(6):462–478, 1994. + """ + # make sure the image is 3D or 2D (if it is, temporarily upcast to 3D) if img_in.ndim < 2 or img_in.ndim > 3: raise ValueError('expect 2D, got ndim = %s' % img_in.ndim) img = img_in.copy() if img.ndim == 2: - img = img.reshape((1,) + img.shape) - - intensity = img.max() + img = img[None, ...] # normalize to binary + maxval = img.max() img[img != 0] = 1 + img = img.astype(np.uint8) - # pad w/ zeros to simplify dealing w/ neighborhood of a pixel - img_o = np.zeros(tuple(s + 2 for s in img.shape), - dtype=np.uint8) - img_o[1:-1, 1:-1, 1:-1] = img.astype(np.uint8) - return img_o, intensity + # pad w/ zeros to simplify dealing w/ boundaries + img_o = np.pad(img, pad_width=1, mode='constant') + # do the computation + img_o = np.asarray(_compute_thin_image(img_o)) -def _postprocess_image(img_o, intensity): - """Clip the image (padding is an implementation detail), convert to b/w. - If the original was 2D, convert back to 2D. - """ - img_oo = img_o[1:-1, 1:-1, 1:-1] - img_oo = img_oo.squeeze() - img_oo *= intensity - return img_oo + # clip it back and restore the original intensity range + img_o = img_o[1:-1, 1:-1, 1:-1] + img_o = img_o.squeeze() + img_o *= maxval - -def skeletonize_3d(img_in): - """Compute the thin image. - """ - img, intensity = _prepare_image(img_in) - img = np.asarray(_compute_thin_image(img)) - img = _postprocess_image(img, intensity) - return img - - -if __name__ == "__main__": - pass + return img_o diff --git a/skimage/morphology/tests/test_skeletonize_3d.py b/skimage/morphology/tests/test_skeletonize_3d.py index 985a5bde..a3832dde 100644 --- a/skimage/morphology/tests/test_skeletonize_3d.py +++ b/skimage/morphology/tests/test_skeletonize_3d.py @@ -3,31 +3,30 @@ from __future__ import division, print_function, absolute_import import os import numpy as np -from numpy.testing import assert_equal +from numpy.testing import assert_equal, run_module_suite import skimage from skimage import io from skimage.morphology import compute_thin_image -import matplotlib.pyplot as plt -import matplotlib.ticker as ticker # nose test generators: - # 2D images def test_simple_2d_images(): for fname in ("strip", "loop", "cross", "two-hole"): yield check_skel, fname + # trivial 3D images def test_simple_3d(): for fname in ['3/stack', '4/stack']: yield check_skel_3d, fname + # 'slow' test: Bat Cochlea from FIJI collections. def test_large(): - for fname in ['bat/bat-cochlea-volume',]: + for fname in ['bat/bat-cochlea-volume']: yield check_skel_3d, fname @@ -39,63 +38,28 @@ def get_data_path(): 'data') -def check_skel(fname, viz=False): +def check_skel(fname): # compute the thin image and compare the result to that of ImageJ - - img = np.loadtxt(os.path.join(get_data_path(), fname+'.txt'), dtype=np.uint8) - - if viz: - ax = _viz(img, **dict(marker='s', color='b', s=99, alpha=0.2)) + img = np.loadtxt(os.path.join(get_data_path(), fname + '.txt'), + dtype=np.uint8) # compute img1_2d = compute_thin_image(img) - if viz: - ax = _viz(img1_2d, ax, **dict(marker='o', color='r', - s=80, alpha=0.7, label='us')) + # and compare to FIJI + img_f = np.loadtxt(os.path.join(get_data_path(), fname + '_fiji.txt'), + dtype=np.uint8) - # compare to FIJI - img_f = np.loadtxt(os.path.join(get_data_path(), fname+'_fiji.txt'), dtype=np.uint8) - - if not viz: - # actually compare images - assert_equal(img1_2d, img_f) - else: - ax = _viz(img_f, ax, **dict(marker='o', color='g', s=45, label='fiji')) - - ax.legend() - ax.grid(True) - - def yformatter(val, pos): - return int(img.shape[1] - val + 1) - def xformatter(val, pos): - return int(val + 1) - ax.xaxis.set_major_formatter(ticker.FuncFormatter(xformatter)) - ax.yaxis.set_major_formatter(ticker.FuncFormatter(yformatter)) - - plt.show() - - -def _viz(img, ax=None, **kwds): - if ax is None: - import matplotlib.pyplot as plt - fix, ax = plt.subplots() - - x, y = np.nonzero(img) - ax.scatter(y, img.shape[1] - x, **kwds) - return ax + assert_equal(img1_2d, img_f) def check_skel_3d(fname): - img = io.imread(os.path.join(get_data_path(), fname+'.tif')) - img_f = io.imread(os.path.join(get_data_path(), fname+'_fiji.tif')) + img = io.imread(os.path.join(get_data_path(), fname + '.tif')) + img_f = io.imread(os.path.join(get_data_path(), fname + '_fiji.tif')) img_s = compute_thin_image(img) assert_equal(img_s, img_f) -if __name__ == "__main__": - import sys - if len(sys.argv) < 2: - sys.exit("Expect an image name from the data/ directory.") - check_skel(sys.argv[1], True) +if __name__ == '__main__': + run_module_suite() From 87541b661d913198de73b72574ea056d0aa12536 Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Wed, 3 Feb 2016 00:26:16 +0000 Subject: [PATCH 28/39] BLD: skel3d: eradicate stray unicode symbols from source files --- skimage/morphology/_skeletonize_3d.py | 2 +- skimage/morphology/_skeletonize_3d_cy.pyx.in | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/skimage/morphology/_skeletonize_3d.py b/skimage/morphology/_skeletonize_3d.py index fe0bf86a..cd63c372 100644 --- a/skimage/morphology/_skeletonize_3d.py +++ b/skimage/morphology/_skeletonize_3d.py @@ -29,7 +29,7 @@ def skeletonize_3d(img_in): ---------- .. [Lee94] Lee et al, Building skeleton models via 3-D medial surface/axis thinning algorithms. Computer Vision, Graphics, and Image Processing, - 56(6):462–478, 1994. + 56(6):462-478, 1994. """ # make sure the image is 3D or 2D (if it is, temporarily upcast to 3D) diff --git a/skimage/morphology/_skeletonize_3d_cy.pyx.in b/skimage/morphology/_skeletonize_3d_cy.pyx.in index 56e6abda..92705d38 100644 --- a/skimage/morphology/_skeletonize_3d_cy.pyx.in +++ b/skimage/morphology/_skeletonize_3d_cy.pyx.in @@ -7,7 +7,7 @@ The original Java code [IAC15]_ carries the following message: * This work is an implementation by Ignacio Arganda-Carreras of the * 3D thinning algorithm from Lee et al. "Building skeleton models via 3-D * medial surface/axis thinning algorithms. Computer Vision, Graphics, and - * Image Processing, 56(6):462–478, 1994." Based on the ITK version from + * Image Processing, 56(6):462-478, 1994." Based on the ITK version from * Hanno Homann http://hdl.handle.net/1926/1292 *

* More information at Skeletonize3D homepage: @@ -23,7 +23,7 @@ References .. [Lee94] Lee et al, Building skeleton models via 3-D medial surface/axis thinning algorithms. Computer Vision, Graphics, and Image Processing, - 56(6):462–478, 1994 + 56(6):462-478, 1994 .. [IAC15] Ignacio Arganda-Carreras, 2015. Skeletonize3D plugin for ImageJ(C). http://fiji.sc/Skeletonize3D From 304660d287b73fbdbf000c599375b4293d4caba3 Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Wed, 3 Feb 2016 11:55:19 +0000 Subject: [PATCH 29/39] BLD: bump the cython version to 0.23.4 on Travis --- tools/travis_before_install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/travis_before_install.sh b/tools/travis_before_install.sh index b4baeaf4..98eef785 100755 --- a/tools/travis_before_install.sh +++ b/tools/travis_before_install.sh @@ -30,7 +30,7 @@ retry () { } # add build dependencies -echo "cython>=0.21" >> requirements.txt +echo "cython>=0.23.4" >> requirements.txt # require networkx 1.9.1 on 2.6, as 2.6 support was dropped in 1.10 # require matplotlib 1.4.3 on 2.6, as 2.6 support was dropped in 1.5 From 42d4eb6123fcdef9cb67d64b2c28a487b94fdae2 Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Wed, 3 Feb 2016 15:09:35 +0000 Subject: [PATCH 30/39] TST: skel3d: add more tests --- skimage/morphology/__init__.py | 2 +- skimage/morphology/_skeletonize_3d.py | 15 ++- .../morphology/tests/test_skeletonize_3d.py | 107 +++++++++++++++++- 3 files changed, 112 insertions(+), 12 deletions(-) diff --git a/skimage/morphology/__init__.py b/skimage/morphology/__init__.py index 4579794f..d1d35ee5 100644 --- a/skimage/morphology/__init__.py +++ b/skimage/morphology/__init__.py @@ -6,7 +6,7 @@ from .selem import (square, rectangle, diamond, disk, cube, octahedron, ball, octagon, star) from .watershed import watershed from ._skeletonize import skeletonize, medial_axis -from ._skeletonize_3d import skeletonize_3d as compute_thin_image +from ._skeletonize_3d import skeletonize_3d from .convex_hull import convex_hull_image, convex_hull_object from .greyreconstruct import reconstruction from .misc import remove_small_objects, remove_small_holes diff --git a/skimage/morphology/_skeletonize_3d.py b/skimage/morphology/_skeletonize_3d.py index cd63c372..8e2460da 100644 --- a/skimage/morphology/_skeletonize_3d.py +++ b/skimage/morphology/_skeletonize_3d.py @@ -1,10 +1,11 @@ from __future__ import division, print_function, absolute_import import numpy as np +from ..util import img_as_ubyte from ._skeletonize_3d_cy import _compute_thin_image -def skeletonize_3d(img_in): +def skeletonize_3d(img): """Compute the skeleton of a binary image. Thinning is used to reduce each connected component in a binary image @@ -12,7 +13,7 @@ def skeletonize_3d(img_in): Parameters ---------- - image : ndarray, 2D or 3D + img : ndarray, 2D or 3D A binary image containing the objects to be skeletonized. Zeros represent background, nonzero values are foreground. @@ -33,17 +34,19 @@ def skeletonize_3d(img_in): """ # make sure the image is 3D or 2D (if it is, temporarily upcast to 3D) - if img_in.ndim < 2 or img_in.ndim > 3: - raise ValueError('expect 2D, got ndim = %s' % img_in.ndim) + if img.ndim < 2 or img.ndim > 3: + raise ValueError('expect 2D, got ndim = %s' % img.ndim) - img = img_in.copy() + img = img_as_ubyte(img) + img = np.ascontiguousarray(img) + + img = img.copy() if img.ndim == 2: img = img[None, ...] # normalize to binary maxval = img.max() img[img != 0] = 1 - img = img.astype(np.uint8) # pad w/ zeros to simplify dealing w/ boundaries img_o = np.pad(img, pad_width=1, mode='constant') diff --git a/skimage/morphology/tests/test_skeletonize_3d.py b/skimage/morphology/tests/test_skeletonize_3d.py index a3832dde..a3f90b9c 100644 --- a/skimage/morphology/tests/test_skeletonize_3d.py +++ b/skimage/morphology/tests/test_skeletonize_3d.py @@ -1,14 +1,111 @@ from __future__ import division, print_function, absolute_import import os +import warnings import numpy as np -from numpy.testing import assert_equal, run_module_suite +from numpy.testing import (assert_equal, run_module_suite, assert_raises, + assert_) + +import scipy.ndimage as ndi import skimage -from skimage import io +from skimage import io, draw, data_dir +#from skimage import draw +from skimage.util import img_as_ubyte -from skimage.morphology import compute_thin_image +from skimage.morphology import skeletonize_3d + + +# basic behavior tests (mostly copied over from 2D skeletonize) + +def test_skeletonize_wrong_dim(): + im = np.zeros(5, dtype=np.uint8) + assert_raises(ValueError, skeletonize_3d, im) + + im = np.zeros((5, 5, 5, 5), dtype=np.uint8) + assert_raises(ValueError, skeletonize_3d, im) + + +def test_skeletonize_no_foreground(): + im = np.zeros((5, 5), dtype=np.uint8) + result = skeletonize_3d(im) + assert_equal(result, im) + + +def test_skeletonize_all_foreground(): + im = np.ones((3, 4), dtype=np.uint8) + assert_equal(skeletonize_3d(im), + np.array([[0, 0, 0, 0], + [1, 1, 1, 1], + [0, 0, 0, 0]], dtype=np.uint8)) + + +def test_skeletonize_single_point(): + im = np.zeros((5, 5), dtype=np.uint8) + im[3, 3] = 1 + result = skeletonize_3d(im) + assert_equal(result, im) + + +def test_skeletonize_already_thinned(): + im = np.zeros((5, 5), dtype=np.uint8) + im[3, 1:-1] = 1 + im[2, -1] = 1 + im[4, 0] = 1 + result = skeletonize_3d(im) + assert_equal(result, im) + + +def test_dtype_conv(): + # check that the operation does the right thing with floats etc + # also check non-contiguous input + img = np.random.random((16, 16))[::2, ::2] + img[img < 0.5] = 0 + + orig = img.copy() + + with warnings.catch_warnings(): + # UserWarning for possible precision loss, expected + warnings.simplefilter('ignore', UserWarning) + res = skeletonize_3d(img) + + assert_equal(res.dtype, np.uint8) + assert_equal(img, orig) # operation does not clobber the original + assert_equal(res.max(), + img_as_ubyte(img).max()) # the intensity range is preserved + + +def test_skeletonize_num_neighbours(): + # an empty image + image = np.zeros((300, 300)) + + # foreground object 1 + image[10:-10, 10:100] = 1 + image[-100:-10, 10:-10] = 1 + image[10:-10, -100:-10] = 1 + + # foreground object 2 + rs, cs = draw.line(250, 150, 10, 280) + for i in range(10): + image[rs + i, cs] = 1 + rs, cs = draw.line(10, 150, 250, 280) + for i in range(20): + image[rs + i, cs] = 1 + + # foreground object 3 + ir, ic = np.indices(image.shape) + circle1 = (ic - 135)**2 + (ir - 150)**2 < 30**2 + circle2 = (ic - 135)**2 + (ir - 150)**2 < 20**2 + image[circle1] = 1 + image[circle2] = 0 + result = skeletonize_3d(image) + + # there should never be a 2x2 block of foreground pixels in a skeleton + mask = np.array([[1, 1], + [1, 1]], np.uint8) + blocks = ndi.correlate(result, mask, mode='constant') + assert_(not np.any(blocks == 4)) # nose test generators: @@ -44,7 +141,7 @@ def check_skel(fname): dtype=np.uint8) # compute - img1_2d = compute_thin_image(img) + img1_2d = skeletonize_3d(img) # and compare to FIJI img_f = np.loadtxt(os.path.join(get_data_path(), fname + '_fiji.txt'), @@ -57,7 +154,7 @@ def check_skel_3d(fname): img = io.imread(os.path.join(get_data_path(), fname + '.tif')) img_f = io.imread(os.path.join(get_data_path(), fname + '_fiji.tif')) - img_s = compute_thin_image(img) + img_s = skeletonize_3d(img) assert_equal(img_s, img_f) From 81d47328ea1af345416e54aa6f9ba254c40f6e82 Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Wed, 3 Feb 2016 21:14:18 +0000 Subject: [PATCH 31/39] DOC: spell out the names of [Lee94], add the Notes section --- skimage/morphology/_skeletonize_3d.py | 19 ++++++++++++++++--- skimage/morphology/_skeletonize_3d_cy.pyx.in | 7 ++++--- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/skimage/morphology/_skeletonize_3d.py b/skimage/morphology/_skeletonize_3d.py index 8e2460da..0aa08979 100644 --- a/skimage/morphology/_skeletonize_3d.py +++ b/skimage/morphology/_skeletonize_3d.py @@ -26,11 +26,24 @@ def skeletonize_3d(img): -------- skeletonize, medial_axis + Notes + ----- + The method of [Lee94]_ uses an octree data structure to examine a 3x3x3 + neighborhood of a pixel. The algorithm proceeds by iteratively sweeping + over the image, and removing pixels at each iteration until the image + stops changing. Each iteration consists of two steps: first, a list of + candidates for removal is assembled; then pixels from this list are + rechecked sequentially, to better preserve connectivity of the image. + + The algorithm this function implements is different from the algorithms + used by either `skeletonize` or `medial_axis`, thus for 2D images the + results produced by this function are generally different. + References ---------- - .. [Lee94] Lee et al, Building skeleton models via 3-D medial surface/axis - thinning algorithms. Computer Vision, Graphics, and Image Processing, - 56(6):462-478, 1994. + .. [Lee94] T.-C. Lee, R.L. Kashyap and C.-N. Chu, Building skeleton models + via 3-D medial surface/axis thinning algorithms. + Computer Vision, Graphics, and Image Processing, 56(6):462-478, 1994. """ # make sure the image is 3D or 2D (if it is, temporarily upcast to 3D) diff --git a/skimage/morphology/_skeletonize_3d_cy.pyx.in b/skimage/morphology/_skeletonize_3d_cy.pyx.in index 92705d38..5926e10a 100644 --- a/skimage/morphology/_skeletonize_3d_cy.pyx.in +++ b/skimage/morphology/_skeletonize_3d_cy.pyx.in @@ -21,9 +21,10 @@ Porting to Cython was done by Evgeni Burovski (evgeny.burovskiy@gmail.com). References ---------- -.. [Lee94] Lee et al, Building skeleton models via 3-D medial surface/axis - thinning algorithms. Computer Vision, Graphics, and Image Processing, - 56(6):462-478, 1994 +.. [Lee94] T.-C. Lee, R.L. Kashyap and C.-N. Chu, Building skeleton models + via 3-D medial surface/axis thinning algorithms. + Computer Vision, Graphics, and Image Processing, 56(6):462-478, 1994. + .. [IAC15] Ignacio Arganda-Carreras, 2015. Skeletonize3D plugin for ImageJ(C). http://fiji.sc/Skeletonize3D From 9648d713602b30c73f4e1ffbe5b50135fc4c349f Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Thu, 4 Feb 2016 11:07:19 +0000 Subject: [PATCH 32/39] MAINT: skel3d: do not copy more than necessary --- skimage/morphology/_skeletonize_3d.py | 16 ++++++++-------- .../morphology/tests/test_skeletonize_3d.py | 19 +++++++++++++++++++ 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/skimage/morphology/_skeletonize_3d.py b/skimage/morphology/_skeletonize_3d.py index 0aa08979..2a419bf4 100644 --- a/skimage/morphology/_skeletonize_3d.py +++ b/skimage/morphology/_skeletonize_3d.py @@ -50,19 +50,19 @@ def skeletonize_3d(img): if img.ndim < 2 or img.ndim > 3: raise ValueError('expect 2D, got ndim = %s' % img.ndim) - img = img_as_ubyte(img) img = np.ascontiguousarray(img) + img = img_as_ubyte(img, force_copy=False) - img = img.copy() + # make an in image 3D pad w/ zeros to simplify dealing w/ boundaries + # NB: careful to not clobber the original *and* minimize copying if img.ndim == 2: - img = img[None, ...] + img_o = np.pad(img[None, ...], pad_width=1, mode='constant') + else: + img_o = np.pad(img, pad_width=1, mode='constant') # normalize to binary - maxval = img.max() - img[img != 0] = 1 - - # pad w/ zeros to simplify dealing w/ boundaries - img_o = np.pad(img, pad_width=1, mode='constant') + maxval = img_o.max() + img_o[img_o != 0] = 1 # do the computation img_o = np.asarray(_compute_thin_image(img_o)) diff --git a/skimage/morphology/tests/test_skeletonize_3d.py b/skimage/morphology/tests/test_skeletonize_3d.py index a3f90b9c..dd1996f0 100644 --- a/skimage/morphology/tests/test_skeletonize_3d.py +++ b/skimage/morphology/tests/test_skeletonize_3d.py @@ -76,6 +76,25 @@ def test_dtype_conv(): img_as_ubyte(img).max()) # the intensity range is preserved +def test_input(): + # check that the input is not clobbered + # for 2D and 3D images of varying dtypes + + imgs = [np.ones((8, 8), dtype=float), np.ones((4, 8, 8), dtype=float), + np.ones((8, 8), dtype=np.uint8), np.ones((4, 8, 8), dtype=np.uint8)] + for img in imgs: + yield check_input, img + + +def check_input(img): + orig = img.copy() + with warnings.catch_warnings(): + # UserWarning for possible precision loss, expected + warnings.simplefilter('ignore', UserWarning) + res = skeletonize_3d(img) + assert_equal(img, orig) + + def test_skeletonize_num_neighbours(): # an empty image image = np.zeros((300, 300)) From 64bb1775be20e41e5fa462ab3f2762050ba36fd7 Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Thu, 4 Feb 2016 11:33:27 +0000 Subject: [PATCH 33/39] TST: inline a two-hole image --- .../morphology/tests/test_skeletonize_3d.py | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/skimage/morphology/tests/test_skeletonize_3d.py b/skimage/morphology/tests/test_skeletonize_3d.py index dd1996f0..10b7e923 100644 --- a/skimage/morphology/tests/test_skeletonize_3d.py +++ b/skimage/morphology/tests/test_skeletonize_3d.py @@ -79,7 +79,6 @@ def test_dtype_conv(): def test_input(): # check that the input is not clobbered # for 2D and 3D images of varying dtypes - imgs = [np.ones((8, 8), dtype=float), np.ones((4, 8, 8), dtype=float), np.ones((8, 8), dtype=np.uint8), np.ones((4, 8, 8), dtype=np.uint8)] for img in imgs: @@ -127,6 +126,42 @@ def test_skeletonize_num_neighbours(): assert_(not np.any(blocks == 4)) +def test_two_hole_image(): + # test a simple 2D image against FIJI + img_o = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0], + [0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0], + [0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0], + [0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0], + [0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], + dtype=np.uint8) + img_f = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], + dtype=np.uint8) + res = skeletonize_3d(img_o) + assert_equal(res, img_f) + + # nose test generators: # 2D images def test_simple_2d_images(): From d83273930050a652f2087b2a42fbef51da680b3b Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Thu, 4 Feb 2016 13:38:47 +0000 Subject: [PATCH 34/39] TST: skel3d: remove 2D test images --- skimage/morphology/tests/data/cross.png | Bin 149 -> 0 bytes skimage/morphology/tests/data/cross.txt | 24 ------------------ skimage/morphology/tests/data/cross_fiji.txt | 24 ------------------ skimage/morphology/tests/data/loop.png | Bin 184 -> 0 bytes skimage/morphology/tests/data/loop.txt | 24 ------------------ skimage/morphology/tests/data/loop_fiji.txt | 24 ------------------ skimage/morphology/tests/data/strip.png | Bin 131 -> 0 bytes skimage/morphology/tests/data/strip.txt | 12 --------- skimage/morphology/tests/data/strip_fiji.txt | 12 --------- skimage/morphology/tests/data/two-hole.png | Bin 176 -> 0 bytes skimage/morphology/tests/data/two-hole.txt | 16 ------------ .../morphology/tests/data/two-hole_fiji.txt | 16 ------------ .../morphology/tests/test_skeletonize_3d.py | 23 ----------------- 13 files changed, 175 deletions(-) delete mode 100644 skimage/morphology/tests/data/cross.png delete mode 100644 skimage/morphology/tests/data/cross.txt delete mode 100644 skimage/morphology/tests/data/cross_fiji.txt delete mode 100644 skimage/morphology/tests/data/loop.png delete mode 100644 skimage/morphology/tests/data/loop.txt delete mode 100644 skimage/morphology/tests/data/loop_fiji.txt delete mode 100644 skimage/morphology/tests/data/strip.png delete mode 100644 skimage/morphology/tests/data/strip.txt delete mode 100644 skimage/morphology/tests/data/strip_fiji.txt delete mode 100644 skimage/morphology/tests/data/two-hole.png delete mode 100644 skimage/morphology/tests/data/two-hole.txt delete mode 100644 skimage/morphology/tests/data/two-hole_fiji.txt diff --git a/skimage/morphology/tests/data/cross.png b/skimage/morphology/tests/data/cross.png deleted file mode 100644 index bd6db21c7758dbaf59a9f8eb43ffaa591301b99f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 149 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaT3?y&uT)!JgF&8^|hH!9j+}(d(|4_WO0;q$*)78&qol`;+03>EBBLDyZ diff --git a/skimage/morphology/tests/data/cross.txt b/skimage/morphology/tests/data/cross.txt deleted file mode 100644 index 2118f7f1..00000000 --- a/skimage/morphology/tests/data/cross.txt +++ /dev/null @@ -1,24 +0,0 @@ -0 0 0 0 0 255 255 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 255 255 255 255 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 255 255 255 255 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 255 255 255 255 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 255 255 255 255 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 255 255 255 255 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 0 0 0 -0 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 0 0 0 -0 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 0 0 -0 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 0 0 -0 0 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 0 0 -0 0 0 0 0 255 255 255 255 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 255 255 255 255 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 255 255 255 255 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 255 255 255 255 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 255 255 255 255 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 255 255 255 255 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 255 255 255 255 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 255 255 255 255 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 255 255 255 255 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 255 255 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 diff --git a/skimage/morphology/tests/data/cross_fiji.txt b/skimage/morphology/tests/data/cross_fiji.txt deleted file mode 100644 index c4ea69a9..00000000 --- a/skimage/morphology/tests/data/cross_fiji.txt +++ /dev/null @@ -1,24 +0,0 @@ -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 255 255 255 255 0 255 255 255 255 255 255 255 255 255 255 255 255 0 0 0 0 -0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 diff --git a/skimage/morphology/tests/data/loop.png b/skimage/morphology/tests/data/loop.png deleted file mode 100644 index 05fdc25955801664638367107253540ee3885375..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 184 zcmeAS@N?(olHy`uVBq!ia0vp^l0YoM$P6S+tB1s;*b3=DinK$vl=HlH+5(9_e!F@$4gaza9aMw$crH~xkr^#UKy zGkDZ62=(wcuzYUhcX;%mS&Hr0V_t{OYK5cq4KqIS_Z@W!H)#L> diff --git a/skimage/morphology/tests/data/loop.txt b/skimage/morphology/tests/data/loop.txt deleted file mode 100644 index 277e1b47..00000000 --- a/skimage/morphology/tests/data/loop.txt +++ /dev/null @@ -1,24 +0,0 @@ -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 255 255 255 255 255 255 255 255 255 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 255 255 255 255 255 255 255 255 255 255 255 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 255 255 255 255 255 255 255 0 0 0 255 255 0 0 0 0 0 -0 0 0 0 0 0 0 0 255 255 255 255 0 0 0 0 0 0 255 255 0 0 0 0 0 -0 0 0 0 0 0 0 255 255 255 255 0 0 0 0 0 0 0 255 255 0 0 0 0 0 -0 0 0 0 0 0 255 255 255 255 0 0 0 0 0 0 255 255 255 255 0 0 0 0 0 -0 0 0 0 0 0 255 255 255 255 0 0 0 0 0 255 255 255 255 0 0 0 0 0 0 -0 0 0 0 0 0 0 255 255 255 0 0 0 255 255 255 255 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 255 255 0 255 255 255 255 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 255 255 255 255 255 255 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 255 255 255 255 255 255 255 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 255 255 255 255 255 255 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 255 255 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 255 255 255 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 255 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 diff --git a/skimage/morphology/tests/data/loop_fiji.txt b/skimage/morphology/tests/data/loop_fiji.txt deleted file mode 100644 index 4d38bfac..00000000 --- a/skimage/morphology/tests/data/loop_fiji.txt +++ /dev/null @@ -1,24 +0,0 @@ -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 255 255 255 255 255 255 255 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 255 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 255 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 255 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 255 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 255 255 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 255 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 255 0 0 0 0 255 255 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 255 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 255 255 255 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 diff --git a/skimage/morphology/tests/data/strip.png b/skimage/morphology/tests/data/strip.png deleted file mode 100644 index 74e9b3e49c0730a12cbf27c91e9dd825acd61822..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 131 zcmeAS@N?(olHy`uVBq!ia0vp^+(693$P6U=(y!hHQq09po*^6@9Je3(KLB#s0(?ST z|Ns9FWQHEPTnD5$3p^r=85sDEfH31!Z9ZwBpopi7V+hC0|22Vm7w!=Se(5=Wj}+%v){v!G=Hi!Tzopr0FyU3r~m)} diff --git a/skimage/morphology/tests/data/two-hole.txt b/skimage/morphology/tests/data/two-hole.txt deleted file mode 100644 index 680e7beb..00000000 --- a/skimage/morphology/tests/data/two-hole.txt +++ /dev/null @@ -1,16 +0,0 @@ -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 255 255 255 255 255 0 0 0 0 0 -0 0 0 0 0 255 255 255 255 255 255 255 255 255 0 255 0 -0 0 0 255 255 255 255 255 255 255 255 255 255 255 255 255 0 -0 0 255 255 255 255 0 0 255 255 255 0 0 255 255 255 0 -0 0 255 255 255 0 0 0 255 255 255 0 0 255 255 255 0 -0 0 0 255 255 0 0 0 255 255 255 0 0 255 255 255 0 -0 0 0 255 255 255 0 0 255 255 255 0 0 255 255 255 0 -0 0 0 0 255 255 0 0 255 255 255 0 0 255 255 255 0 -0 0 0 0 255 255 255 255 255 255 255 255 255 255 255 255 0 -0 0 0 0 0 255 255 255 255 255 255 255 255 255 255 255 0 -0 0 0 0 0 255 255 255 255 255 255 255 255 255 255 255 0 -0 0 0 0 0 0 0 255 255 255 255 255 255 255 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 diff --git a/skimage/morphology/tests/data/two-hole_fiji.txt b/skimage/morphology/tests/data/two-hole_fiji.txt deleted file mode 100644 index 6c16d3f7..00000000 --- a/skimage/morphology/tests/data/two-hole_fiji.txt +++ /dev/null @@ -1,16 +0,0 @@ -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 255 255 0 255 255 0 0 0 0 0 -0 0 0 0 0 255 255 0 0 255 0 0 255 0 0 0 0 -0 0 0 0 255 0 0 0 0 255 0 0 0 255 0 0 0 -0 0 0 255 0 0 0 0 0 255 0 0 0 0 255 0 0 -0 0 0 255 0 0 0 0 0 255 0 0 0 0 255 0 0 -0 0 0 0 255 0 0 0 0 255 0 0 0 0 255 0 0 -0 0 0 0 255 0 0 0 0 255 0 0 0 0 255 0 0 -0 0 0 0 0 255 0 0 0 255 0 0 0 0 255 0 0 -0 0 0 0 0 0 255 255 0 255 0 0 0 255 0 0 0 -0 0 0 0 0 0 0 0 255 0 255 255 255 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 diff --git a/skimage/morphology/tests/test_skeletonize_3d.py b/skimage/morphology/tests/test_skeletonize_3d.py index 10b7e923..08a60f46 100644 --- a/skimage/morphology/tests/test_skeletonize_3d.py +++ b/skimage/morphology/tests/test_skeletonize_3d.py @@ -162,13 +162,6 @@ def test_two_hole_image(): assert_equal(res, img_f) -# nose test generators: -# 2D images -def test_simple_2d_images(): - for fname in ("strip", "loop", "cross", "two-hole"): - yield check_skel, fname - - # trivial 3D images def test_simple_3d(): for fname in ['3/stack', '4/stack']: @@ -188,22 +181,6 @@ def get_data_path(): 'tests', 'data') - -def check_skel(fname): - # compute the thin image and compare the result to that of ImageJ - img = np.loadtxt(os.path.join(get_data_path(), fname + '.txt'), - dtype=np.uint8) - - # compute - img1_2d = skeletonize_3d(img) - - # and compare to FIJI - img_f = np.loadtxt(os.path.join(get_data_path(), fname + '_fiji.txt'), - dtype=np.uint8) - - assert_equal(img1_2d, img_f) - - def check_skel_3d(fname): img = io.imread(os.path.join(get_data_path(), fname + '.tif')) img_f = io.imread(os.path.join(get_data_path(), fname + '_fiji.tif')) From dc702a83eeab95f65ae398dd8d5ec5d9b597f34e Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Fri, 5 Feb 2016 12:43:29 +0000 Subject: [PATCH 35/39] TST: skel3d: redo 3D test images Remove ad-hoc images from morphology/tests/data folder. Add a FIJI generated image to skimage/data. The image was generated via >>> img = binary_blobs(32, 0.05, n_dim=3, seed=1234) >>> img = img.astype(np.uint8)*255 >>> img = img[:-2, ...] then saved as a sequence of png frames, read into FIJI as an image sequence, and skeletonized via Plugins -> Skeletonize (2D/3D). --- skimage/data/_blobs_3d_fiji_skeleton.tif | Bin 0 -> 35293 bytes skimage/morphology/tests/data/3/pngs/1.png | Bin 251 -> 0 bytes skimage/morphology/tests/data/3/pngs/2.png | Bin 248 -> 0 bytes skimage/morphology/tests/data/3/pngs/3.png | Bin 184 -> 0 bytes skimage/morphology/tests/data/3/pngs/5.png | Bin 178 -> 0 bytes skimage/morphology/tests/data/3/pngs/6.png | Bin 178 -> 0 bytes skimage/morphology/tests/data/3/pngs/7.png | Bin 176 -> 0 bytes skimage/morphology/tests/data/3/stack.tif | Bin 15187 -> 0 bytes .../morphology/tests/data/3/stack_fiji.tif | Bin 15187 -> 0 bytes skimage/morphology/tests/data/4/1_fiji.png | Bin 94 -> 0 bytes skimage/morphology/tests/data/4/pngs/1.png | Bin 149 -> 0 bytes skimage/morphology/tests/data/4/pngs/2.png | Bin 137 -> 0 bytes skimage/morphology/tests/data/4/pngs/3.png | Bin 137 -> 0 bytes skimage/morphology/tests/data/4/pngs/4.png | Bin 137 -> 0 bytes skimage/morphology/tests/data/4/pngs/5.png | Bin 137 -> 0 bytes skimage/morphology/tests/data/4/pngs/6.png | Bin 137 -> 0 bytes skimage/morphology/tests/data/4/stack.tif | Bin 1795 -> 0 bytes .../morphology/tests/data/4/stack_fiji.tif | Bin 1795 -> 0 bytes .../tests/data/bat/bat-cochlea-volume.tif | Bin 2138695 -> 0 bytes .../data/bat/bat-cochlea-volume_fiji.tif | Bin 2138695 -> 0 bytes .../morphology/tests/test_skeletonize_3d.py | 31 +++++------------- 21 files changed, 8 insertions(+), 23 deletions(-) create mode 100644 skimage/data/_blobs_3d_fiji_skeleton.tif delete mode 100644 skimage/morphology/tests/data/3/pngs/1.png delete mode 100644 skimage/morphology/tests/data/3/pngs/2.png delete mode 100644 skimage/morphology/tests/data/3/pngs/3.png delete mode 100644 skimage/morphology/tests/data/3/pngs/5.png delete mode 100644 skimage/morphology/tests/data/3/pngs/6.png delete mode 100644 skimage/morphology/tests/data/3/pngs/7.png delete mode 100644 skimage/morphology/tests/data/3/stack.tif delete mode 100644 skimage/morphology/tests/data/3/stack_fiji.tif delete mode 100644 skimage/morphology/tests/data/4/1_fiji.png delete mode 100644 skimage/morphology/tests/data/4/pngs/1.png delete mode 100644 skimage/morphology/tests/data/4/pngs/2.png delete mode 100644 skimage/morphology/tests/data/4/pngs/3.png delete mode 100644 skimage/morphology/tests/data/4/pngs/4.png delete mode 100644 skimage/morphology/tests/data/4/pngs/5.png delete mode 100644 skimage/morphology/tests/data/4/pngs/6.png delete mode 100644 skimage/morphology/tests/data/4/stack.tif delete mode 100644 skimage/morphology/tests/data/4/stack_fiji.tif delete mode 100644 skimage/morphology/tests/data/bat/bat-cochlea-volume.tif delete mode 100644 skimage/morphology/tests/data/bat/bat-cochlea-volume_fiji.tif diff --git a/skimage/data/_blobs_3d_fiji_skeleton.tif b/skimage/data/_blobs_3d_fiji_skeleton.tif new file mode 100644 index 0000000000000000000000000000000000000000..d5cdd051a2672a5b4839aeaee956187f7ca97072 GIT binary patch literal 35293 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 SyK(y8zj=@8U%zqsz#oq6^RZsftubPXO7TIfX ztPA1UWge4v-0y4ChsE0$%~+UycHNec%re7EU*!JC@n^f{dnamtv)-}P)7C%q{dB+L t&vTmHo|WlNlYC~ua5+EwNq+q`_CJqpTPD6SUI}y;gQu&X%Q~loCIGAdUiSb1 diff --git a/skimage/morphology/tests/data/3/pngs/2.png b/skimage/morphology/tests/data/3/pngs/2.png deleted file mode 100644 index ada13a9ee12f81c9f75611f195dc52270cc6b870..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 248 zcmeAS@N?(olHy`uVBq!ia0vp^WU`533hbv~qyzDus+}e6r`e^mMz<_;bE@$^Jho6WudFkV@fBMZ= qmFxaV&D$8h#<}GGMyvn7XY)7c+0A7P650TC6oaR$pUXO@geCy`E?md} diff --git a/skimage/morphology/tests/data/3/pngs/3.png b/skimage/morphology/tests/data/3/pngs/3.png deleted file mode 100644 index 00ee7d1f35fd2a8a5d415ba1be23eaf78328f066..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 184 zcmeAS@N?(olHy`uVBq!ia0vp^WviwG%p$wj`elF{r5}E)ar#q|w diff --git a/skimage/morphology/tests/data/3/pngs/5.png b/skimage/morphology/tests/data/3/pngs/5.png deleted file mode 100644 index 8378036e08c6dbb5b5c81e1cea2d8ef961825c35..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 178 zcmeAS@N?(olHy`uVBq!ia0vp^W;vq2^c)t; zo-ml*xR5U|i>YPW{{wXlE}NZSSfz2~9{gTe~DWM4f6qGbC diff --git a/skimage/morphology/tests/data/3/pngs/6.png b/skimage/morphology/tests/data/3/pngs/6.png deleted file mode 100644 index 3c6051aedc05befee077d00896d023f62c02f212..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 178 zcmeAS@N?(olHy`uVBq!ia0vp^W;vq2^c)t; zo-n#QPqFbQiHHKJLoIHLyB|Li=AOZM<1`P0zAv}Z Unsf7B0gYquboFyt=akR{0A9>Cy8r+H diff --git a/skimage/morphology/tests/data/3/pngs/7.png b/skimage/morphology/tests/data/3/pngs/7.png deleted file mode 100644 index 61eb70015833d281f406b02d04e2fd2c0a720c23..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 176 zcmeAS@N?(olHy`uVBq!ia0vp^W;vq2^c)t; zo-n#QPqFbQiHHKJLp^SbyB|Li=H__C%wX_^ThTyN RrT}OdgQu&X%Q~loCIHSVGu!|G diff --git a/skimage/morphology/tests/data/3/stack.tif b/skimage/morphology/tests/data/3/stack.tif deleted file mode 100644 index 28752135debf71383648d205de0b17e81f45646a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15187 zcmeI3%}x|S5XXB^5jV!f81>}UgCdFs{7AUNCVTea2@MjGRY-`BqTYNHuOvKx2M->7 z4Ym5Kx~iwAr)B_yu}F6K*T=7_yJmJ~A=%l?+%v{3nXBfPxd^I(mpw$i-ZZF}n+0>h zg_j^_F=$9`u9*e!--p+i=6cL;9yB*e&aI%AA^&#Fxw3im$?40$9v^*n{Wn4XH0Ifl zt#{kIJJXHTm9!_Zhf&%(;o+j|y{tQ@<%g2KR#N;IUv6M?xFGg~S?7>K; zpG0s}u?UeQ0p?N&Vx`A2Cp8PL`mqRQ=Qg-$d8!k|xhh5>(4<}s^Rck|5=QEv3{V`J zH8JK(B{M0c2$w~ZiHcCjVNld8Vgf3wC)-!Me(IDJBmt0aYupvA?ntFs4eG=kM9yBe zoOalI>b?BVLRA7-nKfW$sG1QXs=!oR#er~+t)^)3{Q7oCfrJrfzqD9?mS*S#bOI-u zfO~%qsF2>z(zhrlg#8MT;D%77`tBnPvXcmo>QEv?k_4DbA&8Y8%be6KwCcwqlnpJo zX?dy>#knd*A<(2=4fC-$>jFmVpbStPnl&-zOC>WYqzIQqlZlE@$YD^_EMfvGt0&u6 zyMF4F6(j+WZfo2XtnNsqSq@cjCANP&bAXTP*qf0kzG1kUpW+{Hg|f#D4^kYYEU0qmF+*6!c6*5Ws`IFWT5 zb;B5So3TSpcDB2CNSpC=@|Mfl8wz{vRsk{7_p>sBqc4cc_*B}sEeTmi7pL7 zc?m~cz4M?&1fklWqdF=6#{#QS{cO(*f`u#L5p?&6ze)8hMZ-D)oq$e2C(xe2nA@~t z$OEEXwdnC!F;sH`RttB;Mp7Ns)P3p`$gT!KA`eVoxQ2IHlG*$KK1at!b(gY@_|E0#s2g#Qjjd}hG pzK3|~U#YzDRoMOeS#-a1N!Ig%pX-K&JzFt diff --git a/skimage/morphology/tests/data/3/stack_fiji.tif b/skimage/morphology/tests/data/3/stack_fiji.tif deleted file mode 100644 index 9373767547c7d86a4192e6072677d61b4daaa679..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15187 zcmeI3%SyvQ6oyagJ%}KpuH3rtQg5_gpnF^B+J&nswG^TiijSi1d=s~V58%RuOJBo! zk~U*5lXhloLR45TdPj}Ry zLlGT`s34-Eh>k_X4RC;f7J&{|yQk{WagT1X|9g-$TBl?XBSkx6PKsnE*Yfl5tGq?IsqFq8FB^_eoHkk~PC!ULByo0RX; zPMA$di#sHpmnMb84dNBj=Q*T_s>HSvsfrDKv%ifhRw7BTN>9-3;hRZ1A_W2+6gko~VPY9BL|YIoFu00@8p2uwHu ze2h%^4Ib6yMIOA+Ffm|s00ck)1VCW){}hIMG~NUf|EDGLT`4jg??vP4K>!3m z00ck)1VA7|0{9-4A#7NI00<11fIa+|8tn&_ml}yqFZn&hp}$hO@~Y_mnTGqujGFt| UMH=p#RYmuYG~D+d)!d1G0j7(b{r~^~ diff --git a/skimage/morphology/tests/data/4/1_fiji.png b/skimage/morphology/tests/data/4/1_fiji.png deleted file mode 100644 index 03a7b79a4570ba4c6a89d04c3bc351c88bfcb1e3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 94 zcmeAS@N?(olHy`uVBq!ia0vp^JRr;gBp8b2n5}`7s;7%%NX4z>gaiEllnwtYSNxy& qBw)pVo*6bRH|p6^l>hxdo^Q^ewny;2b@lEEAdQ}`elF{r5}E)*&K%YN diff --git a/skimage/morphology/tests/data/4/pngs/1.png b/skimage/morphology/tests/data/4/pngs/1.png deleted file mode 100644 index d03660d2d80e785af86148e963b0466643aab2a4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 149 zcmeAS@N?(olHy`uVBq!ia0vp^JRr=-3?y@QpIZW?n2Vh}LpV4%Za?&Y0OYa-_=LCu z>Hq)#FTFYcE|A4p;1OBOz`%C|gc+x5^GO2*)jVAsLpWw8CnO{%BqmHqPS}v2;9+z4 o!vjW+sWu54><=i^H!$)rOjh7HzImO`Y@iMXPgg&ebxsLQ0NPY4f&c&j diff --git a/skimage/morphology/tests/data/4/pngs/2.png b/skimage/morphology/tests/data/4/pngs/2.png deleted file mode 100644 index efa5c10492ba48e3074e296370deb5054d498853..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 137 zcmeAS@N?(olHy`uVBq!ia0vp^JRr=-3?y@QpIZW?n2Vh}LpV4%Za?&Y0OYa-_=LCu z>Hq)#FTFYcE|A4p;1OBOz`%C|gc+x5^GO2*r9536LpWw8CnTgbG_#1cvGefo?~oLd bn83)Oe~D?bz!bqIpehDWS3j3^P6Hq)#FTFYcE|A4p;1OBOz`%C|gc+x5^GO2*r9536LpWw8CnO{{FthWuG4t^7OGpYy bOkiZtn!psGcy_^QpehDWS3j3^P6Hq)#FTFYcE|A4p;1OBOz`%C|gc+x5^GO2*r9536LpWw8CnO{{FthWuG4t^7OGpYy bOkiZtn!psGcy_^QpehDWS3j3^P6Hq)#FTFYcE|A4p;1OBOz`%C|gc+x5^GO2*r9536LpWw8CnO{{FthWuG4t^7OGpYy bOkiZtn!psGcy_^QpehDWS3j3^P6Hq)#FTFYcE|A4p;1OBOz`%C|gc+x5^GO2*r9536LpWw8CnO{{FthWuG4t^7OGpYy bOkiZtn!psGcy_^QpehDWS3j3^P6%#Pp_QBq#U3e8-pQ(9zy{` z9z!~V5u9ZVXPLlRrf`-SoCP!h1^kEe{sRG&17ZRJhzBEq9H5Yu25LT#nU3X(J<`3=cw@UR+n4h3&GWil|dnZk7ze}9$Z0xkKojI3C+goX3`(NU0XJ7yT2!@p& diff --git a/skimage/morphology/tests/data/4/stack_fiji.tif b/skimage/morphology/tests/data/4/stack_fiji.tif deleted file mode 100644 index c0d182fd31f51f6b1ae14876343a46bd4cf834dd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1795 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 diff --git a/skimage/morphology/tests/data/bat/bat-cochlea-volume_fiji.tif b/skimage/morphology/tests/data/bat/bat-cochlea-volume_fiji.tif deleted file mode 100644 index e6ce0122c41feae592b5141247c70d7256b0f186..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2138695 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 T(;v!^e!_`gO-i5rrMmwILrd19 diff --git a/skimage/morphology/tests/test_skeletonize_3d.py b/skimage/morphology/tests/test_skeletonize_3d.py index 08a60f46..d373f0d5 100644 --- a/skimage/morphology/tests/test_skeletonize_3d.py +++ b/skimage/morphology/tests/test_skeletonize_3d.py @@ -11,7 +11,7 @@ import scipy.ndimage as ndi import skimage from skimage import io, draw, data_dir -#from skimage import draw +from skimage.data import binary_blobs from skimage.util import img_as_ubyte from skimage.morphology import skeletonize_3d @@ -162,30 +162,15 @@ def test_two_hole_image(): assert_equal(res, img_f) -# trivial 3D images -def test_simple_3d(): - for fname in ['3/stack', '4/stack']: - yield check_skel_3d, fname - - -# 'slow' test: Bat Cochlea from FIJI collections. -def test_large(): - for fname in ['bat/bat-cochlea-volume']: - yield check_skel_3d, fname - - -def get_data_path(): - # XXX this is a bad temp hack - return os.path.join(os.path.split(skimage.__file__)[0], - 'morphology', - 'tests', - 'data') - -def check_skel_3d(fname): - img = io.imread(os.path.join(get_data_path(), fname + '.tif')) - img_f = io.imread(os.path.join(get_data_path(), fname + '_fiji.tif')) +def test_3d_vs_fiji(): + # generate an image with blobs and compate its skeleton to + # the skeleton generated by FIJI + img = binary_blobs(32, 0.05, n_dim=3, seed=1234) + img = img[:-2, ...] + img = img.astype(np.uint8)*255 img_s = skeletonize_3d(img) + img_f = io.imread(os.path.join(data_dir, "_blobs_3d_fiji_skeleton.tif")) assert_equal(img_s, img_f) From 2bc8538f9fdc7c3bf489a6a2452202f3ee60e63b Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Sat, 6 Feb 2016 18:46:12 +0000 Subject: [PATCH 36/39] MAINT: address review comments * typos in comments * handle the corner case of width-1 2D image * vanity * change up leftover <= to correct == --- CONTRIBUTORS.txt | 3 +++ skimage/morphology/_skeletonize_3d.py | 5 ++++ skimage/morphology/_skeletonize_3d_cy.pyx.in | 24 ++++++++------------ 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 9e028354..f2a75b33 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -221,3 +221,6 @@ - Egor Panfilov Inpainting with biharmonic equation + +- Evgeni Burovski + Adaptation of ImageJ 3D skeletonization algorithm. diff --git a/skimage/morphology/_skeletonize_3d.py b/skimage/morphology/_skeletonize_3d.py index 2a419bf4..d041f643 100644 --- a/skimage/morphology/_skeletonize_3d.py +++ b/skimage/morphology/_skeletonize_3d.py @@ -56,6 +56,11 @@ def skeletonize_3d(img): # make an in image 3D pad w/ zeros to simplify dealing w/ boundaries # NB: careful to not clobber the original *and* minimize copying if img.ndim == 2: + + if img.shape[0] == 1 or img.shape[1] == 1: + # nothing to do, image is already thin. Bail out. + return img.copy() + img_o = np.pad(img[None, ...], pad_width=1, mode='constant') else: img_o = np.pad(img, pad_width=1, mode='constant') diff --git a/skimage/morphology/_skeletonize_3d_cy.pyx.in b/skimage/morphology/_skeletonize_3d_cy.pyx.in index 5926e10a..e8637799 100644 --- a/skimage/morphology/_skeletonize_3d_cy.pyx.in +++ b/skimage/morphology/_skeletonize_3d_cy.pyx.in @@ -16,16 +16,12 @@ The original Java code [IAC15]_ carries the following message: * @version 1.0 11/13/2015 (unique BSD licensed version for scikit-image) * @author Ignacio Arganda-Carreras (iargandacarreras at gmail.com) -Porting to Cython was done by Evgeni Burovski (evgeny.burovskiy@gmail.com). - References ---------- - .. [Lee94] T.-C. Lee, R.L. Kashyap and C.-N. Chu, Building skeleton models via 3-D medial surface/axis thinning algorithms. Computer Vision, Graphics, and Image Processing, 56(6):462-478, 1994. - .. [IAC15] Ignacio Arganda-Carreras, 2015. Skeletonize3D plugin for ImageJ(C). http://fiji.sc/Skeletonize3D @@ -50,7 +46,7 @@ def _compute_thin_image(pixel_type[:, :, ::1] img not None): those point which can be removed without changing local connectivity in the 3x3x3 neighborhood of a point. - This routine implements the two-pass algorthim of [Lee94]_. Namely, + This routine implements the two-pass algorithm of [Lee94]_. Namely, for each of the six border types (positive and negative x-, y- and z-), the algorithm first collects all possibly deletable points, and then performs a sequential rechecking. @@ -58,7 +54,7 @@ def _compute_thin_image(pixel_type[:, :, ::1] img not None): The input, `img`, is assumed to be a 3D binary image in the (p, r, c) format [i.e., C ordered array], filled by zeros (background) and ones. Furthermore, `img` is assumed to be padded by zeros from all - directions --- this way the zero boundary conditions are authomatic + directions --- this way the zero boundary conditions are automatic and there is need to guard against out-of-bounds access. """ @@ -69,7 +65,7 @@ def _compute_thin_image(pixel_type[:, :, ::1] img not None): bint no_change list simple_border_points - Py_ssize_t num_simple_points, i, j + Py_ssize_t num_border_points, i, j (npy_intp, npy_intp, npy_intp) point pixel_type neighb[27] @@ -146,12 +142,12 @@ cdef list _loop_through(pixel_type[:, :, ::1] img, if img[p, r, c] != 1: continue - is_border_pt = (curr_border == 1 and img[p, r, c-1] <= 0 or #N - curr_border == 2 and img[p, r, c+1] <= 0 or #S - curr_border == 3 and img[p, r+1, c] <= 0 or #E - curr_border == 4 and img[p, r-1, c] <= 0 or #W - curr_border == 5 and img[p+1, r, c] <= 0 or #U - curr_border == 6 and img[p-1, r, c] <= 0) #B + is_border_pt = (curr_border == 1 and img[p, r, c-1] == 0 or #N + curr_border == 2 and img[p, r, c+1] == 0 or #S + curr_border == 3 and img[p, r+1, c] == 0 or #E + curr_border == 4 and img[p, r-1, c] == 0 or #W + curr_border == 5 and img[p+1, r, c] == 0 or #U + curr_border == 6 and img[p-1, r, c] == 0) #B if not is_border_pt: # current point is not deletable continue @@ -271,7 +267,7 @@ cdef bint is_Euler_invariant(pixel_type neighbors[], int[::1] lut): """Check if a point is Euler invariant. - Calculate Euler characteristc for each octant and sum up. + Calculate Euler characteristic for each octant and sum up. Parameters ---------- From ba328e753137498aa639e9d34f6f1e01012c9b32 Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Sun, 7 Feb 2016 20:21:10 +0000 Subject: [PATCH 37/39] MAINT: address review comments Use recommended idioms in the .py wrapper --- skimage/morphology/_skeletonize_3d.py | 28 ++++++++----------- .../morphology/tests/test_skeletonize_3d.py | 10 ++++++- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/skimage/morphology/_skeletonize_3d.py b/skimage/morphology/_skeletonize_3d.py index d041f643..768ec08b 100644 --- a/skimage/morphology/_skeletonize_3d.py +++ b/skimage/morphology/_skeletonize_3d.py @@ -1,7 +1,7 @@ from __future__ import division, print_function, absolute_import import numpy as np -from ..util import img_as_ubyte +from ..util import img_as_ubyte, crop from ._skeletonize_3d_cy import _compute_thin_image @@ -46,24 +46,19 @@ def skeletonize_3d(img): Computer Vision, Graphics, and Image Processing, 56(6):462-478, 1994. """ - # make sure the image is 3D or 2D (if it is, temporarily upcast to 3D) + # make sure the image is 3D or 2D if img.ndim < 2 or img.ndim > 3: raise ValueError('expect 2D, got ndim = %s' % img.ndim) img = np.ascontiguousarray(img) img = img_as_ubyte(img, force_copy=False) - # make an in image 3D pad w/ zeros to simplify dealing w/ boundaries - # NB: careful to not clobber the original *and* minimize copying + # make an in image 3D and pad it w/ zeros to simplify dealing w/ boundaries + # NB: careful here to not clobber the original *and* minimize copying + img_o = img if img.ndim == 2: - - if img.shape[0] == 1 or img.shape[1] == 1: - # nothing to do, image is already thin. Bail out. - return img.copy() - - img_o = np.pad(img[None, ...], pad_width=1, mode='constant') - else: - img_o = np.pad(img, pad_width=1, mode='constant') + img_o = img[np.newaxis, ...] + img_o = np.pad(img_o, pad_width=1, mode='constant') # normalize to binary maxval = img_o.max() @@ -72,9 +67,10 @@ def skeletonize_3d(img): # do the computation img_o = np.asarray(_compute_thin_image(img_o)) - # clip it back and restore the original intensity range - img_o = img_o[1:-1, 1:-1, 1:-1] - img_o = img_o.squeeze() + # crop it back and restore the original intensity range + img_o = crop(img_o, crop_width=1) + if img.ndim == 2: + img_o = img_o[0] img_o *= maxval - return img_o + return img_o \ No newline at end of file diff --git a/skimage/morphology/tests/test_skeletonize_3d.py b/skimage/morphology/tests/test_skeletonize_3d.py index d373f0d5..d5859a5d 100644 --- a/skimage/morphology/tests/test_skeletonize_3d.py +++ b/skimage/morphology/tests/test_skeletonize_3d.py @@ -27,6 +27,13 @@ def test_skeletonize_wrong_dim(): assert_raises(ValueError, skeletonize_3d, im) +def test_skeletonize_1D(): + # a corner case of an image of a shape(1, N) + im = np.ones((5, 1), dtype=np.uint8) + res = skeletonize_3d(im) + assert_equal(res, im) + + def test_skeletonize_no_foreground(): im = np.zeros((5, 5), dtype=np.uint8) result = skeletonize_3d(im) @@ -80,7 +87,8 @@ def test_input(): # check that the input is not clobbered # for 2D and 3D images of varying dtypes imgs = [np.ones((8, 8), dtype=float), np.ones((4, 8, 8), dtype=float), - np.ones((8, 8), dtype=np.uint8), np.ones((4, 8, 8), dtype=np.uint8)] + np.ones((8, 8), dtype=np.uint8), np.ones((4, 8, 8), dtype=np.uint8), + np.ones((8, 8), dtype=bool), np.ones((4, 8, 8), dtype=bool)] for img in imgs: yield check_input, img From fc70f73064a072fda66dce50148653efee7fa518 Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Sun, 7 Feb 2016 20:51:08 +0000 Subject: [PATCH 38/39] STY: more tweaks in response to review comments --- skimage/morphology/_skeletonize_3d.py | 3 +- skimage/morphology/_skeletonize_3d_cy.pyx.in | 32 +++++++++----------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/skimage/morphology/_skeletonize_3d.py b/skimage/morphology/_skeletonize_3d.py index 768ec08b..53861964 100644 --- a/skimage/morphology/_skeletonize_3d.py +++ b/skimage/morphology/_skeletonize_3d.py @@ -48,7 +48,8 @@ def skeletonize_3d(img): """ # make sure the image is 3D or 2D if img.ndim < 2 or img.ndim > 3: - raise ValueError('expect 2D, got ndim = %s' % img.ndim) + raise ValueError("skeletonize_3d can only handle 2D or 3D images; " + "got img.ndim = %s instead." % img.ndim) img = np.ascontiguousarray(img) img = img_as_ubyte(img, force_copy=False) diff --git a/skimage/morphology/_skeletonize_3d_cy.pyx.in b/skimage/morphology/_skeletonize_3d_cy.pyx.in index e8637799..da55d6d8 100644 --- a/skimage/morphology/_skeletonize_3d_cy.pyx.in +++ b/skimage/morphology/_skeletonize_3d_cy.pyx.in @@ -69,6 +69,7 @@ def _compute_thin_image(pixel_type[:, :, ::1] img not None): (npy_intp, npy_intp, npy_intp) point pixel_type neighb[27] + # loop over the six directions in this order (for consistency with ImageJ) borders[:] = [4, 3, 2, 1, 5, 6] # no need to worry about the z direction if the original image is 2D. @@ -84,7 +85,7 @@ def _compute_thin_image(pixel_type[:, :, ::1] img not None): for j in range(num_borders): curr_border = borders[j] - simple_border_points = _loop_through(img, curr_border) + simple_border_points = find_simple_point_candidates(img, curr_border) # sequential re-checking to preserve connectivity when deleting # in a parallel way @@ -106,7 +107,7 @@ def _compute_thin_image(pixel_type[:, :, ::1] img not None): @cython.boundscheck(False) @cython.wraparound(False) -cdef list _loop_through(pixel_type[:, :, ::1] img, +cdef list find_simple_point_candidates(pixel_type[:, :, ::1] img, int curr_border): """Inner loop of compute_thin_image. @@ -154,19 +155,15 @@ cdef list _loop_through(pixel_type[:, :, ::1] img, get_neighborhood(img, p, r, c, neighborhood) - # check if (p, r, c) is an endpoint (then it's not deletable.) - if is_endpoint(neighborhood): - continue - - # check if point is Euler invariant (condition 1 in [Lee94]_): - # if it is not, it's not deletable. - if not is_Euler_invariant(neighborhood, Euler_LUT): - continue - - # check if point is simple (i.e., deletion does not - # change connectivity in the 3x3x3 neighborhood) - # this are conditions 2 and 3 in [Lee94]_ - if not is_simple_point(neighborhood): + # check if (p, r, c) can be deleted: + # * it must not be an endpoint; + # * it must be Euler invariant (condition 1 in [Lee94]_); and + # * it must be simple (i.e., its deletion does not change + # connectivity in the 3x3x3 neighborhood) + # this is conditions 2 and 3 in [Lee94]_ + if (is_endpoint(neighborhood) or + not is_Euler_invariant(neighborhood, Euler_LUT) or + not is_simple_point(neighborhood)): continue # ok, add (p, r, c) to the list of simple border points @@ -310,9 +307,10 @@ cdef inline bint is_endpoint(pixel_type neighbors[]): cdef bint is_simple_point(pixel_type neighbors[]): """Check is a point is a Simple Point. + A point is simple iff its deletion does not change connectivity in + the 3x3x3 neighborhood. (cf conditions 2 and 3 in [Lee94]_). + This method is named "N(v)_labeling" in [Lee94]_. - Outputs the number of connected objects in a neighborhood of a point - after this point would have been removed. Parameters ---------- From 926af0ff53a1f1eef9ed8e267a140e9e0b2e3098 Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Mon, 22 Feb 2016 15:01:03 +0000 Subject: [PATCH 39/39] BUG: fix the conversion from .pyx.in to .pyx files It got botched sometime around reviewing/merging/rebasing of gh-1924 --- skimage/_build.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/_build.py b/skimage/_build.py index a091dd1e..f5db582d 100644 --- a/skimage/_build.py +++ b/skimage/_build.py @@ -91,6 +91,6 @@ def process_tempita_pyx(fromfile): if not fromfile.endswith('.pyx.in'): raise ValueError("Unexpected extension of %s." % fromfile) - pyxfile = os.path.splitext(fromfile)[0] + '.pyx' + pyxfile = os.path.splitext(fromfile)[0] # split off the .in ending with open(pyxfile, "w") as f: f.write(pyxcontent)