Upstream for PGE updates.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
olcPixelGameEngine/OneLoneCoder_PGE_8BitsImPro...

504 lines
14 KiB

/*
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, <EFBFBD>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;
}