mirror of https://github.com/aseprite/aseprite.git
Add new ordered dithering algorithm
This commit is contained in:
parent
cbee0862f3
commit
16aeae0833
|
|
@ -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)"))
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue