265 lines
8.6 KiB
C++
265 lines
8.6 KiB
C++
/*
|
||
Blithering About Dithering (Floyd-Steinberg)
|
||
"2022 lets go!" - javidx9
|
||
|
||
License (OLC-3)
|
||
~~~~~~~~~~~~~~~
|
||
Copyright 2018 - 2021 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.
|
||
|
||
Video:
|
||
~~~~~~
|
||
https://youtu.be/lseR6ZguBNY
|
||
|
||
Use Q and W keys to view quantised image and dithered image respectively
|
||
Use Left mouse button to pan, and mouse wheel to zoom to cursor
|
||
|
||
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
|
||
Homepage: https://www.onelonecoder.com
|
||
|
||
Author
|
||
~~~~~~
|
||
David Barr, aka javidx9, <20>OneLoneCoder 2019, 2020, 2021, 2022
|
||
*/
|
||
|
||
|
||
// Using olc::PixelGameEngine for input and visualisation
|
||
#define OLC_PGE_APPLICATION
|
||
#include "olcPixelGameEngine.h"
|
||
|
||
// Using a transformed view to handle pan and zoom
|
||
#define OLC_PGEX_TRANSFORMEDVIEW
|
||
#include "olcPGEX_TransformedView.h"
|
||
|
||
//#include <algorithm>
|
||
|
||
// Override base class with your custom functionality
|
||
class Dithering : public olc::PixelGameEngine
|
||
{
|
||
public:
|
||
Dithering()
|
||
{
|
||
sAppName = "Floyd Steinberg Dithering";
|
||
}
|
||
|
||
olc::TransformedView tv;
|
||
std::unique_ptr<olc::Sprite> m_pImage;
|
||
std::unique_ptr<olc::Sprite> m_pQuantised;
|
||
std::unique_ptr<olc::Sprite> m_pDithered;
|
||
|
||
public:
|
||
// Called once at start of application
|
||
bool OnUserCreate() override
|
||
{
|
||
// Prepare Pan & Zoom
|
||
tv.Initialise({ ScreenWidth(), ScreenHeight() });
|
||
|
||
// Load Test Image
|
||
m_pImage = std::make_unique<olc::Sprite>("./assets/Flower_640x480.png");
|
||
|
||
// Create two more images with the same dimensions
|
||
m_pQuantised = std::make_unique<olc::Sprite>(m_pImage->width, m_pImage->height);
|
||
m_pDithered = std::make_unique<olc::Sprite>(m_pImage->width, m_pImage->height);
|
||
|
||
// These lambda functions output a new olc::Pixel based on
|
||
// the pixel it is given
|
||
auto Convert_RGB_To_Greyscale = [](const olc::Pixel in)
|
||
{
|
||
uint8_t greyscale = uint8_t(0.2162f * float(in.r) + 0.7152f * float(in.g) + 0.0722f * float(in.b));
|
||
return olc::Pixel(greyscale, greyscale, greyscale);
|
||
};
|
||
|
||
|
||
// Quantising functions
|
||
auto Quantise_Greyscale_1Bit = [](const olc::Pixel in)
|
||
{
|
||
return in.r < 128 ? olc::BLACK : olc::WHITE;
|
||
};
|
||
|
||
auto Quantise_Greyscale_NBit = [](const olc::Pixel in)
|
||
{
|
||
constexpr int nBits = 2;
|
||
constexpr float fLevels = (1 << nBits) - 1;
|
||
uint8_t c = uint8_t(std::clamp(std::round(float(in.r) / 255.0f * fLevels) / fLevels * 255.0f, 0.0f, 255.0f));
|
||
return olc::Pixel(c, c, c);
|
||
};
|
||
|
||
auto Quantise_RGB_NBit = [](const olc::Pixel in)
|
||
{
|
||
constexpr int nBits = 2;
|
||
constexpr float fLevels = (1 << nBits) - 1;
|
||
uint8_t cr = uint8_t(std::clamp(std::round(float(in.r) / 255.0f * fLevels) / fLevels * 255.0f, 0.0f, 255.0f));
|
||
uint8_t cb = uint8_t(std::clamp(std::round(float(in.g) / 255.0f * fLevels) / fLevels * 255.0f, 0.0f, 255.0f));
|
||
uint8_t cg = uint8_t(std::clamp(std::round(float(in.b) / 255.0f * fLevels) / fLevels * 255.0f, 0.0f, 255.0f));
|
||
return olc::Pixel(cr, cb, cg);
|
||
};
|
||
|
||
auto Quantise_RGB_CustomPalette = [](const olc::Pixel in)
|
||
{
|
||
std::array<olc::Pixel, 5> nShades = { olc::BLACK, olc::WHITE, olc::YELLOW, olc::MAGENTA, olc::CYAN };
|
||
|
||
float fClosest = INFINITY;
|
||
olc::Pixel pClosest;
|
||
|
||
for (const auto& c : nShades)
|
||
{
|
||
float fDistance = float(
|
||
std::sqrt(
|
||
std::pow(float(c.r) - float(in.r), 2) +
|
||
std::pow(float(c.g) - float(in.g), 2) +
|
||
std::pow(float(c.b) - float(in.b), 2)));
|
||
|
||
if (fDistance < fClosest)
|
||
{
|
||
fClosest = fDistance;
|
||
pClosest = c;
|
||
}
|
||
}
|
||
|
||
return pClosest;
|
||
};
|
||
|
||
|
||
// We don't need greyscale for the final demonstration, which uses
|
||
// RGB, but I've left this here as reference
|
||
//std::transform(
|
||
// m_pImage->pColData.begin(),
|
||
// m_pImage->pColData.end(),
|
||
// m_pImage->pColData.begin(), Convert_RGB_To_Greyscale);
|
||
|
||
|
||
// Quantise The Image
|
||
std::transform(
|
||
m_pImage->pColData.begin(),
|
||
m_pImage->pColData.end(),
|
||
m_pQuantised->pColData.begin(), Quantise_RGB_NBit);
|
||
|
||
// Perform Dither
|
||
Dither_FloydSteinberg(m_pImage.get(), m_pDithered.get(), Quantise_RGB_NBit);
|
||
|
||
return true;
|
||
}
|
||
|
||
|
||
|
||
void Dither_FloydSteinberg(const olc::Sprite* pSource, olc::Sprite* pDest,
|
||
std::function<olc::Pixel(const olc::Pixel)> funcQuantise)
|
||
{
|
||
// The destination image is primed with the source image as the pixel
|
||
// values become altered as the algorithm executes
|
||
std::copy(pSource->pColData.begin(), pSource->pColData.end(), pDest->pColData.begin());
|
||
|
||
// Iterate through each pixel from top left to bottom right, compare the pixel
|
||
// with that on the "allowed" list, and distribute that error to neighbours
|
||
// not yet computed
|
||
olc::vi2d vPixel;
|
||
for (vPixel.y = 0; vPixel.y < pSource->height; vPixel.y++)
|
||
{
|
||
for (vPixel.x = 0; vPixel.x < pSource->width; vPixel.x++)
|
||
{
|
||
// Grap and get nearest pixel equivalent from our allowed
|
||
// palette
|
||
olc::Pixel op = pDest->GetPixel(vPixel);
|
||
olc::Pixel qp = funcQuantise(op);
|
||
|
||
// olc::Pixels are "inconveniently" clamped to sensible ranges using an unsigned type...
|
||
// ...which means they cant be negative. This hampers us a tad here,
|
||
// so will resort to manual alteration using a signed type
|
||
int32_t error[3] =
|
||
{
|
||
op.r - qp.r,
|
||
op.g - qp.g,
|
||
op.b - qp.b
|
||
};
|
||
|
||
// Set destination pixel with nearest match from quantisation function
|
||
pDest->SetPixel(vPixel, qp);
|
||
|
||
// Distribute Error - Using a little utility lambda to keep the messy code
|
||
// all in one place. It's important to allow pixels to temporarily become
|
||
// negative in order to distribute the error to the neighbours in both
|
||
// directions... value directions that is, not spatial!
|
||
auto UpdatePixel = [&vPixel, &pDest, &error](const olc::vi2d& vOffset, const float fErrorBias)
|
||
{
|
||
olc::Pixel p = pDest->GetPixel(vPixel + vOffset);
|
||
int32_t k[3] = { p.r, p.g, p.b };
|
||
k[0] += int32_t(float(error[0]) * fErrorBias);
|
||
k[1] += int32_t(float(error[1]) * fErrorBias);
|
||
k[2] += int32_t(float(error[2]) * fErrorBias);
|
||
pDest->SetPixel(vPixel + vOffset, olc::Pixel(std::clamp(k[0], 0, 255), std::clamp(k[1], 0, 255), std::clamp(k[2], 0, 255)));
|
||
};
|
||
|
||
UpdatePixel({ +1, 0 }, 7.0f / 16.0f);
|
||
UpdatePixel({ -1, +1 }, 3.0f / 16.0f);
|
||
UpdatePixel({ 0, +1 }, 5.0f / 16.0f);
|
||
UpdatePixel({ +1, +1 }, 1.0f / 16.0f);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Called every frame
|
||
bool OnUserUpdate(float fElapsedTime) override
|
||
{
|
||
// Handle Pan & Zoom using defaults middle mouse button
|
||
tv.HandlePanAndZoom(0);
|
||
|
||
// Erase previous frame
|
||
Clear(olc::BLACK);
|
||
|
||
// Draw Source Image
|
||
if (GetKey(olc::Key::Q).bHeld)
|
||
{
|
||
tv.DrawSprite({ 0,0 }, m_pQuantised.get());
|
||
}
|
||
else if (GetKey(olc::Key::W).bHeld)
|
||
{
|
||
tv.DrawSprite({ 0,0 }, m_pDithered.get());
|
||
}
|
||
else
|
||
{
|
||
tv.DrawSprite({ 0,0 }, m_pImage.get());
|
||
}
|
||
|
||
return true;
|
||
}
|
||
};
|
||
|
||
int main()
|
||
{
|
||
Dithering demo;
|
||
if (demo.Construct(1280, 720, 1, 1))
|
||
demo.Start();
|
||
return 0;
|
||
} |