SFM一般使用SIFT特征。
每张图片都进行SIFT特征检测后,我们需要将不同图片中的特征进行匹配。首先计算两张图像(如图像I和图像J)中特征之间的距离:
如果图像I中的某个特征Fi1与图像J中的特征Fj1之间的距离最小,为d1。此外,Fi1与图像J中的特征Fj2的距离d2仅次于d1。若d1/d2小于0.6,那么我们认为Fi1与Fj1是匹配点。类似的,对图像I中的每个特征都选择一个匹配点。但是我们发现,对于图像I中的每个特征,都有图像J中的唯一特征与之对应。但是对于图像J中的特征,可能存在图像I中的多个特征与之对应。因此,对这种情况需要做一次反向的筛选。最终如果一一匹配的特征数量不小于16个,那图像I和图像J可以作为初选的图像对。
匹配的所有特征对的像素坐标均需要满足对极约束,即:
F矩阵称为基础矩阵(fundamental matrix),反映空间一点P的像素点与该点在不同视角摄像机下图像坐标系中的关系。它可以把两张图片之间的像素坐标联系起来,并包含相机的内参信息。 上述公式可以写为:
进一步,此方程如果要得到唯一解,则必须有8个匹配的特征对。
当匹配对大于8时可以使用最小二乘法计算。
但是,当匹配对中存在噪声时,F矩阵的计算会受到很大的干扰。对此前人提出过诸如加权最小二乘等方法,但是最通用的还是使用RANSAC进行数据滤除。 RANSAC在当前所有匹配对中拟合一个模型,把不符合模型的点进行滤除,以此来保证计算F矩阵的准确性。
其求解过程为:
选取内点最多的对应F,这时候可以找到比较好的匹配点对了。
我们求得基础矩阵F之后可以进一步得到本征矩阵E,F和E之间的关系为:
K为相机内参。 得到的本征矩阵E能够反映空间一点P的像点在不同视角摄像机下相机坐标系中的关系。(此处为三维点)
E能够分解为R和t,即相机外参中的旋转和平移。
获取初始的相机内外参、三维点后。利用多视角的重投影误差可以构建bundle adjustment(BA)问题,进一步优化相机参数和三维点。初始化的两帧图片可以进行第一次BA,然后不断添加新的相机和3D点进行BA。最终得到所有相机参数和估计的点云。
bundle adjustment实际上是一个非线性最小二乘问题,因此初始值的准确性很重要。所以为了获取准确的初始值,需要尽量多的匹配点,同时相机中心与物体之间有足够的距离。
[1] SFM算法流程
[2] 本质矩阵和基础矩阵的区别是什么?
[3] SFM算法原理初简介
[4] 计算机视觉基本原理——RANSAC
SIFT作为一种图像局部特征描述被广泛用于各类图像处理任务。在三维人体重建中,SIFT特征常被用于寻找各视角之间的匹配点,进而实现多视角的三维重建。
1、具有较好的稳定性和不变性,能够适应旋转、尺度缩放、亮度的变化,能在一定程度上不受视角变化、仿射变换、噪声的干扰。
2、区分性好,能够在海量特征数据库中进行快速准确的区分信息进行匹配。
3、多量性,就算只有单个物体,也能产生大量特征向量。
4、高速性,能够快速的进行特征向量匹配。
5、可扩展性,能够与其它形式的特征向量进行联合。
在不同的尺度空间上查找关键点,并计算出关键点的方向。
1、提取关键点:此步骤是搜索所有尺度空间上的图像位置。通过高斯微分函数来识别潜在的具有尺度和旋转不变的兴趣点。
2、定位关键点并确定特征方向:在每个候选的位置上,通过一个拟合精细的模型来确定位置和尺度。关键点的选择依据于它们的稳定程度。然后基于图像局部的梯度方向,分配给每个关键点位置一个或多个方向。所有后面的对图像数据的操作都相对于关键点的方向、尺度和位置进行变换,从而提供对于这些变换的不变性。
3、通过各关键点的特征向量,进行两两比较找出相互匹配的若干对特征点,建立景物间的对应关系。
SIFT具有尺度不变性,生活中也有从不同尺度来观察图像。如观察一棵树,从小尺度观察可以观察树叶、树皮的特征,从大的尺度,可以观察树的整体形状等。
基本思想:在图像信息处理模型中引入一个被视为尺度的参数,通过连续变化尺度参数获得多尺度下的尺度空间表示序列,对这些序列进行尺度空间主轮廓的提取,并以该主轮廓作为一种特征向量,实现边缘、角点检测和不同分辨率上的特征提取等。尺度空间方法将传统的单尺度图像信息处理技术纳入尺度不断变化的动态分析框架中,更容易获取图像的本质特征。尺度空间中各尺度图像的模糊程度逐渐变大,能够模拟人在距离目标由近到远时目标在视网膜上的形成过程。
SIFT利用高斯模糊来模拟尺度的增大(图像越模糊,包含的细节信息越少,整体信息越多),从而能够在不同尺度来寻找特征。并且高斯核函数是唯一的尺度不变核函数。
σ为尺度因子,越大图像越模糊,模拟尺度越大。*为卷积操作。
由不同大小高斯核卷积后的图像可以组成一个高斯金字塔。
差分高斯函数DOG(Difference of Gaussian)用于提取图像像素的变化(如果没有变化也就没有特征)。
做完差分后在图像中寻找极值点。
当然这样产生的极值点并不全都是稳定的特征点,因为某些极值点响应较弱,而且DOG算子会产生较强的边缘响应。所以通过拟合曲线来精确确定关键点的位置和尺度,同时去除低对比度的关键点和不稳定的边缘响应点(因为DoG算子会产生较强的边缘响应),以增强匹配稳定性、提高抗噪声能力。
为了使描述符具有旋转不变性,需要利用图像的局部特征为给每一个关键点分配一个基准方向。使用图像梯度的方法求取局部结构的稳定方向。对于在DOG金字塔中检测出的关键点点,采集其所在高斯金字塔图像3σ邻域窗口内像素的梯度和方向分布特征。梯度的模值和方向如下:
本算法采用梯度直方图统计法,统计以关键点为原点,一定区域内的图像像素点确定关键点方向。在完成关键点的梯度计算后,使用直方图统计邻域内像素的梯度和方向。梯度直方图将0~360度的方向范围分为36个柱,其中每柱10度。如下图所示,直方图的峰值方向代表了关键点的主方向,方向直方图的峰值则代表了该特征点处邻域梯度的方向,以直方图中最大值作为该关键点的主方向。为了增强匹配的鲁棒性,只保留峰值大于主方向峰值80%的方向作为该关键点的辅方向。
Lowe建议描述子使用在关键点尺度空间内44的窗口中计算的8个方向的梯度信息,共448=128维向量表征。
将关键点附近的邻域划分为dd(Lowe建议d=4)个子区域,每个子区域做为一个种子点,每个种子点有8个方向。
将坐标轴旋转为关键点的方向,以确保旋转不变性
以这个向量来表示一个SIFT特征。
以当前图像和目标图像中各个特征向量的欧氏距离。最接近的认为是匹配点。
SIFT在图像的不变特征提取方面拥有无与伦比的优势,但并不完美,仍然存在:
基于静态模板的方法是2010年左右人体重建领域最具代表性的方法。在这篇文章里,我们使用Blender2.79对人体网格模型进行骨架绑定,并使用Linear Blending Skinning(LBS)实现骨架驱动网格模型变形。
首先,我们要按照人体网格模型构建一个合适的骨架。我们以SMPL模型作为例子,将初始的SMPL obj模型导入Blender工作台。(这里也可以使用PhotoScan等三维扫描软件生成自己想要的个性化模型)
可以看到初始导入的SMPL模型原点是位于胸口正中间,为了让绑定的模型更加统一,我们要将模型原点放到根节点位置。所以,我们首先将模型沿Z轴移动一段距离,让盆骨(pelvis)大致落在坐标轴的原点。然后我们将Blender的游标也放置在坐标轴原点。在物体模式下选择我们的obj模型,ctrl+shift+alt+c选择将原点置于游标处。这样obj模型就以盆骨为原点了。
接下来开始建立骨骼。在坐标原点处按shift+A,添加一段骨骼。为了更好地进行可视化,我们在右侧的工具栏中打开透视模式。然后选择骨骼后按TAB键可以对骨骼的大小和方向进行操作。另外,按N键可以显示当前选择的骨骼的各项参数。
完成第一根骨骼的大小调整后,选择骨骼的尾端,按E可以生成子级骨骼,然后将子级骨骼调整到脖子的位置。
我们用同样的方法生成头部骨骼后,需要对手臂和腿进行骨骼生成。由于手臂是左右对称的,所以我们选择骨骼按T,在选项中打开X轴镜像。生成子级骨骼时使用shift+E。可以看到同时生成的是对称的骨骼。
同样的方式生成腿部骨骼后,骨骼的构建就完成了。
完成骨骼构建后,我们首先选择OBJ模型,然后再选中的状态下按shift并点击选择骨架
然后按Ctrl+P,选择骨架形变-自带权重绑定。点击之后Blender会自动计算模型顶点与骨骼的权重。此时,我们已经利用Blender实现了骨骼与人体模型的绑定。选中骨骼后选择姿态模式,拉动骨骼,我们可以看到模型也随之发生了形变。形变之后,我们可以选择骨骼按A键选中所有骨骼,Alt+R可以将变形后的骨骼恢复到初始状态。
由于SMPL是一个裸体模型,因此没有类似裙子之类的非刚性部分。但如果有的话,用之前的自动权重计算可能会导致裙子之类的部分权重不正确,导致不正常的形变。因此,还需要手动对权重进行调整。我们首先选中物体,然后选择权重绘制模式。此时,我们选择每一根骨骼的时候,对应的部分就出出现由蓝到红的颜色。越红表示权重越大,其顶点位置的变化也受该骨骼的影响越大。
按T打开工具中的笔刷,调整权重。其中1代表红色,0代表蓝色。然后按F调整笔刷大小,然后使用笔刷对想要调整的部分进行调整。
在绑定骨架后,我们已经完成了使用Blender来自动计算蒙皮权重,接下来可以使用自己编写的脚本来按照自己的方式导出各项参数,用于其他任务。 在Blender中,所有的对象都在bpy模块中,我们可以调用这个模块来导出我们想要的参数。
import bpy
a = bpy.data.objects[skeleton] # skeleton为定义的骨骼名称
ob_main = bpy.data.objects[mesh] # mesh为定义的网格模型名称
导出参数后,我们可以用代码来控制模型,用于优化匹配等任务,此处是用LBS来变形模型的结果。
[1] 项目代码:https://github.com/boycehbz/HumanRegistration
[2] 顺子老师Blender教程:https://www.bilibili.com/video/BV14W411N75Z?p=32
本文参考CVPR2019-XNect: Real-time Multi-person 3D Human Pose Estimation with a Single RGB Camera,对其所述的利用三维地平面计算人体高度,进而计算人体在场景中绝对位置的方法进行分析和复现。
从二维图像中恢复对象的绝对高度是计算机视觉、数字测量领域的一个重要问题。对于相机参数已知的二维图像,对象的绝对高度和其所在的绝对位置可以相互转换,因此具有重要的应用价值。在此首先总结一下从二维图像估计人体身高的几种方法。
从三维几何获取人体高度
1)通过三维空间中的地平面、相机参数、人体与地面接触点计算,即本文之后介绍的方法。
2)根据场景中已知高度物体作为参考。
该方法对已知条件具有苛刻的要求。
从相机几何获取人体高度
使用shape-from-defocus的方法。该方法需要多张图像进行焦扫描或使用专业相机。
从图像特征获取人体高度
主要为使用machine learning或者deap learning来对图像直接估计。
从图像测量获取人体高度
根据医学的相关研究,人体各部分肢体的比例和身高有一定相关性,可由此推算。(涨知识了)
由于XNect没有提供该部分的源码和测试数据,因此需要自己来获得三维地平面。地平面即三维空间中虚拟人物所在的地面。
准确的三维地平面很难通过单个视角来获取,因此本文使用了9个视角进行点采样,利用相机参数反算出三维点,然后用平面进行拟合。
图中红点即为两个视角中的采样点,通过其二维坐标和相机参数能够对三维点进行解算。
利用求得的三维点进行平面拟合,获得平面经过点采样后使用meshlab可视化,可见这样的采样还是能够基本满足实验要求的。
在获得地平面后,我们需要图像中目标人体与地平面接触的二维点和头顶的二维点坐标,这里我们以人体脚踝关节点和头顶关键点近似替代,2D关节点通过openpose等2D pose检测工具获取。
我们以图示的脚踝关节点作为地面接触点,在相机坐标下由相机光心向图像关节点打一条射线,获取射线与地平面的交点。
再以交点作垂直地平面,正对相机的平面,即平面的法向方向为射线方向与地平面法向作两次叉乘。
由于脚踝并不是准确的地面接触点,因此所作平面会在人体的后方,此处存在一定误差。此时,我们用同样的方法向人体头顶作射线,射线会与刚刚所作平面形成一个交点,改点到地平面的距离即为三维人体高度。
至此,利用高中数学的知识,我们计算得到了三维人体的高度。通过相机参数,我们能够换算到现实的米制空间,并能够轻松得到人体的绝对位置。
[1] XNect: Real-time Multi-person 3D Human Pose Estimation with a Single RGB Camera, In CVPR, 2019.
[2] What Face and Body Shapes Can Tell About Height, In ICCV Workshop, 2019.
二维人体姿态估计的工作已经有很多,如openpose、alphapose等工作已经能够在绝大多数场景下实现较好的效果。在具有颠覆性的解决方案之前,除却遮挡等特定条件,目前二维的准确率已经很难再提升。在近年的各大视觉顶会中,人们也纷纷将目光投向三维,然而单视角情况下深度方向的视觉歧义性始终是一个难以解决的难点。在单人三维姿态估计中,由于只需要估计相对根节点的深度,深度神经网络通过一些先验的推理能够大致估计出相对深度。当把场景拓展到全局,在统一的相机坐标或者世界坐标下,三维姿态的准确率骤然下降。由于无法在相机坐标系下获得准确深度,这也导致了当前许多多人姿态估计的任务都披着多人的外衣,做着单人的事情。但多人的姿态估计由于更加接近现实场景,因此仍然是一个值得研究的问题。在这篇文章中整理了当前单视角多人绝对位置姿态估计任务的相关工作。
文章:Camera Distance-aware Top-down Approach for 3D Multi-person Pose Estimation from a Single RGB Image
人体绝对深度获取方式:粗略计算+CNN校正参数
准确度相对较低
文章:XNect: Real-time Multi-person 3D Human Pose Estimation with a Single RGB Camera
人体绝对深度获取方式:相机参数+地平面高度计算
要求实验前标定相机参数与地平面,存在条件限制,实现参考地平面高度计算
文章:Deep Network for the Integrated 3D Sensing of Multiple People in Natural Images
人体绝对深度获取方式:因为SMPL跟米制空间(mm)变换的scale为1000,优化二维关节点与3D关节的欧氏距离
第一个多人单目带mesh的工作,使用网络回归相对pose
文章:Monocular 3D Pose and Shape Estimation of Multiple People in Natural Scenes
人体绝对深度获取方式:因为SMPL跟米制空间(mm)变换的scale为1000,优化二维关节点与3D关节的欧氏距离
与上文同一个作者,使用优化的方式
文章:Absolute Human Pose Estimation with Depth Prediction Network
人体绝对深度获取方式:结合深度图的网络估计
绝对深度准确度跟(一)相近
文章:Multi-Person Absolute 3D Human Pose Estimation with Weak Depth Supervision
人体绝对深度获取方式:结合深度图的网络估计
上文同一作者,只是加了一步后面的联合深度网络,给人水文章的感觉
本文记录了libtorch的配置过程,方便之后再次安装。
在官网选择合适的版本下载。这里选择了使用CUDA10.1的release版。
https://download.pytorch.org/libtorch/cu101/libtorch-win-shared-with-deps-1.6.0%2Bcu101.zip
libtorch的下载文件中没自带cmakelists。因此我们需要在解压后文件夹中新建一个CMakeLists.txt
并添加一下内容。
# 设置 cmake 版本限制
cmake_minimum_required(VERSION 3.0 FATAL_ERROR)
cmake_policy(SET CMP0054 OLD) #避免错误警告
# 项目名称
project(libtorch-app)
# 设置 libtorch-win-shared-with-deps-latest 目录,主要让 find_package 可以找到 Torch 的 TorchConfig.cmake 配置文件以及其他相关 Config.cmake 配置文件
set(CMAKE_PREFIX_PATH "./libtorch-win-shared-with-deps-1.6.0+cu101")
set(Torch_DIR "./libtorch-win-shared-with-deps-1.6.0+cu101/libtorch/share/cmake/Torch")
find_package(Torch REQUIRED)
add_executable(libtorch-app main.cpp)
target_link_libraries(libtorch-app "${TORCH_LIBRARIES}")
set_property(TARGET libtorch-app PROPERTY CXX_STANDARD 11)
在CMakeLists.txt
的同级目录中创建main.cpp
#include <torch/torch.h>
#include <iostream>
int main()
{
torch::Tensor tensor = torch::rand({ 9,9 });
std::cout << tensor << std::endl;
return 0;
}
使用cmake将source文件夹选择为CMakeLists.txt
所在的文件夹。另自行选取输出的build
文件夹。
打开build
文件夹中的libtorch-app.sln
进行编译。
我习惯于将有关文件单独放在第三方库中。所以我将解压文件夹中的bin/include/lib
三个文件夹单独拿出来。
我们将bin
文件夹的路径添加进环境变量。然后我们新建一个空项目,选择release,x64。将include
和lib
文件夹的路径分别加入到包含目录和库目录。
然后新建一个cpp添加:
#include <torch/torch.h>
#include <iostream>
#ifdef _DEBUG
#pragma comment(lib, "c10.lib")
#pragma comment(lib, "torch_cpu.lib")
#pragma comment(lib, "torch_cuda.lib")
#else
#pragma comment(lib, "c10.lib")
#pragma comment(lib, "torch_cpu.lib")
#pragma comment(lib, "torch_cuda.lib")
#endif // _DEBUG
int main()
{
torch::Tensor tensor = torch::rand({ 9,9 });
std::cout << tensor << std::endl;
return 0;
}
正常输出
[1] 學海無涯:https://www.cnblogs.com/cheungxiongwei/p/10689483.html