关于图像的球面贴合,是全景应用中比较常见的技术,而现有的一些资源大多不太好,比较晦涩。在经过一段时间的摸索之后,发现了这个博客写的相对可以,本文的实现也将其作为重要的参考,如果看过本文之后有什么不明白或者觉得不好的地方可以去看看。
在展开本文之前,先来看看下面的两张图片:
左边的图像被贴合到球面上后,其正视图为右边的图像。而我们要研究的是,如何去贴合的过程。
可以想象,左边的图像是一张极薄的纱,将其蒙到一个大小正好的球面上(薄纱过中点的横轴正好覆盖球赤道的半周),然后正看过去会怎么样。我们将看到一个接近于上面右边图像的图像。为什么说接近而不说相同呢,因为大小是不一样的。左边图像的赤道如果刚刚好覆盖掉球的半个平面,则球的周长将为左边图像的宽度的两倍。假定左边图像的宽度为W,而球的半径为R,则有
πR=W
所以球的正视图的原直径应该为
2R=2*W/π<W
即球的正视图不会触碰到图像边缘,而为了我们看起来舒服一点,我们希望球的正视图刚刚好就触碰到图像边缘,是故球做完贴合之后其正视图需要做一个等比例的放大,该放大系数为π/2。经过放大,我们就得到了右边的图像。之后,我们把放大系数设为k(注意这里的k值跟图像贴合的球体半径有关)。
下面我们正式进入图像贴合的研究,还是遵循上面的设想,把左边图像设想成一张极薄的纱,然后将其蒙到一个大小正好的球面上。那么,左边图像的中点将是右边图像的中点,也就是球正面顶点的位置。
接着,我们将图像的中心点记为点0,然后在左图像上随便取一个点即为点A,假设点A是落在过中点的横轴上的,很容易想象经过贴合后A也将落在右图过中心点的赤道上。进一步想,左图绕中心旋转一个角度之后,原来不在过中点的横轴上的点可能变为其上的点,相应的贴合后的点也会落在右图过中心点的赤道上。也就是说,图像上的任意一点经过贴合之后,将落在原图与中心点连线的线上。接着我们沿着连线切下将得到下面的切面:
这便是这篇文章推导的关键,其中最为关键的是弧长等于OA*k这句话(由上面的薄纱模型很容易得出该弧为左图中的OA经过弯曲而来,长度自然相同,而*k是因为图像经过了放大),有了上面这些条件,我们可以列出以下这些公式:
推导到最后我们可以看到原图像坐标(X,Y)和贴合后图像坐标(X',Y')的换算关系,其跟上面提到的博客最大不同的地方是将X,Y写在左边而X',Y'写在右边。在本人看来,这样才是合理的,因为后面我们需要去原图像找到对应的像素点取像素值,而映射后图像遍历时坐标是知道的,应该为已知条件。详细的可参考我之前写过的 C/C++ BMP(24位真彩色)图像处理(3)------图像の放大缩小(双线性插值)。
OK,到这里推导过程就全部结束了,按照上面的公式便可完成图像的映射。其主要的代码如下:
void DealWithImgData(BYTE *srcdata, BYTE *drcdata,int width,int height)//参数一为原图像的数据区首指针,参数二为贴合后图像的数据区首指针,参数三为图像的宽,参数四为图像的高 { int l_width = WIDTHBYTES(width* 24);//计算位图的实际宽度并确保它为4byte的倍数 double radius1 = height /2;//贴合球面半径 double radius2 = radius1*radius1;//半价的平方 double x1, y1;//目标在球正视图中的坐标位置 double x, y;//目标在球正视图中对应原图的坐标位置 double middle2 = 2 * radius1 / 3.1416;//计算过程式子 double matan;//目标与圆心连线与x轴的夹角 int pixel_point;//遍历图像指针 int pixel_point_row;//遍历图像行首指针 double oa;//点对应弧长度 //双线性插值算法相关变量 int i_original_img_hnum, i_original_img_wnum;//目标点坐标 double distance_to_a_y, distance_to_a_x;//在原图像中与a点的水平距离 int original_point_a, original_point_b, original_point_c, original_point_d; for (int hnum = 0; hnum < height; hnum++) { pixel_point_row = hnum*l_width; for (int wnum = 0; wnum < width; wnum++) { if ((hnum - height / 2)*(hnum - height / 2) + (wnum - width / 2)*(wnum - width / 2) < radius2)//在球体视场内才处理 { pixel_point = pixel_point_row + wnum * 3;//数组位置偏移量,对应于图像的各像素点RGB的起点 /***********球面贴合***********/ x1 = wnum - width / 2; y1 = height / 2 - hnum; if (x1 != 0) { oa = middle2*asin(sqrt(y1*y1 + x1*x1) / radius1);//这里在确定图像大小的情况下可以用查表法来完成,这样会大大的提高其效率 matan = atan2(y1, x1); x = cos(matan)*oa; y = sin(matan)*oa; } else { y = asin(y1 / radius1)*middle2; x = 0; } /***********球面贴合***********/ /***********双线性插值算法***********/ i_original_img_hnum = (height / 2 - y); i_original_img_wnum = (x + width / 2); distance_to_a_y = (height / 2 - y) - i_original_img_hnum; distance_to_a_x = (x + width / 2) - i_original_img_wnum;//在原图像中与a点的垂直距离 original_point_a = i_original_img_hnum*l_width + i_original_img_wnum * 3;//数组位置偏移量,对应于图像的各像素点RGB的起点,相当于点A original_point_b = original_point_a + 3;//数组位置偏移量,对应于图像的各像素点RGB的起点,相当于点B original_point_c = original_point_a+ l_width;//数组位置偏移量,对应于图像的各像素点RGB的起点,相当于点C original_point_d = original_point_c + 3;//数组位置偏移量,对应于图像的各像素点RGB的起点,相当于点D if (hnum == height - 1) { original_point_c = original_point_a; original_point_d = original_point_b; } if (wnum == width - 1) { original_point_a = original_point_b; original_point_c = original_point_d; } drcdata[pixel_point + 0] = srcdata[original_point_a + 0] * (1 - distance_to_a_x)*(1 - distance_to_a_y) + srcdata[original_point_b + 0] * distance_to_a_x*(1 - distance_to_a_y) + srcdata[original_point_c + 0] * distance_to_a_y*(1 - distance_to_a_x) + srcdata[original_point_c + 0] * distance_to_a_y*distance_to_a_x; drcdata[pixel_point + 1] = srcdata[original_point_a + 1] * (1 - distance_to_a_x)*(1 - distance_to_a_y) + srcdata[original_point_b + 1] * distance_to_a_x*(1 - distance_to_a_y) + srcdata[original_point_c + 1] * distance_to_a_y*(1 - distance_to_a_x) + srcdata[original_point_c + 1] * distance_to_a_y*distance_to_a_x; drcdata[pixel_point + 2] = srcdata[original_point_a + 2] * (1 - distance_to_a_x)*(1 - distance_to_a_y) + srcdata[original_point_b + 2] * distance_to_a_x*(1 - distance_to_a_y) + srcdata[original_point_c + 2] * distance_to_a_y*(1 - distance_to_a_x) + srcdata[original_point_c + 2] * distance_to_a_y*distance_to_a_x; /***********双线性插值算法***********/ } } } }
经过本人的测试,如果每次直接这样算效率是比较低的,所以后来本人改由查表法来完成上面的工作,这份工程和可执行程序都已经打包在一起上传了(由于是X64编译的,需要电脑是64位操作系统才可以运行,如果是32位的则可通过修改工程解决,工程利用OpenCV进行解码,需要自行配置,否则无法运行),如果有兴趣的可以去下载。
下载地址在这
下面放出一张处理结果图