-
图像旋转需要一个圆心,通常以图像的中心點为圆心,图像旋转经过三个步骤:
-
从图像坐标系转换为以圆心为原点的直角坐标系。
-
通过旋转算法,将坐标旋转。
-
再将旋转后图像从直角坐标系转换回图像坐标系。
如图:
-
旋转算法:
-
坐标系变换:
- 由图象坐标系换成直角坐标系:。(x , y 为直角坐标)
- 由直角坐标系换成图象坐标系:。(x , y 为图象坐标)
-
坐标旋转:
- Pixel到原点的距离为r与原点的夹角为 ,则Pixel坐标可以表示为: 。
- 当图象顺时针旋转度,则Pixel坐标为: , 如图:。
即:。
-
旋转算法最终公式:
- 顺时针旋转:图像坐标 --> 直角坐标 --> 图像旋转 --> 图像坐标 。
- 逆运算:用于反向映射。
-
两种旋转方式:
- 正向失真映射:当图像旋转时,原图Pixel的坐标经过旋转公式得到的新Pixel坐标通常不会是整数,而图像的Pixel坐标都是以整数记录的。所以,我们会采用四舍六入五凑偶的方式。但是,如果采用近似,那么就会出现多个Pixel旋转后都对应到同一个整数坐标,而有些整数坐标点却没有pixel对应,这样就会出现空洞(失真)。另外对应多个Pixel的点,该点Pixel值是按先后顺序采用覆盖的方式。
- 反向映射:为了避免出现空洞(失真),我们采用反向映射(其中正向映射也能采用相同方法避免空洞)。原理是对旋转后的图片,其整数坐标进行逆变换,可得到对应在原图中的坐标。通常得到的原图坐标也不会是整数,但是我们可以采用下面两种方法取得Pixel然后赋值给旋转后的图片。
- 如图:
- 最邻近插值:直接对原图中映射的非整数坐标进行四舍五入,得到的整数坐标,取Pixel值赋值给旋转图像中对应的点。
- 双线性插值:先说说线性插值。如下图:已知 (x0, y0) 、(x1, y1)、x,求 y。根据斜率相同:即可得:。
现在來看看什么是双线性插值法,说白了就是进行两次线性插值。如下图:(x,y) 为映射到原图的pixel点,是位于(0,0)、(1,0)、(0,1)、(1,1)中的点。f(0,0)、f(1,0)、f(0,1)、f(1,1)为各点的Pixel值,求 (x,y)的pixel值 f(x,y) 。根据线性插值,先进行x方向线性插值:
在(0,0)、(1,0)之间:f(x,0) = f(0,0)*(1-x)/1 + f(1,0)*(x-0) /1 ;
在(0,1)、(1,1)之间:f(x,1) = f(0,1)*(1-x)/1 + f(1,1)*(x-0) /1。
得到 f(x,0) 、f(x,1) 后再进行 y 方向上的线性插值:f(x,y) = f(x,0)*(1-y)/1 + f(x,1)*(y-0)/1 。
f(x,y)即按比例权重求得的Pixel值赋值回旋转图像中对应的点。
-
C#实现正向失真和反向双线性:
C#存在Cos、Sin精度问题,即 Cos(Pi/2)、Sin(Pi) 等,不会等于0之类。目前没有想到很好的解决办法,有人知道的话请指教。
//正向失真旋转
public Bitmap rotateImageDis(Bitmap Image, double degree)
{
int Wo = Image.Width;
int Lo = Image.Height;
//采用向上取整,求旋转后图像大小。
//double Wt = Math.Ceiling(Math.Abs(Wo * Math.Cos(Math.PI * (degree / 180))) + Math.Abs(Lo * Math.Sin(Math.PI * (degree / 180)))-0.00001);
//double Lt = Math.Ceiling(Math.Abs(Wo * Math.Sin(Math.PI * (degree / 180))) + Math.Abs(Lo * Math.Cos(Math.PI * (degree / 180)))-0.00001);
//采用四舍五入,求旋转后图像大小。
double Wt = (int)(Math.Abs(Wo * Math.Cos(Math.PI * (degree / 180))) + Math.Abs(Lo * Math.Sin(Math.PI * (degree / 180))) + 0.5);
double Lt = (int)(Math.Abs(Wo * Math.Sin(Math.PI * (degree / 180))) + Math.Abs(Lo * Math.Cos(Math.PI * (degree / 180))) + 0.5);
Bitmap rotateImageData = new Bitmap((int)Wt, (int)Lt, PixelFormat.Format24bppRgb);
//x、y即原图坐标,x1、y1即旋转后坐标。
for (int y = 0; y < Lo; y++)
{
for (int x = 0; x < Wo; x++)
{
int x1 = (int)Math.Round(x * Math.Cos(Math.PI * (degree / 180)) - y * Math.Sin(Math.PI * (degree / 180)) - (Wo - 1) / 2.0 * Math.Cos(Math.PI * (degree / 180)) + (Lo - 1) / 2.0 * Math.Sin(Math.PI * (degree / 180)) + (Wt - 1) / 2.0);
int y1 = (int)Math.Round(x * Math.Sin(Math.PI * (degree / 180)) + y * Math.Cos(Math.PI * (degree / 180)) - (Wo - 1) / 2.0 * Math.Sin(Math.PI * (degree / 180)) - (Lo - 1) / 2.0 * Math.Cos(Math.PI * (degree / 180)) + (Lt - 1) / 2.0);
rotateImageData.SetPixel(x1, y1, Image.GetPixel(x, y));
}
}
return rotateImageData;
}
//反向双线性插值法
public Bitmap rotareImageOri(Bitmap Image, double degree)
{
int Wo = Image.Width;
int Lo = Image.Height;
//采用向上取整,求旋转后图像大小。
//double Wt = Math.Ceiling(Math.Abs(Wo * Math.Cos(Math.PI * (degree / 180))) + Math.Abs(Lo * Math.Sin(Math.PI * (degree / 180))) - 0.0001);
//double Lt = Math.Ceiling(Math.Abs(Wo * Math.Sin(Math.PI * (degree / 180))) + Math.Abs(Lo * Math.Cos(Math.PI * (degree / 180))) - 0.0001);
//采用四舍五入,求旋转后图像大小。
double Wt = (int)(Math.Abs(Wo * Math.Cos(Math.PI * (degree / 180))) + Math.Abs(Lo * Math.Sin(Math.PI * (degree / 180))) + 0.5);
double Lt = (int)(Math.Abs(Wo * Math.Sin(Math.PI * (degree / 180))) + Math.Abs(Lo * Math.Cos(Math.PI * (degree / 180))) + 0.5);
Bitmap rotateImageData = new Bitmap((int)Wt, (int)Lt, PixelFormat.Format24bppRgb);
//x1、y旋转后图像坐标,x、y原图坐标。
for (int y1 = 0; y1 < Lt; y1++)
{
for (int x1 = 0; x1 < Wt; x1++)
{
double x = x1 * Math.Cos(Math.PI * (degree / 180)) + y1 * Math.Sin(Math.PI * (degree / 180)) - (Wt - 1) / 2.0 * Math.Cos(Math.PI * (degree / 180)) - (Lt - 1) / 2.0 * Math.Sin(Math.PI * (degree / 180)) + (Wo - 1) / 2.0;
double y = -x1 * Math.Sin(Math.PI * (degree / 180)) + y1 * Math.Cos(Math.PI * (degree / 180)) + (Wt - 1) / 2.0 * Math.Sin(Math.PI * (degree / 180)) - (Lt - 1) / 2.0 * Math.Cos(Math.PI * (degree / 180)) + (Lo - 1) / 2.0;
if (-0.001 <= x & x <= (Wo - 1) & -0.001 <= y & y <= (Lo - 1))
{
Color RGB = new Color();
int a1 = (int)x;
int b1 = (int)y;
int a2 = (int)Math.Ceiling(x);
int b2 = (int)y;
int a3 = (int)x;
int b3 = (int)Math.Ceiling(y);
int a4 = (int)Math.Ceiling(x);
int b4 = (int)Math.Ceiling(y);
double xa13 = x - a1;
double xa24 = a2 - x;
double yb12 = y - b1;
double yb34 = b3 - y;
if (xa13 != 0 & xa24 != 0 & yb12 != 0 & yb34 != 0)
{//对应回原图是非整数坐标,双线性插值。
byte R1 = Image.GetPixel(a1, b1).R;
byte G1 = Image.GetPixel(a1, b1).G;
byte B1 = Image.GetPixel(a1, b1).B;
byte R2 = Image.GetPixel(a2, b2).R;
byte G2 = Image.GetPixel(a2, b2).G;
byte B2 = Image.GetPixel(a2, b2).B;
byte R3 = Image.GetPixel(a3, b3).R;
byte G3 = Image.GetPixel(a3, b3).G;
byte B3 = Image.GetPixel(a3, b3).B;
byte R4 = Image.GetPixel(a4, b4).R;
byte G4 = Image.GetPixel(a4, b4).G;
byte B4 = Image.GetPixel(a4, b4).B;
byte R = (byte)((R1 * xa24 + R2 * xa13) * yb34 + (R3 * xa24 + R4 * xa13) * yb12);
byte G = (byte)((G1 * xa24 + G2 * xa13) * yb34 + (G3 * xa24 + G4 * xa13) * yb12);
byte B = (byte)((B1 * xa24 + B2 * xa13) * yb34 + (B3 * xa24 + B4 * xa13) * yb12);
RGB = Color.FromArgb(R, G, B);
}
else
{//对应回原图是整数坐标,直接取Pixel。
RGB = Image.GetPixel(a1, b1);
}
rotateImageData.SetPixel(x1, y1, RGB);
}
}
}
return rotateImageData;
}
仅为个人理解,如有不足,请指教。 https://blog.csdn.net/weixin_35811044