自识别标记(self-identifying marker) -(4) 用于相机标定的CALTag源码剖析(下)

本文主要是介绍自识别标记(self-identifying marker) -(4) 用于相机标定的CALTag源码剖析(下),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

接上篇内容,继续对CALTag源码进行详细剖析~

3、 角点检测

为了方便说明,在此将一个自识别标记,也就是上一步骤保留的连通区域,称为一个quad。下面分析一下如何检测quad的四个角点。
首先找到该quad的外接最小矩形bbox, 二值化掩模mask,然后对mask边界加了3个像素的pad,目的是方便后面做形态学闭运算,运算完再去掉pad。然后找出边界轮廓上的点,计算他们的梯度方向,将这些梯度方向聚成4类,从而获得4个主要的边缘方向。然后分别对每一类的边界点进行线性拟合,得到4条拟合的直线。然后计算它们的交点就是角点。
然后有一个很重要的步骤,就是把这些角点按照逆时针进行排序,这对后面恢复角点、求对应关系至关重要。排序的方法是先求出四个角点的平均坐标,就是该quad的重心。然后分别求每个角点和该重心的向量,将这些向量转化为极坐标系,将极坐标系下的角度按照升序排列就是逆时针角点的顺序。极坐标下的角度如下:
这里写图片描述
上述步骤对应的代码是:

[isq,cnr,cnr0] = fitquad( R(i).BoundingBox, R(i).FilledImage, layout );

这样每个quad就会计算出四个伪角点(下图中四个红色十字),这样每个真实的角点周围就会有四个伪角点,那么如何根据这四个伪角点来计算真实角点坐标呢?
这里写图片描述

首先是根据距离聚类,然后取聚类中心的点作为初始角点saddles_0(下图中绿色圆圈),然后使用和opencv中一样的方法来寻找亚像素级精度的鞍点(下图中绿色十字)。也就是下面几句代码

[saddles] = cornerfinder_saddle_point( flipud(saddles_0), I, 11,11 ); 
[saddles,good] = cornerfinder_saddle_point( saddles, I, 9,9 );
saddles = flipud( saddles(:,good) );  

结果见下图
这里写图片描述

4、 Code/ID提取和验证

要提取标记中的code,首先需要从图片中采样出code的二进制码。流程如下图。首先定义一个理想的单位方形(即代码中的unitSquare),对应下图中左侧的黑色方形。右侧图是图片中真实的quad。首先把unitSquare的四个角点映射到quad的四个角点(下图品红色圆圈),由此得到一个单应矩阵H。然后对unitSquare内部均匀采样(下图左内部的米字形表示采样点),利用上面得到的矩阵H进行映射就得到了右边quad中的真实采样点。对应代码为:

unitSquare = [ 0 1 1 0; 0 0 1 1; 1 1 1 1 ];
R(i).H = homography2d( unitSquare, quadSquare );
R(i).HS = homoTrans( R(i).H, S );

其中quadSquare就是下图右quad对应的四个角点。R(i).H为映射矩阵H,S是下图左unitSquare内部均匀采样点,R(i).HS是计算得到的右边quad中的真实采样点。
这右图中采样点按顺序排成一列就是该quad的code值。
这里写图片描述
接下来就是对code的验证了,由于实际拍摄时棋盘旋转方向未知,所以我们不知道哪个点对应标记的左上角正方向,所以需要对提取的code进行旋转4次,每个方向的code都检测一遍,如果最终四个方向里只有一个方向的code能在code矩阵表里查到就表明code有效。当检测到code有效后,需要对code内包含的ID进行验证。这16bit的code里面包含了10bit的数据,首先需要做CRC验证,验证通过才能说明真正识别了这个标记。然后按照上面找到的正确的方向把角点也转到正确的方向。
可能有人会问了,识别了code为啥还要再识别ID,不嫌麻烦啊?
原因是这样的:首先双重保障鲁棒性肯定很好,不会产生false negative(标定结果对false negative很敏感,要保证为0)。另外,CRC校验一方面是验证ID是否有效,另一方面还可以尝试对错误的采样进行修正,这在有遮挡的情况下还是有可能发生的。
上述过程对应的代码如下:

