ArUco库的学习与使用(三)

ArUco库的学习与使用(三)- MDetector.detect(InImage, CamParam, MarkerSize)


前言

学习成员函数vector<Marker> Markers = MDetector.detect(InImage, CamParam, MarkerSize)

一、创建低分辨率图像

代码如下

    // it must be a 3 channel image
    if (input.type() == CV_8UC3)
        cv::cvtColor(input,grey,CV_BGR2GRAY);
    //  convertToGray(input, grey);
    else grey = input;
    __ARUCO_TIMER_EVENT__("ConvertGrey");

    //
    ///CREATE LOW RESOLUTION IMAGE IN WHICH MARKERS WILL BE DETECTED
    //
    float ResizeFactor=1;
    //use the minimum and markerWarpSize to determine the optimal image size on which to do rectangle detection
    cv::Mat imgToBeThresHolded ;
         cv::Size maxImageSize=grey.size();
        auto minpixsize=getMinMarkerSizePix(input.size());//min pixel size of the marker in the original image
        if ( _params.lowResMarkerSize<minpixsize  ){
            ResizeFactor= float(_params.lowResMarkerSize)/float(minpixsize  ) ;
            if (ResizeFactor<0.9){//do not waste time if smaller than this
                _debug_msg("Scale factor="<<ResizeFactor,1);
                maxImageSize.width=float(grey.cols)*ResizeFactor+0.5;
                maxImageSize.height=float(grey.rows)*ResizeFactor+0.5;
                if ( maxImageSize.width%2!=0) maxImageSize.width++;
                if ( maxImageSize.height%2!=0) maxImageSize.height++;
                cv::resize(grey,imgToBeThresHolded,maxImageSize,0,0,cv::INTER_NEAREST);
                //                cv::resize(grey,imgToBeThresHolded,maxImageSize,0,0,cv::INTER_LINEAR);
            }
        }

     if(imgToBeThresHolded.empty())//if not set in previous step, add original now
        imgToBeThresHolded=grey;

这段代码是一个用于创建低分辨率图像的部分实现。

首先,代码检查输入图像的通道数是否为 3 通道。如果是,将其转换为灰度图像。否则,保持原始图像不变。这个部分使用了 OpenCV 中的 cvtColor 函数。

接下来,代码定义了一个 ResizeFactor 变量,并初始化为 1。接着,它声明了一个名为imgToBeThresHoldedcv::Mat 对象。

然后,代码获取 grey 图像的尺寸作为maxImageSize,并调用之前提到的getMinMarkerSizePix 函数来计算原始图像中的最小标记像素尺寸。

接下来,代码检查参数 _params.lowResMarkerSize 是否小于 minpixsize,如果是,则计算 ResizeFactor,将其设置为 _params.lowResMarkerSize 除以 minpixsize 的比例。如果 ResizeFactor 小于 0.9,则更新 maxImageSize 的宽度和高度,并进行修正使其为偶数。然后,使用 cv::resize 函数将灰度图像 grey 缩放到 maxImageSize,生成 imgToBeThresHolded

最后,代码检查 imgToBeThresHolded 是否为空,如果是,则将 imgToBeThresHolded 设置为灰度图像 grey

这段代码的作用是根据参数设置和输入图像的大小,创建一个低分辨率的图像 imgToBeThresHolded,用于后续的矩形检测。如果参数设置了低分辨率标记的大小,并且该大小小于原始图像中的最小标记大小,那么图像将被缩放到适当的大小,以提高矩形检测的效率。否则,将使用原始灰度图像进行后续处理。

二、构建图像金字塔

代码如下

