用奥比深度相机扫描一个非常平的平面,为什么深度值会出现厘米级的误差?

2023-11-23 13:50

本文主要是介绍用奥比深度相机扫描一个非常平的平面,为什么深度值会出现厘米级的误差?,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

编辑整理自 || 3D视觉开发者社区
✨如果觉得文章内容不错,别忘了三连支持下哦😘~

用奥比深度相机扫描一个非常平的平面,出现了4个毫米的误差。

这个误差是如何产生的?是应为相机平面与我给定的平面存在倾斜造成的?还是相机本身精度造成的?

运算代码如下:

#include <iostream>
#include <vector>
#include "OpenNI.h"
#include "OniSampleUtilities.h"using namespace openni;
using namespace std;#define MIN_DISTANCE 20  //单位毫米
#define MAX_DISTANCE 1200 //单位毫米
#define RESOULTION_X 640.0  //标定时的分辨率
#define RESOULTION_Y 480.0  //标定时的分辨率#define MAX_FRAME_COUNT 50typedef struct SPoint
{float x, y, z;SPoint(float u = 0.0f, float v = 0.0f, float w = 0.0f): x(u), y(v), z(w) { }
} Point;typedef struct xnIntrinsic_Params
{xnIntrinsic_Params() :c_x(320.0), c_y(240.0), f_x(480.0), f_y(480.0){}xnIntrinsic_Params(float c_x_, float c_y_, float f_x_, float f_y_) :c_x(c_x_), c_y(c_y_), f_x(f_x_),f_y(f_y_){}float c_x; //u轴上的归一化焦距float c_y; //v轴上的归一化焦距float f_x; //主点x坐标float f_y; //主点y坐标
}xIntrinsic_Params;xIntrinsic_Params g_IntrinsicParam; //存储相机内参的全局变量void getCameraParams(openni::Device& Device, xIntrinsic_Params& IrParam)
{OBCameraParams cameraParam;int dataSize = sizeof(cameraParam);memset(&cameraParam, 0, sizeof(cameraParam));openni::Status rc = Device.getProperty(openni::OBEXTENSION_ID_CAM_PARAMS, (uint8_t *)&cameraParam, &dataSize);if (rc != openni::STATUS_OK){std::cout << "Error:" << openni::OpenNI::getExtendedError() << std::endl;return;}IrParam.f_x = cameraParam.l_intr_p[0]; //u轴上的归一化焦距IrParam.f_y = cameraParam.l_intr_p[1]; //v轴上的归一化焦距IrParam.c_x = cameraParam.l_intr_p[2]; //主点x坐标IrParam.c_y = cameraParam.l_intr_p[3]; //主点y坐标
}void convertDepthToPointCloud(const uint16_t *pDepth, int width, int height, int middleIndex)
{if (NULL == pDepth){printf("depth frame is NULL!");return;}//分辨率缩放,这里假设标定时的分辨率分RESOULTION_X,RESOULTION_Yfloat fdx = g_IntrinsicParam.f_x * ((float)(width) / RESOULTION_X);float fdy = g_IntrinsicParam.f_y * ((float)(height) / RESOULTION_Y);float u0  = g_IntrinsicParam.c_x * ((float)(width) / RESOULTION_X);float v0  = g_IntrinsicParam.c_y * ((float)(height) / RESOULTION_Y);// 得到被扫描平面中心点的坐标SPoint midPoint(pDepth[middleIndex] * (width / 2 - u0) / fdx, pDepth[middleIndex] * ((height + 1) / 2 - v0) / fdy, pDepth[middleIndex]);vector<Point> points;uint16_t max_depth = MAX_DISTANCE;uint16_t min_depth = MIN_DISTANCE;float x, y, z;for (int v = 0; v < height; ++v){for (int u = 0; u < width; ++u){uint16_t depth = pDepth[v * width + u];if (depth <= 0 || depth < min_depth || depth > max_depth)continue;float tx = (u - u0) / fdx;float ty = (v - v0) / fdy;// 将中心点定义为世界坐标系的原点x = depth * tx - midPoint.x, y = depth * ty - midPoint.y, z = depth;if (x + 100.0f <= 0.001f || x - 100.0f >= 0.001f) continue;  // 滤波if (y + 100.0f <= 0.001f || y - 100.0f >= 0.001f) continue;points.push_back(SPoint(x, y, z));}}for (size_t i = 0; i < points.size(); ++i)cout << points[i].x << " " << points[i].y << " " <<points[i].z << endl;
}int g_imageCount = 0;void analyzeFrame(const VideoFrameRef& frame)
{DepthPixel* pDepth;g_imageCount++;if (MAX_FRAME_COUNT < g_imageCount){return;}//int middleIndex = (frame.getHeight() + 1)*frame.getWidth() / 2;switch (frame.getVideoMode().getPixelFormat()){case PIXEL_FORMAT_DEPTH_1_MM:pDepth = (DepthPixel*)frame.getData();//printf("[%08llu] %8d\n", (long long)frame.getTimestamp(),//    pDepth[middleIndex]);//将深度数据转换为点云并保存成ply文件,每帧深度数据对应一个ply文件convertDepthToPointCloud(pDepth, frame.getWidth(), frame.getHeight(), (frame.getHeight() + 1) * frame.getWidth() / 2);break;default:printf("Unknown format\n");}
}class PrintCallback : public VideoStream::NewFrameListener
{
public:void onNewFrame(VideoStream& stream){stream.readFrame(&m_frame);analyzeFrame(m_frame);}
private:VideoFrameRef m_frame;
};int main(int argc, char* argv[])
{//initialize openNI sdkStatus rc = OpenNI::initialize();if (rc != STATUS_OK){printf("Initialize failed\n%s\n", OpenNI::getExtendedError());return 1;}//open deivceDevice device;rc = device.open(ANY_DEVICE);if (rc != STATUS_OK){printf("Couldn't open device\n%s\n", OpenNI::getExtendedError());return 2;}VideoStream depth;//create depth streamif (device.getSensorInfo(SENSOR_DEPTH) != NULL){rc = depth.create(device, SENSOR_DEPTH);if (rc != STATUS_OK){printf("Couldn't create depth stream\n%s\n", OpenNI::getExtendedError());}}//start depth streamrc = depth.start();if (rc != STATUS_OK){printf("Couldn't start the depth stream\n%s\n", OpenNI::getExtendedError());}PrintCallback depthPrinter;// Register frame listenerdepth.addNewFrameListener(&depthPrinter);//get intrinsic parameter from devicegetCameraParams(device, g_IntrinsicParam);// Wait while we're getting frames through the printerwhile (MAX_FRAME_COUNT > g_imageCount){Sleep(100);}depth.removeNewFrameListener(&depthPrinter);//stop depth streamdepth.stop();//destroy depth streamdepth.destroy();//close devicedevice.close();//shutdown OpenNIOpenNI::shutdown();return 0;
}

