Blob Blame Raw


#include "trop.h"

/*
See Alexander Reshetov's "Morphological Antialiasing" paper on Intel Labs site.

Basically, this antialiasing algorithm is based on the following ideas:

 - Suppose that our image is just made up of flat colors. Then, a simple antialiasing
   approach is that of assuming that the 'actual' line separating two distinct colors
   is the polyline that passes through the midpoint of each edge of its original jaggy
   counterpart.
   As pixels around the border are cut through by the polyline, the area of the pixel
   that is filled of a certain color is its weight in the output filtered pixel.

 - The above method can be applied on each single uniform piece of a scanline, considering
   the lines originated by the vertical extensions of its left and right edges.

 - Of these lines, only those which lie completely on pixels adjacent to the edge are
   considered - so that the antialiasing effect is kept only around the contours.

This algorithm would yield a good result at what may be considered 50% softness. Implementing
a generalized softness simply requires that the line slopes used above are modified
accordingly (divide by 2 * softFactor).
*/

//-----------------------------------------------------------------------------------------

namespace
{
template <typename PIX>
class PixelSelector
{
public:
	typedef PIX pixel_type;

private:
	int m_thresh;

public:
	PixelSelector(int thresh) : m_thresh(thresh) {}

	bool areEqual(const PIX &a, const PIX &b) const
	{
		return std::max({
			abs((int)a.r - b.r),
			abs((int)a.g - b.g),
			abs((int)a.b - b.b),
			abs((int)a.m - b.m)}) < m_thresh;
	}
};

//-----------------------------------------------------------------------------------------

template <>
class PixelSelector<TPixelCM32>
{
public:
	typedef TPixelCM32 pixel_type;

private:
	int m_thresh;

public:
	PixelSelector(int thresh) : m_thresh(thresh) {}

	bool areEqual(const TPixelCM32 &a, const TPixelCM32 &b) const
	{
		return (a.getInk() == b.getInk()) && (abs(a.getTone() - b.getTone()) < m_thresh);
	}
};

//-----------------------------------------------------------------------------------------

template <typename PIX>
inline void weightPix(PIX *out, const PIX *a, const PIX *b, double weightA, double weightB)
{
	out->r = a->r * weightA + b->r * weightB;
	out->g = a->g * weightA + b->g * weightB;
	out->b = a->b * weightA + b->b * weightB;
	out->m = a->m * weightA + b->m * weightB;
}

//-----------------------------------------------------------------------------------------

template <>
inline void weightPix<TPixelCM32>(TPixelCM32 *out, const TPixelCM32 *a, const TPixelCM32 *b,
								  double weightA, double weightB)
{
	*out = TPixelCM32(
		out->isPurePaint() ? b->getInk() : a->getInk(), a->getPaint(),
		a->getTone() * weightA + b->getTone() * weightB);
}

//-----------------------------------------------------------------------------------------

//Returns 0 if pixels to connect are on the 00-11 diagonal, 1 on the 01-10 one.
template <typename PIX, typename SELECTOR>
inline bool checkNeighbourHood(int x, int y, PIX *pix, int lx, int ly, int dx, int dy,
							   const SELECTOR &sel)
{
	int count1 = 0, count2 = 0;
	int dx2 = 2 * dx, dy2 = 2 * dy;
	if (y > 1) {
		//Lower edge
		count1 +=
			(int)sel.areEqual(*(pix - dx), *(pix - dy2)) +
			(int)sel.areEqual(*(pix - dx), *(pix - dy2 - dx));
		count2 +=
			(int)sel.areEqual(*pix, *(pix - dy2)) +
			(int)sel.areEqual(*pix, *(pix - dy2 - dx));
	}
	if (y < ly - 1) {
		//Upper edge
		count1 +=
			(int)sel.areEqual(*(pix - dx), *(pix + dy)) +
			(int)sel.areEqual(*(pix - dx), *(pix + dy - dx));
		count2 +=
			(int)sel.areEqual(*pix, *(pix + dy)) +
			(int)sel.areEqual(*pix, *(pix + dy - dx));
	}
	if (x > 1) {
		//Left edge
		count1 +=
			(int)sel.areEqual(*(pix - dx), *(pix - dx2)) +
			(int)sel.areEqual(*(pix - dx), *(pix - dx2 - dy));
		count2 +=
			(int)sel.areEqual(*pix, *(pix - dx2)) +
			(int)sel.areEqual(*pix, *(pix - dx2 - dy));
	}
	if (x < lx - 1) {
		//Left edge
		count1 +=
			(int)sel.areEqual(*(pix - dx), *(pix + dx)) +
			(int)sel.areEqual(*(pix - dx), *(pix + dx - dy));
		count2 +=
			(int)sel.areEqual(*pix, *(pix + dx)) +
			(int)sel.areEqual(*pix, *(pix + dx - dy));
	}

	//Connect by minority: if there are more pixels like those on the 00-11 diagonal, connect the other,
	//and viceversa.
	return count1 > count2;
}
}

//========================================================================================

