Add new ordered dithering algorithm

This commit is contained in:
David Capello 2017-05-17 13:32:34 -03:00
parent cbee0862f3
commit 16aeae0833
7 changed files with 207 additions and 49 deletions

View File

@ -34,7 +34,8 @@ AppOptions::AppOptions(int argc, const char* argv[])
, m_saveAs(m_po.add("save-as").requiresValue("<filename>").description("Save the last given sprite with other format"))
, m_palette(m_po.add("palette").requiresValue("<filename>").description("Change the palette of the last given sprite"))
, m_scale(m_po.add("scale").requiresValue("<factor>").description("Resize all previously opened sprites"))
, m_colorMode(m_po.add("color-mode").requiresValue("<mode>").description("Change color mode of all previously\nopened sprites:\n rgb\n grayscale\n indexed\n indexed-ordered-dithering"))
, m_ditheringAlgorithm(m_po.add("dithering-algorithm").requiresValue("<algorithm>").description("Dithering algorithm used in --color-mode\nto convert images from RGB to Indexed\n none\n ordered\n old-ordered"))
, m_colorMode(m_po.add("color-mode").requiresValue("<mode>").description("Change color mode of all previously\nopened sprites:\n rgb\n grayscale\n indexed"))
, m_shrinkTo(m_po.add("shrink-to").requiresValue("width,height").description("Shrink each sprite if it is\nlarger than width or height"))
, m_data(m_po.add("data").requiresValue("<filename.json>").description("File to store the sprite sheet metadata"))
, m_format(m_po.add("format").requiresValue("<format>").description("Format to export the data file\n(json-hash, json-array)"))

View File

@ -48,6 +48,7 @@ public:
const Option& saveAs() const { return m_saveAs; }
const Option& palette() const { return m_palette; }
const Option& scale() const { return m_scale; }
const Option& ditheringAlgorithm() const { return m_ditheringAlgorithm; }
const Option& colorMode() const { return m_colorMode; }
const Option& shrinkTo() const { return m_shrinkTo; }
const Option& data() const { return m_data; }
@ -101,6 +102,7 @@ private:
Option& m_saveAs;
Option& m_palette;
Option& m_scale;
Option& m_ditheringAlgorithm;
Option& m_colorMode;
Option& m_shrinkTo;
Option& m_data;

View File

@ -32,6 +32,7 @@
#include "doc/selected_frames.h"
#include "doc/selected_layers.h"
#include "doc/slice.h"
#include "render/dithering_algorithm.h"
namespace app {
@ -138,6 +139,7 @@ void CliProcessor::process()
CliOpenFile cof;
SpriteSheetType sheetType = SpriteSheetType::None;
app::Document* lastDoc = nullptr;
render::DitheringAlgorithm ditheringAlgorithm = render::DitheringAlgorithm::None;
for (const auto& value : m_options.values()) {
const AppOptions::Option* opt = value.option();
@ -342,6 +344,15 @@ void CliProcessor::process()
ctx->executeCommand(command);
}
}
// --dithering-algorithm <algorithm>
else if (opt == &m_options.ditheringAlgorithm()) {
if (value.value() == "none")
ditheringAlgorithm = render::DitheringAlgorithm::None;
else if (value.value() == "old-ordered")
ditheringAlgorithm = render::DitheringAlgorithm::OldOrdered;
else if (value.value() == "ordered")
ditheringAlgorithm = render::DitheringAlgorithm::Ordered;
}
// --color-mode <mode>
else if (opt == &m_options.colorMode()) {
Command* command = CommandsModule::instance()->getCommandByName(CommandId::ChangePixelFormat);
@ -354,15 +365,22 @@ void CliProcessor::process()
}
else if (value.value() == "indexed") {
params.set("format", "indexed");
}
else if (value.value() == "indexed-ordered-dithering") {
params.set("format", "indexed");
params.set("dithering", "ordered");
switch (ditheringAlgorithm) {
case render::DitheringAlgorithm::None:
params.set("dithering", "none");
break;
case render::DitheringAlgorithm::OldOrdered:
params.set("dithering", "old-ordered");
break;
case render::DitheringAlgorithm::Ordered:
params.set("dithering", "ordered");
break;
}
}
else {
throw std::runtime_error("--color-mode needs a valid color mode for conversion\n"
"Usage: --color-mode <mode>\n"
"Where <mode> can be rgb, grayscale, indexed, or indexed-ordered-dithering");
"Where <mode> can be rgb, grayscale, or indexed");
}
for (auto doc : ctx->documents()) {

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2001-2015 David Capello
// Copyright (C) 2001-2017 David Capello
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@ -36,7 +36,7 @@ namespace cmd {
}
private:
void setFormat(PixelFormat format);
void setFormat(doc::PixelFormat format);
doc::PixelFormat m_oldFormat;
doc::PixelFormat m_newFormat;

View File

@ -56,6 +56,9 @@ public:
case render::DitheringAlgorithm::None:
setText("-> Indexed");
break;
case render::DitheringAlgorithm::OldOrdered:
setText("-> Indexed w/Old Ordered Dithering");
break;
case render::DitheringAlgorithm::Ordered:
setText("-> Indexed w/Ordered Dithering");
break;
@ -144,6 +147,7 @@ public:
, m_editor(editor)
, m_image(nullptr)
, m_imageBuffer(new doc::ImageBuffer)
, m_selectedItem(nullptr)
{
doc::PixelFormat from = m_editor->sprite()->pixelFormat();
@ -160,6 +164,7 @@ public:
if (from != IMAGE_INDEXED) {
colorMode()->addChild(new ConversionItem(IMAGE_INDEXED));
colorMode()->addChild(new ConversionItem(IMAGE_INDEXED, render::DitheringAlgorithm::Ordered));
colorMode()->addChild(new ConversionItem(IMAGE_INDEXED, render::DitheringAlgorithm::OldOrdered));
}
if (from != IMAGE_GRAYSCALE)
colorMode()->addChild(new ConversionItem(IMAGE_GRAYSCALE));
@ -176,35 +181,39 @@ public:
}
~ColorModeWindow() {
m_editor->renderEngine().removePreviewImage();
m_editor->invalidate();
stop();
}
doc::PixelFormat pixelFormat() const {
return
static_cast<ConversionItem*>(colorMode()->getSelectedChild())
->pixelFormat();
ASSERT(m_selectedItem);
return m_selectedItem->pixelFormat();
}
render::DitheringAlgorithm ditheringAlgorithm() const {
return
static_cast<ConversionItem*>(colorMode()->getSelectedChild())
->ditheringAlgorithm();
ASSERT(m_selectedItem);
return m_selectedItem->ditheringAlgorithm();
}
private:
void onChangeColorMode() {
m_timer.stop();
void stop() {
m_timer.stop();
if (m_bgThread) {
m_bgThread->stop();
m_bgThread.reset(nullptr);
}
m_editor->renderEngine().removePreviewImage();
m_editor->invalidate();
}
void onChangeColorMode() {
ConversionItem* item =
static_cast<ConversionItem*>(colorMode()->getSelectedChild());
if (item == m_selectedItem) // Avoid restarting the conversion process for the same option
return;
m_selectedItem = item;
stop();
m_image.reset(
Image::create(item->pixelFormat(),
@ -252,6 +261,7 @@ private:
doc::ImageRef m_image;
doc::ImageBufferPtr m_imageBuffer;
base::UniquePtr<ConvertThread> m_bgThread;
ConversionItem* m_selectedItem;
};
} // anonymous namespace
@ -297,6 +307,8 @@ void ChangePixelFormatCommand::onLoadParams(const Params& params)
std::string dithering = params.get("dithering");
if (dithering == "ordered")
m_dithering = render::DitheringAlgorithm::Ordered;
else if (dithering == "old-ordered")
m_dithering = render::DitheringAlgorithm::OldOrdered;
else
m_dithering = render::DitheringAlgorithm::None;
}
@ -314,7 +326,7 @@ bool ChangePixelFormatCommand::onEnabled(Context* context)
if (sprite->pixelFormat() == IMAGE_INDEXED &&
m_format == IMAGE_INDEXED &&
m_dithering == render::DitheringAlgorithm::Ordered)
m_dithering != render::DitheringAlgorithm::None)
return false;
return true;
@ -331,7 +343,7 @@ bool ChangePixelFormatCommand::onChecked(Context* context)
if (sprite &&
sprite->pixelFormat() == IMAGE_INDEXED &&
m_format == IMAGE_INDEXED &&
m_dithering == render::DitheringAlgorithm::Ordered)
m_dithering != render::DitheringAlgorithm::None)
return false;
return

