手把手教你实现条纹结构光三维重建(3)——相机投影仪标定

我们都知道,投影仪其实就是个反向相机,如果我们了解双目标定的原理,那么相机和投影仪的标定就不难,关键是我们怎么得到投影仪在图像特征点(比如棋盘格角点)上的像素位置。

投影仪也类似于一个cmos,图像有像素位置(u,v),那么通过我们上一讲的条纹解码,给图像添加水平方向和垂直方向的投影,就可以通过解码,得到图像对应的投影相位值,此相位值就是投影的像素坐标(xp,yp)。如下图所示,具体的原理可以参考论文《Calibration of fringe projection profilometry A comparative review》。

这里,为了方便大家理解,可以参考中文文献《基于数字光栅投影的结构光三维测量技术与系统研究》 ,我将重点截图如下:

第四点,绝对相位映射到DMD图像坐标,如下所示:

say easy,show me code。。。

代码如下:

#include <iostream>
#include <vector>
#include <opencv2/opencv.hpp>
//#define PROJECTOR_WIDTH_1080P    1920          //这里定义的是1080P投影仪的分辨率
//#define PROJECTOR_HEIGHT_1080P   1080#define PROJECTOR_WIDTH_1080P    912          //这里定义的是1080P投影仪的分辨率
#define PROJECTOR_HEIGHT_1080P   1140#define PROJECTOR_WIDTH_720P    1280          //这里定义的是720P投影仪的分辨率
#define PROJECTOR_HEIGHT_720P   720#define PROJECTOR_WIDTH_480P    854          //这里定义的是480P投影仪的分辨率
#define PROJECTOR_HEIGHT_480P   480#define PROJECTOR_WIDTH_4500    912          //这里定义的是TI的4500投影仪的分辨率
#define PROJECTOR_HEIGHT_4500   1140
#define PI 3.1415926std::vector<float> h_freq_array, v_freq_array;// Absolute phase from 4 frames
cv::Mat get_phase4(const cv::Mat I1, const cv::Mat I2, const cv::Mat I3, const cv::Mat I4) {cv::Mat_<float> I1_(I1);cv::Mat_<float> I2_(I2);cv::Mat_<float> I3_(I3);cv::Mat_<float> I4_(I4);//获取wrap相位int m_nHeight = I1.rows;int m_nWidth = I1.cols;cv::Mat phase = cv::Mat::zeros(m_nHeight, m_nWidth, CV_32FC1);//#pragma omp parallel forfor (int i = 0; i < m_nHeight; i++){for (int j = 0; j < m_nWidth; j++){int a1 = I1_.at<float>(i, j);int a2 = I2_.at<float>(i, j);int a3 = I3_.at<float>(i, j);int a4 = I4_.at<float>(i, j);phase.at<float>(i, j) = (float)atan2((a2 - a4), (a1 - a3));if (phase.at<float>(i, j) < 0)phase.at<float>(i, j) += (2 * PI);}}return phase;
}
cv::Mat unwrap_with_cue(const cv::Mat up, const cv::Mat upCue, float nPhase)
{// Determine number of jumpscv::Mat P = ((upCue)*nPhase - up) / (2 * PI);cv::Mat tmp(P.rows, P.cols, CV_32FC1);for (int i = 0; i < up.rows; i++) {for (int j = 0; j < up.cols; j++) {tmp.at<float>(i, j) = round(P.at<float>(i, j));}}// Add to phasecv::Mat upUnwrapped = up + tmp * 2 * PI;// Scale to range [0; 2pi]upUnwrapped *= 1.0 / nPhase;return upUnwrapped;
}cv::Mat decode_pattern(const std::vector<cv::Mat>& encode_images, const std::vector<float>& phases, const int projector_lens)
{//前面四组图案最低频率的编码(频率为1),所以不需要进行相位展开std::vector<cv::Mat> frames_low_freq(encode_images.begin(), encode_images.begin() + 4);cv::Mat upCue = get_phase4(frames_low_freq[0], frames_low_freq[1], frames_low_freq[2], frames_low_freq[3]);for (int index = 1; index < phases.size(); index++)  //两两求解双频{std::vector<cv::Mat> frames_high_freq(encode_images.begin() + 4 * (index), encode_images.begin() + 4 * (index + 1));cv::Mat unPhase = get_phase4(frames_high_freq[0], frames_high_freq[1], frames_high_freq[2], frames_high_freq[3]);upCue = unwrap_with_cue(unPhase, upCue, phases[index]);}cv::Mat decode_phase_img = projector_lens * ((upCue) / (2 * PI));return decode_phase_img;
}void generate_freqs(std::vector <float>& freq_array, int length, int min_T)
{freq_array[4] = (double)length / min_T;     //我们需要生成五个频率,第五个频率为[投影宽度/周期]double x = sqrtf(sqrtf(freq_array[4]));    //第二个频率定义为第五个频率的开四次根号freq_array[3] = x * x * x; //第四个频率  freq_array[2] = x * x;     //第三个频率freq_array[1] = x;         //第二个频率freq_array[0] = 1;         //第一个频率
}void init()
{v_freq_array.resize(5);generate_freqs(v_freq_array, PROJECTOR_HEIGHT_4500, 10);        //图像垂直方向——横条纹频率h_freq_array.resize(5);generate_freqs(h_freq_array, PROJECTOR_WIDTH_4500, 10);         //图像水平方向——竖条纹频率
}void save_calibrate_xml(std::string filename,const cv::Mat& Kc, const cv::Mat& kc, double cam_error,const cv::Mat& Kp, const cv::Mat& kp, double proj_error,const cv::Mat& Rpc, const cv::Mat& Tpc, double stereo_error)
{cv::FileStorage fs(filename, cv::FileStorage::WRITE);if (!fs.isOpened())return;fs << "Kc" << cv::Mat(Kc) << "kc" << cv::Mat(kc) << "Kp" << cv::Mat(Kp) << "kp" << cv::Mat(kp) << "Rpc" << cv::Mat(Rpc) << "Tpc" << cv::Mat(Tpc) << "cam_error" << cam_error << "proj_error"<< proj_error << "stereo_error" << stereo_error;fs.release();
}bool calibrate_Impl(const std::vector<cv::Mat>& chess_imgs, const std::vector<cv::Mat>& vps, const std::vector<cv::Mat>& ups)
{unsigned int img_height = chess_imgs[0].rows;unsigned int img_width = chess_imgs[0].cols;cv::Size pattern_size(8, 11);float checkerSize = 10;std::vector<cv::Point3f> Qi;for (int h = 0; h < pattern_size.height; h++)for (int w = 0; w < pattern_size.width; w++)Qi.push_back(cv::Point3f(checkerSize * w, checkerSize * h, 0.0));std::vector<std::vector<cv::Point2f> > qc, qp;std::vector<std::vector<cv::Point3f> > object_points;int nFrameSeq = chess_imgs.size();for (unsigned int i = 0; i < nFrameSeq; i++){std::vector<cv::Point2f> qci;bool success =cv::findChessboardCorners(chess_imgs[i], pattern_size, qci, cv::CALIB_CB_ADAPTIVE_THRESH);if (!success){std::cout << "图像 " << i << " 不能找到角点\n";continue;}else {// Refine corner locationscv::cornerSubPix(chess_imgs[i], qci, cv::Size(5, 5), cv::Size(1, 1),cv::TermCriteria(CV_TERMCRIT_EPS + CV_TERMCRIT_ITER, 30, 0.1));}// Draw colored chessboardcv::Mat chess_imgs_color;cv::cvtColor(chess_imgs[i], chess_imgs_color, cv::COLOR_GRAY2RGB);cv::drawChessboardCorners(chess_imgs_color, pattern_size, qci, success);
#if 1cv::resize(chess_imgs_color, chess_imgs_color, cv::Size(chess_imgs_color.cols * 0.5, chess_imgs_color.rows * 0.5));cv::imshow("chess_imgs_color", chess_imgs_color);cv::waitKey(0);
#endif// Vectors of accepted points for current viewstd::vector<cv::Point2f> qpi_a;std::vector<cv::Point2f> qci_a;std::vector<cv::Point3f> Qi_a;// Loop through checkerboard cornersfor (unsigned int j = 0; j < qci.size(); j++) {const cv::Point2f& qcij = qci[j];// Collect neighbor pointsconst unsigned int WINDOW_SIZE = 10;std::vector<cv::Point2f> N_qcij, N_qpij;// avoid going out of boundsunsigned int starth = std::max(int(qcij.y + 0.5) - WINDOW_SIZE, 0u);unsigned int stoph = std::min(int(qcij.y + 0.5) + WINDOW_SIZE, img_height - 1);unsigned int startw = std::max(int(qcij.x + 0.5) - WINDOW_SIZE, 0u);unsigned int stopw = std::min(int(qcij.x + 0.5) + WINDOW_SIZE, img_width - 1);for (unsigned int h = starth; h <= stoph; h++) {for (unsigned int w = startw; w <= stopw; w++) {N_qcij.push_back(cv::Point2f(w, h));float upijwh = ups[i].at<float>(h, w);float vpijwh = vps[i].at<float>(h, w);N_qpij.push_back(cv::Point2f(upijwh, vpijwh));}}// std::cout << i << " findHomography " << N_qcij.size() << " " << N_qpij.size() <<// std::endl;// if enough valid points to build homographyif (N_qpij.size() >= 50) {//                    std::cout << i << " findHomography" << std::endl;// translate qcij into qpij using local homographycv::Mat H = cv::findHomography(N_qcij, N_qpij, cv::RANSAC);/*std::cout << "H:\n" << H << endl;*/if (!H.empty()) {cv::Point3d Q =cv::Point3d(cv::Mat(H * cv::Mat(cv::Point3d(qcij.x, qcij.y, 1.0))));cv::Point2f qpij = cv::Point2f(Q.x / Q.z, Q.y / Q.z);qpi_a.push_back(qpij);qci_a.push_back(qci[j]);Qi_a.push_back(Qi[j]);}}}if (!Qi_a.empty()) {// Store projector corner coordinatesqp.push_back(qpi_a);// Store camera corner coordinatesqc.push_back(qci_a);// Store world corner coordinatesobject_points.push_back(Qi_a);}}if (object_points.size() <= 4) {std::cerr << "没有足够的标定数据!" << std::endl;return false;}// 标定相机cv::Mat Kc, kc;std::vector<cv::Mat> cam_rvecs, cam_tvecs;cv::Size frameSize(img_width, img_height);int cal_flags = 0+ cv::CALIB_FIX_K4+ cv::CALIB_FIX_K5;double cam_error = cv::calibrateCamera(object_points, qc, frameSize, Kc, kc, cam_rvecs, cam_tvecs, cal_flags,cv::TermCriteria(cv::TermCriteria::COUNT + cv::TermCriteria::EPS, 50, DBL_EPSILON));// 标定投影仪cv::Mat Kp, kp;std::vector<cv::Mat> proj_rvecs, proj_tvecs;cv::Size screenSize(PROJECTOR_WIDTH_4500, PROJECTOR_HEIGHT_4500);double proj_error = cv::calibrateCamera(object_points, qp, screenSize, Kp, kp, proj_rvecs, proj_tvecs, cal_flags,cv::TermCriteria(cv::TermCriteria::COUNT + cv::TermCriteria::EPS, 50, DBL_EPSILON));// 双目标定cv::Mat Rp, Tp, E, F;double stereo_error = cv::stereoCalibrate(object_points, qc, qp, Kc, kc, Kp, kp, frameSize, Rp, Tp, E, F,cv::CALIB_FIX_INTRINSIC + cal_flags,cv::TermCriteria(cv::TermCriteria::COUNT + cv::TermCriteria::EPS, 100, DBL_EPSILON));//显示标定结果std::cout << "Kc = \n" << Kc;std::cout << "\nkc = \n" << kc;std::cout << "\ncam_error = " << cam_error << std::endl;std::cout << "\nKp = \n" << Kp;std::cout << "\nkp = \n" << kp;std::cout << "\nproj_error = " << proj_error << std::endl;std::cout << "\nRp = \n" << Rp;std::cout << "\nTp = \n" << Tp;std::cout << "\nstereo_error = " << stereo_error;std::string filename = "calibration_0.xml";save_calibrate_xml(filename, Kc,kc, cam_error,Kp,kp, proj_error,Rp,Tp, stereo_error);
}void read_calib_images(std::string folder_path, int img_count,std::vector<std::vector<cv::Mat>>& multi_calib_images_H, std::vector<std::vector<cv::Mat>>& multi_calib_images_V)
{for (int group = 0; group < img_count; group++)   //总共的图像组数{std::vector<cv::Mat> calib_images_H, calib_images_V;std::string folder_path_index = folder_path + std::to_string(group) + "//*.bmp";std::vector<cv::String> image_files;cv::glob(folder_path_index, image_files);int img_index = 0;for (const auto& file : image_files) {cv::Mat image = cv::imread(file,0);if (image.empty()) {std::cerr << "Failed to read image: " << file << std::endl;continue;}if (img_index < 20)calib_images_V.push_back(image);elsecalib_images_H.push_back(image);img_index++;}multi_calib_images_H.push_back(calib_images_H);multi_calib_images_V.push_back(calib_images_V);}
}int main()
{init();//1. 读取标定图像std::vector<std::vector<cv::Mat>> multi_calib_images_H, multi_calib_images_V;std::string folder_path = ".//calibImage//";int img_count = 7;read_calib_images(folder_path, img_count, multi_calib_images_H, multi_calib_images_V);if (multi_calib_images_V.size() <= 4){std::cout << "至少需要4组图像!\n";return 0;}std::vector<cv::Mat> chess_imgs, vps, ups;int encode_img_count = multi_calib_images_V[0].size();    //获取每一组编码图像的数量,5个频率,4步相移,其实就是20张图像for (int i = 0; i < img_count; i++){cv::Mat chess_img = multi_calib_images_H[i][encode_img_count];   //获取最后一张没有条纹的棋盘格图像cv::Mat decode_img_V = decode_pattern(multi_calib_images_V[i], v_freq_array, PROJECTOR_HEIGHT_4500);cv::Mat decode_img_H = decode_pattern(multi_calib_images_H[i], h_freq_array, PROJECTOR_WIDTH_4500*2);chess_imgs.push_back(chess_img.clone());vps.push_back(decode_img_V.clone());ups.push_back(decode_img_H.clone());}calibrate_Impl(chess_imgs, vps, ups);return 0;
}

 部分运行效果(检测到的棋盘格角点)如下所示:

我们将最后一组图像作为测试,进行三维重建,效果如下:

如果大家有什么疑问,欢迎留言,我将一一解答。

相关的测试数据集可以下面链接中下载

https://download.csdn.net/download/laiyinping/89464283

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/31363.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

WebSocket实现消息实时通知

参考文档&#xff1a;万字长文&#xff0c;一篇吃透WebSocket&#xff1a;概念、原理、易错常识、动手实践、WebSocket 教程 1 背景 有一个需求&#xff0c;需要实现实时通信的功能&#xff0c;如果有新消息&#xff0c;后端会主动发送请求告知前端有新消息&#xff0c;需要前…

改进YOLOv7 | 在 ELAN 模块中添加【Triplet】【SpatialGroupEnhance】【NAM】【S2】注意力机制 | 附详细结构图

改进 YOLOv7 | 在 ELAN 模块中添加【Triplet】【SpatialGroupEnhance】【NAM】【S2】注意力机制&#xff1a;中文详解 1. 简介 YOLOv7 是目前主流的目标检测算法之一&#xff0c;具有速度快、精度高的特点。但 YOLOv7 的原始模型结构中缺乏注意力机制&#xff0c;导致模型对全…

Matlab基础语法:变量和数据类型,基本运算,矩阵和向量,常用函数,脚本文件

目录 一、变量和数据类型 二、基本运算 三、矩阵和向量 四、常用函数 五、脚本文件 六、总结 一、变量和数据类型 Matlab 支持多种数据类型&#xff0c;包括数值类型、字符类型和逻辑类型。掌握这些基本的变量和数据类型&#xff0c;是我们进行数学建模和计算的基础。 数…

嵌入式软件stm32面试

一、STM32的内核型号有哪些&#xff1f; STM32系列是STMicroelectronics&#xff08;意法半导体&#xff09;生产的基于ARM Cortex-M内核的微控制器产品线。这些产品按照不同的内核架构和性能特点分为了主流产品、超低功耗产品和高性能产品。 1.1 主流产品 STM32F0 系列&…

利用sortablejs实现拖拽排序

import Sortable from "sortablejs";created() {//禁止火狐拖拽进行搜索document.body.ondrop function(event){event.preventDefault();event.stopPropagation();}}// 打开对话框的时候调用下openCustomDialog(){this.rowDrop()}// 行拖拽 rowDrop() {this.$nextTi…

Linux工具(包含sudo提权与vim快捷配置)

目录 什么是软件包 查看软件包 如何安装软件 1.官方yum源下载 2.扩展yum源下载 如何卸载软件 补充知识如何将普通用户加入白名单 补充知识rzsz vim编辑器 1.命令模式&#xff08;进入默认为这个模式&#xff09;用户所有的输入都会被当成命令 2.插入模式 3.底行模…

SpringCloud Maven多模块项目导包

目录 一、父项目配置 二、配置子项目 三、Maven执行 四、运行Jar包 一、父项目配置 所有父项目均需确保配置了 <packaging>pom</packaging> 因为Maven某人的打包方式是 <packaging>jar</packaging> 二、配置子项目 仅在有SpringBoot启动类的…

Qt Designer 中设置信号与槽,QT5的四种编辑模式

目录 QT5的四种编辑模式 Qt Designer 中设置信号与槽 Qt Designer 中设置信号与槽 QT5的四种编辑模式 在QT5中,特别是在使用Qt Designer进行界面设计时,存在多种编辑模式以满足不同的开发需求。以下是对QT5中四种主要编辑模式的详细解释: 控件编辑模式(Edit Widgets):…

速盾:cdn高防免备案

云计算和互联网技术的发展&#xff0c;带来了无数便利和机遇&#xff0c;但也带来了各种网络安全威胁。网站被黑、DDoS攻击、敏感信息泄露等问题&#xff0c;给企业和个人带来了巨大的损失和风险。因此&#xff0c;保护网络安全成为了当务之急。 CDN&#xff08;Content Deliv…

【PHP项目实战训练】——使用thinkphp框架对数据进行增删改查功能

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;开发者-曼亿点 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 曼亿点 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a…

Python网页爬虫爬取豆瓣Top250电影数据——Xpath数据解析

Python网页爬虫爬取豆瓣Top250电影数据——Xpath数据解析 将使用Python网页爬虫爬取豆瓣电影Top250的电影数据&#xff0c;网页解析方法使用xpath。 获取数据后会将数据保存到CSV文件中。一、分析网页&#xff0c;初步获取信息 1.1 查看原页面信息 首先打开豆瓣Top250电影页…

操作系统期末快速复习(概念)

文章目录 第一章&#xff1a;操作系统引论操作系统的目标是什么&#xff1f;分时系统是什么&#xff1f;实时系统是什么&#xff1f;分时系统和实时系统的比较操作系统的基本特征是什么&#xff1f;操作系统的主要功能&#xff1f;***重要第二章&#xff1a;进程管理程序的顺序…

【编译报错】syntax error near unexpected token `(‘

背景&#xff1a; 在android.bp中是这样写的 cflags: ["-DEXPORT__attribute__((visibility(\"default\")))","-D__ANDROID_VNDK__",],想转换成android.mk&#xff1a; LOCAL_CFLAGS : -DEXPORT__attribute__((visibility("default")…

ClassCastException(类转换异常)可能原因和解决方法总结

ClassCastException(类转换异常)可能原因和解决方法总结 大家好&#xff0c;我是免费搭建查券返利机器人省钱赚佣金就用微赚淘客系统3.0的小编&#xff0c;也是冬天不穿秋裤&#xff0c;天冷也要风度的程序猿&#xff01;ClassCastException是Java中常见的异常之一&#xff0c…

Ant design Vue 表格中显示不同的状态(多条件显示)

比如&#xff1a;后端一个字段有多种状态&#xff1a; 那么后端接口会返回&#xff1a;0 或者 1 或者 2 其中一个&#xff0c;前端需要展示的是对应的文字&#xff0c;像简单的只有两个状态的可以直接在列里面操作&#xff1a; {title: 状态,dataIndex: usable,customRender: …

Windows10任务栏卡顿解决方案

一、重新启动任务资源管理器 右键底部任务栏选择“任务管理器”&#xff1b;按快捷键“CtrlShiftEsc”&#xff1b;搜索框搜索“任务管理器”并单击“打开”&#xff1b;“WinX”打开开始菜单附属菜单&#xff0c;在列表中选择“任务管理器” &#xff1b;按下“ctrlaltdelete”…

网上考试系统设计文档

网上考试系统设计文档 1. 引言 1.1 目的 本设计文档旨在为“网上考试系统”的开发提供详细的指导方案。系统旨在提供一个全面的在线考试平台&#xff0c;支持在线考试、考试管理以及系统设置等功能&#xff0c;以满足教育机构和教师对考试流程的高效管理和优化需求。 1.2 范…

黄仁勋子女经历曝光:不卷名校,加入英伟达前开餐厅当厨子...

上周&#xff0c;黄仁勋脱下他那标志性的皮夹克&#xff0c;换上黑黄色的学位服&#xff0c;面对加州理工大学近600名毕业生发表演讲。 他提到&#xff0c;“忍受痛苦、应对挫折和看到机遇是我的超能力。” 面对美国媒体的采访&#xff0c;他多次回忆起自己充满逆境的童年&am…

【numpy】知识点和内容的详细解答-2024年6月21日

用户: ∗∗ 在 NumPy 中&#xff0c;ndarray 对象&#xff08;即多维数组&#xff09;要求数组中所有元素的类型必须相同&#xff08;因为所有元素的类型都必须相同&#xff0c;具有了一定的同一性&#xff0c;这就在最大程度上面优化了内存和计算效率&#xff09;&#xff0c;…

【SQL】drop、delete 与 truncate 的区别

在数据库管理中&#xff0c;DROP、DELETE 和 TRUNCATE 是用于删除数据或表的命令&#xff0c;但它们有不同的用途和效果。以下是它们的详细区别&#xff1a; 1. DROP DROP 语句用于删除整个数据库对象&#xff08;如表、视图、索引等&#xff09;。一旦对象被删除&#xff0c…