diff --git a/OneLoneCoder_PGE_8BitsImProc.cpp b/OneLoneCoder_PGE_8BitsImProc.cpp new file mode 100644 index 0000000..eae24cf --- /dev/null +++ b/OneLoneCoder_PGE_8BitsImProc.cpp @@ -0,0 +1,504 @@ +/* + 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, Š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 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()); + 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; +} + +