遥感影像拼接线优化工具:基于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:像素尺寸
合并步骤大概这样:
- 计算两幅影像的整体地理范围
(overallMinX, overallMaxX, overallMinY, overallMaxY) - 计算合并后图像的像素尺寸
mergeWidth, mergeHeight - 创建临时 GeoTIFF 文件
- 按地理坐标把两幅影像写到对应位置(重叠区后写的覆盖先写的)
关键难点: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_overlapRect 和 m_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);
删掉 mousePressEvent、mouseMoveEvent、mouseReleaseEvent 的重写,缩放用 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:业务逻辑,两图合并,协调显示
七、下一步工作
当前版本已经实现了:
- 两图自动合并
- 重叠区识别与直角折线显示
- 基础交互(缩放、漫游、隐藏/显示拼接线)
后面打算重点搞:
- 权重图构建:用OpenCV算重叠区的颜色差异和梯度差异
- Dijkstra最短路径搜索:找到最优拼接线,替换掉现在的直角折线
- 色彩一致性处理:减少拼接缝处的色差
到时候再更一篇博客,分享算法细节和优化效果。
八、致谢与引用
本项目的基础显示框架来自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 *** ** * ** *** > 本文为个人技术总结,欢迎转载,请注明出处。如果觉得有帮助,请点赞支持一下😊