// Toonz includes
#include "tpixelutils.h"
#include "tpalette.h"
#include "tcolorstyles.h"
#include "timage_io.h"
#include "tropcm.h"
#include "ttile.h"
#include "toonz/toonzscene.h"
#include "toonz/tcamera.h"
#include "autoadjust.h"
#include "autopos.h"
#include "cleanuppalette.h"
#include "cleanupcommon.h"
#include "tmsgcore.h"
#include "toonz/cleanupparameters.h"
#include "toonz/tcleanupper.h"
using namespace CleanupTypes;
/* The Cleanup Process Reworked - EXPLANATION (by Daniele)
INTRODUCTION:
The purpose of a Cleanup Process is hereby intended as the task of
transforming
a fullcolor image (any bpp, matte supported*) into a TRasterCM32 -
that is, a colormap image - given an externally specified palette of ink
colors to
be recognized. No paint color is assumed at this stage.
Typically, artists draw outlines using a black or dark color, whereas
different hues are
used to mark lines that denote shadows or lights on characters.
Additional processing steps include the ability to recognize and counter any
linear
transformation in the image which is 'signaled' by the presence of (black)
pegbar holes,
so that the countering linear transformation maps those peg holes in the usual
centered
horizontal fashion.
Post-processing include despeckling (ie removal or recoloration of little
blots with
uniform color), and tones' brightness/contrast manipulation.
(*) The image is first overed on top of a white background
CONSTRAINTS:
We assume the following constraints throughout the process:
- Palette colors:
* Color 0 represents the PAPER color, and will just substitute it in the
colormap.
* Colors with index >= 2 are MATCH-LINE colors, and their HUE ONLY (the H in
HSV coordinates)
is essential to line recognition. The hue of image pixels is compared to
that of each
matchline color - and the nearest matchline color is associated to that
pixel.
If that associated matchline color is still too 'hue-distant' from the pixel
color
(beyond a user-specified parameter), the pixel is ignored (ie associated to
paper).
Furthermore, each matchline color also has a parameter corresponding to a
saturation
threshold; pixels whose color's saturation is below the threshold specified
by the
associated color are reassociated to the PAPER color.
* Color 1 represents the OUTLINE color, and its VALUE (the V in HSV
coordinates) is
assumed to be the image's lowest. Its H and S components are unused.
Pixels whose value is below this value + a user-defined threshold parameter
are
'outline-PRONE' pixels (even matchline-associated pixels can be).
They are assumed to be full outline pixels if their CHROMA (S*V) is above a
'Color threshold'. This condition lets the user settle how outline/matchline
disputed pixels should be considered.
- The Colormap tone for a pixel is extracted according to these rules:
* Paper pixels are completely transparent (tone = 255).
* Undisputed matchline colors build the tone upon the pixel's Saturation,
scaled
so that 1.0 maps to 0, and the saturation threshold for that matchline
color maps to 255.
* Undisputed Outline colors do similarly, with the Value.
* Disputed outline/matchline colors result in the blend PRODUCT of the above
tones.
This makes the tone smoother on outline/matchline intersections.
*/
//**************************************************************************************
// Local namespace stuff
//**************************************************************************************
namespace {
// some useful functions for doing math
inline double affMV1(const TAffine &aff, double v1, double v2) {
return aff.a11 * v1 + aff.a12 * v2 + aff.a13;
}
//-------------------------------------------------------------------------
inline double affMV2(const TAffine &aff, double v1, double v2) {
return aff.a21 * v1 + aff.a22 * v2 + aff.a23;
}
//=========================================================================
//! Auxiliary class for HSV (toonz 4.x style)
struct HSVColor {
double m_h;
double m_s;
double m_v;
public:
HSVColor(double h = 0, double s = 0, double v = 0) : m_h(h), m_s(s), m_v(v) {}
static HSVColor fromRGB(double r, double g, double b);
};
//-------------------------------------------------------------------------
HSVColor HSVColor::fromRGB(double r, double g, double b) {
double h, s, v;
double max, min, delta;
max = std::max({r, g, b});
min = std::min({r, g, b});
v = max;
if (max != 0)
s = (max - min) / max;
else
s = 0;
if (s == 0)
h = 0;
else {
delta = max - min;
if (r == max)
h = (g - b) / delta;
else if (g == max)
h = 2.0 + (b - r) / delta;
else if (b == max)
h = 4.0 + (r - g) / delta;
h = h * 60.0;
if (h < 0) h += 360.0;
}
return HSVColor(h, s, v);
}
//=========================================================================
//! Precomputation data about target colors
struct TargetColorData {
int m_idx; //!< Palette color index
HSVColor m_hsv; //!< HSV coordinates of the color
double m_saturationLower; //!< Pixel colors associated with this color must
//! be above this
double m_hueLower, m_hueUpper; //!< Pixel colors associated with this color
//! must in this range
public:
TargetColorData(const TargetColor &color)
: m_idx(-1)
, m_hsv(HSVColor::fromRGB(color.m_color.r / 255.0,
color.m_color.g / 255.0,
color.m_color.b / 255.0))
, m_saturationLower(1.0 - color.m_threshold / 100.0)
, m_hueLower(m_hsv.m_h - color.m_hRange * 0.5)
, m_hueUpper(m_hsv.m_h + color.m_hRange * 0.5) {
if (m_hueLower < 0.0) m_hueLower += 360.0;
if (m_hueUpper > 360.0) m_hueUpper -= 360.0;
}
};
//=========================================================================
//! Brightness/Contrast color transform data
#define MAX_N_PENCILS 8
/* the following must be updated at every change in palette content */
int N_pencils = 4; /* not counting autoclose */
TPixelRGBM32 Pencil[MAX_N_PENCILS + 1]; /* last is autoclose pencil */
int Pencil_index[MAX_N_PENCILS + 1]; /* "" */
int Pencil_id[MAX_N_PENCILS + 1]; /* "" */
TPixelRGBM32 Paper = TPixel32::White;
//=========================================================================
//! Brightness/Contrast color transform structure
class TransfFunction {
USHORT TransfFun[(MAX_N_PENCILS + 1) << 8];
void setTransfFun(int pencil, int b1, int c1) {
int i, p1, p2, brig, cont, max;
cont = 255 - c1;
brig = 255 - b1;
max = 255;
notLessThan(1, cont);
p2 = brig;
p1 = p2 - cont;
for (i = 0; i <= p1; i++) TransfFun[pencil << 8 | i] = 0;
for (; i < p2; i++)
TransfFun[pencil << 8 | i] = std::min(max, max * (i - p1) / cont);
for (; i < 256; i++) TransfFun[pencil << 8 | i] = max;
}
public:
TransfFunction(const TargetColors &colors) {
memset(TransfFun, 0, sizeof TransfFun);
int count = std::min(colors.getColorCount(), MAX_N_PENCILS);
for (int p = 0; p < count; p++) {
int brightness = troundp(2.55 * colors.getColor(p).m_brightness);
int contrast = troundp(2.55 * colors.getColor(p).m_contrast);
setTransfFun(p, brightness, contrast);
}
}
USHORT *getTransfFun() { return TransfFun; }
};
//=========================================================================
//! Brightness/Contrast functions
void brightnessContrast(const TRasterCM32P &cm, const TargetColors &colors) {
TransfFunction transform(colors);
USHORT *transf_fun = transform.getTransfFun();
int ink, tone;
int newTone, newInk;
for (int y = 0; y < cm->getLy(); ++y) {
TPixelCM32 *pix = cm->pixels(y);
TPixelCM32 *endPix = pix + cm->getLx();
for (; pix < endPix; ++pix) {
tone = pix->getTone();
if (tone < 255) {
ink = pix->getInk();
newTone = transf_fun[ink << 8 | tone];
newInk = (newTone == 255) ? 0 : colors.getColor(ink).m_index;
*pix = TPixelCM32(newInk, 0, newTone);
}
}
}
}
//------------------------------------------------------------------------------------
void brightnessContrastGR8(const TRasterCM32P &cm, const TargetColors &colors) {
TransfFunction transform(colors);
USHORT *transf_fun = transform.getTransfFun();
int val, black = colors.getColor(1).m_index;
for (int y = 0; y < cm->getLy(); ++y) {
TPixelCM32 *pix = cm->pixels(y);
TPixelCM32 *endPix = pix + cm->getLx();
for (; pix < endPix; ++pix) {
val = transf_fun[pix->getValue() + 256];
*pix = (val < 255) ? TPixelCM32(black, 0, val) : TPixelCM32();
}
}
}
//=========================================================================
//! Transparency check
void transparencyCheck(const TRasterCM32P &cmin, const TRaster32P &rasout) {
for (int y = 0; y < cmin->getLy(); ++y) {
TPixelCM32 *pix = cmin->pixels(y);
TPixelCM32 *endPix = pix + cmin->getLx();
TPixel32 *outPix = rasout->pixels(y);
for (; pix < endPix; ++pix, ++outPix) {
int ink = pix->getInk();
int tone = pix->getTone();
if (ink == 4095)
*outPix = TPixel32::Green;
else
*outPix = (tone == 0) ? TPixel32::Black
: (tone == 255) ? TPixel32::White : TPixel32::Red;
}
}
}
} // namespace
//**************************************************************************************
// TCleanupper implementation - elementary functions
//**************************************************************************************
TCleanupper *TCleanupper::instance() {
static TCleanupper theCleanupper;
return &theCleanupper;
}
//------------------------------------------------------------------------------------
void TCleanupper::setParameters(CleanupParameters *parameters) {
m_parameters = parameters;
}
//------------------------------------------------------------------------------------
TPalette *TCleanupper::createToonzPaletteFromCleanupPalette() {
TPalette *cleanupPalette = m_parameters->m_cleanupPalette.getPointer();
return createToonzPalette(cleanupPalette, 1);
}
//**************************************************************************************
// CleanupProcessedImage implementation
//**************************************************************************************
TToonzImageP CleanupPreprocessedImage::getImg() const {
return (TToonzImageP)(TImageCache::instance()->get(m_imgId, true));
}
//-----------------------------------------------------------------------------------
CleanupPreprocessedImage::CleanupPreprocessedImage(
CleanupParameters *parameters, TToonzImageP processed, bool fromGr8)
: m_wasFromGR8(fromGr8)
, m_autocentered(false)
, m_size(processed->getSize()) {
if (!processed)
m_imgId = "";
else {
m_imgId = TImageCache::instance()->getUniqueId();
assert(!processed->getRaster()->getParent());
TImageCache::instance()->add(m_imgId, (TImageP)processed);
}
if (!m_wasFromGR8) {
const TPixel32 white(255, 255, 255, 0);
for (int i = 0; i < parameters->m_colors.getColorCount(); ++i) {
TPixel32 cc = parameters->m_colors.getColor(i).m_color;
for (int tone = 0; tone < 256; tone++) {
m_pixelsLut.push_back(blend(parameters->m_colors.getColor(i).m_color,
white, tone, TPixelCM32::getMaxTone()));
}
}
}
}
//-----------------------------------------------------------------------------------
CleanupPreprocessedImage::~CleanupPreprocessedImage() {
TImageCache::instance()->remove(m_imgId);
}
//-----------------------------------------------------------------------------------
TRasterImageP CleanupPreprocessedImage::getPreviewImage() const {
TRaster32P ras(getSize());
TRasterImageP ri(ras);
double xdpi = 0, ydpi = 0;
getImg()->getDpi(xdpi, ydpi);
ri->setDpi(xdpi, ydpi);
return ri;
}
//**************************************************************************************
// TCleanupper implementation - Process functions
//**************************************************************************************
bool TCleanupper::getResampleValues(const TRasterImageP &image, TAffine &aff,
double &blur, TDimension &outDim,
TPointD &outDpi, bool isCameraTest,
bool &isSameDpi) {
double outlp, outlq;
double scalex, scaley;
double cxin, cyin, cpout, cqout;
double max_blur;
TPointD dpi;
// Locking the input image to be cleanupped
image->getRaster()->lock();
// Retrieve image infos
int rasterLx = image->getRaster()->getLx();
int rasterLy = image->getRaster()->getLy();
/*---入力画像サイズとSaveBoxのサイズが一致しているか?の判定---*/
TRect saveBox = image->getSavebox();
bool raster_is_savebox = true;
if (saveBox == TRect() &&
((saveBox.getLx() > 0 && saveBox.getLx() < rasterLx) ||
(saveBox.getLy() > 0 && saveBox.getLy() < rasterLy)))
raster_is_savebox = false;
// Use the same source dpi throughout the level
dpi = getSourceDpi();
if (dpi == TPointD())
dpi.x = dpi.y = 65.0; // using 65.0 as default DPI //??????WHY
else if (!dpi.x)
dpi.x = dpi.y;
else if (!dpi.y)
dpi.y = dpi.x;
// Retrieve some cleanup parameters
int rotate = m_parameters->m_rotate;
// Build scaling/dpi data
{
m_parameters->getOutputImageInfo(outDim, outDpi.x, outDpi.y);
// input -> output scale factor
scalex = outDpi.x / dpi.x;
scaley = outDpi.y / dpi.y;
outlp = outDim.lx;
outlq = outDim.ly;
}
/*---
* 拡大/縮小をしていない場合(DPIが変わらない場合)、NearestNeighborでリサンプリングする。---*/
isSameDpi = areAlmostEqual(outDpi.x, dpi.x, 0.1) &&
areAlmostEqual(outDpi.y, dpi.y, 0.1);
// Retrieve input center
if (raster_is_savebox) {
cxin = -saveBox.getP00().x + (saveBox.getLx() - 1) / 2.0;
cyin = -saveBox.getP00().y + (saveBox.getLy() - 1) / 2.0;
} else {
cxin = (rasterLx - 1) / 2.0;
cyin = (rasterLy - 1) / 2.0;
}
// Retrieve output center
cpout = (outlp - 1) / 2.0;
cqout = (outlq - 1) / 2.0;
// Perform autocenter if any is found
double angle = 0.0;
double skew = 0.0;
TAffine pre_aff;
image->getRaster()->lock();
bool autocentered =
doAutocenter(angle, skew, cxin, cyin, cqout, cpout, dpi.x, dpi.y,
raster_is_savebox, saveBox, image, scalex);
image->getRaster()->unlock();
// Build the image transform as deduced by the autocenter
if (m_parameters->m_autocenterType == AUTOCENTER_CTR && skew) {
pre_aff.a11 = cos(skew * M_PI_180);
pre_aff.a21 = sin(skew * M_PI_180);
}
aff = (TScale(scalex, scaley) * pre_aff) * TRotation(angle);
aff = aff.place(cxin, cyin, cpout, cqout);
// Apply eventual additional user-defined transforms
TPointD pout = TPointD((outlp - 1) / 2.0, (outlq - 1) / 2.0);
if (m_parameters->m_rotate != 0)
aff = TRotation(-(double)m_parameters->m_rotate).place(pout, pout) * aff;
if (m_parameters->m_flipx || m_parameters->m_flipy)
aff = TScale(m_parameters->m_flipx ? -1 : 1, m_parameters->m_flipy ? -1 : 1)
.place(pout, pout) *
aff;
if (!isCameraTest)
aff = TTranslation(m_parameters->m_offx * outDpi.x / 2,
m_parameters->m_offy * outDpi.y / 2) *
aff;
max_blur = 20.0 * sqrt(fabs(scalex /*** * oversample_factor ***/));
blur = pow(max_blur, (100 - m_parameters->m_sharpness) / (100 - 1));
return autocentered;
}
//------------------------------------------------------------------------------------
// this one incorporate the preprocessColors and the finalize function; used for
// swatch.(typically on very small rasters)
TRasterP TCleanupper::processColors(const TRasterP &rin) {
if (m_parameters->m_lineProcessingMode == lpNone) return rin;
TRasterCM32P rcm = TRasterCM32P(rin->getSize());
if (!rcm) {
assert(!"failed finalRas allocation!");
return TRasterCM32P();
}
// Copy current cleanup palette to parameters' colors
m_parameters->m_colors.update(m_parameters->m_cleanupPalette.getPointer(),
m_parameters->m_noAntialias);
bool toGr8 = (m_parameters->m_lineProcessingMode == lpGrey);
if (toGr8) {
// No (color) processing. Not even thresholding. This just means that all
// the important
// stuff here is made in the brightness/contrast stage...
// NOTE: Most of the color processing should be DISABLED in this case!!
// finalRas->clear();
rin->lock();
rcm->lock();
if (TRasterGR8P(rin)) {
UCHAR *rowin = rin->getRawData();
TUINT32 *rowout = reinterpret_cast<TUINT32 *>(rcm->getRawData());
for (int i = 0; i < rin->getLy(); i++) {
for (int j = 0; j < rin->getLx(); j++)
*rowout++ = *rowin++; // Direct copy for now... :(
rowin += rin->getWrap() - rin->getLx();
rowout += rcm->getWrap() - rcm->getLx();
}
} else {
TPixel32 *rowin = reinterpret_cast<TPixel32 *>(rin->getRawData());
TUINT32 *rowout = reinterpret_cast<TUINT32 *>(rcm->getRawData());
for (int i = 0; i < rin->getLy(); i++) {
for (int j = 0; j < rin->getLx(); j++)
*rowout++ = TPixelGR8::from(*rowin++).value;
rowin += rin->getWrap() - rin->getLx();
rowout += rcm->getWrap() - rcm->getLx();
}
}
rin->unlock();
rcm->unlock();
} else {
assert(TRaster32P(rin));
preprocessColors(rcm, rin, m_parameters->m_colors);
}
// outImg->setDpi(outDpi.x, outDpi.y);
CleanupPreprocessedImage cpi(m_parameters,
TToonzImageP(rcm, rcm->getBounds()), toGr8);
cpi.m_autocentered = true;
TRaster32P rout = TRaster32P(rin->getSize());
finalize(rout, &cpi);
return rout;
}
//------------------------------------------------------------------------------------
CleanupPreprocessedImage *TCleanupper::process(
TRasterImageP &image, bool first_image, TRasterImageP &onlyResampledImage,
bool isCameraTest, bool returnResampled, bool onlyForSwatch,
TAffine *resampleAff, TRasterP templateForResampled) {
TAffine aff;
double blur;
TDimension outDim(0, 0);
TPointD outDpi;
bool isSameDpi = false;
bool autocentered = getResampleValues(image, aff, blur, outDim, outDpi,
isCameraTest, isSameDpi);
if (m_parameters->m_autocenterType != AUTOCENTER_NONE && !autocentered)
DVGui::warning(
QObject::tr("The autocentering failed on the current drawing."));
bool fromGr8 = (bool)TRasterGR8P(image->getRaster());
bool toGr8 = (m_parameters->m_lineProcessingMode == lpGrey);
// If necessary, perform auto-adjust
if (!isCameraTest && m_parameters->m_lineProcessingMode != lpNone && toGr8 &&
m_parameters->m_autoAdjustMode != AUTO_ADJ_NONE && !onlyForSwatch) {
static int ref_cum[256];
UCHAR lut[256];
int cum[256];
double x0_src_f, y0_src_f, x1_src_f, y1_src_f;
int x0_src, y0_src, x1_src, y1_src;
// cleanup_message("Autoadjusting... \n");
TAffine inv = aff.inv();
x0_src_f = affMV1(inv, 0, 0);
y0_src_f = affMV2(inv, 0, 0);
x1_src_f = affMV1(inv, outDim.lx - 1, outDim.ly - 1);
y1_src_f = affMV2(inv, outDim.lx - 1, outDim.ly - 1);
x0_src = tround(x0_src_f);
y0_src = tround(y0_src_f);
x1_src = tround(x1_src_f);
y1_src = tround(y1_src_f);
set_autoadjust_window(x0_src, y0_src, x1_src, y1_src);
if (!TRasterGR8P(image->getRaster())) {
// Auto-adjusting a 32-bit image. This means that a white background must
// be introduced first.
TRaster32P ras32(image->getRaster()->clone());
TRop::addBackground(ras32, TPixel32::White);
image = TRasterImageP(ras32); // old image is released here
ras32 = TRaster32P();
TRasterGR8P rgr(image->getRaster()->getSize());
TRop::copy(rgr, image->getRaster());
// This is now legit. It was NOT before the clone, since the original
// could be cached.
image->setRaster(rgr);
}
switch (m_parameters->m_autoAdjustMode) {
case AUTO_ADJ_HISTOGRAM:
if (first_image) {
build_gr_cum(image, ref_cum);
} else {
build_gr_cum(image, cum);
build_gr_lut(ref_cum, cum, lut);
apply_lut(image, lut);
}
break;
case AUTO_ADJ_HISTO_L:
histo_l_algo(image, first_image);
break;
case AUTO_ADJ_BLACK_EQ:
black_eq_algo(image);
break;
case AUTO_ADJ_NONE:
default:
assert(false);
break;
}
}
fromGr8 = (bool)TRasterGR8P(
image->getRaster()); // may have changed type due to auto-adjust
assert(returnResampled ||
!onlyForSwatch); // if onlyForSwatch, then returnResampled
// Allocate output colormap raster
TRasterCM32P finalRas;
if (!onlyForSwatch) {
finalRas = TRasterCM32P(outDim);
if (!finalRas) {
TImageCache::instance()->outputMap(outDim.lx * outDim.ly * 4,
"C:\\cachelog");
assert(!"failed finalRas allocation!");
return 0;
}
}
// In case the input raster was a greymap, we cannot reutilize finalRas's
// buffer to transform the final
// fullcolor pixels to colormap pixels directly (1 32-bit pixel would hold 4
// 8-bit pixels) - therefore,
// a secondary greymap is allocated.
// NOTE: This should be considered obsolete? By using TRop::resample(
// <TRaster32P& instance> , ...) we
// should get the same effect!!
TRasterP tmp_ras;
if (returnResampled || (fromGr8 && toGr8)) {
if (templateForResampled)
tmp_ras = templateForResampled->create(outDim.lx, outDim.ly);
else if (fromGr8 && toGr8)
tmp_ras = TRasterGR8P(outDim);
else
tmp_ras = TRaster32P(outDim);
if (!tmp_ras) {
TImageCache::instance()->outputMap(outDim.lx * outDim.ly * 4,
"C:\\cachelog");
assert(!"failed tmp_ras allocation!");
return 0;
}
} else
// if finalRas is allocated, and the intermediate raster has to be 32-bit,
// we can perform pixel
// conversion directly on the same output buffer
tmp_ras = TRaster32P(outDim.lx, outDim.ly, outDim.lx,
(TPixel32 *)finalRas->getRawData());
TRop::ResampleFilterType flt_type;
if (isSameDpi)
flt_type = TRop::ClosestPixel; // NearestNeighbor
else if (isCameraTest)
flt_type = TRop::Triangle;
else
flt_type = TRop::Hann2;
TRop::resample(tmp_ras, image->getRaster(), aff, flt_type, blur);
if ((TRaster32P)tmp_ras && !templateForResampled)
// Add white background to deal with semitransparent pixels
TRop::addBackground(tmp_ras, TPixel32::White);
if (resampleAff) *resampleAff = aff;
image->getRaster()->unlock();
image = TRasterImageP();
if (returnResampled) {
onlyResampledImage = TRasterImageP(tmp_ras);
onlyResampledImage->setDpi(outDpi.x, outDpi.y);
}
if (onlyForSwatch) return 0;
assert(finalRas);
// Copy current cleanup palette to parameters' colors
m_parameters->m_colors.update(m_parameters->m_cleanupPalette.getPointer(),
m_parameters->m_noAntialias);
if (toGr8) {
// No (color) processing. Not even thresholding. This just means that all
// the important
// stuff here is made in the brightness/contrast stage...
// NOTE: Most of the color processing should be DISABLED in this case!!
tmp_ras->lock();
finalRas->lock();
assert(tmp_ras->getSize() == finalRas->getSize());
assert(tmp_ras->getLx() == tmp_ras->getWrap());
assert(finalRas->getLx() == finalRas->getWrap());
int pixCount = outDim.lx * outDim.ly;
if (fromGr8) {
UCHAR *rowin = tmp_ras->getRawData();
TUINT32 *rowout = reinterpret_cast<TUINT32 *>(finalRas->getRawData());
for (int i = 0; i < pixCount; i++)
*rowout++ = *rowin++; // Direct copy for now... :(
} else {
TPixel32 *rowin = reinterpret_cast<TPixel32 *>(tmp_ras->getRawData());
TUINT32 *rowout = reinterpret_cast<TUINT32 *>(finalRas->getRawData());
for (int i = 0; i < pixCount; i++)
*rowout++ = TPixelGR8::from(*rowin++).value;
}
tmp_ras->unlock();
finalRas->unlock();
} else {
// WARNING: finalRas and tmp_ras may share the SAME buffer!
assert(TRaster32P(tmp_ras));
preprocessColors(finalRas, tmp_ras, m_parameters->m_colors);
}
TToonzImageP final;
final = TToonzImageP(finalRas, finalRas->getBounds());
final->setDpi(outDpi.x, outDpi.y);
CleanupPreprocessedImage *cpi =
new CleanupPreprocessedImage(m_parameters, final, toGr8);
cpi->m_autocentered = autocentered;
cpi->m_appliedAff = aff;
return cpi;
}
//------------------------------------------------------------------------------------
TRasterImageP TCleanupper::autocenterOnly(const TRasterImageP &image,
bool isCameraTest,
bool &autocentered) {
double xDpi, yDpi;
// double inlx, inly, zoom_factor, max_blur;
double skew = 0, angle = 0,
dist = 0 /*lq_nozoom, lp_nozoom,, cx, cy, scalex, scaley*/;
double cxin, cyin, cpout, cqout;
int rasterIsSavebox = true;
TAffine aff, preAff, inv;
int rasterLx, finalLx, rasterLy, finalLy;
rasterLx = finalLx = image->getRaster()->getLx();
rasterLy = finalLy = image->getRaster()->getLy();
TRect saveBox = image->getSavebox();
if ((saveBox == TRect()) &&
((saveBox.getLx() > 0 && saveBox.getLx() < rasterLx) ||
(saveBox.getLy() > 0 && saveBox.getLy() < rasterLy)))
rasterIsSavebox = false;
int rotate = m_parameters->m_rotate;
image->getDpi(xDpi, yDpi);
if (!xDpi) // using 65.0 as default DPI
xDpi = (yDpi ? yDpi : 65);
if (!yDpi) yDpi = (xDpi ? xDpi : 65);
if (rasterIsSavebox) {
cxin = -saveBox.getP00().x + (saveBox.getLx() - 1) / 2.0;
cyin = -saveBox.getP00().y + (saveBox.getLy() - 1) / 2.0;
} else {
cxin = (rasterLx - 1) / 2.0;
cyin = (rasterLy - 1) / 2.0;
}
cpout = (rasterLx - 1) / 2.0;
cqout = (rasterLy - 1) / 2.0;
if (m_parameters->m_autocenterType != AUTOCENTER_NONE)
autocentered = doAutocenter(angle, skew, cxin, cyin, cqout, cpout, xDpi,
yDpi, rasterIsSavebox, saveBox, image, 1.0);
else
autocentered = true;
if (m_parameters->m_autocenterType == AUTOCENTER_CTR && skew) {
aff.a11 = cos(skew * M_PI_180);
aff.a21 = sin(skew * M_PI_180);
}
aff = aff * TRotation(angle);
aff = aff.place(cxin, cyin, cpout, cqout);
if (rotate != 0 && rotate != 180) std::swap(finalLx, finalLy);
TPointD pin = TPointD((rasterLx - 1) / 2.0, (rasterLy - 1) / 2.0);
TPointD pout = TPointD((finalLx - 1) / 2.0, (finalLy - 1) / 2.0);
if (rotate != 0) aff = TRotation(-(double)rotate).place(pin, pout) * aff;
if (m_parameters->m_flipx || m_parameters->m_flipy)
aff = TScale(m_parameters->m_flipx ? -1 : 1, m_parameters->m_flipy ? -1 : 1)
.place(pout, pout) *
aff;
if (!isCameraTest)
aff = TTranslation(m_parameters->m_offx * xDpi / 2,
m_parameters->m_offy * yDpi / 2) *
aff;
TRasterP tmpRas;
TPoint dp;
if (isCameraTest) // in cameratest, I don't want to crop the image to be
// shown.
// so, I resample without cropping, and I compute the offset needed to have it
// autocentered.
// That offset is stored in the RasterImage(setOffset below) and then used
// when displaying the image in camerastand (in method
// RasterPainter::onRasterImage)
{
// TPointD srcActualCenter = aff.inv()*TPointD(finalLx/2.0, finalLy/2.0);//
// the autocenter position in the source image
// TPointD srcCenter = imageToResample->getRaster()->getCenterD();*/
TPointD dstActualCenter = TPointD(finalLx / 2.0, finalLy / 2.0);
TPointD dstCenter = aff * image->getRaster()->getCenterD();
dp = convert(
dstCenter -
dstActualCenter); // the amount to be offset in the destination image.
TRect r = convert(aff * convert(image->getRaster()->getBounds()));
aff = (TTranslation(convert(-r.getP00())) * aff);
// aff = aff.place(srcActualCenter, dstActualCenter);
tmpRas = image->getRaster()->create(r.getLx(), r.getLy());
} else
tmpRas = image->getRaster()->create(finalLx, finalLy);
TRop::resample(tmpRas, image->getRaster(), aff);
// TImageWriter::save(TFilePath("C:\\temp\\incleanup.tif"), imageToResample);
// TImageWriter::save(TFilePath("C:\\temp\\outcleanup.tif"), tmp_ras);
TRasterImageP final(tmpRas);
final->setOffset(dp);
final->setDpi(xDpi, yDpi);
// final->sethPos(finalHPos);
return final;
}
//**************************************************************************************
// AutoCenter
//**************************************************************************************
bool TCleanupper::doAutocenter(
double &angle, double &skew, double &cxin, double &cyin, double &cqout,
double &cpout,
const double xdpi, const double ydpi, const int raster_is_savebox,
const TRect saveBox, const TRasterImageP &image, const double scalex) {
double sigma = 0, theta = 0;
FDG_INFO fdg_info = m_parameters->getFdgInfo();
switch (m_parameters->m_autocenterType) {
case AUTOCENTER_CTR:
angle = fdg_info.ctr_angle;
skew = fdg_info.ctr_skew;
cxin = mmToPixel(fdg_info.ctr_x, xdpi);
cyin = mmToPixel(fdg_info.ctr_y, ydpi);
if (raster_is_savebox) {
cxin -= saveBox.getP00().x;
cyin -= saveBox.getP00().y;
}
break;
case AUTOCENTER_FDG: {
// e se image->raster_is_savebox?
// cleanup_message ("Autocentering...");
int strip_width = compute_strip_pixel(&fdg_info, xdpi) + 1; /* ?!? */
switch (m_parameters->m_pegSide) {
case PEGS_BOTTOM:
sigma = 0.0;
break;
case PEGS_RIGHT:
sigma = 90.0;
break;
case PEGS_TOP:
sigma = 180.0;
break;
case PEGS_LEFT:
sigma = -90.0;
break;
default:
sigma = 0.0;
break;
}
theta = sigma;
if (theta > 180.0)
theta -= 360.0;
else if (theta <= -180.0)
theta += 360.0;
PEGS_SIDE pegs_ras_side;
if (theta == 0.0)
pegs_ras_side = PEGS_BOTTOM;
else if (theta == 90.0)
pegs_ras_side = PEGS_RIGHT;
else if (theta == 180.0)
pegs_ras_side = PEGS_TOP;
else if (theta == -90.0)
pegs_ras_side = PEGS_LEFT;
else
pegs_ras_side = PEGS_BOTTOM;
switch (pegs_ras_side) {
case PEGS_LEFT:
case PEGS_RIGHT:
notMoreThan(image->getRaster()->getLx(), strip_width);
break;
default:
notMoreThan(image->getRaster()->getLy(), strip_width);
break;
}
convert_dots_mm_to_pixel(&fdg_info.dots[0], fdg_info.dots.size(), xdpi,
ydpi);
double cx, cy;
if (!get_image_rotation_and_center(
image->getRaster(), strip_width, pegs_ras_side, &angle, &cx, &cy,
&fdg_info.dots[0], fdg_info.dots.size())) {
return false;
} else {
angle *= M_180_PI;
cxin = cx;
cyin = cy;
double dist =
(double)mmToPixel(fdg_info.dist_ctr_to_ctr_hole, xdpi * scalex);
switch (m_parameters->m_pegSide) {
case PEGS_BOTTOM:
cqout -= dist;
break;
case PEGS_TOP:
cqout += dist;
break;
case PEGS_LEFT:
cpout -= dist;
break;
case PEGS_RIGHT:
cpout += dist;
break;
default:
// bad pegs side
return false;
}
}
fdg_info.dots.clear();
break;
}
default:
return false;
}
return true;
}
//**************************************************************************************
// (Pre) Processing (ie the core Cleanup procedure)
//**************************************************************************************
inline void preprocessColor(const TPixel32 &pix,
const TargetColorData &blackColor,
const std::vector<TargetColorData> &featureColors,
int nFeatures, TPixelCM32 &outpix) {
// Translate the pixel to HSV
HSVColor pixHSV(
HSVColor::fromRGB(pix.r / 255.0, pix.g / 255.0, pix.b / 255.0));
// First, check against matchline colors. This is needed as outline pixels'
// tone is based upon that
// extracted here.
int idx = -1, tone = 255;
double hDist = (std::numeric_limits<double>::max)(), newHDist;
for (int i = 0; i < nFeatures; ++i) {
const TargetColorData &fColor = featureColors[i];
// Feature Color
// Retrieve the hue distance and, in case it's less than current one, this
// idx better
// approximates the color.
newHDist = (pixHSV.m_h > fColor.m_hsv.m_h)
? std::min(pixHSV.m_h - fColor.m_hsv.m_h,
fColor.m_hsv.m_h - pixHSV.m_h + 360.0)
: std::min(fColor.m_hsv.m_h - pixHSV.m_h,
pixHSV.m_h - fColor.m_hsv.m_h + 360.0);
if (newHDist < hDist) {
hDist = newHDist;
idx = i;
}
}
if (idx >= 0) {
const TargetColorData &fColor = featureColors[idx];
// First, perform saturation check
bool saturationOk = (pixHSV.m_s > fColor.m_saturationLower) &&
((fColor.m_hueLower <= fColor.m_hueUpper)
? (pixHSV.m_h >= fColor.m_hueLower) &&
(pixHSV.m_h <= fColor.m_hueUpper)
: (pixHSV.m_h >= fColor.m_hueLower) ||
(pixHSV.m_h <= fColor.m_hueUpper));
if (saturationOk) {
tone = 255.0 * (1.0 - pixHSV.m_s) / (1.0 - fColor.m_saturationLower);
idx = fColor.m_idx;
} else
idx = -1;
}
// Check against outline color
if (pixHSV.m_v < blackColor.m_hsv.m_v) {
// Outline-sensitive tone is imposed when the value check passes
tone = (tone * pixHSV.m_v / blackColor.m_hsv.m_v);
// A further Chroma test is applied to decide whether a would-be outline
// color
// is to be intended as a matchline color instead (it has too much color)
if ((idx < 0) || (pixHSV.m_s * pixHSV.m_v) < blackColor.m_saturationLower)
// Outline Color
idx = 1;
}
outpix = (idx > 0 && tone < 255) ? TPixelCM32(idx, 0, tone) : TPixelCM32();
}
//-----------------------------------------------------------------------------------------
void TCleanupper::preprocessColors(const TRasterCM32P &outRas,
const TRaster32P &raster32,
const TargetColors &colors) {
assert(outRas && outRas->getSize() == raster32->getSize());
// Convert the target palette to HSV colorspace
std::vector<TargetColorData> pencilsHSV;
for (int i = 2; i < colors.getColorCount(); ++i) {
TargetColorData cdata(colors.getColor(i));
cdata.m_idx = i;
pencilsHSV.push_back(cdata);
}
// Extract the 'black' Value
TargetColor black = colors.getColor(1);
TargetColorData blackData(black);
blackData.m_hsv.m_v += (1.0 - black.m_threshold / 100.0);
blackData.m_saturationLower = sq(1.0 - black.m_hRange / 100.0);
raster32->lock();
outRas->lock();
// For every image pixel, process it
for (int j = 0; j < raster32->getLy(); j++) {
TPixel32 *pix = raster32->pixels(j);
TPixel32 *endPix = pix + raster32->getLx();
TPixelCM32 *outPix = outRas->pixels(j);
while (pix < endPix) {
if (*pix == TPixel32::White ||
pix->m <
255) // sometimes the resampling produces semitransparent pixels
// on the border of the raster; I discards those pixels.
//(which otherwise creates a black border in the final cleanupped image)
// vinz
*outPix = TPixelCM32();
else
preprocessColor(*pix, blackData, pencilsHSV, pencilsHSV.size(),
*outPix);
pix++;
outPix++;
}
}
raster32->unlock();
outRas->unlock();
}
//**************************************************************************************
// Post-Processing
//**************************************************************************************
void TCleanupper::finalize(const TRaster32P &outRas,
CleanupPreprocessedImage *srcImg) {
if (!outRas) return;
if (srcImg->m_wasFromGR8)
doPostProcessingGR8(outRas, srcImg);
else
doPostProcessingColor(outRas, srcImg);
}
//-----------------------------------------------------------------------------------------
TToonzImageP TCleanupper::finalize(CleanupPreprocessedImage *src,
bool isCleanupper) {
if (src->m_wasFromGR8)
return doPostProcessingGR8(src);
else
return doPostProcessingColor(src->getImg(), isCleanupper);
}
//-----------------------------------------------------------------------------------------
void TCleanupper::doPostProcessingGR8(const TRaster32P &outRas,
CleanupPreprocessedImage *srcImg) {
TToonzImageP image = srcImg->getImg();
TRasterCM32P rasCM32 = image->getRaster();
rasCM32->lock();
outRas->lock();
TRasterCM32P cmout(outRas->getLx(), outRas->getLy(), outRas->getWrap(),
(TPixelCM32 *)outRas->getRawData());
TRop::copy(cmout, rasCM32);
rasCM32->unlock();
// Apply brightness/contrast and grayscale conversion directly
brightnessContrastGR8(cmout, m_parameters->m_colors);
// Apply despeckling
if (m_parameters->m_despeckling)
TRop::despeckle(cmout, m_parameters->m_despeckling,
m_parameters->m_transparencyCheckEnabled);
// Morphological antialiasing
if (m_parameters->m_postAntialias) {
TRasterCM32P newRas(cmout->getLx(), cmout->getLy());
TRop::antialias(cmout, newRas, 10, m_parameters->m_aaValue);
cmout->unlock();
cmout = newRas;
cmout->lock();
}
// Finally, do transparency check
if (m_parameters->m_transparencyCheckEnabled)
transparencyCheck(cmout, outRas);
else
// TRop::convert(outRas, cmout, m_parameters->m_cleanupPalette);
TRop::convert(outRas, cmout, createToonzPaletteFromCleanupPalette());
outRas->unlock();
}
//-----------------------------------------------------------------------------------------
TToonzImageP TCleanupper::doPostProcessingGR8(
const CleanupPreprocessedImage *img) {
TToonzImageP image = img->getImg();
TRasterCM32P rasCM32 = image->getRaster();
TRasterCM32P cmout(rasCM32->clone());
cmout->lock();
// Apply brightness/contrast and grayscale conversion directly
brightnessContrastGR8(cmout, m_parameters->m_colors);
// Apply despeckling
if (m_parameters->m_despeckling)
TRop::despeckle(cmout, m_parameters->m_despeckling, false);
// Morphological antialiasing
if (m_parameters->m_postAntialias) {
TRasterCM32P newRas(cmout->getLx(), cmout->getLy());
TRop::antialias(cmout, newRas, 10, m_parameters->m_aaValue);
cmout->unlock();
cmout = newRas;
cmout->lock();
}
cmout->unlock();
// Rebuild the cmap's bbox
TRect bbox;
TRop::computeBBox(cmout, bbox);
// Copy the dpi
TToonzImageP outImg(cmout, bbox);
double dpix, dpiy;
image->getDpi(dpix, dpiy);
outImg->setDpi(dpix, dpiy);
return outImg;
}
//-----------------------------------------------------------------------------------------
void TCleanupper::doPostProcessingColor(const TRaster32P &outRas,
CleanupPreprocessedImage *srcImg) {
assert(srcImg);
assert(outRas->getSize() == srcImg->getSize());
TToonzImageP imgToProcess = srcImg->getImg();
TRasterCM32P rasCM32 = imgToProcess->getRaster();
rasCM32->lock();
outRas->lock();
TRasterCM32P cmout(outRas->getLx(), outRas->getLy(), outRas->getWrap(),
(TPixelCM32 *)outRas->getRawData());
TRop::copy(cmout, rasCM32);
rasCM32->unlock();
// First, deal with brightness/contrast
brightnessContrast(cmout, m_parameters->m_colors);
// Then, apply despeckling
if (m_parameters->m_despeckling)
TRop::despeckle(cmout, m_parameters->m_despeckling,
m_parameters->m_transparencyCheckEnabled);
// Morphological antialiasing
if (m_parameters->m_postAntialias) {
TRasterCM32P newRas(cmout->getLx(), cmout->getLy());
TRop::antialias(cmout, newRas, 10, m_parameters->m_aaValue);
cmout->unlock();
cmout = newRas;
cmout->lock();
}
// Finally, do transparency check
if (m_parameters->m_transparencyCheckEnabled)
transparencyCheck(cmout, outRas);
else
// TRop::convert(outRas, cmout, m_parameters->m_cleanupPalette);
TRop::convert(outRas, cmout, createToonzPaletteFromCleanupPalette());
outRas->unlock();
}
//------------------------------------------------------------------------------
TToonzImageP TCleanupper::doPostProcessingColor(
const TToonzImageP &imgToProcess, bool isCleanupper) {
//(Build and) Copy imgToProcess to output image
TToonzImageP outImage;
if (isCleanupper)
outImage = imgToProcess;
else
outImage = TToonzImageP(imgToProcess->cloneImage());
assert(outImage);
assert(m_parameters->m_colors.getColorCount() < 9);
// Perform post-processing
TRasterCM32P outRasCM32 = outImage->getRaster();
outRasCM32->lock();
// Brightness/Contrast
brightnessContrast(outRasCM32, m_parameters->m_colors);
// Despeckling
if (m_parameters->m_despeckling)
TRop::despeckle(outRasCM32, m_parameters->m_despeckling, false);
// Morphological antialiasing
if (m_parameters->m_postAntialias) {
TRasterCM32P newRas(outRasCM32->getLx(), outRasCM32->getLy());
TRop::antialias(outRasCM32, newRas, 10, m_parameters->m_aaValue);
outRasCM32->unlock();
outRasCM32 = newRas;
outImage->setCMapped(outRasCM32);
outRasCM32->lock();
}
TRect bbox;
TRop::computeBBox(outRasCM32, bbox);
outImage->setSavebox(bbox);
outRasCM32->unlock();
return outImage;
}