Skip to content

Commit 158784d

Browse files
authored
Merge pull request #136 from aferust/master
Implementation of the skeletonize function for binary regions
2 parents fc23041 + d613147 commit 158784d

File tree

7 files changed

+159
-2
lines changed

7 files changed

+159
-2
lines changed

README.md

+1-2
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ Library is licensed under Boost Software License - Version 1.0. Some modules in
5454
## dcv revision/revival notes
5555

5656
* This is an effort to make dcv work with the recent versions of LDC, mir libraries and stuff
57-
* I consider this as a temporary git repo which will be deleted after a big PR to the original DCV repo (if the maintainers accept).
5857

5958
## Done so far:
6059

@@ -68,7 +67,7 @@ Library is licensed under Boost Software License - Version 1.0. Some modules in
6867
## newly-implemented functionality:
6968
* Otsu's method for threshold calculation
7069
* dcv.measure module with refcounted image types: labelling connected regions, moments, ellipsefit, convexhull, findContours, area, perimeter
71-
* dcv.morphology module with distanceTransform (more planned like watershed, skeletonize, end-points, junctions)
70+
* dcv.morphology module with distanceTransform (more planned like watershed, ~~skeletonize~~, end-points, junctions)
7271
* Switched to modern OpenGL for rendering, by reserving the legacy gl support. use "subConfigurations": {"dcv:core": "legacygl"} for the legacy GL support.
7372
* New plot primitives like drawLine and drawCircle at Opengl rendering level. the function plot2imslice copies the rendered buffer to a slice. Take a look at the convexhull examples for more.
7473

examples/skeletonize/.gitignore

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.dub
2+
docs.json
3+
__dummy.html
4+
*.o
5+
*.obj
6+
*.exe
7+
/filter
8+
*.selections.json

examples/skeletonize/dub.json

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"name": "skeletonize",
3+
"description": "skeletonization of binary regions.",
4+
"copyright": "Copyright © 2022, Ferhat Kurtulmuş",
5+
"authors": ["Ferhat Kurtulmuş"],
6+
"dependencies": {
7+
"dcv:core": {"path": "../../"},
8+
"dcv:imageio": {"path": "../../"},
9+
"dcv:plot": {"path": "../../"}
10+
}
11+
}

examples/skeletonize/have_glfw3_dynamic_lib_here_or_on_the_path.txt

Whitespace-only changes.

examples/skeletonize/source/app.d

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import std.stdio;
2+
3+
import dcv.imageio.image : imread, imwrite;
4+
import dcv.core;
5+
import dcv.plot;
6+
import dcv.imgproc;
7+
import dcv.morphology;
8+
9+
import mir.rc;
10+
11+
void main()
12+
{
13+
Image img = imread("../data/test_labels.png");
14+
15+
Slice!(ubyte*, 2, Contiguous) gray = img.sliced.rgb2gray; // the test image is already binary here
16+
17+
auto skel = skeletonize2D(gray);
18+
19+
imwrite(skel.asImage(ImageFormat.IF_MONO), "result/skel.png");
20+
21+
imshow(skel, "skel");
22+
23+
waitKey();
24+
}

source/dcv/morphology/package.d

+1
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ public {
44
import dcv.morphology.distancetransform;
55
import dcv.morphology.floodfill;
66
import dcv.morphology.geometry;
7+
import dcv.morphology.skeletonize;
78
}

source/dcv/morphology/skeletonize.d