bool needPyramid=  true;//ResizeFactor< 1/_params.pyrfactor;//only use pyramid if working on a big image.
    std::thread buildPyramidThread;
    if(needPyramid){
        if (_params.maxThreads>1)
            buildPyramidThread=std::thread([&]{buildPyramid(imagePyramid,grey ,2*getMarkerWarpSize());});
        else buildPyramid(imagePyramid,grey,2*getMarkerWarpSize());

这段代码首先定义了一个名为 needPyramid 的布尔变量,并将其初始化为 true。然后,它声明了一个名为 buildPyramidThread 的 std::thread 对象。

接着,代码检查 needPyramid 的值,如果为真,则继续执行下面的逻辑。在此情况下,它检查参数 _params.maxThreads 是否大于1,如果是,则创建一个新的线程来异步执行 buildPyramid 函数;否则,直接调用 buildPyramid 函数。

在 buildPyramid 函数中,它将灰度图像 grey 作为输入,并计算出图像金字塔 imagePyramid,同时在计算金字塔时使用了 2 倍的标记变换尺寸(getMarkerWarpSize 的两倍)。

如果 needPyramid 为假,则该代码段不会执行任何操作。

该代码段的作用是根据参数设置和需要金字塔的情况,异步或同步地构建图像金字塔,以便在后续处理中使用。

代码如下

void MarkerDetector_Impl::buildPyramid(vector<cv::Mat> &ImagePyramid,const cv::Mat &grey,int minSize){
    //determine numbre of pyramid images
    int npyrimg=1;
    cv::Size imgpsize=grey.size();
    while (imgpsize.width > minSize) { imgpsize=cv::Size(imgpsize.width/_params.pyrfactor,imgpsize.height/_params.pyrfactor) ;npyrimg++;}

    ImagePyramid.resize(npyrimg);
    imagePyramid[0]=grey;
    //now, create pyramid images
    imgpsize=grey.size();
    for(int i=1;i<npyrimg;i++){
            cv::Size nsize( ImagePyramid[i-1].cols/_params.pyrfactor,ImagePyramid[i-1].rows/_params.pyrfactor);
            cv::resize(ImagePyramid[i-1],ImagePyramid[i],nsize);
    }
}

这段代码定义了一个名为 buildPyramid 的函数,用来构建图像金字塔。下面是这个函数的功能和实现细节的解释:

  1. 首先,函数计算需要的金字塔图像数量 npyrimg。它通过迭代计算图像尺寸 imgpsize 直到它小于等于给定的最小大小 minSize,每次迭代都将图像的尺寸按照参数 _params.pyrfactor 进行缩小,同时记录迭代次数,最终得到金字塔图像数量 npyrimg。

  2. 接着,函数调整 ImagePyramid 的大小为 npyrimg,以便容纳金字塔图像。

  3. 然后,函数将初始的灰度图像 grey 存储到金字塔的第一层(index 0)。

  4. 最后,函数使用循环构建剩余的金字塔图像。对于每一层 i,它将图像尺寸缩小为前一层的 1/_params.pyrfactor 倍,并存储到 ImagePyramid[i] 中。

总的来说,该函数的作用是根据给定的最小大小和缩放因子构建金字塔图像,以便在后续处理中使用。这样的图像金字塔常用于多尺度特征检测和匹配。

三、图像阈值化和矩形检测

代码如下

///
        /// THRESHOLD IMAGES AND DETECT INITIAL RECTANGLES
        ///
        vector<MarkerCandidate> MarkerCanditates;
        MarkerCanditates=thresholdAndDetectRectangles(imgToBeThresHolded );
        thres    =_thres_Images[0];

//        _debug_exec(10,
//        {//only executes when compiled in DEBUG mode if debug level is at least 10
//         //show the thresholded images
//         for (size_t i = 0; i < _thres_Images.size(); i++) {
//             stringstream sstr; sstr << "thres-" << i;
//             cv::namedWindow(sstr.str(),cv::WINDOW_NORMAL);
//             cv::imshow(sstr.str(),_thres_Images[i]);
//         }});


        __ARUCO_TIMER_EVENT__("Threshold and Detect rectangles");
        //prefilter candidates
//        _debug_exec(10,//only executes when compiled in DEBUG mode if debug level is at least 10
//                    //show the thresholded images
//                    cv::Mat imrect;
//                cv::cvtColor(imgToBeThresHolded,imrect,CV_GRAY2BGR);
//        for(auto m: MarkerCanditates )
//            m.draw(imrect,cv::Scalar(0,245,0));
//        cv::imshow("rect-nofiltered",imrect);
//        );

        MarkerCanditates=prefilterCandidates(MarkerCanditates,imgToBeThresHolded.size());

        __ARUCO_TIMER_EVENT__("prefilterCandidates");

//        _debug_exec(10,//only executes when compiled in DEBUG mode if debug level is at least 10
//                    //show the thresholded images
//                    cv::Mat imrect;
//                cv::cvtColor(imgToBeThresHolded,imrect,CV_GRAY2BGR);
//        for(auto m: MarkerCanditates)
//            m.draw(imrect,cv::Scalar(0,245,0));
//        cv::imshow("rect-filtered",imrect);
//        );
        //before going on, make sure the piramid is built
        if (buildPyramidThread.joinable())
            buildPyramidThread.join();

这段代码主要进行了以下操作:

  1. 首先,代码定义了一个名为 MarkerCanditates 的向量,用于存储检测到的标记候选对象。

  2. 然后,代码调用 thresholdAndDetectRectangles 函数,对 imgToBeThresHolded 进行阈值处理和矩形检测,将检测到的标记候选对象存储到 MarkerCanditates 中。

  3. 接下来,代码将 _thres_Images[0] 赋值给 thres,即将阈值化后的图像的第一层(通道)赋值给 thres。

  4. 之后,代码进行了一系列的调试操作,用于显示阈值化后的图像和经过预过滤处理后的标记候选对象。这部分代码被注释掉了。

  5. 代码调用 prefilterCandidates 函数对标记候选对象进行预过滤处理,得到筛选后的候选对象列表 MarkerCanditates。

  6. 代码检查 buildPyramidThread 是否可加入(joinable),如果是,则等待线程执行完成再继续往下执行。这是为了确保在继续下一步操作之前,图像金字塔的构建已经完成。

整个代码段的作用是进行图像阈值化和矩形检测,然后对检测到的标记候选对象进行预过滤处理,并等待图像金字塔的构建完成。

四、对候选标记进行分类

代码如下

 ///
        /// CANDIDATE CLASSIFICATION: Decide which candidates are really markers
        ///

        //Debug::setLevel(10);
        auto markerWarpSize=getMarkerWarpSize();

        detectedMarkers.clear();
        _candidates_wcontour.clear();
        for(auto &b:hist) b=0;
        float desiredarea = std::pow(static_cast<float>(markerWarpSize), 2.f);
        for (size_t i = 0; i < MarkerCanditates.size(); i++)
        {
            // Find proyective homography
            cv::Mat canonicalMarker,canonicalMarkerAux;

            cv::Mat inToWarp=imgToBeThresHolded;
            MarkerCandidate points2d_pyr = MarkerCanditates[i];
            if (needPyramid){
                // warping is one of the most time consuming operations, especially when the region is large.
                // To reduce computing time, let us find in the image pyramid, the best configuration to save time
                // indicates how much bigger observation is wrt to desired patch
                size_t imgPyrIdx = 0;
                for (size_t p = 1; p < imagePyramid.size(); p++)
                {
                    if (MarkerCanditates[i].getArea() / pow(4, p) >= desiredarea) imgPyrIdx = p;
                    else break;
                }
                inToWarp=imagePyramid[imgPyrIdx];
                //move points to the image level p
                float ratio=float(inToWarp.cols)/float(imgToBeThresHolded.cols);
                for (auto& p : points2d_pyr)  p *= ratio;//1. / pow(2, imgPyrIdx);

            }
            warp( inToWarp, canonicalMarker,  Size(markerWarpSize,markerWarpSize),points2d_pyr);
            int id, nRotations;
            double min,Max;
            cv::minMaxIdx(canonicalMarker,&min,&Max);
            canonicalMarker.copyTo(canonicalMarkerAux);
            string additionalInfo;
//            _debug_exec(10,//only executes when compiled in DEBUG mode if debug level is at least 10
//                        //show the thresholded images
//                        stringstream sstr;sstr<<"test-"<<i;
//            cout  <<"test"<<i<<endl;
//            cv::namedWindow(sstr.str(),cv::WINDOW_NORMAL);
//            cv::imshow(sstr.str(),canonicalMarkerAux);
//            cv::waitKey(0);
//             );
            if (markerIdDetector->detect(canonicalMarkerAux, id, nRotations,additionalInfo))
            {
                detectedMarkers.push_back(MarkerCanditates[i]);
                detectedMarkers.back().id = id;
                detectedMarkers.back().dict_info=additionalInfo;
                detectedMarkers.back().contourPoints=MarkerCanditates[i].contourPoints;
                // sort the points so that they are always in the same order no matter the camera orientation
                std::rotate(detectedMarkers.back().begin(),
                            detectedMarkers.back().begin() + 4 - nRotations,
                            detectedMarkers.back().end());
//                _debug_exec(10,//only executes when compiled in DEBUG mode if debug level is at least 10
//                            //show the thresholded images
//                            stringstream sstr;sstr<<"can-"<<detectedMarkers.back().id;
//                cv::namedWindow(sstr.str(),cv::WINDOW_NORMAL);
//                cv::imshow(sstr.str(),canonicalMarker);
//                cout<<"ID="<<id<<" "<< detectedMarkers.back()<<endl;
//                );
                if (_params.thresMethod==MarkerDetector::THRES_AUTO_FIXED)
                    addToImageHist(canonicalMarker,hist);
            }
            else {
                 _candidates_wcontour.push_back(MarkerCanditates[i]);
            }

        }
        __ARUCO_TIMER_EVENT__("Marker classification. ");
        if (detectedMarkers.size()==0 &&  _params.thresMethod==MarkerDetector::THRES_AUTO_FIXED && ++nAttemptsAutoFix < _params.NAttemptsAutoThresFix){
            _params.ThresHold=  10+ rand()%230 ;
            keepLookingFor=true;
        }
        else keepLookingFor=false;
    }while(keepLookingFor);

  //  Debug::setLevel(5);


    if (_params.thresMethod==MarkerDetector::THRES_AUTO_FIXED){
        int newThres=Otsu(hist);;
        if(newThres>0)
            _params.ThresHold=  float(newThres) ;
    }

