Image rotation with bilinear interpolation and alpha progressive borders

So a blog reader, Fabien Auréjac, emailed me an improvement over the code I posted on image rotation. Here’s the one with bilinear interpolation, and here’s the one with bilinear interpolation and no clipping.

You can find Fabien here and here (warning: French ahead), and also at Stack Overflow.

Fabien translated the core of my code into PHP. The improvement was on assigning alpha values to the edge pixels after rotation. Edge pixels are pixels beside the “blank” pixels (I used black in my code, for instance). The alpha values mean the edge pixels are “softer” and thus the resulting image looks smoother.

I suppose if you really want to, you could also “dumb down” the values of the red, green and blue colour components for more softening (in addition to the alpha component). I say “dumb down” because the blank pixels I used are black (meaning zero for the RGB values). You’re free to go ahead and do more interpolation.

Fabien has given permission for me to post the code here. I’ll leave it as an exercise for you to translate to your programming language.

$distOmbre=3;
$flouOmbre=4;
$angleRot=60;
$img=imagecreatefromjpeg("media/diapo-Chinon.jpg");
$size=getimagesize("media/diapo-Chinon.jpg");
$LsupH=($size[0]>$size[1])?1:0;
$angleBool=(int)($angleRot/90)%2==0?0:1;
if (($angleBool+$LsupH)%2==0) {
	$largeur=round(abs($size[0]*sin($angleRot%90*pi()/180))+abs($size[1]*sin((90-$angleRot%90)*pi()/180)));
	$hauteur=round(abs($size[0]*cos($angleRot%90*pi()/180))+abs($size[1]*cos((90-$angleRot%90)*pi()/180)));
} else {
	$largeur=round(abs($size[0]*cos($angleRot%90*pi()/180))+abs($size[1]*cos((90-$angleRot%90)*pi()/180)));
	$hauteur=round(abs($size[0]*sin($angleRot%90*pi()/180))+abs($size[1]*sin((90-$angleRot%90)*pi()/180)));
}
$largeur+=$distOmbre+$flouOmbre*2;
$hauteur+=$distOmbre+$flouOmbre*2;
$angleRot*=pi()/180;
$imgRot=imagecreatetruecolor($largeur, $hauteur);
imagealphablending($imgRot, true);
imageantialias($imgRot, true);
for ($i=0; $i<$hauteur; $i++) {
	for ($j=0; $j<$largeur; $j++) {
		// convert raster to Cartesian
        $x = $j - $largeur*0.5;
        $y = $hauteur*0.5 - $i;

        // convert Cartesian to polar
        $fDistance = sqrt($x * $x + $y * $y);
   	 	$fPolarAngle = atan2($y, $x);

        // the crucial rotation part
        // "reverse" rotate, so minus instead of plus
        $fPolarAngle -= $angleRot;
		 // convert polar to Cartesian
        $fTrueX = $fDistance * cos($fPolarAngle);
        $fTrueY = $fDistance * sin($fPolarAngle);

        // convert Cartesian to raster
        $fTrueX = $fTrueX + $size[0]*0.5;
        $fTrueY = $size[1]*0.5 - $fTrueY;

        $iFloorX = (int)(floor($fTrueX));
        $iFloorY = (int)(floor($fTrueY));
        $iCeilingX = (int)(ceil($fTrueX));
        $iCeilingY = (int)(ceil($fTrueY));
        //echo $fTrueX." ".$fTrueY." ".$iFloorX." ".$iCeilingX." ".$iFloorY." ".$iCeilingY."<br>";
		if ($iFloorX >= 0 && $iCeilingX >= 0 && $iFloorX < $size[0] && $iCeilingX < $size[0] && $iFloorY >= 0 && $iCeilingY >= 0 && $iFloorY < $size[1] && $iCeilingY < $size[1]) {
			$fDeltaX = $fTrueX - $iFloorX;
			$fDeltaY = $fTrueY - $iFloorY;
			$clrTopLeft = imagecolorat($img, $iFloorX, $iFloorY);
			$colorsTopLeft = imagecolorsforindex($img, $clrTopLeft);
			$clrTopRight = imagecolorat($img, $iCeilingX, $iFloorY); 
			$colorsTopRight = imagecolorsforindex($img, $clrTopRight);
			$clrBottomLeft = imagecolorat($img, $iFloorX, $iCeilingY);
			$colorsBottomLeft = imagecolorsforindex($img, $clrBottomLeft);
			$clrBottomRight = imagecolorat($img, $iCeilingX, $iCeilingY);
			$colorsBottomRight = imagecolorsforindex($img, $clrBottomRight);
			// linearly interpolate horizontally between top neighbours
			$fTopRed = (1 - $fDeltaX) * $colorsTopLeft['red'] + $fDeltaX * $colorsTopRight['red'];
			$fTopGreen = (1 - $fDeltaX) * $colorsTopLeft['green'] + $fDeltaX * $colorsTopRight['green'];
			$fTopBlue = (1 - $fDeltaX) * $colorsTopLeft['blue'] + $fDeltaX * $colorsTopRight['blue'];
			// linearly interpolate horizontally between bottom neighbours
			$fBottomRed = (1 - $fDeltaX) * $colorsBottomLeft['red'] + $fDeltaX * $colorsBottomRight['red'];
			$fBottomGreen = (1 - $fDeltaX) * $colorsBottomLeft['green'] + $fDeltaX * $colorsBottomRight['green'];
			$fBottomBlue = (1 - $fDeltaX) * $colorsBottomLeft['blue'] + $fDeltaX * $colorsBottomRight['blue'];
			// linearly interpolate vertically between top and bottom interpolated results
			$iRed = (int)(round((1 - $fDeltaY) * $fTopRed + $fDeltaY * $fBottomRed));
			$iGreen = (int)(round((1 - $fDeltaY) * $fTopGreen + $fDeltaY * $fBottomGreen));
			$iBlue = (int)(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;
			if ($iFloorX > 0 && $iCeilingX > 0 && $iFloorX < $size[0]-1 && $iCeilingX < $size[0]-1 && $iFloorY > 0 && $iCeilingY > 0 && $iFloorY < $size[1]-1 && $iCeilingY < $size[1]-1) {
				$colorallocation=imagecolorallocate($imgRot, $iRed, $iGreen, $iBlue);
				imagesetpixel($imgRot, $j, $i, $colorallocation);
			} else if ($iFloorX == 0 && $iFloorY >= 0 && $iCeilingY >= 0 && $iFloorY < $size[1] && $iCeilingY < $size[1]) {//left
				$alpha=round((1-abs($fDeltaX))*127);
				$colorallocation=imagecolorallocatealpha($imgRot, $iRed, $iGreen, $iBlue, $alpha);
				imagesetpixel($imgRot, $j, $i, $colorallocation);
			} else if ($iFloorX >= 0 && $iCeilingX >= 0 && $iFloorX < $size[0] && $iCeilingX < $size[0] && $iFloorY == 0) {//top
				$alpha=round((1-abs($fDeltaY))*127);
				$colorallocation=imagecolorallocatealpha($imgRot, $iRed, $iGreen, $iBlue, $alpha);
				imagesetpixel($imgRot, $j, $i, $colorallocation);
			} else if ($iCeilingX == $size[0]-1 && $iFloorY >= 0 && $iCeilingY >= 0 && $iFloorY < $size[1] && $iCeilingY < $size[1]) {//right
				$alpha=round(abs($fDeltaX)*127);
				$colorallocation=imagecolorallocatealpha($imgRot, $iRed, $iGreen, $iBlue, $alpha);
				imagesetpixel($imgRot, $j, $i, $colorallocation);
			} else if ($iFloorX >= 0 && $iCeilingX >= 0 && $iFloorX < $size[0] && $iCeilingX < $size[0] && $iCeilingY == $size[1]-1) {//bottom
				$alpha=round(abs($fDeltaY)*127);
				$colorallocation=imagecolorallocatealpha($imgRot, $iRed, $iGreen, $iBlue, $alpha);
				imagesetpixel($imgRot, $j, $i, $colorallocation);
			}
		}
	}
}

Fabien is French (I think), which is why you get variable names such as distOmbre (shadow distance?), flouOmbre (fuzzy shadow?), largeur (width), hauteur (height). And this one took me a bit more time to translate… LsupH is probably “width greater than height?”. The “L” probably refers to “largeur”, and “H” refers to “hauteur”.

Reading international programming code is fun. *smile*

There’s also an interesting piece of code:

$size=getimagesize("media/diapo-Chinon.jpg");
$LsupH=($size[0]>$size[1])?1:0;
$angleBool=(int)($angleRot/90)%2==0?0:1;
if (($angleBool+$LsupH)%2==0) {
	$largeur=round(abs($size[0]*sin($angleRot%90*pi()/180))+abs($size[1]*sin((90-$angleRot%90)*pi()/180)));
	$hauteur=round(abs($size[0]*cos($angleRot%90*pi()/180))+abs($size[1]*cos((90-$angleRot%90)*pi()/180)));
} else {
	$largeur=round(abs($size[0]*cos($angleRot%90*pi()/180))+abs($size[1]*cos((90-$angleRot%90)*pi()/180)));
	$hauteur=round(abs($size[0]*sin($angleRot%90*pi()/180))+abs($size[1]*sin((90-$angleRot%90)*pi()/180)));
}

So here’s your mission, should you choose to accept it (I recently watched Mission Impossible…). What is Fabien trying to accomplish in that section of code? Hint: it has something to do with getting a “nice” resulting image width and height.

I’ll tell you a more “elegant” alternative to that code section. But it’ll involve some mathematics. And drawings. Prepare for poorly drawn diagrams…

Sorry, comments are closed, but you can contact me directly if you like.