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。接着,它声明了一个名为imgToBeThresHolded
的 cv::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 的函数,用来构建图像金字塔。下面是这个函数的功能和实现细节的解释:
-
首先,函数计算需要的金字塔图像数量 npyrimg。它通过迭代计算图像尺寸 imgpsize 直到它小于等于给定的最小大小 minSize,每次迭代都将图像的尺寸按照参数 _params.pyrfactor 进行缩小,同时记录迭代次数,最终得到金字塔图像数量 npyrimg。
-
接着,函数调整 ImagePyramid 的大小为 npyrimg,以便容纳金字塔图像。
-
然后,函数将初始的灰度图像 grey 存储到金字塔的第一层(index 0)。
-
最后,函数使用循环构建剩余的金字塔图像。对于每一层 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();
这段代码主要进行了以下操作:
-
首先,代码定义了一个名为 MarkerCanditates 的向量,用于存储检测到的标记候选对象。
-
然后,代码调用 thresholdAndDetectRectangles 函数,对 imgToBeThresHolded 进行阈值处理和矩形检测,将检测到的标记候选对象存储到 MarkerCanditates 中。
-
接下来,代码将 _thres_Images[0] 赋值给 thres,即将阈值化后的图像的第一层(通道)赋值给 thres。
-
之后,代码进行了一系列的调试操作,用于显示阈值化后的图像和经过预过滤处理后的标记候选对象。这部分代码被注释掉了。
-
代码调用 prefilterCandidates 函数对标记候选对象进行预过滤处理,得到筛选后的候选对象列表 MarkerCanditates。
-
代码检查 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
容器中。
此外,代码中还包含了一些调试输出和自适应阈值的处理逻辑。
在最后的部分,代码通过对检测到的标记坐标进行插值操作来将坐标还原到原始图像中。
什么是单应性矩阵?
考虑图中所示两个平面图像。红点代表两个图像中的相同物理点,在计算机视觉中,这些点称为对应点,图中显示了四种不同颜色(红色、绿色、黄色和橙色)的四个对应点,单应性矩阵将一个图像中的点映射到另一图像中的对应点
单应性矩阵写为
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
:边界填充像素的值,只有当borderMode
为BORDER_CONSTANT
时才生效,默认为黑色。
总结
以上为对ArUco标记检测中MDetector.detect(InImage, CamParam, MarkerSize)
的初步学习,较为笼统