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 }