图像处理系列——融合去畸变和投影变换两张Map加速计算

融合去畸变和投影变换两张Map加速计算

1. 背景

图像处理中一些涉及到像素位置变换的操作,比如去畸变、投影变换等,如果变换的模式固定(不会实时变化)则可以通过先计算一次map,然后实时运行只需要查找表(LUT),更加高效,而不是每次去畸变或变换都计算一次map。适用于批量处理场景,如视频流等。

  • 对于去畸变:不直接调用 undistort(),而是把这个过程拆分开来,先生成 map,然后再 remap实现去畸变
  • 对于投影变换:不直接调用 warpPerspective() ,而是手动根据投影矩阵H计算好Map,然后在循环中remap

这样做的前提是变换矩阵和内参矩阵不变。

2. 去畸变

initUndistortRectifyMap() 得到的MapX和MapY可以用于remap(),只用生成一次map

fisheye::estimateNewCameraMatrixForUndistortRectify(K, D, srcSize, Matx33d::eye(), newK, balance, srcSize, fovScale);
fisheye::initUndistortRectifyMap(K, D, Matx33d::eye(), newK, srcSize, CV_16SC2, MapX, MapY);
cv::remap(inputImg, undistorted, MapX, MapY, INTER_LINEAR, BORDER_CONSTANT);

3. 投影变换