isvalid=@(x)crc_validate(b2d(x),idBits,crcBits)&&ismember(b2d(x),CODE);
validity = [ isvalid(c),  isvalid(rot90(c,-1)),  isvalid(rot90(c,-2)),  isvalid(rot90(c,-3)) ];
R(i).isValid = (nnz(validity) == 1);
shift = find( validity ) - 1;
R(i).code = rot90( R(i).code, -shift );
data = b2d( R(i).code );
R(i).id = rightshift( bitand(data,idmask), crcBits );
R(i).crc = bitand( data, 2^crcBits-1 );
R(i).saddles = circshift( R(i).saddles,[0,-shift-1] );

还有一个小插曲就是对全部检测成功的标记再进行一次过滤。方法就是计算每个标记的方向,如果某个标记的方向和其他标记的方向差别较大,就过滤掉。那么问题来了,如何计算标记的方向呢?这就是上面为什么要把角点转到正确的方向的原因之一。用连接第一、二个角点的矢量方向表示该标记的方向就OK了。
下图中每个quad中绿色的十字表示经过验证有效的code的采样点;每个quad边缘上的红线表示连接第一、二个角点的矢量方向,用来标记该quad的正方向。
这里写图片描述

5、 恢复丢失的角点

由于我们事先知道棋盘中每个标记的ID、位置排列等信息(我们称之为标记信息表),所以在上述检测角点验证ID结束之后,我们查找标记信息表就能发现哪些标记没有检测到,从而尝试去找到这些丢失的/未检测到的标记和他们的角点。
那么在此有个问题,为什么上面的步骤检测不到呢?是什么原因导致这些角点被忽视了?
请看下图的一个例子,图中深红色圆圈内的角点是经过上述步骤(验证CODE,识别ID)检测到的角点。品红色圆圈内的角点就是利用标记信息表恢复出来的角点。从图中就可以很明显的看出为什么品红色角点没被检测到,这是因为他们所在的quad(标记)因为遮挡无法被检测,并且他们周围正确被识别的quad也没有把他们包含进去。
这里写图片描述
下面具体分析一下算法是如何恢复出这些丢失的角点的?
目前对于检测成功的标记,我们知道他们的CODE, ID,在标记信息表中的位置(第几行第几列),比如实验用的自识别标记图案的标记信息表如下:
这里写图片描述
这里写图片描述
那么缺失的标记在标记信息表中的位置wPtMissing就可以知道了。我们列出所有检测到的角点的图像坐标iPt、标记信息表坐标wPt,然后用RANSAC的方法求从wPt映射到iPt的单应矩阵H。那么用该矩阵H乘以wPtMissing就得到了丢失标记的图像坐标iPtMissing。
以上过程主要对应如下代码:

H = ransacfithomography( wPt', iPtUndist', 0.1 );
trialPoints = homoTrans( H, [mrow;mcol;ones(1,length(mrow))] );

其中iPtUndist就是对应所有检测到的角点的图像坐标iPt,只是经过了去畸变。
[mrow;mcol;ones(1,length(mrow))]就是缺失的标记在标记信息表中的位置wPtMissing。
trialPoints就是丢失标记的图像坐标iPtMissing。
这里我们用的是普通的摄像头,拍摄的图片畸变都非常小,略过了对于畸变较大镜头的去畸变过程。如果使用鱼眼广角镜头,畸变的影响会较大,此时就需要先对畸变的角度就行去畸变再做上述变换。
到这里还没完,你以为恢复出来的较大就是真的角点了?图样图森破!如上图中品红色的*也是恢复出的“角点”,但是其实他们是伪角点,不具备角点的性质,因为棋盘被遮挡了,他们的位置如果不被遮挡的话下面可能是真正的角点。所以下面就要对这些恢复出来的角点进行验证,必须经过如下两个验证通过,才能加入真正角点的队伍:

