ArUco库的学习与使用(二)
文章目录
前言
OpenCV以及ArUco库安装完成后,打开aruco.sln文件,首先开始学习aruco_simple.cpp文件中相关代码。
一、包含头文件
代码如下:
#include "aruco.h"
#include <iostream>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <string>
#include <stdexcept>
using namespace cv;
using namespace std;
using namespace aruco;
#include <stdexcept>
C++ 标准库的头文件之一,其中包含了标准异常类 std::exception 的定义,以及一些其他相关的异常类型。当在 C++ 程序中包含这个头文件时,就可以使用 C++ 标准库提供的异常处理功能,比如抛出和捕获异常。通常情况下,当想要以一种结构化和组织良好的方式处理程序中的异常情况时,就会使用这个头文件。
二、使用 CmdLineParser 类解析命令行参数
代码如下:
class CmdLineParser
{
int argc;
char **argv;
public:
/*
类CmdLineParser的构造函数,用于初始化类的成员变量
int _argc 表示命令行参数个数
char **_argv 表示命令行参数的字符串数组
argc(_argc),argv(_argv) 构造函数初始化列表,用于在对象创建时对成员变量初始化
*/
CmdLineParser(int _argc, char **_argv) : argc(_argc), argv(_argv) {}
/*
重载的运算符函数含有关键字operator
返回类型 operator 运算符 (参数列表)
{
函数体
}
遍历命令行参数数组查找参数param是否存在 -存在 将其索引赋值给idx -不存在 idx保持默认值-1
*/
bool operator[](string param)
{
int idx = -1; //初始化变量idx为-1
for (int i = 0; i < argc && idx == -1; i++) //遍历命令行参数数组,查找参数位置
if (string(argv[i]) == param)
idx = i; //找到参数param,索引赋值给idx
return (idx != -1); //
}
/*
用于获取特定参数param的取值,可以指定默认值defvalue
*/
string operator()(string param, string defvalue = "-1")
{
int idx = -1;
for (int i = 0; i < argc && idx == -1; i++)
if (string(argv[i]) == param)
idx = i;
if (idx == -1)
return defvalue;
else return (argv[idx + 1]);
}
};
相关知识点已在代码注释中给出
三、调整图像大小
代码如下:
cv::Mat __resize(const cv::Mat& in, int width)
{
if (in.size().width <= width) //检查输入图像宽度是否小于等于目标宽度
return in;
float yf = float(width) / float(in.size().width); //计算调整比例:目标宽度与输入图像宽度比值
cv::Mat im2;
cv::resize(in, im2, cv::Size(width, static_cast<int>(in.size().height*yf)));
return im2;
}
知识点总结
-
cv::resize()
:用于调整图像的大小。它可以将图像放大或缩小到指定的尺寸。
语法:cv::resize(input, output, size, fx, fy, interpolation)
。input
是输入图像,output
是输出图像,size
是指定的目标大小,fx
和fy
是沿水平和垂直轴的缩放比例,interpolation
是插值方法 -
cv::Mat::size()
:用于获取矩阵或图像的大小,返回一个cv::Size
对象,其中包括图像的宽度和高度信息 -
static_cast<type> ()
:用于执行显式类型转换,将一种数据类型转换为另一种数据类型
四、main主函数
分段学习
try语句块
:异常处理部分使用try语句块处理异常–以关键字try开始,并以一个或多个catch子句结束。try语句块中代码抛出的异常通常会被某个catch子句处理命令行参数解析器
:解析用户在命令行中输入的参数
代码如下:
/*
命令行参数解析器,用于解析用户在命令行中输入的参数
*/
CmdLineParser cml(argc, argv);
if (argc == 1 || cml["-h"]) //判断命令行参数的个数是否为1或是否存在"-h"参数
{
cerr << "Usage:(in_image|video.avi) [-c cameraParams.yml] [-s markerSize] [-d <dictionary>:ALL_DICTS default] [-f arucoConfig.yml]" << endl; //输出使用说明,列出命令行参数用法
cerr << "\tDictories"; //输出用户可选择字典类型
for (auto dict : aruco::Dictionary::getDicTypes())
cerr << dict << " ";
cerr << endl;
cerr << "\t Instead of these, you can directly indicate the path to a file with your own generated"
"dictionary"
<< endl;
cout << "Example to work with apriltags dictionary : video.avi -d TAG36h11" << endl;
return 0;
}
(1) cerr
:一个ostream对象,关联到标准错误,通常写入到与标准输出相同的设备。默认情况下,写入cerr的数据是不缓冲的。cerr通常用于输出错误信息或其他不属于程序正常逻辑的输出内容
读取输入的图像或视频文件
代码如下
/*
读取输入的图像或视频文件
*/
aruco::CameraParameters CamParam; //定义名为CameraParameters的相机参数对象
cv::Mat InImage;
VideoCapture vreader(argv[1]);
if (vreader.isOpened()) //判断是否成功打开视频文件
vreader >> InImage; //将下一帧图像读取到InImage中
else
throw std::runtime_error("Could not open input"); //打开视频文件失败则抛出异常
(1)cv::Mat
: OpenCV 中的图像矩阵类,用于存储图像数据。
(2)VideoCapture
: OpenCV 中用于读取视频的类,它可以打开一个视频文件并读取帧。
(3).isOpened()
:isOpened() 是 VideoCapture 类的一个函数,用于判断视频文件是否成功打开。
(4)vreader >> InImage
:使用 >> 运算符从 VideoCapture 对象中读取下一帧图像,并将图像数据存储到 InImage 的 cv::Mat 对象中。
(5)throw std::runtime_error()
:throw 语句用于抛出异常,std::runtime_error 是 C++ 标准库中定义的一个异常类,可以通过传递错误消息来创建异常对象。使用 throw 抛出的异常可以被 try-catch 块捕获和处理。
注意:
上述操作只会读取视频或者摄像头中的第一帧图像数据。如果希望连续读取视频的多帧图像,你可以将 vreader >> InImage; 这行代码放在循环中,这样可以不断地读取视频的下一帧图像,直到视频结束或者达到你所需的帧数。
读取命令行中该相机参数文件的内容
代码如下
// read camera parameters if specifed
if (cml["-c"])
Camparam.readFromXMLFile(cml("-c"));
将参数值加载到 CamParam 对象中,以便在后续的相机操作中使用。关于CameraParameters类以及类内的readFromXMLFile()成员函数在之后再做学习,暂时不用
根据命令行参数设置标记大小
代码如下
// read marker size if specified (default value -1)
float MarkerSize = std::stof(cml("-s", "-1")); //获取-s参数的值并转换为浮点数类型.默认值为-1
// Create the detector
MarkerDetector MDetector;
if (cml["-f"])
{
// uses a configuration file. You can create it from aruco_test application
MDetector.loadParamsFromFile(cml("-f"));
}
else
{
// Set the dictionary you want to work with, if you included option -d command line
// By default,all valid dictionaries are examined
if (cml["-d"])
MDetector.setDictionary(cml("-d"), 0.f);
}
(1)std::stof
: C++ 中用于将字符串转换为浮点数的函数
语法:float std::stof( const std::string& str, std::size_t* pos = 0 )
;
(2)MarkerDetector类
:重点!!!!!!!!!!!!!
a. MDetector.loadParamsFromFile()
:见五、3.
b. MDetector.setDictionary()
:见五、4.
6.对检测到的标记进行处理和绘制
代码如下
vector<Marker> Markers = MDetector.detect(InImage, CamParam, MarkerSize);
// for each marker, draw info and its boundaries in the image
for (unsigned int i = 0; i < Markers.size(); i++)
{
cout << Markers[i] << endl;
Markers[i].draw(InImage, Scalar(0, 0, 255), 2);
}
// draw a 3d cube in each marker if there is 3d info
if (CamParam.isValid() && MarkerSize != -1)
for (unsigned int i = 0; i < Markers.size(); i++)
{
if(Markers[i].id==229 || Markers[i].id==161)
cout<< "Camera Location= "<<Markers[i].id<<" "<<CamParam.getCameraLocation(Markers[i].Rvec,Markers[i].Tvec)<<endl;
CvDrawingUtils::draw3dAxis(InImage, Markers[i], CamParam);
// CvDrawingUtils::draw3dCube(InImage, Markers[i], CamParam);
}
这段代码对检测到的标记进行了一些处理和绘制操作
(1)vector<Marker> Markers = MDetector.detect(InImage, CamParam, MarkerSize)
这行代码调用了 MDetector 对象的 detect 方法(见五、5.),使用输入图像 InImage、相机参数 CamParam 和标记大小 MarkerSize 来检测标记,并将结果存储在名为 Markers 的 std::vector 中。
(2)for (unsigned int i = 0; i < Markers.size(); i++) { ... }
这个循环遍历了存储在 Markers 中的每个标记对象,并对每个标记执行以下操作:
a. 输出标记信息到控制台,即使用 cout 打印标记对象的信息
b. 在输入图像 InImage 中绘制检测到的标记的边界,并用红色线条(Scalar(0, 0, 255))绘制
(3)if (CamParam.isValid() && MarkerSize != -1) { ... }
这段代码检测了相机参数的有效性和标记大小的值,然后对每个标记执行以下操作:
如果标记的 id 是 229 或 161,则输出标记的相机位置信息到控制台
在输入图像 InImage 中绘制每个标记的三维坐标轴,使用相机参数 CamParam 来进行绘制
显示图像
代码如下
// show input with augmented information
cv::namedWindow("in", 1); // 窗口大小可被用户调整
cv::imshow("in", __resize(InImage, 1280)); // 显示经过调整后的输入图像
while (char(cv::waitKey(0) != 27))
; // wait for esc to be pressed
异常处理块
代码如下
catch (std::exception& ex)
{
cout << "Exception :" << ex.what() << endl;
}
(1)try { ... } catch (std::exception& ex) { ... }
这个代码块用于捕获可能在 try 代码块中发生的 std::exception 类型的异常,并将其作为引用传递给名为 ex 的异常对象。
(2)cout << "Exception :" << ex.what() << endl
在发生异常时,将异常对象的 what() 方法返回的异常描述信息输出到控制台。ex.what() 返回一个 const char* 类型的指针,指向包含异常原因的字符串。
五、MarkerDetector类
- 定义MarkerDetector类
代码如下
class ARUCO_EXPORT MarkerDetector {};
ARUCO_EXPORT
是一个宏,宏的作用是根据当前的开发平台,动态地定义类的导出规则,以确保在使用 ArUco 库时,MarkerDetector 类能够正确地被外部代码访问和使用。在不同的平台上,库的使用方式可能会有所不同。在某些平台上,需要使用 __declspec(dllexport) 来显式导出类的符号。而在其他平台上,可能会使用其他的导出规则。–上述内容不知道是否正确
- 定义枚举类型ThresMethod以及友元MarkerDetector_Impl
代码如下
enum ThresMethod: int{THRES_ADAPTIVE=0,THRES_AUTO_FIXED=1 };
friend class MarkerDetector_Impl;
上述代码定义了一个枚举类型 ThresMethod,其基础类型为 int。该枚举类型包含两个枚举常量:
THRES_ADAPTIVE 被赋值为 0
THRES_AUTO_FIXED 被赋值为 1
声明了一个类 MarkerDetector_Impl 为 MarkerDetector 类的友元。通过声明为友元,MarkerDetector_Impl 类可以访问和使用 MarkerDetector 类的私有成员,即使这些成员在普通情况下是不可访问的。
关于枚举的一些知识点:
枚举是一种用于定义符号常量集合的数据类型。在许多编程语言中,枚举类型可以帮助程序员更清晰地定义和使用一组相关的常量,提高了代码的可读性和可维护性。以下是关于枚举的一些基本知识点:
(1)枚举的定义
:枚举类型定义了一组有限的命名常量,这些常量也被称为枚举成员。在大多数编程语言中,枚举类型可以使用 enum 关键字来定义。
(2)枚举成员
:枚举类型中的每个常量都被称为枚举成员,它们代表了一些具体的值。例如,在一个表示颜色的枚举类型中,可能包含红色、绿色和蓝色等枚举成员。
(3)基础类型
:枚举类型可以有一个关联的基础类型,代表枚举成员的存储类型。在某些编程语言中,枚举成员可以是整数、字符或其他数据类型。
(4)枚举的使用
:在程序中,可以使用枚举成员来代替硬编码的整数或其他值,以提高代码的可读性。例如,可以使用 Color.Red 来表示红色,而不是使用数字 0。
(5)枚举的优点
:枚举类型可以使代码更具可读性,减少了硬编码值的使用,有助于提高代码的可维护性和可理解性。
(6)枚举的限制
:在一些编程语言中,枚举类型可能有一些限制,例如枚举成员的命名规则、基础类型等方面的限制。
- 成员函数:
void loadParamsFromFile(const std::string &path)
代码如下
void MarkerDetector::loadParamsFromFile(const std::string &path){
_impl->loadParamsFromFile(path);
}
void MarkerDetector_Impl::loadParamsFromFile(const std::string &path){
cv::FileStorage fs(path, cv::FileStorage::READ);
if(!fs.isOpened())throw std::runtime_error("Could not open "+path);
_params.load(fs);
setDictionary(_params.dictionary,_params.error_correction_rate);
}
名为 loadParamsFromFile 的成员函数的定义,属于名为 MarkerDetector_Impl 的类。该函数接受一个常量引用类型的 std::string 参数 path,用于指定文件路径。在函数体内部,它首先使用 OpenCV 的 FileStorage 类打开指定路径的文件,然后加载其中的参数并对其进行处理。
具体来说,代码首先尝试打开指定路径的文件,如果文件打开失败,则抛出一个 std::runtime_error 异常。如果文件成功打开,则将参数加载到 _params 中,然后使用加载的参数设置对象的字典和纠错率。
(1)cv::FileStorage
:这是 OpenCV 中用于读写文件的类。它通常用于从文件中读取配置参数、模型权重等数据。
- 成员函数:
void setDictionary(std::string dict_type, float error_correction_rate = 0)
代码如下
void MarkerDetector::setDictionary(string dict_type, float error_correction_rate)
{
_impl->setDictionary(dict_type,error_correction_rate);
}
void MarkerDetector_Impl::setDictionary(string dict_type, float error_correction_rate)
{
auto _to_string=[](float i){
std::stringstream str;str<<i;return str.str();
};
_params.dictionary=dict_type;
markerIdDetector = MarkerLabeler::create(dict_type, _to_string(error_correction_rate));
_params.error_correction_rate=error_correction_rate;
}
该函数接受一个 std::string
类型的参数 dict_type
和一个float
类型的参数 error_correction_rate
,用于设置对象的字典和错误纠正率。在函数体内部,首先定义了一个 lambda 表达式
_to_string,用于将浮点数转换为字符串。然后,将传入的 dict_type 赋值给 _params.dictionary,用于存储字典类型。使用 MarkerLabeler::create
静态函数创建MarkerLabeler 对象,并将 dict_type 和转换后的字符串形式的error_correction_rate 作为参数传递给它,从而创建相应字典类型的标记检测器对象,并将其存储在 markerIdDetector 变量中。最后,将传入的 error_correction_rate 赋值给 _params.error_correction_rate,用于存储错误纠正率。
(1)std::stringstream
是 C++ 标准库中的一个类,它是基于字符串的流,用于在内存中读写字符串。它可以像 std::cin
和 std::cout
一样用于输入和输出,但是数据并不是来自于标准输入或输出,而是从字符串缓冲区中读取或写入。
通过 std::stringstream,我们可以将数据从其他类型(如整数、浮点数、字符串等)转换为字符串形式,或者将字符串解析为其他类型。它的用法类似于文件流 std::ifstream
和 std::ofstream
。
以下是 std::stringstream 常用的用法示例:
/*将其他类型转换为字符串*/
int main() {
std::stringstream ss;
int num = 123;
ss << num; // 将整数转换为字符串
std::string str = ss.str(); // 获取转换后的字符串
return 0;
}
/*将字符串解析为其他类型*/
int main() {
std::stringstream ss("123.45");
float num;
ss >> num; // 将字符串解析为浮点数
std::cout << num << std::endl; // 输出解析结果
return 0;
}
- 成员函数:
void MarkerDetector_Impl::detect(const cv::Mat& input, std::vector<Marker>& detectedMarkers, CameraParameters camParams,float markerSizeMeters, bool setYPerpendicular)
代码如下
std::vector<aruco::Marker> MarkerDetector::detect(const cv::Mat& input, const CameraParameters& camParams,
float markerSizeMeters,
bool setYPerperdicular)
{
return _impl->detect(input,camParams,markerSizeMeters,setYPerperdicular);
}
std::vector<aruco::Marker> MarkerDetector_Impl::detect(const cv::Mat& input, const CameraParameters& camParams,
float markerSizeMeters,
bool setYPerperdicular)
{
std::vector<Marker> detectedMarkers;
detect(input, detectedMarkers, camParams, markerSizeMeters, setYPerperdicular);
return detectedMarkers;
}
void MarkerDetector_Impl::detect(const cv::Mat& input, std::vector<Marker>& detectedMarkers, CameraParameters camParams,
float markerSizeMeters, bool setYPerpendicular)
{
if (camParams.CamSize != input.size() && camParams.isValid() && markerSizeMeters > 0)
{
// must resize camera parameters if we want to compute properly marker poses
CameraParameters cp_aux = camParams;
cp_aux.resize(input.size());
detect(input, detectedMarkers, cp_aux.CameraMatrix, cp_aux.Distorsion, markerSizeMeters, setYPerpendicular);
}
else
{
detect(input, detectedMarkers, camParams.CameraMatrix, camParams.Distorsion, markerSizeMeters,setYPerpendicular);
}
}
void MarkerDetector_Impl::detect(const cv::Mat& input, vector<Marker>& detectedMarkers, Mat camMatrix, Mat distCoeff,
float markerSizeMeters, bool setYPerpendicular)
函数体里面的内容太多了
六、Marker类
代码如下
class ARUCO_EXPORT Marker : public std::vector<cv::Point2f>
{
public:
// id of the marker
int id;
// size of the markers sides in meters
float ssize;
// matrices of rotation and translation respect to the camera
cv::Mat Rvec, Tvec;
//additional info about the dictionary
std::string dict_info;
//points of the contour
vector<cv::Point> contourPoints;
/**
*/
Marker();
/**
*/
Marker(int id);
/**
*/
Marker(const Marker& M);
/**
*/
Marker(const std::vector<cv::Point2f>& corners, int _id = -1);
/**
*/
~Marker()
{
}
/**Indicates if this object is valid
*/
bool isValid() const
{
return id != -1 && size() == 4;
}
bool isPoseValid()const{return !Rvec.empty() && !Tvec.empty();}
/**Draws this marker in the input image
*/
void draw(cv::Mat& in, cv::Scalar color=cv::Scalar(0,0,255), int lineWidth = -1, bool writeId = true,bool writeInfo=false) const;
/**Calculates the extrinsics (Rvec and Tvec) of the marker with respect to the camera
* @param markerSize size of the marker side expressed in meters
* @param CP parmeters of the camera
* @param setYPerpendicular If set the Y axis will be perpendicular to the surface. Otherwise, it will be the Z
* axis
*/
void calculateExtrinsics(float markerSize, const CameraParameters& CP,
bool setYPerpendicular = true);
/**Calculates the extrinsics (Rvec and Tvec) of the marker with respect to the camera
* @param markerSize size of the marker side expressed in meters
* @param CameraMatrix matrix with camera parameters (fx,fy,cx,cy)
* @param Distorsion matrix with distorsion parameters (k1,k2,p1,p2)
* @param setYPerpendicular If set the Y axis will be perpendicular to the surface. Otherwise, it will be the Z
* axis
*/
void calculateExtrinsics(float markerSize, cv::Mat CameraMatrix, cv::Mat Distorsion = cv::Mat(),
bool setYPerpendicular = true);
/**Given the extrinsic camera parameters returns the GL_MODELVIEW matrix for opengl.
* Setting this matrix, the reference coordinate system will be set in this marker
*/
void glGetModelViewMatrix(double modelview_matrix[16]);
/**
* Returns position vector and orientation quaternion for an Ogre scene node or entity.
* Use:
* ...
* Ogre::Vector3 ogrePos (position[0], position[1], position[2]);
* Ogre::Quaternion ogreOrient (orientation[0], orientation[1], orientation[2], orientation[3]);
* mySceneNode->setPosition( ogrePos );
* mySceneNode->setOrientation( ogreOrient );
* ...
*/
void OgreGetPoseParameters(double position[3], double orientation[4]);
/**Returns the centroid of the marker
*/
cv::Point2f getCenter() const;
/**Returns the perimeter of the marker
*/
float getPerimeter() const;
/**Returns the area
*/
float getArea() const;
/**Returns radius of enclosing circle
*/
float getRadius()const;
/**compares ids
*/
bool operator==(const Marker& m) const
{
return m.id == id;
}
void copyTo(Marker &m) const;
/**compares ids
*/
Marker & operator=(const Marker& m) ;
/**
*/
friend bool operator<(const Marker& M1, const Marker& M2)
{
return M1.id < M2.id;
}
/**
*/
friend std::ostream& operator<<(std::ostream& str, const Marker& M){
str << M.id << "=";
for (int i = 0; i < 4; i++)
str << "(" << M[i].x << "," << M[i].y << ") ";
if( !M.Tvec.empty() && !M.Rvec.empty()){
str << "Txyz=";
for (int i = 0; i < 3; i++)
str << M.Tvec.ptr<float>(0)[i] << " ";
str << "Rxyz=";
for (int i = 0; i < 3; i++)
str << M.Rvec.ptr<float>(0)[i] << " ";
}
return str;
}
// saves to a binary stream
void toStream(std::ostream& str) const;
// reads from a binary stream
void fromStream(std::istream& str);
// returns the 3d points of a marker wrt its center
static vector<cv::Point3f> get3DPoints(float msize);
//returns the 3d points of this marker wrt its center
inline vector<cv::Point3f> get3DPoints()const{
return get3DPoints(ssize);
}
//returns the SE3 (4x4) transform matrix
cv::Mat getTransformMatrix()const;
private:
void rotateXAxis(cv::Mat& rotation);
};
该类的一些重要特性和功能的解释:
成员变量:
id
:标记的标识符
ssize
:标记的边长(米)
Rvec
和 Tvec
:相对于相机的旋转和平移矩阵
dict_info
:关于字典的附加信息
contourPoints
:轮廓点的列表
构造函数和析构函数:
Marker()
Marker(int id)
Marker(const Marker& M)
Marker(const std::vector<cv::Point2f>& corners, int _id = -1)
~Marker()
公共成员函数:
isValid()
:判断标记对象是否有效
isPoseValid()
:判断标记的姿态是否有效
draw()
:在图像中绘制标记
calculateExtrinsics()
:计算相对于相机的外部参数
glGetModelViewMatrix()
:返回 OpenGL 的 GL_MODELVIEW 矩阵
OgreGetPoseParameters()
:返回用于 Ogre 场景节点或实体的位置向量和方向四元数
其他用于计算标记属性或进行变换的函数
运算符重载:
operator==()
:比较标记的标识符
operator=()
:赋值运算符重载
operator<()
:比较运算符重载
operator<<()
:输出流运算符重载
私有成员函数:
rotateXAxis()
:X 轴旋转函数
此类提供了对标记对象进行操作和绘制的功能,并且包含了一些额外的实用函数,用于计算标记的特征和进行姿态变换
总结
虽然代码中的注释显示既可以传入视频也可以传入图片,但是aruco_simple.cpp只能显示图片中检测到的标记或视频的第一帧