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