[cairo] Premultiplied transparency causes streaks in images

Andrea Canciani ranma42 at gmail.com
Fri Nov 12 02:20:43 PST 2010


On Fri, Nov 12, 2010 at 10:46 AM, Paril <paril at alteredsoftworks.com> wrote:
> Here, I just threw this together very very quickly in C#; it CLEARLY shows
> the output "streaking" black transparent pixels down the purple brush
> stroke, when the expected output should show a complete purple circle with
> the same color on the inside of the capsule-like shape.
>
> http://alteredsoftworks.com/cairo/streaking.txt
>
> It should be rather easy to convert to C/C++. I used no .NET-specific
> classes; DevIL for the image loading and Cairo for the drawing, just as I
> use in the actual program.
>
> The reason I suspect premultiplication is because the only time these
> streaks can be created is when the background transparency is turned from
> white to black - which is done by multiplying the color values by their
> alpha. If all of the images' alpha and color values remain, when they are
> blended they should not create any streaks at all.

Observation: your code is (should be, as I'm unable to test it) equivalent
to the attached one, which replaces the PaintWithAlpha operation with a
multiplication of the source alpha

The problem is caused by color quantization (and amplified by
premultiplication).

In your case:
Brush color: ARGB = (0.16, 0.42, 0.7, 1) -> [40, 17, 28, 40] premultiplied 8bit

Where the brush image is opaque, you actually get this values, but where it
is not, you end up with smaller values, for example:

Image alpha: 15 (in 8 bit integer) -> [2, 1, 1, 2] (or something like that)
When you composite multiple times this pixel over itself you would expect
the original color, you obviously will get something very different
(something like: 40, 20, 20, 40, if my example computations were correct).

I think that integer formats are not well suited for repeated compositing of the
same image as they obviously break the assumption:

n*color OVER clear == color OVER^n clear

which you seem to rely on.

NB: this would be a problem with nonpremultiplied as well, but it would be much
harder to notice (because it would only affect alpha, instead of both alpha and
color).
Example: (assuming 8-bit nonpremultiplied compositing)
Start color: opaque red (1, 0, 0, 1) = [255, 0, 0, 255]
Use it with a very light brush (1/1000), so that the color becomes [0,
0, 0, 255]
Even if you composite it 1000 times, you don't get opaque red.

I hope I managed to guess what is the real issue.
I'm not sure as I'm unable to test it, but it would be be confirmed if
the attachment streaking-working.txt (or something along that lines) worked
(or at least showed much less artifacts).

Andrea
-------------- next part --------------
static void OutputStreakTest()
{
	// Load circle data
	int devid = Il.ilGenImage();
	Il.ilBindImage(devid);

	// assume pass
	Il.ilLoadImage("brushes/circle.png");

	unsafe
	{
		// get data ptr
		byte* inData = (byte*)Il.ilGetData().ToPointer();
		int width = Il.ilGetInteger(Il.IL_IMAGE_WIDTH);
		int height = Il.ilGetInteger(Il.IL_IMAGE_HEIGHT);

		var brushImage = new Cairo.ImageSurface(Cairo.Format.Argb32, width, height);

		Cairo.Color selectedColor = new Cairo.Color(0.16, 0.42, 0.7, 1);

		unsafe
		{
			int* ptr = (int*)brushImage.Data.ToPointer();

			for (int x = 0; x < width; ++x)
				for (int y = 0; y < height; ++y)
				{
					int start = (y * (width * 4)) + (x * 4);
					var col = Color.FromArgb(inData[start + 3], inData[start], inData[start + 1], inData[start + 2]);

					Cairo.Color color = new Cairo.Color(
						(double)col.R / 255.0f,
						(double)col.G / 255.0f,
						(double)col.B / 255.0f,
						(double)col.A / 255.0f);

					color.A = color.A * selectedColor.R;
					color.R = (color.R * selectedColor.R) * color.A;
					color.G = (color.G * selectedColor.G) * color.A;
					color.B = (color.B * selectedColor.B) * color.A;

					ptr[(y * width) + x] = Color.FromArgb(
						(byte)(color.A * 255),
						(byte)(color.R * 255),
						(byte)(color.G * 255),
						(byte)(color.B * 255)).ToArgb();
				}
		}

		const double BrushScale = 5.0; // draw 5x bigger (make streaks more visible)
		using (var surface = new Cairo.ImageSurface(Cairo.Format.Argb32, 256, 256))
		{
			// Create the surface for test drawing
			using (var imgpat = new Cairo.SurfacePattern(brushImage))
			{
				var scaler = new Cairo.Matrix();
				imgpat.Filter = Cairo.Filter.Gaussian;
				scaler.Scale(1.0 / BrushScale, 1.0 / BrushScale);
				imgpat.Matrix = scaler;

				using (var context = new Cairo.Context(surface))
				{
					float w = (float)(brushImage.Width * BrushScale);
					float h = (float)(brushImage.Height * BrushScale);
					int x = (int)((256 / 2) - (w / 2));
					int y = (int)((256 / 2) - (h / 2));

					for (int i = 0; i < 30; ++i) // draw the brush several times, to show the effect more
					{
						for (int z = 0; z < 4; ++z)
						{
							context.Save();
							context.Translate(x, y);
							context.SetSource(imgpat);
							context.Restore();
						}

						y += 1;
					}

					surface.WriteToPng("output.png");
				}
			}
		}
	}
}
-------------- next part --------------
static void OutputStreakTest()
{
	// Load circle data
	int devid = Il.ilGenImage();
	Il.ilBindImage(devid);

	// assume pass
	Il.ilLoadImage("brushes/circle.png");

	unsafe
	{
		// get data ptr
		byte* inData = (byte*)Il.ilGetData().ToPointer();
		int width = Il.ilGetInteger(Il.IL_IMAGE_WIDTH);
		int height = Il.ilGetInteger(Il.IL_IMAGE_HEIGHT);

		var brushImage = new Cairo.ImageSurface(Cairo.Format.Argb32, width, height);

		Cairo.Color selectedColor = new Cairo.Color(0.16, 0.42, 0.7, 1);

		unsafe
		{
			int* ptr = (int*)brushImage.Data.ToPointer();

			for (int x = 0; x < width; ++x)
				for (int y = 0; y < height; ++y)
				{
					int start = (y * (width * 4)) + (x * 4);
					var col = Color.FromArgb(inData[start + 3], inData[start], inData[start + 1], inData[start + 2]);

					Cairo.Color color = new Cairo.Color(
						(double)col.R / 255.0f,
						(double)col.G / 255.0f,
						(double)col.B / 255.0f,
						(double)col.A / 255.0f);

					color.A = min(1, color.A * selectedColor.R * 30); // don't draw the brush several times, because it shows the effect more
					color.R = (color.R * selectedColor.R) * color.A;
					color.G = (color.G * selectedColor.G) * color.A;
					color.B = (color.B * selectedColor.B) * color.A;

					ptr[(y * width) + x] = Color.FromArgb(
						(byte)(color.A * 255),
						(byte)(color.R * 255),
						(byte)(color.G * 255),
						(byte)(color.B * 255)).ToArgb();
				}
		}

		const double BrushScale = 5.0; // draw 5x bigger (make streaks more visible)
		using (var surface = new Cairo.ImageSurface(Cairo.Format.Argb32, 256, 256))
		{
			// Create the surface for test drawing
			using (var imgpat = new Cairo.SurfacePattern(brushImage))
			{
				var scaler = new Cairo.Matrix();
				imgpat.Filter = Cairo.Filter.Gaussian;
				scaler.Scale(1.0 / BrushScale, 1.0 / BrushScale);
				imgpat.Matrix = scaler;

				using (var context = new Cairo.Context(surface))
				{
					float w = (float)(brushImage.Width * BrushScale);
					float h = (float)(brushImage.Height * BrushScale);
					int x = (int)((256 / 2) - (w / 2));
					int y = (int)((256 / 2) - (h / 2));

					for (int i = 0; i < 1; ++i) // don't draw the brush several times, because it shows the effect more
					{
						for (int z = 0; z < 4; ++z)
						{
							context.Save();
							context.Translate(x, y);
							context.SetSource(imgpat);
							context.Restore();
						}

						y += 1;
					}

					surface.WriteToPng("output.png");
				}
			}
		}
	}
}


More information about the cairo mailing list