OpenCV使用平面拼接图片

原图

拼接过程中的图

拼接后的图片

源码

main.cpp
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
cpp 复制代码
#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
cpp 复制代码
#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

运行后的结果

cpp 复制代码
============================================================
         图像拼接示例
============================================================

[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
[保存] 全景拼接图

============================================================
相关推荐
guohuang1 小时前
写 Prompt 的三要素:目标、约束、验收(附实战模板)
人工智能
sunneo1 小时前
02-GAP模型重构-AI产品闭环设计实战
人工智能·产品运营·aigc·产品经理·ai-native
番茄炒西红柿炒洋柿子1 小时前
OpenCV实现相机畸变校准
人工智能·数码相机·opencv
科学熊1 小时前
将chm文件格式转为PDF格式文件
人工智能
数据法师1 小时前
告别付费云端转写!Memo AI:一款部署在本地的无限次音视频转文字神器
人工智能·音视频
阿乔外贸日记1 小时前
以色列电商市场现状:规模、机遇与挑战
大数据·人工智能·智能手机·云计算·汽车
-cywen-1 小时前
扩散模型 2
人工智能
沪漂阿龙1 小时前
面试题:集成学习是什么?Boosting、Bagging、AdaBoost、随机森林为什么有效,一文讲透
人工智能·机器学习·集成学习
财经资讯数据_灵砚智能1 小时前
基于全球经济类多源新闻的NLP情感分析与数据可视化(日间)2026年5月12日
人工智能·python·信息可视化·自然语言处理·ai编程