OpenCV的remap支持多种类型的映射表输入,它们之间的关系是什么?尤其是如何理解整型的映射表呢?
最近在做一些三维视觉方向的算法时,首先会调用initUndistortRectifyMap函数,计算从原图像到目标图像的映射表【map1, map2】,接着使用remap函数完成图像的映射。算法跑在嵌入式设备上,对时间性能非常敏感。
经过调研和实验,initUndistortRectifyMap通过设置不同的m1type参数,可以输出不同类型的映射表,相应的remap函数也支持这些不同类型的输入。具体支持类型如下:
参数m1type | map1的类型 | map2的类型 |
CV_32FC1 | CV_32FC1 | CV_32FC1 |
CV_32FC2 | CV_32FC2 | 无,为空 |
CV_16SC2 | CV_16SC2 | CV_16UC1 |
参数m1type顾名思义,就是map1的数据类型,而map2的数据类型和map1对应。
接下来说明不同的map类型是如何存储映射表的。首先我们需要理解一件事,两张map表的尺寸都是和映射后图像的尺寸一致的,它们的目的就是告诉我们,映射后图像的每一个像素需要从原图上什么位置去找。
- m1type = CV_32FC1
map1(x, y) = 目标点在原图上的x坐标,map2(x, y) = 目标点在原图上的y坐标。两个坐标均为单精度浮点数。
- m1type = CV_32FC2
map1(x, y) = 目标点在原图上的(x, y)坐标,同样为单精度浮点数。
map2为空
- m1type = CV_16SC1
这种情况是最复杂的,我在网上搜了很多,没有搜到明确的答案。也问了AI,AI给的答复只说对了一半,即:
map1(x, y) = 目标点在原图上的(x, y)坐标的整数部分
map2(x, y) = 目标点在原图上的(x, y)坐标的小数部分
这里整数部分好理解,关键是小数部分,AI的解释是错误的。AI说,16位无符号整数,分为低八位和高八位。其中低八位的范围为[0,255],通过除以256,映射到[0,1)之间的小数,高八位同理。分别代表xy的小数部分。
这听上去是非常合理的,但与我实际的观察不符。我观察发现,map2的最大值只有1023,恰好是2^10-1。那么高八位最多取到3,与事实严重不符。
通过阅读opencv源码,终于发现了map2的秘密,它正确的解析方式如下
16分为0~4位,5~9位以及10~15位。
0~4位,代表一个[0, 31]之间的数,通过除以32得到一个[0,1)之间的小数,代表x坐标的小数部分。
5~9位,代表一个[0, 31]之间的数,通过除以32得到一个[0,1)之间的小数,代表y坐标的小数部分。
0~15位,无用途,全部为0。
举一个例子,如果map2(x, y)=521=(000000,10000,01001)二进制
其中高六位全部为0,中间6位等于16,代表y坐标的小数部分为16/32=0.5,后5位等于9,代表x坐标的小数部分为9/32=0.28125。
这里贴上opencv的源码,源码面前了无秘密:
enum InterpolationMasks {
INTER_BITS = 5,
INTER_BITS2 = INTER_BITS * 2,
INTER_TAB_SIZE = 1 << INTER_BITS, // 32
INTER_TAB_SIZE2 = INTER_TAB_SIZE * INTER_TAB_SIZE //1024
};
void cv::convertMaps( InputArray _map1, InputArray _map2,
OutputArray _dstmap1, OutputArray _dstmap2,
int dstm1type, bool nninterpolate )
{
// scale = 1/32
const float scale = 1.f/static_cast<float>(INTER_TAB_SIZE);
// ....
if( m1type == CV_32FC1 && dstm1type == CV_16SC2 )
{
// ...
}
else if( m1type == CV_32FC2 && dstm1type == CV_16SC2 )
{
// ...
}
else if( m1type == CV_16SC2 && dstm1type == CV_32FC1 )
{
for( ; x < size.width; x++ )
{
// fxy为src2[x]的低10位
int fxy = src2 ? src2[x] & (INTER_TAB_SIZE2-1) : 0;
// dst1f[x]整数部分为src1[x*2]
// dst1f[x]的小数部分:取fxy的低5位,除以32
dst1f[x] = src1[x*2] + (fxy & (INTER_TAB_SIZE-1))*scale;
// dst2f[x]整数部分为src1[x*2+1]
// dst2f[x]的小数部分:取fxy的中间5位,除以32
dst2f[x] = src1[x*2+1] + (fxy >> INTER_BITS)*scale;
}
}
else if( m1type == CV_16SC2 && dstm1type == CV_32FC2 )
{
// ...
}
// ...
}
使用这种m1type为CV_16SC2后,remap速度实测提升约20%,内存占用减少50%。
显然使用0/32,1/32, …, 31/32来表示0到1之间的小数是有误差的,但这样的误差可以认为是无关紧要的,因为当像素坐标不是整数时,目标图像的像素值需要取原图上找相近的若干个像素点进行插值,既然是插值,例如双线性插值,最近的四个点各自的贡献稍微有一些误差,和毫无误差,都是插值出来的,可以忽略不计。
最后还有一个小小的疑问,为什么选择用两个5位来存储小数部分,导致留了6位的浪费。这里明明可以有两种选择,一是把16位用完,两个小数部分都用8位来存储,这样可以稍微提升一点精度。二是直接把map2格式改成CV_8UC1,用4位来存储小数部分,进一步提速和节省内存。
发表回复