template <typename PIX>
inline void filterLine(PIX *inLPix, PIX *inUPix, PIX *outLPix, PIX *outUPix,
					   int ll, int inDl, int outLDl, int outUDl,
					   double hStart, double slope, bool filterLower)
{
	assert(hStart >= 0.0 && slope > 0.0);

	double h0 = hStart, h1, area;
	double base = hStart / slope;

	int i, end = std::min(tfloor(base), ll);
	if (filterLower) {
		//Filter lower line
		for (i = 0; i < end; ++i, h0 = h1,
			inLPix += inDl, inUPix += inDl, outLPix += outLDl) {
			h1 = h0 - slope;
			area = 0.5 * (h0 + h1);
			weightPix(outLPix, outLPix, inUPix, 1.0 - area, area);
		}

		if (i < ll) {
			double remnant = base - end;
			area = 0.5 * remnant * h0;
			weightPix(outLPix, outLPix, inUPix, 1.0 - area, area);
		}
	} else {
		//Filter upper line
		for (i = 0; i < end; ++i, h0 = h1,
			inLPix += inDl, inUPix += inDl, outUPix += outUDl) {
			h1 = h0 - slope;
			area = 0.5 * (h0 + h1);
			weightPix(outUPix, outUPix, inLPix, 1.0 - area, area);
		}

		if (i < ll) {
			double remnant = base - end;
			area = 0.5 * remnant * h0;
			weightPix(outUPix, outUPix, inLPix, 1.0 - area, area);
		}
	}
}

//---------------------------------------------------------------------------------------

template <typename PIX, typename SELECTOR>
inline bool checkLength(int lLine, int y, int ly, int dy,
						PIX *pixL1, PIX *pixU1, PIX *pixL2, PIX *pixU2,
						bool uniteU, bool do1Line,
						const SELECTOR &sel)
{
	//1-length edges must be processed (as primary edges) only if explicitly required,
	//and only when its associated secondary edge is of the same length.

	return (lLine > 1) ||
		   (do1Line &&
			((uniteU && (y > 1 && !(sel.areEqual(*pixL1, *(pixL1 - dy)) && sel.areEqual(*pixL2, *(pixL2 - dy))))) ||
			 (y < ly - 1 && !(sel.areEqual(*pixU1, *(pixU1 + dy)) && sel.areEqual(*pixU2, *(pixU2 + dy))))));
}

//---------------------------------------------------------------------------------------

