## Image rotation with bilinear interpolation and no clipping

I wrote an article on image rotation with bilinear interpolation to calculate the pixel information. Commenter Jahn wanted to know if the resulting image could be obtained without clipping. I replied with the required code changes (it was fairly minimal), then I thought it might be useful to you too.

The code was cleaned up a bit, and here it is:

const int cnSizeBuffer = 20; // 30 deg = PI/6 rad // rotating clockwise, so it's negative relative to Cartesian quadrants const double cnAngle = -0.52359877559829887307710723054658; // use whatever image you fancy Bitmap bm = new Bitmap("slantedtreetopsky.jpg"); // general iterators int i, j; // calculated indices in Cartesian coordinates int x, y; double fDistance, fPolarAngle; // for use in neighbouring indices in Cartesian coordinates int iFloorX, iCeilingX, iFloorY, iCeilingY; // calculated indices in Cartesian coordinates with trailing decimals double fTrueX, fTrueY; // for interpolation double fDeltaX, fDeltaY; // pixel colours Color clrTopLeft, clrTopRight, clrBottomLeft, clrBottomRight; // interpolated "top" pixels double fTopRed, fTopGreen, fTopBlue; // interpolated "bottom" pixels double fBottomRed, fBottomGreen, fBottomBlue; // final interpolated colour components int iRed, iGreen, iBlue; int iCentreX, iCentreY; int iDestCentre; int iWidth, iHeight; int iDiagonal; iWidth = bm.Width; iHeight = bm.Height; iDiagonal = (int)(Math.Ceiling(Math.Sqrt((double)(bm.Width * bm.Width + bm.Height * bm.Height)))) + cnSizeBuffer; iCentreX = iWidth / 2; iCentreY = iHeight / 2; iDestCentre = iDiagonal / 2; Bitmap bmBilinearInterpolation = new Bitmap(iDiagonal, iDiagonal); for (i = 0; i < iDiagonal; ++i) { for (j = 0; j < iDiagonal; ++j) { bmBilinearInterpolation.SetPixel(j, i, Color.Black); } } // assigning pixels of destination image from source image // with bilinear interpolation for (i = 0; i < iDiagonal; ++i) { for (j = 0; j < iDiagonal; ++j) { // convert raster to Cartesian x = j - iDestCentre; y = iDestCentre - i; // convert Cartesian to polar fDistance = Math.Sqrt(x * x + y * y); fPolarAngle = 0.0; if (x == 0) { if (y == 0) { // centre of image, no rotation needed bmBilinearInterpolation.SetPixel(j, i, bm.GetPixel(iCentreX, iCentreY)); continue; } else if (y < 0) { fPolarAngle = 1.5 * Math.PI; } else { fPolarAngle = 0.5 * Math.PI; } } else { fPolarAngle = Math.Atan2((double)y, (double)x); } // the crucial rotation part // "reverse" rotate, so minus instead of plus fPolarAngle -= cnAngle; // convert polar to Cartesian fTrueX = fDistance * Math.Cos(fPolarAngle); fTrueY = fDistance * Math.Sin(fPolarAngle); // convert Cartesian to raster fTrueX = fTrueX + (double)iCentreX; fTrueY = (double)iCentreY - fTrueY; iFloorX = (int)(Math.Floor(fTrueX)); iFloorY = (int)(Math.Floor(fTrueY)); iCeilingX = (int)(Math.Ceiling(fTrueX)); iCeilingY = (int)(Math.Ceiling(fTrueY)); // check bounds if (iFloorX < 0 || iCeilingX < 0 || iFloorX >= iWidth || iCeilingX >= iWidth || iFloorY < 0 || iCeilingY < 0 || iFloorY >= iHeight || iCeilingY >= iHeight) continue; fDeltaX = fTrueX - (double)iFloorX; fDeltaY = fTrueY - (double)iFloorY; clrTopLeft = bm.GetPixel(iFloorX, iFloorY); clrTopRight = bm.GetPixel(iCeilingX, iFloorY); clrBottomLeft = bm.GetPixel(iFloorX, iCeilingY); clrBottomRight = bm.GetPixel(iCeilingX, iCeilingY); // linearly interpolate horizontally between top neighbours fTopRed = (1 - fDeltaX) * clrTopLeft.R + fDeltaX * clrTopRight.R; fTopGreen = (1 - fDeltaX) * clrTopLeft.G + fDeltaX * clrTopRight.G; fTopBlue = (1 - fDeltaX) * clrTopLeft.B + fDeltaX * clrTopRight.B; // linearly interpolate horizontally between bottom neighbours fBottomRed = (1 - fDeltaX) * clrBottomLeft.R + fDeltaX * clrBottomRight.R; fBottomGreen = (1 - fDeltaX) * clrBottomLeft.G + fDeltaX * clrBottomRight.G; fBottomBlue = (1 - fDeltaX) * clrBottomLeft.B + fDeltaX * clrBottomRight.B; // linearly interpolate vertically between top and bottom interpolated results iRed = (int)(Math.Round((1 - fDeltaY) * fTopRed + fDeltaY * fBottomRed)); iGreen = (int)(Math.Round((1 - fDeltaY) * fTopGreen + fDeltaY * fBottomGreen)); iBlue = (int)(Math.Round((1 - fDeltaY) * fTopBlue + fDeltaY * fBottomBlue)); // make sure colour values are valid if (iRed < 0) iRed = 0; if (iRed > 255) iRed = 255; if (iGreen < 0) iGreen = 0; if (iGreen > 255) iGreen = 255; if (iBlue < 0) iBlue = 0; if (iBlue > 255) iBlue = 255; bmBilinearInterpolation.SetPixel(j, i, Color.FromArgb(iRed, iGreen, iBlue)); } } bmBilinearInterpolation.Save("rotatedslantedtreetopsky.jpg", System.Drawing.Imaging.ImageFormat.Jpeg);

In the original article, the resulting image had the same dimensions as the source image. This meant that after rotation, some pixel information would be lost. To counter that, we make the dimensions of the resulting image “large enough”.

Considering that this is a rotation operation with an arbitrary angle, when all possible rotations of the source image are placed on top of one another, you get a smudgy circle. Not sure if you can visualise that. So the resulting image must be able to contain a circle, and because our images are stored in rectangular format, we need a square. That’s why in the code, I didn’t use a separate X and Y component of the dimensions, because they’re the same.

The width of the square has to be, at the minimum, the diagonal of the source image (which you can use Pythagoras’ Theorem to calculate). I added a buffer (of 20 pixels) to ensure that our square can contain the resulting image.

Here’s the source image:

And here’s the resulting image:

Alright, have fun with the code.