Dan Byström’s Bwain

Blog without an interesting name

Thumbnails with glass table reflection in GDI+

Posted by Dan Byström on January 12, 2009

vistathumbnailsI’ve been playing around with image processing lately and since my last post about loading thumbnail images from files I couldn’t help myself from trying to roll my own “Web 2.0 reflection effect” directly in .NET 2.0 with no 3D support whatsoever. Actually, I think was more inspired by Windows Vista’s thumbnails (to the right) than the web.

This is what I eventually came up with:

reflectionsamples1

Although this is all easy – since there were a few things that couldn’t be done in “pure” GDI+ and then some uncommon approaches involved in my solution, I think that there may be some people out there who don’t find this totally trivial. So I thought that it might be worth writing this down.

From the original picture I work through four steps:

reflectionsteps2

1. The first step merely shrinks the original picture to the desired size and puts a frame around it. This is trivial:

	protected virtual Bitmap createFramedBitmap( Bitmap bmpSource, Size szFull )
	{
		Bitmap bmp = new Bitmap( szFull.Width, szFull.Height );
		using ( Graphics g = Graphics.FromImage( bmp ) )
		{
			g.FillRectangle( FrameBrush, 0, 0, szFull.Width, szFull.Height );
			g.DrawRectangle( BorderPen, 0, 0, szFull.Width - 1, szFull.Height - 1 );
			g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
			g.DrawImage(
				bmpSource,
				new Rectangle( FrameWidth, FrameWidth, szFull.Width - FrameWidth * 2, szFull.Height - FrameWidth * 2 ),
				new Rectangle( Point.Empty, bmpSource.Size ),
				GraphicsUnit.Pixel );
		}
		return bmp;
	}

(Note the InterpolationMode property. It is important in order to resize the image with good quality!)

2. The second step is substantially more involved. It takes the result from step 1 and does this, all in one go:

  1. Flip the image upside down (omitting the upper and lower parts of frame, since we don’t want them to be present in the reflection).
  2. Apply a Gaussian blur convolution effect to make the image look…, well, blurred… 🙂
  3. Wash out some color to make the reflection a little bit grayish.
  4. Apply an alpha blend fall out.

Flipping the image and the color wash-out can both be done directly in GDI+. Flip either with Bitmap.RotateFlip or with a transformation matrix and use a ColorMatrix to alter the colors. But since neither a blur effect nor an alpha blend can be done without direct pixel manipulation I did it all in one go. For the blur effect, see Christian Graus excellent article series Image Processing for Dummies with C# and GDI+. I’ve blogged earlier on how to perform alpha blending previously by drawing the blend using a PathGradientBrush or a LinearGradientBrush in Soft edged images in GDI+. This time I will calculate the alpha value instead. The calculation is done once for every scan line and is located in a virtual function so that this formula can be overridden.

All four “effects” are handled in this loop:

	for ( int y = height-1 ; y >= 0 ; y-- )
	{
		byte alpha = (byte)(255 * calculateAlphaFallout( (double)(height - y) / height ));
		Pixel* pS = (Pixel*)bdS.Scan0.ToPointer() + bdS.Width * (bdS.Height - y - FrameWidth - 1);
		Pixel* pD = (Pixel*)bdD.Scan0.ToPointer() + bdD.Width * y;
		for ( int x = bdD.Width ; x > 0 ; x--, pD++, pS++ )
		{
			int R = gaussBlur( &pS->R, nWidthInPixels );
			int G = gaussBlur( &pS->G, nWidthInPixels );
			int B = gaussBlur( &pS->B, nWidthInPixels );
			pD->R = (byte)((R * 3 + G * 2 + B * 2) / 7);
			pD->G = (byte)((R * 2 + G * 3 + B * 2) / 7);
			pD->B = (byte)((R * 2 + G * 2 + B * 3) / 7);
			pD->A = alpha;
		}
	}

Flipping happens on line 4, blurring on lines 8-10 (the gaussBlur function is just a one-liner). Color wash-out is done on lines 11-13 and the alpha fall-out on lines 3 and 14.

The very observant reader will notice that I let the blurring wrap from one edge to another. This is a hack, but it works since the left and right edges are always exactly identical. In production code, it might be a good idea to make the blurring optional (or even to provide a user-defined convolution matrix) and also to do the same from the color wash-out which currently uses hard-coded values.

3. Create the “half sheared” bitmap:

This transform cannot be accomplished using a linear transformation, but (after taking quite a a detour on this) I realized that it can be done embarrassingly simple:

	using ( Graphics g = Graphics.FromImage( Thumbnail ) )
		for ( int x = 0 ; x < sz.Width ; x++ )
			g.DrawImage(
				bmpFramed,
				new RectangleF( x, 0, 1, sz.Height - Skew * (float)(sz.Width - x) / sz.Width ),
				new RectangleF( x, 0, 1, sz.Height ),
				GraphicsUnit.Pixel );
