博客 / 詳情

返回

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

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.