C++(Qt)-显示离线瓦片图

版权声明:
本文为原创内容,作者:[Yzi321]。
转载请注明出处:
原博主主页:https://www.cnblogs.com/Yzi321
本文链接:https://www.cnblogs.com/Yzi321/p/19269003
许可协议:CC BY 4.0

Qt版本:5.11.2
编译平台:MSVC 2017 x64

一、背景

在高分辨率图像浏览、地图渲染或工业视觉中,单张完整图像往往非常大,直接加载会导致内存占用过高甚至崩溃。因此,常采用 瓦片化(Tile-based)策略,将大图切分为小块(Tile),按需加载和渲染。

二、瓦片图的基本结构

与地图瓦片结构不同,本文瓦片图格式使用 level.x.y.ext 的格式,其中level是当前瓦片实际缩放的倍数。

如:1:1显示的最底层的level是1,上一层使用1:2的缩放的level是2,再上一层使用1:4的缩放的level是4,以此类推。

所以最高层的level最大,也是最大的鸟瞰图,同时level也一定是2的倍数。

这次层级关系是与常见的GIS模式不用, 如果需要兼容,可以重写以下部分 即可:

MXTGraphicsTileLoader::Private::ParseLevelInfo,修改瓦片文件夹的解析流程,进行层级转化到本文的level模式

MXTGraphicsTileLoader::Private::MakeTilePath,生成瓦片文件的路径

  1. 层级(Level / Zoom Level)
    • 每个瓦片图通常有多个层级,用于支持不同缩放级别
    • 层级 N 表示最小缩放(整体缩略图)
    • 层级 1 表示最大分辨率
    • 每上升一层,瓦片分辨率 / 2,瓦片层级 × 2
  2. 瓦片坐标(x, y)
    • 每个层级的图像被切分成大小固定的瓦片(如 256×256)
    • (x, y) 作为瓦片坐标,且从(1,1)开始计算
    • 常用命名规则:level.x.y.pnglevel.x.y.tif
  3. 瓦片存储路径
    • 单文件夹全部存储
    • 如果需要分层分行存储,需要修改代码

三、核心配置结构

// 瓦片加载器配置信息
struct MXTGraphicsTileLoaderConfig {
    QString         folderPath;                         // 文件夹路径,瓦片所在根目录
    QByteArray      format = "tif";                     // 瓦片图像格式(tif/png/jpg/bmp)
    QSize           tileSize = QSize(1024, 1024);       // 每个瓦片在 scene 中显示的大小
                                                        // 影响缩放比例,可与真实瓦片不一致,实现缩放/拉伸
                                                        // 若原图512x512,设置为256x256则缩小显示
    int             ringRadius = 1;                     // 环形加载半径(视野扩展加载优化)
    int             crossLevelRadius = 1;               // 交叉层加载跨度(通常用于多层级预加载)
    int             debounceMs = 80;                    // 加载请求防抖控制(毫秒)
                                                        // 避免鼠标滚轮或平移时过多请求
    long long       maxCacheCount = 200;                // Tile 最大缓存数量(LRU 淘汰)
    int             mutexSleepTime = 200;               // 互斥锁等待超时(毫秒)
    LayerFilterMode filterMode = LayerFilterMode::Auto; // 层级过滤模式(自动/手动)
    QSet<ZLEVEL>    filterLevels;                       // 若为手动模式,这里指定要加载的层级
    bool            autoCropEdges = true;               // 自动裁剪右/下边缘黑边(不规则大图常用)
};

结构体要点解析(重点)

  • tileSize ≠ 原图分割瓦片大小
    • tileSize 控制瓦片的 显示比例
    • 可以用来让整图整体缩放、拉伸显示、甚至做分辨率适配
    • 例如想降低 GPU 压力,可把 1024×1024 的瓦片以 512×512 的方式显示
  • ringRadius 与 crossLevelRadius 是性能关键参数
    • ringRadius 用于“视野周边提前加载”
    • crossLevelRadius 用于“缩放时提前加载其他层级瓦片”
  • maxCacheCount 推荐根据显存或内存调整
    • 200 → 一般合适
    • 1000 → 内存大时能减少频繁加载
  • autoCropEdges 自动裁剪
    • 因为原图尺寸不太可能是瓦片的整数倍,这里假设会在右侧和下侧的边缘区域填充黑色像素
    • 开启后,可显示自动裁剪该黑色区域

四、关键函数

计算当前需要显示哪一个层级时,获取参数显示视图尺寸visibleRect,获取显示视图内物理显示屏的像素尺寸physicalViewportSize,去计算不同层级下显示视图尺寸在当前缩放倍数下和像素尺寸的差距,以最小值作为显示层级。

详细代码如下:

    static ZLEVEL FindClosestLevel( const QMap<ZLEVEL, TileLevelInfo>& levels,
                                    const QRectF& visibleRect,
                                    const QSize& physicalViewportSize)
    {
        ZLEVEL bestLevel = 0;
        double minDist2 = std::numeric_limits<double>::max();

        for (auto it = levels.constBegin(); it != levels.constEnd(); ++it) {
            const TileLevelInfo& info = it.value();

            // 计算当前 level 对应的 point
            double w = visibleRect.width() / info.level_;
            double h = visibleRect.height() / info.level_;
            QPointF pt(w, h);

            // 计算与 physicalViewportSize 的平方距离
            double dx = pt.x() - physicalViewportSize.width();
            double dy = pt.y() - physicalViewportSize.height();
            double dist2 = dx * dx + dy * dy;

            if (dist2 < minDist2) {
                minDist2 = dist2;
                bestLevel = it.key();
            }
        }

        return bestLevel;
    }

