1 /*
2 Copyright (c) 2019- Ferhat Kurtulmuş
3 Boost Software License - Version 1.0 - August 17th, 2003
4 Permission is hereby granted, free of charge, to any person or organization
5 obtaining a copy of the software and accompanying documentation covered by
6 this license (the "Software") to use, reproduce, display, distribute,
7 execute, and transmit the Software, and to prepare derivative works of the
8 Software, and to permit third-parties to whom the Software is furnished to
9 do so, all subject to the following:
10 The copyright notices in the Software and this entire statement, including
11 the above license grant, this restriction and the following disclaimer,
12 must be included in all copies of the Software, in whole or in part, and
13 all derivative works of the Software, unless such copies or derivative
14 works are solely in the form of machine-executable object code generated by
15 a source language processor.
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
19 SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
20 FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
21 ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
22 DEALINGS IN THE SOFTWARE.
23 */
24 
25 module measure.regionprops;
26 
27 import std.stdio;
28 import std.math;
29 import std.typecons;
30 import std.array;
31 import std.algorithm;
32 
33 import measure.types;
34 import measure.chull;
35 
36 private
37 {
38     alias DfsFun = void function(size_t, size_t, uint, uint[], Mat2D!ubyte);
39     size_t row_count;
40     size_t col_count;
41     XYList[uint] pmap;
42 }
43 
44 
45 immutable int[4] dx4 = [1, 0, -1,  0];
46 immutable int[4] dy4 = [0, 1,  0, -1];
47 
48 immutable int[8] dx8 = [1, -1, 1, 0, -1,  1,  0, -1];
49 immutable int[8] dy8 = [0,  0, 1, 1,  1, -1, -1, -1];
50 
51 private void dfs4(size_t i, size_t j, uint current_label, uint[] label, Mat2D!ubyte img)
52 {
53     if (i < 0 || i == row_count) return;
54     if (j < 0 || j == col_count) return;
55     if (label[i*col_count + j] || !img.data[i*col_count + j]) return;
56     
57     label[i*col_count + j] = current_label;
58     
59     if(current_label !in pmap)
60     {
61         XYList tmp;
62         tmp.xs ~= cast(uint)j;
63         tmp.ys ~= cast(uint)i;
64         pmap[current_label] = tmp;
65     }else{
66         pmap[current_label].xs ~= cast(uint)j;
67         pmap[current_label].ys ~= cast(uint)i;
68     }
69     
70     
71     foreach(direction; 0..4)
72         dfs4(i + dx4[direction], j + dy4[direction], current_label, label, img);
73 }
74 
75 private void dfs8(size_t i, size_t j, uint current_label, uint[] label, Mat2D!ubyte img)
76 {
77     if (i < 0 || i == row_count) return;
78     if (j < 0 || j == col_count) return;
79     if (label[i*col_count + j] || !img.data[i*col_count + j]) return;
80     
81     label[i*col_count + j] = current_label;
82     if(current_label !in pmap)
83     {
84         XYList tmp;
85         tmp.xs ~= cast(uint)j;
86         tmp.ys ~= cast(uint)i;
87         pmap[current_label] = tmp;
88     }else{
89         pmap[current_label].xs ~= cast(uint)j;
90         pmap[current_label].ys ~= cast(uint)i;
91     }
92     
93     foreach(direction; 0..8)
94         dfs8(i + dx8[direction], j + dy8[direction], current_label, label, img);
95 }
96 
97 Mat2D!uint bwlabel(Mat2D!ubyte img, uint conn = 8)
98 {
99     /* The algorithm is based on:
100      * https://stackoverflow.com/questions/14465297/connected-component-labelling
101      * 
102      * this needs a high value of max stack size,
103      * so add "dflags": ["-L/STACK:1500000000"] to the dub.json
104      */
105     pmap = null;
106     row_count = img.height;
107     col_count = img.width;
108     
109     auto label = new uint[row_count*col_count]; //uninitializedArray!(ubyte[])(row_count*col_count);
110     auto res = Mat2D!uint(row_count, col_count);
111     
112     DfsFun dfs;
113     
114     if(conn == 4)
115         dfs = &dfs4;
116     else
117         dfs = &dfs8;
118     
119     ubyte component = 0;
120     foreach (i; 0..row_count) 
121         foreach (j; 0..col_count){
122             if (!label[i*col_count + j] && img.data[i*col_count + j]) 
123                 dfs(i, j, ++component, label, img);
124         }
125         
126             
127     
128     // the number of blobs is "label.maxElement"
129     
130     res.data[] = label[];
131     return res;     
132 }
133 
134 XYList bin2coords(Mat2D!ubyte img)
135 {
136     size_t rc = img.height;
137     size_t cc = img.width;
138     
139     XYList coords;
140     
141     foreach (i; 0..rc) 
142         foreach (j; 0..cc)
143             if(img.data[i*cc + j] == 255)
144             {
145                     coords.xs ~= cast(int)j;
146                     coords.ys ~= cast(int)i;
147             }
148     
149     return coords;
150 }
151 
152 Mat2D!ubyte coords2mat(XYList xylist, Rectangle rect)
153 {
154     auto im = Mat2D!ubyte(rect.height, rect.width);
155     
156     auto n = xylist.xs.length;
157     
158     foreach(i; 0..n)
159     {
160         im[xylist.ys[i]-rect.y, xylist.xs[i]-rect.x] = 255;
161     }
162     return im;   
163 }
164 
165 private void _setValAtIdx_with_padding(Mat2D!ubyte img, XYList xylist, int val, int pad = 2)
166 {
167     foreach (i; 0..xylist.xs.length)
168         img[xylist.ys[i]+pad/2, xylist.xs[i]+pad/2] = cast(ubyte)val;
169 }
170 
171 XYList
172 getContinousBoundaryPoints( Mat2D!ubyte unpadded)
173 {
174     // https://www.codeproject.com/Articles/1105045/Tracing-Boundary-in-D-Image-Using-Moore-Neighborho
175     // https://www.codeproject.com/info/cpol10.aspx
176     // author Udaya K Unnikrishnan
177     auto _rows = unpadded.height;
178     auto _cols = unpadded.width;
179     int pad = 2;
180     
181     auto region = Mat2D!ubyte(_rows + pad, _cols + pad);
182     XYList coords = bin2coords(unpadded);
183     _setValAtIdx_with_padding(region, coords, 255);
184     
185     Point[] BoundaryPoints;
186     int Width_i = cast(int)region.width;
187     int Height_i = cast(int)region.height;
188     ubyte[] InputImage = region.data;
189     if( InputImage !is null)
190     {
191         
192         
193         int nImageSize = Width_i * Height_i;
194         
195         int[][] Offset = [
196                             [ -1, -1 ],
197                             [  0, -1 ], 
198                             [  1, -1 ],  
199                             [  1,  0 ], 
200                             [  1,  1 ],
201                             [  0,  1 ], 
202                             [ -1,  1 ],
203                             [ -1,  0 ]
204         ];
205         
206         const int NEIGHBOR_COUNT = 8;
207         auto BoundaryPixelCord = Point(0, 0);
208         auto BoundaryStartingPixelCord = Point(0, 0);
209         auto BacktrackedPixelCord = Point(0, 0);
210         int[][] BackTrackedPixelOffset = [ [0,0] ];
211         bool bIsBoundaryFound = false;
212         bool bIsStartingBoundaryPixelFound = false;
213         for(int Idx = 0; Idx < nImageSize; ++Idx ) // getting the starting pixel of boundary
214         {
215             if( 0 != InputImage[Idx] )
216             {
217                 BoundaryPixelCord.x = Idx % Width_i;
218                 BoundaryPixelCord.y = Idx / Width_i;
219                 BoundaryStartingPixelCord = BoundaryPixelCord;
220                 BacktrackedPixelCord.x = ( Idx - 1 ) % Width_i;
221                 BacktrackedPixelCord.y = ( Idx - 1 ) / Width_i;
222                 BackTrackedPixelOffset[0][0] = BacktrackedPixelCord.x - BoundaryPixelCord.x;
223                 BackTrackedPixelOffset[0][1] = BacktrackedPixelCord.y - BoundaryPixelCord.y;
224                 BoundaryPoints ~= BoundaryPixelCord;
225                 bIsStartingBoundaryPixelFound = true;
226                 break;
227             }            
228         }
229         auto CurrentBoundaryCheckingPixelCord = Point(0, 0);
230         auto PrevBoundaryCheckingPixxelCord = Point(0, 0);
231         if( !bIsStartingBoundaryPixelFound )
232         {
233             BoundaryPoints.length--;
234         }
235         while( true && bIsStartingBoundaryPixelFound )
236         {
237             int CurrentBackTrackedPixelOffsetInd = -1;
238             foreach( int Ind; 0..NEIGHBOR_COUNT )
239             {
240                 if( BackTrackedPixelOffset[0][0] == Offset[Ind][0] &&
241                     BackTrackedPixelOffset[0][1] == Offset[Ind][1] )
242                 {
243                     CurrentBackTrackedPixelOffsetInd = Ind;// Finding the bracktracked 
244                                                            // pixel's offset index
245                     break;
246                 }
247             }
248             int Loop = 0;
249             while( Loop < ( NEIGHBOR_COUNT - 1 ) && CurrentBackTrackedPixelOffsetInd != -1 )
250             {
251                 int OffsetIndex = ( CurrentBackTrackedPixelOffsetInd + 1 ) % NEIGHBOR_COUNT;
252                 CurrentBoundaryCheckingPixelCord.x = BoundaryPixelCord.x + Offset[OffsetIndex][0];
253                 CurrentBoundaryCheckingPixelCord.y = BoundaryPixelCord.y + Offset[OffsetIndex][1];
254                 int ImageIndex = CurrentBoundaryCheckingPixelCord.y * Width_i + 
255                                     CurrentBoundaryCheckingPixelCord.x;
256                 
257                 if( 0 != InputImage[ImageIndex] )// finding the next boundary pixel
258                 {
259                     BoundaryPixelCord = CurrentBoundaryCheckingPixelCord; 
260                     BacktrackedPixelCord = PrevBoundaryCheckingPixxelCord;
261                     BackTrackedPixelOffset[0][0] = BacktrackedPixelCord.x - BoundaryPixelCord.x;
262                     BackTrackedPixelOffset[0][1] = BacktrackedPixelCord.y - BoundaryPixelCord.y;
263                     BoundaryPoints ~= BoundaryPixelCord;
264                     break;
265                 }
266                 PrevBoundaryCheckingPixxelCord = CurrentBoundaryCheckingPixelCord;
267                 CurrentBackTrackedPixelOffsetInd += 1;
268                 Loop++;
269             }
270             if( BoundaryPixelCord.x == BoundaryStartingPixelCord.x &&
271                 BoundaryPixelCord.y == BoundaryStartingPixelCord.y ) // if the current pixel = 
272                                                                      // starting pixel
273             {
274                 BoundaryPoints.length--;
275                 bIsBoundaryFound = true;
276                 break;
277             }
278         }
279         if( !bIsBoundaryFound ) // If there is no connected boundary clear the list
280         {
281             BoundaryPoints.length=0;
282         }
283     }
284     XYList xys;
285     foreach(i; 0..BoundaryPoints.length){
286         xys.xs ~= BoundaryPoints[i].x - pad/2;
287         xys.ys ~= BoundaryPoints[i].y - pad/2;
288     }
289     return xys;
290 }
291 
292 double contourArea(XYList xylist)
293 {
294     auto npoints = xylist.xs.length;
295     auto xx = xylist.xs;
296     auto yy = xylist.ys;
297     
298     double area = 0.0;
299     
300     foreach(i; 0..npoints){
301         auto j = (i + 1) % npoints;
302         area += xx[i] * yy[j];
303         area -= xx[j] * yy[i];
304     }
305     area = abs(area) / 2.0;
306     return area;
307 }
308 
309 double arcLength(XYList xylist)
310 {
311     double perimeter = 0.0, xDiff = 0.0, yDiff = 0.0;
312     for( auto k = 0; k < xylist.xs.length-1; k++ ) {
313         xDiff = xylist.xs[k+1] - xylist.xs[k];
314         yDiff = xylist.ys[k+1] - xylist.ys[k];
315         perimeter += pow( xDiff*xDiff + yDiff*yDiff, 0.5 );
316     }
317     xDiff = xylist.xs[xylist.xs.length-1] - xylist.xs[0];
318     yDiff = xylist.ys[xylist.xs.length-1] - xylist.ys[0];
319     perimeter += pow( xDiff*xDiff + yDiff*yDiff, 0.5 );
320     
321     return perimeter;
322 }
323 
324 Rectangle boundingBox(XYList xylist)
325 {
326     int minx = xylist.xs.minElement;
327     int miny = xylist.ys.minElement;
328     int width = xylist.xs.maxElement - xylist.xs.minElement + 1;
329     int height = xylist.ys.maxElement - xylist.ys.minElement + 1;
330     
331     Rectangle rect = {x: minx, y: miny, width: width, height: height};
332     return rect;
333 }
334 
335 Mat2D!ubyte idxListToSubImage(Rectangle rect, XYList idxlist)
336 {
337     
338     auto res = Mat2D!ubyte(rect.height, rect.width);
339     
340     int yOffset = rect.y;
341     int xOffset = rect.x;
342     
343     foreach(i; 0..idxlist.xs.length)
344         res[idxlist.ys[i]-yOffset, idxlist.xs[i]-xOffset] = 255;
345     
346     return res;
347     
348 }
349 
350 Mat2D!ubyte subImage(Mat2D!ubyte img, Rectangle ROI)
351 {
352     auto cc =img.width;
353     auto subIm = Mat2D!ubyte(ROI.height, ROI.width);
354     ubyte* ptr = subIm.data.ptr;
355     
356     foreach (int i; ROI.y..ROI.y+ROI.height) 
357         foreach (int j; ROI.x..ROI.x+ROI.width)
358         {
359             *ptr = img.data[i*cc + j];
360             ptr ++;
361         }
362     
363     return subIm;
364 }
365 
366 void addXYOffset(ref XYList xylist, int xOffset, int yOffset)
367 {
368     auto npoints = xylist.xs.length;
369     foreach(i; 0..npoints)
370     {
371         xylist.xs[i] += xOffset;
372         xylist.ys[i] += yOffset;
373     }
374 }
375 
376 import measure.chull;
377 import measure.ellipsefit;
378 import measure.moments;
379 import measure.label2;
380 
381 class RegionProps
382 {
383     Region[] regions;
384     Mat2D!ubyte parentBin;
385     
386     size_t parentHeight;
387     size_t parentWidth;
388     
389     Rectangle[] bboxes;
390     XYList[] coords;
391     
392     size_t count = 0;
393     
394     this(Mat2D!ubyte imbin, bool recursiveLabeling = true)
395     {
396         parentHeight = imbin.height;
397         parentWidth = imbin.width;
398         
399         parentBin = imbin;
400         
401         if(recursiveLabeling)
402         {
403             Mat2D!uint labelIm;
404             labelIm = bwlabel(parentBin);
405             foreach(i; 0..pmap.keys.length)
406             {
407                 bboxes ~= boundingBox(pmap[cast(uint)(i+1)]);
408                 coords ~= pmap[cast(uint)(i+1)];
409             }
410         }else
411         {
412             coords = bwlabel2(parentBin);
413             foreach(coord; coords)
414                 bboxes ~= boundingBox(coord);
415         }
416 
417         count = bboxes.length;
418         
419         regions.length = count;
420     }
421     
422     void calculateProps()
423     {
424         foreach(i; 0..count)
425         {
426             Region region = new Region();
427             region.bBox = bboxes[i];
428             Mat2D!ubyte imsub = idxListToSubImage(bboxes[i],coords[i]);
429             
430             auto contourIdx_sorted = getContinousBoundaryPoints(imsub);
431             
432             addXYOffset(contourIdx_sorted, region.bBox.x, region.bBox.y);
433             
434             region.image = imsub;
435             
436             region.aspect_Ratio = region.bBox.width / cast(double)region.bBox.height;
437             region.extent = cast(double)region.area/(region.bBox.width*region.bBox.height);
438             
439             region.perimeter = arcLength(contourIdx_sorted); // holes are ignored
440             region.areaFromContour = contourArea(contourIdx_sorted); // holes are ignored
441             region.area = coords[i].xs.length;
442             region.pixelList = coords[i];
443             
444             region.contourPixelList = contourIdx_sorted;
445             
446             calculateMoments(region);
447             
448             // centroid is computed correctly, so we ensure that raw moments are correct
449             region.centroid = Point(cast(int)round(region.m10/region.m00) + region.bBox.x + 1,
450                                     cast(int)round(region.m01/region.m00) + region.bBox.y + 1);
451             /* or region.centroid = Point(cast(int)round(mean(coords[i].xs)) + 1,
452                                     cast(int)round(mean(coords[i].ys)) + 1); */
453             
454             region.equivalentDiameter = sqrt(4*region.area/PI);
455             
456             //region.ellipse = ellipseFit(region.pixelList);
457             region.ellipse = ellipseFit2(region);
458             
459             region.orientation = region.ellipse.angle;
460             
461             region.majorAxisLength = region.ellipse.maj;
462             region.minorAxisLength = region.ellipse.min; 
463             
464             region.eccentricity = sqrt(1.0 - (region.minorAxisLength / region.majorAxisLength)^^2);
465             
466             region.convexHull = convexHull(region.contourPixelList);
467             region.convexArea = contourArea(region.convexHull);
468             region.solidity = region.area / region.convexArea;
469             
470             regions[i] = region;
471             
472         }
473     }
474 }