1 /**
2 Module contains optical flow pyramid implementation.
3 
4 Pyramidal optical flow evaluation in $(BIG DCV) is designed to be a wrapper to actual optical flow algorithm. $(LINK2 #SparesePyramidFlow, Sparse) and 
5 $(LINK2 #DensePyramidFlow, dense) optical flow algorithms have a corresponding utility class which will evaluate the algorithm 
6 in $(LINK3 https://en.wikipedia.org/wiki/Pyramid_(image_processing)#Gaussian_pyramid, pyramid), coarse-to-fine fashion.
7 
8 ----
9 // Evaluate Horn-Schunck method in pyramid.
10 HornSchunckFlow hsFlow = new HornSchunckFlow(props);
11 DensePyramidFlow densePyramid = new DensePyramidFlow(hsFlow, pyramidLevels); 
12 
13 auto flow = densePyramid.evaluate(current, next);
14 ----
15 
16 Copyright: Copyright Relja Ljubobratovic 2016.
17 
18 Authors: Relja Ljubobratovic
19 
20 License: $(LINK3 http://www.boost.org/LICENSE_1_0.txt, Boost Software License - Version 1.0).
21 */
22 module dcv.tracking.opticalflow.pyramidflow;
23 
24 import mir.ndslice.algorithm: each;
25 
26 import dcv.core.utils : emptySlice;
27 import dcv.core.image;
28 import dcv.imgproc.imgmanip : warp, resize;
29 import dcv.tracking.opticalflow.base;
30 
31 import mir.ndslice.allocation: slice;
32 import mir.ndslice.topology: as, flattened, iota;
33 
34 /**
35 Sparse pyramidal optical flow utility class.
36 */
37 class SparsePyramidFlow : SparseOpticalFlow
38 {
39 
40     private SparseOpticalFlow flowAlgorithm;
41     private uint levelCount;
42 
43     this(SparseOpticalFlow flow, uint levels)
44     in
45     {
46         assert(flow !is null);
47         assert(levels > 0);
48     }
49     body
50     {
51         flowAlgorithm = flow;
52         levelCount = levels;
53     }
54 
55     override float[2][] evaluate(inout Image f1, inout Image f2, in float[2][] points,
56             in float[2][] searchRegions, float[2][] flow = null, bool usePrevious = false)
57     in
58     {
59         assert(!f1.empty && !f2.empty && f1.size == f2.size && f1.channels == 1 && f1.depth == f2.depth);
60         assert(points.length == searchRegions.length);
61         if (usePrevious)
62         {
63             assert(flow !is null);
64             assert(points.length == flow.length);
65         }
66     }
67     body
68     {
69         import std.array : uninitializedArray;
70 
71         size_t[2] size = [f1.height, f1.width];
72         const auto pointCount = points.length;
73 
74         // pyramid flow array - each item is double sized flow from the next
75         size_t[2][] flowPyramid;
76         flowPyramid.length = levelCount;
77         flowPyramid[$ - 1] = size.dup;
78 
79         foreach_reverse (i; 0 .. (levelCount - 1))
80         {
81             size[] /= 2;
82             if (size[0] < 1 || size[1] < 1)
83                 throw new Exception("Pyramid downsampling exceeded minimal image size");
84             flowPyramid[i] = size.dup;
85         }
86 
87         auto flowScale = [cast(float)f1.height / cast(float)flowPyramid[0][0],
88             cast(float)f1.width / cast(float)flowPyramid[0][1]];
89 
90         auto lpoints = points.dup;
91         auto lsearchRegions = searchRegions.dup;
92 
93         alias scale = (ref v) { v[0] /= flowScale[0]; v[1] /= flowScale[1]; };
94         lpoints.sliced.each!scale;
95         lsearchRegions.sliced.each!scale;
96 
97         if (usePrevious)
98         {
99             flow.sliced.each!scale;
100         }
101         else
102         {
103             flow = uninitializedArray!(float[2][])(pointCount);
104             flow[] = [0.0f, 0.0f];
105         }
106 
107         auto h = f1.height;
108         auto w = f1.width;
109 
110         Slice!(SliceKind.contiguous, [2], float*) current, next, f1s, f2s;
111         switch (f1.depth) 
112         {
113             case BitDepth.BD_32:
114                 f1s = f1.sliced!float.flattened.sliced(f1.height, f1.width);
115                 f2s = f2.sliced!float.flattened.sliced(f2.height, f2.width);
116                 break;
117             case BitDepth.BD_16:
118                 f1s = f1.sliced!ushort.flattened.sliced(f1.height, f1.width).as!float.slice;
119                 f2s = f2.sliced!ushort.flattened.sliced(f2.height, f2.width).as!float.slice;
120                 break;
121             default:
122                 f1s = f1.sliced!ubyte.flattened.sliced(f1.height, f1.width).as!float.slice;
123                 f2s = f2.sliced!ubyte.flattened.sliced(f2.height, f2.width).as!float.slice;
124         }
125 
126         // calculate pyramid flow
127         foreach (i; 0 .. levelCount)
128         {
129 
130             auto lh = flowPyramid[i][0];
131             auto lw = flowPyramid[i][1];
132 
133             if (lh != h || lw != w)
134             {
135                 current = f1s.resize([lh, lw]);
136                 next = f2s.resize([lh, lw]);
137             }
138             else
139             {
140                 current = f1s;
141                 next = f2s;
142             }
143 
144             flowAlgorithm.evaluate(current.asImage(f1.format), next.asImage(f2.format), lpoints,
145                     lsearchRegions, flow, true);
146 
147             if (i < levelCount - 1)
148             {
149                 alias twice = (ref v) { v[0] += v[0]; v[1] += v[1]; };
150                 flow.sliced.each!twice;
151                 lpoints.sliced.each!twice;
152                 lsearchRegions.sliced.each!twice;
153             }
154         }
155 
156         return flow;
157     }
158 }
159 
160 /**
161 Dense pyramidal optical flow utility class.
162 */
163 class DensePyramidFlow : DenseOpticalFlow
164 {
165 
166     private DenseOpticalFlow flowAlgorithm;
167     private uint levelCount;
168 
169     this(DenseOpticalFlow flow, uint levels)
170     in
171     {
172         assert(flow !is null);
173         assert(levels > 0);
174     }
175     body
176     {
177         flowAlgorithm = flow;
178         levelCount = levels;
179     }
180 
181     override DenseFlow evaluate(inout Image f1, inout Image f2, DenseFlow prealloc = emptySlice!([3],
182             float), bool usePrevious = false)
183     in
184     {
185         assert(prealloc.length!2 == 2);
186         assert(!f1.empty && f1.size == f2.size && f1.depth == f2.depth && f1.depth == BitDepth.BD_8);
187         if (usePrevious)
188         {
189             assert(prealloc.length!0 == f1.height && prealloc.length!1 == f1.width);
190         }
191     }
192     body
193     {
194         size_t[2] size = [f1.height, f1.width];
195         uint level = 0;
196 
197         // pyramid flow array - each item is double sized flow from the next
198         size_t[2][] flowPyramid;
199         flowPyramid.length = levelCount;
200         flowPyramid[$ - 1] = size.dup;
201 
202         DenseFlow flow;
203 
204         foreach_reverse (i; 0 .. (levelCount - 1))
205         {
206             size[] /= 2;
207             if (size[0] < 1 || size[1] < 1)
208                 throw new Exception("Pyramid downsampling exceeded minimal image size");
209             flowPyramid[i] = size.dup;
210         }
211 
212         // allocate flow for each pyramid level
213         if (usePrevious)
214         {
215             flow = prealloc.resize(flowPyramid[0]);
216         }
217         else
218         {
219             flow = new float[flowPyramid[0][0] * flowPyramid[0][1] * 2].sliced(flowPyramid[0][0], flowPyramid[0][1], 2);
220             flow[] = 0.0f;
221         }
222 
223         auto h = f1.height;
224         auto w = f1.width;
225 
226         Slice!(SliceKind.contiguous, [2], float*) current, next, corig, norig;
227         switch (f1.depth) 
228         {
229             case BitDepth.BD_32:
230                 corig = f1.sliced!float.flattened.sliced(f1.height, f1.width);
231                 norig = f2.sliced!float.flattened.sliced(f2.height, f2.width);
232                 break;
233             case BitDepth.BD_16:
234                 corig = f1.sliced!ushort.flattened.sliced(f1.height, f1.width).as!float.slice;
235                 norig = f2.sliced!ushort.flattened.sliced(f2.height, f2.width).as!float.slice;
236                 break;
237             default:
238                 corig = f1.sliced.flattened.sliced(f1.height, f1.width).as!float.slice;
239                 norig = f2.sliced.flattened.sliced(f2.height, f2.width).as!float.slice;
240         }
241 
242         // first flow used as indicator to skip the first warp.
243         bool firstFlow = usePrevious;
244 
245         // calculate pyramid flow
246         foreach (i; 0 .. levelCount)
247         {
248             auto lh = flow.length!0;
249             auto lw = flow.length!1;
250 
251             if (lh != h || lw != w)
252             {
253                 current = corig.resize([lh, lw]);
254                 next = norig.resize([lh, lw]);
255             }
256             else
257             {
258                 current = corig;
259                 next = norig;
260             }
261 
262             if (!firstFlow)
263             {
264                 // warp the image using previous flow, 
265                 // except if this is the first level
266                 // or usePrevious is false.
267                 current = warp(current, flow);
268             }
269 
270             // evaluate the flow algorithm
271             auto lflow = flowAlgorithm.evaluate(current.asImage(f1.format), next.asImage(f2.format));
272 
273             // add flow calculated in this iteration to previous one.
274             flow[] += lflow;
275 
276             if (i < levelCount - 1)
277             {
278                 flow = flow.resize(flowPyramid[i + 1]);
279                 flow[] *= 2.0f;
280             }
281             // assign the first flow indicator to false.
282             firstFlow = false;
283         }
284 
285         return flow;
286     }
287 
288 }
289 
290 // TODO: implement functional tests.
291 version (unittest)
292 {
293 
294     import std.algorithm.iteration : map;
295     import std.array : array;
296     import std.random : uniform;
297 
298     private auto createImage()
299     {
300         return new Image(32, 32, ImageFormat.IF_MONO, BitDepth.BD_8, (32 * 32)
301                 .iota.map!(v => cast(ubyte)uniform(0, 255)).array);
302     }
303 
304     class DummySparseFlow : SparseOpticalFlow
305     {
306         override float[2][] evaluate(inout Image f1, inout Image f2, in float[2][] points,
307                 in float[2][] searchRegions, float[2][] prevflow = null, bool usePrevious = false)
308         {
309             import std.array : uninitializedArray;
310 
311             return uninitializedArray!(float[2][])(points.length);
312         }
313     }
314 
315     class DummyDenseFlow : DenseOpticalFlow
316     {
317         override DenseFlow evaluate(inout Image f1, inout Image f2, DenseFlow prealloc = emptySlice!([3],
318                 float), bool usePrevious = false)
319         {
320             return new float[f1.height * f1.width * 2].sliced(f1.height, f1.width, 2);
321         }
322     }
323 }
324 
325 unittest
326 {
327     SparsePyramidFlow flow = new SparsePyramidFlow(new DummySparseFlow, 3);
328     auto f1 = createImage();
329     auto f2 = createImage();
330     auto p = 10.iota.map!(v => cast(float[2])[cast(float)uniform(0, 2), cast(float)uniform(0, 2)]).array;
331     auto r = 10.iota.map!(v => cast(float[2])[3.0f, 3.0f]).array;
332     auto f = flow.evaluate(f1, f2, p, r);
333     assert(f.length == p.length);
334 }
335 
336 unittest
337 {
338     DensePyramidFlow flow = new DensePyramidFlow(new DummyDenseFlow, 3);
339     auto f1 = createImage();
340     auto f2 = createImage();
341     auto f = flow.evaluate(f1, f2);
342     assert(f.length!0 == f1.height && f.length!1 == f1.width && f.length!2 == 2);
343 }