&#91;/sourcecode&#93;

I simply use DrawImage to draw each column by its own, transferring one column from the framed image from step 1 to a column of different height. Note that it is extremely important that we pass <strong>float</strong>s and not <strong>int</strong>s - in the latter case the result will be a disaster.

<span style="font-size:x-large;">4.</span> Draw the reflection image through a shear transform, like this:


	using ( Graphics g = Graphics.FromImage( Thumbnail ) )
	{
		System.Drawing.Drawing2D.Matrix m = g.Transform;
		m.Shear( 0, (float)Skew / sz.Width);
		m.Translate( 0, sz.Height - Skew - 1 );
		g.Transform = m;
		g.DrawImage( bmpReflection, Point.Empty );
	}
 

Download demo source code

A Lesson Learned

At first, I tried to do the shearing in both step 3 & 4 myself using code similar to this (still one column at a time):

	// this was a bad idea
	private static void paintRowWithResize(
		BitmapData bdDst,
		BitmapData bdSrc,
		int nDstColumn,
		int nSrcColumn,
		int nDstRow,
		double dblSrcRow,
		int nRows,
		double dblStep )
	{
		unchecked
		{
			unsafe
			{
				Pixel* pD = (Pixel*)bdDst.Scan0.ToPointer() + nDstColumn + nDstRow * bdDst.Width;
				Pixel* pS = (Pixel*)bdSrc.Scan0.ToPointer() + nSrcColumn;
				while ( nRows -- > 0 )
				{
					int nYSrc = (int)dblSrcRow;
					Pixel p1 = pS[nYSrc * bdSrc.Width];
					Pixel p2 = p2 = pS[(nYSrc + 1) * bdSrc.Width];
					double frac2 = dblSrcRow - nYSrc;
					double frac1 = 1.0 - frac2;
					pD->R = (byte)(p1.R * frac1 + p2.R * frac2);
					pD->G = (byte)(p1.G * frac1 + p2.G * frac2);
					pD->B = (byte)(p1.B * frac1 + p2.B * frac2);
					pD->A = (byte)(p1.A * frac1 + p2.A * frac2);

					dblSrcRow += dblStep;
					pD += bdDst.Width;
				}
			}
		}
	}

The result looked perfectly good, but after thinking about it for awhile I realized that this piece of code actually is completely and utterly wrong in the general case: it only works when the alpha values of the two adjacent pixels are very close. In other cases the result will be poor.

Perhaps the easiest way to see this is to think about what happens when we want a 50% mix of a completely transparent pixel and a completely opaque while pixel. Intuitively I think it’s clear that we want the result to be a white pixel with 50% transparency. However, if we represent the transparent pixel with (0,0,0,0) (the most common value I’d guess, although (0,x,x,x) is transparent regardless of the value of x) we get a gray half transparent pixel instead (127,127,127,127). Not right at all. The reason I thought my attempt looked good in the first place was just because I had a gray border around the images!

So how do we mix pixels with alpha values? Obviously “normal” alpha blending is not sufficient when we have alpha values on both pixels… after thinking about this for a few minutes,  I said to myself “why not ask someone who knows instead?”. That someone is of course Graphics.DrawImage, and so I ended up with much cleaner code. And although I never bothered to figure out how to mix and blend pixels when both pixels contain alpha values I ended up realizing this:

Graphics.DrawImage has quite a bit of work to do when we draw a 32-bit bitmap on top of another. If we have some a-priori knowledge of the nature of the bitmaps we’re working with (are any of them totally opaque?) then it is actually possible to do this ourselves much faster than Graphics.DrawImage has a chance to, because it is forced to work with the general case: both bitmaps may be semi-transparent.

I will get back on how we in some cases can outperform Graphics.DrawImage (when it comes to speed), and hopefully a real life case when we actually bother. Stay tuned. Ekeforshus

