一、概覽:
GpuMat對應于cuda;HostMem 可以看作是一種特殊的Mat,其存儲對應cuda在主機分配的鎖頁內存,可以不經顯示download upload自動轉變成GpuMat(但是和GpuMat并無繼承關系);UMat對應于opencl的存儲 Matx指代常量Mat,編譯時即確定:InputArray則是一種代理模式。 注意,InputAray和Mat UMat GpuMat Matx等無繼承關系!!
二、然后我們通過幾個點來深入了解一下opencv為何這么設計,以及一些細節。
一、為何一些數據結構之間有時候可以轉換有時候不可以
首先要知道opencv的數據結構本質是要管理一塊存儲,也許是主機內存也許是cuda顯存 也許是opencl存儲 那么,無論是Mat Matx 還是其他數據結構,本質上都是一個header+一個數據指針,不同數據結構之間并無繼承關系。那么有一個情況需要解釋,比如HostMem和Mat同樣是主機內存,那么可以HostMem就會有從HostMem轉變為Mat的構造函數,同時因為HostMem是cuda分配的,如果是帶有deviceMapped的主機內存(opencv管這叫shared HostMem),也可以調用轉變為GpuMat的構造函數,需要強調,這些構造函數本質上是轉移了data指針并創造了一個新的header。
二、為何InputArray
不是一個基類?
對于許多OpenCV的C++開發者來說,第一次在函數簽名中遇到 cv::InputArray
、cv::OutputArray
時,心中難免會產生疑問:“這到底是什么類型?為什么我不直接傳遞 cv::Mat
?” 當我們進一步發現,Mat
、GpuMat
甚至 std::vector
都可以被傳遞給一個 InputArray
參數時,這種好奇心會變得更加強烈。
這背后,隱藏著OpenCV設計者們關于性能、靈活性和擴展性的深刻思考。本文將結合我們對OpenCV數據結構的理解,深入探討這個看似“奇怪”卻極其精妙的設計選擇。
1、舞臺上的演員們:OpenCV的數據江湖
在深入探討設計哲學之前,我們必須先認識一下舞臺上的主要“演員們”。它們的核心任務都是管理一塊內存,但這塊內存的“家”卻各不相同。
cv::Mat
: 最家喻戶曉的明星。它是一個通用的N維數組容器,主要負責管理主機(CPU)內存。它是OpenCV圖像處理的基石。cv::cuda::GpuMat
: CUDA陣營的先鋒。它專門管理在NVIDIA GPU顯存中的數據,是進行CUDA加速運算的主體。cv::cuda::HostMem
:GpuMat
的得力助手。它可以看作是一種特殊的Mat
,其數據存儲在由CUDA分配的**主機端鎖頁內存(Pinned Memory)**中。這種內存的特殊之處在于,它可以被GPU直接訪問(DMA),從而極大地加速了主機與設備之間的數據傳輸,甚至可以實現數據流的并發。cv::UMat
: OpenCL陣營的代表,透明計算的未來。它是一個更為抽象的容器,其管理的內存可能在CPU上,也可能在GPU、DSP或其他OpenCL設備上。UMat
的美妙之處在于它能根據計算上下文自動處理數據同步,對開發者隱藏了復雜的內存遷移操作。cv::Matx
: 輕量級的“便簽條”。它是一個小尺寸、固定大小的矩陣,其內存通常直接在**棧(Stack)**上分配。由于大小在編譯時就已確定,避免了堆內存分配的開銷,非常適合用于表示3D點、像素值等小型數據。std::vector
: 來自C++標準庫的“外援”。無論是std::vector<Point>
還是std::vector<float>
,它們都是OpenCV算法中常見的數據結構。
關鍵點:這些數據結構,尤其是 Mat
、GpuMat
、UMat
和 std::vector
,彼此之間并無繼承關系。它們是獨立的、為了不同目的而設計的類。
2、核心挑戰:如何讓一個函數“通吃”所有數據類型?
現在,問題來了。假設我們要寫一個函數,比如計算數組的均值。我們希望這個函數既能處理CPU上的Mat
,也能處理GPU上的GpuMat
,甚至還能處理一個std::vector<float>
。
一個遵循傳統面向對象(OOP)思路的開發者可能會立刻想到:繼承!
我們可以設計一個抽象基類 Array
,然后讓 Mat
、GpuMat
等都公有繼承自它:
// 一個看似很美的“繼承”方案(但OpenCV沒有采納)
class Array {
public:virtual ~Array() {}virtual int getRows() const = 0;// ... 其他通用接口
};class Mat : public Array { /*...*/ };
class GpuMat : public Array { /*...*/ };// 函數簽名
void calculateMean(const Array& arr);
然而,這個方案存在三個對于高性能計算庫而言幾乎是致命的缺陷。
-
性能的枷鎖——虛函數開銷:為了實現多態,基類中的函數必須是虛函數。這意味著每個對象都需要額外存儲一個虛函數表指針,并且每次函數調用都需要一次間接尋址。在像素級的海量循環中,這種微小的開銷會被無限放大,違背了OpenCV追求極致性能的初衷。
-
靈活性的噩夢——侵入式設計:這個方案最大的問題是,它要求所有被處理的類型都必須從
Array
繼承。我們不可能去修改C++標準庫,讓std::vector
繼承自我們的Array
!我們也無法讓一個C風格的原始數組指針繼承一個類。這種“侵入式”的設計會極大地限制庫的通用性。 -
穩定性的隱患——脆弱的ABI:對于一個被全球開發者使用的庫,保持二進制接口(ABI)的穩定至關重要。一旦基類
Array
的結構(如增刪虛函數)發生改變,所有依賴它的、已編譯的程序都可能需要重新編譯,這是一場災難。
3、OpenCV的答案:優雅的代理模式(Proxy Pattern)
面對上述挑戰,OpenCV的設計者們給出了一個非凡的答案:代理模式。InputArray
、OutputArray
就是這個模式的實現者。
InputArray
不是一個基類,而是一個輕量級的“代理”或“適配器”。
它本身不擁有數據,而是像一個經紀人一樣,持有對“真正”數據(Mat
, GpuMat
, vector
…)的引用或指針,并對外提供一個統一的接口。
這種設計是如何工作的呢?
模式一:轉發共同能力
當函數需要執行一個通用操作時(比如獲取尺寸),它會調用InputArray
的接口,例如arr.size()
。InputArray
內部會判斷自己當前代理的是哪位“明星”(Mat
? GpuMat
?),然后將這個調用轉發給實際對象的對應方法。
// 函數實現者視角
void myFunction(cv::InputArray arr) {// 無需關心 arr 到底是 Mat 還是 GpuMat// InputArray 會自動將調用轉發給它代理的對象的 .size() 方法cv::Size sz = arr.size(); // ...
}```這實現了多態的好處,卻沒有虛函數的性能開銷,也無需修改任何原始類。#### 模式二:直接獲取特有能力當需要執行某個特定類型才有的操作時(比如將`GpuMat`傳入一個自定義的CUDA核函數),`InputArray`也提供了一個“逃生通道”。你可以從它那里獲取到原始對象的引用。```cpp
// 函數實現者視角
void myCudaFunction(cv::InputArray arr) {// 確認代理的是GpuMat后,獲取其可寫引用cv::cuda::GpuMat& d_mat = arr.getGpuMatRef(); // 現在可以調用 GpuMat 的所有特有方法了my_cuda_kernel<<<...>>>(d_mat.ptr<float>(), ...);
}
這保證了設計的靈活性和功能的完整性,我們不會因為使用了代理而丟失對底層對象的完全控制。
結論:一場工程智慧的勝利
現在,我們可以清晰地回答最初的問題了。
OpenCV之所以不采用傳統的繼承體系,而是設計出InputArray
這樣的代理類,是為了在一套API中,同時實現三個看似矛盾的目標:
- 極致的性能:避免了虛函數帶來的開銷。
- 無與倫比的靈活性:通過非侵入式的設計,使其能夠適配
Mat
、GpuMat
、UMat
、std::vector
等眾多類型,而無需它們做出任何改變。 - 堅如磐石的穩定性:代理類本身結構穩定,易于擴展以支持新類型,而不會破壞二進制兼容性。
InputArray
的設計哲學,是典型的用組合(代理是一種組合形式)優于繼承的工程實踐。它或許在初學時帶來一絲困惑,但一旦理解其背后的深意,你便會由衷地贊嘆這種設計的優雅與強大。它不僅僅是一個技術選擇,更是OpenCV作為一個高性能、高通用性計算庫的立身之本。
InputArray除了可以作為通用接口接受不同數據結構外,還有什么作用?
兩個層面:抽象接口的轉發和具體對象的直接訪問。這兩種模式是相輔相成的。
模式一:轉發/代理 (Forwarding/Delegation) - 處理“共同能力”
當一個操作是所有或大多數數組類型(Mat
, UMat
, GpuMat
, vector
…)都應該具備的通用能力時,_OutputArray
類會為這個操作提供一個自己的成員函數。
最典型的“共同能力”就是:
- 創建/分配內存 (
create
,createSameSize
) - 釋放/清空 (
release
,clear
) - 賦值 (
setTo
,assign
,move
)
工作流程:
- 函數實現者調用
OutputArray
的方法,例如dst.create(size, type)
。 _OutputArray
內部會檢查它當前“代理”的是哪種具體對象(Mat
?GpuMat
?)。- 然后,它將這個調用**轉發(Forward)**給它所代理的那個具體對象的相應方法。
- 如果
dst
包裹的是一個Mat
,它內部會調用the_mat.create(size, type)
。 - 如果
dst
包裹的是一個GpuMat
,它內部會調用the_gpumat.create(size, type)
。
- 如果
這么做的好處是:多態性和代碼復用。函數實現者無需寫 if-else
來判斷 dst
的具體類型,只需面向 OutputArray
這個統一的抽象接口編程即可。這使得一個函數(如 cv::cvtColor
)可以無縫地同時支持 Mat
、UMat
和 GpuMat
作為輸出。
模式二:直接獲取 (Direct Access) - 處理“特有能力”
當一個操作是某個具體類(比如 GpuMat
)特有的能力,而其他類(如 Mat
)沒有這個能力時,_OutputArray
接口中就不會包含這個操作。
例如:
- 直接訪問
GpuMat
的step
成員進行指針運算。 - 調用
Mat
特有的push_back()
方法。 - 將
GpuMat
傳遞給一個需要cudaStream_t
參數的自定義CUDA核函數。
在這種情況下,函數實現者就必須先“揭開”OutputArray
的代理面紗,拿到它背后包裹的那個原始對象。
工作流程:
- 函數實現者首先需要知道或判斷
OutputArray
代理的是哪種類型。 - 然后調用
get...Ref()
方法,如dst.getMatRef()
或dst.getGpuMatRef()
,來獲取一個可寫的引用。 - 拿到這個引用后,就可以像操作一個普通的
Mat
或GpuMat
對象一樣,調用它所有特有的方法和成員。
這么做的好處是:靈活性和完整性。它提供了一個“逃生通道”,確保了即使OutputArray
的抽象接口沒有覆蓋某個功能,開發者依然可以使用具體類的全部能力,不會因為使用了代理類而丟失功能。
總結對比
行為模式 | 調用方式 | 適用場景 | 設計目的 |
---|---|---|---|
轉發/代理 | 直接調用 dst.create(...) , dst.setTo(...) 等 OutputArray 的方法。 | 處理所有數組類型都支持的通用操作。 | 抽象與多態:隱藏具體實現,讓函數可以處理多種數據類型。 |
直接獲取 | 先調用 dst.getMatRef() 等獲取具體引用,再調用該引用的特有方法。 | 處理某個特定數組類型才有的專屬操作。 | 靈活性與完整性:不限制開發者使用具體類的全部功能。 |
OpenCV 的 Input/Output
代理類設計,是一個在高度抽象(為了易用和通用)和完全控制(為了性能和功能完整性)之間取得精妙平衡的典范。