Linemod 代码笔记

最近了解到 Linemod 这个模板匹配算法,印象不错
准备仔细学习一下,先做点代码笔记,免得后面不好回顾
目前的笔记基本上把 核心流程都分析得比较清楚了,除了一些阈值的选取

opencv 的contrib 模块有这个算法的实现

我看的代码来自这里
https://github.com/meiqua/shape_based_matching

先大概记录下 代码思路:
分两个阶段, train 和 test

Train

Train 中 , shapeInfo_producer 负责用来对 模板进行 各种旋转和尺度缩放,
shapes.src_of 可以根据旋转和尺度 生成变换后的 模板

对每一个模板 执行 detector.addTemplate 操作,

最后调用 shapes.save_infos 和 detector.writeClasses 这两个保存训练 结果。保存的信息用于 后续的匹配中。

首先构造
line2Dup::Detector detector(20, { 4, 8 });
第一个参数为 特征点个数 , 第二个参数是一个 vector, 每个元素代表每一层的T
构建 this->modality 对象

shape_based_matching::shapeInfo_producer shapes(padded_img, padded_mask);
两个入参都是 图像,第一个是用 输入图像构建,填充像素为0, 第二个用输入图像大小的大小构建掩码图像,掩码为1, 填充像素为0

然后填充shapes.scale_range、 shapes.scale_step、 shapes.angle_range 、shapes.angle_step
这四个是对模板图像进行 尺度缩放 和 旋转的 量

shapes.produce_infos();
主要是用 尺度范围 和 旋转范围 的组合 构建 std::vector infos
然后 就是 遍历 shapes.infos
执行
detector.addTemplate(shapes.src_of(info), class_id, shapes.mask_of(info));

shapes.src_of(info) 产生变换后的图像
class_id 是一个固定的字符串
shapes.mask_of(info) 返回 shapes.src_of(info) 产生变换后的图像是否大于0的 掩码图像
addTemplate 是 核心函数,主要作用为 提取模板图像的特征点,即梯度较强的点,得到 这些点的坐标和梯度方向值。

接着调用两个函数

  1. shapes.save_infos 保存 的信息是 每张图片是 原始图像经过哪种旋转和缩放得到的
  2. detector.writeClasses 则 保存 每个模板 的信息,包括cropTemplates(tp) 后的高宽和坐标、 特征点坐标信息,特征点的label 就是梯度方向

=============================================================================

Detector::addTemplate

构建模板的流程图
1 modality->process(source, object_mask)

这个是 直接构造一个 ColorGradientPyramid 对象,返回其指针
ColorGradientPyramid 构造函数中 update(); ,内部是
quantizedOrientations(src, magnitude, angle, weak_threshold);
先做 高斯模糊, 然后 在水平和垂直方向 调用 Sobel,
调用 phase 计算梯度方向,

调用 hysteresisGradient, 主要输出就是 quantized_angle
过程为: 先把 连续的梯度方向 划分为16个区间, 然后量化为8个方向
quant_r[c] &= 7; 这个代码还没看明白,这 相当于把一个整数 对8 求模
这么做没问题应该是因为 认为 180度和190度之间的方向 和0度到10度之间的 方向是一个方向。

然后就是 对梯度幅值 超过一定阈值的 像素点 的 3*3 邻域 求 梯度直方图
投票数 超过 阈值的 方向 作为最终的 量化方向

至此, modality->process 完成
返回 一个 Ptr qp

然后 开始遍历金字塔每一层, 如果不是最底层, 那么 qp 降采样,并且 做梯度量化操作, 即调用上面的 update()

然后qp->extractTemplate(tp[l])

这一步是 提取第 L 层特征点, 保存在 tp[l]中。 细节参考后文
说明: tp是个vector, 每个 元素都是一个模板,对应金字塔某一层提取出来的特征点

每一层都遍历完后, cropTemplates(tp)

这个函数 先 遍历每一个 模板, 找出特征点最大最小坐标,注意,高层次的金字塔图像的坐标会进行放大(根据层次)
得到 4个最小、最大坐标。 注意: 是所有层共用信息

然后再一次遍历每个模板, 调整 templ.width ,templ.height ,templ.tl_x,templ.tl_y
然后用 templ.tl_x,templ.tl_y 修正了特征点坐标,
TODO: 这就 有点麻烦了, 修正后的 坐标肯定和 原始图像 对应不上了啊!

返回 Rect(min_x, min_y, max_x - min_x, max_y - min_y)
但 外部并未接收 这个返回值

addTemplate 的最后 template_pyramids.push_back(tp);
ColorGradientPyramid::extractTemplate(Template &templ)
函数输出应该是 templ.features, 即提取出 特征点
先对 mask 进行 腐蚀,

Magnitude 是 之前 quantizedOrientations 中计算出的梯度幅值(梯度平方和)

对 Magnitude 搞一个 遍历,
如果对每个像素,如果 magnitude_valid 值 大于0
如果其邻域内 有像素的梯度幅值超过它,
那么 is_max 为 false, 如果遍历完后 , is_max 为true, 那么 所有 邻域像素对应 magnitude_valid 值 置为0