12 Responses to “Thumbnails with glass table reflection in GDI+”

  1. […] Thumbnails with glass table reflection in GDI+ […]

  2. Kim Svedmark said

    Hej Dan,

    Thanks for publishing your ideas to the public community!

    I did almost the exact solution for a plane rotation a while back and I would like to suggest two small modifications for a better 3D effect: 1) make more of a trapezoid shape, and 2) make a slight horizontal squeeze. The trapezoid can be done simply by just offsetting the destination rectangle’s Y progressively (adding half of the total column skew for a middle height perspective effect – the progression can be adjusted for frog or bird perspectives). Something like this using your code: (no compiler at hand, so this is just to illustrate approximately what I mean)


    using ( Graphics g = Graphics.FromImage( Thumbnail ) )
    for ( int x = 0 ; x < sz.Width ; x++ )
    {
    float col_skew = Skew * (float)(sz.Width - x) / sz.Width;
    g.DrawImage(
    bmpFramed,
    new RectangleF( x, col_skew / 2, 1, sz.Height - col_skew),
    new RectangleF( x, 0, 1, sz.Height ),
    GraphicsUnit.Pixel );
    }

    However as I recall, this method brings with it an anti-alias issue since it will do full pixel steps in the top resulting in perceived non-anti-aliased upper skew. Don’t ask me why =] This can however be fixed by encapsuling the original image in a 1 pixel Transparent (or your preferred destination background color) border before the skewing is made…

    The horizontal squeeze gives an even better depth effect since the aspect of the original image won’t normally be the same when rotated. The algorithm of how much to squeeze is up to personal preference – how dramatic the final perspective should be… A bigger squeeze ends up looking like the image is closer to the viewpoint, and vice versa. You get the picture =]

  3. danbystrom said

    Hi Kim,
    thanks for your feedback. For the moment – I’ll leave this as an “exercise to the reader” :-). Maybe someone out there will enhance my code using your ideas? 🙂

    I fully agree that the perspective is a little odd, but as I wrote – I wanted to mimic Vista’s thumnails – and they really look that odd!

    One other thing that I missed is the reflection itself. Of course it need to be “in perspective” as well (it shole become smaller on the left side as well). It isn’t – and that is wrong. When the skew factor is small, it isn’t noticable, but as the skew factor increases, it will be worse.

    Ha dé gott!

  4. Dave said

    This throws a generic GDI+ exception on Vista x64 Changing the compiler from Any CPU to x86 I can run it. The offending code is …

    g.DrawImage(
    bmpSource,
    new Rectangle(FrameWidth, FrameWidth, szFull.Width – FrameWidth * 2, szFull.Height – FrameWidth * 2),
    new Rectangle(Point.Empty, bmpSource.Size),
    GraphicsUnit.Pixel);

  5. Nader said

    Hi,

    It gives “A generic error occurred in GDI+” error in the below code,Why?

    protected virtual Bitmap createFramedBitmap( Bitmap bmpSource, Size szFull )
    {

    Bitmap bmp = new Bitmap( szFull.Width, szFull.Height );
    using ( Graphics g = Graphics.FromImage( bmp ) )
    {
    g.FillRectangle( FrameBrush, 0, 0, szFull.Width, szFull.Height );
    g.DrawRectangle( BorderPen, 0, 0, szFull.Width – 1, szFull.Height – 1 );
    g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
    g.DrawImage(
    bmpSource,
    new Rectangle( FrameWidth, FrameWidth, szFull.Width – FrameWidth * 2, szFull.Height – FrameWidth * 2 ),
    new Rectangle( Point.Empty, bmpSource.Size ),
    GraphicsUnit.Pixel );
    }
    return bmp;
    }

    Thanks,

    Nader

    • danbystrom said

      It has been said earlier that this is connected to 64-bit Vista. Are you by any chance using that? I would suspect that the problem may lie in the way I dig out thumbnail images from the image files and not within the ReflectedThumbnail class itself.

    • Nader said

      Hi,

      I have XP. So, the problem returns to the DrawImage() function. I agree with you about thumbnail. Please, read these articles:

      http://support.microsoft.com/?id=814675
      And
      http://www.kerrywong.com/2007/11/15/understanding-a-generic-error-occurred-in-gdi-error/

      Please,find a solution for this great article. I believe it can be a good base for an image carousel component in .NET.

      Regards,

      Nader

      • danbystrom said

        It depends on the image file you’re displaying, but if it does contain an EXIF thumbnail image, maybe this code:
        foreach ( PropertyItem pi in img.PropertyItems )
        if ( pi.Id == 20507 )
        return (Bitmap)Image.FromStream( new MemoryStream( pi.Value ) );

        should be changed into:
        foreach ( PropertyItem pi in img.PropertyItems )
        if ( pi.Id == 20507 )
        return new Bitmap( (Bitmap)Image.FromStream( new MemoryStream( pi.Value ) ) );

        Just a thought – since I have never been able to reproduce the error on any of my systems I’m just guessing….

  6. Josh said

    AWESOME information. This is exactly what I was looking for but I am writing my program in vb.net. Any possibility anyone has translated it?

    Thanks! (And again, very cool!)

    Josh

    • danbystrom said

      Since VB.NET lacks pointers, a VB.NET version would be waaaaaay slower. You’d be better off just putting the class in a separate C# assembly and reference it from VB.NET.

  7. gdi said

    gdi…

    […]Thumbnails with glass table reflection in GDI+ « Dan Byström’s Bwain[…]…

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

 
%d bloggers like this: