Add different formulas to convert RGB to Grayscale

This commit is contained in:
David Capello 2020-04-20 10:18:12 -03:00
parent 07f1510ebd
commit 1f34d0e46e
12 changed files with 132 additions and 27 deletions

View File

@ -117,6 +117,12 @@
<value id="SRGB" value="1" />
<value id="SPECIFIC" value="2" />
</enum>
<enum id="ToGrayAlgorithm">
<value id="DEFAULT" value="0" />
<value id="LUMA" value="0" />
<value id="HSV" value="1" />
<value id="HSL" value="2" />
</enum>
</types>
<global>
@ -258,6 +264,7 @@
<option id="with_alpha" type="bool" default="true" />
<option id="dithering_algorithm" type="std::string" />
<option id="dithering_factor" type="int" default="100" />
<option id="to_gray" type="ToGrayAlgorithm" default="ToGrayAlgorithm::DEFAULT" />
</section>
<section id="eyedropper" text="Editor">
<option id="channel" type="EyedropperChannel" default="EyedropperChannel::COLOR_ALPHA" />

View File

@ -1,5 +1,5 @@
<!-- Aseprite -->
<!-- Copyright (C) 2019 Igara Studio S.A. -->
<!-- Copyright (C) 2019-2020 Igara Studio S.A. -->
<!-- Copyright (C) 2017 David Capello -->
<gui>
<window id="color_mode" text="@.title">
@ -13,6 +13,11 @@
<slider min="0" max="100" id="factor" minwidth="100" />
<label text="%" />
</hbox>
<combobox id="to_gray_combobox">
<listitem text="!Luminance" />
<listitem text="!HSV" />
<listitem text="!HSL" />
</combobox>
<check text="@.flatten" id="flatten" />
<separator horizontal="true" />
<hbox>

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019 Igara Studio S.A.
// Copyright (C) 2019-2020 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -69,6 +69,7 @@ private:
SetPixelFormat::SetPixelFormat(Sprite* sprite,
const PixelFormat newFormat,
const render::Dithering& dithering,
doc::rgba_to_graya_func toGray,
render::TaskDelegate* delegate)
: WithSprite(sprite)
, m_oldFormat(sprite->pixelFormat())
@ -89,6 +90,7 @@ SetPixelFormat::SetPixelFormat(Sprite* sprite,
sprite->palette(cel->frame()),
cel->layer()->isBackground(),
old_image->maskColor(),
toGray,
&superDel));
m_seq.add(new cmd::ReplaceImage(sprite, old_image, new_image));

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019 Igara Studio S.A.
// Copyright (C) 2019-2020 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This program is distributed under the terms of
@ -11,6 +11,7 @@
#include "app/cmd/with_sprite.h"
#include "app/cmd_sequence.h"
#include "doc/color.h"
#include "doc/pixel_format.h"
namespace doc {
@ -31,6 +32,7 @@ namespace cmd {
SetPixelFormat(doc::Sprite* sprite,
const doc::PixelFormat newFormat,
const render::Dithering& dithering,
doc::rgba_to_graya_func toGray,
render::TaskDelegate* delegate);
protected:

View File

@ -44,6 +44,7 @@
#include "ui/size_hint_event.h"
#include "color_mode.xml.h"
#include <string>
namespace app {
@ -51,6 +52,15 @@ using namespace ui;
namespace {
rgba_to_graya_func get_gray_func(gen::ToGrayAlgorithm toGray) {
switch (toGray) {
case gen::ToGrayAlgorithm::LUMA: return &rgba_to_graya_using_luma;
case gen::ToGrayAlgorithm::HSV: return &rgba_to_graya_using_hsv;
case gen::ToGrayAlgorithm::HSL: return &rgba_to_graya_using_hsl;
}
return nullptr;
}
class ConversionItem : public ListItem {
public:
ConversionItem(const doc::PixelFormat pixelFormat)
@ -79,6 +89,7 @@ public:
const doc::frame_t frame,
const doc::PixelFormat pixelFormat,
const render::Dithering& dithering,
const gen::ToGrayAlgorithm toGray,
const gfx::Point& pos,
const bool newBlend)
: m_image(dstImage)
@ -91,10 +102,12 @@ public:
sprite, frame,
pixelFormat,
dithering,
toGray,
newBlend]() { // Copy the matrix
run(sprite, frame,
pixelFormat,
dithering,
toGray,
newBlend);
})
{
@ -118,6 +131,7 @@ private:
const doc::frame_t frame,
const doc::PixelFormat pixelFormat,
const render::Dithering& dithering,
const gen::ToGrayAlgorithm toGray,
const bool newBlend) {
doc::ImageRef tmp(
Image::create(sprite->pixelFormat(),
@ -142,6 +156,7 @@ private:
sprite->palette(frame),
(sprite->backgroundLayer() != nullptr),
0,
get_gray_func(toGray),
this);
m_running = false;
@ -211,9 +226,12 @@ public:
else {
amount()->setVisible(false);
}
if (from != IMAGE_GRAYSCALE)
if (from != IMAGE_GRAYSCALE) {
colorMode()->addChild(new ConversionItem(IMAGE_GRAYSCALE));
toGrayCombobox()->Change.connect(base::Bind<void>(&ColorModeWindow::onToGrayChange, this));
}
colorModeView()->setMinSize(
colorModeView()->sizeHint() +
colorMode()->sizeHint());
@ -249,6 +267,15 @@ public:
return d;
}
gen::ToGrayAlgorithm toGray() const {
static_assert(
int(gen::ToGrayAlgorithm::LUMA) == 0 &&
int(gen::ToGrayAlgorithm::HSV) == 1 &&
int(gen::ToGrayAlgorithm::HSL) == 2,
"Check that 'to_gray_combobox' combobox items matches these indexes in color_mode.xml");
return (gen::ToGrayAlgorithm)toGrayCombobox()->getSelectedItemIndex();
}
bool flattenEnabled() const {
return flatten()->isSelected();
}
@ -307,6 +334,11 @@ private:
amount()->setVisible(toIndexed && errorDiff);
}
{
const bool toGray = (dstPixelFormat == doc::IMAGE_GRAYSCALE);
toGrayCombobox()->setVisible(toGray);
}
m_image.reset(
Image::create(dstPixelFormat,
visibleBounds.w,
@ -336,6 +368,7 @@ private:
m_editor->frame(),
dstPixelFormat,
dithering(),
toGray(),
visibleBounds.origin(),
Preferences::instance().experimental.newBlend()));
@ -348,6 +381,12 @@ private:
onChangeColorMode();
}
void onToGrayChange() {
stop();
m_selectedItem = nullptr;
onChangeColorMode();
}
void onMonitorProgress() {
ASSERT(m_bgThread);
if (!m_bgThread)
@ -402,6 +441,7 @@ private:
bool m_useUI;
doc::PixelFormat m_format;
render::Dithering m_dithering;
gen::ToGrayAlgorithm m_toGray;
};
ChangePixelFormatCommand::ChangePixelFormatCommand()
@ -410,6 +450,7 @@ ChangePixelFormatCommand::ChangePixelFormatCommand()
m_useUI = true;
m_format = IMAGE_RGB;
m_dithering = render::Dithering();
m_toGray = gen::ToGrayAlgorithm::DEFAULT;
}
void ChangePixelFormatCommand::onLoadParams(const Params& params)
@ -418,7 +459,8 @@ void ChangePixelFormatCommand::onLoadParams(const Params& params)
std::string format = params.get("format");
if (format == "rgb") m_format = IMAGE_RGB;
else if (format == "grayscale") m_format = IMAGE_GRAYSCALE;
else if (format == "grayscale" ||
format == "gray") m_format = IMAGE_GRAYSCALE;
else if (format == "indexed") m_format = IMAGE_INDEXED;
else
m_useUI = true;
@ -454,6 +496,16 @@ void ChangePixelFormatCommand::onLoadParams(const Params& params)
// TODO object slicing here (from BayerMatrix -> DitheringMatrix)
m_dithering.matrix(render::BayerMatrix(8));
}
std::string toGray = params.get("toGray");
if (toGray == "luma")
m_toGray = gen::ToGrayAlgorithm::LUMA;
else if (dithering == "hsv")
m_toGray = gen::ToGrayAlgorithm::HSV;
else if (dithering == "hsl")
m_toGray = gen::ToGrayAlgorithm::HSL;
else
m_toGray = gen::ToGrayAlgorithm::DEFAULT;
}
bool ChangePixelFormatCommand::onEnabled(Context* context)
@ -518,6 +570,7 @@ void ChangePixelFormatCommand::onExecute(Context* context)
m_format = window.pixelFormat();
m_dithering = window.dithering();
m_toGray = window.toGray();
flatten = window.flattenEnabled();
window.saveDitheringOptions();
@ -554,6 +607,7 @@ void ChangePixelFormatCommand::onExecute(Context* context)
new cmd::SetPixelFormat(
sprite, m_format,
m_dithering,
get_gray_func(m_toGray),
&job)); // SpriteJob is a render::TaskDelegate
});
job.waitJob();

View File

@ -1179,7 +1179,8 @@ void DocExporter::renderTexture(Context* ctx,
sample.sprite(),
textureImage->pixelFormat(),
render::Dithering(),
nullptr) // TODO add a delegate to show progress
nullptr, // toGray is not needed because the texture is Indexed or RGB
nullptr) // TODO add a delegate to show progress
.execute(ctx);
}

View File

@ -770,12 +770,13 @@ private:
Image* oldImage = cel->image();
ImageRef newImage(
render::convert_pixel_format
(oldImage, NULL, IMAGE_RGB,
(oldImage, nullptr, IMAGE_RGB,
render::Dithering(),
nullptr,
m_sprite->palette(cel->frame()),
m_opaque,
m_bgIndex));
m_bgIndex,
nullptr));
m_sprite->replaceImage(oldImage->id(), newImage);
}

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019 Igara Studio S.A.
// Copyright (C) 2019-2020 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@ -162,6 +162,7 @@ FOR_ENUM(app::gen::StopAtGrid)
FOR_ENUM(app::gen::SymmetryMode)
FOR_ENUM(app::gen::TimelinePosition)
FOR_ENUM(app::gen::WindowColorProfile)
FOR_ENUM(app::gen::ToGrayAlgorithm)
FOR_ENUM(app::tools::FreehandAlgorithm)
FOR_ENUM(app::tools::InkType)
FOR_ENUM(app::tools::RotationAlgorithm)

View File

@ -1,4 +1,4 @@
Copyright (c) 2018-2019 Igara Studio S.A.
Copyright (c) 2018-2020 Igara Studio S.A.
Copyright (c) 2001-2018 David Capello
Permission is hereby granted, free of charge, to any person obtaining

View File

@ -1,4 +1,5 @@
// Aseprite Document Library
// Copyright (c) 2020 Igara Studio S.A.
// Copyright (c) 2001-2016 David Capello
//
// This file is released under the terms of the MIT license.
@ -10,6 +11,8 @@
#include "base/ints.h"
#include <algorithm>
namespace doc {
// The greatest int type to storage a color for an image in the
@ -87,6 +90,37 @@ namespace doc {
return graya(v, 255);
}
//////////////////////////////////////////////////////////////////////
// Conversions
typedef color_t (*rgba_to_graya_func)(const color_t c);
inline color_t rgba_to_graya_using_hsv(const color_t c) {
const uint8_t M = std::max(rgba_getr(c),
std::max(rgba_getg(c),
rgba_getb(c)));
return graya(M,
rgba_geta(c));
}
inline color_t rgba_to_graya_using_hsl(const color_t c) {
const int m = std::min(rgba_getr(c),
std::min(rgba_getg(c),
rgba_getb(c)));
const int M = std::max(rgba_getr(c),
std::max(rgba_getg(c),
rgba_getb(c)));
return graya((M + m) / 2,
rgba_geta(c));
}
inline color_t rgba_to_graya_using_luma(const color_t c) {
return graya(rgb_luma(rgba_getr(c),
rgba_getg(c),
rgba_getb(c)),
rgba_geta(c));
}
} // namespace doc
#endif

