#include "blend.h" // TPoint structure #include "tgeometry.h" // Palette - pixel functions #include "tpalette.h" #include "tpixelutils.h" #include #include //================================================================================= //=========================== // Blur pattern class //--------------------------- //! The BlurPattern class delineates the idea of a 'blur' //! pattern from a number of random sample points taken //! in a neighbourhood of the blurred pixel. The pattern //! develops in a radial manner if specified, so that possible //! 'obstacles' in the blur can be identified. class BlurPattern { public: typedef std::vector SamplePath; std::vector m_samples; std::vector m_samplePaths; BlurPattern(double distance, unsigned int samplesCount, bool radial); ~BlurPattern() {} }; //--------------------------------------------------------------------------------- // Builds the specified number of samples count, inside the specified distance // from the origin. If the pattern is radial, paths to the samples points are // calculated. BlurPattern::BlurPattern(double distance, unsigned int samplesCount, bool radial) { const double randFactor = 2.0 * distance / RAND_MAX; m_samples.resize(samplesCount); // Build the samples unsigned int i; for (i = 0; i < samplesCount; ++i) { // NOTE: The following method ensures a perfectly flat probability // distribution. TPoint candidatePoint(tround(rand() * randFactor - distance), tround(rand() * randFactor - distance)); double distanceSq = sq(distance); while (sq(candidatePoint.x) + sq(candidatePoint.y) > distanceSq) candidatePoint = TPoint(tround(rand() * randFactor - distance), tround(rand() * randFactor - distance)); m_samples[i] = candidatePoint; } m_samplePaths.resize(samplesCount); // If necessary, build the paths if (radial) { for (i = 0; i < samplesCount; ++i) { TPoint &sample = m_samples[i]; int l = std::max(abs(sample.x), abs(sample.y)); m_samplePaths[i].reserve(l); double dx = sample.x / (double)l; double dy = sample.y / (double)l; double x, y; int j; for (j = 0, x = dx, y = dy; j < l; x += dx, y += dy, ++j) m_samplePaths[i].push_back(TPoint(tround(x), tround(y))); } } } //================================================================================= //================================= // Raster Selection classes //--------------------------------- struct SelectionData { UCHAR m_selectedInk : 1; UCHAR m_selectedPaint : 1; UCHAR m_pureInk : 1; UCHAR m_purePaint : 1; }; //================================================================================= // Implements an array of selection infos using bitfields. It seems that // bitfields are more optimized than // using raw bits and bitwise operators, and use just the double of the space // required with bit arrays. class SelectionArrayPtr { std::unique_ptr m_buffer; public: inline void allocate(unsigned int count) { m_buffer.reset(new SelectionData[count]); memset(m_buffer.get(), 0, count * sizeof(SelectionData)); } inline void destroy() { m_buffer.reset(); } inline SelectionData *data() const { return m_buffer.get(); } inline SelectionData *data() { return m_buffer.get(); } }; //================================================================================= // Bitmap used to store blend color selections and pure color informations. class SelectionRaster { SelectionArrayPtr m_selection; int m_wrap; public: SelectionRaster(TRasterCM32P cm); void updateSelection(TRasterCM32P cm, const BlendParam ¶m); SelectionData *data() const { return m_selection.data(); } SelectionData *data() { return m_selection.data(); } void destroy() { m_selection.destroy(); } bool isSelectedInk(int xy) const { return (m_selection.data() + xy)->m_selectedInk; } bool isSelectedInk(int x, int y) const { return isSelectedInk(x + y * m_wrap); } bool isSelectedPaint(int xy) const { return (m_selection.data() + xy)->m_selectedPaint; } bool isSelectedPaint(int x, int y) const { return isSelectedPaint(x + y * m_wrap); } bool isPureInk(int xy) const { return (m_selection.data() + xy)->m_pureInk; } bool isPureInk(int x, int y) const { return isPureInk(x + y * m_wrap); } bool isPurePaint(int xy) const { return (m_selection.data() + xy)->m_purePaint; } bool isPurePaint(int x, int y) const { return isPurePaint(x + y * m_wrap); } bool isToneColor(int xy) const { return !(isPureInk(xy) || isPurePaint(xy)); } bool isToneColor(int x, int y) const { return isToneColor(x + y * m_wrap); } }; //--------------------------------------------------------------------------------- inline UCHAR linearSearch(const int *v, unsigned int vSize, int k) { const int *vEnd = v + vSize; for (; v < vEnd; ++v) if (*v == k) return 1; return 0; } //--------------------------------------------------------------------------------- // I've seen the std::binary_search go particularly slow... perhaps it was the // debug mode, // but I guess this is the fastest version possible. inline UCHAR binarySearch(const int *v, unsigned int vSize, int k) { // NOTE: v.size() > 0 due to external restrictions. See SelectionRaster's // constructor. int a = -1, b, c = vSize; for (b = c >> 1; b != a; b = (a + c) >> 1) { if (v[b] == k) return 1; else if (k < v[b]) c = b; else a = b; } return 0; } //--------------------------------------------------------------------------------- SelectionRaster::SelectionRaster(TRasterCM32P cm) { unsigned int lx = cm->getLx(), ly = cm->getLy(), wrap = cm->getWrap(); unsigned int size = lx * ly; m_wrap = lx; m_selection.allocate(size); cm->lock(); TPixelCM32 *pix, *pixBegin = (TPixelCM32 *)cm->getRawData(); SelectionData *selData = data(); unsigned int i, j; for (i = 0; i < ly; ++i) { pix = pixBegin + i * wrap; for (j = 0; j < lx; ++j, ++pix, ++selData) { selData->m_pureInk = pix->getTone() == 0; selData->m_purePaint = pix->getTone() == 255; } } cm->unlock(); } //--------------------------------------------------------------------------------- void SelectionRaster::updateSelection(TRasterCM32P cm, const BlendParam ¶m) { // Make a hard copy of color indexes. We do so since we absolutely prefer // having them SORTED! std::vector cIndexes = param.colorsIndexes; std::sort(cIndexes.begin(), cIndexes.end()); unsigned int lx = cm->getLx(), ly = cm->getLy(), wrap = cm->getWrap(); // Scan each cm pixel, looking if its ink or paint is in param's colorIndexes. cm->lock(); TPixelCM32 *pix, *pixBegin = (TPixelCM32 *)cm->getRawData(); SelectionData *selData = data(); const int *v = &cIndexes[0]; // NOTE: cIndexes.size() > 0 due to external check. unsigned int vSize = cIndexes.size(); unsigned int i, j; // NOTE: It seems that linear searches are definitely best for small color // indexes. if (vSize > 50) { for (i = 0; i < ly; ++i) { pix = pixBegin + i * wrap; for (j = 0; j < lx; ++j, ++pix, ++selData) { selData->m_selectedInk = binarySearch(v, vSize, pix->getInk()); selData->m_selectedPaint = binarySearch(v, vSize, pix->getPaint()); } } } else { for (i = 0; i < ly; ++i) { pix = pixBegin + i * wrap; for (j = 0; j < lx; ++j, ++pix, ++selData) { selData->m_selectedInk = linearSearch(v, vSize, pix->getInk()); selData->m_selectedPaint = linearSearch(v, vSize, pix->getPaint()); } } } cm->unlock(); } //================================================================================= //======================== // Blend functions //------------------------ // Pixel whose channels are doubles. Used to store intermediate values for pixel // blending. struct DoubleRGBMPixel { double r; double g; double b; double m; DoubleRGBMPixel() : r(0.0), g(0.0), b(0.0), m(0.0) {} }; //--------------------------------------------------------------------------------- const double maxTone = TPixelCM32::getMaxTone(); // Returns the ink & paint convex factors associated with passed tone. inline void getFactors(int tone, double &inkFactor, double &paintFactor) { paintFactor = tone / maxTone; inkFactor = (1.0 - paintFactor); } //--------------------------------------------------------------------------------- // Copies the cmIn paint and ink colors to the output rasters. void buildLayers(const TRasterCM32P &cmIn, const std::vector &palColors, TRaster32P &inkRaster, TRaster32P &paintRaster) { // Separate cmIn by copying the ink & paint colors directly to the layer // rasters. TPixelCM32 *cmPix, *cmBegin = (TPixelCM32 *)cmIn->getRawData(); TPixel32 *inkPix = (TPixel32 *)inkRaster->getRawData(); TPixel32 *paintPix = (TPixel32 *)paintRaster->getRawData(); unsigned int i, j, lx = cmIn->getLx(), ly = cmIn->getLy(), wrap = cmIn->getWrap(); for (i = 0; i < ly; ++i) { cmPix = cmBegin + i * wrap; for (j = 0; j < lx; ++j, ++cmPix, ++inkPix, ++paintPix) { *inkPix = palColors[cmPix->getInk()]; *paintPix = palColors[cmPix->getPaint()]; // Should pure colors be checked...? } } } //--------------------------------------------------------------------------------- // Returns true or false whether the selectedColor is the only selectable color // in the neighbourhood. If so, the blend copies it to the output layer pixel // directly. inline bool isFlatNeighbourhood(int selectedColor, const TRasterCM32P &cmIn, const TPoint &pos, const SelectionRaster &selRas, const BlurPattern &blurPattern) { TPixelCM32 &pix = cmIn->pixels(pos.y)[pos.x]; int lx = cmIn->getLx(), ly = cmIn->getLy(); unsigned int xy; TPoint samplePix; const TPoint *samplePoint = blurPattern.m_samples.empty() ? 0 : &blurPattern.m_samples[0]; // Read the samples to determine if they only have posSelectedColor unsigned int i, samplesCount = blurPattern.m_samples.size(); for (i = 0; i < samplesCount; ++i, ++samplePoint) { // Make sure the sample is inside the image samplePix.x = pos.x + samplePoint->x; samplePix.y = pos.y + samplePoint->y; xy = samplePix.x + lx * samplePix.y; if (samplePix.x < 0 || samplePix.y < 0 || samplePix.x >= lx || samplePix.y >= ly) continue; if (!selRas.isPurePaint(xy) && selRas.isSelectedInk(xy)) if (cmIn->pixels(samplePix.y)[samplePix.x].getInk() != selectedColor) return false; if (!selRas.isPureInk(xy) && selRas.isSelectedPaint(xy)) if (cmIn->pixels(samplePix.y)[samplePix.x].getPaint() != selectedColor) return false; } return true; } //--------------------------------------------------------------------------------- // Calculates the estimate of blend selection in the neighbourhood specified by // blurPattern. inline void addSamples(const TRasterCM32P &cmIn, const TPoint &pos, const TRaster32P &inkRas, const TRaster32P &paintRas, const SelectionRaster &selRas, const BlurPattern &blurPattern, DoubleRGBMPixel &pixSum, double &factorsSum) { double inkFactor, paintFactor; unsigned int xy, j, l; int lx = cmIn->getLx(), ly = cmIn->getLy(); TPixel32 *color; TPoint samplePos, pathPos; const TPoint *samplePoint = blurPattern.m_samples.empty() ? 0 : &blurPattern.m_samples[0]; const TPoint *pathPoint; unsigned int i, blurSamplesCount = blurPattern.m_samples.size(); for (i = 0; i < blurSamplesCount; ++i, ++samplePoint) { // Add each samples contribute to the sum samplePos.x = pos.x + samplePoint->x; samplePos.y = pos.y + samplePoint->y; if (samplePos.x < 0 || samplePos.y < 0 || samplePos.x >= lx || samplePos.y >= ly) continue; // Ensure that each pixel on the sample's path (if any) is selected l = blurPattern.m_samplePaths[i].size(); pathPoint = blurPattern.m_samplePaths[i].empty() ? 0 : &blurPattern.m_samplePaths[i][0]; for (j = 0; j < l; ++j, ++pathPoint) { pathPos.x = pos.x + pathPoint->x; pathPos.y = pos.y + pathPoint->y; xy = pathPos.x + lx * pathPos.y; if (!(selRas.isPurePaint(xy) || selRas.isSelectedInk(xy))) break; if (!(selRas.isPureInk(xy) || selRas.isSelectedPaint(xy))) break; } if (j < l) continue; xy = samplePos.x + lx * samplePos.y; if (selRas.isSelectedInk(xy) && !selRas.isPurePaint(xy)) { getFactors(cmIn->pixels(samplePos.y)[samplePos.x].getTone(), inkFactor, paintFactor); color = &inkRas->pixels(samplePos.y)[samplePos.x]; pixSum.r += inkFactor * color->r; pixSum.g += inkFactor * color->g; pixSum.b += inkFactor * color->b; pixSum.m += inkFactor * color->m; factorsSum += inkFactor; } if (selRas.isSelectedPaint(xy) && !selRas.isPureInk(xy)) { getFactors(cmIn->pixels(samplePos.y)[samplePos.x].getTone(), inkFactor, paintFactor); color = &paintRas->pixels(samplePos.y)[samplePos.x]; pixSum.r += paintFactor * color->r; pixSum.g += paintFactor * color->g; pixSum.b += paintFactor * color->b; pixSum.m += paintFactor * color->m; factorsSum += paintFactor; } } } //--------------------------------------------------------------------------------- typedef std::pair RGBMRasterPair; //--------------------------------------------------------------------------------- // Performs a single color blending. This function can be repeatedly invoked to // perform multiple color blending. inline void doBlend(const TRasterCM32P &cmIn, RGBMRasterPair &inkLayer, RGBMRasterPair &paintLayer, const SelectionRaster &selRas, const std::vector &blurPatterns) { // Declare some vars unsigned int blurPatternsCount = blurPatterns.size(); int lx = cmIn->getLx(), ly = cmIn->getLy(); double totalFactor; TPixelCM32 *cmPix, *cmBegin = (TPixelCM32 *)cmIn->getRawData(); TPixel32 *inkIn = (TPixel32 *)inkLayer.first->getRawData(), *inkOut = (TPixel32 *)inkLayer.second->getRawData(), *paintIn = (TPixel32 *)paintLayer.first->getRawData(), *paintOut = (TPixel32 *)paintLayer.second->getRawData(); const BlurPattern *blurPattern, *blurPatternsBegin = &blurPatterns[0]; bool builtSamples = false; DoubleRGBMPixel samplesSum; // For every cmIn pixel TPoint pos; SelectionData *selData = selRas.data(); cmPix = cmBegin; for (pos.y = 0; pos.y < ly; ++pos.y, cmPix = cmBegin + pos.y * cmIn->getWrap()) for (pos.x = 0; pos.x < lx; ++pos.x, ++inkIn, ++inkOut, ++paintIn, ++paintOut, ++selData, ++cmPix) { blurPattern = blurPatternsBegin + (rand() % blurPatternsCount); // Build the ink blend color if (!selData->m_purePaint && selData->m_selectedInk) { if (!builtSamples) { // Build samples contributes totalFactor = 1.0; samplesSum.r = samplesSum.g = samplesSum.b = samplesSum.m = 0.0; if (!isFlatNeighbourhood(cmPix->getInk(), cmIn, pos, selRas, *blurPattern)) addSamples(cmIn, pos, inkLayer.first, paintLayer.first, selRas, *blurPattern, samplesSum, totalFactor); builtSamples = true; } // Output the blended pixel inkOut->r = (samplesSum.r + inkIn->r) / totalFactor; inkOut->g = (samplesSum.g + inkIn->g) / totalFactor; inkOut->b = (samplesSum.b + inkIn->b) / totalFactor; inkOut->m = (samplesSum.m + inkIn->m) / totalFactor; } else { // If the color is not blended, then just copy the old layer pixel *inkOut = *inkIn; } // Build the paint blend color if (!selData->m_pureInk && selData->m_selectedPaint) { if (!builtSamples) { // Build samples contributes totalFactor = 1.0; samplesSum.r = samplesSum.g = samplesSum.b = samplesSum.m = 0.0; if (!isFlatNeighbourhood(cmPix->getPaint(), cmIn, pos, selRas, *blurPattern)) addSamples(cmIn, pos, inkLayer.first, paintLayer.first, selRas, *blurPattern, samplesSum, totalFactor); builtSamples = true; } // Output the blended pixel paintOut->r = (samplesSum.r + paintIn->r) / totalFactor; paintOut->g = (samplesSum.g + paintIn->g) / totalFactor; paintOut->b = (samplesSum.b + paintIn->b) / totalFactor; paintOut->m = (samplesSum.m + paintIn->m) / totalFactor; } else { // If the color is not blended, then just copy the old layer pixel *paintOut = *paintIn; } builtSamples = false; } } //--------------------------------------------------------------------------------- typedef std::vector BlurPatternContainer; //--------------------------------------------------------------------------------- /*! This function performs a group of spatial color blending <\a> operations on Toonz Images. The BlendParam structure stores the blend options recognized by this function; it includes a list of the palette indexes involved in the blend operation, plus: \li \b Intensity represents the \a radius of the blur operation between blend colors. \li \b Smoothness is the number of samples per pixel used to approximate the blur.
  • Stop at Contour <\b> specifies if lines from pixels to neighbouring samples should not trespass color indexes not included in the blend operation <\li> The succession of input blend parameters are applied in the order. */ template void blend(TToonzImageP ti, TRasterPT rasOut, const std::vector ¶ms) { assert(ti->getRaster()->getSize() == rasOut->getSize()); // Extract the interesting raster. It should be the savebox of passed cmap, // plus - if // some param has the 0 index as blending color - the intensity of that blend // param. unsigned int i, j; TRect saveBox(ti->getSavebox()); int enlargement = 0; for (i = 0; i < params.size(); ++i) for (j = 0; j < params[i].colorsIndexes.size(); ++j) if (params[i].colorsIndexes[j] == 0) enlargement = std::max(enlargement, tceil(params[i].intensity)); saveBox = saveBox.enlarge(enlargement); TRasterCM32P cmIn(ti->getRaster()->extract(saveBox)); TRasterPT rasOutExtract = rasOut->extract(saveBox); // Ensure that cmIn and rasOut have the same size unsigned int lx = cmIn->getLx(), ly = cmIn->getLy(); // Build the pure colors infos SelectionRaster selectionRaster(cmIn); // Now, build a little group of BlurPatterns - and for each, one for passed // param. // A small number of patterns per param is needed to make the pattern look not // ever the same. const int blurPatternsPerParam = 10; std::vector blurGroup(params.size()); for (i = 0; i < params.size(); ++i) { BlurPatternContainer &blurContainer = blurGroup[i]; blurContainer.reserve(blurPatternsPerParam); for (j = 0; j < blurPatternsPerParam; ++j) blurContainer.push_back(BlurPattern( params[i].intensity, params[i].smoothness, params[i].stopAtCountour)); } // Build the palette TPalette *palette = ti->getPalette(); std::vector paletteColors; paletteColors.resize(palette->getStyleCount()); for (i = 0; i < paletteColors.size(); ++i) paletteColors[i] = premultiply(palette->getStyle(i)->getAverageColor()); // Build the 4 auxiliary rasters for the blending procedure: they are ink / // paint versus input / output in the blend. // The output raster is reused to spare some memory - it should be, say, the // inkLayer's second at the end of the overall // blending procedure. It could be the first, without the necessity of // clearing it before blending the layers, but things // get more complicated when PIXEL is TPixel64... RGBMRasterPair inkLayer, paintLayer; TRaster32P rasOut32P_1(lx, ly, lx, (TPixel32 *)rasOut->getRawData(), false); inkLayer.first = (params.size() % 2) ? rasOut32P_1 : TRaster32P(lx, ly); inkLayer.second = (params.size() % 2) ? TRaster32P(lx, ly) : rasOut32P_1; if (PIXEL::maxChannelValue >= TPixel64::maxChannelValue) { TRaster32P rasOut32P_2(lx, ly, lx, ((TPixel32 *)rasOut->getRawData()) + lx * ly, false); paintLayer.first = (params.size() % 2) ? rasOut32P_2 : TRaster32P(lx, ly); paintLayer.second = (params.size() % 2) ? TRaster32P(lx, ly) : rasOut32P_2; } else { paintLayer.first = TRaster32P(lx, ly); paintLayer.second = TRaster32P(lx, ly); } inkLayer.first->clear(); inkLayer.second->clear(); paintLayer.first->clear(); paintLayer.second->clear(); // Now, we have to perform the blur of each of the cm's pixels. cmIn->lock(); rasOut->lock(); inkLayer.first->lock(); inkLayer.second->lock(); paintLayer.first->lock(); paintLayer.second->lock(); // Convert the initial cmIn to fullcolor ink - paint layers buildLayers(cmIn, paletteColors, inkLayer.first, paintLayer.first); // Perform the blend on separated ink - paint layers for (i = 0; i < params.size(); ++i) { if (params[i].colorsIndexes.size() == 0) continue; selectionRaster.updateSelection(cmIn, params[i]); doBlend(cmIn, inkLayer, paintLayer, selectionRaster, blurGroup[i]); tswap(inkLayer.first, inkLayer.second); tswap(paintLayer.first, paintLayer.second); } // Release the unnecessary rasters inkLayer.second->unlock(); paintLayer.second->unlock(); inkLayer.second = TRaster32P(); paintLayer.second = TRaster32P(); // Clear rasOut - since it was reused to spare space... rasOut->clear(); // Add the ink & paint layers on the output raster double PIXELmaxChannelValue = PIXEL::maxChannelValue; double toPIXELFactor = PIXELmaxChannelValue / (double)TPixel32::maxChannelValue; double inkFactor, paintFactor; TPoint pos; PIXEL *outPix, *outBegin = (PIXEL *)rasOutExtract->getRawData(); TPixelCM32 *cmPix, *cmBegin = (TPixelCM32 *)cmIn->getRawData(); int wrap = rasOutExtract->getWrap(); TPixel32 *inkPix = (TPixel32 *)inkLayer.first->getRawData(); TPixel32 *paintPix = (TPixel32 *)paintLayer.first->getRawData(); for (i = 0; i < ly; ++i) { outPix = outBegin + wrap * i; cmPix = cmBegin + wrap * i; for (j = 0; j < lx; ++j, ++outPix, ++cmPix, ++inkPix, ++paintPix) { getFactors(cmPix->getTone(), inkFactor, paintFactor); outPix->r = tcrop( toPIXELFactor * (inkFactor * inkPix->r + paintFactor * paintPix->r), 0.0, PIXELmaxChannelValue); outPix->g = tcrop( toPIXELFactor * (inkFactor * inkPix->g + paintFactor * paintPix->g), 0.0, PIXELmaxChannelValue); outPix->b = tcrop( toPIXELFactor * (inkFactor * inkPix->b + paintFactor * paintPix->b), 0.0, PIXELmaxChannelValue); outPix->m = tcrop( toPIXELFactor * (inkFactor * inkPix->m + paintFactor * paintPix->m), 0.0, PIXELmaxChannelValue); } } inkLayer.first->unlock(); paintLayer.first->unlock(); cmIn->unlock(); rasOut->unlock(); // Destroy the auxiliary bitmaps selectionRaster.destroy(); } template void blend(TToonzImageP cmIn, TRasterPT rasOut, const std::vector ¶ms); template void blend(TToonzImageP cmIn, TRasterPT rasOut, const std::vector ¶ms);