前一阵朋友碰到这么一道题:将图像原地顺时针旋转90度,不开辟新空间。此题看似平易(题目简短),仔细研究发现着实不容易。经过一番探索后,终于找到了正确的算法,但是当使用opencv实现时,有碰到了困难而且费了一番周折才找到问题所在。
首先,解决这个问题,先简化成原地90度旋转一M×N的矩阵A(注意不是N×N方阵)。对于2×3的矩阵A = {1,2,3;4,5,6},其目标为矩阵B = {4,1;5,2;6,3}。因为是原地旋转,这里A和B应指向同一大小为6的内存空间。
这里有这样一个重要的导出公式,就是 B[i][j] = A[M-1-j][i],读者可自己推敲,是后面算法的基石。
好了,这样如果B指向的是一块新开辟内存,问题就简单了,只要遍历B的空间按照上面公式取A元素赋值即可。但是,对于原地旋转就面临一个问题,给B[i][j]赋值时,该位置的原先值被覆盖,如何处理?直观的解决方案是交换元素,即将B[i][j]和A[M-1-j][i]两处元素交换。但是新问题出来了,如果简单采用此交换策略问题如下:
注意:A和B指向同一大小为6的内存空间,首地址为同一叫做S,则B[i][j] = S[i*M+j],A[i][j] = S[i*N+j]。
S S+5
1 2 3 4 5 6
B[0][0] <-> A[1][0] (内存空间中元素1和4交换位置)
B[0][1] <-> A[0][0] (内存空间中元素2应和原先的元素1交换,但是该位置现在已经不是1了)
所以,需要有一种新策略,如果发现待交换位置的元素已经被更换了,需要继续查找到该元素被换到哪里了。新算法核心伪代码如下:(参考)
for i = 0 to N-1:
for j = 0 to M-1:
// B[i][j] 前的元素均已正确
if (i*M+j) < ((M-1-j)*N+i): // 待替换元素在B[i][j]位置后面
swap(B[i][j], A[M-1-j][i]);
else: // 待替换元素在B[i][j]前面,已经被替换过,需要迭代查找该元素被替换到哪里,肯定在B[i][j]后面
tar_i = M-1-j;
tar_j = i;
while (i*M+j) > (tar_i*N+tar_j):
pos = tar_i*N+tar_j;
tmp_i = pos / M;
tmp_j = pos % M;
tar_i = M-1-tmp_j;
tar_j = tmp_i;
swap(B[i][j], A[tar_i][tar_j])
上述代码可以实现任意矩阵原地90度顺时旋转,按理说彩色图像只不过是3通道每个位置由3个元素组成而已,用上述算法应该同样能够搞定,采用opencv写出了如下程序:
#include <cv.h> #include <highgui.h> void swap(char& a, char& b){ char c = a; a = b; b = c; } int main() { IplImage* img = cvLoadImage("test.jpg"); cvShowImage("src", img); int w = img->width; int h = img->height; for (int i = 0; i < w; i++) { for (int j = 0; j < h; j++) { int I = h - 1 - j; int J = i; while ((i*h + j) > (I*w + J)) { int p = I*w + J; int tmp_i = p / h; int tmp_j = p % h; I = h - 1 - tmp_j; J = tmp_i; } swap(*(img->imageData + i*h*3 + j*3 + 0), *(img->imageData + I*w*3 + J*3 + 0)); swap(*(img->imageData + i*h*3 + j*3 + 1), *(img->imageData + I*w*3 + J*3 + 1)); swap(*(img->imageData + i*h*3 + j*3 + 2), *(img->imageData + I*w*3 + J*3 + 2)); } } img->width = h; img->height = w; img->widthStep = h*3; cvShowImage("dst", img); cvWaitKey(); cvReleaseImage(&img); return 0; }
然而,心满意足之时,忽然出现了意外,个别图出现了如下效果:
这里让人非常沮丧,算法难道错了么?仔细思考后发现算法是没有漏洞的,那问题出在哪里呢?经过一番思考,终于找到这个潜在的问题。
请仔细看上面的C代码,你会发现笔者假定了一件事,就是认为 img->widthStep = img->width * img->nChannels,这其实就是潜在的隐患!我们通常直觉认为图像imageData这个一维数组的存储就是每行的w×3个字节连续存储h段,然而实际上是每行widthStep个字节连续存储h段,但widthStep并不总是等于w×3,往往会多出几个字节。因为在32位机上,opencv分配字节要按4字节对齐。所以如上问题图w = 706,本来应分配2118个字节,实际上却是分配2120个字节(4的倍数),每行2个冗余字节。
这点在平时很难察觉到,但在原地旋转图片这个问题中就凸显出来,因为冗余字节的存在,旋转前后opencv图像需要空间可能不一样。例如,原图700×401,图像字节大小为700×401×3,而选转90度后401×700,图像字节大小变为404×700×3,说明原空间大小是不足以表示新图的。
综上,如果真的碰到原地旋转图像90度这种问题的话,较简便的解决方法是先将图片就近resize到宽和高为4的倍数的值。
原文地址:http://blog.csdn.net/seanwang_25/article/details/43273593