forked from OneLoneCoder/Javidx9
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathOneLoneCoder_AR_OpticFlow.cpp
319 lines (270 loc) · 11.4 KB
/
OneLoneCoder_AR_OpticFlow.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
/*
OneLoneCoder.com - Augmenting Reality #1 - Optical Flow
"My arms are tired now." - @Javidx9
License
~~~~~~~
One Lone Coder Console Game Engine Copyright (C) 2018 Javidx9
This program comes with ABSOLUTELY NO WARRANTY.
This is free software, and you are welcome to redistribute it
under certain conditions; See license for details.
Original works located at:
https://www.github.com/onelonecoder
https://www.onelonecoder.com
https://www.youtube.com/javidx9
GNU GPLv3
https://github.com/OneLoneCoder/videos/blob/master/LICENSE
From Javidx9 :)
~~~~~~~~~~~~~~~
Hello! Ultimately I don't care what you use this for. It's intended to be
educational, and perhaps to the oddly minded - a little bit of fun.
Please hack this, change it and use it in any way you see fit. You acknowledge
that I am not responsible for anything bad that happens as a result of
your actions. However this code is protected by GNU GPLv3, see the license in the
github repo. This means you must attribute me if you use it. You can view this
license here: https://github.com/OneLoneCoder/videos/blob/master/LICENSE
Cheers!
Background
~~~~~~~~~~
Optical flow is the determination of motion in a video stream at the pixel level.
Each pixel is associated with a motion vector that is used to create a map of
velocity vectors which are then used to interact with a virtual object superimposed
on the video stream.
You will need to have watched my webcam video for this one to make sense!
https://youtu.be/pk1Y_26j1Y4
Author
~~~~~~
Twitter: @javidx9
Blog: www.onelonecoder.com
Video:
~~~~~~
https://youtu.be/aNtzgoEGC1Y
Last Updated: 15/11/2017
*/
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;
#include "olcConsoleGameEngine.h"
#include "escapi.h"
class OneLoneCoder_AROpticFlow : public olcConsoleGameEngine
{
public:
OneLoneCoder_AROpticFlow()
{
m_sAppName = L"Augmented Reality Part #1 - Optic Flow";
}
private:
union RGBint
{
int rgb;
unsigned char c[4];
};
int nCameras = 0;
SimpleCapParams capture;
// 2D Maps for image processing
float *fOldCamera = nullptr; // Previous raw frame from camera
float *fNewCamera = nullptr; // Recent raw frame from camera
float *fFilteredCamera = nullptr; // low-pass filtered image
float *fOldFilteredCamera = nullptr; // low-pass filtered image
float *fOldMotionImage = nullptr; // previous motion image
float *fMotionImage = nullptr; // recent motion image
float *fFlowX = nullptr; // x-component of flow field vector
float *fFlowY = nullptr; // y-component of flow field vector
// Object Physics Variables
float fBallX = 0.0f; // Ball position 2D
float fBallY = 0.0f;
float fBallVX = 0.0f; // Ball Velocity 2D
float fBallVY = 0.0f;
protected:
virtual bool OnUserCreate()
{
// Initialise webcam to console dimensions
nCameras = setupESCAPI();
if (nCameras == 0) return false;
capture.mWidth = ScreenWidth();
capture.mHeight = ScreenHeight();
capture.mTargetBuf = new int[ScreenWidth() * ScreenHeight()];
if (initCapture(0, &capture) == 0) return false;
// Allocate memory for images
fOldCamera = new float[ScreenWidth() * ScreenHeight()];
fNewCamera = new float[ScreenWidth() * ScreenHeight()];
fFilteredCamera = new float[ScreenWidth() * ScreenHeight()];
fOldFilteredCamera = new float[ScreenWidth() * ScreenHeight()];
fFlowX = new float[ScreenWidth() * ScreenHeight()];
fFlowY = new float[ScreenWidth() * ScreenHeight()];
fOldMotionImage = new float[ScreenWidth() * ScreenHeight()];
fMotionImage = new float[ScreenWidth() * ScreenHeight()];
// Initialise images to 0
memset(fOldCamera, 0, sizeof(float) * ScreenWidth() * ScreenHeight());
memset(fNewCamera, 0, sizeof(float) * ScreenWidth() * ScreenHeight());
memset(fFilteredCamera, 0, sizeof(float) * ScreenWidth() * ScreenHeight());
memset(fOldFilteredCamera, 0, sizeof(float) * ScreenWidth() * ScreenHeight());
memset(fFlowX, 0, sizeof(float) * ScreenWidth() * ScreenHeight());
memset(fFlowY, 0, sizeof(float) * ScreenWidth() * ScreenHeight());
memset(fOldMotionImage, 0, sizeof(float) * ScreenWidth() * ScreenHeight());
memset(fMotionImage, 0, sizeof(float) * ScreenWidth() * ScreenHeight());
// Set ball position to middle of frame
fBallX = ScreenWidth() / 2.0f;
fBallY = ScreenHeight() / 2.0f;
return true;
}
virtual bool OnUserUpdate(float fElapsedTime)
{
// Lambda function to draw "image" in greyscale
auto draw_image = [&](float *image)
{
for (int x = 0; x < capture.mWidth; x++)
{
for (int y = 0; y < capture.mHeight; y++)
{
wchar_t sym = 0;
short bg_col = 0;
short fg_col = 0;
int pixel_bw = (int)(image[y*ScreenWidth() + x] * 13.0f);
switch (pixel_bw)
{
case 0: bg_col = BG_BLACK; fg_col = FG_BLACK; sym = PIXEL_SOLID; break;
case 1: bg_col = BG_BLACK; fg_col = FG_DARK_GREY; sym = PIXEL_QUARTER; break;
case 2: bg_col = BG_BLACK; fg_col = FG_DARK_GREY; sym = PIXEL_HALF; break;
case 3: bg_col = BG_BLACK; fg_col = FG_DARK_GREY; sym = PIXEL_THREEQUARTERS; break;
case 4: bg_col = BG_BLACK; fg_col = FG_DARK_GREY; sym = PIXEL_SOLID; break;
case 5: bg_col = BG_DARK_GREY; fg_col = FG_GREY; sym = PIXEL_QUARTER; break;
case 6: bg_col = BG_DARK_GREY; fg_col = FG_GREY; sym = PIXEL_HALF; break;
case 7: bg_col = BG_DARK_GREY; fg_col = FG_GREY; sym = PIXEL_THREEQUARTERS; break;
case 8: bg_col = BG_DARK_GREY; fg_col = FG_GREY; sym = PIXEL_SOLID; break;
case 9: bg_col = BG_GREY; fg_col = FG_WHITE; sym = PIXEL_QUARTER; break;
case 10: bg_col = BG_GREY; fg_col = FG_WHITE; sym = PIXEL_HALF; break;
case 11: bg_col = BG_GREY; fg_col = FG_WHITE; sym = PIXEL_THREEQUARTERS; break;
case 12: bg_col = BG_GREY; fg_col = FG_WHITE; sym = PIXEL_SOLID; break;
}
Draw(x, y, sym, bg_col | fg_col);
}
}
};
// Lambda function to read from a 2D array without error
auto get_pixel = [&](float* image, int x, int y)
{
if (x >= 0 && x < ScreenWidth() && y >= 0 && y < ScreenHeight())
return image[y*ScreenWidth() + x];
else
return 0.0f;
};
// === Capture & Filter New Input Image ==========================================
// Get Image from webcam
doCapture(0); while (isCaptureDone(0) == 0) {}
// Do Temporal Filtering per pixel
for (int y = 0; y < capture.mHeight; y++)
for (int x = 0; x < capture.mWidth; x++)
{
RGBint col;
int id = y * capture.mWidth + x;
col.rgb = capture.mTargetBuf[id];
int r = col.c[2], g = col.c[1], b = col.c[0];
float fR = (float)r / 255.0f;
float fG = (float)g / 255.0f;
float fB = (float)b / 255.0f;
// Store previous camera frame for temporal processing
fOldCamera[y*ScreenWidth() + x] = fNewCamera[y*ScreenWidth() + x];
// Store previous camera frame for temporal processing
fOldFilteredCamera[y*ScreenWidth() + x] = fFilteredCamera[y*ScreenWidth() + x];
// Store previous motion only frame
fOldMotionImage[y*ScreenWidth() + x] = fMotionImage[y*ScreenWidth() + x];
// Calculate luminance (greyscale equivalent) of pixel
float fLuminance = 0.2987f * fR + 0.5870f * fG + 0.1140f * fB;
fNewCamera[y*ScreenWidth() + x] = fLuminance;
// Low-Pass filter camera image, to remove pixel jitter
fFilteredCamera[y*ScreenWidth() + x] += (fNewCamera[y*ScreenWidth() + x] - fFilteredCamera[y*ScreenWidth() + x]) * 0.8f;
// Create motion image as difference between two successive camera frames
float fDiff = fabs(get_pixel(fFilteredCamera, x, y) - get_pixel(fOldFilteredCamera, x, y));
// Threshold motion image to remove filter out camera noise
fMotionImage[y*ScreenWidth() + x] = (fDiff >= 0.05f) ? fDiff : 0.0f;
}
// === Calculate Optic Flow Vector Map ==========================================
// Brute Force Local Spatial Pattern Matching
int nPatchSize = 9;
int nSearchSize = 7;
for (int x = 0; x < ScreenWidth(); x++)
{
for (int y = 0; y < ScreenHeight(); y++)
{
// Initialise serach variables
float fPatchDifferenceMax = INFINITY;
float fPatchDifferenceX = 0.0f;
float fPatchDifferenceY = 0.0f;
fFlowX[y*ScreenWidth() + x] = 0.0f;
fFlowY[y*ScreenWidth() + x] = 0.0f;
// Search over a given rectangular area for a "patch" of old image
// that "resembles" a patch of the new image.
for (int sx = 0; sx < nSearchSize; sx++)
{
for (int sy = 0; sy < nSearchSize; sy++)
{
// Search vector is centre of patch test
int nSearchVectorX = x + (sx - nSearchSize / 2);
int nSearchVectorY = y + (sy - nSearchSize / 2);
float fAccumulatedDifference = 0.0f;
// For each pixel in search patch, accumulate difference with base patch
for (int px = 0; px < nPatchSize; px++)
for (int py = 0; py < nPatchSize; py++)
{
// Work out search patch offset indices
int nPatchPixelX = nSearchVectorX + (px - nPatchSize / 2);
int nPatchPixelY = nSearchVectorY + (py - nPatchSize / 2);
// Work out base patch indices
int nBasePixelX = x + (px - nPatchSize / 2);
int nBasePixelY = y + (py - nPatchSize / 2);
// Get adjacent values for each patch
float fPatchPixel = get_pixel(fNewCamera, nPatchPixelX, nPatchPixelY);
float fBasePixel = get_pixel(fOldCamera, nBasePixelX, nBasePixelY);
// Accumulate difference
fAccumulatedDifference += fabs(fPatchPixel - fBasePixel);
}
// Record the vector offset for the search patch that is the
// least different to the base patch
if (fAccumulatedDifference <= fPatchDifferenceMax)
{
fPatchDifferenceMax = fAccumulatedDifference;
fFlowX[y*ScreenWidth() + x] = (float)(nSearchVectorX - x);
fFlowY[y*ScreenWidth() + x] = (float)(nSearchVectorY - y);
}
}
}
}
}
// Modulate Optic Flow Vector Map with motion map, to remove vectors that
// errornously indicate large local motion
for (int i = 0; i < ScreenWidth()*ScreenHeight(); i++)
{
fFlowX[i] *= fMotionImage[i] > 0 ? 1.0f : 0.0f;
fFlowY[i] *= fMotionImage[i] > 0 ? 1.0f : 0.0f;
}
// === Update Ball Physics ========================================================
// Ball velocity is updated by optic flow vector field
fBallVX += 100.0f * fFlowX[(int)fBallY * ScreenWidth() + (int)fBallX] * fElapsedTime;
fBallVY += 100.0f * fFlowY[(int)fBallY * ScreenWidth() + (int)fBallX] * fElapsedTime;
// Ball position is updated by velocity
fBallX += 1.0f * fBallVX * fElapsedTime;
fBallY += 1.0f * fBallVY * fElapsedTime;
// Add "drag" effect to ball velocity
fBallVX *= 0.85f;
fBallVY *= 0.85f;
// Wrap ball around screen
if (fBallX >= ScreenWidth()) fBallX -= (float)ScreenWidth();
if (fBallY >= ScreenHeight()) fBallY -= (float)ScreenHeight();
if (fBallX < 0) fBallX += (float)ScreenWidth();
if (fBallY < 0) fBallY += (float)ScreenHeight();
// === Update Screen =================================================================
// Draw Camera Image
draw_image(fNewCamera);
// Draw "Ball"
Fill(fBallX - 4, fBallY - 4, fBallX + 4, fBallY + 4, PIXEL_SOLID, FG_RED);
return true;
}
};
int main()
{
OneLoneCoder_AROpticFlow game;
game.ConstructConsole(80, 60, 16, 16);
game.Start();
return 0;
}