+114
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/*
2+
Copyright (c) 2021- Ferhat Kurtulmuş
3+
Boost Software License - Version 1.0 - August 17th, 2003
4+
*/
5+
module dcv.morphology.skeletonize;
6+
7+
import mir.ndslice;
8+
import mir.rc;
9+
10+
@nogc nothrow:
11+
12+
// A rewrite of https://github.com/scikit-image/scikit-image/blob/main/skimage/morphology/_skeletonize_cy.pyx
13+
14+
/** Apply fast skeletonize algorithm to given binary image.
15+
16+
Params:
17+
binary = Input binary image of ubyte. Agnostic to SliceKind
18+
Returns a refcounted Slice representing one pixel width skeletons of the binary regions. The resulting slice is Canonical.
19+
*/
20+
Slice!(RCI!ubyte, 2LU, Canonical) skeletonize2D(InputType)(auto ref InputType binary, int whiteValue = 255){
21+
22+
// we copy over the image into a larger version with a single pixel border
23+
// this removes the need to handle border cases below
24+
25+
immutable size_t nrows = binary.shape[0] + 2;
26+
immutable size_t ncols = binary.shape[1] + 2;
27+
28+
auto skeleton = uninitRCslice!ubyte(nrows, ncols);
29+
skeleton[] = 0;
30+
31+
skeleton[1..$-1, 1..$-1] = binary[]; // copy original data in the bordered frame
32+
33+
auto cleaned_skeleton = rcslice!ubyte(skeleton.shape[0], skeleton.shape[1]);
34+
cleaned_skeleton[] = skeleton[]; // dup
35+
36+
bool pixel_removed = true;
37+
38+
while(pixel_removed){
39+
pixel_removed = false;
40+
41+
// there are two phases, in the first phase, pixels labeled
42+
// (see below) 1 and 3 are removed, in the second 2 and 3
43+
44+
// nogil can't iterate through `(True, False)` because it is a Python
45+
// tuple. Use the fact that 0 is Falsy, and 1 is truthy in C
46+
// for the iteration instead.
47+
// for first_pass in (True, False):
48+
bool first_pass;
49+
foreach(pass_num; 0..2){
50+
first_pass = (pass_num == 0);
51+
foreach(row; 1..nrows-1){
52+
foreach(col; 1..ncols-1){
53+
// all set pixels ...
54+
55+
if(skeleton[row, col]){
56+
// are correlated with a kernel
57+
// (coefficients spread around here ...)
58+
// to apply a unique number to every
59+
// possible neighborhood ...
60+
61+
// which is used with the lut to find the
62+
// "connectivity type"
63+
64+
immutable neighbors = lut[skeleton[row - 1, col - 1] / whiteValue +
65+
2 * skeleton[row - 1, col] / whiteValue +
66+
4 * skeleton[row - 1, col + 1] / whiteValue +
67+
8 * skeleton[row, col + 1] / whiteValue +
68+
16 * skeleton[row + 1, col + 1] / whiteValue +
69+
32 * skeleton[row + 1, col] / whiteValue +
70+
64 * skeleton[row + 1, col - 1] / whiteValue +
71+
128 * skeleton[row, col - 1] / whiteValue];
72+
73+
if (neighbors == 0)
74+
continue;
75+
else if ((neighbors == 3) ||
76+
(neighbors == 1 && first_pass) ||
77+
(neighbors == 2 && !first_pass)){
78+
// Remove the pixel
79+
cleaned_skeleton[row, col] = 0;
80+
pixel_removed = true;
81+
}
82+
}
83+
}
84+
}
85+
// once a step has been processed, the original skeleton
86+
// is overwritten with the cleaned version
87+
skeleton[] = cleaned_skeleton[];
88+
}
89+
}
90+
91+
// because of the dropborders the resulting slice is Canonical
92+
// with an extra copying process it can be converted to a Contiguous Slice if this is needed.
93+
return skeleton.dropBorders;
94+
}
95+
96+
private immutable ubyte[256] lut = [0, 0, 0, 1, 0, 0, 1, 3, 0, 0, 3, 1, 1, 0,
97+
1, 3, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 2, 0,
98+
3, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0,
99+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
100+
2, 0, 0, 0, 3, 0, 2, 2, 0, 0, 0, 0, 0, 0,
101+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
102+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0,
103+
0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0,
104+
3, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 3, 0,
105+
2, 0, 0, 0, 3, 1, 0, 0, 1, 3, 0, 0, 0, 0,
106+
0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
107+
0, 0, 0, 0, 0, 1, 3, 1, 0, 0, 0, 0, 0, 0,
108+
0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0,
109+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 3, 1, 3,
110+
0, 0, 1, 3, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0,
111+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
112+
2, 3, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0,
113+
0, 0, 3, 3, 0, 1, 0, 0, 0, 0, 2, 2, 0, 0,
114+
2, 0, 0, 0];

0 commit comments

Comments
 (0)