parent
bb767c871e
commit
dbeabb8ffc
@ -0,0 +1,265 @@ |
||||
/*
|
||||
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, ©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; |
||||
} |
Loading…
Reference in new issue