遥感影像拼接线优化工具:基于Qt+GDAL+OpenCV的从零到一实践

遥感影像拼接线优化工具:基于Qt+GDAL+OpenCV的从零到一实践

一个C++图像处理老兵的求职作品集项目,从老旧代码迁移到完整的两图合并与拼接线可视化,附详细踩坑记录。

〇、成果速览

先上最终效果(展示加载左右影像、自动合并、红色折线为初始拼接线):
核心功能:

  • 加载两张有重叠区域的遥感影像(支持GeoTIFF、JPEG等)
  • 自动根据地理坐标合并成一张大图
  • 计算重叠区并绘制直角折线作为初始拼接线
  • 流畅的缩放、漫游操作
  • 支持超大影像(1GB+)采样显示

一、为什么做这个项目?

我干了14年C++图像处理,最近想往遥感/三维方向走走。为了面试的时候能有个东西拿得出手,我决定自己撸一个遥感影像拼接线优化演示工具

目标不是搞成商业软件,而是:

  • 展示常规图像处理算法(拼接线搜索、权重图构建)
  • 体现C++工程能力(框架迁移、内存管理、多线程)
  • 提供一个能交互的界面(加载影像、显示拼接线、缩放漫游)

技术栈:C++ + Qt 5.14.2 + GDAL 3.0.2 + OpenCV 4.5.5,开发环境VS2015。

二、站在巨人的肩膀上:基础框架的选择与迁移

遥感影像显示这玩意儿,底层细节挺多的------GDAL读写、多波段处理、坐标变换,从头造轮子不现实。我在CSDN上翻到了博主 Jacory 2014年发的《Qt + GDAL遥感图像基本浏览功能实现》[1],里面的 MapCanvas 继承自 QGraphicsView,实现了影像加载、缩放、漫游,代码清晰,正合我意。

原始项目环境: VS2010 + Qt4.8.4 + GDAL 1.10
我的目标环境: VS2015 + Qt5.14.2 + GDAL 3.0.2

迁移过程踩了不少坑,下面一个个说:

2.1 头文件与模块变化

Qt4里很多类在 <QtGui/...> 下,Qt5挪到了 <QtWidgets/...>。批量改呗:

cpp 复制代码
// 旧写法
#include <QtGui/QGraphicsView>
#include <QtGui/QWheelEvent>
// 新写法
#include <QGraphicsView>
#include <QWheelEvent>

2.2 QWheelEvent::delta() 已弃用

Qt5里用 angleDelta().y() 代替:

cpp 复制代码
void MapCanvas::wheelEvent(QWheelEvent *event)
{
    if (event->angleDelta().y() > 0)
        ZoomIn();
    else if (event->angleDelta().y() < 0)
        ZoomOut();
}

2.3 QTextCodec::setCodecForTr 没了

Qt5默认UTF-8,直接删掉相关代码就行。

2.4 GDAL 1.10 → 3.0.2

GISInternals 下了预编译包 release-1900-x64,配置项目属性:

  • 包含目录:D:\Libs\gdal-1900-x64\include
  • 库目录:D:\Libs\gdal-1900-x64\lib
  • 附加依赖项:gdal_i.lib

中文路径问题 :原代码里有一行 CPLSetConfigOption("GDAL_FILENAME_IS_UTF8", "NO"),这会导致中文路径打不开。删掉之后GDAL自动判断编码,完美解决。

折腾完这些,基础显示框架总算跑通了(此处插入单图加载、缩放动图)。

三、核心功能一:从单图显示到两图合并

为了演示拼接线,程序得能同时处理两张有重叠的影像。我搞了个对话框 TwoImagesDialog,让用户分别选左图和右图,点确定后自动合并显示。

3.1 对话框实现

cpp 复制代码
// TwoImagesDialog.h
class TwoImagesDialog : public QDialog
{
    Q_OBJECT
public:
    QString getLeftPath() const;
    QString getRightPath() const;
private slots:
    void onSelectLeft();
    void onSelectRight();
private:
    Ui::TwoImagesDialog ui;
};

界面就是两个文件选择行加确定/取消按钮(此处插入对话框截图)。

3.2 合并算法核心:地理坐标计算