template <typename PIX, typename SELECTOR>
void processLine(int r, int lx, int ly,
				 PIX *inLRow, PIX *inURow, PIX *outLRow, PIX *outURow,
				 int inDx, int inDy, int outLDx, int outUDx,
				 bool do1Line, double hStart, double slope,
				 const SELECTOR &sel)
{
	//Using a 'horizontal' notation here - but the same applies in vertical too
	++r;

	//As long as we don't reach row end, process uninterrupted separation lines
	//between colors

	PIX *inLL = inLRow, *inLR, *inUL = inURow, *inUR;
	PIX *inLL_1, *inUL_1, *inLR_1, *inUR_1;
	PIX *inLEnd = inLRow + lx * inDx;
	int x, lLine;
	bool uniteLL, uniteUL, uniteLR, uniteUR;

	//Special case: a line at row start has different weights
	if (!sel.areEqual(*inLL, *inUL)) {
		//Look for line ends
		for (inLR = inLL + inDx, inUR = inUL + inDx;
			 inLR != inLEnd && sel.areEqual(*inLL, *inLR) && sel.areEqual(*inUL, *inUR);
			 inLR += inDx, inUR += inDx)
			;

		if (inLR != inLEnd) {
			//Found a line to process
			lLine = (inLR - inLL) / inDx;

			inLR_1 = inLR - inDx, inUR_1 = inUR - inDx;
			x = (inLR_1 - inLRow) / inDx;

			uniteUR = sel.areEqual(*inUR_1, *inLR);
			uniteLR = sel.areEqual(*inLR_1, *inUR);
			if (uniteUR || uniteLR) {
				if (uniteUR && uniteLR)
					//Ambiguous case. Check neighborhood to find out which one must be actually united.
					uniteUR = !checkNeighbourHood(x + 1, r, inUR, lx, ly, inDx, inDy, sel);

				if (checkLength(lLine, r, ly, inDy, inLR_1, inUR_1, inLR, inUR, uniteUR, do1Line, sel))
					filterLine(inLR_1, inUR_1, outLRow + x * outLDx, outURow + x * outUDx,
							   lLine, -inDx, -outLDx, -outUDx, hStart, slope / (lLine << 1), uniteUR);
			}
		}

		//Update lefts
		inLL = inLR, inUL = inUR;
	}

	//Search for a line start
	for (; inLL != inLEnd && sel.areEqual(*inLL, *inUL); inLL += inDx, inUL += inDx)
		;

	while (inLL != inLEnd) {
		//Look for line ends
		for (inLR = inLL + inDx, inUR = inUL + inDx;
			 inLR != inLEnd && sel.areEqual(*inLL, *inLR) && sel.areEqual(*inUL, *inUR);
			 inLR += inDx, inUR += inDx)
			;

		if (inLR == inLEnd)
			break; //Dealt with later

		//Found a line to process
		lLine = (inLR - inLL) / inDx;

		//First, filter left to right
		inLL_1 = inLL - inDx, inUL_1 = inUL - inDx;
		x = (inLL - inLRow) / inDx;

		uniteUL = sel.areEqual(*inUL, *inLL_1);
		uniteLL = sel.areEqual(*inLL, *inUL_1);
		if (uniteUL || uniteLL) {
			if (uniteUL && uniteLL)
				uniteUL = checkNeighbourHood(x, r, inUL, lx, ly, inDx, inDy, sel);

			if (checkLength(lLine, r, ly, inDy, inLL_1, inUL_1, inLL, inUL, uniteUL, do1Line, sel))
				filterLine(inLL, inUL, outLRow + x * outLDx, outURow + x * outUDx,
						   lLine, inDx, outLDx, outUDx, hStart, slope / lLine, uniteUL);
		}

		//Then, filter right to left
		inLR_1 = inLR - inDx, inUR_1 = inUR - inDx;
		x = (inLR_1 - inLRow) / inDx;

		uniteUR = sel.areEqual(*inUR_1, *inLR);
		uniteLR = sel.areEqual(*inLR_1, *inUR);
		if (uniteUR || uniteLR) {
			if (uniteUR && uniteLR)
				uniteUR = !checkNeighbourHood(x + 1, r, inUR, lx, ly, inDx, inDy, sel);

			if (checkLength(lLine, r, ly, inDy, inLR_1, inUR_1, inLR, inUR, uniteUR, do1Line, sel))
				filterLine(inLR_1, inUR_1, outLRow + x * outLDx, outURow + x * outUDx,
						   lLine, -inDx, -outLDx, -outUDx, hStart, slope / lLine, uniteUR);
		}

		//Update lefts - search for a new line start
		inLL = inLR, inUL = inUR;
		for (; inLL != inLEnd && sel.areEqual(*inLL, *inUL); inLL += inDx, inUL += inDx)
			;
	}

	//Special case: filter the last line in the row
	if (inLL != inLEnd) {
		//Found a line to process
		lLine = (inLR - inLL) / inDx;

		inLL_1 = inLL - inDx, inUL_1 = inUL - inDx;
		x = (inLL - inLRow) / inDx;

		uniteUL = sel.areEqual(*inUL, *inLL_1);
		uniteLL = sel.areEqual(*inLL, *inUL_1);
		if (uniteUL || uniteLL) {
			if (uniteUL && uniteLL)
				uniteUL = checkNeighbourHood(x, r, inUL, lx, ly, inDx, inDy, sel);

			if (checkLength(lLine, r, ly, inDy, inLL_1, inUL_1, inLL, inUL, uniteUL, do1Line, sel))
				filterLine(inLL, inUL, outLRow + x * outLDx, outURow + x * outUDx,
						   lLine, inDx, outLDx, outUDx, hStart, slope / (lLine << 1), uniteUL);
		}
	}
}

//---------------------------------------------------------------------------------------

template <typename PIX>
void makeAntialias(const TRasterPT<PIX> &src, TRasterPT<PIX> &dst, int threshold, int softness)
{
	dst->copy(src);
	if (softness == 0)
		return;

	double slope = (50.0 / softness);
	double hStart = 0.5; //fixed for now

	src->lock();
	dst->lock();

	PixelSelector<PIX> sel(threshold);

	//First, filter by rows
	int x, y, lx = src->getLx(), ly = src->getLy(), lx_1 = lx - 1, ly_1 = ly - 1;
	for (y = 0; y < ly_1; ++y) {
		processLine(y, lx, ly,
					src->pixels(y), src->pixels(y + 1),
					dst->pixels(y), dst->pixels(y + 1),
					1, src->getWrap(), 1, 1,
					true, hStart, slope,
					sel);
	}

	//Then, go by columns
	for (x = 0; x < lx_1; ++x) {
		processLine(x, ly, lx,
					src->pixels(0) + x, src->pixels(0) + x + 1,
					dst->pixels(0) + x, dst->pixels(0) + x + 1,
					src->getWrap(), 1, dst->getWrap(), dst->getWrap(),
					false, hStart, slope,
					sel);
	}

	dst->unlock();
	src->unlock();
}

//---------------------------------------------------------------------------------------

void TRop::antialias(const TRasterP &src, const TRasterP &dst, int threshold, int softness)
{
	assert(src->getSize() == dst->getSize());

	TRaster32P src32(src), dst32(dst);
	if (src32 && dst32) {
		makeAntialias<TPixel32>(src32, dst32, threshold, softness);
		return;
	}

	TRaster64P src64(src), dst64(dst);
	if (src64 && dst64) {
		makeAntialias<TPixel64>(src64, dst64, threshold << 8, softness);
		return;
	}

	TRasterCM32P srcCM(src), dstCM(dst);
	if (srcCM && dstCM) {
		makeAntialias<TPixelCM32>(srcCM, dstCM, threshold, softness);
		return;
	}

	assert(!"Source and destination rasters must be of the same type!");
}