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 }