五、使用示例

// 创建瓦片图 Item
MXTGraphicsTilePixmapItem* item = new MXTGraphicsTilePixmapItem();

// 构建加载配置
MXTGraphicsTileLoaderConfig lConfig;
lConfig.folderPath = "D:/TEST_IMAGES";
lConfig.tileSize   = QSize(500, 500);  // 控制瓦片显示的缩放比例
lConfig.format     = "tif";
lConfig.maxCacheCount = 200;

// 初始化加载器
item->InitializeLoader(lConfig);

// 添加到 scene
view->scene()->addItem(item);

// !!! 必须绑定 view,触发视图更新瓦片缓存
item->bindView(view);

// 删除当前图片
item->ClearLoader();

// 加载新图片
item->InitializeLoader(lAnotherConfig);

六、流程图

1.初始化流程

sequenceDiagram autonumber participant User as 用户操作 participant View as QGraphicsView participant Controller as MXTGraphicsTilePixmapItem participant LevelItem as SingleLevelPixmapItem participant Loader as MXTGraphicsTileLoader participant Cache as TileCache %% --- 初始化流程 --- User->>Controller: InitializeLoader(config) Controller->>Loader: 创建 TileLoader(config) Loader->>Cache: 初始化缓存(按 maxCacheCount) Loader-->>Controller: 完成初始化 %% --- 创建多个单层 SingleLevelPixmapItem --- Controller->>Controller: 依据文件结构扫描 ZLEVEL 列表 loop 每个 ZLEVEL Controller->>LevelItem: 创建 SingleLevelPixmapItem Controller->>LevelItem: 设置 tileSize、层级值等 Controller->>Controller: 保存到内部容器 (levelItems[z]) end %% --- 绑定 View --- User->>Controller: bindView(view) Controller->>View: 注册视图绑定

2.视图拖动和缩放

sequenceDiagram autonumber participant User as 用户操作 participant View as QGraphicsView participant Item as MXTGraphicsTilePixmapItem participant LevelItem as SingleLevelPixmapItem participant Loader as MXTGraphicsTileLoader(线程) participant SyncLoader as QThread(加载线程) participant Cache as TileCache participant IO as ImageIO LevelItem->>LevelItem: paint() LevelItem->>Cache: loadTile(TileKey) Cache-->>LevelItem: 不为空就显示pixmap %% --- 用户操作触发 --- User->>View: 拖动 / 缩放 View->>Item: viewportChanged() %% --- Item 不计算层级,仅把可见区域传入 Loader --- Item->>Loader: updateViewport(visibleRect, transform) %% --- Loader 在独立线程中开始工作 --- Loader->>Loader: 根据缩放比例计算最优层级 ZLEVEL Loader->>Item: notifyLevelChanged(ZLEVEL) %% --- Item 切换成对应的 SingleLevelPixmapItem --- Item->>Item: hide all other levels Item->>LevelItem: show 当前层 Loader->>LevelItem: 通知当前层级显示的tile坐标范围 %% --- Loader 计算需要的瓦片列表 --- Loader->>Loader: 依据视口计算可见 tile 列表 Loader->>Loader: 依据 ringRadius/crossLevelRadius 扩展加载范围 Loader->>Loader: 整理可见tile到cache主链表 Loader->>Loader: 整理缓存tile到cache缓存链表(LRU 淘汰) Loader->>SyncLoader: 释放条件锁 %% --- Loader 逐个请求瓦片 --- loop 每个 TileKey SyncLoader->>Cache: find(TileKey) alt 缓存命中 SyncLoader-->>SyncLoader: continue else 缓存未命中 SyncLoader->>IO: loadTileAsync(TileKey) IO-->>SyncLoader: pixmap SyncLoader->>Cache: insert(TileKey, pixmap) end end

七、源码链接

例程环境:VS2022 Qt5.11.2 MSVC2017_x64

这是文件下载链接1(GoogleDrive)文件下载链接2(百度网盘)。(包含了演示里的两个测试用例)

优势:
代码实现了一个派生QGraphicsItem,可以直接作为一个item使用简单,不与scene和view的耦合过多
不足:
瓦片文件夹结构需要根据自己的环境重写,且定义的层级结构和当前流行的GIS结构相反

八、效果演示

九、总结

瓦片图策略核心是 分块 + 层级 + 按需加载,能够在保证大图可视化的同时控制内存占用和性能。
在工业图像处理、地图可视化、高分辨率图像浏览等场景中都非常实用。

参考链接

https://www.cnblogs.com/IntelligencePointer/p/18443664

© 原创作者:[Yzi321]
原文链接:https://www.cnblogs.com/Yzi321/p/19269003
转载请注明出处。
协议:CC BY 4.0

posted @ 2025-11-25 16:33  Yzi321  阅读(181)  评论(0)    收藏  举报