news 2026/5/13 8:05:04

OpenCV使用平面拼接图片

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
OpenCV使用平面拼接图片

原图

拼接过程中的图

拼接后的图片

源码

main.cpp
/** * @file test_camera_calibrator.cpp * @brief 图像拼接测试程序 * * 功能说明: * 图像拼接 - 将多张重叠图像拼接成全景图 * * 拼接算法: 简单水平平移拼接 + 渐变融合 * * @author Auto Generated * @date 2026 */ #include "jpeg_reader.h" #include "jpeg_writer.h" #include <iostream> #include <sys/stat.h> #include <opencv2/stitching.hpp> #include <opencv2/features2d.hpp> /** * @brief 保存JPEG图像 * @param path 保存路径 * @param img 要保存的图像 * @param desc 描述信息(用于日志输出) * * 使用自定义JPEG读写器保存图像,失败时输出错误信息 */ void saveImage(const std::string& path, const cv::Mat& img, const std::string& desc) { if (imwrite_jpeg(path, img)) { std::cout << "[保存] " << desc << std::endl; } else { std::cerr << "[错误] 保存失败: " << path << std::endl; } } // ============================================================================ // 图像拼接示例 // ============================================================================ /** * @brief main - 图像拼接主函数 * * 拼接流程: * 1. 读取要拼接的图像序列 * 2. 使用ORB算法检测特征点 * 3. 使用BFMatcher进行特征匹配 * 4. 使用Lowe比率筛选和RANSAC单应性矩阵过滤误匹配 * 5. 逐张拼接,使用渐变融合消除接缝 * * 拼接算法说明: * - 特征检测: ORB (Oriented FAST + Rotated BRIEF) * - 特征匹配: BFMatcher with HAMMING distance * - 误匹配过滤: Lowe比率测试 + RANSAC单应性矩阵 * - 图像融合: 线性渐变融合 * * @param argc 命令行参数个数 * @param argv 命令行参数数组 * @return 程序退出码 */ int main(int argc, char** argv) { std::cout << "\n" << std::string(60, '=') << std::endl; std::cout << " 图像拼接示例" << std::endl; std::cout << std::string(60, '=') << std::endl; /** * 拼接图片列表 - 放在 stitch_data 目录 * 支持最多6张图片的拼接 */ const char* stitchImages[] = { "../stitch_data/img01.jpg", "../stitch_data/img02.jpg", "../stitch_data/img03.jpg", "../stitch_data/img04.jpg", "../stitch_data/img05.jpg", "../stitch_data/img06.jpg" }; int numImages = 6; // ======================================================================== // [第1步] 读取拼接图片 // ======================================================================== std::cout << "\n[1] 读取拼接图片..." << std::endl; std::vector<cv::Mat> images; // 遍历加载所有图片 for (int i = 0; i < numImages; i++) { // 使用自定义JPEG读取器读取图像 cv::Mat img = imread_jpeg(stitchImages[i]); if (img.empty()) { std::cerr << "警告: 无法加载图像 " << stitchImages[i] << std::endl; continue; } // 转换为彩色图像 (Stitcher 需要 CV_8UC3) if (img.channels() == 1) { cv::cvtColor(img, img, cv::COLOR_GRAY2BGR); } images.push_back(img); std::cout << "已加载: " << stitchImages[i] << " (" << img.cols << "x" << img.rows << ", " << img.channels() << "通道)" << std::endl; } // 至少需要2张图片才能拼接 if (images.size() < 2) { std::cerr << "错误: 需要至少2张图片进行拼接" << std::endl; return -1; } // ======================================================================== // [第2步] 特征检测 // 使用ORB (Oriented FAST + Rotated BRIEF) 算法 // ORB是一种快速且无需专利授权的特征检测算法 // ======================================================================== std::cout << "\n[2] 特征检测..." << std::endl; // 创建ORB特征检测器,最多检测5000个特征点 cv::Ptr<cv::ORB> orb = cv::ORB::create(5000); std::vector<cv::Mat> descriptors; // 存储特征描述子 std::vector<std::vector<cv::KeyPoint>> keypoints; // 存储特征点 // 对每张图像进行特征检测 for (size_t i = 0; i < images.size(); i++) { std::vector<cv::KeyPoint> kp; cv::Mat desc; // detectAndCompute: 检测特征点并计算描述子 orb->detectAndCompute(images[i], cv::noArray(), kp, desc); keypoints.push_back(kp); descriptors.push_back(desc); std::cout << "图片" << (i+1) << ": 检测到 " << kp.size() << " 个特征点" << std::endl; } // ======================================================================== // [第3步] 特征匹配 // 使用BFMatcher (Brute Force Matcher) 进行暴力匹配 // HAMMING距离适合ORB描述子(二进制向量) // ======================================================================== // 创建暴力匹配器,使用HAMMING距离 cv::Ptr<cv::DescriptorMatcher> matcher = cv::BFMatcher::create(cv::NORM_HAMMING); std::cout << "\n[3] 匹配特征点..." << std::endl; // 匹配相邻图片(顺序很重要) for (size_t i = 0; i < images.size() - 1; i++) { std::vector<std::vector<cv::DMatch>> knnMatches; /** * KNN匹配 * 为每个特征点找2个最近邻匹配 * 用于Lowe比率测试 */ matcher->knnMatch(descriptors[i], descriptors[i+1], knnMatches, 2); /** * Lowe比率筛选 * 如果最近邻的距离远大于次近邻,说明匹配不准确 * 比率阈值0.7是经典值 */ std::vector<cv::DMatch> goodMatches; for (auto& match : knnMatches) { if (match.size() >= 2) { float ratio = match[0].distance / match[1].distance; // 双重过滤:比率 < 0.7 且 距离 < 50 if (ratio < 0.7 && match[0].distance < 50) { goodMatches.push_back(match[0]); } } } /** * 基于单应性矩阵的几何验证 * 使用RANSAC算法剔除离群点 */ if (goodMatches.size() > 4) { std::vector<cv::Point2f> src_pts, dst_pts; for (auto& m : goodMatches) { src_pts.push_back(keypoints[i][m.queryIdx].pt); dst_pts.push_back(keypoints[i+1][m.trainIdx].pt); } // 计算单应性矩阵并获取内点mask std::vector<uchar> mask; cv::Mat H = cv::findHomography(src_pts, dst_pts, mask, cv::RANSAC, 3); // 只保留被判定为内点的匹配 std::vector<cv::DMatch> inliers; for (size_t j = 0; j < goodMatches.size(); j++) { if (mask[j]) { inliers.push_back(goodMatches[j]); } } goodMatches = inliers; } std::cout << "图片" << (i+1) << "->" << (i+2) << ": 过滤后匹配点: " << goodMatches.size() << std::endl; // 绘制匹配结果并保存 cv::Mat matchImg; cv::drawMatches(images[i], keypoints[i], images[i+1], keypoints[i+1], goodMatches, matchImg, cv::Scalar::all(-1), cv::Scalar::all(-1), std::vector<char>(), cv::DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS); // 确保是彩色图 if (matchImg.channels() == 1) { cv::cvtColor(matchImg, matchImg, cv::COLOR_GRAY2BGR); } char buf[64]; sprintf(buf, "../stitch_data/match_%zu_%zu.jpg", i+1, i+2); saveImage(buf, matchImg, "特征匹配"); } // ======================================================================== // [第4步] 执行图像拼接 // 使用简单水平平移拼接算法 // // 算法原理: // 1. 假设相机做水平平移运动 // 2. 每张新图放置在上一张图的右边 // 3. 重叠区域使用线性渐变融合 // // 关键参数: // - overlap: 重叠区域宽度(像素) // - step: 步进距离(像素) // // 拼接公式: // new_width = current_width + new_image_width - overlap // ======================================================================== std::cout << "\n[4] 执行图像拼接..." << std::endl; // 预处理:计算最大图像尺寸 int maxWidth = 0, maxHeight = 0; for (auto& img : images) { maxWidth = std::max(maxWidth, img.cols); maxHeight = std::max(maxHeight, img.rows); } std::cout << "图像最大尺寸: " << maxWidth << "x" << maxHeight << std::endl; std::cout << "\n[4] 执行图像拼接(逐张拼接法)..." << std::endl; // 以第一张图像作为初始拼接结果 cv::Mat result = images[0].clone(); /** * 重叠区域宽度(像素) * 这个值需要根据实际拍摄情况设置 * 重叠越多,拼接越容易成功,但效率越低 */ const int overlap = 530; /** * 步进距离(像素) * 理论上: step = image_width - overlap * 用于记录当前拼接进度 */ const int step = 1235; // ======================================================================== // 逐张拼接循环 // ======================================================================== for (size_t i = 1; i < images.size(); i++) { std::cout << "拼接第 " << (i+1) << "/" << images.size() << " 张..." << std::endl; const cv::Mat& next_img = images[i]; int h = result.rows; /** * 计算新画布宽度 * 公式:已有宽度 + 新图宽度 - 重叠区域 * 重叠区域只计算一次,避免重复 */ int new_w = result.cols + next_img.cols - overlap; // 创建新的画布,黑色背景 cv::Mat new_result(h, new_w, CV_8UC3, cv::Scalar(0, 0, 0)); // 将已有结果复制到新画布左侧 result.copyTo(new_result(cv::Rect(0, 0, result.cols, h))); // ==================================================================== // 渐变融合重叠区域 // // 原理:从左到右逐渐从旧图过渡到新图 // 避免明显接缝,让拼接更自然 // // alpha = 0.0 时完全使用旧图 // alpha = 1.0 时完全使用新图 // ==================================================================== for (int y = 0; y < h; y++) { for (int x = 0; x < overlap; x++) { // 计算渐变权重 (0.0 ~ 1.0) float alpha = (float)x / overlap; // 获取重叠区域对应像素值 cv::Vec3b val1 = new_result.at<cv::Vec3b>(y, result.cols - overlap + x); // 来自旧图 cv::Vec3b val2 = next_img.at<cv::Vec3b>(y, x); // 来自新图 // 加权混合三个通道 new_result.at<cv::Vec3b>(y, result.cols - overlap + x) = cv::Vec3b( cv::saturate_cast<uchar>((1-alpha)*val1[0] + alpha*val2[0]), cv::saturate_cast<uchar>((1-alpha)*val1[1] + alpha*val2[1]), cv::saturate_cast<uchar>((1-alpha)*val1[2] + alpha*val2[2]) ); } } // ==================================================================== // 复制非重叠区域 // 新图右侧不重叠的部分直接复制到画布 // ==================================================================== for (int y = 0; y < h; y++) { for (int x = overlap; x < next_img.cols; x++) { new_result.at<cv::Vec3b>(y, result.cols + x - overlap) = next_img.at<cv::Vec3b>(y, x); } } // 更新结果图像 result = new_result; std::cout << " 当前尺寸: " << result.cols << "x" << result.rows << std::endl; } // 保存最终全景图 std::cout << "拼接完成! 尺寸: " << result.cols << "x" << result.rows << std::endl; saveImage("../stitch_data/panorama.jpg", result, "全景拼接图"); std::cout << "\n" << std::string(60, '=') << std::endl; return 0; } /** * @brief main - 程序主入口 * * 显示菜单供用户选择要执行的功能: * 1. 相机标定 (main1) * 2. 图像拼接 (main2) * * @param argc 命令行参数个数 * @param argv 命令行参数数组 * @return 程序退出码 */ int main(int argc, char** argv) { std::cout << "======================================" << std::endl; std::cout << " 机器视觉示例程序集" << std::endl; std::cout << "======================================" << std::endl; std::cout << "\n请选择功能:" << std::endl; std::cout << " 1. 相机标定 (main1)" << std::endl; std::cout << " 2. 图像拼接 (main2)" << std::endl; std::cout << "\n请输入选择 [1/2]: "; // 读取用户输入 char choice = '1'; std::cin >> choice; // 根据选择执行相应功能 if (choice == '2') { return main2(argc, argv); } else { return main1(argc, argv); } }
jpeg_writer.h
#ifndef JPEG_WRITER_H #define JPEG_WRITER_H #include <opencv2/opencv.hpp> #include <cstdio> #include <jpeglib.h> // 使用 libjpeg 保存 JPEG 文件 - 强制保存为彩色 static bool imwrite_jpeg(const std::string& filepath, const cv::Mat& img, int quality = 90) { if (img.empty()) return false; cv::Mat rgb; // OpenCV 使用 BGR,libjpeg 使用 RGB if (img.channels() == 1) { cv::cvtColor(img, rgb, cv::COLOR_GRAY2RGB); } else if (img.channels() == 3) { cv::cvtColor(img, rgb, cv::COLOR_BGR2RGB); } else if (img.channels() == 4) { cv::cvtColor(img, rgb, cv::COLOR_BGRA2RGBA); } else { img.copyTo(rgb); } struct jpeg_compress_struct cinfo; struct jpeg_error_mgr jerr; FILE* outfile; JSAMPROW row_pointer[1]; cinfo.err = jpeg_std_error(&jerr); jpeg_create_compress(&cinfo); if ((outfile = fopen(filepath.c_str(), "wb")) == NULL) { return false; } jpeg_stdio_dest(&cinfo, outfile); cinfo.image_width = rgb.cols; cinfo.image_height = rgb.rows; cinfo.input_components = 3; cinfo.in_color_space = JCS_RGB; jpeg_set_defaults(&cinfo); jpeg_set_quality(&cinfo, quality, TRUE); jpeg_start_compress(&cinfo, TRUE); while (cinfo.next_scanline < cinfo.image_height) { row_pointer[0] = rgb.data + cinfo.next_scanline * rgb.cols * 3; jpeg_write_scanlines(&cinfo, row_pointer, 1); } jpeg_finish_compress(&cinfo); fclose(outfile); jpeg_destroy_compress(&cinfo); return true; } #endif // JPEG_WRITER_H
jpeg_reader.h
#ifndef JPEG_READER_H #define JPEG_READER_H #include <opencv2/opencv.hpp> #include <cstdio> #include <jpeglib.h> // 使用 libjpeg 读取 JPEG 文件 static cv::Mat imread_jpeg(const std::string& filepath) { struct jpeg_decompress_struct cinfo; struct jpeg_error_mgr jerr; FILE* infile = fopen(filepath.c_str(), "rb"); if (!infile) { return cv::Mat(); } cinfo.err = jpeg_std_error(&jerr); jpeg_create_decompress(&cinfo); jpeg_stdio_src(&cinfo, infile); if (jpeg_read_header(&cinfo, TRUE) != JPEG_HEADER_OK) { jpeg_destroy_decompress(&cinfo); fclose(infile); return cv::Mat(); } jpeg_start_decompress(&cinfo); int width = cinfo.output_width; int height = cinfo.output_height; int channels = cinfo.output_components; cv::Mat img(height, width, channels == 3 ? CV_8UC3 : CV_8UC1); JSAMPROW row_pointer[1]; while (cinfo.output_scanline < height) { row_pointer[0] = img.data + cinfo.output_scanline * width * channels; jpeg_read_scanlines(&cinfo, row_pointer, 1); } jpeg_finish_decompress(&cinfo); jpeg_destroy_decompress(&cinfo); fclose(infile); // libjpeg 返回 RGB,OpenCV 使用 BGR if (channels == 3) { cv::cvtColor(img, img, cv::COLOR_RGB2BGR); } return img; } #endif // JPEG_READER_H