输出结果:

转换世界坐标系后的点云坐标
在这里插入图片描述

解答:

1:误差是由相机平面与给定的平面存在倾斜造成的?

——这个因素也是有可能的,因为很难保证相机光轴与墙面绝对垂直,只能是尽量减少倾斜带来的影响。

2:还是由相机本身精度造成的误差?

——相机本身是基本结构光的原理来工作的, 测量精度也是随着物镜距离增加从而带来一定的精度误差,这个误差一般来说与距离的平方成正比。

回复:

通过拟合平面,可以减小误差。

相机平面与墙面绝对不会平行,所以,要先算相机平面与墙面的夹角。

然后再通过给定的(x, y)来求修正后的深度值,误差不大了。谢谢大佬的提携和指点~

解答:

这是由立体视觉的原理决定的,结构光相机本质上是个双目相机。

双目估计深度的原理是,估计左眼的像素点在右眼中的偏移,称为视差,而深度值的变化量和图像视差成反比:

1/Z1-1/Z2= d(x1-x2)/(Baseline*FocalLength)

假设视差d只能估计到1/8像素精度,基线B=75mm,图像焦距F=555pixel@VGA分辨率。

那么带入公式左右差不多相等的,即:
1/1152-1/1156 = 0.125/(75*555)
就是说在1152mm处,深度量化的间隔是4mm。

已知Z1求Z2,公式:
Z2=1/(1/Z1-0.125/(75*555))
Z1=2000时,Z2=2012mm,增量是12mm。
Z1=4000时,Z2=4049mm,增量是49mm。
Z1=8000时,Z2=8197mm,增量是197mm。

可见,双目相机测距的误差,随着距离增加不是线性增加的,距离越远增大得越快。

实际上,结构光深度相机虽然有0到10000mm的量程,但能输出的有效整数深度值也只不过有800多个。

版权声明:本文为奥比中光3D视觉开发者社区特约作者授权原创发布,未经授权不得转载,本文仅做学术分享,版权归原作者所有,若涉及侵权内容请联系删文

3D视觉开发者社区是由奥比中光给所有开发者打造的分享与交流平台,旨在将3D视觉技术开放给开发者。平台为开发者提供3D视觉领域免费课程、奥比中光独家资源与专业技术支持。

