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 }