OpenCV 4.12.0
開源計算機視覺
載入中...
搜尋中...
無匹配項
核心API

G-API Kernel API

G-API 背後的核心思想是可移植性 – 使用 G-API 構建的 pipeline 必須是可移植的(或者至少能夠被移植)。這意味著,當為新平臺編譯時,它可以直接執行,或者 G-API 提供必要的工具來使其在那裡執行,而演算法本身幾乎不需要進行任何更改。

這個想法可以透過將核心介面與其實現分離來實現。一旦使用核心介面構建了 pipeline,它就變成了與實現無關的 – 實現細節(即使用哪個核心)在單獨的階段傳遞(圖編譯)。

核心實現層次結構可能如下所示

核心 API/實現層次結構示例

那麼 pipeline 本身只能用 AB 等來表達,並且在執行中選擇使用哪個實現就變成了一個外部引數。

定義核心

G-API 提供了一個宏來定義一個新的核心介面 – G_TYPED_KERNEL()

#include <opencv2/gapi.hpp>
G_TYPED_KERNEL(GFilter2D,
"org.opencv.imgproc.filters.filter2D")
{
static cv::GMatDesc // outMeta 的返回值型別
outMeta(cv::GMatDesc in , // 輸入 GMat 的描述符
int ddepth , // depth 引數
cv::Mat /* coeffs */, // (未使用)
cv::Point /* anchor */, // (未使用)
double /* scale */, // (未使用)
int /* border */, // (未使用)
cv::Scalar /* bvalue */ ) // (未使用)
{
return in.withDepth(ddepth);
}
};

這個宏是定義新型別的快捷方式。它接受三個引數來註冊一個新型別,並且需要型別主體存在(參見下文)。宏引數是

  1. 核心介面名稱 – 也用作使用此宏定義的新型別的名稱;
  2. 核心簽名 – 一個 std::function<>-like 簽名,它定義了核心的 API;
  3. 核心的唯一名稱 – 用於在系統內剝離其型別資訊時識別核心。

核心宣告可以被視為函式宣告 – 在這兩種情況下,都必須按照定義的方式使用新實體。

核心簽名定義了核心的使用語法 – 在圖構建期間它需要哪些引數。實現還可以使用此簽名將其派生到後端特定的回撥簽名中(參見下一章)。

核心可以接受任何型別的值,並且 G-API 動態型別以特殊方式處理。所有其他型別對 G-API 都是不透明的,並且按原樣傳遞給核心的 outMeta() 或執行回撥中。

核心的返回值只能是 G-API 動態型別 – cv::GMatcv::GScalarcv::GArray<T>。如果一個操作有多個輸出,它應該被包裝到一個 std::tuple<> 中(它只能包含提到的 G-API 型別)。不支援任意輸出數的操作。

一旦定義了一個核心,它就可以在帶有特殊的、G-API 提供的 "::on()" 方法的 pipeline 中使用。此方法具有與核心中定義的相同的簽名,因此以下程式碼

cv::GMat out = GFilter2D::on(/* GMat */ in,
/* int */ -1,
/* Mat */ conv_kernel_mat,
/* Point */ cv::Point(-1,-1),
/* double */ 0.,
/* int */ cv::BORDER_DEFAULT,
/* Scalar */ cv::Scalar(0));

是一個完全合法的構造。不過,這個例子有些冗長,所以通常核心宣告帶有一個 C++ 函式包裝器(“工廠方法”),它可以啟用可選引數、更緊湊的語法、Doxygen 註釋等。

int ddepth,
cv::Point anchor = cv::Point(-1,-1),
double scale = 0.,
int border = cv::BORDER_DEFAULT,
{
return GFilter2D::on(in, ddepth, k, anchor, scale, border, bval);
}

所以現在它可以像這樣使用

cv::GMat out = filter2D(in, -1, conv_kernel_mat);

額外資訊

在當前版本中,核心宣告主體(大括號內的所有內容)必須包含一個靜態函式 outMeta()。此函式建立操作的輸入和輸出元資料之間的函式依賴關係。

元資料是核心操作的資料的資訊。由於非 G-API 型別對 G-API 是不透明的,因此 G-API 僅關心 G* 資料描述符(即 cv::GMat 等的維度和格式)。

outMeta() 也是如何將核心簽名轉換為派生回撥的一個示例 – 請注意,在此示例中,outMeta() 簽名完全遵循核心簽名(在宏中定義),但不同 – 核心期望 cv::GMat,而 outMeta() 接受並返回 cv::GMatDesccv::GMat 的 G-API 結構元資料)。

outMeta() 的目的是在計算中傳播元資料資訊,從輸入到輸出,並推斷內部(中間的、臨時的)資料物件的元資料。此資訊是進一步 pipeline 最佳化、記憶體分配以及 G-API 框架在圖編譯期間執行的其他操作所必需的。

實現核心

