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