通过上述检验的点 , 如果 幅值超过阈值, 且 方向不为 0, 进入 candidates
(注意 opencv在这里的实现方法, 先设置了一个 score = 0, 如果没通过上述检验, 该值依然为0, 这种实现方法好吗?)

遍历完后,如果 candidates 个数低于阈值, 返回 false, 此次 抽取失败。。。

对 candidates 按照 score 进行一次稳定排序
selectScatteredFeatures 最后 从 candidates 中 选取一些 散得 比较开的点, 这里while 循环写得还比较有技巧, 如果遍历完一轮, 数量不够,那么 降低 距离阈值, 再选!
和 orb-slam或者说opencv 里面 ORBextractor 提取特征点 那个 四叉树的方法谁优谁劣?

选取的特征点保存 在 templ.features 中

Test

先读取 train 阶段保存的两个信息文件
detector.readClasses(ids, prefix + “myCase/%s_templ.yaml”);
读取 每个模板 的信息,包括cropTemplates(tp) 后的高宽和坐标、 特征点坐标信息,特征点的label 就是梯度方向。
构建出: class_templates

shape_based_matching::shapeInfo_producer::load_infos
每张图片是 原始图像经过哪种旋转和缩放得到的

对测试图像 进行一下调整, 使得高宽都是 16 的倍数

auto matches = detector.match(img, 90, ids);
90 是阈值, ids 是 训练时 指定的id字符串 test

然后 modality->process(source, mask),
这个调用在前面已经介绍过了,会 构造一个 ColorGradientPyramid 对象,对source图像计算量化后的梯度信息

然后遍历 金字塔, construct response map
先不看 具体的函数调用实现过层, 从函数名字 和 注释来看, 这就是 论文当中第三节讲的东西, 包括 方向扩散spread、 梯度响应计算computeResponseMaps、 线性化存储linearize。 最终存在在 LinearMemoryPyramid 结构里面。

遍历class_ids, 从 class_templates获取 对应 std::vector
matchClass(lm_pyramid, sizes, threshold, matches, it->first, it->second);
这个函数完成整个匹配过程

=============================================================================

Detector::matchClass

遍历template_pyramids, 提取出 每个 Template,
调用 similarity, 计算相似性, similarity中, 核心调用是 accessLinearMemory,
这里面第一行代码
const Mat &memory_grid = linear_memories[f.label];
很关键,这是根据模板中特征点 来 定位 response map 相应的数据
定位到以后,然后 就是 SIMD 指令 来 累加数据了!

static void spread(const Mat &src, Mat &dst, int T)

这个地方实现的是 论文3.3 节的所谓 梯度方向展开
所要实现的功能很好理解, 即把每个像素及其邻域的离散化的梯度方向进行 或运算。
OpenCV 这里再一次展现了实现技巧, 最直观的方法是 每次遍历一个像素时,取出其所有邻域内的像素的梯度方向值,然后做一个或运算, 这样做 内存访问性能较低, 因为图像的下一行和上一行 距离较大, 很可能缓存命中失败。

OpenCV 的做法是: 每次遍历时, 只做整个邻域内某个特定位置的像素梯度方向值 的 或运算,这个地方说的邻域包含像素自身,即邻域中心。 所以总共循环 T*T次。 T 为邻域直径。
这样做, 内存访问友好,并且方便使用 SSE指令进行优化, 因为连续参与运算的数据在内存中是连续的!
梯度方向在邻域中的传播

static void computeResponseMaps

(const Mat &src, std::vector &response_maps)

实现论文3.4节 响应图的计算
这个地方 把论文中的相似度 也给离散化了。
并且事先计算了 某个方向 和 某组方向的余弦值的最大值,并且离散化, (或者称为根据余弦值 实行打分制) 存储到一个数组SIMILARITY_LUT 中,即查找表。 这个查找表中针对某个方向的值有32个元素, 总共8个方向, 所以有 256个元素。 32个元素中 , 又分为两组, 前16个是8个方向中前4个方向的各种组合 与 当前32个元素针对的方向 的余弦值的最大值对应的得分。

这个数组, 上交这个学生 对原来的值 进行了修改: 1,2–>0 3–>1
为什么这么改?
https://zhuanlan.zhihu.com/p/35683990
这篇文章给出了 修改的解释

论文3.4 节 也给出了 这个查找表的计算啊!

疑问待定: n0 为8的时候, 针对某个方向的查找表元素 按照论文实际上应该是有 2的8次方, 即 256种情况。 这个地方是不想搞出那么大一个数组, 所以, 把8位分拆成两组, 每组只需16个元素, 然后再进行一次比较,拿到最终的最大值? 为啥不直接构建大小为 256*8的查找表? 这样可以省掉一次 max的运算。
看了下 _mm_shuffle_epi8 的介绍
SSE指令
这个地方 index 只用低4位进行运算, 也就是只支持 4个bit作为索引值,
如果只能用这个指令,的确 只能把 8位拆分成两组4位,再max
不知道有没有 能直接用8位作为 所以索引的SSE指令
查找表,即预先计算好梯度方向之间的差异

