読者です 読者をやめる 読者になる 読者になる

デジタル・デザイン・ラボラトリーな日々

アラフィフプログラマーが数学と物理を基礎からやり直す

画像の回転処理 穴が空かない方法

今回は、画像をCanvasのRotateメソッドを使用しないで自前で回転させてみます。
使用する画像は、いつもの猫画像です。
f:id:Yaju3D:20130223142237j:plain

画像の回転は、画像を構成する全ピクセルの回転後の座標を計算して点を打つだけです。

回転をする場合の入力点(x1,y1)を、画像の中心(cx,cy)を回転の中心としてA度回転させる場合の出力点の座標(x2,y2)は下記の式となります。

x2 = (x1-cx) * cos(-A) - (y1-cy) * sin(-A) + cx 
y2 = (x1-cx) * sin(-A) + (y1-cy) * cos(-A) + cy

しかし、この計算式を使って実際に画像を回転させてみると、下記の画像のように穴が空いてしまいます。
f:id:Yaju3D:20130430230100j:plain

これはコンピュータ画像は1ピクセルが最小単位となっており、小数点がある座標(0.5,0.5)等を指定した場合には、どちらかに寄せる必要があります。例えば四捨五入した場合では、X方向なら右へ、Y方向なら下に寄ることになります。
f:id:Yaju3D:20130430230125j:plain

※ちなみにHTML5Canvasでは整数以外の座標でレンダリングすると、線が滑らかになるよう自動的にアンチエイリアスが使用されます。実は回転時の点を打つ際に最初はRectangleメソッドを使って実装していたのですが、穴があきませんでした。穴はアンチエイリアスの機能によって勝手に埋まってしまっていたのです。その為、createImageDataメソッドによる方法に変更しました。

//回転描画(穴あく方式)
function drawRotate_Hole(degrees, offset){

	var ctx = cvs.ctx;
	var rad = -degrees / 180 * Math.PI;
	var ct = new Point(texture.width / 2  , texture.height / 2);

	var dstWidth = (Math.abs(texture.width * Math.cos(rad)) + Math.abs(texture.height * Math.sin(rad))) | 0;
	var dstHeight = (Math.abs(texture.width * Math.sin(rad)) + Math.abs(texture.height * Math.cos(rad))) | 0;
	var pixelImage = ctx.createImageData(dstWidth, dstHeight);

	data = ctx.getImageData(0, 0, texture.width, texture.height).data;

	var sx = (dstWidth / 2) | 0;
	var sy = (dstHeight / 2) | 0;

	ctx.fillStyle = "black";
	for(var y=0; y<texture.height; y++){
		for(var x=0; x<texture.width; x++){
			var n = (y * texture.width + x) * 4;
			var pt = rotate2d(x - ct.x, y - ct.y, rad);
			var R = data[n + 0];
			var G = data[n + 1];
			var B = data[n + 2];
			var A = data[n + 3];
			
			var ptr = ((pt.y + sy) * dstWidth + (pt.x + sx)) * 4;
			pixelImage.data[ptr + 0] = R;
			pixelImage.data[ptr + 1] = G;
			pixelImage.data[ptr + 2] = B;
			pixelImage.data[ptr + 3] = A;
		}
	}
	
	ctx.putImageData(pixelImage, offset.x, offset.y);
	
	ctx.strokeRect(offset.x, offset.y, dstWidth, dstHeight);
}

では、画像の回転させた場合に穴が空かないようにするにはどうしたらいいでしょう?
f:id:Yaju3D:20130430230100j:plain

実は単純な逆転の発想です、入力座標から出力座標で計算させると整数にする際に誤差によって穴が空くわけですから、反対に出力座標から入力座標に逆計算させてあげればいいのです。
f:id:Yaju3D:20130430234347j:plain

このときに用いる計算式は下記になります。逆計算のため、Aは-AからAと符号が変わっています。

x1 = (x2-cx) * cos(A) - (y2-cy) * sin(A) + cx 
y1 = (x2-cx) * sin(A) + (y2-cy) * cos(A) + cy
//回転描画(穴あかない方式)
function drawRotate_NoHole(degrees, offset){

	var ctx = cvs.ctx;
	var rad = degrees / 180 * Math.PI;
	var ct = new Point(texture.width / 2  , texture.height / 2);

	var dstWidth = (Math.abs(texture.width * Math.cos(rad)) + Math.abs(texture.height * Math.sin(rad))) | 0;
	var dstHeight = (Math.abs(texture.width * Math.sin(rad)) + Math.abs(texture.height * Math.cos(rad))) | 0;
	var pixelImage = ctx.createImageData(dstWidth, dstHeight);

	data = ctx.getImageData(0, 0, texture.width, texture.height).data;

	var sx = (dstWidth / 2) | 0;
	var sy = (dstHeight / 2) | 0;

	ctx.fillStyle = "black";
	for(var y=0; y<dstHeight; y++){
		for(var x=0; x<dstWidth; x++){
			var pt = rotate2d(x - sx, y - sy, rad);
			if((pt.x + ct.x) < 0 || (pt.y + ct.y) < 0 ||
			   (pt.x + ct.x) > texture.width || (pt.y + ct.y) > texture.height) continue;
			var n = ((pt.y + ct.y) * texture.width + (pt.x + ct.x)) * 4;
			var R = data[n + 0];
			var G = data[n + 1];
			var B = data[n + 2];
			var A = data[n + 3];
			
			var ptr = (y * dstWidth + x) * 4;
			pixelImage.data[ptr + 0] = R;
			pixelImage.data[ptr + 1] = G;
			pixelImage.data[ptr + 2] = B;
			pixelImage.data[ptr + 3] = A;
		}
	}
	
	ctx.putImageData(pixelImage, offset.x, offset.y);
	
	ctx.strokeRect(offset.x, offset.y, dstWidth, dstHeight);
}

参考サイト
C言語による画像回転処理について

上記を踏まえてツールを作成してみました。