Add color fit criteria for Color Mode conversion (fix #2787, #4416)

Added other color comparison criterias (fit criteria) during
color mode conversion RGBA to Indexed or Grayscale to Indexed.
The 'fit criteria' will help us to recolor an RGB image with
a limited color palette taking into account different color
perception criteria (color spaces: RGB, linearized RGB,
CIE XYZ, CIE LAB).
This commit is contained in:
Gaspar Capello 2024-08-20 14:46:43 -03:00 committed by David Capello
parent 3cc1c63274
commit e1bd7990a3
11 changed files with 42 additions and 33 deletions

View File

@ -130,7 +130,8 @@ SetPixelFormat::SetPixelFormat(Sprite* sprite,
false, // TODO is background? it depends of the layer where this tileset is used false, // TODO is background? it depends of the layer where this tileset is used
mapAlgorithm, mapAlgorithm,
toGray, toGray,
&superDel); &superDel,
fitCriteria);
} }
superDel.nextImage(); superDel.nextImage();
} }

View File

@ -39,7 +39,7 @@ namespace cmd {
const doc::RgbMapAlgorithm mapAlgorithm, const doc::RgbMapAlgorithm mapAlgorithm,
doc::rgba_to_graya_func toGray, doc::rgba_to_graya_func toGray,
render::TaskDelegate* delegate, render::TaskDelegate* delegate,
const doc::FitCriteria fitCriteria = doc::FitCriteria::DEFAULT); const doc::FitCriteria fitCriteria);
protected: protected:
void onExecute() override; void onExecute() override;

View File

@ -668,9 +668,10 @@ void ChangePixelFormatCommand::onExecute(Context* context)
window.saveOptions(); window.saveOptions();
} }
else { else {
// TO DO: in a first approach a simple conversion to indexed color mode if (m_format == IMAGE_INDEXED) {
// it's just via the old fit criteria (Euclidean color distance). m_rgbmap = Preferences::instance().quantization.rgbmapAlgorithm();
m_fitCriteria = FitCriteria::DEFAULT; m_fitCriteria = Preferences::instance().quantization.fitCriteria();
}
} }
// No conversion needed // No conversion needed

View File

@ -263,6 +263,11 @@ void NewFileCommand::onExecute(Context* ctx)
else else
layer->setName(fmt::format("{} {}", Strings::commands_NewLayer_Layer(), 1)); layer->setName(fmt::format("{} {}", Strings::commands_NewLayer_Layer(), 1));
} }
if (sprite->pixelFormat() == IMAGE_INDEXED) {
sprite->rgbMap(0, Sprite::RgbMapFor(!layer->isBackground()),
Preferences::instance().quantization.rgbmapAlgorithm(),
Preferences::instance().quantization.fitCriteria());
}
// Show the sprite to the user // Show the sprite to the user
std::unique_ptr<Doc> doc(new Doc(sprite.get())); std::unique_ptr<Doc> doc(new Doc(sprite.get()));

View File

@ -1,5 +1,5 @@
// Aseprite // Aseprite
// Copyright (C) 2018-2023 Igara Studio S.A. // Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello // Copyright (C) 2001-2018 David Capello
// //
// This program is distributed under the terms of // This program is distributed under the terms of
@ -1289,13 +1289,18 @@ void DocExporter::renderTexture(Context* ctx,
// Make the sprite compatible with the texture so the render() // Make the sprite compatible with the texture so the render()
// works correctly. // works correctly.
if (sample.sprite()->pixelFormat() != textureImage->pixelFormat()) { if (sample.sprite()->pixelFormat() != textureImage->pixelFormat()) {
RgbMapAlgorithm rgbmapAlgo =
Preferences::instance().quantization.rgbmapAlgorithm();
FitCriteria fc =
Preferences::instance().quantization.fitCriteria();
cmd::SetPixelFormat( cmd::SetPixelFormat(
sample.sprite(), sample.sprite(),
textureImage->pixelFormat(), textureImage->pixelFormat(),
render::Dithering(), render::Dithering(),
Sprite::DefaultRgbMapAlgorithm(), // TODO add rgbmap algorithm preference rgbmapAlgo,
nullptr, // toGray is not needed because the texture is Indexed or RGB nullptr, // toGray is not needed because the texture is Indexed or RGB
nullptr) // TODO add a delegate to show progress nullptr, // TODO add a delegate to show progress
fc)
.execute(ctx); .execute(ctx);
} }

View File

@ -290,6 +290,13 @@ bool AseFormat::onLoad(FileOp* fop)
return false; return false;
Sprite* sprite = delegate.sprite(); Sprite* sprite = delegate.sprite();
// Assign RgbMap
if (sprite->pixelFormat() == IMAGE_INDEXED)
sprite->rgbMap(0, Sprite::RgbMapFor(sprite->isOpaque()),
fop->config().rgbMapAlgorithm,
fop->config().fitCriteria);
fop->createDocument(sprite); fop->createDocument(sprite);
if (sprite->colorSpace() != nullptr && if (sprite->colorSpace() != nullptr &&

View File

@ -1356,6 +1356,11 @@ ImageRef FileOp::sequenceImageToLoad(
// Add the layer // Add the layer
sprite->root()->addLayer(layer); sprite->root()->addLayer(layer);
// Assign RgbMap
if (sprite->pixelFormat() == IMAGE_INDEXED)
sprite->rgbMap(0, Sprite::RgbMapFor(sprite->isOpaque()),
m_config.rgbMapAlgorithm,
m_config.fitCriteria);
// Done // Done
createDocument(sprite); createDocument(sprite);
m_seq.layer = layer; m_seq.layer = layer;

View File

@ -1,5 +1,5 @@
// Aseprite // Aseprite
// Copyright (C) 2019-2023 Igara Studio S.A. // Copyright (C) 2019-2024 Igara Studio S.A.
// //
// This program is distributed under the terms of // This program is distributed under the terms of
// the End-User License Agreement for Aseprite. // the End-User License Agreement for Aseprite.
@ -25,6 +25,7 @@ void FileOpConfig::fillFromPreferences()
defaultSliceColor = pref.slices.defaultColor(); defaultSliceColor = pref.slices.defaultColor();
workingCS = get_working_rgb_space_from_preferences(); workingCS = get_working_rgb_space_from_preferences();
rgbMapAlgorithm = pref.quantization.rgbmapAlgorithm(); rgbMapAlgorithm = pref.quantization.rgbmapAlgorithm();
fitCriteria = pref.quantization.fitCriteria();
cacheCompressedTilesets = pref.tileset.cacheCompressedTilesets(); cacheCompressedTilesets = pref.tileset.cacheCompressedTilesets();
} }

View File

@ -1,5 +1,5 @@
// Aseprite // Aseprite
// Copyright (C) 2019-2023 Igara Studio S.A. // Copyright (C) 2019-2024 Igara Studio S.A.
// //
// This program is distributed under the terms of // This program is distributed under the terms of
// the End-User License Agreement for Aseprite. // the End-User License Agreement for Aseprite.
@ -33,9 +33,14 @@ namespace app {
app::Color defaultSliceColor = app::Color::fromRgb(0, 0, 255); app::Color defaultSliceColor = app::Color::fromRgb(0, 0, 255);
// Algorithm used to create a palette from RGB files. // Algorithm used to fit any color into the available palette colors in
// Indexed Color Mode.
doc::RgbMapAlgorithm rgbMapAlgorithm = doc::RgbMapAlgorithm::DEFAULT; doc::RgbMapAlgorithm rgbMapAlgorithm = doc::RgbMapAlgorithm::DEFAULT;
// Fit criteria used to compare colors during the conversion to
// Indexed Color Mode.
doc::FitCriteria fitCriteria = doc::FitCriteria::DEFAULT;
// Cache compressed tilesets. When we load a tileset from a // Cache compressed tilesets. When we load a tileset from a
// .aseprite file, the compressed data will be stored on memory to // .aseprite file, the compressed data will be stored on memory to
// make the save operation faster (as we can re-use the already // make the save operation faster (as we can re-use the already

View File

@ -67,14 +67,6 @@ Preferences::Preferences()
load(); load();
// Create a connection with the default RgbMapAlgorithm preferences
// to change the default algorithm in the "doc" layer.
quantization.rgbmapAlgorithm.AfterChange.connect(
[](const doc::RgbMapAlgorithm& newValue){
doc::Sprite::SetDefaultRgbMapAlgorithm(newValue);
});
doc::Sprite::SetDefaultRgbMapAlgorithm(quantization.rgbmapAlgorithm());
// Create a connection with the default document preferences grid // Create a connection with the default document preferences grid
// bounds to sync the default grid bounds for new sprites in the // bounds to sync the default grid bounds for new sprites in the
// "doc" layer. // "doc" layer.

View File

@ -36,7 +36,6 @@
namespace doc { namespace doc {
static RgbMapAlgorithm g_rgbMapAlgorithm = RgbMapAlgorithm::DEFAULT;
static gfx::Rect g_defaultGridBounds(0, 0, 16, 16); static gfx::Rect g_defaultGridBounds(0, 0, 16, 16);
// static // static
@ -56,18 +55,6 @@ void Sprite::SetDefaultGridBounds(const gfx::Rect& defGridBounds)
g_defaultGridBounds.h = 1; g_defaultGridBounds.h = 1;
} }
// static
RgbMapAlgorithm Sprite::DefaultRgbMapAlgorithm()
{
return g_rgbMapAlgorithm;
}
// static
void Sprite::SetDefaultRgbMapAlgorithm(const RgbMapAlgorithm mapAlgo)
{
g_rgbMapAlgorithm = mapAlgo;
}
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
// Constructors/Destructor // Constructors/Destructor
@ -441,7 +428,7 @@ RgbMap* Sprite::rgbMap(const frame_t frame,
const RgbMapFor forLayer) const const RgbMapFor forLayer) const
{ {
FitCriteria fc = FitCriteria::DEFAULT; FitCriteria fc = FitCriteria::DEFAULT;
RgbMapAlgorithm algo = g_rgbMapAlgorithm; RgbMapAlgorithm algo = RgbMapAlgorithm::DEFAULT;
if (m_rgbMap) { if (m_rgbMap) {
fc = m_rgbMap->fitCriteria(); fc = m_rgbMap->fitCriteria();
algo = m_rgbMap->rgbmapAlgorithm(); algo = m_rgbMap->rgbmapAlgorithm();