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.