View File

@ -18,8 +18,6 @@
#include "doc/remap.h"
#include "doc/rgbmap.h"
#include "doc/sprite.h"
#include "gfx/hsv.h"
#include "gfx/rgb.h"
#include "render/dithering.h"
#include "render/error_diffusion.h"
#include "render/ordered_dither.h"
@ -90,6 +88,7 @@ Image* convert_pixel_format(
const Palette* palette,
bool is_background,
color_t new_mask_color,
rgba_to_graya_func toGray,
TaskDelegate* delegate)
{
if (!new_image)
@ -119,6 +118,14 @@ Image* convert_pixel_format(
return new_image;
}
// RGB/Indexed -> Gray
if ((image->pixelFormat() == IMAGE_RGB ||
image->pixelFormat() == IMAGE_INDEXED) &&
new_image->pixelFormat() == IMAGE_GRAYSCALE) {
if (!toGray)
toGray = &rgba_to_graya_using_luma;
}
color_t c;
int r, g, b, a;
@ -141,17 +148,12 @@ Image* convert_pixel_format(
LockImageBits<GrayscaleTraits>::iterator dst_it = dstBits.begin();
#ifdef _DEBUG
LockImageBits<GrayscaleTraits>::iterator dst_end = dstBits.end();
ASSERT(toGray);
#endif
for (; src_it != src_end; ++src_it, ++dst_it) {
ASSERT(dst_it != dst_end);
c = *src_it;
g = 255 * Hsv(Rgb(rgba_getr(c),
rgba_getg(c),
rgba_getb(c))).valueInt() / 100;
*dst_it = graya(g, rgba_geta(c));
*dst_it = (*toGray)(*src_it);
}
ASSERT(dst_it == dst_end);
break;
@ -280,6 +282,7 @@ Image* convert_pixel_format(
LockImageBits<GrayscaleTraits>::iterator dst_it = dstBits.begin();
#ifdef _DEBUG
LockImageBits<GrayscaleTraits>::iterator dst_end = dstBits.end();
ASSERT(toGray);
#endif
for (; src_it != src_end; ++src_it, ++dst_it) {
@ -290,13 +293,7 @@ Image* convert_pixel_format(
*dst_it = graya(0, 0);
else {
c = palette->getEntry(c);
r = rgba_getr(c);
g = rgba_getg(c);
b = rgba_getb(c);
a = rgba_geta(c);
g = 255 * Hsv(Rgb(r, g, b)).valueInt() / 100;
*dst_it = graya(g, a);
*dst_it = (*toGray)(c);
}
}
ASSERT(dst_it == dst_end);

View File

@ -1,5 +1,5 @@
// Aseprite Rener Library
// Copyright (c) 2019 Igara Studio S.A.
// Copyright (c) 2019-2020 Igara Studio S.A.
// Copyright (c) 2001-2017 David Capello
//
// This file is released under the terms of the MIT license.
@ -58,6 +58,7 @@ namespace render {
const doc::Palette* palette,
bool is_background,
doc::color_t new_mask_color,
doc::rgba_to_graya_func toGray = nullptr,
TaskDelegate* delegate = nullptr);
} // namespace render