点击加入3D视觉开发者社区,和开发者们一起讨论分享吧~
也可以移步微信关注官方公众号 3D视觉开发者社区 ,获取更多干货知识哦~

这篇关于用奥比深度相机扫描一个非常平的平面,为什么深度值会出现厘米级的误差?的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



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

相关文章

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

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

韦季李输入法_输入法和鼠标的深度融合

在数字化输入的新纪元,传统键盘输入方式正悄然进化。以往,面对实体键盘,我们常需目光游离于屏幕与键盘之间,以确认指尖下的精准位置。而屏幕键盘虽直观可见,却常因占据屏幕空间,迫使我们在操作与视野间做出妥协,频繁调整布局以兼顾输入与界面浏览。 幸而,韦季李输入法的横空出世,彻底颠覆了这一现状。它不仅对输入界面进行了革命性的重构,更巧妙地将鼠标这一传统外设融入其中,开创了一种前所未有的交互体验。 想象

免费也能高质量!2024年免费录屏软件深度对比评测

我公司因为客户覆盖面广的原因经常会开远程会议,有时候说的内容比较广需要引用多份的数据,我记录起来有一定难度,所以一般都用录屏工具来记录会议内容。这次我们来一起探索有什么免费录屏工具可以提高我们的工作效率吧。 1.福晰录屏大师 链接直达:https://www.foxitsoftware.cn/REC/  录屏软件录屏功能就是本职,这款录屏工具在录屏模式上提供了多种选项,可以选择屏幕录制、窗口

C语言指针入门 《C语言非常道》

C语言指针入门 《C语言非常道》 作为一个程序员,我接触 C 语言有十年了。有的朋友让我推荐 C 语言的参考书,我不敢乱推荐,尤其是国内作者写的书,往往七拼八凑,漏洞百出。 但是,李忠老师的《C语言非常道》值得一读。对了,李老师有个官网,网址是: 李忠老师官网 最棒的是,有配套的教学视频,可以试看。 试看点这里 接下来言归正传,讲解指针。以下内容很多都参考了李忠老师的《C语言非

动手学深度学习【数据操作+数据预处理】

import osos.makedirs(os.path.join('.', 'data'), exist_ok=True)data_file = os.path.join('.', 'data', 'house_tiny.csv')with open(data_file, 'w') as f:f.write('NumRooms,Alley,Price\n') # 列名f.write('NA

深度优先(DFS)和广度优先(BFS)——算法

深度优先 深度优先搜索算法(英语:Depth-First-Search,DFS)是一种用于遍历或搜索树或图的算法。 沿着树的深度遍历树的节点,尽可能深的搜索树的分支,当节点v的所在边都己被探寻过,搜索将回溯到发现节点v的那条边的起始节点。这一过程一直进行到已发现从源节点可达的所有节点为止。如果还存在未被发现的节点,则选择其中一个作为源节点并重复以上过程,整个进程反复进行直到所有节点都被访

图解TCP三次握手|深度解析|为什么是三次

写在前面 这篇文章我们来讲解析 TCP三次握手。 TCP 报文段 传输控制块TCB:存储了每一个连接中的一些重要信息。比如TCP连接表,指向发送和接收缓冲的指针,指向重传队列的指针,当前的发送和接收序列等等。 我们再来看一下TCP报文段的组成结构 TCP 三次握手 过程 假设有一台客户端,B有一台服务器。最初两端的TCP进程都是处于CLOSED关闭状态,客户端A打开链接,服务器端

java线程深度解析(六)——线程池技术

http://blog.csdn.net/Daybreak1209/article/details/51382604 一种最为简单的线程创建和回收的方法: [html]  view plain copy new Thread(new Runnable(){                @Override               public voi

java线程深度解析(五)——并发模型(生产者-消费者)

http://blog.csdn.net/Daybreak1209/article/details/51378055 三、生产者-消费者模式     在经典的多线程模式中,生产者-消费者为多线程间协作提供了良好的解决方案。基本原理是两类线程,即若干个生产者和若干个消费者,生产者负责提交用户请求任务(到内存缓冲区),消费者线程负责处理任务(从内存缓冲区中取任务进行处理),两类线程之

java线程深度解析(四)——并发模型(Master-Worker)

http://blog.csdn.net/daybreak1209/article/details/51372929 二、Master-worker ——分而治之      Master-worker常用的并行模式之一,核心思想是由两个进程协作工作,master负责接收和分配任务,worker负责处理任务,并把处理结果返回给Master进程,由Master进行汇总,返回给客