运行后的结果

============================================================ 图像拼接示例 ============================================================ [1] 读取拼接图片... 已加载: ../stitch_data/img01.jpg (1765x2944, 3通道) 已加载: ../stitch_data/img02.jpg (1765x2944, 3通道) 已加载: ../stitch_data/img03.jpg (1765x2944, 3通道) 已加载: ../stitch_data/img04.jpg (1765x2944, 3通道) 已加载: ../stitch_data/img05.jpg (1765x2944, 3通道) 已加载: ../stitch_data/img06.jpg (1765x2944, 3通道) [2] 特征检测... 图片1: 检测到 5000 个特征点 图片2: 检测到 5000 个特征点 图片3: 检测到 5000 个特征点 图片4: 检测到 5000 个特征点 图片5: 检测到 5000 个特征点 图片6: 检测到 5000 个特征点 [3] 匹配特征点... 图片1->2: 过滤后匹配点: 651 [保存] 特征匹配 图片2->3: 过滤后匹配点: 1346 [保存] 特征匹配 图片3->4: 过滤后匹配点: 427 [保存] 特征匹配 图片4->5: 过滤后匹配点: 623 [保存] 特征匹配 图片5->6: 过滤后匹配点: 984 [保存] 特征匹配 [4] 执行图像拼接... 图像最大尺寸: 1765x2944 [4] 执行图像拼接(逐张拼接法)... 拼接第 2/6 张... 当前尺寸: 3000x2944 拼接第 3/6 张... 当前尺寸: 4235x2944 拼接第 4/6 张... 当前尺寸: 5470x2944 拼接第 5/6 张... 当前尺寸: 6705x2944 拼接第 6/6 张... 当前尺寸: 7940x2944 拼接完成! 尺寸: 7940x2944 [保存] 全景拼接图 ============================================================
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/13 8:04:08

