×

Ultra96 V2上基于标记的增强现实

消耗积分:2 | 格式:zip | 大小:0.22 MB | 2022-11-16

李骏鹏

分享资料个

描述

介绍

增强现实 (AR) 是真实世界环境的实时视图,已通过计算机生成的图形进行了增强。AR有两种实现方式:

  • 基于标记的增强现实
  • 无标记 AR

Marker-less AR 更加复杂,并且依赖于真实环境来确定参考点。这通常涉及识别物体和/或背景线索,例如地板和墙壁。

基于标记的 AR 更易于实现,并且依赖于场景中的方形标记。

该项目将描述如何使用 OpenCV 在 Ultra96-V2 上实现基于标记的 AR。

让我们开始吧 !

灵感

在开始之前,我想分享一下这个项目的灵感和动力。

我受到以下创新产品/教程的启发:

  • 彩通,配色卡,
  • Adrian Rosebrock,使用 OpenCV 和 Python 检测 ArUco 标记,PyImageSearch,

动机

我实现类似功能的动机是能够使用标记自动触发某种校准,例如:

  • 白平衡,使用白色参考图表
  • 立体校准,使用棋盘参考图

为了实现这一点,我创建了以下三个图表(使用 Microsoft Word)进行实验。

 

在这项目中,我会检测这些图表的存在,并根据图表进行额外的处理:

  • 图表 1 - 在棋盘图案周围画一个绿色框
  • 图表 2 - 测量标记内区域中 B、GR 像素的平均值,并在图表上显示带有值的条形图
  • 图表 2 - 测量标记内区域的颜色直方图,并在图表上显示直方图

有关生成这些标记的更多信息,请参阅以下优秀教程:

  • Adrian Rosebrock,使用 OpenCV 和 Python 生成 ArUco 标记,PyImageSearch,

检测场景中的标记

定义好目标并创建带有标记的图表后,我们就可以开始实施了。

我选择用 C++ 实现这个项目,作为一个 gstreamer 插件。我需要感谢Tom Simpson为创建 gstreamer 插件奠定了基础。

第一步是检测场景中的标记。这是通过 OpenCV 完成的。

/* Aruco Markers */
#include 

...

//
// Detect ARUCO markers
//   ref : https://docs.opencv.org/master/d5/dae/tutorial_aruco_detection.html
//
std::vector markerIds;
std::vector<std::vector<cv::Point2f>> markerCorners, rejectedCandidates;
cv::Ptr<cv::aruco::DetectorParameters> parameters = cv::aruco::DetectorParameters::create();
cv::Ptr<cv::aruco::Dictionary> dictionary = cv::aruco::getPredefinedDictionary(cv::aruco::DICT_ARUCO_ORIGINAL);
cv::aruco::detectMarkers(img, dictionary, markerCorners, markerIds, parameters, rejectedCandidates);
if ( markerIds.size() > 0 )
{
  cv::aruco::drawDetectedMarkers(img, markerCorners, markerIds);
}

此时,您会注意到我正在使用 DICT_ARUCO_ORIGINAL 系列标记。标记包含一个由黑色边框包围的 5x5 方格。

 
 
 
pYYBAGNy7QOAYTfaAAALiIPgwzk770.png
 
1 / 6DICT_ARUCO_ORIGINAL - ID = 923
 

对于每个检测到的标记,OpenCV API 返回以下信息:

  • markerIds : 每个标记的标识
  • markerCorners : 每个标记的坐标,格式如下: [0] top-left [1] top-right [2] bottom-right [3] bottom-left
pYYBAGNy7QWAPSfrAAAUQAvaWxA531.png
检测到的标记信息
 

下表和图像说明了我创建的图表中使用了哪些标记。

pYYBAGNy7QeAGPX7AABL0ceJpXc987.png
ArUco 标记用法
 

我使用 switch case 状态扫描检测到的标记,并识别感兴趣的标记。

...
  if (markerIds.size() >= 4 )
  {
    int tl_id = 0;
    int tr_id = 0;
    int bl_id = 0;
    int br_id = 0;
    cv::Point2f tl_xy, tr_xy, bl_xy, br_xy;
    for ( unsigned i = 0; i < markerIds.size(); i++ )
    {
      switch ( markerIds[i] )
      {
      case 923:
        tl_id = markerIds[i];
        tl_xy = markerCorners[i][3]; // bottom left corner of top left marker
        break;
      case 1001:
      case 1002:
      case 1003:
      case 1004:
      case 1005:
      case 1006:
        tr_id = markerIds[i];
        tr_xy = markerCorners[i][2]; // bottom right corner of top right marker
        break;
      case 1007:
        bl_id = markerIds[i];
        bl_xy = markerCorners[i][0]; // top left corner of bottom left marker
        break;
      case 241:
        br_id = markerIds[i];
        br_xy = markerCorners[i][1]; // top right corner of bottom right marker
        break;
      default:
        break;
      }
    }
    ...
  }
  ...

