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 }