BoltBait's Pages - Pastel Filter

Sarcasm - just one of the services I offer...

 

Paint.NET Pastel Filter

OK, so I had this idea to create a pastel filter for the free paint program Paint.NET.  What I evenually came up with looked fair on some images and poor on others.  Well, I finally updated this effect and the results look pretty good.

Here is a "before & after" picture:

And, here is another:

 

 

The Idea

Here is my idea:

First, I normalize the Hue to reduce the number of overall colors--similar to a "cutout" effect.  Then, I compress all of the Saturation (S) information from 15-100 into 15-50.  This should lighten up all the colors to a nice pastel tone.  Then, it compresses the Value (V) from 5-100 into 85-100.  This should really get rid of all the dark colors.  The only thing left to deal with was the near black pixels.  Finally, I converted all of the near black pixels to a shade of gray.

 

The Effect DLL

You can download the precompiled effect here: Pastel.dll

Just drop this file in your \program files\Paint.NET\effects directory and you should be all set.

 

The Source Code

Here is the source code to the older version that didn't work too well:

void Render(Surface dst, Surface src, Rectangle rect)
{ // How big is your box of pastels?
	double BaseColors = 72; // 1-360, step 1, Default = 72
 
	PdnRegion selectionRegion = EnvironmentParameters.GetSelection(src.Bounds);
	Rectangle selection = this.EnvironmentParameters.GetSelection(src.Bounds).GetBoundsInt();
	ColorBgra CurrentPixel;
	double Q = 360 / BaseColors;
	double H = 0,S = 0,V = 0;
	for(int y = rect.Top; y < rect.Bottom; y++)
	{
		for (int x = rect.Left; x < rect.Right; x++)
		{
			if (selectionRegion.IsVisible(x, y))
			{
				CurrentPixel = src[x,y];
				EvanRGBtoHSV(CurrentPixel.R,CurrentPixel.G,CurrentPixel.B,ref H,ref S,ref V);
				H = Q*(double)Math.Round(H/Q); // Normalize Hue
				if (S > .15) // Compress Saturation
				{
					S = ( (0.35*(S-0.15)) / (1.0-0.15) ) + 0.15;
				}
				if (V > 0.05) // Compress Value
				{
					V = ( (0.15*(V-0.05)) / (1.0-0.05) ) + 0.85;
				} 
				else // Near Black Pixels
				{
					S = 0; // turn them gray
					V += 0.75;
				}
				EvanHSVtoRGB(H,S,V,ref CurrentPixel.R,ref CurrentPixel.G,ref CurrentPixel.B);
				dst[x,y] = CurrentPixel;
			}
		}
	}
}

Here are the support functions written by someone named Evan:

public void EvanHSVtoRGB(double H, double S, double V, ref byte bR, ref byte bG, ref byte bB)
{
	const double HSV_UNDEFINED = -999.0;
	// Parameters must satisfy the following ranges:
	// 0.0 <= H < 360.0
	// 0.0 <= S <= 1.0
	// 0.0 <= V <= 1.0

	// Handle special case first
	if (S == 0.0 || H == HSV_UNDEFINED)
	{
		byte x = (byte)(int)(V * 255.0);
		bR = x;
		bG = x;
		bB = x;
		return;
	}

	if (H >= 360.0)
	{
		H = AngleConstrain(H);
	}

	double R = V, G = V, B = V;
	double Hi = Math.Floor(H / 60.0);
	double f = H / 60.0 - Hi;
	double p = V * (1.0 - S);
	double q = V * (1.0 - f * S);
	double t = V * (1.0 - (1.0 - f) * S);
	if (Hi == 0.0)
	{
		R = V;
		G = t;
		B = p;
	}
	else if (Hi == 1.0)
	{
		R = q;
		G = V;
		B = p;
	}
	else if (Hi == 2.0)
	{
		R = p;
		G = V;
		B = t;
	}
	else if (Hi == 3.0)
	{
		R = p;
		G = q;
		B = V;
	}
	else if (Hi == 4.0)
	{
		R = t;
		G = p;
		B = V;
	}
	else if (Hi == 5.0)
	{
		R = V;
		G = p;
		B = q;
	}

	int iR = (int)(R * 255.0);
	int iG = (int)(G * 255.0);
	int iB = (int)(B * 255.0);
	bR = (byte)iR;
	bG = (byte)iG;
	bB = (byte)iB;
}

public void EvanRGBtoHSV(int R, int G, int B, ref double outH, ref double outS, ref double outV)
{
	const double HSV_UNDEFINED = -999.0;
	// R, G, and B must range from 0 to 255
	// Ouput value ranges:
	// outH - 0.0 to 360.0
	// outS - 0.0 to 1.0
	// outV - 0.0 to 1.0

	double dR = (double)R / 255.0;
	double dG = (double)G / 255.0;
	double dB = (double)B / 255.0;
	double dmaxRGB = EvanMax3(dR, dG, dB);
	double dminRGB = EvanMin3(dR, dG, dB);
	double delta = dmaxRGB - dminRGB;

	// Set value
	outV = dmaxRGB;

	// Handle special case
	if (dmaxRGB == 0)
	{
		outH = HSV_UNDEFINED;
		outS = 0.0;
		return;
	}

	outS = delta / dmaxRGB;
	if (dmaxRGB == dminRGB)
	{
		outH = HSV_UNDEFINED;
		return;
	}

	// Finally, compute hue
	if (dR == dmaxRGB)
	{
		outH = (dG - dB) / delta * 60.0;
	}
	else if (dG == dmaxRGB)
	{
		outH = (2.0 + (dB - dR) / delta) * 60.0;
	}
	else //if (dB == dmaxRGB)
	{
		outH = (4.0 + (dR - dG) / delta) * 60.0;
	}

	if (outH < 0)
	{
		outH += 360.0;
	}
}

public double EvanMax3(double x, double y, double z)
{
	return (x > y) ? ((x > z) ? x : z) : ((y > z) ? y : z);
}

public double EvanMin3(double x, double y, double z)
{
	return (x < y) ? ((x < z) ? x : z) : ((y < z) ? y : z);
}

public double AngleConstrain(double MyAngle)
{
	// Makes sure that 0.0 <= MyAngle < 360.0
	// Wraps around the value if its outside this range
	if (MyAngle >= 360.0)
	{
		MyAngle -= Math.Floor(MyAngle / 360.0) * 360.0;
	}
	if (MyAngle < 0.0)
	{
		MyAngle += 360.0;
	}
	return MyAngle;
}

I hope you enjoy this update.

Monday, February 18, 2008