+ Foundations for more system implementations + More compiler friendly on Linux + Tidied up public repo
505 lines
13 KiB
C++
505 lines
13 KiB
C++
/*
|
||
8-Bits Of Image Processing You Should Know
|
||
"Colin, at least you'll always get 700s now..." - javidx9
|
||
|
||
License (OLC-3)
|
||
~~~~~~~~~~~~~~~
|
||
|
||
Copyright 2018-2019 OneLoneCoder.com
|
||
|
||
Redistribution and use in source and binary forms, with or without
|
||
modification, are permitted provided that the following conditions
|
||
are met:
|
||
|
||
1. Redistributions or derivations of source code must retain the above
|
||
copyright notice, this list of conditions and the following disclaimer.
|
||
|
||
2. Redistributions or derivative works in binary form must reproduce
|
||
the above copyright notice. This list of conditions and the following
|
||
disclaimer must be reproduced in the documentation and/or other
|
||
materials provided with the distribution.
|
||
|
||
3. Neither the name of the copyright holder nor the names of its
|
||
contributors may be used to endorse or promote products derived
|
||
from this software without specific prior written permission.
|
||
|
||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||
|
||
Instructions:
|
||
~~~~~~~~~~~~~
|
||
Choose algorithm 1-8, instructions on screen
|
||
|
||
|
||
Relevant Video: https://youtu.be/mRM5Js3VLCk
|
||
|
||
Links
|
||
~~~~~
|
||
YouTube: https://www.youtube.com/javidx9
|
||
https://www.youtube.com/javidx9extra
|
||
Discord: https://discord.gg/WhwHUMV
|
||
Twitter: https://www.twitter.com/javidx9
|
||
Twitch: https://www.twitch.tv/javidx9
|
||
GitHub: https://www.github.com/onelonecoder
|
||
Patreon: https://www.patreon.com/javidx9
|
||
Homepage: https://www.onelonecoder.com
|
||
|
||
Author
|
||
~~~~~~
|
||
David Barr, aka javidx9, <20>OneLoneCoder 2019
|
||
*/
|
||
|
||
|
||
#define OLC_PGE_APPLICATION
|
||
#include "olcPixelGameEngine.h"
|
||
|
||
#include "escapi.h"
|
||
|
||
int nFrameWidth = 320;
|
||
int nFrameHeight = 240;
|
||
|
||
struct frame
|
||
{
|
||
float *pixels = nullptr;
|
||
|
||
frame()
|
||
{
|
||
pixels = new float[nFrameWidth * nFrameHeight];
|
||
}
|
||
|
||
~frame()
|
||
{
|
||
delete[] pixels;
|
||
}
|
||
|
||
|
||
float get(int x, int y)
|
||
{
|
||
if (x >= 0 && x < nFrameWidth && y >= 0 && y < nFrameHeight)
|
||
{
|
||
return pixels[y*nFrameWidth + x];
|
||
}
|
||
else
|
||
return 0.0f;
|
||
}
|
||
|
||
void set(int x, int y, float p)
|
||
{
|
||
if (x >= 0 && x < nFrameWidth && y >= 0 && y < nFrameHeight)
|
||
{
|
||
pixels[y*nFrameWidth + x] = p;
|
||
}
|
||
}
|
||
|
||
|
||
void operator=(const frame &f)
|
||
{
|
||
memcpy(this->pixels, f.pixels, nFrameWidth * nFrameHeight * sizeof(float));
|
||
}
|
||
};
|
||
|
||
|
||
|
||
class WIP_ImageProcessing : public olc::PixelGameEngine
|
||
{
|
||
public:
|
||
WIP_ImageProcessing()
|
||
{
|
||
sAppName = "WIP_ImageProcessing";
|
||
}
|
||
|
||
union RGBint
|
||
{
|
||
int rgb;
|
||
unsigned char c[4];
|
||
};
|
||
|
||
int nCameras = 0;
|
||
SimpleCapParams capture;
|
||
|
||
public:
|
||
bool OnUserCreate() override
|
||
{
|
||
// Initialise webcam to screen dimensions
|
||
nCameras = setupESCAPI();
|
||
if (nCameras == 0) return false;
|
||
capture.mWidth = nFrameWidth;
|
||
capture.mHeight = nFrameHeight;
|
||
capture.mTargetBuf = new int[nFrameWidth * nFrameHeight];
|
||
if (initCapture(0, &capture) == 0) return false;
|
||
return true;
|
||
}
|
||
|
||
void DrawFrame(frame &f, int x, int y)
|
||
{
|
||
for (int i = 0; i < nFrameWidth; i++)
|
||
for (int j = 0; j < nFrameHeight; j++)
|
||
{
|
||
int c = (int)std::min(std::max(0.0f, f.pixels[j*nFrameWidth + i] * 255.0f), 255.0f);
|
||
Draw(x + i, y + j, olc::Pixel(c, c, c));
|
||
}
|
||
}
|
||
|
||
enum ALGORITHM
|
||
{
|
||
THRESHOLD, MOTION, LOWPASS, CONVOLUTION,
|
||
SOBEL, MORPHO, MEDIAN, ADAPTIVE,
|
||
};
|
||
|
||
enum MORPHOP
|
||
{
|
||
DILATION,
|
||
EROSION,
|
||
EDGE
|
||
};
|
||
|
||
frame input, output, prev_input, activity, threshold;
|
||
|
||
// Algorithm Currently Running
|
||
ALGORITHM algo = THRESHOLD;
|
||
MORPHOP morph = DILATION;
|
||
int nMorphCount = 1;
|
||
|
||
float fThresholdValue = 0.5f;
|
||
float fLowPassRC = 0.1f;
|
||
float fAdaptiveBias = 1.1f;
|
||
|
||
float *pConvoKernel = kernel_blur;
|
||
|
||
float kernel_blur[9] =
|
||
{
|
||
0.0f, 0.125, 0.0f,
|
||
0.125f, 0.5f, 0.125f,
|
||
0.0f, 0.125f, 0.0f,
|
||
};
|
||
|
||
float kernel_sharpen[9] =
|
||
{
|
||
0.0f, -1.0f, 0.0f,
|
||
-1.0f, 5.0f, -1.0f,
|
||
0.0f, -1.0f, 0.0f,
|
||
};
|
||
|
||
float kernel_sobel_v[9] =
|
||
{
|
||
-1.0f, 0.0f, +1.0f,
|
||
-2.0f, 0.0f, +2.0f,
|
||
-1.0f, 0.0f, +1.0f,
|
||
};
|
||
|
||
float kernel_sobel_h[9] =
|
||
{
|
||
-1.0f, -2.0f, -1.0f,
|
||
0.0f, 0.0f, 0.0f,
|
||
+1.0f, +2.0f, +1.0f,
|
||
};
|
||
|
||
bool OnUserUpdate(float fElapsedTime) override
|
||
{
|
||
// CAPTURING WEBCAM IMAGE
|
||
prev_input = input;
|
||
doCapture(0); while (isCaptureDone(0) == 0) {}
|
||
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];
|
||
input.pixels[y*nFrameWidth + x] = (float)col.c[1] / 255.0f;
|
||
}
|
||
|
||
if (GetKey(olc::Key::K1).bReleased) algo = THRESHOLD;
|
||
if (GetKey(olc::Key::K2).bReleased) algo = MOTION;
|
||
if (GetKey(olc::Key::K3).bReleased) algo = LOWPASS;
|
||
if (GetKey(olc::Key::K4).bReleased) algo = CONVOLUTION;
|
||
if (GetKey(olc::Key::K5).bReleased) algo = SOBEL;
|
||
if (GetKey(olc::Key::K6).bReleased) algo = MORPHO;
|
||
if (GetKey(olc::Key::K7).bReleased) algo = MEDIAN;
|
||
if (GetKey(olc::Key::K8).bReleased) algo = ADAPTIVE;
|
||
|
||
|
||
switch (algo)
|
||
{
|
||
case THRESHOLD:
|
||
|
||
// Respond to user input
|
||
if (GetKey(olc::Key::Z).bHeld) fThresholdValue -= 0.1f * fElapsedTime;
|
||
if (GetKey(olc::Key::X).bHeld) fThresholdValue += 0.1f * fElapsedTime;
|
||
if (fThresholdValue > 1.0f) fThresholdValue = 1.0f;
|
||
if (fThresholdValue < 0.0f) fThresholdValue = 0.0f;
|
||
|
||
// Perform threshold per pixel
|
||
for (int i = 0; i < nFrameWidth; i++)
|
||
for (int j = 0; j < nFrameHeight; j++)
|
||
output.set(i, j, input.get(i, j) >= fThresholdValue ? 1.0f : 0.0f);
|
||
break;
|
||
|
||
case MOTION:
|
||
|
||
// Returns the absolute difference between successive frames per pixel
|
||
for (int i = 0; i < nFrameWidth; i++)
|
||
for (int j = 0; j < nFrameHeight; j++)
|
||
output.set(i, j, fabs(input.get(i, j) - prev_input.get(i, j)));
|
||
break;
|
||
|
||
|
||
case LOWPASS:
|
||
|
||
// Respond to user input
|
||
if (GetKey(olc::Key::Z).bHeld) fLowPassRC -= 0.1f * fElapsedTime;
|
||
if (GetKey(olc::Key::X).bHeld) fLowPassRC += 0.1f * fElapsedTime;
|
||
if (fLowPassRC > 1.0f) fLowPassRC = 1.0f;
|
||
if (fLowPassRC < 0.0f) fLowPassRC = 0.0f;
|
||
|
||
// Pass each pixel through a temporal RC filter
|
||
for (int i = 0; i < nFrameWidth; i++)
|
||
for (int j = 0; j < nFrameHeight; j++)
|
||
{
|
||
float dPixel = input.get(i, j) - output.get(i, j);
|
||
dPixel *= fLowPassRC;
|
||
output.set(i, j, dPixel + output.get(i, j));
|
||
}
|
||
break;
|
||
|
||
case CONVOLUTION:
|
||
// Respond to user input
|
||
if (GetKey(olc::Key::Z).bHeld) pConvoKernel = kernel_blur;
|
||
if (GetKey(olc::Key::X).bHeld) pConvoKernel = kernel_sharpen;
|
||
|
||
for (int i = 0; i < nFrameWidth; i++)
|
||
for (int j = 0; j < nFrameHeight; j++)
|
||
{
|
||
float fSum = 0.0f;
|
||
for (int n = -1; n < +2; n++)
|
||
for (int m = -1; m < +2; m++)
|
||
fSum += input.get(i + n, j + m) * pConvoKernel[(m + 1) * 3 + (n + 1)];
|
||
|
||
output.set(i, j, fSum);
|
||
}
|
||
break;
|
||
|
||
case SOBEL:
|
||
for (int i = 0; i < nFrameWidth; i++)
|
||
for (int j = 0; j < nFrameHeight; j++)
|
||
{
|
||
float fKernelSumH = 0.0f;
|
||
float fKernelSumV = 0.0f;
|
||
|
||
for (int n = -1; n < +2; n++)
|
||
for (int m = -1; m < +2; m++)
|
||
{
|
||
fKernelSumH += input.get(i + n, j + m) * kernel_sobel_h[(m + 1) * 3 + (n + 1)];
|
||
fKernelSumV += input.get(i + n, j + m) * kernel_sobel_v[(m + 1) * 3 + (n + 1)];
|
||
}
|
||
|
||
output.set(i, j, fabs((fKernelSumH + fKernelSumV) / 2.0f));
|
||
}
|
||
break;
|
||
|
||
case MORPHO:
|
||
|
||
// Respond to user input
|
||
if (GetKey(olc::Key::Z).bHeld) morph = DILATION;
|
||
if (GetKey(olc::Key::X).bHeld) morph = EROSION;
|
||
if (GetKey(olc::Key::C).bHeld) morph = EDGE;
|
||
|
||
if (GetKey(olc::Key::A).bReleased) nMorphCount--;
|
||
if (GetKey(olc::Key::S).bReleased) nMorphCount++;
|
||
if (nMorphCount > 10.0f) nMorphCount = 10.0f;
|
||
if (nMorphCount < 1.0f) nMorphCount = 1.0f;
|
||
|
||
// Threshold First to binarise image
|
||
for (int i = 0; i < nFrameWidth; i++)
|
||
for (int j = 0; j < nFrameHeight; j++)
|
||
{
|
||
activity.set(i, j, input.get(i, j) > fThresholdValue ? 1.0f : 0.0f);
|
||
}
|
||
|
||
threshold = activity;
|
||
|
||
switch (morph)
|
||
{
|
||
case DILATION:
|
||
for (int n = 0; n < nMorphCount; n++)
|
||
{
|
||
output = activity;
|
||
|
||
for (int i = 0; i < nFrameWidth; i++)
|
||
for (int j = 0; j < nFrameHeight; j++)
|
||
{
|
||
if (activity.get(i, j) == 1.0f)
|
||
{
|
||
output.set(i, j, 1.0f);
|
||
output.set(i - 1, j, 1.0f);
|
||
output.set(i + 1, j, 1.0f);
|
||
output.set(i, j - 1, 1.0f);
|
||
output.set(i, j + 1, 1.0f);
|
||
output.set(i - 1, j - 1, 1.0f);
|
||
output.set(i + 1, j + 1, 1.0f);
|
||
output.set(i + 1, j - 1, 1.0f);
|
||
output.set(i - 1, j + 1, 1.0f);
|
||
}
|
||
}
|
||
|
||
activity = output;
|
||
}
|
||
break;
|
||
|
||
case EROSION:
|
||
for (int n = 0; n < nMorphCount; n++)
|
||
{
|
||
output = activity;
|
||
for (int i = 0; i < nFrameWidth; i++)
|
||
for (int j = 0; j < nFrameHeight; j++)
|
||
{
|
||
|
||
float sum = activity.get(i - 1, j) + activity.get(i + 1, j) + activity.get(i, j - 1) + activity.get(i, j + 1) +
|
||
activity.get(i - 1, j - 1) + activity.get(i + 1, j + 1) + activity.get(i + 1, j - 1) + activity.get(i - 1, j + 1);
|
||
|
||
if (activity.get(i, j) == 1.0f && sum < 8.0f)
|
||
{
|
||
output.set(i, j, 0.0f);
|
||
}
|
||
}
|
||
activity = output;
|
||
}
|
||
break;
|
||
|
||
case EDGE:
|
||
output = activity;
|
||
for (int i = 0; i < nFrameWidth; i++)
|
||
for (int j = 0; j < nFrameHeight; j++)
|
||
{
|
||
|
||
float sum = activity.get(i - 1, j) + activity.get(i + 1, j) + activity.get(i, j - 1) + activity.get(i, j + 1) +
|
||
activity.get(i - 1, j - 1) + activity.get(i + 1, j + 1) + activity.get(i + 1, j - 1) + activity.get(i - 1, j + 1);
|
||
|
||
if (activity.get(i, j) == 1.0f && sum == 8.0f)
|
||
{
|
||
output.set(i, j, 0.0f);
|
||
}
|
||
}
|
||
break;
|
||
|
||
}
|
||
break;
|
||
|
||
case MEDIAN:
|
||
for (int i = 0; i < nFrameWidth; i++)
|
||
for (int j = 0; j < nFrameHeight; j++)
|
||
{
|
||
std::vector<float> v;
|
||
|
||
for (int n = -2; n < +3; n++)
|
||
for (int m = -2; m < +3; m++)
|
||
v.push_back(input.get(i + n, j + m));
|
||
|
||
std::sort(v.begin(), v.end(), std::greater<float>());
|
||
output.set(i, j, v[12]);
|
||
}
|
||
break;
|
||
|
||
case ADAPTIVE:
|
||
// Respond to user input
|
||
if (GetKey(olc::Key::Z).bHeld) fAdaptiveBias -= 0.1f * fElapsedTime;
|
||
if (GetKey(olc::Key::X).bHeld) fAdaptiveBias += 0.1f * fElapsedTime;
|
||
if (fAdaptiveBias > 1.5f) fAdaptiveBias = 1.5f;
|
||
if (fAdaptiveBias < 0.5f) fAdaptiveBias = 0.5f;
|
||
|
||
|
||
for (int i = 0; i < nFrameWidth; i++)
|
||
for (int j = 0; j < nFrameHeight; j++)
|
||
{
|
||
float fRegionSum = 0.0f;
|
||
|
||
for (int n = -2; n < +3; n++)
|
||
for (int m = -2; m < +3; m++)
|
||
fRegionSum += input.get(i + n, j + m);
|
||
|
||
fRegionSum /= 25.0f;
|
||
output.set(i, j, input.get(i, j) > (fRegionSum * fAdaptiveBias) ? 1.0f : 0.0f);
|
||
}
|
||
break;
|
||
}
|
||
|
||
// DRAW STUFF ONLY HERE
|
||
Clear(olc::DARK_BLUE);
|
||
DrawFrame(algo == MORPHO ? threshold : input, 10, 10);
|
||
DrawFrame(output, 340, 10);
|
||
|
||
DrawString(150, 255, "INPUT");
|
||
DrawString(480, 255, "OUTPUT");
|
||
|
||
DrawString(10, 275, "1) Threshold");
|
||
DrawString(10, 285, "2) Absolute Motion");
|
||
DrawString(10, 295, "3) Low-Pass Temporal Filtering");
|
||
DrawString(10, 305, "4) Convolution (Blurring/Sharpening)");
|
||
DrawString(10, 315, "5) Sobel Edge Detection");
|
||
DrawString(10, 325, "6) Binary Morphological Operations (Erosion/Dilation)");
|
||
DrawString(10, 335, "7) Median Filter");
|
||
DrawString(10, 345, "8) Adaptive Threshold");
|
||
|
||
|
||
switch (algo)
|
||
{
|
||
case THRESHOLD:
|
||
DrawString(10, 375, "Change threshold value with Z and X keys");
|
||
DrawString(10, 385, "Current value = " + std::to_string(fThresholdValue));
|
||
break;
|
||
|
||
case LOWPASS:
|
||
DrawString(10, 375, "Change RC constant value with Z and X keys");
|
||
DrawString(10, 385, "Current value = " + std::to_string(fLowPassRC));
|
||
break;
|
||
|
||
case CONVOLUTION:
|
||
DrawString(10, 375, "Change convolution kernel with Z and X keys");
|
||
DrawString(10, 385, "Current kernel = " + std::string((pConvoKernel == kernel_blur) ? "Blur" : "Sharpen"));
|
||
break;
|
||
|
||
case MORPHO:
|
||
DrawString(10, 375, "Change operation with Z and X and C keys");
|
||
if (morph == DILATION) DrawString(10, 385, "Current operation = DILATION");
|
||
if (morph == EROSION) DrawString(10, 385, "Current operation = EROSION");
|
||
if (morph == EDGE) DrawString(10, 385, "Current operation = EDGE");
|
||
DrawString(10, 395, "Change Iterations with A and S keys");
|
||
DrawString(10, 405, "Current iteration count = " + std::to_string(nMorphCount));
|
||
|
||
|
||
break;
|
||
|
||
case ADAPTIVE:
|
||
DrawString(10, 375, "Change adaptive threshold bias with Z and X keys");
|
||
DrawString(10, 385, "Current value = " + std::to_string(fAdaptiveBias));
|
||
break;
|
||
|
||
default:
|
||
break;
|
||
}
|
||
|
||
if (GetKey(olc::Key::ESCAPE).bPressed) return false;
|
||
return true;
|
||
}
|
||
};
|
||
|
||
|
||
int main()
|
||
{
|
||
WIP_ImageProcessing demo;
|
||
if (demo.Construct(670, 460, 2, 2))
|
||
demo.Start();
|
||
|
||
return 0;
|
||
}
|
||
|
||
|