每幅影像都有地理变换参数(GeoTransform),通过 CGdalUser(对GDAL的封装)可以拿到:

  • m_LBX, m_RTY:左上角地理坐标
  • m_lfGSD:分辨率(像素大小)
  • m_nImgWidth, m_nImgHeight:像素尺寸

合并步骤大概这样:

  1. 计算两幅影像的整体地理范围 (overallMinX, overallMaxX, overallMinY, overallMaxY)
  2. 计算合并后图像的像素尺寸 mergeWidth, mergeHeight
  3. 创建临时 GeoTIFF 文件
  4. 按地理坐标把两幅影像写到对应位置(重叠区后写的覆盖先写的)

关键难点:Y轴方向颠倒

GDAL读取的图像数据以左上角为原点,Y轴向下,而地理坐标里Y轴向上。直接写会导致图像上下颠倒。折腾了好一会儿,最终确认正确的映射关系:

  • 源图像左上角地理Y(src->m_RTY)应对齐到目标图像顶部(overallMaxY
  • 写入时不翻转数据 (因为CGdalUser::ReadImg已经输出正确的方向)
  • 偏移计算:
cpp 复制代码
int topRow = (int)((overallMaxY - src->m_RTY) / gsd + 0.5);
int offsetX = (int)((src->m_LBX - overallMinX) / gsd + 0.5);

写入循环:

cpp 复制代码
for (int y = 0; y < srcHeight; ++y) {
    int dstY = topRow + y;
    memcpy(dstBuffer + (dstY * mergeWidth + offsetX) * nBands,
           srcData + y * srcWidth * nBands,
           srcWidth * nBands);
}

合并成功后,用 MapCanvas::ReadImg 显示临时文件(此处插入合并前后对比图)。

四、核心功能二:初始拼接线可视化

拼接线是算法演示的核心视觉元素。我先整了个简单的直角折线作为初始拼接线,后面再替换成Dijkstra优化路径。

4.1 计算重叠区矩形

在合并图像里,左图和右图各占一个矩形区域,它们的交集就是重叠区:

cpp 复制代码
QRectF leftRect(leftOffsetX, leftTopRow, leftWidth, leftHeight);
QRectF rightRect(rightOffsetX, rightTopRow, rightWidth, rightHeight);
QRectF overlapRect = leftRect.intersected(rightRect);

4.2 绘制直角折线

MapCanvas 里加了成员 m_overlapRectm_seamItem,并提供 setOverlapRect() 方法。绘制函数 ShowHideSeam() 生成从重叠区左上角 → 右上角 → 右下角的折线:

cpp 复制代码
void MapCanvas::ShowHideSeam()
{
    if (m_seamVisible) {
        // 隐藏
        if (m_seamItem) {
            scene()->removeItem(m_seamItem);
            delete m_seamItem;
            m_seamItem = nullptr;
        }
        m_seamVisible = false;
    } else {
        if (m_overlapRect.isNull()) return;
        QPainterPath path;
        path.moveTo(m_overlapRect.topLeft());
        path.lineTo(m_overlapRect.topRight());
        path.lineTo(m_overlapRect.bottomRight());
        QGraphicsPathItem* item = new QGraphicsPathItem(path);
        QPen pen(Qt::red);
        pen.setWidth(3);
        item->setPen(pen);
        scene()->addItem(item);
        m_seamItem = item;
        m_seamVisible = true;
    }
}

4.3 与菜单联动

主窗口构造函数里连接信号槽:

cpp 复制代码
connect(ui.actionMosaic_Line, SIGNAL(triggered()), myMap, SLOT(ShowHideSeam()));

合并完成后自动显示:

cpp 复制代码
myMap->ReadImg(tempFile);
myMap->ShowHideSeam();   // 默认显示拼接线

效果如下(此处插入显示/隐藏动图)。

五、工程化细节:大图性能优化

5.1 大图像采样显示

原来的 MapCanvas 会一次性读全图,1GB的JPG直接内存不足。我加了个采样控制宏:

cpp 复制代码
#define MAX_IMAGE_PIXELS (4096 * 4096)   // 约1600万像素

ShowImg 里计算采样比例:

cpp 复制代码
double sampleRatio = 1.0;
double totalPixels = (double)imgWidth * imgHeight;
if (totalPixels > MAX_IMAGE_PIXELS) {
    sampleRatio = sqrt(MAX_IMAGE_PIXELS / totalPixels);
}
int iScaleWidth = (int)(imgWidth * sampleRatio);
int iScaleHeight = (int)(imgHeight * sampleRatio);

这样一来,原始影像再大,内存占用也始终可控(此处插入大图加载流畅动图)。

5.2 漫游流畅化

原项目用中键拖拽手动操作滚动条,卡顿明显。我改成了Qt原生左键拖拽:

cpp 复制代码
setDragMode(QGraphicsView::ScrollHandDrag);

删掉 mousePressEventmouseMoveEventmouseReleaseEvent 的重写,缩放用 scale() 而不是 setMatrix()。改完之后拖拽顺滑多了。

5.3 窗口最大化

main.cpp 里的 w.show() 改成 w.showMaximized(),注意在 UIMosaicSeamTool 构造函数里不要 再调 showMaximized(),否则会被后面的 setCentralWidget 覆盖掉。

六、项目结构一览

复制代码
MosaicSeamTool/
├── MapCanvas.h/cpp        # 显示核心(继承QGraphicsView)
├── UIMosaicSeamTool.h/cpp # 主窗口,处理菜单和两图合并
├── TwoImagesDialog.h/cpp  # 选择左右图的对话框
├── GdalUser.h/cpp         # GDAL封装类(读写、地理坐标)
├── main.cpp               # 入口
└── gdal-1900-x64/         # GDAL依赖库

关键类职责:

  • MapCanvas:影像显示、缩放、漫游、拼接线叠加
  • CGdalUser:GDAL底层读写、地理变换、16位影像拉伸
  • UIMosaicSeamTool:业务逻辑,两图合并,协调显示

七、下一步工作

当前版本已经实现了:

  • 两图自动合并
  • 重叠区识别与直角折线显示
  • 基础交互(缩放、漫游、隐藏/显示拼接线)

后面打算重点搞:

  1. 权重图构建:用OpenCV算重叠区的颜色差异和梯度差异
  2. Dijkstra最短路径搜索:找到最优拼接线,替换掉现在的直角折线
  3. 色彩一致性处理:减少拼接缝处的色差

到时候再更一篇博客,分享算法细节和优化效果。

八、致谢与引用

本项目的基础显示框架来自CSDN博主 Jacory 2014年发的《Qt + GDAL遥感图像基本浏览功能实现》[1],必须感谢一下!他的代码结构清晰,给我省了不少事。

同时也感谢GDAL、Qt、OpenCV开源社区的大佬们。

九、源码与资源

项目还在开发中,完整源码暂时不公开。核心算法片段文中已经给了,读者可以照着实现。有问题欢迎评论区交流。

开发环境一键配置清单:

  • VS2015 (VC14)
  • Qt 5.14.2 (MSVC2015_64)
  • GDAL 3.0.2 (GISInternals预编译包)
  • OpenCV 4.5.5 (官方预编译版)

参考文献

1\] Jacory. Qt + GDAL遥感图像基本浏览功能实现. CSDN, 2014. https://blog.csdn.net/deirjie/article/details/37872743 *** ** * ** *** > 本文为个人技术总结,欢迎转载,请注明出处。如果觉得有帮助,请点赞支持一下😊

相关推荐
xwz小王子2 小时前
智元发布 GO-2:动作空间推理 + 全生命周期闭环,让机器人稳定可靠落地
开发语言·golang·机器人
charlie1145141912 小时前
通用GUI编程技术——图形渲染实战(二十八)——图像格式与编解码:PNG/JPEG全掌握
开发语言·c++·windows·学习·图形渲染·win32
ZC跨境爬虫2 小时前
海南大学交友平台登录页开发实战day4(解决python传输并读取登录信息的问题)
开发语言·前端·python·flask·html
wjs20242 小时前
SQL LEN() 函数详解
开发语言
姓刘的哦2 小时前
Qt自定义控件
开发语言·qt
Ricky_Theseus2 小时前
C++静态库
开发语言·c++
SuperEugene2 小时前
Python 异步 async/await:为什么 AI 框架大量使用?| 基础篇
开发语言·人工智能·python
SMF19192 小时前
【uv】Python包管理器uv安装和应用
开发语言·python·uv
Lyyaoo.2 小时前
【JAVA基础面经】String、StringBuffer、StringBuilder
java·开发语言