OpenCV图像重映射指南:m1type的秘密

OpenCV的remap支持多种类型的映射表输入,它们之间的关系是什么?尤其是如何理解整型的映射表呢?

最近在做一些三维视觉方向的算法时,首先会调用initUndistortRectifyMap函数,计算从原图像到目标图像的映射表【map1, map2】,接着使用remap函数完成图像的映射。算法跑在嵌入式设备上,对时间性能非常敏感。

经过调研和实验,initUndistortRectifyMap通过设置不同的m1type参数,可以输出不同类型的映射表,相应的remap函数也支持这些不同类型的输入。具体支持类型如下:

参数m1typemap1的类型map2的类型
CV_32FC1CV_32FC1CV_32FC1
CV_32FC2CV_32FC2无,为空
CV_16SC2CV_16SC2CV_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位来存储小数部分,进一步提速和节省内存。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注