Open Source Computer Vision Library
https://opencv.org/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1372 lines
54 KiB
1372 lines
54 KiB
// This file is part of OpenCV project. |
|
// It is subject to the license terms in the LICENSE file found in the top-level directory |
|
// of this distribution and at http://opencv.org/license.html. |
|
// |
|
// Copyright (C) 2020 Intel Corporation |
|
|
|
#include "precomp.hpp" |
|
#include "backends/onnx/gonnxbackend.hpp" |
|
|
|
#ifdef HAVE_ONNX |
|
|
|
#include "backends/onnx/dml_ep.hpp" |
|
#include "backends/onnx/coreml_ep.hpp" |
|
|
|
#include <ade/util/algorithm.hpp> // any_of |
|
#include <ade/util/zip_range.hpp> |
|
#include <opencv2/gapi/infer.hpp> |
|
#include <opencv2/gapi/own/convert.hpp> |
|
#include <opencv2/gapi/gframe.hpp> |
|
#include <codecvt> // wstring_convert |
|
|
|
#include "api/gbackend_priv.hpp" // FIXME: Make it part of Backend SDK! |
|
#include "logger.hpp" |
|
|
|
namespace { |
|
struct ONNXCallContext; |
|
} |
|
|
|
namespace cv { |
|
namespace gimpl { |
|
namespace onnx { |
|
|
|
enum TensorPosition : int { |
|
INPUT, |
|
OUTPUT |
|
}; |
|
|
|
static std::string pdims(const std::vector<int64_t> &dims) { |
|
std::stringstream ss; |
|
auto it = dims.begin(); |
|
ss << *it++; |
|
for (; it != dims.end(); ++it) { |
|
ss << '/' << *it; |
|
} |
|
return ss.str(); |
|
} |
|
|
|
struct TensorInfo { |
|
TensorInfo() = default; |
|
|
|
explicit TensorInfo(const Ort::ConstTensorTypeAndShapeInfo &info) |
|
: dims(info.GetShape()) |
|
, type(info.GetElementType()) |
|
, is_dynamic(ade::util::find(dims, -1) != dims.end()) { |
|
|
|
// Double-check if the tensor is really dynamic |
|
// Allow N to be -1 |
|
if (is_dynamic |
|
&& dims[0] == -1 |
|
&& dims.size() > 1 |
|
&& std::find(dims.begin() + 1, dims.end(), -1) == dims.end()) { |
|
|
|
GAPI_LOG_WARNING(NULL, "Promoting N=-1 to N=1 for tensor " << pdims(dims)); |
|
dims[0] = 1; |
|
is_dynamic = false; |
|
} |
|
|
|
if (!is_dynamic) { |
|
size = std::accumulate(dims.begin(), |
|
dims.end(), |
|
static_cast<int64_t>(1), |
|
std::multiplies<int64_t>()); |
|
} |
|
// Heuristic: check if the tensor is grayscale input |
|
if (dims.size() == 4u |
|
&& dims[0] == 1 |
|
&& dims[1] == 1 |
|
&& dims[2] > 1 |
|
&& dims[3] > 1) { |
|
is_grayscale = true; |
|
} |
|
} |
|
|
|
std::string name; |
|
std::vector<int64_t> dims; |
|
ONNXTensorElementDataType type = ONNX_TENSOR_ELEMENT_DATA_TYPE_UNDEFINED; |
|
int64_t size = -1; |
|
|
|
bool normalize = true; |
|
|
|
bool is_dynamic = false; |
|
bool is_grayscale = false; |
|
|
|
struct MeanStdev { |
|
cv::Scalar mean; |
|
cv::Scalar stdev; |
|
}; |
|
cv::util::optional<MeanStdev> mstd; |
|
}; |
|
|
|
using Views = std::vector<std::unique_ptr<cv::MediaFrame::View>>; |
|
|
|
class ONNXCompiled { |
|
// ONNX Resources |
|
// NOTE: Env must live with the session, otherwise segfaults. |
|
Ort::Env this_env{nullptr}; |
|
Ort::Session this_session{nullptr}; |
|
Ort::MemoryInfo this_memory_info{nullptr}; |
|
|
|
std::vector<TensorInfo> in_tensor_info; |
|
std::vector<TensorInfo> out_tensor_info; |
|
bool is_dynamic = false; |
|
bool is_postproc = false; |
|
|
|
// G-API <Net> description |
|
gapi::onnx::detail::ParamDesc params; |
|
|
|
// Input/output tensor information |
|
std::vector<TensorInfo> getTensorInfo(TensorPosition pos); |
|
|
|
// Run-time data structures |
|
std::vector<cv::Mat> in_data; |
|
std::vector<cv::Mat> out_data; |
|
|
|
void Run(const std::vector<cv::Mat>& ins, |
|
std::vector<cv::Mat>& outs); |
|
|
|
std::vector<std::string> in_names_without_const; |
|
public: |
|
explicit ONNXCompiled(const gapi::onnx::detail::ParamDesc &pp); |
|
|
|
// Extract the information about output layer #i |
|
cv::GMatDesc outMeta(int i) const; |
|
|
|
// Assign input/output info |
|
std::size_t numInputs() const { return params.num_in; } |
|
std::size_t numOutputs() const { return params.num_out; } |
|
void setInput(int i, const cv::Mat &m); |
|
void setOutput(int idx, cv::Mat &m); |
|
cv::Mat allocOutput(int i) const; |
|
// Gets exMat from input |
|
void extractMat(ONNXCallContext &ctx, const size_t in_idx, Views &views); |
|
// Extracted cv::Mat from input cv::Mat/cv::MediaFrame |
|
cv::Mat exMat; |
|
// Run with the assigned inputs/outputs |
|
void run(); |
|
}; |
|
|
|
static void addCUDAExecutionProvider(Ort::SessionOptions *session_options, |
|
const cv::gapi::onnx::ep::CUDA &cuda_ep) { |
|
OrtCUDAProviderOptions options{}; |
|
options.device_id = cuda_ep.device_id; |
|
|
|
try { |
|
session_options->AppendExecutionProvider_CUDA(options); |
|
} catch (const std::exception &e) { |
|
std::stringstream ss; |
|
ss << "ONNX Backend: Failed to enable CUDA" |
|
<< " Execution Provider: " << e.what(); |
|
cv::util::throw_error(std::runtime_error(ss.str())); |
|
} |
|
} |
|
|
|
static void addTensorRTExecutionProvider(Ort::SessionOptions *session_options, |
|
const cv::gapi::onnx::ep::TensorRT &trt_ep) { |
|
OrtTensorRTProviderOptions options{}; |
|
options.device_id = trt_ep.device_id; |
|
|
|
try { |
|
session_options->AppendExecutionProvider_TensorRT(options); |
|
} catch (const std::exception &e) { |
|
std::stringstream ss; |
|
ss << "ONNX Backend: Failed to enable TensorRT" |
|
<< " Execution Provider: " << e.what(); |
|
cv::util::throw_error(std::runtime_error(ss.str())); |
|
} |
|
} |
|
|
|
static void addOpenVINOExecutionProvider(Ort::SessionOptions *session_options, |
|
const cv::gapi::onnx::ep::OpenVINO &ov_ep) { |
|
std::unordered_map<std::string, std::string> options; |
|
|
|
try { |
|
// If the OpenVINO Execution Provider object was initialized with a parameters map, |
|
// those parameters are used directly. |
|
// Otherwise, the function constructs the options map from the individual member |
|
// variables of the OpenVINO object. |
|
if (ov_ep.params_map.empty()) { |
|
options = { |
|
{"device_type", ov_ep.device_type}, |
|
{"cache_dir", ov_ep.cache_dir}, |
|
{"num_of_threads", ov_ep.num_of_threads > 0 ? std::to_string(ov_ep.num_of_threads) : ""}, |
|
{"enable_opencl_throttling", ov_ep.enable_opencl_throttling ? "True" : "False"}, |
|
{"enable_dynamic_shapes", ov_ep.enable_dynamic_shapes ? "True" : "False"}, |
|
}; |
|
} else { |
|
options.insert(ov_ep.params_map.begin(), ov_ep.params_map.end()); |
|
} |
|
// AppendExecutionProvider function expects a const std::unordered_map as its second argument |
|
session_options->AppendExecutionProvider("OpenVINO", options); |
|
} catch (const std::exception &e) { |
|
std::stringstream ss; |
|
ss << "ONNX Backend: Failed to enable OpenVINO" |
|
<< " Execution Provider: " << e.what(); |
|
cv::util::throw_error(std::runtime_error(ss.str())); |
|
} |
|
} |
|
|
|
static void addExecutionProvider(Ort::SessionOptions *session_options, |
|
const cv::gapi::onnx::ep::EP &execution_provider) { |
|
namespace ep = cv::gapi::onnx::ep; |
|
switch (execution_provider.index()) { |
|
case ep::EP::index_of<ep::OpenVINO>(): { |
|
GAPI_LOG_INFO(NULL, "OpenVINO Execution Provider is added."); |
|
const auto &ov_ep = cv::util::get<ep::OpenVINO>(execution_provider); |
|
addOpenVINOExecutionProvider(session_options, ov_ep); |
|
break; |
|
} |
|
case ep::EP::index_of<ep::DirectML>(): { |
|
GAPI_LOG_INFO(NULL, "DirectML Execution Provider is added."); |
|
const auto &dml_ep = cv::util::get<ep::DirectML>(execution_provider); |
|
addDMLExecutionProvider(session_options, dml_ep); |
|
break; |
|
} |
|
case ep::EP::index_of<ep::CoreML>(): { |
|
GAPI_LOG_INFO(NULL, "CoreML Execution Provider is added."); |
|
const auto &coreml_ep = cv::util::get<ep::CoreML>(execution_provider); |
|
addCoreMLExecutionProvider(session_options, coreml_ep); |
|
break; |
|
} |
|
case ep::EP::index_of<ep::CUDA>(): { |
|
GAPI_LOG_INFO(NULL, "CUDA Execution Provider is added."); |
|
const auto &cuda_ep = cv::util::get<ep::CUDA>(execution_provider); |
|
addCUDAExecutionProvider(session_options, cuda_ep); |
|
break; |
|
} |
|
case ep::EP::index_of<ep::TensorRT>(): { |
|
GAPI_LOG_INFO(NULL, "TensorRT Execution Provider is added."); |
|
const auto &trt_ep = cv::util::get<ep::TensorRT>(execution_provider); |
|
addTensorRTExecutionProvider(session_options, trt_ep); |
|
break; |
|
} |
|
default: |
|
GAPI_LOG_INFO(NULL, "CPU Execution Provider is added."); |
|
break; |
|
} |
|
} |
|
|
|
} // namespace onnx |
|
} // namespace gimpl |
|
} // namespace cv |
|
|
|
namespace { |
|
|
|
inline std::vector<const char*> getCharNames(const std::vector<std::string>& names) { |
|
std::vector<const char*> out_vec; |
|
for (const auto& el : names) { |
|
out_vec.push_back(el.data()); |
|
} |
|
return out_vec; |
|
} |
|
|
|
inline int getIdxByName(const std::vector<cv::gimpl::onnx::TensorInfo>& info, const std::string& name) { |
|
// FIXME: Cache the ordering |
|
const auto it = ade::util::find_if(info, [&](const cv::gimpl::onnx::TensorInfo &i) { |
|
return i.name == name; |
|
}); |
|
GAPI_Assert(it != info.end()); |
|
return std::distance(info.begin(), it); |
|
} |
|
|
|
inline int toCV(ONNXTensorElementDataType prec) { |
|
switch (prec) { |
|
case ONNX_TENSOR_ELEMENT_DATA_TYPE_UINT8: return CV_8U; |
|
case ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT: return CV_32F; |
|
case ONNX_TENSOR_ELEMENT_DATA_TYPE_INT32: return CV_32S; |
|
case ONNX_TENSOR_ELEMENT_DATA_TYPE_INT64: return CV_32S; |
|
default: GAPI_Error("ONNX. Unsupported data type"); |
|
} |
|
return -1; |
|
} |
|
|
|
inline std::vector<int> toCV(const std::vector<int64_t> &vsz) { |
|
std::vector<int> result; |
|
result.reserve(vsz.size()); |
|
for (auto sz : vsz) { |
|
result.push_back(ade::util::checked_cast<int>(sz)); |
|
} |
|
return result; |
|
} |
|
|
|
inline void copyFromONNX(Ort::Value &v, cv::Mat& mat) { |
|
const auto info = v.GetTensorTypeAndShapeInfo(); |
|
const auto prec = info.GetElementType(); |
|
const auto shape = toCV(info.GetShape()); |
|
mat.create(shape, toCV(prec)); |
|
switch (prec) { |
|
#define HANDLE(E,T) \ |
|
case E: std::copy_n(v.GetTensorMutableData<T>(), \ |
|
mat.total(), \ |
|
reinterpret_cast<T*>(mat.data)); \ |
|
break; |
|
HANDLE(ONNX_TENSOR_ELEMENT_DATA_TYPE_UINT8, uint8_t); |
|
HANDLE(ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT, float); |
|
HANDLE(ONNX_TENSOR_ELEMENT_DATA_TYPE_INT32, int); |
|
#undef HANDLE |
|
case ONNX_TENSOR_ELEMENT_DATA_TYPE_INT64: { |
|
GAPI_LOG_WARNING(NULL, "INT64 isn't supported for cv::Mat. Conversion to INT32 is used."); |
|
cv::gimpl::convertInt64ToInt32(v.GetTensorMutableData<int64_t>(), |
|
reinterpret_cast<int*>(mat.data), |
|
mat.total()); |
|
break; |
|
} |
|
default: GAPI_Error("ONNX. Unsupported data type"); |
|
} |
|
} |
|
|
|
inline std::vector<int64_t> toORT(const cv::MatSize &sz) { |
|
return cv::to_own<int64_t>(sz); |
|
} |
|
|
|
inline void preprocess(const cv::Mat& src, |
|
const cv::gimpl::onnx::TensorInfo& ti, |
|
cv::Mat& dst) { |
|
// CNN input type |
|
const auto type = toCV(ti.type); |
|
if (src.depth() != CV_8U) { |
|
// Just pass the tensor as-is. |
|
// No layout or dimension transformations done here! |
|
// TODO: This needs to be aligned across all NN backends. |
|
const auto tensor_dims = toORT(src.size); |
|
if (tensor_dims.size() == ti.dims.size()) { |
|
for (size_t i = 0; i < ti.dims.size(); ++i) { |
|
GAPI_Assert((ti.dims[i] == -1 || ti.dims[i] == tensor_dims[i]) && |
|
"Non-U8 tensor dimensions should match with all non-dynamic NN input dimensions"); |
|
} |
|
} else { |
|
GAPI_Error("Non-U8 tensor size should match with NN input"); |
|
} |
|
|
|
dst = src; |
|
} else { |
|
// 8U input: full preprocessing path |
|
GAPI_Assert(src.depth() == CV_8U && "Only 8U data type is supported for preproc"); |
|
GAPI_Assert((ti.dims.size() == 4u || ti.dims.size() == 3u) |
|
&& "Only NCHW/NHWC/CHW/HWC layouts are supported for preproc"); |
|
|
|
const bool with_batch = ti.dims.size() == 4u ? true : false; |
|
const int shift = with_batch ? 0 : 1; |
|
|
|
GAPI_Assert((type == CV_8U || type == CV_32F) |
|
&& "Only 8U and 32F model input is supported for 8U input data"); |
|
|
|
// Assess the expected input layout |
|
const bool is_hwc = [&](int ch) { |
|
if (ti.is_grayscale) return false; // 1,1,h,w |
|
else if (ti.dims[3 - shift] == ch) return true; // ?,_,_,c |
|
else if (ti.dims[1 - shift] == ch) return false; // ?,c,_,_ |
|
else cv::util::throw_error(std::logic_error("Couldn't identify input tensor layout")); |
|
} (src.channels()); |
|
|
|
int new_c = src.channels(); |
|
cv::Mat csc; |
|
if (ti.is_grayscale && new_c == 3) { |
|
cv::cvtColor(src, csc, cv::COLOR_BGR2GRAY); |
|
new_c = 1; |
|
} else { |
|
csc = src; |
|
} |
|
|
|
// NHWC vs NCHW |
|
int new_h = -1, new_w = -1; |
|
if (ti.is_dynamic) { |
|
// reuse h & w from the input image |
|
new_h = src.rows; |
|
new_w = src.cols; |
|
} else { |
|
// take h & w from the ONNX tensor info |
|
new_h = ti.dims[(is_hwc ? 1 : 2) - shift]; |
|
new_w = ti.dims[(is_hwc ? 2 : 3) - shift]; |
|
} |
|
GAPI_Assert(new_h != -1 && new_w != -1); |
|
|
|
cv::Mat rsz, pp; |
|
cv::resize(csc, rsz, cv::Size(new_w, new_h)); |
|
if (src.depth() == CV_8U && type == CV_32F) { |
|
rsz.convertTo(pp, type, ti.normalize ? 1.f / 255 : 1.f); |
|
|
|
if (ti.mstd.has_value()) { |
|
pp -= ti.mstd->mean; |
|
pp /= ti.mstd->stdev; |
|
} |
|
} else { |
|
pp = rsz; |
|
} |
|
|
|
if (!is_hwc && new_c > 1) { |
|
// Convert to CHW |
|
dst.create(cv::Size(new_w, new_h * new_c), type); |
|
std::vector<cv::Mat> planes(new_c); |
|
for (int ch = 0; ch < new_c; ++ch) { |
|
planes[ch] = dst.rowRange(ch * new_h, (ch + 1) * new_h); |
|
} |
|
cv::split(pp, planes); |
|
} else { |
|
// Keep HWC |
|
dst = pp; |
|
} |
|
|
|
// Ensure dst is a tensor shape (not a 2D image) |
|
if (ti.is_dynamic) { |
|
// Reshape to input dimensions |
|
const std::vector<int> out_dims = is_hwc |
|
? with_batch |
|
? std::vector<int>{1, new_h, new_w, new_c} |
|
: std::vector<int>{new_h, new_w, new_c} |
|
: with_batch |
|
? std::vector<int>{1, new_c, new_h, new_w} |
|
: std::vector<int>{new_c, new_h, new_w}; |
|
dst = dst.reshape(1, out_dims); |
|
} else { |
|
// Reshape to ONNX dimensions (no -1s there!) |
|
dst = dst.reshape(1, toCV(ti.dims)); |
|
} |
|
} |
|
} |
|
|
|
void preprocess(const cv::MediaFrame::View& view, |
|
const cv::GFrameDesc& desc, |
|
cv::Mat& dst) { |
|
// This overload constructs cv::Mat from cv::MediaFrame |
|
switch (desc.fmt) { |
|
case cv::MediaFormat::BGR: { |
|
dst = cv::Mat(desc.size, CV_8UC3, view.ptr[0], view.stride[0]); |
|
break; |
|
} |
|
case cv::MediaFormat::NV12: { |
|
const auto y_plane = cv::Mat(desc.size, CV_8UC1, view.ptr[0], view.stride[0]); |
|
const auto uv_plane = cv::Mat(desc.size / 2, CV_8UC2, view.ptr[1], view.stride[1]); |
|
cvtColorTwoPlane(y_plane, uv_plane, dst, cv::COLOR_YUV2BGR_NV12); |
|
break; |
|
} |
|
default: |
|
GAPI_Error("Unsupported media format for ONNX backend"); |
|
} |
|
} |
|
|
|
template <typename T> |
|
inline Ort::Value createTensor(const Ort::MemoryInfo& memory_info, |
|
const cv::gimpl::onnx::TensorInfo& tensor_params, |
|
const cv::Mat& data) { |
|
(void) tensor_params; |
|
auto ort_dims = toORT(data.size); |
|
return Ort::Value::CreateTensor<T>(memory_info, |
|
const_cast<T*>(data.ptr<T>()), |
|
data.total(), |
|
ort_dims.data(), |
|
ort_dims.size()); |
|
} |
|
|
|
inline Ort::Value createTensor(const Ort::MemoryInfo& memory_info, |
|
const cv::gimpl::onnx::TensorInfo& tensor_params, |
|
const cv::Mat& data) { |
|
GAPI_Assert(data.isContinuous ()); |
|
switch (tensor_params.type) { |
|
case ONNX_TENSOR_ELEMENT_DATA_TYPE_UINT8: |
|
return createTensor<uint8_t>(memory_info, tensor_params, data); |
|
case ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT: |
|
return createTensor<float>(memory_info, tensor_params, data); |
|
case ONNX_TENSOR_ELEMENT_DATA_TYPE_INT32: |
|
return createTensor<int32_t>(memory_info, tensor_params, data); |
|
case ONNX_TENSOR_ELEMENT_DATA_TYPE_INT64:{ |
|
// cv::Mat does not support int64 data directly. |
|
// Following steps are applied to create an ONNX tensor from cv::Mat data: |
|
// - First create a new ONNX tensor 'i64_tensor' with data type int64_t using the default allocator |
|
// - Next retrieve a pointer to the mutable data buffer of 'i64_tensor' |
|
// - Convert the data from int32 (see toCV function) to int64 and deep copy it into 'i64_tensor' |
|
auto ort_dims = toORT(data.size); |
|
|
|
Ort::AllocatorWithDefaultOptions allocator; |
|
Ort::Value i64_tensor = Ort::Value::CreateTensor<int64_t>(allocator, |
|
ort_dims.data(), |
|
ort_dims.size()); |
|
int64_t* tensor_data = i64_tensor.GetTensorMutableData<int64_t>(); |
|
|
|
cv::gimpl::convertInt32ToInt64(data.ptr<int>(), |
|
tensor_data, |
|
data.total()); |
|
return i64_tensor; |
|
} |
|
default: |
|
GAPI_Error("ONNX. Unsupported data type"); |
|
} |
|
return Ort::Value{nullptr}; |
|
} |
|
|
|
struct ONNXUnit { |
|
static const char *name() { return "ONNXModelConfig"; } |
|
|
|
std::shared_ptr<cv::gimpl::onnx::ONNXCompiled> oc; |
|
|
|
explicit ONNXUnit(const cv::gapi::onnx::detail::ParamDesc &pp) |
|
: oc(new cv::gimpl::onnx::ONNXCompiled(pp)) { |
|
} |
|
}; |
|
|
|
struct ONNXCallContext { |
|
// Input parameters passed to an inference operation. |
|
std::vector<cv::GArg> args; |
|
cv::GShapes in_shapes; |
|
//FIXME: avoid conversion of arguments from internal representation to OpenCV one on each call |
|
//to OCV kernel. (This can be achieved by a two single time conversions in GCPUExecutable::run, |
|
//once on enter for input and output arguments, and once before return for output arguments only |
|
//FIXME: check if the above applies to this backend (taken from CPU) |
|
std::unordered_map<std::size_t, cv::GRunArgP> results; |
|
|
|
// Generic accessor API |
|
template<typename T> |
|
const T& inArg(std::size_t input) { return args.at(input).get<T>(); } |
|
|
|
// Syntax sugar |
|
const cv::Mat& inMat(std::size_t input) { |
|
return inArg<cv::Mat>(input); |
|
} |
|
|
|
const cv::MediaFrame& inFrame(std::size_t input) { |
|
return inArg<cv::MediaFrame>(input); |
|
} |
|
|
|
cv::Mat& outMatR(std::size_t output) { |
|
return *cv::util::get<cv::Mat*>(results.at(output)); |
|
} |
|
|
|
template<typename T> std::vector<T>& outVecR(std::size_t output) { // FIXME: the same issue |
|
return outVecRef(output).wref<T>(); |
|
} |
|
cv::detail::VectorRef& outVecRef(std::size_t output) { |
|
return cv::util::get<cv::detail::VectorRef>(results.at(output)); |
|
} |
|
}; |
|
|
|
struct ONNXCallable { |
|
static const char *name() { return "ONNXRequestCallable"; } |
|
using Run = std::function<void(const ONNXUnit &, ONNXCallContext &)>; |
|
Run run; |
|
}; |
|
|
|
struct KImpl { |
|
cv::gimpl::CustomMetaFunction::CM customMetaFunc; |
|
ONNXCallable::Run run; |
|
}; |
|
|
|
// FIXME: Is there a way to take a typed graph (our GModel), |
|
// and create a new typed graph _ATOP_ of that (by extending with a couple of |
|
// new types?). |
|
// Alternatively, is there a way to compose types graphs? |
|
// |
|
// If not, we need to introduce that! |
|
using GONNXModel = ade::TypedGraph |
|
< cv::gimpl::Protocol |
|
, cv::gimpl::Op |
|
, cv::gimpl::NetworkParams |
|
, cv::gimpl::CustomMetaFunction |
|
, ONNXUnit |
|
, ONNXCallable |
|
>; |
|
|
|
// FIXME: Same issue with Typed and ConstTyped |
|
using GConstGONNXModel = ade::ConstTypedGraph |
|
< cv::gimpl::Protocol |
|
, cv::gimpl::Op |
|
, cv::gimpl::NetworkParams |
|
, cv::gimpl::CustomMetaFunction |
|
, ONNXUnit |
|
, ONNXCallable |
|
>; |
|
} // anonymous namespace |
|
|
|
// GCPUExcecutable implementation ////////////////////////////////////////////// |
|
cv::gimpl::onnx::GONNXExecutable::GONNXExecutable(const ade::Graph &g, |
|
const std::vector<ade::NodeHandle> &nodes) |
|
: m_g(g), m_gm(m_g) { |
|
// FIXME: Currently this backend is capable to run a single inference node only. |
|
// Need to extend our island fusion with merge/not-to-merge decision making parametrization |
|
GConstGONNXModel iem(g); |
|
|
|
for (auto &nh : nodes) { |
|
switch (m_gm.metadata(nh).get<NodeType>().t) { |
|
case NodeType::OP: |
|
if (this_nh == nullptr) { |
|
this_nh = nh; |
|
} |
|
else { |
|
util::throw_error(std::logic_error("Multi-node inference is not supported!")); |
|
} |
|
break; |
|
|
|
case NodeType::DATA: { |
|
m_dataNodes.push_back(nh); |
|
const auto &desc = m_gm.metadata(nh).get<Data>(); |
|
if (desc.storage == Data::Storage::CONST_VAL) { |
|
util::throw_error(std::logic_error("No const data supported in backend!")); |
|
} |
|
if (desc.storage == Data::Storage::INTERNAL) { |
|
util::throw_error(std::logic_error("No internal data supported in backend!")); |
|
} |
|
break; |
|
} |
|
default: util::throw_error(std::logic_error("Unsupported NodeType")); |
|
} |
|
} |
|
} |
|
|
|
// FIXME: Document what it does |
|
cv::GArg cv::gimpl::onnx::GONNXExecutable::packArg(const cv::GArg &arg) { |
|
// No API placeholders allowed at this point |
|
// FIXME: this check has to be done somewhere in compilation stage. |
|
GAPI_Assert( arg.kind != cv::detail::ArgKind::GMAT |
|
&& arg.kind != cv::detail::ArgKind::GSCALAR |
|
&& arg.kind != cv::detail::ArgKind::GARRAY |
|
&& arg.kind != cv::detail::ArgKind::GOPAQUE |
|
&& arg.kind != cv::detail::ArgKind::GFRAME); |
|
|
|
if (arg.kind != cv::detail::ArgKind::GOBJREF) { |
|
util::throw_error(std::logic_error("Inference supports G-types ONLY!")); |
|
} |
|
GAPI_Assert(arg.kind == cv::detail::ArgKind::GOBJREF); |
|
|
|
// Wrap associated CPU object (either host or an internal one) |
|
// FIXME: object can be moved out!!! GExecutor faced that. |
|
const cv::gimpl::RcDesc &ref = arg.get<cv::gimpl::RcDesc>(); |
|
switch (ref.shape) |
|
{ |
|
case GShape::GMAT: return GArg(m_res.slot<cv::Mat>()[ref.id]); |
|
|
|
// Note: .at() is intentional for GArray as object MUST be already there |
|
// (and constructed by either bindIn/Out or resetInternal) |
|
case GShape::GARRAY: return GArg(m_res.slot<cv::detail::VectorRef>().at(ref.id)); |
|
|
|
// Note: .at() is intentional for GOpaque as object MUST be already there |
|
// (and constructed by either bindIn/Out or resetInternal) |
|
case GShape::GOPAQUE: return GArg(m_res.slot<cv::detail::OpaqueRef>().at(ref.id)); |
|
|
|
case GShape::GFRAME: return GArg(m_res.slot<cv::MediaFrame>().at(ref.id)); |
|
|
|
default: |
|
util::throw_error(std::logic_error("Unsupported GShape type")); |
|
break; |
|
} |
|
} |
|
|
|
void cv::gimpl::onnx::GONNXExecutable::run(std::vector<InObj> &&input_objs, |
|
std::vector<OutObj> &&output_objs) { |
|
// Update resources with run-time information - what this Island |
|
// has received from user (or from another Island, or mix...) |
|
// FIXME: Check input/output objects against GIsland protocol |
|
|
|
for (auto& it : input_objs) magazine::bindInArg (m_res, it.first, it.second); |
|
for (auto& it : output_objs) magazine::bindOutArg(m_res, it.first, it.second); |
|
|
|
// FIXME: Running just a single node now. |
|
// Not sure if need to support many of them, though |
|
// FIXME: Make this island-unmergeable? |
|
const auto &op = m_gm.metadata(this_nh).get<Op>(); |
|
|
|
// Initialize kernel's execution context: |
|
// - Input parameters |
|
ONNXCallContext context; |
|
context.args.reserve(op.args.size()); |
|
using namespace std::placeholders; |
|
ade::util::transform(op.args, |
|
std::back_inserter(context.args), |
|
std::bind(&GONNXExecutable::packArg, this, _1)); |
|
|
|
// NB: Need to store inputs shape to recognize GFrame/GMat |
|
context.in_shapes.reserve(op.args.size()); |
|
ade::util::transform(op.args, |
|
std::back_inserter(context.in_shapes), |
|
[](const cv::GArg& arg) { |
|
return arg.get<cv::gimpl::RcDesc>().shape; |
|
}); |
|
|
|
// - Output parameters. |
|
for (const auto &out_it : ade::util::indexed(op.outs)) { |
|
// FIXME: Can the same GArg type resolution mechanism be reused here? |
|
const auto out_port = ade::util::index(out_it); |
|
const auto out_desc = ade::util::value(out_it); |
|
context.results[out_port] = magazine::getObjPtr(m_res, out_desc); |
|
} |
|
|
|
// And now trigger the execution |
|
GConstGONNXModel giem(m_g); |
|
const auto &uu = giem.metadata(this_nh).get<ONNXUnit>(); |
|
const auto &kk = giem.metadata(this_nh).get<ONNXCallable>(); |
|
kk.run(uu, context); |
|
|
|
for (auto &it : output_objs) magazine::writeBack(m_res, it.first, it.second); |
|
} |
|
|
|
namespace cv { |
|
namespace gimpl { |
|
namespace onnx { |
|
|
|
static GraphOptimizationLevel convertToGraphOptimizationLevel(const int opt_level) { |
|
switch (opt_level) { |
|
case ORT_DISABLE_ALL: |
|
return ORT_DISABLE_ALL; |
|
case ORT_ENABLE_BASIC: |
|
return ORT_ENABLE_BASIC; |
|
case ORT_ENABLE_EXTENDED: |
|
return ORT_ENABLE_EXTENDED; |
|
case ORT_ENABLE_ALL: |
|
return ORT_ENABLE_ALL; |
|
default: |
|
if (opt_level > ORT_ENABLE_ALL) { // relax constraint |
|
return ORT_ENABLE_ALL; |
|
} |
|
else { |
|
cv::util::throw_error(std::invalid_argument("Invalid argument opt_level = " + std::to_string(opt_level))); |
|
} |
|
} |
|
} |
|
|
|
ONNXCompiled::ONNXCompiled(const gapi::onnx::detail::ParamDesc &pp) |
|
: params(pp) { |
|
// Validate input parameters before allocating any resources |
|
if (params.num_in > 1u && params.num_in != params.input_names.size()) { |
|
cv::util::throw_error(std::logic_error("Please specify input layer names for " |
|
+ params.model_path)); |
|
} |
|
if (params.num_out > 1u && params.num_out != params.output_names.size()) { |
|
cv::util::throw_error(std::logic_error("Please specify output layer names for " |
|
+ params.model_path)); |
|
} |
|
// Create and initialize the ONNX session |
|
Ort::SessionOptions session_options; |
|
GAPI_LOG_INFO(NULL, "Adding Execution Providers for \"" << pp.model_path << "\""); |
|
for (const auto &ep : pp.execution_providers) { |
|
cv::gimpl::onnx::addExecutionProvider(&session_options, ep); |
|
} |
|
|
|
for (const auto &option : pp.session_options) { |
|
session_options.AddConfigEntry(option.first.c_str(), option.second.c_str()); |
|
} |
|
|
|
if (pp.disable_mem_pattern) { |
|
session_options.DisableMemPattern(); |
|
} |
|
|
|
if (pp.opt_level.has_value()) { |
|
session_options.SetGraphOptimizationLevel(convertToGraphOptimizationLevel(pp.opt_level.value())); |
|
} |
|
this_env = Ort::Env(ORT_LOGGING_LEVEL_WARNING, ""); |
|
#ifndef _WIN32 |
|
this_session = Ort::Session(this_env, params.model_path.data(), session_options); |
|
#else |
|
std::wstring_convert<std::codecvt_utf8<wchar_t>, wchar_t> converter; |
|
std::wstring w_model_path = converter.from_bytes(params.model_path.data()); |
|
this_session = Ort::Session(this_env, w_model_path.data(), session_options); |
|
#endif |
|
this_memory_info = Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault); |
|
|
|
in_tensor_info = getTensorInfo(INPUT); |
|
out_tensor_info = getTensorInfo(OUTPUT); |
|
|
|
const auto is_dyn = [](const TensorInfo &ti) { |
|
return ti.is_dynamic; |
|
}; |
|
is_dynamic = ade::util::any_of(in_tensor_info, is_dyn) |
|
|| ade::util::any_of(out_tensor_info, is_dyn); |
|
if (is_dynamic && !params.custom_post_proc) { |
|
util::throw_error(std::logic_error("This network has dynamic shapes. " |
|
"Please provide a custom post-processing function " |
|
"(.cfgPostProc) in network parameters")); |
|
} |
|
is_postproc = (params.custom_post_proc != nullptr); |
|
|
|
// Update parameters based on session information |
|
if (params.num_in == 1u && params.input_names.empty()) { |
|
params.input_names = { in_tensor_info.front().name }; |
|
} |
|
if (params.num_out == 1u && params.output_names.empty()) { |
|
params.output_names = { out_tensor_info.front().name }; |
|
} |
|
|
|
// Validate what is supported currently |
|
GAPI_Assert(std::all_of(in_tensor_info.begin(), |
|
in_tensor_info.end(), |
|
[](const cv::gimpl::onnx::TensorInfo &p) { |
|
return p.type == ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT |
|
|| p.type == ONNX_TENSOR_ELEMENT_DATA_TYPE_UINT8 |
|
|| p.type == ONNX_TENSOR_ELEMENT_DATA_TYPE_INT32 |
|
|| p.type == ONNX_TENSOR_ELEMENT_DATA_TYPE_INT64; |
|
}) |
|
&& "Only FP32, INT32, INT64 and U8 inputs for NN are supported"); |
|
|
|
// Put mean and std in appropriate tensor params |
|
if (!params.mean.empty() || !params.stdev.empty()) { |
|
GAPI_Assert(params.mean.size() == params.stdev.size() && |
|
params.mean.size() == params.input_names.size()); |
|
for (auto idx : ade::util::iota(params.num_in)) { |
|
const auto ort_idx = getIdxByName(in_tensor_info, params.input_names[idx]); |
|
using M = TensorInfo::MeanStdev; |
|
in_tensor_info[ort_idx].mstd = util::make_optional(M{ params.mean[idx] |
|
, params.stdev[idx] }); |
|
} |
|
} |
|
|
|
// Update normalize flags for input tensors |
|
if (!params.normalize.empty()) { |
|
for (auto idx : ade::util::iota(params.num_in)) { |
|
const auto ort_idx = getIdxByName(in_tensor_info, params.input_names[idx]); |
|
in_tensor_info[ort_idx].normalize = params.normalize[idx]; |
|
} |
|
} |
|
|
|
if (!params.const_inputs.empty()) { |
|
// Form input names order without const input names |
|
in_names_without_const.clear(); |
|
std::copy_if(params.input_names.begin(), params.input_names.end(), |
|
std::back_inserter(in_names_without_const), |
|
[&](const std::string& name) { |
|
const auto it = params.const_inputs.find(name); |
|
return it == params.const_inputs.end(); |
|
}); |
|
} |
|
|
|
// Pre-allocate vectors (not buffers) for runtime info |
|
in_data.resize(params.num_in); |
|
out_data.resize(params.num_out); |
|
} |
|
|
|
std::vector<TensorInfo> ONNXCompiled::getTensorInfo(TensorPosition pos) { |
|
GAPI_Assert(pos == INPUT || pos == OUTPUT); |
|
|
|
const auto num_nodes = pos == INPUT |
|
? this_session.GetInputCount() |
|
: this_session.GetOutputCount(); |
|
|
|
std::vector<TensorInfo> tensor_info; |
|
tensor_info.reserve(num_nodes); |
|
|
|
Ort::AllocatorWithDefaultOptions allocator; |
|
for (auto i : ade::util::iota(num_nodes)) { |
|
const auto info = pos == INPUT |
|
? this_session.GetInputTypeInfo(i) |
|
: this_session.GetOutputTypeInfo(i); |
|
tensor_info.emplace_back(info.GetTensorTypeAndShapeInfo()); |
|
|
|
Ort::AllocatedStringPtr name_p = pos == INPUT |
|
? this_session.GetInputNameAllocated(i, allocator) |
|
: this_session.GetOutputNameAllocated(i, allocator); |
|
tensor_info.back().name = std::string(name_p.get()); |
|
} |
|
|
|
return tensor_info; |
|
} |
|
|
|
cv::GMatDesc ONNXCompiled::outMeta(int idx) const { |
|
if (is_dynamic || is_postproc) { |
|
GAPI_Assert(!params.out_metas.empty() |
|
&& "Metadata must be specified if NN has dynamic inputs or post-processing function is used!"); |
|
return params.out_metas.at(idx); |
|
} |
|
const auto ort_idx = getIdxByName(out_tensor_info, params.output_names[idx]); |
|
return cv::GMatDesc(toCV(out_tensor_info[ort_idx].type), |
|
toCV(out_tensor_info[ort_idx].dims)); |
|
} |
|
|
|
void ONNXCompiled::setInput(int in_idx, const cv::Mat &m) { |
|
GAPI_Assert(!m.empty() && "Input data can't be empty!"); |
|
const auto in_name = params.input_names[in_idx]; |
|
const auto ort_idx = getIdxByName(in_tensor_info, in_name); |
|
preprocess(m, in_tensor_info[ort_idx], in_data[in_idx]); |
|
} |
|
|
|
void ONNXCompiled::extractMat(ONNXCallContext &ctx, const size_t in_idx, Views& views) { |
|
switch (ctx.in_shapes[in_idx]) { |
|
case cv::GShape::GFRAME: { |
|
const cv::MediaFrame& frame = ctx.inFrame(in_idx); |
|
views.emplace_back(new cv::MediaFrame::View(frame.access(cv::MediaFrame::Access::R))); |
|
GAPI_Assert(views.size() <= numInputs()); |
|
preprocess(*views.back(), frame.desc(), exMat); |
|
break; |
|
} |
|
case cv::GShape::GMAT: { |
|
exMat = ctx.inMat(in_idx); |
|
break; |
|
} |
|
default: { |
|
GAPI_Assert("Unsupported input shape for ONNX backend"); |
|
} |
|
} |
|
} |
|
|
|
void ONNXCompiled::setOutput(int i, cv::Mat &m) |
|
{ |
|
// FIXME: No need in double-indexing? |
|
out_data[i] = m; |
|
} |
|
|
|
cv::Mat ONNXCompiled::allocOutput(int i) const { |
|
cv::Mat m; |
|
m.create(toCV(out_tensor_info[i].dims), |
|
toCV(out_tensor_info[i].type)); |
|
return m; |
|
} |
|
|
|
void ONNXCompiled::Run(const std::vector<cv::Mat>& ins, |
|
std::vector<cv::Mat>& outs) { |
|
std::vector<Ort::Value> in_tensors, out_tensors; |
|
|
|
// Layer names order for run |
|
auto input_names = (in_names_without_const.empty() && params.const_inputs.empty()) |
|
? params.input_names |
|
: in_names_without_const; |
|
// Creates tensors for unique names that don't contain constant input |
|
for (const auto it : ade::util::indexed(input_names)) { |
|
auto i = ade::util::index(it); |
|
auto in_name = ade::util::value(it); |
|
const auto idx = getIdxByName(in_tensor_info, in_name); |
|
in_tensors.emplace_back(createTensor(this_memory_info, |
|
in_tensor_info[idx], |
|
ins[i])); |
|
} |
|
|
|
for (auto &&c_in_pair : params.const_inputs) { |
|
const auto idx = getIdxByName(in_tensor_info, c_in_pair.first); |
|
in_tensors.emplace_back(createTensor(this_memory_info, |
|
in_tensor_info[idx], |
|
c_in_pair.second.first)); |
|
// Puts const input names in sequence for Run |
|
// ONNXRuntime can match input tensors to CNN inputs by names |
|
input_names.emplace_back(c_in_pair.first); |
|
} |
|
GAPI_Assert(input_names.size() == this_session.GetInputCount()); |
|
|
|
auto in_run_names = getCharNames(input_names); |
|
if (!is_dynamic && !is_postproc) { |
|
// Easy path - just run the session which is bound to G-API's |
|
// internal data |
|
for (auto i : ade::util::iota(params.output_names.size())) { |
|
out_tensors.emplace_back(createTensor(this_memory_info, |
|
out_tensor_info[i], |
|
outs[i])); |
|
} |
|
auto out_run_names = getCharNames(params.output_names); |
|
this_session.Run(Ort::RunOptions{nullptr}, |
|
in_run_names.data(), |
|
&in_tensors.front(), |
|
input_names.size(), |
|
out_run_names.data(), |
|
&out_tensors.front(), |
|
params.output_names.size()); |
|
if (out_tensor_info[0].type == ONNX_TENSOR_ELEMENT_DATA_TYPE_INT64) { |
|
// cv::Mat does not support int64 output data. |
|
// Conversion from int 64 to int 32 is carried in the copyFromONNX function |
|
// The output is written to out_mat |
|
for (auto &&iter : ade::util::zip(ade::util::toRange(out_tensors), |
|
ade::util::toRange(outs))) { |
|
auto &out_tensor = std::get<0>(iter); |
|
auto &out_mat = std::get<1>(iter); |
|
copyFromONNX(out_tensor, out_mat); |
|
} |
|
} |
|
} else { |
|
// Hard path - run session & user-defined post-processing |
|
// NOTE: use another list of output names here |
|
std::vector<const char*> out_names; |
|
out_names.reserve(outs.size()); |
|
params.names_to_remap.empty() |
|
? ade::util::transform(out_tensor_info, std::back_inserter(out_names), |
|
[] (const TensorInfo& ti) { return ti.name.c_str(); }) |
|
: ade::util::transform(params.names_to_remap, std::back_inserter(out_names), |
|
[] (const std::string& ntr) { return ntr.c_str(); }); |
|
|
|
auto outputs = this_session.Run(Ort::RunOptions{nullptr}, |
|
in_run_names.data(), |
|
&in_tensors.front(), |
|
input_names.size(), |
|
out_names.data(), |
|
out_names.size()); |
|
std::unordered_map<std::string, cv::Mat> onnx_outputs; |
|
std::unordered_map<std::string, cv::Mat> gapi_outputs; |
|
|
|
GAPI_Assert(outputs.size() == out_names.size()); |
|
// Fill in ONNX tensors |
|
for (auto &&iter : ade::util::zip(ade::util::toRange(out_names), |
|
ade::util::toRange(outputs))) { |
|
const auto &out_name = std::get<0>(iter); |
|
auto &out_tensor = std::get<1>(iter); |
|
copyFromONNX(out_tensor, onnx_outputs[out_name]); |
|
} |
|
std::vector<uint8_t *> tracked_mat_ptrs; |
|
// Fill in G-API outputs |
|
for (auto &&it: ade::util::indexed(params.output_names)) { |
|
gapi_outputs[ade::util::value(it)] = outs[ade::util::index(it)]; |
|
tracked_mat_ptrs.push_back(outs[ade::util::index(it)].data); |
|
} |
|
params.custom_post_proc(onnx_outputs, gapi_outputs); |
|
// Checking for possible data reallocation after remapping |
|
GAPI_Assert(tracked_mat_ptrs.size() == params.output_names.size()); |
|
for (auto &&iter : ade::util::zip(ade::util::toRange(tracked_mat_ptrs), |
|
ade::util::toRange(params.output_names))) { |
|
const auto &original_data = std::get<0>(iter); |
|
const auto &received_data = gapi_outputs.at(std::get<1>(iter)).data; |
|
if (original_data != received_data) { |
|
cv::util::throw_error |
|
(std::logic_error |
|
("OpenCV kernel output parameter was reallocated after remapping of ONNX output. \n" |
|
"Incorrect logic in remapping function?")); |
|
} |
|
} |
|
} |
|
} |
|
|
|
void ONNXCompiled::run() { |
|
Run(in_data, out_data); |
|
} |
|
|
|
static void checkInputMeta(const cv::GMetaArg mm) { |
|
switch (mm.index()) { |
|
case cv::GMetaArg::index_of<cv::GMatDesc>(): break; |
|
case cv::GMetaArg::index_of<cv::GFrameDesc>(): { |
|
const auto &meta = util::get<cv::GFrameDesc>(mm); |
|
switch (meta.fmt) { |
|
case cv::MediaFormat::NV12: break; |
|
case cv::MediaFormat::BGR: break; |
|
default: |
|
GAPI_Error("Unsupported media format for ONNX backend"); |
|
} break; |
|
} break; |
|
default: |
|
util::throw_error(std::runtime_error("Unsupported input meta for ONNX backend")); |
|
} |
|
} |
|
|
|
struct Infer: public cv::detail::KernelTag { |
|
using API = cv::GInferBase; |
|
static cv::gapi::GBackend backend() { return cv::gapi::onnx::backend(); } |
|
static KImpl kernel() { return KImpl{outMeta, run}; } |
|
|
|
static cv::GMetaArgs outMeta(const ade::Graph &gr, |
|
const ade::NodeHandle &nh, |
|
const cv::GMetaArgs &in_metas, |
|
const cv::GArgs &/*in_args*/) { |
|
cv::GMetaArgs result; |
|
|
|
GConstGONNXModel gm(gr); |
|
const auto &uu = gm.metadata(nh).get<ONNXUnit>(); |
|
|
|
GAPI_Assert(uu.oc->numInputs() == in_metas.size() |
|
&& "Known input layers count doesn't match input meta count"); |
|
for (auto &&mm : in_metas) { |
|
checkInputMeta(mm); |
|
} |
|
for (auto &&idx : ade::util::iota(uu.oc->numOutputs())) { |
|
result.emplace_back(uu.oc->outMeta(idx)); |
|
} |
|
return result; |
|
} |
|
|
|
static void run(const ONNXUnit &uu, ONNXCallContext &ctx) { |
|
Views views; |
|
for (auto &&idx : ade::util::iota(uu.oc->numInputs())) { |
|
uu.oc->extractMat(ctx, idx, views); |
|
uu.oc->setInput(idx, uu.oc->exMat); |
|
} |
|
for (auto &&idx : ade::util::iota(uu.oc->numOutputs())) { |
|
uu.oc->setOutput(idx, ctx.outMatR(idx)); |
|
} |
|
uu.oc->run(); |
|
} |
|
}; |
|
|
|
struct InferROI: public cv::detail::KernelTag { |
|
using API = cv::GInferROIBase; |
|
static cv::gapi::GBackend backend() { return cv::gapi::onnx::backend(); } |
|
static KImpl kernel() { return KImpl{outMeta, run}; } |
|
|
|
static cv::GMetaArgs outMeta(const ade::Graph &gr, |
|
const ade::NodeHandle &nh, |
|
const cv::GMetaArgs &in_metas, |
|
const cv::GArgs &/*in_args*/) { |
|
cv::GMetaArgs result; |
|
|
|
GConstGONNXModel gm(gr); |
|
const auto &uu = gm.metadata(nh).get<ONNXUnit>(); |
|
GAPI_Assert(1u == uu.oc->numInputs()); |
|
GAPI_Assert(2u == in_metas.size()); |
|
checkInputMeta(in_metas.at(1)); |
|
for (auto &&idx : ade::util::iota(uu.oc->numOutputs())) { |
|
result.emplace_back(uu.oc->outMeta(idx)); |
|
} |
|
return result; |
|
} |
|
|
|
static void run(const ONNXUnit &uu, ONNXCallContext &ctx) { |
|
Views views; |
|
// non-generic version for now, per the InferROI's definition |
|
GAPI_Assert(uu.oc->numInputs() == 1u); |
|
const auto& this_roi = ctx.inArg<cv::detail::OpaqueRef>(0).rref<cv::Rect>(); |
|
uu.oc->extractMat(ctx, 1, views); |
|
uu.oc->setInput(0, uu.oc->exMat(this_roi)); |
|
for (auto &&idx : ade::util::iota(uu.oc->numOutputs())) { |
|
uu.oc->setOutput(idx, ctx.outMatR(idx)); |
|
} |
|
uu.oc->run(); |
|
} |
|
}; |
|
|
|
struct InferList: public cv::detail::KernelTag { |
|
using API = cv::GInferListBase; |
|
static cv::gapi::GBackend backend() { return cv::gapi::onnx::backend(); } |
|
static KImpl kernel() { return KImpl{outMeta, run}; } |
|
|
|
static cv::GMetaArgs outMeta(const ade::Graph &gr, |
|
const ade::NodeHandle &nh, |
|
const cv::GMetaArgs &in_metas, |
|
const cv::GArgs &/*in_args*/) { |
|
GConstGONNXModel gm(gr); |
|
const auto &uu = gm.metadata(nh).get<ONNXUnit>(); |
|
|
|
// Note our input layers list order matches the API order and so |
|
// meta order. |
|
GAPI_Assert(uu.oc->numInputs() == (in_metas.size() - 1u) |
|
&& "Known input layers count doesn't match input meta count"); |
|
|
|
for (auto i : ade::util::iota(uu.oc->numInputs())) { |
|
const auto &mm = in_metas[i + 1]; |
|
checkInputMeta(mm); |
|
} |
|
|
|
// roi-list version is much easier at the moment. |
|
// All our outputs are vectors which don't have |
|
// metadata at the moment - so just create a vector of |
|
// "empty" array metadatas of the required size. |
|
return cv::GMetaArgs(uu.oc->numOutputs(), |
|
cv::GMetaArg{cv::empty_array_desc()}); |
|
} |
|
|
|
static void run(const ONNXUnit &uu, ONNXCallContext &ctx) { |
|
Views views; |
|
// non-generic version for now: |
|
// - assumes input 0 is always ROI list |
|
// - assumes all inputs/outputs are always Mats |
|
GAPI_Assert(uu.oc->numInputs() == 1); // roi list is not counted in net's inputs |
|
|
|
const auto& in_roi_vec = ctx.inArg<cv::detail::VectorRef>(0u).rref<cv::Rect>(); |
|
|
|
for (auto i : ade::util::iota(uu.oc->numOutputs())) { |
|
ctx.outVecR<cv::Mat>(i).clear(); |
|
} |
|
uu.oc->extractMat(ctx, 1, views); |
|
for (const auto &rc : in_roi_vec) { |
|
uu.oc->setInput(0, uu.oc->exMat(rc)); |
|
std::vector<cv::Mat> out_mats(uu.oc->numOutputs()); |
|
for (auto i : ade::util::iota(uu.oc->numOutputs())) { |
|
out_mats[i] = uu.oc->allocOutput(i); |
|
uu.oc->setOutput(i, out_mats[i]); |
|
} |
|
uu.oc->run(); |
|
for (auto i : ade::util::iota(uu.oc->numOutputs())) { |
|
std::vector<cv::Mat> &out_vec = ctx.outVecR<cv::Mat>(i); |
|
out_vec.push_back(std::move(out_mats[i])); |
|
} |
|
} |
|
} |
|
}; |
|
|
|
struct InferList2: public cv::detail::KernelTag { |
|
using API = cv::GInferList2Base; |
|
static cv::gapi::GBackend backend() { return cv::gapi::onnx::backend(); } |
|
static KImpl kernel() { return KImpl{outMeta, run}; } |
|
|
|
static cv::GMetaArgs outMeta(const ade::Graph &gr, |
|
const ade::NodeHandle &nh, |
|
const cv::GMetaArgs &in_metas, |
|
const cv::GArgs &/*in_args*/) { |
|
|
|
GConstGONNXModel gm(gr); |
|
const auto &uu = gm.metadata(nh).get<ONNXUnit>(); |
|
|
|
// Note our input layers list order matches the API order and so |
|
// meta order. |
|
GAPI_Assert(uu.oc->numInputs() == (in_metas.size() - 1u) |
|
&& "Known input layers count doesn't match input meta count"); |
|
|
|
// In contrast to InferList, the InferList2 has only one |
|
// "full-frame" image argument, and all the rest are arrays of |
|
// ether ROI or blobs. So here we set the 0th arg image format |
|
// to all inputs which are ROI-based (skipping the |
|
// "blob"-based ones) |
|
// FIXME: this is filtering not done, actually! GArrayDesc has |
|
// no hint for type! |
|
const auto &mm_0 = in_metas[0u]; |
|
switch (in_metas[0u].index()) { |
|
case cv::GMetaArg::index_of<cv::GMatDesc>(): { |
|
const auto &meta_0 = util::get<cv::GMatDesc>(mm_0); |
|
GAPI_Assert( !meta_0.isND() |
|
&& !meta_0.planar |
|
&& "Only images are supported as the 0th argument"); |
|
break; |
|
} |
|
case cv::GMetaArg::index_of<cv::GFrameDesc>(): { |
|
const auto &meta_0 = util::get<cv::GFrameDesc>(mm_0); |
|
GAPI_Assert( (meta_0.fmt == cv::MediaFormat::BGR) |
|
|| (meta_0.fmt == cv::MediaFormat::NV12)); |
|
GAPI_Assert((meta_0.size.height !=0) && (meta_0.size.width !=0)); |
|
break; |
|
} |
|
default: |
|
util::throw_error(std::runtime_error("Unsupported input meta for ONNX backend")); |
|
} |
|
if (util::holds_alternative<cv::GMatDesc>(mm_0)) { |
|
const auto &meta_0 = util::get<cv::GMatDesc>(mm_0); |
|
GAPI_Assert( !meta_0.isND() |
|
&& !meta_0.planar |
|
&& "Only images are supported as the 0th argument"); |
|
} |
|
for (auto i : ade::util::iota(uu.oc->numInputs())) { |
|
const auto &mm = in_metas[i + 1]; |
|
GAPI_Assert(util::holds_alternative<cv::GArrayDesc>(mm) |
|
&& "Non-array inputs are not supported"); |
|
} |
|
|
|
// roi-list version is much easier at the moment. |
|
// All our outputs are vectors which don't have |
|
// metadata at the moment - so just create a vector of |
|
// "empty" array metadatas of the required size. |
|
return cv::GMetaArgs(uu.oc->numOutputs(), |
|
cv::GMetaArg{cv::empty_array_desc()}); |
|
} |
|
|
|
static void run(const ONNXUnit &uu, ONNXCallContext &ctx) { |
|
Views views; |
|
GAPI_Assert(ctx.args.size() > 1u |
|
&& "This operation must have at least two arguments"); |
|
uu.oc->extractMat(ctx, 0, views); |
|
// Since we do a ROI list inference, always assume our input buffer is image |
|
// Take the next argument, which must be vector (of any kind). |
|
// Use this only to obtain the ROI list size (sizes of all |
|
// other vectors must be equal to this one) |
|
const auto list_size = ctx.inArg<cv::detail::VectorRef>(1u).size(); |
|
|
|
for (auto i : ade::util::iota(uu.oc->numOutputs())) { |
|
ctx.outVecR<cv::Mat>(i).clear(); |
|
} |
|
// For every ROI in the list {{{ |
|
for (const auto &list_idx : ade::util::iota(list_size)) { |
|
std::vector<Ort::Value> in_tensors, out_tensors; |
|
std::vector<cv::Mat> in_mats(uu.oc->numInputs()); |
|
// For every input of the net {{{ |
|
for (auto in_idx : ade::util::iota(uu.oc->numInputs())) { |
|
const auto &this_vec = ctx.inArg<cv::detail::VectorRef>(in_idx+1u); |
|
GAPI_Assert(this_vec.size() == list_size); |
|
// Prepare input {{{ |
|
// FIXME: Terrible run-time logic based on RTTI! |
|
// FIXME: Will never work on non-RTTI systems! |
|
// FIXME: Need to replace with a static type tags |
|
// (like with serialization) instead! |
|
if (this_vec.holds<cv::Rect>()) { |
|
// ROI case - create an ROI blob |
|
const auto &vec = this_vec.rref<cv::Rect>(); |
|
uu.oc->setInput(in_idx, uu.oc->exMat(vec[list_idx])); |
|
} else if (this_vec.holds<cv::Mat>()) { |
|
// Mat case - create a regular blob |
|
// FIXME: NOW Assume Mats are always BLOBS (not |
|
// images) |
|
const auto &vec = this_vec.rref<cv::Mat>(); |
|
uu.oc->setInput(in_idx, vec[list_idx]); |
|
} else { |
|
GAPI_Error("Only Rect and Mat types are supported for infer list 2!"); |
|
} |
|
// }}} (Prepare input) |
|
} // }}} (For every input of the net) |
|
|
|
std::vector<cv::Mat> out_mats(uu.oc->numOutputs()); |
|
for (auto i : ade::util::iota(uu.oc->numOutputs())) { |
|
out_mats[i] = uu.oc->allocOutput(i); |
|
uu.oc->setOutput(i, out_mats[i]); |
|
} |
|
uu.oc->run(); |
|
|
|
for (auto i : ade::util::iota(uu.oc->numOutputs())) { |
|
std::vector<cv::Mat> &out_vec = ctx.outVecR<cv::Mat>(i); |
|
out_vec.push_back(std::move(out_mats[i])); |
|
} |
|
} // }}} (For every ROI in the list) |
|
} |
|
}; |
|
|
|
} // namespace onnx |
|
} // namespace gapi |
|
} // namespace cv |
|
|
|
namespace { |
|
class GONNXBackendImpl final: public cv::gapi::GBackend::Priv { |
|
virtual void unpackKernel(ade::Graph &gr, |
|
const ade::NodeHandle &nh, |
|
const cv::GKernelImpl &ii) override { |
|
using namespace cv::gimpl; |
|
// FIXME: Introduce a DNNBackend interface which'd specify |
|
// the framework for this??? |
|
GONNXModel gm(gr); |
|
auto &np = gm.metadata(nh).get<NetworkParams>(); |
|
auto &pp = cv::util::any_cast<cv::gapi::onnx::detail::ParamDesc>(np.opaque); |
|
const auto &ki = cv::util::any_cast<KImpl>(ii.opaque); |
|
|
|
GModel::Graph model(gr); |
|
auto& op = model.metadata(nh).get<Op>(); |
|
if (pp.is_generic) { |
|
auto& info = cv::util::any_cast<cv::detail::InOutInfo>(op.params); |
|
|
|
for (const auto& layer_name : info.in_names) |
|
{ |
|
pp.input_names.push_back(layer_name); |
|
if (!pp.generic_mstd.empty()) { |
|
const auto &ms = pp.generic_mstd.at(layer_name); |
|
pp.mean.push_back(ms.first); |
|
pp.stdev.push_back(ms.second); |
|
} |
|
if (!pp.generic_norm.empty()) { |
|
pp.normalize.push_back(pp.generic_norm.at(layer_name)); |
|
} |
|
} |
|
pp.num_in = info.in_names.size(); |
|
|
|
// Incorporate extra parameters associated with input layer names |
|
// FIXME(DM): The current form assumes ALL input layers require |
|
// this information, this is obviously not correct |
|
|
|
for (const auto& a : info.out_names) |
|
{ |
|
pp.output_names.push_back(a); |
|
} |
|
pp.num_out = info.out_names.size(); |
|
} // if(is_generic) -- note, the structure is already filled at the user |
|
// end when a non-generic Params are used |
|
|
|
gm.metadata(nh).set(ONNXUnit{pp}); |
|
gm.metadata(nh).set(ONNXCallable{ki.run}); |
|
gm.metadata(nh).set(CustomMetaFunction{ki.customMetaFunc}); |
|
} |
|
|
|
virtual EPtr compile(const ade::Graph &graph, |
|
const cv::GCompileArgs &, |
|
const std::vector<ade::NodeHandle> &nodes) const override { |
|
return EPtr{new cv::gimpl::onnx::GONNXExecutable(graph, nodes)}; |
|
} |
|
|
|
virtual cv::GKernelPackage auxiliaryKernels() const override { |
|
return cv::gapi::kernels< cv::gimpl::onnx::Infer |
|
, cv::gimpl::onnx::InferROI |
|
, cv::gimpl::onnx::InferList |
|
, cv::gimpl::onnx::InferList2 |
|
>(); |
|
} |
|
}; |
|
} |
|
|
|
cv::gapi::GBackend cv::gapi::onnx::backend() { |
|
static cv::gapi::GBackend this_backend(std::make_shared<GONNXBackendImpl>()); |
|
return this_backend; |
|
} |
|
#else // HAVE_ONNX |
|
|
|
cv::gapi::GBackend cv::gapi::onnx::backend() { |
|
// Still provide this symbol to avoid linking issues |
|
util::throw_error(std::runtime_error("G-API has been compiled without ONNX support")); |
|
} |
|
#endif // HAVE_ONNX
|
|
|