parent
5ed77fbf79
commit
6f57a46f6a
@ -0,0 +1,304 @@ |
||||
/*
|
||||
OneLoneCoder.com - Augmenting Reality #1 - Optical Flow |
||||
"My arms are tired now." - @Javidx9 |
||||
|
||||
Disclaimer |
||||
~~~~~~~~~~ |
||||
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. BUT, you acknowledge that I am not responsible for anything |
||||
bad that happens as a result of your actions. However, if good stuff happens, I |
||||
would appreciate a shout out, or at least give the blog some publicity for me. |
||||
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; |
||||
} |
Loading…
Reference in new issue