验证1:角点圆周上颜色反转次数。想法非常直观,好理解,就是如果以一个真正的角点为中心,一定的半径R画圆,取圆周上连续的点排成一列,应该是黑、白、黑、白间隔的顺序,反应到二进制就是0101或者1010间隔排列,也就是01翻转刚好4次。具体做起来,需要先对角点所在的窗口做个高斯平滑,避免有些噪点混入影响翻转次数。另外就是如何选择这个半径还是比较难的,见下图,图中点1,2,3,4半径选的比较合适。点5,6选的不合适。但是他们的半径都不一样。半径过小和过大都容易引入干扰:点5,6就选的过大,半径穿过了code;点7半径选的过小,如果二值化处理不好很容易引入噪声;这些角点会通不过角点翻转验证。一幅图中的角点半径都有如此大差异,何况要求算法要在不同环境不同角度下都非常稳定,半径的选取就要谨慎了。一种是固定半径值,找出图中所有角点半径不穿过code所需的最大半径,然后选择其中最小的那个作为固定的半径值。另一种思路是自适应的半径,对不同角点选择不同的半径,这个听起来很棒实现比较难。反转次数验证对应的代码如下:

valid(i) = validate_point( I, iPt(i,1), iPt(i,2), rad );

验证2:beta分布验证。想法也容易理解,就是角点所在的邻域内的像素灰度应该服从一定的分布,这里用beta分布来描述,参数

0<alpha≈beta<1

计算出已经确认角点的beta分布参数,取参数的中值,如果恢复角点的beta分布参数和参数的中值差在一定阈值T范围内,认为符合成为角点的条件,否则认为不是角点。同样的,这种方法也需要阈值T,下图中我们看到点2中黑白面积比例基本相等,但点3中黑色像素比例明显高于白色,而点4中反过来了,白色像素比例明显高于黑色,这一反一正参数就差开了不少。所以这个阈值的选取要靠经验判断。Beta分布验证对应的代码如下:

beta(i,:) = betafit( double(zi(:)) );
beta = abs( beta(:,1) - beta(:,2) );
beta_median = median( beta );
beta_std = std( beta );
outliers = (beta > 0.05) & (abs( beta - beta_median ) > (3 * beta_std));

这里写图片描述

最后的结果如下图。集中解释一下不同颜色标记的含义:
红色圆圈表示通过CODE, ID识别后的标记的角点位置。
绿色*表示通过CODE, ID识别后的标记的采样点位置。
品红色*表示恢复出的伪角点位置,这些角点没有通过角点验证,通过的话会在角点出画圈。
每个标记边缘上的红线表示连接第一、二个角点的矢量方向,用来标记该标记的正方向。
这里写图片描述

参考资料

参考论文:
CALTag: High Precision Fiducial Markers for Camera Calibration
参考网站:
http://www.cs.ubc.ca/labs/imager/tr/2010/Atcheson_VMV2010_CALTag/
https://github.com/brada/caltag

这篇关于自识别标记(self-identifying marker) -(4) 用于相机标定的CALTag源码剖析(下)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/829479

相关文章

Java汇编源码如何查看环境搭建

《Java汇编源码如何查看环境搭建》:本文主要介绍如何在IntelliJIDEA开发环境中搭建字节码和汇编环境,以便更好地进行代码调优和JVM学习,首先,介绍了如何配置IntelliJIDEA以方... 目录一、简介二、在IDEA开发环境中搭建汇编环境2.1 在IDEA中搭建字节码查看环境2.1.1 搭建步

Node.js 中 http 模块的深度剖析与实战应用小结

《Node.js中http模块的深度剖析与实战应用小结》本文详细介绍了Node.js中的http模块,从创建HTTP服务器、处理请求与响应,到获取请求参数,每个环节都通过代码示例进行解析,旨在帮... 目录Node.js 中 http 模块的深度剖析与实战应用一、引言二、创建 HTTP 服务器:基石搭建(一

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

阿里开源语音识别SenseVoiceWindows环境部署

SenseVoice介绍 SenseVoice 专注于高精度多语言语音识别、情感辨识和音频事件检测多语言识别: 采用超过 40 万小时数据训练,支持超过 50 种语言,识别效果上优于 Whisper 模型。富文本识别:具备优秀的情感识别,能够在测试数据上达到和超过目前最佳情感识别模型的效果。支持声音事件检测能力,支持音乐、掌声、笑声、哭声、咳嗽、喷嚏等多种常见人机交互事件进行检测。高效推

Java ArrayList扩容机制 (源码解读)

结论:初始长度为10,若所需长度小于1.5倍原长度,则按照1.5倍扩容。若不够用则按照所需长度扩容。 一. 明确类内部重要变量含义         1:数组默认长度         2:这是一个共享的空数组实例,用于明确创建长度为0时的ArrayList ,比如通过 new ArrayList<>(0),ArrayList 内部的数组 elementData 会指向这个 EMPTY_EL

如何在Visual Studio中调试.NET源码

今天偶然在看别人代码时,发现在他的代码里使用了Any判断List<T>是否为空。 我一般的做法是先判断是否为null,再判断Count。 看了一下Count的源码如下: 1 [__DynamicallyInvokable]2 public int Count3 {4 [__DynamicallyInvokable]5 get

工厂ERP管理系统实现源码(JAVA)

工厂进销存管理系统是一个集采购管理、仓库管理、生产管理和销售管理于一体的综合解决方案。该系统旨在帮助企业优化流程、提高效率、降低成本,并实时掌握各环节的运营状况。 在采购管理方面,系统能够处理采购订单、供应商管理和采购入库等流程,确保采购过程的透明和高效。仓库管理方面,实现库存的精准管理,包括入库、出库、盘点等操作,确保库存数据的准确性和实时性。 生产管理模块则涵盖了生产计划制定、物料需求计划、

基于UE5和ROS2的激光雷达+深度RGBD相机小车的仿真指南(五):Blender锥桶建模

前言 本系列教程旨在使用UE5配置一个具备激光雷达+深度摄像机的仿真小车,并使用通过跨平台的方式进行ROS2和UE5仿真的通讯,达到小车自主导航的目的。本教程默认有ROS2导航及其gazebo仿真相关方面基础,Nav2相关的学习教程可以参考本人的其他博客Nav2代价地图实现和原理–Nav2源码解读之CostMap2D(上)-CSDN博客往期教程: 第一期:基于UE5和ROS2的激光雷达+深度RG

Spring 源码解读:自定义实现Bean定义的注册与解析

引言 在Spring框架中,Bean的注册与解析是整个依赖注入流程的核心步骤。通过Bean定义,Spring容器知道如何创建、配置和管理每个Bean实例。本篇文章将通过实现一个简化版的Bean定义注册与解析机制,帮助你理解Spring框架背后的设计逻辑。我们还将对比Spring中的BeanDefinition和BeanDefinitionRegistry,以全面掌握Bean注册和解析的核心原理。

脏页的标记方式详解

脏页的标记方式 一、引言 在数据库系统中,脏页是指那些被修改过但还未写入磁盘的数据页。为了有效地管理这些脏页并确保数据的一致性,数据库需要对脏页进行标记。了解脏页的标记方式对于理解数据库的内部工作机制和优化性能至关重要。 二、脏页产生的过程 当数据库中的数据被修改时,这些修改首先会在内存中的缓冲池(Buffer Pool)中进行。例如,执行一条 UPDATE 语句修改了某一行数据,对应的缓