#ifdef debug_lines
    cv::imshow("image-lines",image);
    cv::waitKey(10);
#endif
    //now, move the points to the original image (upsample corners)
    if (input.cols!=imgToBeThresHolded.cols){
        cornerUpsample(detectedMarkers,imgToBeThresHolded.size());
        __ARUCO_TIMER_EVENT__("Corner Upsample");
    }

这段代码是对候选标记进行分类的部分。在这段代码中,首先清空了一些变量和计数器。然后,遍历所有的候选标记。

对于每一个候选标记,首先进行投影单应性矩阵(homography)的计算。然后根据需要使用图像金字塔进行图像的缩放。接下来,通过进行仿射变换(warp)将候选标记变换为规范化的标记图像。

接下来,调用markerIdDetector->detect函数进行标记的识别。如果成功识别出标记,则将该标记添加到detectedMarkers容器中,并进行一些信息的赋值操作,如标记的ID、附加信息、轮廓点等。

如果识别失败,则将该候选标记添加到_candidates_wcontour容器中。

此外,代码中还包含了一些调试输出和自适应阈值的处理逻辑。

在最后的部分,代码通过对检测到的标记坐标进行插值操作来将坐标还原到原始图像中。

什么是单应性矩阵?
考虑图中所示两个平面图像。红点代表两个图像中的相同物理点,在计算机视觉中,这些点称为对应点,图中显示了四种不同颜色(红色、绿色、黄色和橙色)的四个对应点,单应性矩阵将一个图像中的点映射到另一图像中的对应点
图1单应性矩阵写为

