計與實現(xiàn))
簡介在圖形界面開發(fā)中交互式繪圖與標注是常見的需求而Qt提供的Graphics View框架為處理復(fù)雜圖形場景和用戶交互提供了優(yōu)雅的解決方案。該框架通過視圖、場景與圖形項的三層結(jié)構(gòu)將坐標變換、碰撞檢測、選中事件等底層邏輯封裝起來極大地提升了開發(fā)效率和可擴展性。坐標映射是標注工具的核心理解視圖坐標、場景坐標與圖像原始坐標的轉(zhuǎn)換關(guān)系才能確保標注數(shù)據(jù)的準確性。Graphics View的技術(shù)價值在于它將每個標注對象建模為獨立的圖形項支持靈活的樣式定制和交互響應(yīng)廣泛應(yīng)用于圖片標注、地圖編輯、CAD等場景。本文基于Qt 5和C詳細講解如何使用QGraphicsView構(gòu)建一個功能完備的圖片標注工具涵蓋坐標變換、圖形項封裝、數(shù)據(jù)序列化及常見問題排查幫助開發(fā)者快速掌握這一實用技術(shù)。2. 為什么選擇 QGraphics View 而不是直接在 QWidget 上畫先說結(jié)論用 QGraphics View 做圖片標注工具尤其是要做矩形框、多邊形、點選這類交互式標注幾乎是首選架構(gòu)。為什么不直接在 QWidget 的 paintEvent 里畫原因很簡單可擴展性和交互能力差太多。直接繪制方案每個標注框都要自己維護坐標、自己處理鼠標命中檢測、自己管理重繪區(qū)域標注數(shù)量一多代碼就亂成了麻。而 QGraphics View 體系下每個標注就是一個 QGraphicsItem 對象它自帶了坐標變換、碰撞檢測、選中事件、層級關(guān)系這些能力如果全都自己造輪子一個月都寫不完。2.1 坐標變換是標注工具的靈魂標注工具里最容易翻車的點就是坐標變換。圖片標注這個場景里有三套坐標體系一是視圖坐標就是 QGraphicsView 上的像素坐標鼠標事件返回的 event-pos() 是這一套二是場景坐標就是 QGraphicsScene 里的坐標所有圖形項都生活在這一層三是圖片原始坐標也就是要寫進標注文件里的像素坐標。Qt 提供了 mapToScene 和 mapFromScene 這兩組方法作用是把鼠標的視圖坐標轉(zhuǎn)換到場景坐標。但注意這還不能直接存盤。因為圖片被縮放、平移之后場景坐標不等于圖片坐標。正確做法是拿標注框的場景坐標去跟圖片 item 的坐標做一次映射。我給標注矩形加了 getRectInImageCoords 方法返回的是相對圖片左上角的坐標。這個值是最終要寫入 JSON 文件的數(shù)據(jù)。2.2 圖形項的設(shè)計直接決定后期維護成本標注矩形我用了 QGraphicsRectItem但沒讓它直接裸奔而是做了兩層封裝第一層是 AnnotationRectItem負責繪制樣式邊的粗細、顏色、高亮效果、尺寸約束第二層是 AnnotationData負責純數(shù)據(jù)坐標、類別標簽。這樣 UI 和數(shù)據(jù)的邊界非常清晰。有一個經(jīng)驗值得分享一定要給 QGraphicsRectItem 開啟 setFlags否則你會發(fā)現(xiàn)標注框無法被鼠標選中更別提拖動了。需要設(shè)置的標志位至少包括ItemIsSelectable、ItemIsMovable、ItemIsFocusable。第一個管選中第二個管拖動第三個管接收鍵盤事件比如按 Delete 鍵刪除標注框就是靠它。如果加了 ItemIsFocusable 之后鼠標滾輪縮放事件會失靈——因為焦點在 item 上滾輪事件被 item 吃掉了。解決方式是在 MainWindow 的 eventFilter 里做處理或者重寫 item 的 wheelEvent 直接忽略掉。2.3 標注數(shù)據(jù)的序列化要有遠見標注數(shù)據(jù)我選了 JSON 格式?jīng)]有用自定義的二進制格式原因很簡單JSON 可讀性高、調(diào)試方便、和其他工具對接也容易。Qt 自帶的 QJsonDocument 完全夠用不需要引入第三方庫。標注結(jié)果的 JSON 結(jié)構(gòu)我一開始就設(shè)計得很寬每個標注矩形包含label類別的字符串標識className類別的顯示名比如carx、y、width、height圖片原始像素坐標浮點數(shù)可選字段confidence置信度、group_id實例分組、is_hard困難樣本標記預(yù)留這些字段的代價幾乎為零但等你要做二次開發(fā)時就知道當初多寫了一個字段有多香了。3. 代碼實現(xiàn)環(huán)境配置、函數(shù)設(shè)計、關(guān)鍵代碼這一節(jié)直接把代碼邏輯拆開揉碎了講。環(huán)境是 Windows 11 Qt 5.15.2 MinGW 64 位編譯器用 MinGW 而不是 MSVC原因是 Qt 開源自帶的套件就是 MinGW省去單獨裝 Visual Studio 的麻煩。如果你手頭是 Qt 6.x代碼基本兼容個別 API 有小改動。工程文件用 qmake完整 .pro 文件如下這段配置里 include 和 DEPENDPATH 是指向當前目錄add 了 widgets 模塊。整個工程的類只有 5 個沒有用 model/view 那一套重型架構(gòu)因為標注工具的復(fù)雜度用不著上 MVC。3.1 MainWindow 的主界面布局主界面的布局邏輯很簡單菜單欄 中央 QGraphicsView 狀態(tài)欄 右側(cè)浮動面板。右側(cè)面板包含類別管理區(qū)可以添加、編輯類別類別的顏色自動分配標注列表區(qū)顯示當前圖片的所有標注框選中哪一項對應(yīng)框體高亮保存區(qū)導(dǎo)出當前圖片的標注 JSON3.2 圖片加載和縮放圖片加載后會把 QPixmap 塞進一個 QGraphicsPixmapItem然后 setSceneRect 讓場景和圖片尺寸保持一致。這部分有一個坑需要專門說圖片加載后的顯示模式選擇適應(yīng)窗口而不是原始大小第一張圖看起來是鋪滿的但一旦你縮放了圖片點擊自適應(yīng)窗口時應(yīng)該調(diào)用 view-fitInView(pixmapItem, Qt::KeepAspectRatio)而不是手動去 setSceneRect。fitInView 的第二個參數(shù)選項KeepAspectRatio 是等比縮放IgnoreAspectRatio 會拉伸圖片導(dǎo)致變形標注工具的標注框坐標是基于圖片的所以必須保持等比這一點不能偷懶。狀態(tài)欄顯示的縮放比例我是從 view-transform().m11() 拿到的因為做了等比縮放m11 和 m22 相等直接取一個就行。3.3 標注矩形繪制的完整流程鼠標畫矩形的完整邏輯按下鼠標左鍵時如果當前處于畫框模式記錄場景坐標作為起點并創(chuàng)建一個寬度和高度為 0 的 QGraphicsRectItem拖動過程中實時更新矩形的右下角坐標松開鼠標時如果矩形的寬高小于 10 像素直接刪除該 item認為是誤操作這個防誤操作設(shè)置太重要了。我最初做完功能自測時經(jīng)常因為鼠標單擊導(dǎo)致生成了 1 像素寬高的矩形框后來加了 10 像素的最小限制這類問題再也沒出現(xiàn)過。3.4 標注數(shù)據(jù)導(dǎo)入導(dǎo)出導(dǎo)出邏輯把每個 QGraphicsRectItem 的 AnnotationData 序列化到 JSON。這里需要注意保存時標注坐標必須取圖片原始坐標而不能直接存場景坐標。轉(zhuǎn)換公式很簡單得到一個相對圖片的坐標imgX rect.x() - imageItem-pos().x(); imgY rect.y() - imageItem-pos().y(); 寬度和高度直接取 rect 的 width 和 height。注意這里的坐標換算必須用 item-pos() 而不能寫成 imageItem-mapFromScene(rect.topLeft())因為 QGraphicsRectItem 的 rect() 本身就是基于 item 自身的坐標系scence 坐標轉(zhuǎn) item 坐標用 mapFromScene 更準但這里換算出的是相對圖片左上角的偏移量imgX 和 imgY 直接等于偏移量。4. 核心代碼全解標注工具的骨架與五臟六腑這一部分把上面設(shè)計思路落成代碼。我分幾個文件來拆解。4.1 工程文件與入口MainWindow 的核心信號槽用 connect 連接代碼結(jié)構(gòu)上把畫框模式切換用了一個簡易狀態(tài)機管理當前模式是畫矩形、選擇圖形還是縮放圖片。這樣設(shè)計的核心好處是以后要擴展畫多邊形標注或畫點標注只需要新增對應(yīng)的繪制函數(shù)和狀態(tài)即可。4.2 AnnotationRectItem 類的實現(xiàn)AnnotationRectItem 的核心區(qū)別在于它復(fù)寫了 paint() 函數(shù)。默認的 QGraphicsRectItem 的繪制只能設(shè)置統(tǒng)一的畫筆顏色我需要一個選中時高亮的效果以及一個邊角處顯示縮放控制點的效果。判斷 item 是否被選中在 paint 函數(shù)里使用 option-state 與 QStyle::State_Selected 的按位與操作。一旦檢測到選中畫筆寬度從 2 像素加粗到 4 像素同時畫出四個角的錨點方塊。4.3 視圖與事件處理QGraphicsView 默認的事件分發(fā)在某些場景下不夠用。比如滾輪縮放圖片時我希望以鼠標所在位置為中心縮放這樣更符合人的操作直覺。默認的 QGraphicsView 縮放行為是以視圖中心為錨點的需要用 setTransformationAnchor(QGraphicsView::AnchorUnderMouse)。我在做滾動縮放時額外做了對比度限制縮放比例 minScale0.1maxScale10.0。小于 0.1 時圖片占的像素太小標注已經(jīng)失去意義大于 10.0 時像素被過度放大邊緣模糊。這個限制對標注工具的用戶體驗提升很大。4.4 拖拽標注框的邊界檢查圖片標注時允許用戶拖動已畫好的矩形框但矩形框不能拖出圖片范圍。我在 AnnotationRectItem 的 itemChange 事件里捕獲 ItemPositionChange 變化一旦發(fā)現(xiàn)新位置越界就將位置反饋為舊位置。這個邏輯看似簡單但寫錯位置會導(dǎo)致矩形抖動或者拖不動的詭異問題。5. 環(huán)境配置與編譯運行全流程這個項目用到的依賴只有 Qt 5.15.2 的 Widgets 模塊沒有任何第三方庫。但是 Qt 環(huán)境本身配置也有一坑要說明。5.1 minGW 編譯器與 Qt 版本匹配很多人下載 Qt 后編譯不過查了半天發(fā)現(xiàn)是環(huán)境變量沒配好。Qt 5.15 之后官方不再單獨提供離線 installer要用在線安裝器而且 5.15 的在線安裝包只提供商業(yè)或開源注冊版。這一步其實很多人都卡在這里不斷試錯。其實有更省心的方案直接從 Qt 鏡像站下載 Qt Online Installer登錄 Qt 賬號后選擇 5.15.2 版本把 MinGW 8.1.0 64 位的那個選項勾上。不要把 Qt 的維護工具跟編譯器混為一談MinGW 編譯器是需要單獨勾選的組件。5.2 編譯 .pro 文件進入源碼目錄終端執(zhí)行qmake mingw32-make如果沒報錯直接得到可執(zhí)行文件。這時候最小依賴的運行庫有三四個 DLL部署起來比較麻煩推薦用 windeployqt 工具去掃描依賴它會把 Qt 運行時依賴的 DLL 全部拷貝到 exe 同目錄。6. 常見問題與排查技巧實錄這個項目我在代碼調(diào)試時遇到了幾個典型問題都是花了不少時間踩坑才解決的列出來供大家參考。問題現(xiàn)象根本原因解決方法圖片加載后一片空白QGraphicsPixmapItem 未調(diào)用 setSceneRect 或者 pixmap 為 null檢查文件路徑是否正確用 QFile::exists 驗證路徑后再 setPixmap標注矩形框畫不出來沒有設(shè)置 item 的 flag或者畫筆顏色太淺導(dǎo)致看不見給 item 設(shè)置 setFlags 選中和移動標志畫筆顏色用飽和度較高的值畫矩形時造成 view 整體移動滾輪事件或拖拽事件被 QGraphicsView 截獲設(shè)置 view 的 dragMode 為 RubberBandDrag同時確保 annotation 模式下忽略鼠標移動事件矩形框旋轉(zhuǎn)后位置錯亂QGraphicsRectItem 的 rect() 和場景坐標混用所有坐標轉(zhuǎn)換統(tǒng)一用 mapToScene / mapFromScene不要混用導(dǎo)出 JSON 時中文亂碼控制臺輸出編碼不一致用 QString::toUtf8() 或 QFile 寫文件時指定編碼 UTF-86.1 中文路徑導(dǎo)致圖片加載失敗Windows 下 Qt 的 QFile 本身就是支持中文路徑的但如果你用了 ifstream 或者 C 標準庫去讀取文件遇到中文路徑就會失敗。這個項目里圖片加載全部走 QPixmap 和 QFile所以沒有這個問題。但如果把代碼移植到其他平臺或者用了第三方庫要格外注意路徑編碼。6.2 圖像縮略圖與真實圖片的比例如果你最后把導(dǎo)出的標注框映射到縮略圖上必須使用同一套坐標縮放比例不能直接拿原圖坐標除以某個整數(shù)必須精確計算 scaleX thumbWidth / imageWidthscaleY thumbHeight / imageHeight然后分別乘以 x、y、width、height再取整。很多人在這一步偷懶導(dǎo)致框位偏大幾個像素但實際上像素級的偏差對模型訓(xùn)練影響不大不過做可視化檢查時很不美觀。7. 后續(xù)擴展方向與項目總結(jié)這個標注工具做到能用很簡單做到好用卻有不少路要走。我把整理出來的擴展方向和優(yōu)先級列一下。最值得優(yōu)先做的擴展是標注類別預(yù)設(shè)管理在實際標注時類別通常有幾十種比如目標檢測里的車輛、行人、交通燈等每個類別的顏色應(yīng)該保持固定并且能一鍵切換。我在當前版本里只是把顏色隨機分配但標注工具一旦需要多人協(xié)作就需要一個統(tǒng)一的類別配置文件。其次是標注數(shù)據(jù)檢查功能標注完成后應(yīng)該提供查看所有標注框的列表視圖并支持一鍵定位到某個框、刪除誤標注、修改類別等操作。這個和導(dǎo)出 JSON 的 schema 嚴格對應(yīng)。再進一步是支持導(dǎo)入外部標注格式比如 Pascal VOC 的 XML 格式、YOLO 的 txt 格式。這樣標注工具就能無縫接入常用的訓(xùn)練流程。實際上如果只是給自己用支持一種格式就夠了但如果你需要開源出去多格式導(dǎo)入導(dǎo)出幾乎是標配。最后還有一個我實測過的小技巧定期自動保存。標注是個耗時活如果程序中途崩潰所有標注數(shù)據(jù)都會丟失。我后來給項目加了一個 QTimer 自動保存機制每 30 秒把當前圖片的標注數(shù)據(jù)寫入臨時備份文件這個改動成本極低但實用價值極大。用 Qt 和 C 實現(xiàn)圖片標注工具本質(zhì)上練習(xí)的是三件事C 對象設(shè)計能力、Qt 事件系統(tǒng)與 View/Scene 框架的理解、以及數(shù)據(jù)格式設(shè)計能力。這個項目做完本文還有配套的精品資源點擊獲取