首页 > 代码库 > opencv图像原地(不开辟新空间)顺时旋转90度

opencv图像原地(不开辟新空间)顺时旋转90度

前一阵朋友碰到这么一道题:将图像原地顺时针旋转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的倍数的值。

opencv图像原地(不开辟新空间)顺时旋转90度