static void linearize

(const Mat &response_map, Mat &linearized, int T)

这个是改变存储方式,先行后列, 间隔T 读取,然后写入。没有比较复杂和特殊的处理。
改变存储方式

similarity_64

这个函数计算 模板和 输入图像的 相似性, 即论文中的 similarity map
计算相似性的时候, 并不是 把 模板上的每个像素都和 输入图像上对应的像素 一一对应,然后进行 某种计算, 这和 NCC, SSD 这些方法的做法不一样!一开始受这些方法先入为主的影响,导致论文里的Fig 7 以及代码中的操作

实际上, 只比较模板上提取的特征点, 以及 模板 覆盖在 输入图像上某个位置时, 这些模板特征点对应到 输入图像上的像素点 之间的梯度差异。

意识到这点以后,就比较好理解代码了。 因为模板需要在输入图像上进行 滑动,所以产生了 similarity map。 每次滑动,模板和输入图像产生一个 相似度。 模板在 水平和垂直方向进行滑动, 所以 产生一个 二维的相似度矩阵。这个矩阵的宽 自然就是 输入图像的宽减去模板的宽, 也就是代码中的span_x。 高的情况类似。

代码当中用 template_positions 表示 模板的当前滑动位置。

计算similarity map最直观的方法是:对每个模板位置, 找出所有特征点在输入图像上对应的像素, 计算所有梯度方向的相似性,累加。 然后 处理下一个模板位置。

但代码中的做法是: 对每个特征点,计算出所有模板位置上 这个特征点 和 所有输入图像上对应点的 梯度方向相似性,保存到similarity map中。 然后 计算下一个特征点的相似性,累加到 similarity map中。

整个算法中 不是第一次使用这种思路了。

  • 15
    点赞
  • 122
    收藏
    觉得还不错? 一键收藏
  • 37
    评论
### 回答1: Python 代码笔记是对 Python 程序代码的解释和说明。它可以帮助你理解代码的工作原理,并在以后更好地维护和编写代码。常用的代码笔记格式有注释、文档字符串等。示例代码: ```python # 计算平方 def square(x): """ 返回x的平方 """ return x*x print(square(4)) ``` 在上面的代码中,`# 计算平方`是注释,`"""返回x的平方"""`是文档字符串。 ### 回答2: Python代码笔记是程序员在学习和实践Python编程语言时记录的一种文档。它包括通过编写实际的Python代码示例来记录各种语法、函数、模块、库和算法的用法和应用。 Python代码笔记通常用于记录和整理编程语言的基本知识,并用代码示例来演示这些知识的具体使用。因为Python语言本身较为简洁易读,因此在代码笔记中使用Python语言编写示例代码非常方便。 通过编写Python代码笔记,程序员可以更好地理解和掌握Python编程语言的特性和用法。而且代码笔记还可以作为程序员的参考资料,帮助他们在遇到问题时快速找到解决方案并进行复用。 除了记录基本知识之外,Python代码笔记还可以用于记录程序员在实际项目中遇到的问题和解决方案。通过记录这些问题和解决方案,程序员可以在未来的项目中预防和避免相同的问题,并且能够提高自己的编程技巧和经验。 总之,Python代码笔记是程序员学习和实践Python编程语言时记录的一种文档。它可以帮助程序员整理知识、提高编程技巧,并成为他们解决问题和提高效率的有力工具。 ### 回答3: Python代码笔记是程序员在学习和使用Python语言时记录的一种方式。它可以包括以下内容: 首先,Python代码笔记通常会记录Python代码的基本语法和用法。这些笔记会列举Python的关键字、变量类型、运算符、控制流语句等基本知识点,以便在需要的时候进行快速查阅和复习。 其次,Python代码笔记还会记录一些常用的Python库和模块的使用方法。Python具有丰富的第三方库和模块,如numpy、pandas、matplotlib等,这些库在数据处理、科学计算、绘图等领域都有广泛的应用。通过记录库和模块的使用方法,可以帮助程序员实现特定的功能或解决具体的问题。 此外,Python代码笔记还会记录一些常见的编程技巧和经验。比如如何提高代码的效率、如何优化算法、如何进行调试等等。这些技巧和经验是程序员在实际开发中积累的宝贵资料,可以帮助他们更好地解决问题和提高工作效率。 最后,Python代码笔记还可以记录一些项目示例和实践经验。当程序员在开发具体的项目时,他们会遇到各种问题和挑战,记录下来的项目示例和实践经验可以为他们以后的开发工作提供参考和借鉴。这些实践经验可以包括项目的架构设计、数据库操作、接口调用等方面的知识。 综上所述,Python代码笔记是程序员学习和使用Python语言的重要辅助工具,它通过记录基本语法、常用库和模块的使用、编程技巧和经验以及项目示例和实践经验等内容,帮助程序员提高开发效率,解决问题,并不断提升自己的编程能力。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 37
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值