一、坐标旋转:
假设我要沿着图像M的中心旋转角度b,那么需要先将图像M的中心移动到与坐标轴中心重合(否则将导致图像M沿坐标轴原点旋转):
然后假设原坐标(x,y)距离坐标轴原点距离为r,与x轴夹角a度,需要绕原点旋转b度,那么可有如下推导:
设宽高分别为w,h
原坐标为:
x = r * cos(a)
y = r * sin(a)
新坐标为
x_new = r * cos(a+b) = r*cos(a)*cos(b) - r*sin(a)*sin(b)
=(x-w/2)*cos(b) - (y-h/2)*sin(b)
y_new = r * sin(a+b) = r*sin(a)*cos(b) + r*cos(a)*sin(b)
=(y-h/2)*cos(b) + (x-w/2)*sin(b)
至此即可通过以上规律求得原图像中每一个二维坐标,在旋转后对应的新坐标,然后在新坐标中赋值原来的颜色值,即可实现普通图像的任意角度旋转
- 新容器面积
由于旋转后的图像边角会超出原有的图像边框,因此需要重算旋转后图像的容器大小。
从图像可知,
width_new = |cos(b)*w| + |sin(b)*h|
height_new = |cos(b)*h| + |sin(b)*w|
- 边框偏移
由于转换后的图像坐标部分在负坐标轴,原因如图:
(深灰色为新容器,浅灰色为旧容器)
所以需要通过加上偏移值使得移出负坐标变正。
从图易知,x偏移值为(width_new - w) / 2,记为offsetX, y偏移值为(height_new - h) / 2,记为offsetY,旋转后得到的坐标加上偏移值即可在新容器中完整包裹。
- 填充方式
通过三角函数算出第一行的起点和终点,通过Bresenham直线算法算出符合w长度的一系列直线坐标,再算出最后一行的起点,从而求得每行x的偏移距离ddx和y偏移距离ddy,每行的坐标值除了加上offsetX和offsetY外,加上偏移距离ddx*当前已遍历行数和ddy*当前遍历行数,即可把旧容器的数据逐行转移到新容器中
- 空洞现象以及处理方式
由于计算机图像是由无数像素组成,并非真正的连续量,因此倾斜的线条不一定总是能咬合完美,就入下图的情况:
此时旋转后的画面将会呈现无数空洞,非常难看,一个简单的间接办法是把源数据两行同一个x值的数据,求平均后填充于容器的x-1位置上,从而得到补全空洞,得到更优质的图像:
- YUV420的旋转处理
对比位图和灰度图,YUV420格式的特殊之处在于其分为两层,一层为Y层表达亮度,
一层为UV层,每两行两列共4个y享有一对UV,因此UV填充时,一定要以U开头V结尾为一个单位,否则将会产生严重的色差;对应的行号也因为UV层高只有Y层的一半而要除以2。也因为这个原因,在新容器的宽度和高度上,若求得得数为奇数,则必须+1,以免读写出错。
若要旋转YUV420格式的图像,需要把容器的一维数组长度从[width_new * height_new]提升到[width_new * height_new * 3 / 2]以扩充出UV层的容器。
填充数据时,设当前遍历旧容器时的行为y,列为x,判断当前new_x%2是否等于0,若不是则不进行下一步,是则再判断x%2运算是否为0,是则取值x,记为uvX,如果不是话则uvX取值x-1。在遍历过程中,求得在新容器中偏移值分别为
[width_new * height_new + y_new / 2 * width_new + x_new] 和
[width_new * height_new + y_new / 2 * width_new + x_new + 1]
并把容器中这两个单元的值分别赋为旧容器的[w * h + y / 2 * w + uvX]和[w * h + y / 2 * w + uvX + 1]的值(row为当前遍历到源数据的第几行),这样即可以2个单元为一组地把UV数据写到新容器中。空洞的处理办法和第五部分一样。
最后实现效果如下:
输入原图:
旋转28度:
旋转90度:
实现代码已放到GitHub上,如果有用欢迎Star: