1 /**
2 Image manipulation module.
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             $(LINK2 #resize, resize)
13             $(LINK2 #scale, scale)
14             $(LINK2 #transformAffine,transformAffine)
15             $(LINK2 #transformPerspective,transformPerspective)
16             $(LINK2 #warp,warp)
17             $(LINK2 #remap,remap)
18     )
19 )
20 */
21 module dcv.imgproc.imgmanip;
22 
23 import std.exception : enforce;
24 import std.parallelism : TaskPool, taskPool, parallel;
25 import std.range.primitives : ElementType;
26 import std.traits;
27 
28 import dcv.core.utils;
29 public import dcv.imgproc.interpolate;
30 
31 import mir.ndslice.slice;
32 import mir.ndslice.topology;
33 
34 /**
35 Resize array using custom interpolation function.
36 
37 Primarilly implemented as image resize. 
38 1D, 2D and 3D arrays are supported, where 3D array is
39 treated as channeled image - each channel is interpolated 
40 as isolated 2D array (matrix).
41 
42 Interpolation function is given as a template parameter. 
43 Default interpolation function is linear. Custom interpolation
44 function can be implemented in the 3rd party code, by following
45 interpolation function rules in dcv.imgproc.interpolation.
46 
47 Params:
48     slice = Slice to an input array.
49     newsize = tuple that defines new shape. New dimension has to be
50     the same as input slice in the 1D and 2D resize, where in the 
51     3D resize newsize has to be 2D.
52     pool = Optional TaskPool instance used to parallelize computation.
53 
54 TODO: consider size input as array, and add prealloc
55 */
56 Slice!(SliceKind.contiguous, packs, V*) resize(alias interp = linear, SliceKind kind, size_t[] packs, V, size_t SN)(Slice!(kind, packs, V*) slice, size_t[SN] newsize, TaskPool pool = taskPool)
57     if (packs.length == 1)
58     //if (isInterpolationFunc!interp)
59 {
60     static if (packs[0] == 1)
61     {
62         static assert(SN == 1, "Invalid new-size setup - dimension does not match with input slice.");
63         return resizeImpl_1!interp(slice, newsize[0], pool);
64     }
65     else static if (packs[0] == 2)
66     {
67         static assert(SN == 2, "Invalid new-size setup - dimension does not match with input slice.");
68         return resizeImpl_2!interp(slice, newsize[0], newsize[1], pool);
69     }
70     else static if (packs[0] == 3)
71     {
72         static assert(SN == 2, "Invalid new-size setup - 3D resize is performed as 2D."); // TODO: find better way to say this...
73         return resizeImpl_3!interp(slice, newsize[0], newsize[1], pool);
74     }
75     else
76     {
77         static assert(0, "Resize is not supported for slice with " ~ N.stringof ~ " dimensions.");
78     }
79 }
80 
81 unittest
82 {
83     auto vector = [0.0f, 0.1f, 0.2f].sliced(3);
84     auto matrix = [0.0f, 0.1f, 0.2f, 0.3f].sliced(2, 2);
85     auto image = [0.0f, 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f].sliced(2, 2, 2);
86 
87     auto resv = vector.resize!linear([10]);
88     assert(resv.shape.length == 1);
89     assert(resv.length == 10);
90 
91     auto resm = matrix.resize!linear([10, 15]);
92     assert(resm.shape.length == 2);
93     assert(resm.length!0 == 10 && resm.length!1 == 15);
94 
95     auto resi = image.resize!linear([20, 14]);
96     assert(resi.shape.length == 3);
97     assert(resi.length!0 == 20 && resi.length!1 == 14 && resi.length!2 == 2);
98 }
99 
100 /**
101 Scale array size using custom interpolation function.
102 
103 Implemented as convenience function which calls resize 
104 using scaled shape of the input slice as:
105 
106 $(D_CODE scaled = resize(input, input.shape*scale))
107 
108  */
109 Slice!(kind, packs, V*) scale(alias interp = linear, V, ScaleValue, SliceKind kind, size_t[] packs, size_t SN)(Slice!(kind, packs, V*) slice, ScaleValue[SN] scale, TaskPool pool = taskPool)
110         if (isFloatingPoint!ScaleValue && isInterpolationFunc!interp)
111 {
112     foreach (v; scale)
113         assert(v > 0., "Invalid scale values (v > 0.0)");
114 
115     static if (packs[0] == 1)
116     {
117         static assert(SN == 1, "Invalid scale setup - dimension does not match with input slice.");
118         size_t newsize = cast(size_t)(slice.length * scale[0]);
119         enforce(newsize > 0, "Scaling value invalid - after scaling array size is zero.");
120         return resizeImpl_1!interp(slice, newsize, pool);
121     }
122     else static if (packs[0] == 2)
123     {
124         static assert(SN == 2, "Invalid scale setup - dimension does not match with input slice.");
125         size_t [2]newsize = [cast(size_t)(slice.length!0 * scale[0]), cast(size_t)(slice.length!1 * scale[1])];
126         enforce(newsize[0] > 0 && newsize[1] > 0, "Scaling value invalid - after scaling array size is zero.");
127         return resizeImpl_2!interp(slice, newsize[0], newsize[1], pool);
128     }
129     else static if (packs[0] == 3)
130     {
131         static assert(SN == 2, "Invalid scale setup - 3D scale is performed as 2D."); // TODO: find better way to say this...
132         size_t [2]newsize = [cast(size_t)(slice.length!0 * scale[0]), cast(size_t)(slice.length!1 * scale[1])];
133         enforce(newsize[0] > 0 && newsize[1] > 0, "Scaling value invalid - after scaling array size is zero.");
134         return resizeImpl_3!interp(slice, newsize[0], newsize[1], pool);
135     }
136     else
137     {
138         import std.conv : to;
139         static assert(0, "Resize is not supported for slice with " ~ N.to!string ~ " dimensions.");
140     }
141 }
142 
143 unittest
144 {
145     auto vector = [0.0f, 0.1f, 0.2f].sliced(3);
146     auto matrix = [0.0f, 0.1f, 0.2f, 0.3f].sliced(2, 2);
147     auto image = [0.0f, 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f].sliced(2, 2, 2);
148 
149     auto resv = vector.scale!linear([2.0f]);
150     assert(resv.shape.length == 1);
151     assert(resv.length == vector.length * 2);
152 
153     auto resm = matrix.scale!linear([3.0f, 4.0f]);
154     assert(resm.shape.length == 2);
155     assert(resm.length!0 == matrix.length!0 * 3 && resm.length!1 == matrix.length!1 * 4);
156 
157     auto resi = image.scale!linear([5.0f, 8.0f]);
158     assert(resi.shape.length == 3);
159     assert(resi.length!0 == image.length!0 * 5 && resi.length!1 == image.length!1 * 8 && resi.length!2 == 2);
160 }
161 
162 /**
163 Pixel-wise warping of the image.
164 
165 Displace each pixel of an image by given [x, y] values.
166 
167 Params:
168     interp = (template parameter) Interpolation function, default linear.
169     image = Input image, which is warped. Single and multiple channel images are allowed.
170     map = Displacement map, which holds [x, y] displacement for each pixel of an image.
171     prealloc = Pre-allocated memory where resulting warped image is stored, if defined. 
172     Should be of same shape as input image, or an emptySlice, which implies newly allocated image is used.
173 
174 Returns:
175     Warped input image by given map.
176 */
177 pure auto warp(alias interp = linear, ImageTensor, MapTensor)(ImageTensor image, MapTensor map,
178         ImageTensor prealloc = ImageTensor.init)
179 {
180     return pixelWiseDisplacement!(linear, DisplacementType.WARP, ImageTensor, MapTensor)
181         (image, map, prealloc);
182 }
183 
184 /**
185 Pixel-wise remapping of the image.
186 
187 Move each pixel of an image to a given [x, y] location defined by the map.
188 Function is similar to dcv.imgproc.imgmanip.warp, except displacement of pixels
189 is absolute, rather than relative.
190 
191 Params:
192     interp = (template parameter) Interpolation function, default linear.
193     image = Input image, which is remapped. Single and multiple channel images are allowed.
194     map = Target map, which holds [x, y] position for each pixel of an image.
195     prealloc = Pre-allocated memory where resulting remapped image is stored, if defined. 
196     Should be of same shape as input image, or an emptySlice, which implies newly allocated image is used.
197 
198 Returns:
199     Remapped input image by given map.
200 */
201 pure auto remap(alias interp = linear, ImageTensor, MapTensor)(ImageTensor image, MapTensor map,
202         ImageTensor prealloc = ImageTensor.init)
203 {
204     return pixelWiseDisplacement!(linear, DisplacementType.REMAP, ImageTensor, MapTensor)
205         (image, map, prealloc);
206 }
207 
208 /// Test if warp and remap always returns slice of corresponding format.
209 unittest
210 {
211     import std.random : uniform01;
212     import mir.ndslice.allocation;
213 
214     auto image = iota(3, 3).as!float.slice;
215     auto wmap = iota(3, 3, 2).map!(v => cast(float)uniform01).slice;
216 
217     auto warped = image.warp(wmap);
218     assert(warped.shape == image.shape);
219 
220     auto remapped = image.remap(wmap);
221     assert(remapped.shape == image.shape);
222 }
223 
224 unittest
225 {
226     import std.random : uniform01;
227     import mir.ndslice.allocation;
228 
229     auto image = iota(3, 3, 3).as!float.slice;
230     auto wmap = iota(3, 3, 2).map!(v => cast(float)uniform01).slice;
231     auto warped = image.warp(wmap);
232     assert(warped.shape == image.shape);
233 
234     auto remapped = image.remap(wmap);
235     assert(remapped.shape == image.shape);
236 }
237 
238 unittest
239 {
240     import std.random : uniform01;
241     import mir.ndslice.allocation;
242 
243     auto image = iota(3, 3).as!float.slice;
244     auto warped = slice!float(3, 3);
245     auto remapped = slice!float(3, 3);
246     auto wmap = iota(3, 3, 2).map!(v => cast(float)uniform01).slice;
247     auto warpedRetVal = image.warp(wmap, warped);
248     assert(warped.shape == image.shape);
249     assert(warpedRetVal.shape == image.shape);
250     assert(&warped[0, 0] == &warpedRetVal[0, 0]);
251 
252     auto remappedRetVal = image.remap(wmap, remapped);
253     assert(remapped.shape == image.shape);
254     assert(remappedRetVal.shape == image.shape);
255     assert(&remapped[0, 0] == &remappedRetVal[0, 0]);
256 }
257 
258 private enum DisplacementType
259 {
260     WARP,
261     REMAP
262 }
263 
264 private pure auto pixelWiseDisplacement(alias interp, DisplacementType disp, ImageTensor, MapTensor)
265     (ImageTensor image, MapTensor map, ImageTensor prealloc)
266 in
267 {
268     assert(!image.empty, "Input image is empty");
269     assert(map.shape[0 .. 2] == image.shape[0 .. 2], "Invalid map size.");
270 }
271 body
272 {
273     static assert(isSlice!ImageTensor, "Image type has to be of type mir.ndslice.slice.Slice");
274     static assert(isSlice!MapTensor, "Map type has to be of type mir.ndslice.slice.Slice");
275     immutable N = isSlice!ImageTensor[0];
276     static assert(isSlice!MapTensor == [3],
277             "Invalid map tensor dimension - should be matrix of [x, y] displacements (3D).");
278 
279     if (prealloc.shape != image.shape)
280     {
281         import mir.ndslice.allocation;
282         prealloc = uninitializedSlice!(DeepElementType!ImageTensor)(image.shape);
283     }
284 
285     static if (N == 2)
286     {
287         displacementImpl!(interp, disp, ImageTensor, MapTensor)
288             (image, map, prealloc);
289     }
290     else static if (N == 3)
291     {
292         foreach (i; 0 .. image.length!2)
293         {
294             auto imagec = image[0 .. $, 0 .. $, i];
295             auto resultc = prealloc[0 .. $, 0 .. $, i];
296             displacementImpl!(interp, disp, typeof(imagec), MapTensor)
297                 (imagec, map, resultc);
298         }
299     }
300     else
301         static assert(0, "Pixel displacement operations are supported only for 2D and 3D slices.");
302 
303     return prealloc;
304 }
305 
306 private pure void displacementImpl(alias interp, DisplacementType disp, ImageMatrix, MapTensor)
307     (ImageMatrix image, MapTensor map, ImageMatrix result)
308 {
309     static if (disp == DisplacementType.WARP)
310         float r = 0.0f, c;
311     immutable rf = cast(float)image.length!0;
312     immutable cf = cast(float)image.length!1;
313     for (; !result.empty; result.popFront, map.popFront)
314     {
315         auto rrow = result.front;
316         auto mrow = map.front;
317         static if (disp == DisplacementType.WARP) c = 0.0f;
318         for (; !rrow.empty; rrow.popFront, mrow.popFront)
319         {
320             auto m = mrow.front;
321             static if (disp == DisplacementType.WARP)
322             {
323                 float rr = r + m[1];
324                 float cc = c + m[0];
325             }
326             else
327             {
328                 float rr = m[1];
329                 float cc = m[0];
330             }
331             if (rr >= 0.0f && rr < rf && cc >= 0.0f && cc < cf)
332             {
333                 rrow.front = interp(image, rr, cc);
334             }
335             static if (disp == DisplacementType.WARP) ++c;
336         }
337         static if (disp == DisplacementType.WARP) ++r;
338     }
339 }
340 
341 private enum TransformType : size_t
342 {
343     AFFINE_TRANSFORM = 0,
344     PERSPECTIVE_TRANSFORM = 1
345 }
346 
347 private static bool isTransformMatrix(TransformMatrix)()
348 {
349     // static if its float[][], or its Slice!(SliceKind.contiguous, [2], float*)
350     import std.traits : isScalarType, isPointer, TemplateArgsOf, PointerTarget;
351 
352     static if (isArray!TransformMatrix)
353     {
354         static if (isArray!(ElementType!TransformMatrix)
355                 && isScalarType!(ElementType!(ElementType!TransformMatrix))
356                 && isFloatingPoint!(ElementType!(ElementType!TransformMatrix)))
357             return true;
358         else
359             return false;
360     }
361     else static if (isSlice!TransformMatrix)
362     {
363         static if (kindOf!TransformMatrix == SliceKind.contiguous &&
364                 TemplateArgsOf!(TransformMatrix)[1] == [2] &&
365                 isFloatingPoint!(DeepElementType!TransformMatrix))
366             return true;
367         else
368             return false;
369     }
370     else
371     {
372         return false;
373     }
374 }
375 
376 unittest
377 {
378     static assert(isTransformMatrix!(float[][]));
379     static assert(isTransformMatrix!(double[][]));
380     static assert(isTransformMatrix!(real[][]));
381     static assert(isTransformMatrix!(real[3][3]));
382     static assert(isTransformMatrix!(Slice!(SliceKind.contiguous, [2], float*)));
383     static assert(isTransformMatrix!(Slice!(SliceKind.contiguous, [2], double*)));
384     static assert(isTransformMatrix!(Slice!(SliceKind.contiguous, [2], real*)));
385 
386     static assert(!isTransformMatrix!(Slice!(SliceKind.universal, [2], real*)));
387     static assert(!isTransformMatrix!(Slice!(SliceKind.canonical, [2], real*)));
388 
389     static assert(!isTransformMatrix!(int[][]));
390     static assert(!isTransformMatrix!(real[]));
391     static assert(!isTransformMatrix!(real[][][]));
392     static assert(!isTransformMatrix!(Slice!(SliceKind.contiguous, [2], int*)));
393     static assert(!isTransformMatrix!(Slice!(SliceKind.contiguous, [1], float*)));
394 }
395 
396 /**
397 Transform an image by given affine transformation.
398 
399 Params:
400     interp = (template parameter) Interpolation function. Default linear.
401     slice = Slice of an image which is transformed.
402     transform = 2D Transformation matrix (3x3). Its element type must be floating point type,
403     and it can be defined as Slice object, dynamic or static 2D array.
404     outSize = Output image size - if transformation potentially moves parts of image out
405     of input image bounds, output image can be sized differently to maintain information.
406 
407 Note:
408     Given transformation is considered to be an affine transformation. If it is not, result is undefined.
409 
410 Returns:
411     Transformed image.
412 */
413 Slice!(kind, packs, V*) transformAffine(alias interp = linear, V, TransformMatrix, SliceKind kind, size_t[] packs)(Slice!(kind, packs, V*) slice, inout TransformMatrix transform, size_t[2] outSize = [0, 0])
414 {
415     static if (isTransformMatrix!TransformMatrix)
416     {
417       return transformImpl!(TransformType.AFFINE_TRANSFORM, interp)(slice, transform, outSize);
418     }
419     else
420     {
421         static assert(0, "Invalid transform matrix type: " ~ typeof(transform).stringof);
422     }
423 }
424 
425 /**
426 Transform an image by given perspective transformation.
427 
428 Params:
429     interp = (template parameter) Interpolation function. Default linear.
430     slice = Slice of an image which is transformed.
431     transform = 2D Transformation matrix (3x3). Its element type must be floating point type,
432     and it can be defined as Slice object, dynamic or static 2D array.
433     outSize = Output image size [width, height] - if transformation potentially moves parts of image out
434     of input image bounds, output image can be sized differently to maintain information.
435 
436 Note:
437     Given transformation is considered to be an perspective transformation. If it is not, result is undefined.
438 
439 Returns:
440     Transformed image.
441 */
442 Slice!(kind, packs, V*) transformPerspective(alias interp = linear, V, TransformMatrix, SliceKind kind, size_t[] packs)(
443     Slice!(kind, packs, V*) slice,
444     TransformMatrix transform,
445     size_t[2] outSize = [0, 0])
446 {
447     static if (isTransformMatrix!TransformMatrix)
448     {
449       return transformImpl!(TransformType.PERSPECTIVE_TRANSFORM, linear)(slice, transform, outSize);
450     }
451     else
452     {
453         static assert(0, "Invalid transform matrix type: " ~ typeof(transform).stringof);
454     }
455 }
456 
457 version (unittest)
458 {
459     auto transformMatrix = [[1.0f, 0.0f, 5.0f], [0.0f, 1.0f, 5.0f], [0.0f, 0.0f, 1.0f]];
460 }
461 
462 unittest
463 {
464     // test if affine transform without outSize parameter gives out same-shaped result.
465     import mir.ndslice.allocation;
466     auto image = slice!float(3, 3);
467     auto transformed = transformAffine(image, transformMatrix);
468     assert(image.shape == transformed.shape);
469 }
470 
471 unittest
472 {
473     // test if affine transform with outSize parameter gives out proper-shaped result.
474     import mir.ndslice.allocation;
475     auto image = slice!float(3, 3);
476     auto transformed = transformAffine(image, transformMatrix, [5, 10]);
477     assert(transformed.length!0 == 10 && transformed.length!1 == 5);
478 }
479 
480 unittest
481 {
482     // test if perspective transform without outSize parameter gives out same-shaped result.
483     import mir.ndslice.allocation;
484     auto image = slice!float(3, 3);
485     auto transformed = transformPerspective(image, transformMatrix);
486     assert(image.shape == transformed.shape);
487 }
488 
489 unittest
490 {
491     // test if perspective transform with outSize parameter gives out proper-shaped result.
492     import mir.ndslice.allocation;
493     auto image = slice!float(3, 3);
494     auto transformed = transformPerspective(image, transformMatrix, [5, 10]);
495     assert(transformed.length!0 == 10 && transformed.length!1 == 5);
496 }
497 
498 private:
499 
500 // 1d resize implementation
501 Slice!(SliceKind.contiguous, [1], V*) resizeImpl_1(alias interp, V)(Slice!(SliceKind.contiguous, [1], V*) slice, size_t newsize, TaskPool pool)
502 {
503 
504     enforce(!slice.empty && newsize > 0);
505 
506     auto retval = new V[newsize];
507     auto resizeRatio = cast(float)(newsize - 1) / cast(float)(slice.length - 1);
508 
509     foreach (i; pool.parallel(newsize.iota))
510     {
511         retval[i] = interp(slice, cast(float)i / resizeRatio);
512     }
513 
514     return retval.sliced(newsize);
515 }
516 
517 // 1d resize implementation
518 Slice!(SliceKind.contiguous, [2], V*) resizeImpl_2(alias interp, SliceKind kind, V)(Slice!(kind, [2], V*) slice, size_t height, size_t width, TaskPool pool)
519 {
520 
521     enforce(!slice.empty && width > 0 && height > 0);
522 
523     auto retval = new V[width * height].sliced(height, width);
524 
525     auto rows = slice.length!0;
526     auto cols = slice.length!1;
527 
528     auto r_v = cast(float)(height - 1) / cast(float)(rows - 1); // horizontaresize ratio
529     auto r_h = cast(float)(width - 1) / cast(float)(cols - 1);
530 
531     foreach (i; pool.parallel(iota(height)))
532     {
533         auto row = retval[i, 0 .. width];
534         foreach (j; iota(width))
535         {
536             row[j] = interp(slice, cast(float)i / r_v, cast(float)j / r_h);
537         }
538     }
539 
540     return retval;
541 }
542 
543 // 1d resize implementation
544 Slice!(SliceKind.contiguous, [3], V*) resizeImpl_3(alias interp, SliceKind kind, V)(Slice!(kind, [3], V*) slice, size_t height, size_t width, TaskPool pool)
545 {
546 
547     enforce(!slice.empty && width > 0 && height > 0);
548 
549     auto rows = slice.length!0;
550     auto cols = slice.length!1;
551     auto channels = slice.length!2;
552 
553     auto retval = new V[width * height * channels].sliced(height, width, channels);
554 
555     auto r_v = cast(float)(height - 1) / cast(float)(rows - 1); // horizontaresize ratio
556     auto r_h = cast(float)(width - 1) / cast(float)(cols - 1);
557 
558     foreach (c; iota(channels))
559     {
560         auto sl_ch = slice[0 .. rows, 0 .. cols, c];
561         auto ret_ch = retval[0 .. height, 0 .. width, c];
562         foreach (i; pool.parallel(iota(height)))
563         {
564             auto row = ret_ch[i, 0 .. width];
565             foreach (j; iota(width))
566             {
567                 row[j] = interp(sl_ch, cast(float)i / r_v, cast(float)j / r_h);
568             }
569         }
570     }
571 
572     return retval;
573 }
574 
575 Slice!(SliceKind.contiguous, [2], float*) invertTransformMatrix(TransformMatrix)(TransformMatrix t)
576 {
577     import mir.ndslice.allocation;
578     auto result = slice!float(3, 3);
579 
580     double determinant = +t[0][0] * (t[1][1] * t[2][2] - t[2][1] * t[1][2]) - t[0][1] * (
581             t[1][0] * t[2][2] - t[1][2] * t[2][0]) + t[0][2] * (t[1][0] * t[2][1] - t[1][1] * t[2][0]);
582 
583     enforce(determinant != 0.0f, "Transform matrix determinant is zero.");
584 
585     double invdet = 1 / determinant;
586     result[0][0] = (t[1][1] * t[2][2] - t[2][1] * t[1][2]) * invdet;
587     result[0][1] = -(t[0][1] * t[2][2] - t[0][2] * t[2][1]) * invdet;
588     result[0][2] = (t[0][1] * t[1][2] - t[0][2] * t[1][1]) * invdet;
589     result[1][0] = -(t[1][0] * t[2][2] - t[1][2] * t[2][0]) * invdet;
590     result[1][1] = (t[0][0] * t[2][2] - t[0][2] * t[2][0]) * invdet;
591     result[1][2] = -(t[0][0] * t[1][2] - t[1][0] * t[0][2]) * invdet;
592     result[2][0] = (t[1][0] * t[2][1] - t[2][0] * t[1][1]) * invdet;
593     result[2][1] = -(t[0][0] * t[2][1] - t[2][0] * t[0][1]) * invdet;
594     result[2][2] = (t[0][0] * t[1][1] - t[1][0] * t[0][1]) * invdet;
595 
596     return result;
597 }
598 
599 Slice!(kind, packs, V*) transformImpl(TransformType transformType, alias interp, V, TransformMatrix, SliceKind kind, size_t[] packs)
600     (Slice!(kind, packs, V*) slice, TransformMatrix transform, size_t[2] outSize)
601 in
602 {
603     static assert(packs[0] == 2 || packs[0] == 3, "Unsupported slice dimension (only 2D and 3D supported)");
604 
605     uint rcount = 0;
606     foreach (r; transform)
607     {
608         assert(r.length == 3);
609         rcount++;
610     }
611     assert(rcount == 3);
612 }
613 body
614 {
615     // outsize is [width, height]
616     if (outSize[0] == 0)
617         outSize[0] = slice.length!1;
618     if (outSize[1] == 0)
619         outSize[1] = slice.length!0;
620 
621     static if (packs[0] == 2)
622     {
623         auto tSlice = new V[outSize[0] * outSize[1]].sliced(outSize[1], outSize[0]);
624     }
625     else
626     {
627         auto tSlice = new V[outSize[0] * outSize[1] * slice.length!2].sliced(outSize[1], outSize[0], slice.length!2);
628     }
629 
630     tSlice[] = cast(V)0;
631 
632     auto t = transform.invertTransformMatrix;
633 
634     static if (packs[0] == 3)
635     {
636         auto sliceChannels = new typeof(slice[0..$, 0..$, 0])[packs[0]];
637         foreach (c; iota(slice.length!2))
638         {
639             sliceChannels[c] = slice[0 .. $, 0 .. $, c];
640         }
641     }
642 
643     double outOffset_x = cast(double)outSize[0] / 2.;
644     double outOffset_y = cast(double)outSize[1] / 2.;
645     double inOffset_x = cast(double)slice.length!1 / 2.;
646     double inOffset_y = cast(double)slice.length!0 / 2.;
647 
648     foreach (i; iota(outSize[1]))
649     { // height, rows
650         foreach (j; iota(outSize[0]))
651         { // width, columns
652             double src_x, src_y;
653             double dst_x = cast(double)j - outOffset_x;
654             double dst_y = cast(double)i - outOffset_y;
655             static if (transformType == TransformType.AFFINE_TRANSFORM)
656             {
657                 src_x = t[0, 0] * dst_x + t[0, 1] * dst_y + t[0, 2];
658                 src_y = t[1, 0] * dst_x + t[1, 1] * dst_y + t[1, 2];
659             }
660             else static if (transformType == TransformType.PERSPECTIVE_TRANSFORM)
661             {
662                 double d = (t[2, 0] * dst_x + t[2, 1] * dst_y + t[2, 2]);
663                 src_x = (t[0, 0] * dst_x + t[0, 1] * dst_y + t[0, 2]) / d;
664                 src_y = (t[1, 0] * dst_x + t[1, 1] * dst_y + t[1, 2]) / d;
665             }
666             else
667             {
668                 static assert(0, "Invalid transform type"); // should never happen
669             }
670             src_x += inOffset_x;
671             src_y += inOffset_y;
672             if (src_x >= 0 && src_x < slice.length!1 && src_y >= 0 && src_y < slice.length!0)
673             {
674                 static if (packs[0] == 2)
675                 {
676                     tSlice[i, j] = interp(slice, src_y, src_x);
677                 }
678                 else if (packs[0] == 3)
679                 {
680                     foreach (c; iota(slice.length!2))
681                     {
682                         tSlice[i, j, c] = interp(sliceChannels[c], src_y, src_x);
683                     }
684                 }
685             }
686         }
687     }
688 
689     return tSlice;
690 }