投影变换的Map可以根据投影矩阵手工计算出来

   // Calculate projection maps according to the H matrix.
    for (int i = 0; i < camNum; i++)
    {
        cv::Mat inverseTransMatrix;
        cv::invert(vecH[i], inverseTransMatrix);

        // Generate the warp matrix
        cv::Mat map_x, map_y, srcTM;
        srcTM = inverseTransMatrix.clone(); // If WARP_INVERSE, set srcTM to transformationMatrix

        map_x.create(imgSize, CV_32FC1);
        map_y.create(imgSize, CV_32FC1);

        double M11, M12, M13, M21, M22, M23, M31, M32, M33;
        M11 = srcTM.at<double>(0,0);
        M12 = srcTM.at<double>(0,1);
        M13 = srcTM.at<double>(0,2);
        M21 = srcTM.at<double>(1,0);
        M22 = srcTM.at<double>(1,1);
        M23 = srcTM.at<double>(1,2);
        M31 = srcTM.at<double>(2,0);
        M32 = srcTM.at<double>(2,1);
        M33 = srcTM.at<double>(2,2);

        for (int y = 0; y < imgSize.height; y++) {
            double fy = (double)y;
            for (int x = 0; x < imgSize.width; x++) {
                double fx = (double)x;
                double w = ((M31 * fx) + (M32 * fy) + M33);
                w = w != 0.0f ? 1.f / w : 0.0f;
                float new_x = (float)((M11 * fx) + (M12 * fy) + M13) * w;
                float new_y = (float)((M21 * fx) + (M22 * fy) + M23) * w;
                map_x.at<float>(y,x) = new_x;
                map_y.at<float>(y,x) = new_y;
        }
        }

        // fixed-point representation 转为定点计算效率更高(但实际测试效果不明显) 
        cv::Mat transformation_x, transformation_y;
        transformation_x.create(imgSize, CV_16SC2);
        transformation_y.create(imgSize, CV_16UC1);
        cv::convertMaps(map_x, map_y, transformation_x, transformation_y, false);
        vecMapHX[i] = transformation_x.clone();
        vecMapHY[i] = transformation_y.clone();

4. 更进一步,融合去畸变和投影变换

在具有已知逆映射的两个映射的特殊情况下,另一种方法是手动计算映射。这种手动方法比双重映射方法更准确,因为它不涉及坐标映射的插值。大多数应用程序都符合这种特殊情况。因为第一个映射对应于图像去畸变(其逆操作是图像畸变,与一个众所周知的分析模型相关联),而第二个映射对应于一个透视转换(其逆可以解析地表示)。

    for (int i = 0; i < camNum; i++)
    {
        int dst_width = imgSize.width, dst_height=imgSize.height;        
        Mat newK;
        fisheye::estimateNewCameraMatrixForUndistortRectify(vecK.at(i), vecD.at(i), srcSize, Matx33d::eye(), newK, balance, srcSize, fovScale);
        cv::Mat Hinv = vecH[i].inv(), Kinv=newK.inv();         
        cv::Mat map_undist_warped_x32f(dst_height, dst_width, CV_32F); 
        cv::Mat map_undist_warped_y32f(dst_height, dst_width, CV_32F);   
        for(int y=0; y<dst_height; ++y) {
            std::vector<cv::Point3d> pts_undist_norm(dst_width);
            for(int x=0; x<dst_width; ++x) {
                cv::Mat_<double> pt(3,1); pt << x,y,1;
                pt = Kinv*Hinv*pt;
                pt(2) = pt(2) != 0.0f ? 1.f / pt(2) : 0.0f;
                pts_undist_norm[x].x = pt(0) * pt(2);
                pts_undist_norm[x].y = pt(1) * pt(2);
                pts_undist_norm[x].z = 1;
            }
            std::vector<cv::Point2d> pts_dist;
            fisheye::projectPoints(pts_undist_norm, pts_dist, Mat::zeros(3,1,CV_32F), Mat::zeros(3,1,CV_32F), vecK[i], vecD[i]);
            for(int x=0; x<dst_width; ++x) {
                map_undist_warped_x32f.at<float>(y,x) = pts_dist[x].x;
                map_undist_warped_y32f.at<float>(y,x) = pts_dist[x].y;
            }
        }

		// // CPU版本
        // cv::Mat map_undist_warped_x16s,map_undist_warped_y16s;
        // cv::convertMaps(map_undist_warped_x32f,map_undist_warped_y32f,map_undist_warped_x16s,map_undist_warped_y16s,CV_16SC2);
        // vecDoubleMapX[i] = map_undist_warped_x16s.clone();
        // vecDoubleMapY[i] = map_undist_warped_y16s.clone();
        vecDoubleMapX[i] = map_undist_warped_x32f.clone();
        vecDoubleMapY[i] = map_undist_warped_y32f.clone();
    	
    	// GPU版本:使用Nppi库进行GPU加速处理需要把map传入device内存
        Npp32f* d_xMap = NULL;
        Npp32f* d_yMap = NULL;
        int mapMemSize = imgSize.height * imgSize.width * sizeof(float);
        cudaMalloc((void**)&d_xMap, mapMemSize);
        cudaMalloc((void**)&d_yMap, mapMemSize);
        cudaMemcpy(d_xMap, vecDoubleMapX[i].data, mapMemSize, cudaMemcpyHostToDevice);
        cudaMemcpy(d_yMap, vecDoubleMapY[i].data, mapMemSize, cudaMemcpyHostToDevice);
        d_xMaps.push_back(d_xMap);
        d_yMaps.push_back(d_yMap);
    }

5. 更进一步,融合两个一般 Map

当Map难以手工计算时,可以直接对第一个map作用第二个map

cv::remap(map1_x, combined_mapx, map2_x, map2_y, cv::INTER_LINEAR);
cv::remap(map1_y, combined_mapy, map2_x, map2_y, cv::INTER_LINEAR);

// 融合后的map为:combined_mapx,combined_mapy,可以直接它们remap
cv::remap(input_img, res_img, combined_mapx, combined_mapy, cv::INTER_LINEAR);

但是这样存在的问题在于融合的map和独立做两次remap的结果会有略微差异,以下是分析:

  • Remap()函数从旧图像的像素中计算新图像中的像素。在线性插值的情况下,新图像中的每个像素是从旧图像中4个像素的加权平均值。根据所提供map的值,每个像素的权重不同。如果值是或多或少的整数,那么大部分权重是从单个像素取的。因此,新图像将与原始图像一样清晰。另一方面,如果值远不是整数(即整数+ 0.5),那么权重是相似的。这将产生平滑效果。要理解我所说的,请看这张未变形的图片。你会看到图像的某些部分比其他部分更清晰/平滑。
  • 现在回到对将两个重映射操作合并为一个操作时会发生什么的解释。组合map中的坐标是正确的,即最终图片中的像素是从originalImage中正确的4个像素计算出来的。但两次单独remap操作的结果并不完全相同,第一次remap的结果图片中的每个像素是originalImage中4个像素的加权平均值。这意味着第二次remap的结果图中的每个像素都是来自originalimage的9-16像素的加权平均值。结论:使用一次remap()不可能得到与两次使用remap()相同的结果
  • 但是融合的一次remap效果不一定就比做两次remap差,因为过多的插值可能带来一些问题,所以最终效果要具体问题具体分析。

6. 参考

https://stackoverflow.com/questions/29944709/how-to-combine-two-remap-operations-into-one

你可能感兴趣的:(计算机视觉,C++,CUDA,图像处理,计算机视觉,人工智能)