对话式AI学习助手:构建个性化计算机科学教学系统

1. 项目概述&#xff1a;当代码生成遇上“对话式”学习最近在GitHub上看到一个挺有意思的项目&#xff0c;叫chataize/generative-cs。光看名字&#xff0c;你可能会觉得这又是一个普通的代码生成工具&#xff0c;或者是一个计算机科学&#xff08;CS&#xff09;的教程仓库。但…

作者头像 李华
网站建设 2026/5/13 8:03:04

构建基于大语言模型的智能体:从核心原理到工程实践

1. 智能体构建&#xff1a;从大脑到行动构建一个基于大语言模型的智能体&#xff0c;本质上是在为一段代码赋予“生命”。这个生命体的核心&#xff0c;我们称之为“大脑”&#xff0c;它负责思考、决策和指挥。但一个真正能用的智能体&#xff0c;光有大脑还不够&#xff0c;它…

作者头像 李华
网站建设 2026/5/13 8:02:05

5G独立组网(SA)技术解析:从NSA到SA的演进与行业应用

1. 5G独立组网&#xff1a;从“半成品”到“完全体”的漫长征途我们正处在一个被“5G”这个词包围的时代。从手机广告牌到科技新闻头条&#xff0c;它无处不在&#xff0c;承诺着改变一切&#xff1a;无人驾驶、远程手术、万物互联的智能工厂。然而&#xff0c;作为一个在通信行…

作者头像 李华
网站建设 2026/5/13 7:55:08

Shopify上线AI Toolkit:卖家运营提效新利器,却也暗藏风险与挑战

AI Toolkit&#xff1a;卖家运营的双刃剑近期&#xff0c;Shopify打开“后门”&#xff0c;允许卖家将Claude Code、Codex、Cursor等AI工具引入独立站后台&#xff0c;还上线“连接器”Shopify AI Toolkit。它能按卖家“口头交代”自动“读取”店铺信息并“执行”后台操作&…

作者头像 李华
网站建设 2026/5/13 7:55:07

马云回归阿里押注3800亿AI,千问×淘宝整合能否重写电商底层逻辑?

马云回归&#xff1a;为阿里注入士气2026年5月10日“阿里日”&#xff0c;全网关注阿里巴巴各园区寻找马云。他未参与园区内一些热门体验&#xff0c;而是在忙大事。次日&#xff0c;阿里官宣旗下AI平台千问与淘宝、天猫深度整合&#xff0c;从“让用户搜”切换到“让AI替用户搜…

作者头像 李华