在这篇文章中,我们将解释什么是 ArUco 标记,以及如何使用 OpenCV 将它们用于简单的增强现实任务。
ArUco 标记已经在增强现实、相机姿态估计和相机校准中使用了一段时间。让我们更多地了解它们。
1.什么是 ArUco 标记?
ArUco 代表科尔多瓦增强现实大学。那是它在西班牙开发的地方。下面是 ArUco 标记的一些示例。 aruco 标记是放置在被成像物体或场景上的基准标记。它是一个带有黑色背景和边界的二进制正方形,其中生成的白色图案唯一地标识了它。黑色的边界使它们更容易被发现。它们可以以各种大小生成。根据物体大小和场景来选择尺寸,以便成功检测。如果非常小的标记没有被检测到,只需增加它们的大小就可以使它们更容易被检测到。
这个想法是,你打印这些标记,并把它们放在现实世界中。您可以拍摄真实世界,并独特地检测这些标记。
如果您是初学者,您可能会想这有什么用?让我们看几个用例。
在我们分享的示例中,我们将印刷品和标记放在相框的角上。当我们唯一地识别标记时,我们就能够用任意视频或图像替换相框。当我们移动相机时,新图片具有正确的透视失真。
在机器人应用程序中,您可以将这些标记放置在配备摄像头的仓库机器人的路径上。当安装在机器人上的摄像头检测到这些标记中的一个时,它可以知道它在仓库中的精确位置,因为每个标记都有一个唯一的 ID,我们知道标记在仓库中的位置。
2.在 OpenCV 中生成 ArUco 标记
我们可以使用 OpenCV 非常轻松地生成这些标记。 OpenCV 中的 aruco 模块共有 25 个预定义的标记字典。字典中的所有标记包含相同数量的块或位(4×4、5×5、6×6 或 7×7),每个字典包含固定数量的标记(50、100、250 或 1000) .下面我们将展示如何在 C++ 和 Python 中生成和检测各种类型的 aruco 标记。
我们需要在代码中使用 aruco 模块。
下面的函数调用 getPredefinedDictionary 显示了如何加载 250 个标记的字典,其中每个标记包含一个 6×6 位二进制模式。 (1)Python
import cv2 as cv
import numpy as np
dictionary = cv.aruco.Dictionary_get(cv.aruco.DICT_6X6_250)
markerImage = np.zeros((200, 200), dtype=np.uint8)
markerImage = cv.aruco.drawMarker(dictionary, 33, 200, markerImage, 1);
cv.imwrite("marker33.png", markerImage);
(2)C++
#include <opencv2/highgui.hpp>
#include <opencv2/aruco.hpp>
using namespace cv;
int main(int argc, char *argv[]) {
Mat markerImage;
Ptr<cv::aruco::Dictionary> dictionary = aruco::getPredefinedDictionary(cv::aruco::DICT_6X6_250);
aruco::drawMarker(dictionary, 33, 200, markerImage, 1);
imwrite("marker33.png", markerImage);
}
上面的 drawMarker 函数让我们从 250 个标记的集合中选择具有给定 id(第二个参数 - 33)的标记,这些标记的 id 从 0 到 249。 drawMarker 函数的第三个参数决定生成的标记的大小。在上面的例子中,它会生成一个 200×200 像素的图像。第四个参数表示将存储生成的标记的对象(上面的markerImage)。最后,第五个参数是厚度参数,它决定了应该添加多少块作为生成的二进制模式的边界。在上面的示例中,将在 6×6 生成的模式周围添加 1 位边界,以在 200×200 像素图像中生成具有 7×7 位的图像。使用上述代码生成的标记如上图所示。
3.检测 Aruco 标记
使用 aruco 标记对场景进行成像后,我们需要检测它们并将其用于进一步处理。下面我们将展示如何检测标记。
Ptr<Dictionary> dictionary = getPredefinedDictionary(DICT_6X6_250);
Ptr<DetectorParameters> parameters = DetectorParameters::create();
vector<vector<Point2f>> markerCorners, rejectedCandidates;
vector<int> markerIds;
detectMarkers(frame, dictionary, markerCorners, markerIds, parameters, rejectedCandidates);
dictionary = cv.aruco.Dictionary_get(cv.aruco.DICT_6X6_250)
parameters = cv.aruco.DetectorParameters_create()
markerCorners, markerIds, rejectedCandidates = cv.aruco.detectMarkers(frame, dictionary, parameters=parameters)
我们首先加载用于生成标记的相同字典。
使用 DetectorParameters::create() 初始化检测器参数。 OpenCV 允许我们在检测过程中更改多个参数。可以在此处找到可以调整的参数列表,包括自适应阈值。在大多数情况下,默认参数运行良好,OpenCV 建议使用这些参数。所以我们将坚持使用默认参数。
对于每个成功的标记检测,按从左上、右上、右下和左下的顺序检测标记的四个角点。在 C++ 中,这 4 个检测到的角点被存储为一个点向量,图像中的多个标记一起存储在一个点向量向量中。在 Python 中,它们存储为 Numpy 数组。
detectMarkers 函数用于检测和定位标记的角点。第一个参数是带有标记的场景图像。第二个参数是用于生成标记的字典。成功检测到的标记将存储在markerCorners 中,它们的id 存储在markerIds 中。之前初始化的 DetectorParameters 对象也作为参数传递。最后,被拒绝的候选人被存储在rejectedCandidates中。
在场景中打印、剪切和放置标记时,重要的是我们在标记的黑色边界周围保留一些白色边框,以便可以轻松检测到它们。
4.增强现实应用
ArUco 标记主要是为了解决包括增强现实在内的各种应用的相机姿态估计问题而开发的。 OpenCV 在其文档中详细描述了姿态估计过程。
在这篇文章中,我们将它们用于增强现实应用程序,让我们可以将任何新场景叠加到现有图像或视频上。我们在家里挑了一个有大相框的场景,我们想用新的替换相框中的图片,看看它们在墙上看起来如何。然后我们继续尝试在电影中插入视频。为此,我们将大的 aruco 标记打印、剪切并粘贴到图像区域的角落,如下图所示,然后捕获视频。捕获的视频位于帖子顶部的视频左侧。然后我们按顺序单独处理视频的每一帧。
对于每个图像,首先检测标记。下图显示了以绿色绘制的检测到的标记。第一个点用小红圈标记。可以通过顺时针遍历标记的边界来访问第二、第三和第四点。
输入图像和新场景图像中的四个对应点集用于计算单应性。给定场景不同视图中的对应点,单应性是一种将一个对应点映射到另一个对应点的变换。
在我们的例子中,单应矩阵用于将新场景图像扭曲成由我们捕获的图像中的标记定义的四边形。我们在下面的代码中展示了如何做到这一点。 (1)C++
Mat h = cv::findHomography(pts_src, pts_dst);
Mat warpedImage;
warpPerspective(im_src, warpedImage, h, frame.size(), INTER_CUBIC);
Mat mask = Mat::zeros(frame.rows, frame.cols, CV_8UC1);
fillConvexPoly(mask, pts_dst, Scalar(255, 255, 255));
Mat element = getStructuringElement( MORPH_RECT, Size(3,3) );
erode(mask, mask, element);
Mat imOut = frame.clone();
warpedImage.copyTo(imOut, mask);
(2)Python
h, status = cv.findHomography(pts_src, pts_dst)
warped_image = cv.warpPerspective(im_src, h, (frame.shape[1],frame.shape[0]))
mask = np.zeros([frame.shape[0], frame.shape[1]], dtype=np.uint8);
cv.fillConvexPoly(mask, np.int32([pts_dst_m]), (255, 255, 255), cv.LINE_AA);
element = cv.getStructuringElement(cv.MORPH_RECT, (3,3));
mask = cv.erode(mask, element, iterations=3);
warped_image = warped_image.astype(float)
mask3 = np.zeros_like(warped_image)
for i in range(0, 3):
mask3[:,:,i] = mask/255
warped_image_masked = cv.multiply(warped_image, mask3)
frame_masked = cv.multiply(frame.astype(float), 1-mask3)
im_out = cv.add(warped_image_masked, frame_masked)
我们使用新的场景图像角点作为源点(pts_src),并将我们捕获的图像中相框内对应的图片角点作为目标点(dst_src)。 OpenCV 函数 findHomography 计算源点和目标点之间的单应函数 h。然后使用单应矩阵来扭曲新图像以适应目标帧。扭曲的图像被屏蔽并复制到目标帧中。在视频的情况下,这个过程在每一帧上重复。
5.完整代码
链接:https:
提取码:123a
(1)Python
import cv2 as cv
import argparse
import sys
import os.path
import numpy as np
parser = argparse.ArgumentParser(description='Augmented Reality using Aruco markers in OpenCV')
parser.add_argument('--image', help='Path to image file.', default="test.jpg")
parser.add_argument('--video', help='Path to video file.')
args = parser.parse_args()
im_src = cv.imread("new_scenery.jpg");
outputFile = "ar_out_py.avi"
if (args.image):
if not os.path.isfile(args.image):
print("Input image file ", args.image, " doesn't exist")
sys.exit(1)
cap = cv.VideoCapture(args.image)
outputFile = args.image[:-4]+'_ar_out_py.jpg'
elif (args.video):
if not os.path.isfile(args.video):
print("Input video file ", args.video, " doesn't exist")
sys.exit(1)
cap = cv.VideoCapture(args.video)
outputFile = args.video[:-4]+'_ar_out_py.avi'
print("Storing it as :", outputFile)
else:
cap = cv.VideoCapture(0)
if (not args.image):
vid_writer = cv.VideoWriter(outputFile, cv.VideoWriter_fourcc('M','J','P','G'), 28, (round(2*cap.get(cv.CAP_PROP_FRAME_WIDTH)),round(cap.get(cv.CAP_PROP_FRAME_HEIGHT))))
winName = "Augmented Reality using Aruco markers in OpenCV"
while cv.waitKey(1) < 0:
try:
hasFrame, frame = cap.read()
if not hasFrame:
print("Done processing !!!")
print("Output file is stored as ", outputFile)
cv.waitKey(3000)
break
dictionary = cv.aruco.Dictionary_get(cv.aruco.DICT_6X6_250)
parameters = cv.aruco.DetectorParameters_create()
markerCorners, markerIds, rejectedCandidates = cv.aruco.detectMarkers(frame, dictionary, parameters=parameters)
index = np.squeeze(np.where(markerIds==25));
refPt1 = np.squeeze(markerCorners[index[0]])[1];
index = np.squeeze(np.where(markerIds==33));
refPt2 = np.squeeze(markerCorners[index[0]])[2];
distance = np.linalg.norm(refPt1-refPt2);
scalingFac = 0.02;
pts_dst = [[refPt1[0] - round(scalingFac*distance), refPt1[1] - round(scalingFac*distance)]];
pts_dst = pts_dst + [[refPt2[0] + round(scalingFac*distance), refPt2[1] - round(scalingFac*distance)]];
index = np.squeeze(np.where(markerIds==30));
refPt3 = np.squeeze(markerCorners[index[0]])[0];
pts_dst = pts_dst + [[refPt3[0] + round(scalingFac*distance), refPt3[1] + round(scalingFac*distance)]];
index = np.squeeze(np.where(markerIds==23));
refPt4 = np.squeeze(markerCorners[index[0]])[0];
pts_dst = pts_dst + [[refPt4[0] - round(scalingFac*distance), refPt4[1] + round(scalingFac*distance)]];
pts_src = [[0,0], [im_src.shape[1], 0], [im_src.shape[1], im_src.shape[0]], [0, im_src.shape[0]]];
pts_src_m = np.asarray(pts_src)
pts_dst_m = np.asarray(pts_dst)
h, status = cv.findHomography(pts_src_m, pts_dst_m)
warped_image = cv.warpPerspective(im_src, h, (frame.shape[1],frame.shape[0]))
mask = np.zeros([frame.shape[0], frame.shape[1]], dtype=np.uint8);
cv.fillConvexPoly(mask, np.int32([pts_dst_m]), (255, 255, 255), cv.LINE_AA);
element = cv.getStructuringElement(cv.MORPH_RECT, (3,3));
mask = cv.erode(mask, element, iterations=3);
warped_image = warped_image.astype(float)
mask3 = np.zeros_like(warped_image)
for i in range(0, 3):
mask3[:,:,i] = mask/255
warped_image_masked = cv.multiply(warped_image, mask3)
frame_masked = cv.multiply(frame.astype(float), 1-mask3)
im_out = cv.add(warped_image_masked, frame_masked)
concatenatedOutput = cv.hconcat([frame.astype(float), im_out]);
cv.imshow("AR using Aruco markers", concatenatedOutput.astype(np.uint8))
if (args.image):
cv.imwrite(outputFile, concatenatedOutput.astype(np.uint8));
else:
vid_writer.write(concatenatedOutput.astype(np.uint8))
except Exception as inst:
print(inst)
cv.destroyAllWindows()
if 'vid_writer' in locals():
vid_writer.release()
print('Video writer released..')
(2)C++
#include <fstream>
#include <sstream>
#include <iostream>
#include <opencv2/aruco.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/calib3d.hpp>
const char* keys =
"{help h usage ? | | Usage examples: \n\t\t./augmented_reality_with_aruco.out --image=test.jpg \n\t\t./augmented_reality_with_aruco.out --video=test.mp4}"
"{image i |<none>| input image }"
"{video v |<none>| input video }"
;
using namespace cv;
using namespace aruco;
using namespace std;
int main(int argc, char** argv)
{
CommandLineParser parser(argc, argv, keys);
parser.about("Use this script to do Augmented Reality using Aruco markers in OpenCV.");
if (parser.has("help"))
{
parser.printMessage();
return 0;
}
string str, outputFile;
VideoCapture cap;
VideoWriter video;
Mat frame, blob;
Mat im_src = imread("new_scenery.jpg");
try {
outputFile = "ar_out_cpp.avi";
if (parser.has("image"))
{
str = parser.get<String>("image");
ifstream ifile(str);
if (!ifile) throw("error");
cap.open(str);
str.replace(str.end()-4, str.end(), "_ar_out_cpp.jpg");
outputFile = str;
}
else if (parser.has("video"))
{
str = parser.get<String>("video");
ifstream ifile(str);
if (!ifile) throw("error");
cap.open(str);
str.replace(str.end()-4, str.end(), "_ar_out_cpp.avi");
outputFile = str;
}
else cap.open(parser.get<int>("device"));
}
catch(...) {
cout << "Could not open the input image/video stream" << endl;
return 0;
}
if (!parser.has("image")) {
video.open(outputFile, VideoWriter::fourcc('M','J','P','G'), 28, Size(2*cap.get(CAP_PROP_FRAME_WIDTH), cap.get(CAP_PROP_FRAME_HEIGHT)));
}
static const string kWinName = "Augmented Reality using Aruco markers in OpenCV";
namedWindow(kWinName, WINDOW_NORMAL);
while (waitKey(1) < 0)
{
cap >> frame;
try {
if (frame.empty()) {
cout << "Done processing !!!" << endl;
cout << "Output file is stored as " << outputFile << endl;
waitKey(3000);
break;
}
vector<int> markerIds;
Ptr<Dictionary> dictionary = getPredefinedDictionary(DICT_6X6_250);
vector<vector<Point2f>> markerCorners, rejectedCandidates;
Ptr<DetectorParameters> parameters = DetectorParameters::create();
detectMarkers(frame, dictionary, markerCorners, markerIds, parameters, rejectedCandidates);
vector<Point> pts_dst;
float scalingFac = 0.02;
Point refPt1, refPt2, refPt3, refPt4;
std::vector<int>::iterator it = std::find(markerIds.begin(), markerIds.end(), 25);
int index = std::distance(markerIds.begin(), it);
refPt1 = markerCorners.at(index).at(1);
it = std::find(markerIds.begin(), markerIds.end(), 33);
index = std::distance(markerIds.begin(), it);
refPt2 = markerCorners.at(index).at(2);
float distance = norm(refPt1-refPt2);
pts_dst.push_back(Point(refPt1.x - round(scalingFac*distance), refPt1.y - round(scalingFac*distance)));
pts_dst.push_back(Point(refPt2.x + round(scalingFac*distance), refPt2.y - round(scalingFac*distance)));
it = std::find(markerIds.begin(), markerIds.end(), 30);
index = std::distance(markerIds.begin(), it);
refPt3 = markerCorners.at(index).at(0);
pts_dst.push_back(Point(refPt3.x + round(scalingFac*distance), refPt3.y + round(scalingFac*distance)));
it = std::find(markerIds.begin(), markerIds.end(), 23);
index = std::distance(markerIds.begin(), it);
refPt4 = markerCorners.at(index).at(0);
pts_dst.push_back(Point(refPt4.x - round(scalingFac*distance), refPt4.y + round(scalingFac*distance)));
vector<Point> pts_src;
pts_src.push_back(Point(0,0));
pts_src.push_back(Point(im_src.cols, 0));
pts_src.push_back(Point(im_src.cols, im_src.rows));
pts_src.push_back(Point(0, im_src.rows));
Mat h = cv::findHomography(pts_src, pts_dst);
Mat warpedImage;
warpPerspective(im_src, warpedImage, h, frame.size(), INTER_CUBIC);
Mat mask = Mat::zeros(frame.rows, frame.cols, CV_8UC1);
fillConvexPoly(mask, pts_dst, Scalar(255, 255, 255), LINE_AA);
Mat element = getStructuringElement( MORPH_RECT, Size(5,5));
erode(mask, mask, element);
Mat imOut = frame.clone();
warpedImage.copyTo(imOut, mask);
Mat concatenatedOutput;
hconcat(frame, imOut, concatenatedOutput);
if (parser.has("image")) imwrite(outputFile, concatenatedOutput);
else video.write(concatenatedOutput);
imshow(kWinName, concatenatedOutput);
}
catch(const std::exception& e) {
cout << endl << " e : " << e.what() << endl;
cout << "Could not do homography !! " << endl;
}
}
cap.release();
if (!parser.has("image")) video.release();
return 0;
}
参考目录
https://learnopencv.com/augmented-reality-using-aruco-markers-in-opencv-c-python/
|