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

============================================================
相关推荐
2601_9679873710 小时前
用Python+OpenCV+YOLO写个游戏脚本:从自动瞄准到自动采集的保姆级教程
python·opencv·yolo·目标检测·游戏自动化
在所不辞兄10 小时前
【零基础学智能仿真-63】ABACUS作业工具与MCP接口
人工智能·深度学习·神经网络·机器学习·智能仿真
楚山之石10 小时前
【Medical Image Analysis】深度学习医学图像合成综述:伪CT、合成MRI与合成PET的架构、性能与临床边界
图像处理·人工智能·学习·mr·mri
blackappleD10 小时前
百度 Unlimited OCR 2 拆解:SR-SWA 如何让百页文档的 KV 缓存不再涨
人工智能·llm
数字新视界10 小时前
国产动环数据库在智能机房环境监控中的应用与优势分析
大数据·人工智能·数据中心·机房管理·模块化机房
holly就是我10 小时前
AI 编程助手为什么跑偏?用开源 ccglass 看清 Prompt、工具调用和 Token 消耗
人工智能·后端
雨中飘荡的记忆11 小时前
额度又见底了?这个开源项目,给 AI 编程助手接上 61 家"加油站"
人工智能
天远Date Lab11 小时前
零信任架构实战:基于天远企业年报信息核验构建自动化商业保理财务与社保穿透评估网关
人工智能·架构·自动化
EnCi Zheng11 小时前
后端怎么把AI接口稳定安全低成本接进业务:超时重试、密钥隔离与成本熔断清单
人工智能·后端·安全·架构·api
loulanyue_11 小时前
给概率大模型套上确定性缰绳:GraphRAG、神经符号与知识超图重构可信AI底座——读陈华钧杨博等CNCC2026第十届知识图谱论坛实录
人工智能·rag