近来在做三维姿态恐怕的东西,用到了上的深度摄像头(),我们都晓得深度摄像头可以获得某个点的三维信息(基于此可以做好多有趣的东西,例如三维重建,三维关键点跟踪与检查,后续有空渐渐填坑),但具体怎么获得网上能找到的资料也不多,我在这儿整理了一下我近来收集到的资料并提供了基于Swift的示例代码,该文章分成下边几小节来探讨。
虽然最后估算获得三维座标系的方式很简单凸透镜成像原理应用,但要了解为何要这样估算须要清楚单反成像的原理。
单反大致工作原理
X采用的是结构光的方案,这儿以的工作流程来解释下它是怎样获取深度值的:
点阵投影器和泛光照明器都可以投射红外线光点,不同之处在于后者帧率高前者帧率低,且后者投射结构光,前者投射非结构光。
这儿并不对原理展开讲,感盛行的可以移步阅读尾部的参考链接
凸透镜成像中的焦距和光心
不同角度出发的光线经过透镜,跟透镜表面产生不同的倾角,形成不同程度的折射。
从一个真实世界w的一点出发的光,经过透镜,又重新汇集到一点,最终产生了点对点的成像关系,从上图我们可以得出以下几个名词的定义。
光心:凸透镜的中心
焦点:一束光以凸透镜的主轴穿过凸透镜时,在凸透镜的另外两侧会被凸透镜凝聚成一点,这一点称作焦点。
焦距:焦点到凸透镜光心的距离就称作这个凸透镜的焦距,一个凸透镜的一侧各自有一个焦点。
清楚这几个基本概念后理解下边几个座标系就愈加容易了。
单反成像中所用到的世界,单反凸透镜成像原理应用,图象,象素座标系
我们换一张成像的原理图
成像过程中须要经过几个座标系的转换,最后显示在我们的屏幕上。
通常来说,须要经过四个座标系的转换。
世界座标系
描述现实世界中物体所处的三维座标。
单反座标系
以单反的光心为座标原点,x轴和y轴分别平行于图象座标系的x轴和y轴,单反的光轴为z轴。
图象座标系
以图象平面(通常指传感)的中心为座标原点,x轴和y轴分别平行于图象平面的两条垂直边,用(x,y)表示其座标值,图象座标系是用化学单位(比如毫米)表示象素在图象中的位置。
象素座标系
以图象平面左上角的顶点为原点,x轴和y轴分别平行于图象坐标的x轴和y轴,用(u,v)表示其座标值。这个座标系也就是最终在我们手机上显示的座标系。
所以,假若我们假如我们想获得象素点对应的三维坐标的话,就要按照象素座标系反推回单反座标系中。而怎样反推就涉及到几个座标系之间的转换方式。
已知一个现实世界中的物体点在世界座标系中的座标为(X,Y,Z),单反座标系为(Xc,Yc,Zc),图象座标系中的座标为(x,y),象素座标系上的座标为(u,v)
象素座标系与图象座标系之间的转换为:
其中u0,v0是图象座标系原点在象素座标系中的座标,dx和dy分别是每位象素在图象平面上x和y方向上的规格,这种值也被称为图象的内参矩阵,是可以通过API领到的。
图象座标系与单反座标系之间的转换为:
其中f为焦距,为何如此转换是按照相像三角形定律得到的,如右图所示:
最后则是单反座标系与世界座标系的转换关系:
其中R为3x3的正交旋转矩阵,t为三维平移向量,这几个参数也被称为单反的外参矩阵,也是可以领到的。
在通常的应用中,我们只须要从象素座标系转换到单反座标系就够用了。
基本知识都打算完毕,接下来看怎样在上获取象素点的三维座标。
在Swift按照象素点估算出它基于单反的三维座标
在Swift中启动单反主要有以下三个步骤
// 1. 发现 TruthDepth 相机
let videoDeviceDiscoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInTrueDepthCamera], mediaType: .video, position: .front)
// 2. 初始化输入和输出
let videoDeviceInput = try AVCaptureDeviceInput(device: videoDeviceDiscoverySession.devices.first!)
// 3. 给 session 添加输出
let depthDataOutput = AVCaptureDepthDataOutput() session.addOutput(depthDataOutput) depthDataOutput.setDelegate(self, callbackQueue: dataOutputQueue)
由于我们设置了,所以单反只要捕捉到一帧深度图都会反弹下边这个函数
func dataOutputSynchronizer(_ synchronizer: AVCaptureDataOutputSynchronizer, didOutput synchronizedDataCollection: AVCaptureSynchronizedDataCollection) {
...
// 获得相机内参数和对应的分辨率
let intrinsicMartix = syncedDepthData.depthData.cameraCalibrationData?.intrinsicMatrix
let refenceDimension = syncedDepthData.depthData.cameraCalibrationData?.intrinsicMatrixReferenceDimensions
self.camFx = intrinsicMartix![0][0]
self.camFy = intrinsicMartix![1][1]
self.camOx = intrinsicMartix![0][2]
self.camOy = intrinsicMartix![1][2]
self.refWidth = Float(refenceDimension!.width)
self.refHeight = Float(refenceDimension!.height)
...
}
在这个反弹函数里,我们可以获得摄像头的内参数,示例的程序中,只要触摸预览图中某一个象素点,程序会调用下边代码块输出该象素点在单反座标系下的X,Y和Z的值。
override func touchesBegan(_ touches: Set, with event: UIEvent?) {
let touchPoint = (touches as NSSet).allObjects[0] as! UITouch
// 获得像素坐标系的坐标
let coord = touchPoint.location(in: self.preview)
let viewContent = self.preview.bounds
let xRatio = Float(coord.x / viewContent.size.width)
let yRatio = Float(coord.y / viewContent.size.height)
// 获得触摸像素点的深度值 Z,单位为 cm
let realZ = getDepth(from: depthPixelBuffer!, atXRatio: xRatio, atYRatio: yRatio)
// 获得对应的 X 和 Y 值,计算公式其实就是两个坐标转换矩阵之间相乘后的结果
// 像素 -> 图像 -> 相机坐标系
let realX = (xRatio * refWidth! - camOx!) * realZ / camFx!
let realY = (yRatio * refHeight! - camOy!) * realZ / camFy!
DispatchQueue.main.async {
self.touchCoord.text = String.localizedStringWithFormat("X = %.2f cm, Y = %.2f cm, Z = %.2f cm", realX, realY, realZ)
}
}
示例程序的疗效图如下:
image.png
这儿输出的是黑色点对应的X,Y,Z值,完整代码戳这儿。
参考链接
[1]手臂辨识技术解析
[2]Quoar:WhatisthefloodinXfor?
[3]世界,单反,图象,象素座标系之间的关系
[4]Guide-Apple
[5]PhotoandVideoUsingDepth