1 /** 2 Module introduces image filtering functions and utilities. 3 4 Copyright: Copyright Relja Ljubobratovic 2016. 5 6 Authors: Relja Ljubobratovic 7 8 License: $(LINK3 http://www.boost.org/LICENSE_1_0.txt, Boost Software License - Version 1.0). 9 10 $(DL Module contains: 11 $(DD 12 $(BIG Filter kernel generators: ) 13 $(LINK2 #boxKernel,boxKernel) 14 $(LINK2 #radianKernel,radianKernel) 15 $(LINK2 #gaussian, gaussian) 16 $(LINK2 #laplacian,laplacian) 17 $(LINK2 #laplacianOfGaussian,laplacianOfGaussian) 18 $(LINK2 #sobel, sobel) 19 $(LINK2 #scharr,scharr) 20 $(LINK2 #prewitt,prewitt) 21 ) 22 $(DD 23 $(BIG Image processing functions: ) 24 $(LINK2 #filterNonMaximum, filterNonMaximum) 25 $(LINK2 #calcPartialDerivatives,calcPartialDerivatives) 26 $(LINK2 #calcGradients,calcGradients) 27 $(LINK2 #nonMaximaSupression,nonMaximaSupression) 28 $(LINK2 #canny,canny) 29 $(LINK2 #bilateralFilter,bilateralFilter) 30 $(LINK2 #medianFilter,medianFilter) 31 $(LINK2 #calcHistogram,calcHistogram) 32 $(LINK2 #histEqualize,histEqualize) 33 $(LINK2 #erode,erode) 34 $(LINK2 #dilate,dilate) 35 $(LINK2 #open,open) 36 $(LINK2 #close,close) 37 ) 38 ) 39 40 */ 41 42 module dcv.imgproc.filter; 43 44 import std.traits; 45 import std.range.primitives : ElementType, isForwardRange; 46 import std.exception : enforce; 47 import std.algorithm.sorting : topN; 48 import std.array : uninitializedArray; 49 import std.parallelism : parallel, taskPool, TaskPool; 50 51 import mir.utility : min, max; 52 import mir.math.common; 53 import mir.ndslice.allocation; 54 import mir.ndslice.internal : fastmath; 55 import mir.ndslice.topology; 56 import mir.ndslice.slice; 57 import mir.ndslice.algorithm : reduce, each; 58 59 import dcv.core.algorithm; 60 import dcv.core.utils; 61 62 /** 63 Box kernel creation. 64 65 Creates square kernel of given size, filled with given value. 66 67 Params: 68 rows = Rows, or height of kernel. 69 cols = Columns, or width of kernel. 70 value = Value of elements in the kernel. 71 72 Returns: 73 Kernel of size [rows, cols], filled with given value. 74 */ 75 Slice!(Contiguous, [2], T*) boxKernel(T)(size_t rows, size_t cols, T value = 1) 76 in 77 { 78 assert(rows > 1 && cols > 1, 79 "Invalid kernel size - rows, and columns have to be larger than 1."); 80 } 81 body 82 { 83 return slice!T([rows, cols], value); 84 } 85 86 /// ditto 87 Slice!(Contiguous, [2], T*) boxKernel(T)(size_t size, T value = 1) 88 in 89 { 90 assert(size > 1, "Invalid kernel size - has to be larger than 1."); 91 } 92 body 93 { 94 return boxKernel!T(size, size, value); 95 } 96 97 /** 98 Radial kernel creation. 99 100 Creates square kernel of given radius as edge length, with given values. 101 102 Params: 103 radius = Radius of kernel. Pixels in kernel with distance to center lesser than 104 radius will have value of foreground, other pixels will have value of background. 105 foreground = Foreground kernel values, or in the given radius (circle). Default is 1. 106 background = Background kernel values, or out of the given radius (circle). Default is 0. 107 108 Returns: 109 Kernel of size [radius, radius], filled with given values. 110 */ 111 Slice!(Contiguous, [2], T*) radialKernel(T)(size_t radius, T foreground = 1, T background = 0) 112 in 113 { 114 assert(radius >= 3, "Radial dilation kernel has to be of larger radius than 3."); 115 assert(radius % 2 != 0, "Radial dilation kernel has to be of odd radius."); 116 } 117 body 118 { 119 auto kernel = uninitializedSlice!T(radius, radius); 120 121 auto rf = cast(float)radius; 122 auto mid = radius / 2; 123 124 foreach (r; 0 .. radius) 125 { 126 foreach (c; 0 .. radius) 127 { 128 auto distanceToCenter = sqrt(cast(float)((mid - r) ^^ 2 + (mid - c) ^^ 2)); 129 kernel[r, c] = (distanceToCenter > mid) ? background : foreground; 130 } 131 } 132 133 return kernel; 134 } 135 136 /** 137 Instantiate 2D gaussian kernel. 138 */ 139 Slice!(Contiguous, [2], V*) gaussian(V = double)(V sigma, size_t width, size_t height) pure 140 { 141 142 static assert(isFloatingPoint!V, 143 "Gaussian kernel can be constructed only using floating point types."); 144 145 enforce(width > 2 && height > 2 && sigma > 0, "Invalid kernel values"); 146 147 auto h = uninitializedSlice!V(height, width); 148 149 int arrv_w = -(cast(int)width - 1) / 2; 150 int arrv_h = -(cast(int)height - 1) / 2; 151 float sgm = 2 * (sigma ^^ 2); 152 153 // build rows 154 foreach (r; 0 .. height) 155 { 156 h[r][] = iota!ptrdiff_t([width], arrv_w).map!"a * a"; 157 } 158 159 // build columns 160 foreach (c; 0 .. width) 161 { 162 h[0 .. height, c][] += iota([width], -arrv_h + 1).map!"a * a"; 163 h[0 .. height, c].each!((ref v) => v = exp(-v / sgm)); 164 } 165 166 // normalize 167 import mir.math.sum : sum; 168 169 h[] /= h.flattened.sum; 170 171 return h; 172 } 173 174 unittest 175 { 176 // TODO: design the test 177 178 auto fg = gaussian!float(1.0, 3, 3); 179 auto dg = gaussian!double(1.0, 3, 3); 180 auto rg = gaussian!real(1.0, 3, 3); 181 182 import std.traits; 183 184 static assert(__traits(compiles, gaussian!int(1, 3, 3)) == false, 185 "Integral test failed in gaussian kernel."); 186 } 187 188 /** 189 Create negative laplacian 3x3 kernel matrix. 190 191 Creates laplacian kernel matrix using 192 193 $(D_CODE 194 I - image 195 Laplacian(I) = 196 [a/4, (1-a)/4, a/4] 197 4/(a+1) * |(1-a)/4 -1 (1-a)/4| 198 [a/4, (1-a)/4, a/4] 199 ) 200 201 */ 202 Slice!(Contiguous, [2], T*) laplacian(T = double)(T a = 0.) pure nothrow if (isNumeric!T) 203 in 204 { 205 assert(a >= 0 && a <= 1); 206 } 207 body 208 { 209 auto k = uninitializedSlice!T(3, 3); 210 auto m = 4 / (a + 1); 211 auto e1 = (a / 4) * m; 212 auto e2 = ((1 - a) / 4) * m; 213 k[0, 0] = e1; 214 k[0, 2] = e1; 215 k[2, 0] = e1; 216 k[2, 2] = e1; 217 k[0, 1] = e2; 218 k[1, 0] = e2; 219 k[1, 2] = e2; 220 k[2, 1] = e2; 221 k[1, 1] = -m; 222 return k; 223 } 224 225 /// 226 unittest 227 { 228 assert(laplacian().flattened == [0, 1, 0, 1, -4, 1, 0, 1, 0]); 229 } 230 231 /** 232 Create laplacian of gaussian $(LINK3 http://homepages.inf.ed.ac.uk/rbf/HIPR2/log.htm, (LoG)) filter kernel. 233 234 Params: 235 sigma = gaussian sigma variance value 236 width = width of the kernel matrix 237 height = height of the kernel matrix 238 */ 239 Slice!(Contiguous, [2], T*) laplacianOfGaussian(T = double)(T sigma, 240 size_t width, size_t height) 241 { 242 import std.traits : isFloatingPoint; 243 244 static assert(isFloatingPoint!T); 245 246 import mir.math.sum : sum; 247 import mir.math.common : exp; 248 import mir.utility : max; 249 import std.math : E, PI; 250 251 auto k = slice!T(height, width); 252 253 auto ss = sigma * sigma; 254 auto ts = -1 / (cast(T)PI * (ss * ss)); 255 auto ss2 = 2 * ss; 256 auto w_h = cast(T)max(1u, width / 2); 257 auto h_h = cast(T)max(1u, height / 2); 258 259 foreach (i; 0 .. height) 260 foreach (j; 0 .. width) 261 { 262 auto xx = (cast(T)j - w_h); 263 auto yy = (cast(T)i - h_h); 264 xx *= xx; 265 yy *= yy; 266 auto xy = (xx + yy) / ss2; 267 k[i, j] = ts * (1. - xy) * exp(-xy); 268 } 269 270 k[] += -cast(T)(cast(float)k.flattened.sum / cast(float)(width * height)); 271 return k; 272 } 273 274 /// 275 unittest 276 { 277 import std.algorithm.comparison : equal; 278 import std.math : approxEqual; 279 280 auto log = laplacianOfGaussian!float(0.84f, 3, 3); 281 auto expected = [0.147722, -0.00865228, 0.147722, -0.00865228, 282 -0.556277, -0.00865228, 0.147722, -0.00865228, 0.147722].sliced(3, 3); 283 assert(equal!approxEqual(log.flattened, expected.flattened)); 284 } 285 286 enum GradientDirection 287 { 288 DIR_X, // x direction (x partial gradients) 289 DIR_Y, // y direction (y partial gradients) 290 DIAG, // diagonal, from top-left to bottom right 291 DIAG_INV, // inverse diagonal, from top-right to bottom left 292 } 293 294 /** 295 Convolution kernel type for edge detection. 296 */ 297 public enum EdgeKernel 298 { 299 SIMPLE, 300 SOBEL, 301 SCHARR, 302 PREWITT 303 } 304 305 /// Create a Sobel edge kernel. 306 Slice!(Contiguous, [2], T*) sobel(T = double)(GradientDirection direction) nothrow pure @trusted 307 { 308 return edgeKernelImpl!(T)(direction, cast(T)1, cast(T)2); 309 } 310 311 /// Create a Scharr edge kernel. 312 Slice!(Contiguous, [2], T*) scharr(T = double)(GradientDirection direction) nothrow pure @trusted 313 { 314 return edgeKernelImpl!(T)(direction, cast(T)3, cast(T)10); 315 } 316 317 /// Create a Prewitt edge kernel. 318 Slice!(Contiguous, [2], T*) prewitt(T = double)(GradientDirection direction) nothrow pure @trusted 319 { 320 return edgeKernelImpl!(T)(direction, cast(T)1, cast(T)1); 321 } 322 323 /// Create a kernel of given type. 324 Slice!(Contiguous, [2], T*) edgeKernel(T)(EdgeKernel kernelType, 325 GradientDirection direction) nothrow pure @trusted 326 { 327 typeof(return) k; 328 final switch (kernelType) 329 { 330 case EdgeKernel.SOBEL: 331 k = sobel!T(direction); 332 break; 333 case EdgeKernel.SCHARR: 334 k = scharr!T(direction); 335 break; 336 case EdgeKernel.PREWITT: 337 k = prewitt!T(direction); 338 break; 339 case EdgeKernel.SIMPLE: 340 break; 341 } 342 return k; 343 } 344 345 private Slice!(Contiguous, [2], T*) edgeKernelImpl(T)( 346 GradientDirection direction, T lv, T hv) nothrow pure @trusted 347 { 348 final switch (direction) 349 { 350 case GradientDirection.DIR_X: 351 return [-lv, 0, lv, -hv, 0, hv, -lv, 0, lv].sliced(3, 3).as!T.slice; 352 case GradientDirection.DIR_Y: 353 return [-lv, -hv, -lv, 0, 0, 0, lv, hv, lv].sliced(3, 3).as!T.slice; 354 case GradientDirection.DIAG: 355 return [-hv, -lv, 0, -lv, 0, lv, 0, lv, hv].sliced(3, 3).as!T.slice; 356 case GradientDirection.DIAG_INV: 357 return [0, -lv, -hv, lv, 0, -lv, hv, lv, 0].sliced(3, 3).as!T.slice; 358 } 359 } 360 361 // test sobel and scharr 362 unittest 363 { 364 auto s = edgeKernelImpl!int(GradientDirection.DIR_X, 1, 2); 365 auto expected = (cast(int[])[-1, 0, 1, -2, 0, 2, -1, 0, 1]).sliced(3, 3); 366 assert(s.flattened == expected.flattened); 367 } 368 369 unittest 370 { 371 auto s = edgeKernelImpl!int(GradientDirection.DIR_Y, 1, 2); 372 auto expected = (cast(int[])[-1, -2, -1, 0, 0, 0, 1, 2, 1]).sliced(3, 3); 373 assert(s.flattened == expected.flattened); 374 } 375 376 unittest 377 { 378 auto s = edgeKernelImpl!int(GradientDirection.DIAG, 1, 2); 379 auto expected = (cast(int[])[-2, -1, 0, -1, 0, 1, 0, 1, 2]).sliced(3, 3); 380 assert(s.flattened == expected.flattened); 381 } 382 383 unittest 384 { 385 auto s = edgeKernelImpl!int(GradientDirection.DIAG_INV, 1, 2); 386 auto expected = (cast(int[])[0, -1, -2, 1, 0, -1, 2, 1, 0]).sliced(3, 3); 387 assert(s.flattened == expected.flattened); 388 } 389 390 /** 391 Perform non-maxima filtering of the image. 392 393 Params: 394 input = Input matrix. 395 filterSize = Size of filtering kernel (aperture). 396 397 Returns: 398 Input matrix, after filtering. 399 */ 400 auto filterNonMaximum(SliceKind kind, Iterator)(Slice!(kind, [2], 401 Iterator) input, size_t filterSize = 10) 402 in 403 { 404 assert(!input.empty && filterSize); 405 } 406 body 407 { 408 import mir.ndslice.topology : universal; 409 import mir.ndslice.dynamic : strided; 410 411 immutable fs = filterSize; 412 immutable fsh = max(size_t(1), fs / 2); 413 414 input.universal.windows(fs, fs).strided!(0, 1)(fsh, fsh).each!filterNonMaximumImpl; 415 416 return input; 417 } 418 419 /** 420 Calculate partial derivatives of an slice. 421 422 Partial derivatives are calculated by convolving an slice with 423 [-1, 1] kernel, horizontally and vertically. 424 */ 425 void calcPartialDerivatives(InputTensor, V = DeepElementType!InputTensor)( 426 InputTensor input, ref Slice!(Contiguous, [2], V*) fx, 427 ref Slice!(Contiguous, [2], V*) fy, TaskPool pool = taskPool) if (isFloatingPoint!V) 428 in 429 { 430 static assert(isSlice!InputTensor, 431 "Invalid input tensor type - has to be of type mir.ndslice.slice.Slice."); 432 static assert(InputTensor.init.shape.length == 2, 433 "Input tensor has to be 2 dimensional. (matrix)"); 434 } 435 body 436 { 437 if (input.empty) 438 return; 439 440 if (fx.shape != input.shape) 441 fx = uninitializedSlice!V(input.shape); 442 if (fy.shape != input.shape) 443 fy = uninitializedSlice!V(input.shape); 444 445 if (input.length!0 > 1) 446 foreach (r; pool.parallel(iota([input.length!0 - 1], 1))) 447 { 448 auto x_row = fx[r, 0 .. $]; 449 auto y_row = fy[r, 0 .. $]; 450 foreach (c; 1 .. input.length!1) 451 { 452 auto imrc = input[r, c]; 453 x_row[c] = cast(V)(-1. * input[r, c - 1] + imrc); 454 y_row[c] = cast(V)(-1. * input[r - 1, c] + imrc); 455 } 456 } 457 458 // calc border edges 459 auto x_row = fx[0, 0 .. $]; 460 auto y_row = fy[0, 0 .. $]; 461 462 if (input.length!1 > 1) 463 foreach (c; pool.parallel(iota(input.length!1 - 1))) 464 { 465 auto im_0c = input[0, c]; 466 x_row[c] = cast(V)(-1. * im_0c + input[0, c + 1]); 467 y_row[c] = cast(V)(-1. * im_0c + input[1, c]); 468 } 469 470 auto x_col = fx[0 .. $, 0]; 471 auto y_col = fy[0 .. $, 0]; 472 473 if (input.length!0 > 1) 474 foreach (r; pool.parallel(iota(input.length!0 - 1))) 475 { 476 auto im_r_0 = input[r, 0]; 477 x_col[r] = cast(V)(-1. * im_r_0 + input[r, 1]); 478 y_col[r] = cast(V)(-1. * im_r_0 + input[r + 1, 0]); 479 } 480 481 // edges corner pixels 482 fx[0, input.length!1 - 1] = cast(V)(-1 * input[0, 483 input.length!1 - 2] + input[0, input.length!1 - 1]); 484 fy[0, input.length!1 - 1] = cast(V)(-1 * input[0, 485 input.length!1 - 1] + input[1, input.length!1 - 1]); 486 fx[input.length!0 - 1, 0] = cast(V)(-1 * input[input.length!0 - 1, 487 0] + input[input.length!0 - 1, 1]); 488 fy[input.length!0 - 1, 0] = cast(V)(-1 * input[input.length!0 - 2, 489 0] + input[input.length!0 - 1, 0]); 490 } 491 492 /** 493 Calculate gradient magnitude and orientation of an image slice. 494 495 Params: 496 input = Input slice of an image. 497 mag = Output magnitude value of gradients. If shape does not correspond to input, is 498 allocated anew. 499 orient = Orientation value of gradients in radians. If shape does not correspond to 500 input, is allocated anew. 501 edgeKernelType = Optional convolution kernel type to calculate partial derivatives. 502 Default value is EdgeKernel.SIMPLE, which calls calcPartialDerivatives function 503 to calculate derivatives. Other options will perform convolution with requested 504 kernel type. 505 pool = TaskPool instance used parallelise the algorithm. 506 507 Note: 508 Input slice's memory has to be contiguous. Magnitude and orientation slices' strides 509 have to be the identical. 510 */ 511 void calcGradients(InputTensor, V = DeepElementType!InputTensor) 512 ( 513 InputTensor input, 514 ref Slice!(Contiguous, [2], V*) mag, 515 ref Slice!(Contiguous, [2], V*) orient, 516 EdgeKernel edgeKernelType = EdgeKernel.SIMPLE, 517 TaskPool pool = taskPool 518 ) if (isFloatingPoint!V) 519 in 520 { 521 static assert(isSlice!InputTensor, "Input tensor has to be of type mir.ndslice.slice.Slice"); 522 static assert(InputTensor.init.shape.length == 2, 523 "Input tensor has to be 2 dimensional. (matrix)"); 524 assert(!input.empty); 525 } 526 body 527 { 528 if (mag.shape != input.shape) 529 mag = uninitializedSlice!V(input.shape); 530 531 if (orient.shape != input.shape) 532 orient = uninitializedSlice!V(input.shape); 533 534 Slice!(Contiguous, [2], V*) fx, fy; 535 if (edgeKernelType == EdgeKernel.SIMPLE) 536 { 537 calcPartialDerivatives(input, fx, fy, pool); 538 } 539 else 540 { 541 import dcv.imgproc.convolution; 542 543 Slice!(Contiguous, [2], V*) kx, ky; 544 kx = edgeKernel!V(edgeKernelType, GradientDirection.DIR_X); 545 ky = edgeKernel!V(edgeKernelType, GradientDirection.DIR_Y); 546 fx = input.conv(kx, emptySlice!([2], V), emptySlice!([2], V), pool); 547 fy = input.conv(ky, emptySlice!([2], V), emptySlice!([2], V), pool); 548 } 549 550 assert(fx.strides == mag.strides || fx.strides == orient.strides, 551 "Magnitude and orientation slices must be contiguous."); 552 553 if (mag.strides == orient.strides && mag.strides == fx.strides) 554 { 555 auto data = zip!true(fx, fy, mag, orient); 556 foreach (row; pool.parallel(data)) 557 { 558 row.each!(p => calcGradientsImpl(p.a, p.b, p.c, p.d)); 559 } 560 } 561 else 562 { 563 foreach (row; pool.parallel(ndiota(input.shape))) 564 { 565 row.each!(i => calcGradientsImpl(fx[i], fy[i], mag[i], orient[i])); 566 } 567 } 568 } 569 570 @fastmath void calcGradientsImpl(T)(T fx, T fy, ref T mag, ref T orient) 571 { 572 import mir.math.common : sqrt; 573 import std.math : atan2; 574 575 mag = sqrt(fx * fx + fy * fy); 576 orient = atan2(fy, fx); 577 } 578 579 /** 580 Edge detection impulse non-maxima supression. 581 582 Filtering used in canny edge detection algorithm - suppresses all 583 edge impulses (gradient values along edges normal) except the peek value. 584 585 Params: 586 mag = Gradient magnitude. 587 orient = Gradient orientation of the same image source as magnitude. 588 prealloc = Optional pre-allocated buffer for output slice. 589 pool = TaskPool instance used parallelise the algorithm. 590 591 Note: 592 Orientation and pre-allocated structures must match. If prealloc 593 buffer is not given, orient memory has to be contiguous. 594 See: 595 dcv.imgproc.filter.calcGradients, dcv.imgproc.convolution 596 */ 597 Slice!(Contiguous, [2], V*) nonMaximaSupression(InputTensor, V = DeepElementType!InputTensor) 598 ( 599 InputTensor mag, 600 InputTensor orient, 601 Slice!(Contiguous, [2], V*) prealloc = emptySlice!([2], V), 602 TaskPool pool = taskPool 603 ) 604 in 605 { 606 static assert(isSlice!InputTensor, "Input tensor has to be of type mir.ndslice.slice.Slice"); 607 static assert(InputTensor.init.shape.length == 2, 608 "Input tensor has to be 2 dimensional. (matrix)"); 609 610 assert(!mag.empty && !orient.empty); 611 assert(mag.shape == orient.shape); 612 assert(mag.strides == orient.strides, 613 "Magnitude and Orientation tensor strides have to be the same."); 614 } 615 body 616 { 617 import std.array : uninitializedArray; 618 import std.math : PI; 619 620 alias F = DeepElementType!InputTensor; 621 622 if (prealloc.shape != orient.shape || prealloc.strides != mag.strides) 623 prealloc = uninitializedSlice!V(mag.shape); 624 625 assert(prealloc.strides == orient.strides, 626 "Orientation and preallocated slice strides do not match."); 627 628 auto magWindows = mag.windows(3, 3); 629 auto dPack = zip!true(prealloc[1 .. $ - 1, 1 .. $ - 1], orient[1 .. $ - 1, 1 .. $ - 1]); 630 631 auto innerShape = magWindows.shape; 632 633 foreach (r; pool.parallel(iota(innerShape[0]))) 634 { 635 auto d = dPack[r]; 636 auto m = magWindows[r]; 637 foreach (c; 0 .. innerShape[1]) 638 { 639 nonMaximaSupressionImpl(d[c], m[c]); 640 } 641 } 642 643 return prealloc; 644 } 645 646 /** 647 Perform canny filtering on an image to expose edges. 648 649 Params: 650 slice = Input image slice. 651 lowThresh = lower threshold value after non-maxima suppression. 652 upThresh = upper threshold value after non-maxima suppression. 653 edgeKernelType = Type of edge kernel used to calculate image gradients. 654 prealloc = Optional pre-allocated buffer. 655 pool = TaskPool instance used parallelise the algorithm. 656 */ 657 Slice!(Contiguous, [2], V*) canny(V, T, SliceKind kind) 658 ( 659 Slice!(kind, [2], T*) slice, 660 T lowThresh, 661 T upThresh, 662 EdgeKernel edgeKernelType = EdgeKernel.SOBEL, 663 Slice!(Contiguous, [2], V*) prealloc = emptySlice!([2], V), 664 TaskPool pool = taskPool 665 ) 666 { 667 import dcv.imgproc.threshold; 668 import dcv.core.algorithm : ranged; 669 670 V upval = isFloatingPoint!V ? 1 : V.max; 671 672 Slice!(Contiguous, [2], float*) mag, orient; 673 calcGradients(slice, mag, orient, edgeKernelType); 674 675 return nonMaximaSupression(mag, orient, emptySlice!([2], T), pool).ranged(0, 676 upval).threshold(lowThresh, upThresh, prealloc); 677 } 678 679 /** 680 Perform canny filtering on an image to expose edges. 681 682 Convenience function to call canny with same lower and upper threshold values, 683 similar to dcv.imgproc.threshold.threshold. 684 */ 685 Slice!(Contiguous, [2], V*) canny(V, T, SliceKind kind) 686 ( 687 Slice!(kind, [2], T*) slice, 688 T thresh, 689 EdgeKernel edgeKernelType = EdgeKernel.SOBEL, 690 Slice!(Contiguous, [2], V*) prealloc = emptySlice!([2], V) 691 ) 692 { 693 return canny!(V, T)(slice, thresh, thresh, edgeKernelType, prealloc); 694 } 695 696 /** 697 $(LINK2 https://en.wikipedia.org/wiki/Bilateral_filter,Bilateral) filtering implementation. 698 699 Non-linear, edge-preserving and noise-reducing smoothing filtering algorithm. 700 701 Params: 702 bc = Boundary condition test used to index the image slice. 703 input = Slice of the input image. 704 sigmaCol = Color sigma value. 705 sigmaSpace = Spatial sigma value. 706 kernelSize = Size of convolution kernel. Must be odd number. 707 prealloc = Optional pre-allocated result image buffer. If not of same shape as input slice, its allocated anew. 708 pool = Optional TaskPool instance used to parallelize computation. 709 710 Returns: 711 Slice of filtered image. 712 */ 713 Slice!(Contiguous, packs, OutputType*) bilateralFilter(OutputType, alias bc = neumann, SliceKind kind, size_t[] packs, Iterator) 714 ( 715 Slice!(kind, packs, Iterator) input, 716 float sigmaCol, 717 float sigmaSpace, 718 size_t kernelSize, 719 Slice!(Contiguous, packs, OutputType*) prealloc = emptySlice!(packs, OutputType), 720 TaskPool pool = taskPool 721 ) 722 in 723 { 724 static assert(isBoundaryCondition!bc, "Invalid boundary condition test function."); 725 assert(!input.empty); 726 assert(kernelSize % 2); 727 } 728 body 729 { 730 immutable N = packs[0]; 731 732 if (prealloc.shape != input.shape) 733 prealloc = uninitializedSlice!OutputType(input.shape); 734 735 static if (N == 2) 736 { 737 bilateralFilter2(input, sigmaCol, sigmaSpace, kernelSize, prealloc, pool); 738 } 739 else static if (N == 3) 740 { 741 foreach (channel; 0 .. input.length!2) 742 { 743 auto inch = input[0 .. $, 0 .. $, channel]; 744 auto prech = prealloc[0 .. $, 0 .. $, channel]; 745 bilateralFilter2(inch, sigmaCol, sigmaSpace, kernelSize, prech, pool); 746 } 747 } 748 else 749 { 750 static assert(0, "Invalid slice dimensionality - 2 and 3 dimensional slices allowed."); 751 } 752 753 return prealloc; 754 } 755 756 private void bilateralFilter2(OutputType, alias bc = neumann, 757 SliceKind outputKind, SliceKind kind, Iterator)(Slice!(kind, [2], 758 Iterator) input, float sigmaCol, float sigmaSpace, size_t kernelSize, 759 Slice!(outputKind, [2], OutputType*) prealloc, TaskPool pool = taskPool) 760 in 761 { 762 assert(prealloc.shape == input.shape); 763 } 764 body 765 { 766 auto ks = kernelSize; 767 auto kh = max(1u, ks / 2); 768 769 auto inputWindows = input.windows(kernelSize, kernelSize); 770 auto innerBody = prealloc[kh .. $ - kh, kh .. $ - kh]; 771 auto inShape = innerBody.shape; 772 auto shape = input.shape; 773 774 auto threadMask = pool.workerLocalStorage(slice!float(ks, ks)); 775 776 foreach (r; pool.parallel(iota(inShape[0]))) 777 { 778 auto maskBuf = threadMask.get(); 779 foreach (c; 0 .. inShape[1]) 780 { 781 innerBody[r, c] = bilateralFilterImpl(inputWindows[r, c], maskBuf, sigmaCol, sigmaSpace); 782 } 783 } 784 785 foreach (border; pool.parallel(input.shape.borders(ks)[])) 786 { 787 auto maskBuf = threadMask.get(); 788 foreach (r; border.rows) 789 foreach (c; border.cols) 790 { 791 import mir.ndslice.field; 792 import mir.ndslice.iterator; 793 794 static struct ndIotaWithShiftField 795 { 796 ptrdiff_t[2] _shift; 797 ndIotaField!2 _field; 798 Slice!(kind, [2], Iterator) _input; 799 auto opIndex(ptrdiff_t index) 800 { 801 auto ret = _field[index]; 802 ptrdiff_t r = _shift[0] - cast(ptrdiff_t)ret[0]; 803 ptrdiff_t c = _shift[1] - cast(ptrdiff_t)ret[1]; 804 return bc(_input, r, c); 805 } 806 } 807 808 auto inputWindow = FieldIterator!ndIotaWithShiftField(0, ndIotaWithShiftField([r + kh, c + kh], ndIotaField!2(ks), input)).sliced(ks, ks); 809 prealloc[r, c] = bilateralFilterImpl(inputWindow, maskBuf, sigmaCol, sigmaSpace); 810 } 811 } 812 } 813 814 /** 815 Median filtering algorithm. 816 817 Params: 818 slice = Input image slice. 819 kernelSize = Square size of median kernel. 820 prealloc = Optional pre-allocated return image buffer. 821 pool = Optional TaskPool instance used to parallelize computation. 822 823 Returns: 824 Returns filtered image of same size as the input. If prealloc parameter is not an empty slice, and is 825 of same size as input slice, return value is assigned to prealloc buffer. If not, newly allocated buffer 826 is used. 827 */ 828 Slice!(Contiguous, packs, O*) medianFilter(alias BoundaryConditionTest = neumann, T, O = T, SliceKind kind, size_t[] packs) 829 ( 830 Slice!(kind, packs, T*) slice, 831 size_t kernelSize, 832 Slice!(Contiguous, packs, O*) prealloc = emptySlice!(packs, O), 833 TaskPool pool = taskPool 834 ) 835 in 836 { 837 import std.traits : isAssignable; 838 839 static assert(isAssignable!(T, O), 840 "Output slice value type is not assignable to the input value type."); 841 static assert(isBoundaryCondition!BoundaryConditionTest, 842 "Given boundary condition test is not DCV valid boundary condition test function."); 843 844 assert(!slice.empty()); 845 } 846 body 847 { 848 if (prealloc.shape != slice.shape) 849 prealloc = uninitializedSlice!O(slice.shape); 850 851 static if (packs == [1]) 852 alias medianFilterImpl = medianFilterImpl1; 853 else static if (packs == [2]) 854 alias medianFilterImpl = medianFilterImpl2; 855 else static if (packs == [3]) 856 alias medianFilterImpl = medianFilterImpl3; 857 else 858 static assert(0, "Invalid slice dimension for median filtering."); 859 860 medianFilterImpl!BoundaryConditionTest(slice, prealloc, kernelSize, pool); 861 862 return prealloc; 863 } 864 865 unittest 866 { 867 auto imvalues = [1, 20, 3, 54, 5, 643, 7, 80, 9].sliced(9); 868 assert(imvalues.medianFilter!neumann(3) == [1, 3, 20, 5, 54, 7, 80, 9, 9]); 869 } 870 871 unittest 872 { 873 auto imvalues = [1, 20, 3, 54, 5, 643, 7, 80, 9].sliced(3, 3); 874 assert(imvalues.medianFilter!neumann(3).flattened == [5, 5, 5, 7, 9, 9, 7, 9, 9]); 875 } 876 877 unittest 878 { 879 auto imvalues = [1, 20, 3, 43, 65, 76, 12, 5, 7, 54, 5, 643, 12, 54, 76, 880 15, 68, 9, 65, 87, 17, 38, 0, 12, 21, 5, 7].sliced(3, 3, 3); 881 assert(imvalues.medianFilter!neumann(3).flattened == [12, 20, 76, 12, 20, 9, 882 12, 54, 9, 43, 20, 17, 21, 20, 12, 15, 5, 9, 54, 54, 17, 38, 5, 12, 21, 5, 883 9]); 884 } 885 886 /** 887 Calculate range value histogram. 888 889 Params: 890 HistogramType = (template parameter) Histogram type. Can be static or dynamic array, most commonly 891 of 32 bit integer, of size T.max + 1, where T is element type of input range. 892 range = Input forward range, for which histogram is calculated. 893 894 Returns: 895 Histogram for given forward range. 896 */ 897 HistogramType calcHistogram(Range, HistogramType = int[(ElementType!Range).max + 1]) 898 ( 899 Range range 900 ) if (isForwardRange!Range && (isDynamicArray!HistogramType || isStaticArray!HistogramType)) 901 in 902 { 903 static if (isStaticArray!HistogramType) 904 { 905 static assert( 906 HistogramType.init.length == ( 907 ElementType!Range.max + 1), 908 "Invalid histogram size - if given histogram type is static array, it has to be of lenght T.max + 1"); 909 } 910 } 911 body 912 { 913 alias ValueType = ElementType!Range; 914 915 HistogramType histogram; 916 static if (isDynamicArray!HistogramType) 917 { 918 histogram.lenght = ValueType.max + 1; 919 } 920 histogram[] = cast(ElementType!HistogramType)0; 921 922 foreach (v; range) 923 { 924 histogram[cast(size_t)v]++; 925 } 926 927 return histogram; 928 } 929 930 /** 931 Histogram Equalization. 932 933 Equalize histogram of given image slice. Slice can be 2D for grayscale, and 3D for color images. 934 If 3D slice is given, histogram is applied separatelly for each channel. 935 936 Example: 937 ---- 938 import dcv.core, dcv.io, dcv.imgproc, dcv.plot; 939 940 void main() 941 { 942 Image image = imread("dcv/examples/data/lena.png"); 943 944 auto slice = image.sliced.rgb2gray; 945 auto equalized = slice.histEqualize(slice.flattened.calcHistogram); 946 947 slice.imshow("Original"); 948 equalized.imshow("Equalized"); 949 950 waitKey(); 951 } 952 ---- 953 Example code will equalize grayscale Lena image, from this: 954 955 $(IMAGE https://github.com/libmir/dcv/blob/master/examples/data/lena_gray.png?raw=true) 956 957 ... to this: 958 959 $(IMAGE https://github.com/libmir/dcv/blob/master/examples/data/histEqualExample.png?raw=true) 960 961 Note: 962 For more valid color histogram equalization results, try converting image to HSV color model 963 to perform equalization for V channel, $(LINK2 https://en.wikipedia.org/wiki/Histogram_equalization#Histogram_equalization_of_color_images,to alter the color as less as possible). 964 965 Params: 966 HistogramType = (template parameter) Histogram type, see $(LINK2 #calcHistogram,calcHistogram) function for details. 967 slice = Input image slice. 968 histogram = Histogram values for input image slice. 969 prealloc = Optional pre-allocated buffer where equalized image is saved. 970 971 Returns: 972 Copy of input image slice with its histogram values equalized. 973 */ 974 auto histEqualize(T, HistogramType, SliceKind kind, size_t[] packs) 975 ( 976 Slice!(kind, packs, T*) slice, 977 HistogramType histogram, 978 Slice!(Contiguous, packs, T*) prealloc = emptySlice!(packs, T) 979 ) 980 in 981 { 982 static assert(packs.length == 1, "Packed slices are not allowed."); 983 assert(!slice.empty()); 984 static if (isDynamicArray!HistogramType) 985 { 986 assert(histogram.length == T.max + 1, "Invalid histogram length."); 987 } 988 } 989 body 990 { 991 immutable N = packs[0]; 992 993 int n = cast(int)slice.elementsCount; // number of pixels in image. 994 immutable tmax = cast(int)T.max; // maximal possible value for pixel value type. 995 996 // The probability of an occurrence of a pixel of level i in the image 997 float[tmax + 1] cdf; 998 999 cdf[0] = cast(float)histogram[0] / cast(float)n; 1000 foreach (i; 1 .. tmax + 1) 1001 { 1002 cdf[i] = cdf[i - 1] + cast(float)histogram[i] / cast(float)n; 1003 } 1004 1005 if (prealloc.shape != slice.shape) 1006 prealloc = uninitializedSlice!T(slice.shape); 1007 1008 static if (N == 2) 1009 { 1010 histEqualImpl(slice, cdf, prealloc); 1011 } 1012 else static if (N == 3) 1013 { 1014 foreach (c; 0 .. slice.length!2) 1015 { 1016 histEqualImpl(slice[0 .. $, 0 .. $, c], cdf, prealloc[0 .. $, 0 .. $, c]); 1017 } 1018 } 1019 else 1020 { 1021 static assert(0, 1022 "Invalid dimension for histogram equalization. Only 2D and 3D slices supported."); 1023 } 1024 1025 return prealloc; 1026 } 1027 1028 /** 1029 Perform morphological $(LINK3 https://en.wikipedia.org/wiki/Erosion_(morphology),erosion). 1030 1031 Use given kernel matrix to estimate image erosion for given image slice. Given slice is 1032 considered to be binarized with $(LINK2 #threshold, threshold) method. 1033 1034 For given input slice: 1035 ---- 1036 1 1 1 1 1 1 1 1 1 1 1 1 1 1037 1 1 1 1 1 1 0 1 1 1 1 1 1 1038 1 1 1 1 1 1 1 1 1 1 1 1 1 1039 1 1 1 1 1 1 1 1 1 1 1 1 1 1040 1 1 1 1 1 1 1 1 1 1 1 1 1 1041 1 1 1 1 1 1 1 1 1 1 1 1 1 1042 1 1 1 1 1 1 1 1 1 1 1 1 1 1043 1 1 1 1 1 1 1 1 1 1 1 1 1 1044 1 1 1 1 1 1 1 1 1 1 1 1 1 1045 1 1 1 1 1 1 1 1 1 1 1 1 1 1046 1 1 1 1 1 1 1 1 1 1 1 1 1 1047 1 1 1 1 1 1 1 1 1 1 1 1 1 1048 1 1 1 1 1 1 1 1 1 1 1 1 1 1049 ---- 1050 ... And erosion kernel of: 1051 ---- 1052 1 1 1 1053 1 1 1 1054 1 1 1 1055 ---- 1056 ... Resulting slice is: 1057 ---- 1058 0 0 0 0 0 0 0 0 0 0 0 0 0 1059 0 1 1 1 1 0 0 0 1 1 1 1 0 1060 0 1 1 1 1 0 0 0 1 1 1 1 0 1061 0 1 1 1 1 1 1 1 1 1 1 1 0 1062 0 1 1 1 1 1 1 1 1 1 1 1 0 1063 0 1 1 1 1 1 1 1 1 1 1 1 0 1064 0 1 1 1 1 1 1 1 1 1 1 1 0 1065 0 1 1 1 1 1 1 1 1 1 1 1 0 1066 0 1 1 1 1 1 1 1 1 1 1 1 0 1067 0 1 1 1 1 1 1 1 1 1 1 1 0 1068 0 1 1 1 1 1 1 1 1 1 1 1 0 1069 0 1 1 1 1 1 1 1 1 1 1 1 0 1070 0 0 0 0 0 0 0 0 0 0 0 0 0 1071 ---- 1072 1073 Note: 1074 Erosion works only for 2D binary images. 1075 1076 Params: 1077 slice = Input image slice, to be eroded. 1078 kernel = Erosion kernel. Default value is radialKernel!T(3). 1079 prealloc = Optional pre-allocated buffer to hold result. 1080 pool = Optional TaskPool instance used to parallelize computation. 1081 1082 Returns: 1083 Eroded image slice, of same type as input image. 1084 */ 1085 Slice!(kind, [2], T*) erode(alias BoundaryConditionTest = neumann, T, SliceKind kind) 1086 ( 1087 Slice!(kind, [2], T*) slice, 1088 Slice!(kind, [2], T*) kernel = radialKernel!T(3), 1089 Slice!(kind, [2], T*) prealloc = emptySlice!([2], T), 1090 TaskPool pool = taskPool 1091 ) if (isBoundaryCondition!BoundaryConditionTest) 1092 { 1093 return morphOp!(MorphologicOperation.ERODE, BoundaryConditionTest)(slice, 1094 kernel, prealloc, pool); 1095 } 1096 1097 /** 1098 Perform morphological $(LINK3 https://en.wikipedia.org/wiki/Dilation_(morphology),dilation). 1099 1100 Use given kernel matrix to estimate image dilation for given image slice. Given slice is 1101 considered to be binarized with $(LINK2 #threshold, threshold) method. 1102 1103 For given input slice: 1104 ---- 1105 0 0 0 0 0 0 0 0 0 0 0 1106 0 1 1 1 1 0 0 1 1 1 0 1107 0 1 1 1 1 0 0 1 1 1 0 1108 0 1 1 1 1 1 1 1 1 1 0 1109 0 1 1 1 1 1 1 1 1 1 0 1110 0 1 1 0 0 0 1 1 1 1 0 1111 0 1 1 0 0 0 1 1 1 1 0 1112 0 1 1 0 0 0 1 1 1 1 0 1113 0 1 1 1 1 1 1 1 0 0 0 1114 0 1 1 1 1 1 1 1 0 0 0 1115 0 0 0 0 0 0 0 0 0 0 0 1116 ---- 1117 ... And dilation kernel of: 1118 ---- 1119 1 1 1 1120 1 1 1 1121 1 1 1 1122 ---- 1123 ... Resulting slice is: 1124 ---- 1125 1 1 1 1 1 1 1 1 1 1 1 1126 1 1 1 1 1 1 1 1 1 1 1 1127 1 1 1 1 1 1 1 1 1 1 1 1128 1 1 1 1 1 1 1 1 1 1 1 1129 1 1 1 1 1 1 1 1 1 1 1 1130 1 1 1 1 1 1 1 1 1 1 1 1131 1 1 1 1 0 1 1 1 1 1 1 1132 1 1 1 1 1 1 1 1 1 1 1 1133 1 1 1 1 1 1 1 1 1 1 1 1134 1 1 1 1 1 1 1 1 1 0 0 1135 1 1 1 1 1 1 1 1 1 0 0 1136 ---- 1137 1138 Note: 1139 Dilation works only for 2D binary images. 1140 1141 Params: 1142 slice = Input image slice, to be eroded. 1143 kernel = Dilation kernel. Default value is radialKernel!T(3). 1144 prealloc = Optional pre-allocated buffer to hold result. 1145 pool = Optional TaskPool instance used to parallelize computation. 1146 1147 Returns: 1148 Dilated image slice, of same type as input image. 1149 */ 1150 Slice!(kind, [2], T*) dilate(alias BoundaryConditionTest = neumann, T, SliceKind kind) 1151 ( 1152 Slice!(kind, [2], T*) slice, 1153 Slice!(kind, [2], T*) kernel = radialKernel!T(3), 1154 Slice!(kind, [2], T*) prealloc = emptySlice!([2], T), 1155 TaskPool pool = taskPool 1156 ) if (isBoundaryCondition!BoundaryConditionTest) 1157 { 1158 return morphOp!(MorphologicOperation.DILATE, BoundaryConditionTest)(slice, 1159 kernel, prealloc, pool); 1160 } 1161 1162 /** 1163 Perform morphological $(LINK3 https://en.wikipedia.org/wiki/Opening_(morphology),opening). 1164 1165 Performs erosion, than on the resulting eroded image performs dilation. 1166 1167 Note: 1168 Opening works only for 2D binary images. 1169 1170 Params: 1171 slice = Input image slice, to be eroded. 1172 kernel = Erosion/Dilation kernel. Default value is radialKernel!T(3). 1173 prealloc = Optional pre-allocated buffer to hold result. 1174 pool = Optional TaskPool instance used to parallelize computation. 1175 1176 Returns: 1177 Opened image slice, of same type as input image. 1178 */ 1179 Slice!(kind, [2], T*) open(alias BoundaryConditionTest = neumann, T, SliceKind kind) 1180 ( 1181 Slice!(kind, [2], T*) slice, 1182 Slice!(kind, [2], T*) kernel = radialKernel!T(3), 1183 Slice!(kind, [2], T*) prealloc = emptySlice!([2], T), 1184 TaskPool pool = taskPool 1185 ) if (isBoundaryCondition!BoundaryConditionTest) 1186 { 1187 return morphOp!(MorphologicOperation.DILATE, BoundaryConditionTest)( 1188 morphOp!(MorphologicOperation.ERODE, BoundaryConditionTest)(slice, 1189 kernel, emptySlice!([2], T), pool), kernel, prealloc, pool); 1190 } 1191 1192 /** 1193 Perform morphological $(LINK3 https://en.wikipedia.org/wiki/Closing_(morphology),closing). 1194 1195 Performs dilation, than on the resulting dilated image performs erosion. 1196 1197 Note: 1198 Closing works only for 2D binary images. 1199 1200 Params: 1201 slice = Input image slice, to be eroded. 1202 kernel = Erosion/Dilation kernel. Default value is radialKernel!T(3). 1203 prealloc = Optional pre-allocated buffer to hold result. 1204 pool = Optional TaskPool instance used to parallelize computation. 1205 1206 Returns: 1207 Closed image slice, of same type as input image. 1208 */ 1209 Slice!(kind, [2], T*) close(alias BoundaryConditionTest = neumann, T, SliceKind kind)( 1210 Slice!(kind, [2], T*) slice, Slice!(kind, [2], 1211 T*) kernel = radialKernel!T(3), Slice!(kind, [2], 1212 T*) prealloc = emptySlice!([2], T), TaskPool pool = taskPool) if ( 1213 isBoundaryCondition!BoundaryConditionTest) 1214 { 1215 return morphOp!(MorphologicOperation.ERODE, BoundaryConditionTest)( 1216 morphOp!(MorphologicOperation.DILATE, BoundaryConditionTest)(slice, 1217 kernel, emptySlice!([2], T), pool), kernel, prealloc, pool); 1218 } 1219 1220 @fastmath void calcBilateralMask(Window, Mask)(Window window, Mask mask, 1221 float sigmaCol, float sigmaSpace) 1222 { 1223 import mir.math.common : exp, sqrt; 1224 1225 auto in_val = window[$ / 2, $ / 2]; 1226 float rd, cd, c_val, s_val; 1227 float i, j, wl2; 1228 wl2 = -(cast(float)window.length!0 / 2.0f); 1229 i = wl2; 1230 foreach (r; 0 .. window.length!0) 1231 { 1232 rd = i * i; 1233 j = wl2; 1234 foreach (c; 0 .. window.length!1) 1235 { 1236 cd = j * j; 1237 auto cdiff = cast(float)(window[r, c] - in_val); 1238 c_val = exp((cd + rd) / (-2.0f * sigmaCol * sigmaCol)); 1239 s_val = exp((cdiff * cdiff) / (-2.0f * sigmaSpace * sigmaSpace)); 1240 mask[r, c] = c_val * s_val; 1241 j++; 1242 } 1243 i++; 1244 } 1245 } 1246 1247 @fastmath T calcBilateralValue(T, M)(T r, T i, M m) 1248 { 1249 return cast(T)(r + i * m); 1250 } 1251 1252 void filterNonMaximumImpl(Window)(Window window) 1253 { 1254 alias T = DeepElementType!Window; 1255 1256 static if (isFloatingPoint!T) 1257 auto lmsVal = -T.max; 1258 else 1259 auto lmsVal = T.min; 1260 1261 T* locPtr = null; 1262 1263 foreach (row; window) 1264 foreach (ref e; row) 1265 { 1266 if (e > lmsVal) 1267 { 1268 locPtr = &e; 1269 lmsVal = e; 1270 } 1271 e = T(0); 1272 } 1273 1274 if (locPtr !is null) 1275 { 1276 *locPtr = lmsVal; 1277 } 1278 } 1279 1280 private: 1281 1282 void nonMaximaSupressionImpl(DataPack, MagWindow)(DataPack p, MagWindow magWin) 1283 { 1284 import mir.math.common; 1285 1286 alias F = typeof(p.a); 1287 1288 auto ang = p.b; 1289 auto aang = fabs(ang); 1290 1291 auto mag = magWin[1, 1]; 1292 typeof(mag) mb, ma; // magnitude before and after cursor 1293 1294 immutable pi = 3.15f; 1295 immutable pi8 = pi / 8.0f; 1296 1297 if (aang <= pi && aang > 7.0f * pi8) 1298 { 1299 mb = magWin[1, 0]; 1300 ma = magWin[1, 2]; 1301 } 1302 else if (ang >= -7.0f * pi8 && ang < -5.0f * pi8) 1303 { 1304 mb = magWin[0, 0]; 1305 ma = magWin[2, 2]; 1306 } 1307 else if (ang <= 7.0f * pi8 && ang > 5.0f * pi8) 1308 { 1309 mb = magWin[0, 2]; 1310 ma = magWin[2, 0]; 1311 } 1312 else if (ang >= pi8 && ang < 3.0f * pi8) 1313 { 1314 mb = magWin[2, 0]; 1315 ma = magWin[0, 2]; 1316 } 1317 else if (ang <= -pi8 && ang > -3.0f * pi8) 1318 { 1319 mb = magWin[2, 2]; 1320 ma = magWin[0, 0]; 1321 } 1322 else if (ang >= -5.0f * pi8 && ang < -3.0f * pi8) 1323 { 1324 mb = magWin[0, 1]; 1325 ma = magWin[2, 1]; 1326 } 1327 else if (ang <= 5.0f * pi8 && ang > 3.0f * pi8) 1328 { 1329 mb = magWin[2, 1]; 1330 ma = magWin[0, 1]; 1331 } 1332 else if (aang >= 0.0f && aang < pi8) 1333 { 1334 mb = magWin[1, 2]; 1335 ma = magWin[1, 0]; 1336 } 1337 1338 p.a = cast(F)((ma > mb) ? 0 : mag); 1339 } 1340 1341 auto bilateralFilterImpl(Window, Mask)(Window window, Mask mask, float sigmaCol, float sigmaSpace) 1342 { 1343 import mir.math.common; 1344 1345 calcBilateralMask(window, mask, sigmaCol, sigmaSpace); 1346 mask[] *= 1f / reduce!"a + b"(0f, mask.as!float.map!fabs); 1347 1348 alias T = DeepElementType!Window; 1349 alias M = DeepElementType!Mask; 1350 1351 return reduce!(calcBilateralValue!(T, M))(T(0), window, mask); 1352 } 1353 1354 void medianFilterImpl1(alias bc, T, O, SliceKind kind0, SliceKind kind1)( 1355 Slice!(kind0, [1], T*) slice, Slice!(kind1, [1], O*) filtered, 1356 size_t kernelSize, TaskPool pool) 1357 { 1358 import std.parallelism; 1359 1360 import mir.utility : max; 1361 1362 int kh = max(1, cast(int)kernelSize / 2); 1363 1364 auto kernelStorage = pool.workerLocalStorage(new T[kernelSize]); 1365 1366 foreach (i; pool.parallel(slice.length!0.iota!ptrdiff_t)) 1367 { 1368 auto kernel = kernelStorage.get(); 1369 size_t ki = 0; 1370 foreach (ii; i - kh .. i + kh + 1) 1371 { 1372 kernel[ki++] = bc(slice, ii); 1373 } 1374 topN(kernel, kh); 1375 filtered[i] = kernel[kh]; 1376 } 1377 } 1378 1379 void medianFilterImpl2(alias bc, T, O, SliceKind kind0, SliceKind kind1)( 1380 Slice!(kind0, [2], T*) slice, Slice!(kind1, [2], O*) filtered, 1381 size_t kernelSize, TaskPool pool) 1382 { 1383 int kh = max(1, cast(int)kernelSize / 2); 1384 int n = cast(int)(kernelSize * kernelSize); 1385 int m = n / 2; 1386 1387 auto kernelStorage = pool.workerLocalStorage(new T[kernelSize * kernelSize]); 1388 1389 foreach (r; pool.parallel(slice.length!0.iota!ptrdiff_t)) 1390 { 1391 auto kernel = kernelStorage.get(); 1392 foreach (ptrdiff_t c; 0 .. slice.length!1) 1393 { 1394 size_t i = 0; 1395 foreach (rr; r - kh .. r + kh + 1) 1396 { 1397 foreach (cc; c - kh .. c + kh + 1) 1398 { 1399 kernel[i++] = bc(slice, rr, cc); 1400 } 1401 } 1402 topN(kernel, m); 1403 filtered[r, c] = kernel[m]; 1404 } 1405 } 1406 } 1407 1408 void medianFilterImpl3(alias bc, T, O, SliceKind kind) 1409 (Slice!(kind, [3], T*) slice, Slice!(Contiguous, [3], O*) filtered, size_t kernelSize, TaskPool pool) 1410 { 1411 foreach (channel; 0 .. slice.length!2) 1412 { 1413 medianFilterImpl2!bc(slice[0 .. $, 0 .. $, channel], filtered[0 .. $, 1414 0 .. $, channel], kernelSize, pool); 1415 } 1416 } 1417 1418 void histEqualImpl(T, Cdf, SliceKind kind0, SliceKind kind1) 1419 (Slice!(kind0, [2], T*) slice, Cdf cdf, Slice!(kind1, [2], T*) prealloc = emptySlice!([2], T)) 1420 { 1421 foreach (e; zip(prealloc.flattened, slice.flattened)) 1422 e.a = cast(T)(e.b * cdf[e.b]); 1423 } 1424 1425 enum MorphologicOperation 1426 { 1427 ERODE, 1428 DILATE 1429 } 1430 1431 Slice!(kind, [2], T*) morphOp(MorphologicOperation op, alias BoundaryConditionTest = neumann, T, SliceKind kind) 1432 (Slice!(kind, [2], T*) slice, Slice!(kind, [2], T*) kernel, Slice!(kind, [2], T*) prealloc, TaskPool pool) 1433 if (isBoundaryCondition!BoundaryConditionTest) 1434 in 1435 { 1436 assert(!slice.empty); 1437 } 1438 body 1439 { 1440 if (prealloc.shape != slice.shape) 1441 prealloc = uninitializedSlice!T(slice.shape); 1442 1443 ptrdiff_t khr = max(size_t(1), kernel.length!0 / 2); 1444 ptrdiff_t khc = max(size_t(1), kernel.length!1 / 2); 1445 1446 static if (op == MorphologicOperation.ERODE) 1447 { 1448 immutable checkSlice = "!slice[r, c]"; 1449 immutable checkMorphResult = "!v"; 1450 T value = cast(T)0; 1451 } 1452 else 1453 { 1454 immutable checkSlice = "slice[r, c]"; 1455 immutable checkMorphResult = "v"; 1456 1457 static if (isIntegral!T) 1458 T value = T.max; 1459 else 1460 T value = cast(T)1.0; 1461 } 1462 1463 foreach (r; pool.parallel(slice.length!0.iota!ptrdiff_t)) 1464 { 1465 foreach (ptrdiff_t c; 0 .. slice.length!1) 1466 { 1467 1468 if (mixin(checkSlice)) 1469 { 1470 prealloc[r, c] = value; 1471 continue; 1472 } 1473 1474 size_t rk = 0; 1475 foreach (rr; r - khr .. r + khr + 1) 1476 { 1477 size_t ck = 0; 1478 foreach (cc; c - khc .. c + khc + 1) 1479 { 1480 auto kv = kernel[rk, ck]; 1481 if (kv) 1482 { 1483 auto v = BoundaryConditionTest(slice, rr, cc) * kv; 1484 if (mixin(checkMorphResult)) 1485 { 1486 prealloc[r, c] = value; 1487 goto skip_dil; 1488 } 1489 } 1490 ++ck; 1491 } 1492 ++rk; 1493 } 1494 prealloc[r, c] = slice[r, c]; 1495 skip_dil: 1496 } 1497 } 1498 1499 return prealloc; 1500 }