下图说明了我保留哪些 ID 和 X/Y 坐标来定义感兴趣区域 (ROI) 以进行附加处理。

poYBAGNy7QqAba8GAABSJMHlrRY300.png
感兴趣区域 (ROI) 规范
 

检测特定图表是通过以下简单的条件语句完成的。

// Chart 1 - Checkerboard (9x7)
if ( (tl_id==923) && (tr_id==1001) && (bl_id==1007) && (br_id==241) )
{
   ...
}
// Chart 2 - White Reference
if ( (tl_id==923) && (tr_id==1002) && (bl_id==1007) && (br_id==241) )
   ...
}
{
// Chart 3 - Histogram
if ( (tl_id==923) && (tr_id==1003) && (bl_id==1007) && (br_id==241) )
{
   ...
}

将计算机生成的图形添加到场景中

对于“Chart 1 - CheckerBoard (9x7)”,绘制了一个绿色矩形来标识感兴趣的区域。

// Extract ROI (area, ideally within 4 markers)
      std::vector<cv::Point> polygonPoints;
      polygonPoints.push_back(cv::Point(tl_xy.x,tl_xy.y));
      polygonPoints.push_back(cv::Point(tr_xy.x,tr_xy.y));
      polygonPoints.push_back(cv::Point(br_xy.x,br_xy.y));
      polygonPoints.push_back(cv::Point(bl_xy.x,bl_xy.y));
      // Draw border around "checkerboard"
      cv::polylines(img, polygonPoints, true, cv::Scalar (0, 255, 0), 2, 16);

对于“图表 2 - 白色参考”,感兴趣区域用于计算蓝色 (B)、绿色 (G) 和红色 (R) 分量中的每一个的平均值。下面的代码使用了一个掩码,它支持一个不是完美矩形的 ROI。

//
      // Calculate color gains
      //   ref : https://stackoverflow.com/questions/32466616/finding-the-average-color-within-a-polygon-bound-in-opencv
      //
      cv::Point pts[1][4];
      pts[0][0] = cv::Point(tl_xy.x,tl_xy.y);
      pts[0][1] = cv::Point(tr_xy.x,tr_xy.y);
      pts[0][2] = cv::Point(br_xy.x,br_xy.y);
      pts[0][3] = cv::Point(bl_xy.x,bl_xy.y);
      const cv::Point* points[1] = {pts[0]};
      int npoints = 4;
      // Create the mask with the polygon
      cv::Mat1b mask(img.rows, img.cols, uchar(0));
      cv::fillPoly(mask, points, &npoints, 1, cv::Scalar(255));
      // Calculate mean in masked area
      auto bgr_mean = cv::mean( img, mask );
      double b_mean = bgr_mean(0);
      double g_mean = bgr_mean(1);
      double r_mean = bgr_mean(2);

计算出平均值后,将创建一个 plotImage,其中包含一个带有 B、G 和 R 像素平均值的条形图图像。

// Draw bars
int plot_w = 100, plot_h = 100;
cv::Mat plotImage( plot_h, plot_w, CV_8UC3, cv::Scalar(255,255,255) );
int b_bar = int((b_mean/256.0)*80.0);
int g_bar = int((g_mean/256.0)*80.0);
int r_bar = int((r_mean/256.0)*80.0);
// layout of bars : |<-10->|<---20-->|<-10->|<---20-->|<-10->|<---20-->|<-10->|
cv::rectangle(plotImage, cv::Rect(10,(80-b_bar),20,b_bar), cv::Scalar(255, 0, 0), cv::FILLED, cv::LINE_8);
cv::rectangle(plotImage, cv::Rect(40,(80-g_bar),20,g_bar), cv::Scalar(0, 255, 0), cv::FILLED, cv::LINE_8);
cv::rectangle(plotImage, cv::Rect(70,(80-r_bar),20,r_bar), cv::Scalar(0, 0, 255), cv::FILLED, cv::LINE_8);
std::stringstream b_str;
std::stringstream g_str;
std::stringstream r_str;
b_str << int(b_mean);
g_str << int(g_mean);
r_str << int(r_mean);
cv::putText(plotImage, b_str.str(), cv::Point(10,90), cv::FONT_HERSHEY_PLAIN, 0.75, cv::Scalar(255,0,0), 1, cv::LINE_AA);
cv::putText(plotImage, g_str.str(), cv::Point(40,90), cv::FONT_HERSHEY_PLAIN, 0.75, cv::Scalar(0,255,0), 1, cv::LINE_AA);
cv::putText(plotImage, r_str.str(), cv::Point(70,90), cv::FONT_HERSHEY_PLAIN, 0.75, cv::Scalar(0,0,255), 1, cv::LINE_AA);
poYBAGNy7QyADLBjAAAOqFvHp2s114.png
颜色平均值的示例条形图图像
 

最后,使用以下 OpenCV 函数将此绘图图像扭曲到感兴趣区域:

  • findHomography :计算源坐标和目标坐标之间的变换矩阵
  • warpPerspective :将源图像扭曲到目标坐标

然后使用掩码将扭曲的条形图图像与实时图像组合。

// Calculate transformation matrix
std::vector<cv::Point2f> srcPoints;
std::vector<cv::Point2f> dstPoints;
srcPoints.push_back(cv::Point(       0,       0)); // top left
srcPoints.push_back(cv::Point(plot_w-1,       0)); // top right
srcPoints.push_back(cv::Point(plot_w-1,plot_h-1)); // bottom right
srcPoints.push_back(cv::Point(       0,plot_h-1)); // bottom left
dstPoints.push_back(tl_xy);
dstPoints.push_back(tr_xy);
dstPoints.push_back(br_xy);
dstPoints.push_back(bl_xy);
cv::Mat h = cv::findHomography(srcPoints,dstPoints);
// Warp plot image onto video frame
cv::Mat img_temp = img.clone();
cv::warpPerspective(plotImage, img_temp, h, img_temp.size());
cv::Point pts_dst[4];
for( int i = 0; i < 4; i++)
{
pts_dst[i] = dstPoints[i];
}
cv::fillConvexPoly(img, pts_dst, 4, cv::Scalar(0), cv::LINE_AA);
img = img + img_temp;

对于“图表 3 - 直方图”,使用了与“图表 2”类似的技术。不是显示颜色平均值的条形图,而是显示每个颜色分量的直方图。

poYBAGNy7Q-AB7k-AABPYTtbqQI822.png
颜色分量的示例直方图绘图图像
 

了解了所有理论之后,是时候在 Ultra96-V2 上进行嵌入式实现了。

第 0 步 - 打印图表

为了运行此示例,您将需要三个图表,这些图表已在“原理图”部分以 PDF 格式提供。

  • 图 1 - 棋盘 (9x7)
  • 图表 2 - 白色参考
  • 图表 2 - 直方图

第 1 步 - 创建 SD 卡

为以下 Avnet 平台提供了预构建的 Vitis-AI 1.3 SD 卡映像:

  • u96v2_sbc_base : Ultra96-V2 开发板
  • uz7ev_evcc_base:UltraZed-EV SOM (7EV) + FMC 载卡
  • uz3eg_iocc_base:UltraZed-EG SOM (3EG) + IO 载卡

可在此处找到预构建 SD 卡映像的下载链接:

下载并解压后,.img 文件可以编程到 16GB 微型 SD 卡。

0.解压压缩包得到.img文件

1. 将开发板特定的 SD 卡映像编程到 16GB(或更大)的 micro SD 卡

一个。在 Windows 机器上,使用 Balena Etcher 或 Win32DiskImager(免费开源软件)

湾。在 linux 机器上,使用 Balena Etcher 或使用 dd 实用程序

$ sudo dd bs=4M if=Avnet-{platform}-Vitis-AI-1-3-{date}.img of=/dev/sd{X} status=progress conv=fsync

其中 {X} 是一个小写字母,用于指定 SD 卡的设备。您可以使用“df -h”来确定您的 SD 卡对应的设备。

第 2 步 - 克隆源代码存储库

本项目中使用的源代码可以从以下存储库中获取:

如果您有活动的互联网连接,您可以简单地将存储库克隆到嵌入式平台的根目录:

$ cd ~
$ git clone 

第 3 步 - 编译和安装 gstreamer 插件

gstreamer 插件可以在 Ultra96-V2 嵌入式平台上使用 make 命令构建:

$ cd vitis_ai_gstreamer_plugins
$ cd markerdetect
$ make

编译完成后,gstreamer 插件可以按如下方式安装:

$ cp libgstmarkerdetect.so /usr/lib/gstreamer-1.0/.

可以使用 gst-inspect-1.0 实用程序验证 gstreamer 插件的安装:

$ gst-inspect-1.0 | grep markerdetect
markerdetect:  markerdetect: Marker detection using the OpenCV Library

$ gst-inspect-1.0 markerdetect
Factory Details:
  Rank                     none (0)
  Long-name                Marker detection using the OpenCV Library
  Klass                    Video Filter
  Description              Marker Detection
  Author                   FIXME 

Plugin Details:
  Name                     markerdetect
  Description              Marker detection using the OpenCV Library
  Filename                 /usr/lib/gstreamer-1.0/libgstmarkerdetect.so
  Version                  0.0.0
  License                  LGPL
  Source module            markerdetect
  Binary package           OpenCV Library
  Origin URL               http://avnet.com

GObject
 +----GInitiallyUnowned
       +----GstObject
             +----GstElement
                   +----GstBaseTransform
                         +----GstVideoFilter
                               +----GstMarkerDetect

Pad Templates:
  SRC template: 'src'
    Availability: Always
    Capabilities:
      video/x-raw
                 format: { (string)BGR }
                  width: [ 1, 1920 ]
                 height: [ 1, 1080 ]
              framerate: [ 0/1, 2147483647/1 ]

  SINK template: 'sink'
    Availability: Always
    Capabilities:
      video/x-raw
                 format: { (string)BGR }
                  width: [ 1, 1920 ]
                 height: [ 1, 1080 ]
              framerate: [ 0/1, 2147483647/1 ]

Element has no clocking capabilities.
Element has no URI handling capabilities.

Pads:e--
  SINK: 'sink'
    Pad Template: 'sink'
  SRC: 'src'
    Pad Template: 'src'

Element Properties:
  name                : The name of the object
                        flags: readable, writable
                        String. Default: "markerdetect0"
  parent              : The parent of the object
                        flags: readable, writable
                        Object of type "GstObject"
  qos                 : Handle Quality-of-Service events
                        flags: readable, writable
                        Boolean. Default: true

第 4 步 - 使用实时视频源执行示例

为了便于启动示例,请在您的嵌入式平台上创建以下启动脚本 (launch_usb_markerdetect.sh):

#!/bin/sh

gst-launch-1.0  \
  v4l2src device=/dev/video0 io-mode=4 ! \
  video/x-raw, width=640, height=480, format=YUY2, framerate=30/1 ! \
  videoconvert ! \
  video/x-raw, format=BGR ! \
  queue ! markerdetect ! queue ! \
  videoconvert ! \
  fpsdisplaysink sync=false text-overlay=false fullscreen-overlay=true \
  \
  -v

在启动示例之前,我们要定义我们的 DISPLAY 环境变量,并配置我们的 DisplayPort 显示器的分辨率。

$ export DISPLAY=:0.0
$ xrandr --output DP-1 --mode 800x600

该示例可以使用我们刚刚创建的脚本启动:

$ ./launch_usb_markerdetect.sh

我第一次使用 USB 摄像头执行这个“markerdetect”gstreamer 插件让我感到惊讶。我使用的 USB 相机是具有自动白平衡功能的罗技 C720,所以我希望看到蓝色、绿色和红色的平均值大致相同。

标记检测 - 面向监视器
 

事实证明,DisplayPort 监视器正在生成一种蓝色色调,该色调被图表拾取,并略微扭曲了结果。

pYYBAGNy7RGAO-pPAABttB3bYdM738.png
颜色平均值 - 面向显示器
 

我跑了同样的测试,这次远离显示器,结果更符合我的预期。

标记检测 - 远离显示器
 

这一次,蓝色、绿色和红色的平均值大致相同。

pYYBAGNy7ROAN2XWAABeX-vWKnA614.png
颜色平均值 - 远离显示器
 

结论

我希望本教程能激发您在 Ultra96-V2 上尝试增强现实 (AR)。

你能想到其他应用程序会使用这些类型的标记吗?

如果您还想看到任何其他相关内容,请在下面的评论中分享您的想法。

 


声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

评论(0)
发评论

下载排行榜

全部0条评论

快来发表一下你的评论吧 !