一旦聲明瞭一個核心,它的介面就可以用於在不同的後端實現此核心的版本。這個概念自然是從面向物件程式設計的“介面/實現”習語中投影出來的:一個介面可以被多次實現,並且核心的不同實現應該可以相互替換,而不會破壞演算法(pipeline)邏輯(里氏替換原則)。

每個後端都定義了自己實現核心介面的方式。儘管如此,這種方式是常規的 – 無論外掛是什麼,它的核心實現都必須從核心介面型別“派生”。

然後,核心實現被組織成核心包。核心包作為編譯引數傳遞給 cv::GComputation::compile(),並提供一些提示給 G-API,說明如何選擇合適的核心(有關這方面的更多資訊,請參見“異構性”[待定])。

例如,前面提到的 Filter2D 在“參考”CPU(OpenCV)外掛中以這種方式實現(注意 – 這是一個簡化的形式,帶有不正確的邊界處理)

#include <opencv2/gapi/cpu/gcpukernel.hpp> // GAPI_OCV_KERNEL()
#include <opencv2/imgproc.hpp> // cv::filter2D()
GAPI_OCV_KERNEL(GCPUFilter2D, GFilter2D)
{
static void
run(const cv::Mat &in, // in - 從 GMat 派生
const int ddepth, // 不透明(按原樣傳遞)
const cv::Mat &k, // 不透明(按原樣傳遞)
const cv::Point &anchor, // 不透明(按原樣傳遞)
const double delta, // 不透明(按原樣傳遞)
const int border, // 不透明(按原樣傳遞)
const cv::Scalar &, // 不透明(按原樣傳遞)
cv::Mat &out) // out - 從 GMat 派生(返回值)
{
cv::filter2D(in, out, ddepth, k, anchor, delta, border);
}
};

請注意 CPU(OpenCV)外掛是如何轉換原始核心簽名的

  • 輸入 cv::GMat 已被替換為 cv::Mat,它儲存了底層 OpenCV 函式呼叫的實際輸入資料;
  • 輸出 cv::GMat 已被轉換為額外的輸出引數,因此 GCPUFilter2D::run() 比原始核心簽名多接受一個引數。

核心開發人員的基本直覺是不要關心 cv::Mat 物件來自哪裡,而不是原始的 cv::GMat – 只需遵循外掛定義的簽名約定即可。G-API 將在執行期間呼叫此方法並提供所有必要的資訊(並按原樣轉發原始不透明資料)。

複合核心

有時,核心只是 API 級別上的一個單獨的東西。這對於使用者來說很方便,但在特定的實現方面,最好有多個核心(一個子圖)來完成這件事。一個例子是 goodFeaturesToTrack() – 雖然在 OpenCV 後端它可能仍然是一個單獨的核心,但在 Fluid 中它變成了複合的 – Fluid 可以處理 Harris 響應計算,但不能對 STL 向量進行稀疏非極大值抑制和點提取

可以使用通用宏 GAPI_COMPOUND_KERNEL() 定義複合核心實現

#include <opencv2/gapi/gcompoundkernel.hpp> // GAPI_COMPOUND_KERNEL()
using PointArray2f = cv::GArray<cv::Point2f>;
G_TYPED_KERNEL(HarrisCorners,
<PointArray2f(cv::GMat,int,double,double,int,double)>,
"org.opencv.imgproc.harris_corner")
{
static cv::GArrayDesc outMeta(const cv::GMatDesc &,
int,
double,
double,
int,
double)
{
// G-API 中陣列沒有特殊的元資料(尚未)
}
};
// 定義形成 GoodFeatures 的 Fluid-backend-local 核心
G_TYPED_KERNEL(HarrisResponse,
<cv::GMat(cv::GMat,double,int,double)>,
"org.opencv.fluid.harris_response")
{
static cv::GMatDesc outMeta(const cv::GMatDesc &in,
double,
int,
double)
{
return in.withType(CV_32F, 1);
}
};
G_TYPED_KERNEL(ArrayNMS,
<PointArray2f(cv::GMat,int,double)>,
"org.opencv.cpu.nms_array")
{
static cv::GArrayDesc outMeta(const cv::GMatDesc &,
int,
double)
{
}
};
GAPI_COMPOUND_KERNEL(GFluidHarrisCorners, HarrisCorners)
{
static PointArray2f
expand(cv::GMat in,
int maxCorners,
double quality,
double minDist,
int blockSize,
double k)
{
cv::GMat response = HarrisResponse::on(in, quality, blockSize, k);
return ArrayNMS::on(response, maxCorners, minDist);
}
};
// 然後將 HarrisResponse 實現為 Fluid 核心,將 NMSresponse 實現為通用(OpenCV)核心
// 作為通用 (OpenCV) 核心

區分複合核心與 G-API 高階函式很重要,G-API 高階函式是一個看起來像核心的 C++ 函式,但實際上生成了一個子圖。核心區別在於,複合核心是一個實現細節,核心實現可以是複合的也可以不是(取決於後端能力),而高階函式是 G-API 術語中的“宏”,因此不能充當需要由後端實現的介面。