1 /**
2 Module contains $(LINK3 https://en.wikipedia.org/wiki/Lucas%E2%80%93Kanade_method, Lucas-Kanade) optical flow algorithm implementation.
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 
11 module dcv.tracking.opticalflow.lucaskanade;
12 
13 import std.math : PI, floor;
14 
15 import dcv.core.image;
16 import dcv.imgproc.convolution;
17 import dcv.tracking.opticalflow.base;
18 public import dcv.imgproc.interpolate;
19 
20 /**
21 Lucas-Kanade optical flow method implementation.
22 */
23 class LucasKanadeFlow : SparseOpticalFlow
24 {
25 
26     public
27     {
28         float sigma = 0.84f;
29         float[] cornerResponse;
30         size_t iterationCount = 10;
31     }
32 
33     /**
34     Lucas-Kanade optical flow algorithm implementation.
35     
36     Params:
37         f1 = First frame image.
38         f2 = Second frame image.
39         points = points which are tracked.
40         searchRegions = search region width and height for each point.
41         flow = displacement values preallocated array.
42         usePrevious = if algorithm should continue iterating by 
43         using presented values in the flow array, set this to true.
44 
45     See:
46         dcv.features.corner
47     
48     */
49     override float[2][] evaluate(inout Image f1, inout Image f2, in float[2][] points,
50             in float[2][] searchRegions, float[2][] flow = null, bool usePrevious = false)
51     in
52     {
53         assert(!f1.empty && !f2.empty && f1.size == f2.size && f1.channels == 1 && f1.depth == f2.depth);
54         assert(points.length == searchRegions.length);
55         if (usePrevious)
56         {
57             assert(flow !is null);
58             assert(points.length == flow.length);
59         }
60     }
61     body
62     {
63         import std.array : uninitializedArray;
64         import std.parallelism : parallel;
65 
66         import mir.ndslice.allocation;
67         import mir.ndslice.topology;
68         import mir.ndslice.algorithm : each;
69 
70         import dcv.core.algorithm : ranged, ranged;
71         import dcv.imgproc.interpolate : linear;
72         import dcv.imgproc.filter;
73         import dcv.core.memory;
74 
75         const auto rows = f1.height;
76         const auto cols = f1.width;
77         const auto rl = cast(int)(rows - 1);
78         const auto cl = cast(int)(cols - 1);
79         const auto pointCount = points.length;
80         const auto pixelCount = rows * cols;
81 
82         if (!usePrevious)
83         {
84             if (flow.length != pointCount)
85                 flow = uninitializedArray!(float[2][])(pointCount);
86             flow[] = [0.0f, 0.0f];
87         }
88 
89         Slice!(SliceKind.contiguous, [2], float*) current, next;
90         switch (f1.depth)
91         {
92         case BitDepth.BD_32:
93             current = f1.sliced!float.flattened.sliced(f1.height, f1.width);
94             next = f2.sliced!float.flattened.sliced(f2.height, f2.width);
95             break;
96         case BitDepth.BD_16:
97             current = f1.sliced!ushort.flattened.sliced(f1.height, f1.width).as!float.slice;
98             next = f2.sliced!ushort.flattened.sliced(f2.height, f2.width).as!float.slice;
99             break;
100         default:
101             current = f1.sliced.flattened.sliced(f1.height, f1.width).as!float.slice;
102             next = f2.sliced.flattened.sliced(f2.height, f2.width).as!float.slice;
103         }
104 
105         float gaussMul = 1.0f / (2.0f * PI * sigma);
106         float gaussDel = 2.0f * (sigma ^^ 2);
107 
108         // Temporary buffers, used in algorithm -------------------------------
109         // TODO: cache these in class, and reuse
110         auto floatPool = [
111             alignedAlloc!float(pixelCount), alignedAlloc!float(pixelCount),
112             alignedAlloc!float(pixelCount), alignedAlloc!float(pixelCount)
113         ];
114         auto ubytePool = [alignedAlloc!ubyte(pixelCount), alignedAlloc!ubyte(pixelCount)];
115 
116         scope (exit)
117         {
118             import std.algorithm.iteration : each;
119             floatPool.each!(v => alignedFree(v));
120             ubytePool.each!(v => alignedFree(v));
121         }
122         // --------------------------------------------------------------------
123 
124         auto f1s = floatPool[0].sliced(rows, cols);
125         auto f2s = floatPool[1].sliced(rows, cols);
126         auto fxs = floatPool[2].sliced(rows, cols);
127         auto fys = floatPool[3].sliced(rows, cols);
128         auto fxmask = ubytePool[0].sliced(rows, cols);
129         auto fymask = ubytePool[1].sliced(rows, cols);
130 
131         if (f1.depth == BitDepth.BD_32)
132         {
133             f1s[] = current[];
134             f2s[] = next[];
135         }
136         else
137         {
138             auto f1d = f1.data.sliced(f1s.shape);
139             auto f2d = f2.data.sliced(f2s.shape);
140 
141             zip!true(f1s, f2s, f1d, f2d).each!((v) {
142                 v.a = cast(float)v.c;
143                 v.b = cast(float)v.d;
144             });
145         }
146 
147         fxs[] = 0.0f;
148         fys[] = 0.0f;
149         fxmask[] = ubyte(0);
150         fymask[] = ubyte(0);
151 
152         // Fill-in masks where points are present
153         import std.range : lockstep;
154         foreach (p, r; lockstep(points, searchRegions))
155         {
156             auto rb = cast(int)(p[0] - r[0] / 2.0f);
157             auto re = cast(int)(p[0] + r[0] / 2.0f);
158             auto cb = cast(int)(p[1] - r[1] / 2.0f);
159             auto ce = cast(int)(p[1] + r[1] / 2.0f);
160 
161             import mir.utility : min, max;
162             rb = max(1, rb);
163             re = min(re, rl);
164             cb = max(1, cb);
165             ce = min(ce, cl);
166 
167             if (re - rb <= 0 || ce - cb <= 0)
168                 continue;
169 
170             fxmask[rb .. re, cb .. ce][] = ubyte(1);
171             fymask[rb .. re, cb .. ce][] = ubyte(1);
172         }
173 
174         f1s.conv(sobel!float(GradientDirection.DIR_X), fxs, fxmask);
175         f1s.conv(sobel!float(GradientDirection.DIR_Y), fys, fymask);
176 
177         cornerResponse.length = pointCount;
178 
179         foreach (ptn; iota(pointCount).parallel)
180         {
181             import std.math : sqrt, exp;
182 
183             auto p = points[ptn];
184             auto r = searchRegions[ptn];
185 
186             auto rb = cast(int)(p[0] - r[0] / 2.0f);
187             auto re = cast(int)(p[0] + r[0] / 2.0f);
188             auto cb = cast(int)(p[1] - r[1] / 2.0f);
189             auto ce = cast(int)(p[1] + r[1] / 2.0f);
190 
191             rb = rb < 1 ? 1 : rb;
192             re = re > rl ? rl : re;
193             cb = cb < 1 ? 1 : cb;
194             ce = ce > cl ? cl : ce;
195 
196             if (re - rb <= 0 || ce - cb <= 0)
197             {
198                 continue;
199             }
200 
201             float a1, a2, a3;
202             float b1, b2;
203 
204             a1 = 0.0f;
205             a2 = 0.0f;
206             a3 = 0.0f;
207             b1 = 0.0f;
208             b2 = 0.0f;
209 
210             const auto rm = floor(cast(float)re - (r[0] / 2.0f));
211             const auto cm = floor(cast(float)ce - (r[1] / 2.0f));
212 
213             foreach (iteration; 0 .. iterationCount)
214             {
215                 foreach (i; rb .. re)
216                 {
217                     foreach (j; cb .. ce)
218                     {
219 
220                         const float nx = cast(float)j + flow[ptn][0];
221                         const float ny = cast(float)i + flow[ptn][1];
222 
223                         if (nx < 0.0f || nx > cast(float)ce || ny < 0.0f || ny > cast(float)re)
224                         {
225                             continue;
226                         }
227 
228                         // TODO: gaussian weighting produces errors - examine
229                         float w = 1.0f; //gaussMul * exp(-((rm - cast(float)i)^^2 + (cm - cast(float)j)^^2) / gaussDel);
230 
231                         // TODO: consider subpixel precision for gradient sampling.
232                         const float fx = fxs[i, j];
233                         const float fy = fys[i, j];
234                         const float ft = cast(float)(linear(next, ny, nx) - current[i, j]);
235 
236                         const float fxx = fx * fx;
237                         const float fyy = fy * fy;
238                         const float fxy = fx * fy;
239 
240                         a1 += w * fxx;
241                         a2 += w * fxy;
242                         a3 += w * fyy;
243 
244                         b1 += w * fx * ft;
245                         b2 += w * fy * ft;
246                     }
247                 }
248 
249                 // TODO: consider resp normalization...
250                 cornerResponse[ptn] = ((a1 + a3) - sqrt((a1 - a3) * (a1 - a3) + a2 * a2));
251 
252                 auto d = (a1 * a3 - a2 * a2);
253 
254                 if (d)
255                 {
256                     d = 1.0f / d;
257                     flow[ptn][0] += (a2 * b2 - a3 * b1) * d;
258                     flow[ptn][1] += (a2 * b1 - a1 * b2) * d;
259                 }
260             }
261         }
262 
263         return flow;
264     }
265 
266 }
267 
268 // TODO: implement functional tests.
269 version (unittest)
270 {
271     import std.algorithm.iteration : map;
272     import std.range : iota;
273     import std.array : array;
274     import std.random : uniform;
275 
276     private auto createImage()
277     {
278         return new Image(5, 5, ImageFormat.IF_MONO, BitDepth.BD_8,
279                 25.iota.map!(v => cast(ubyte)uniform(0, 255)).array);
280     }
281 }
282 
283 unittest
284 {
285     LucasKanadeFlow flow = new LucasKanadeFlow;
286     auto f1 = createImage();
287     auto f2 = createImage();
288     auto p = 10.iota.map!(v => cast(float[2])[cast(float)uniform(0, 2), cast(float)uniform(0, 2)]).array;
289     auto r = 10.iota.map!(v => cast(float[2])[3.0f, 3.0f]).array;
290     auto f = flow.evaluate(f1, f2, p, r);
291     assert(f.length == p.length);
292     assert(flow.cornerResponse.length == p.length);
293 }
294 
295 unittest
296 {
297     LucasKanadeFlow flow = new LucasKanadeFlow;
298     auto f1 = createImage();
299     auto f2 = createImage();
300     auto p = 10.iota.map!(v => cast(float[2])[cast(float)uniform(0, 2), cast(float)uniform(0, 2)]).array;
301     auto f = 10.iota.map!(v => cast(float[2])[cast(float)uniform(0, 2), cast(float)uniform(0, 2)]).array;
302     auto r = 10.iota.map!(v => cast(float[2])[3.0f, 3.0f]).array;
303     auto fe = flow.evaluate(f1, f2, p, r, f);
304     assert(f.length == fe.length);
305     assert(f.ptr == fe.ptr);
306     assert(flow.cornerResponse.length == p.length);
307 }