View File

@ -13,6 +13,8 @@
#include "doc/palette.h"
#include "doc/rgbmap.h"
#include <limits>
namespace render {
// Creates a Bayer dither matrix.
@ -23,7 +25,7 @@ namespace render {
int m_matrix[N*N];
public:
int maxValue() const { return N*N; }
int maxValue() const { return N*N-1; }
BayerMatrix() {
int c = 0;
@ -145,36 +147,151 @@ namespace render {
nearest1idx);
}
template<typename Matrix>
void ditherRgbImageToIndexed(const Matrix& matrix,
const doc::Image* srcImage,
doc::Image* dstImage,
int u, int v,
const doc::RgbMap* rgbmap,
const doc::Palette* palette,
bool* stopFlag = nullptr) {
const doc::LockImageBits<doc::RgbTraits> srcBits(srcImage);
doc::LockImageBits<doc::IndexedTraits> dstBits(dstImage);
auto srcIt = srcBits.begin();
auto dstIt = dstBits.begin();
int w = srcImage->width();
int h = srcImage->height();
private:
int m_transparentIndex;
};
for (int y=0; y<h; ++y) {
for (int x=0; x<w; ++x, ++srcIt, ++dstIt) {
ASSERT(srcIt != srcBits.end());
ASSERT(dstIt != dstBits.end());
*dstIt = ditherRgbPixelToIndex(matrix, *srcIt, x+u, y+v, rgbmap, palette);
}
if (stopFlag && *stopFlag)
break;
// New ordered dithering algorithm using the best match between two
// indexes to create a mix that can reproduce the original RGB
// color.
//
// TODO it's too slow for big color palettes:
// O(W*H*P) where P is the number of palette entries
//
// Some ideas from:
// http://bisqwit.iki.fi/story/howto/dither/jy/
//
class OrderedDither2 {
static int colorDistance(int r1, int g1, int b1, int a1,
int r2, int g2, int b2, int a2) {
// The factor for RGB components came from doc::rba_luma()
return int(std::abs(r1-r2) * 2126 +
std::abs(g1-g2) * 7152 +
std::abs(b1-b2) * 722 +
std::abs(a1-a2));
}
public:
OrderedDither2(int transparentIndex = -1) : m_transparentIndex(transparentIndex) {
}
template<typename Matrix>
doc::color_t ditherRgbPixelToIndex(
const Matrix& matrix,
doc::color_t color,
int x, int y,
const doc::RgbMap* rgbmap,
const doc::Palette* palette) {
// Alpha=0, output transparent color
if (m_transparentIndex >= 0 &&
doc::rgba_geta(color) == 0) {
return m_transparentIndex;
}
// Get RGBA values
const int r = doc::rgba_getr(color);
const int g = doc::rgba_getg(color);
const int b = doc::rgba_getb(color);
const int a = doc::rgba_geta(color);
// Find the best palette entry for the given color.
const int index =
(rgbmap ? rgbmap->mapColor(r, g, b, a):
palette->findBestfit(r, g, b, a, m_transparentIndex));
const doc::color_t color0 = palette->getEntry(index);
const int r0 = doc::rgba_getr(color0);
const int g0 = doc::rgba_getg(color0);
const int b0 = doc::rgba_getb(color0);
const int a0 = doc::rgba_geta(color0);
// Find the best combination between the found nearest index and
// an alternative palette color to create the original RGB color.
int bestMix = 0;
int altIndex = -1;
int closestDistance = std::numeric_limits<int>::max();
for (int i=0; i<palette->size(); ++i) {
const doc::color_t color1 = palette->getEntry(i);
const int r1 = doc::rgba_getr(color1);
const int g1 = doc::rgba_getg(color1);
const int b1 = doc::rgba_getb(color1);
const int a1 = doc::rgba_geta(color1);
// Find the best "mix factor" between both palette indexes to
// reproduce the original RGB color. A possible algorithm
// would be to iterate all possible mix factors from 0 to
// maxMixValue, but this is too slow, so we try to figure out
// a good mix factor using the RGB values of color0 and
// color1.
int maxMixValue = matrix.maxValue();
int mix = 0;
int div = 0;
if (r1-r0) mix += 2126 * maxMixValue * (r-r0) / (r1-r0), div += 2126;
if (g1-g0) mix += 7152 * maxMixValue * (g-g0) / (g1-g0), div += 7152;
if (b1-b0) mix += 722 * maxMixValue * (b-b0) / (b1-b0), div += 722;
if (mix) {
if (div)
mix /= div;
mix = MID(0, mix, maxMixValue);
}
const int rM = r0 + (r1-r0) * mix / maxMixValue;
const int gM = g0 + (g1-g0) * mix / maxMixValue;
const int bM = b0 + (b1-b0) * mix / maxMixValue;
const int aM = a0 + (a1-a0) * mix / maxMixValue;
const int d =
colorDistance(r, g, b, a, rM, gM, bM, aM)
// Don't use an alternative index if it's too far away from the first index
+ colorDistance(r0, g0, b0, a0, r1, g1, b1, a1) / 10;
if (closestDistance > d) {
closestDistance = d;
bestMix = mix;
altIndex = i;
}
}
// Using the bestMix factor the dithering matrix tells us if we
// should paint with altIndex or index in this x,y position.
if (altIndex >= 0 && matrix(x, y) < bestMix)
return altIndex;
else
return index;
}
private:
int m_transparentIndex;
};
template<typename Dithering,
typename Matrix>
void dither_rgb_image_to_indexed(Dithering& dithering,
const Matrix& matrix,
const doc::Image* srcImage,
doc::Image* dstImage,
int u, int v,
const doc::RgbMap* rgbmap,
const doc::Palette* palette,
bool* stopFlag = nullptr) {
const doc::LockImageBits<doc::RgbTraits> srcBits(srcImage);
doc::LockImageBits<doc::IndexedTraits> dstBits(dstImage);
auto srcIt = srcBits.begin();
auto dstIt = dstBits.begin();
int w = srcImage->width();
int h = srcImage->height();
for (int y=0; y<h; ++y) {
for (int x=0; x<w; ++x, ++srcIt, ++dstIt) {
ASSERT(srcIt != srcBits.end());
ASSERT(dstIt != dstBits.end());
*dstIt = dithering.ditherRgbPixelToIndex(matrix, *srcIt, x+u, y+v, rgbmap, palette);
if (stopFlag && *stopFlag)
break;
}
}
}
} // namespace render
#endif

View File

@ -95,12 +95,20 @@ Image* convert_pixel_format(
// RGB -> Indexed with ordered dithering
if (image->pixelFormat() == IMAGE_RGB &&
pixelFormat == IMAGE_INDEXED &&
ditheringAlgorithm == DitheringAlgorithm::Ordered) {
ditheringAlgorithm != DitheringAlgorithm::None) {
BayerMatrix<8> matrix;
OrderedDither dither;
dither.ditherRgbImageToIndexed(matrix, image, new_image,
0, 0, rgbmap, palette,
stopFlag);
switch (ditheringAlgorithm) {
case DitheringAlgorithm::OldOrdered: {
OrderedDither dither;
dither_rgb_image_to_indexed(dither, matrix, image, new_image, 0, 0, rgbmap, palette, stopFlag);
break;
}
case DitheringAlgorithm::Ordered: {
OrderedDither2 dither;
dither_rgb_image_to_indexed(dither, matrix, image, new_image, 0, 0, rgbmap, palette, stopFlag);
break;
}
}
return new_image;
}