H = [ h 00 h 01 h 02 h 10 h 11 h 12 h 11 h 21 h 22 ] H=\left[ \begin{matrix} {{h}_{00}} & {{h}_{01}} & {{h}_{02}} \\ {{h}_{10}} & {{h}_{11}} & {{h}_{12}} \\ {{h}_{11}} & {{h}_{21}} & {{h}_{22}} \\ \end{matrix} \right] H= h00h10h11h01h11h21h02h12h22

考虑第一组对应点 ( x 1 , y 1 ) ({{x}_{1}},{{y}_{1}}) (x1,y1) ( x 2 , y 2 ) ({{x}_{2}},{{y}_{2}}) (x2,y2)在第二张图片中,然后按单应性矩阵H按以下方式映射

[ x 1 y 1 1 ] = H [ x 2 y 2 1 ] = [ h 00 h 01 h 02 h 10 h 11 h 12 h 11 h 21 h 22 ] [ x 2 y 2 1 ] \left[ \begin{matrix} {{x}_{1}} \\ {{y}_{1}} \\ 1 \\ \end{matrix} \right]=H\left[ \begin{matrix} {{x}_{2}} \\ {{y}_{2}} \\ 1 \\ \end{matrix} \right]=\left[ \begin{matrix} {{h}_{00}} & {{h}_{01}} & {{h}_{02}} \\ {{h}_{10}} & {{h}_{11}} & {{h}_{12}} \\ {{h}_{11}} & {{h}_{21}} & {{h}_{22}} \\ \end{matrix} \right]\left[ \begin{matrix} {{x}_{2}} \\ {{y}_{2}} \\ 1 \\ \end{matrix} \right] x1y11 =H x2y21 = h00h10h11h01h11h21h02h12h22 x2y21

上述方程对所有对应点集都成立,只要它们位于真实世界的同一平面上。换句话说,可以将单应性应用到第一个图像中,第一张图像中的书将与第二张图像中的书对齐
图二使用opencv c++的单应性示例
下面的代码显示了如何获取两个图像中的四个对应点并将图像扭曲到另一个图像上。

