//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' brigthness/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 = tmax(r, g, b); min = tmin(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; } }; //========================================================================= //! Birghtness/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; //========================================================================= //! Birghtness/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] = tmin(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 = tmin(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; image->getDpi(dpi.x, dpi.y); if (dpi == TPointD()) { dpi = getCustomDpi(); if (dpi == TPointD()) dpi.x = dpi.y = 65.0; //using 65.0 as default DPI } 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 * TConsts::pi_180); pre_aff.a21 = sin(skew * TConsts::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.(tipically 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(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(rin->getRawData()); TUINT32 *rowout = reinterpret_cast(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) { 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::MsgBox(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); } } CASE AUTO_ADJ_HISTO_L : histo_l_algo(image, first_image); CASE AUTO_ADJ_BLACK_EQ : black_eq_algo(image); CASE AUTO_ADJ_NONE : DEFAULT : assert(false); } } 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( , ...) we //should get the same effect!! TRasterP tmp_ras; if (returnResampled || (fromGr8 && toGr8)) { 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) //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(finalRas->getRawData()); for (int i = 0; i < pixCount; i++) *rowout++ = *rowin++; //Direct copy for now... :( } else { TPixel32 *rowin = reinterpret_cast(tmp_ras->getRawData()); TUINT32 *rowout = reinterpret_cast(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 * TConsts::pi_180); aff.a21 = sin(skew * TConsts::pi_180); } aff = aff * TRotation(angle); aff = aff.place(cxin, cyin, cpout, cqout); if (rotate != 0 && rotate != 180) tswap(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; CASE PEGS_RIGHT : sigma = 90.0; CASE PEGS_TOP : sigma = 180.0; CASE PEGS_LEFT : sigma = -90.0; DEFAULT: sigma = 0.0; } 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: __OR PEGS_RIGHT : notMoreThan(image->getRaster()->getLx(), strip_width); DEFAULT: notMoreThan(image->getRaster()->getLy(), strip_width); } 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 *= TConsts::invOf_pi_180; 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; CASE PEGS_TOP : cqout += dist; CASE PEGS_LEFT : cpout -= dist; CASE PEGS_RIGHT : cpout += dist; 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 &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::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) ? tmin(pixHSV.m_h - fColor.m_hsv.m_h, fColor.m_hsv.m_h - pixHSV.m_h + 360.0) : tmin(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 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; }