#include "opencv2/opencv.hpp"
 
using namespace cv;
using namespace std;
 
int main( int argc, char** argv)
{
    // Read source image.
    Mat im_src = imread("book2.jpg");
    // Four corners of the book in source image
    vector<Point2f> pts_src;
    pts_src.push_back(Point2f(141, 131));
    pts_src.push_back(Point2f(480, 159));
    pts_src.push_back(Point2f(493, 630));
    pts_src.push_back(Point2f(64, 601));
 
    // Read destination image.
    Mat im_dst = imread("book1.jpg");
    // Four corners of the book in destination image.
    vector<Point2f> pts_dst;
    pts_dst.push_back(Point2f(318, 256));
    pts_dst.push_back(Point2f(534, 372));
    pts_dst.push_back(Point2f(316, 670));
    pts_dst.push_back(Point2f(73, 473));
 
    // Calculate Homography
    Mat h = findHomography(pts_src, pts_dst);
 
    // Output image
    Mat im_out;
    // Warp source image to destination based on homography
    warpPerspective(im_src, im_out, h, im_dst.size());
 
    // Display images
    imshow("Source Image", im_src);
    imshow("Destination Image", im_dst);
    imshow("Warped Source Image", im_out);
 
    waitKey(0);
}

单应性的应用
(1)使用单应性进行透视矫正
假设您有一张如图 1 所示的照片。可以单击书的四个角并快速获得如图 3 所示的图像,以下是步骤。
a. 编写一个用户界面来收集书的四个角。将这些点称为pts_src
b. 需要知道书的长宽比。对于本书,宽高比(宽度/高度)为 3/4。因此,我们可以选择输出图像大小为 300×400,目标点 ( pts_dst ) 为 (0,0)、(299,0)、(299,399) 和 (0,399)
c. 使用pts_src和pts_dst获取单应性。
d. 将单应性应用到源图像以获得图 3 中的图像。
图三
cv::findHomography() 是 OpenCV 库中的一个函数,用于计算两个平面上的点之间的单应性矩阵(homography matrix),也被称为透视变换矩阵。

函数原型如下:

cv::Mat findHomography(InputArray srcPoints, InputArray dstPoints, int method = 0, double ransacReprojThreshold = 3, OutputArray mask = noArray());

参数说明:

  • srcPoints:源平面上的点,可以是一个 vector<Point2f> 作为输入,或者一个 Mat 对象,其每行包含一个点的坐标。
  • dstPoints:目标平面上的点,与 srcPoints 一样,可以是一个 vector<Point2f>,或者一个 Mat 对象。
  • method:求解单应性矩阵的方法,默认为 0,表示使用所有的点对进行计算。
  • ransacReprojThreshold:随机抽样一致算法(RANSAC)的投影误差阈值,默认为 3,用于过滤异常值。
  • mask:输出的掩码,表明哪些点被用于计算单应性矩阵。

返回值:
函数返回一个 3x3 的单应性矩阵,表示源平面到目标平面的变换关系。

warpPerspective() 函数,它会根据给定的单应性矩阵将源图像 im_src 进行透视变换,并将结果存储在目标图像 im_out 中。

函数原型如下:

void warpPerspective(InputArray src, OutputArray dst, InputArray M, Size dsize,
                     int flags = INTER_LINEAR, int borderMode = BORDER_CONSTANT,
                     const Scalar& borderValue = Scalar());

参数说明:

  • src:输入的源图像,可以是一个 Mat 对象或图像文件的路径。
  • dst:输出的目标图像,将透视变换后的图像存储到这里,类型和尺寸与 dsize 参数相同。
  • M:单应性矩阵,可以使用 cv::findHomography() 等函数计算得到。
  • dsize:输出图像的尺寸,可以是 Size 对象或包含输出图像宽度和高度的 Size(width, height)
  • flags:插值方法的标志,用于指定图像的插值方式,默认使用 INTER_LINEAR 进行双线性插值。
  • borderMode:边界模式,默认为 BORDER_CONSTANT,表示边界扩展时使用的像素填充方式。
  • borderValue:边界填充像素的值,只有当 borderModeBORDER_CONSTANT 时才生效,默认为黑色。

总结

以上为对ArUco标记检测中MDetector.detect(InImage, CamParam, MarkerSize)的初步学习,较为笼统

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值