diff --git a/CMakeLists.txt b/CMakeLists.txt index 16df90c9ae..0ca33755ff 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -296,6 +296,9 @@ OCV_OPTION(WITH_INF_ENGINE "Include Intel Inference Engine support" OFF OCV_OPTION(WITH_NGRAPH "Include nGraph support" WITH_INF_ENGINE VISIBLE_IF TRUE VERIFY TARGET ngraph::ngraph) +OCV_OPTION(WITH_WEBNN "Include WebNN support" OFF + VISIBLE_IF TRUE + VERIFY HAVE_WEBNN) OCV_OPTION(WITH_JASPER "Include JPEG2K support (Jasper)" ON VISIBLE_IF NOT IOS VERIFY HAVE_JASPER) @@ -793,6 +796,11 @@ if(WITH_VULKAN) include(cmake/OpenCVDetectVulkan.cmake) endif() +# --- WebNN --- +if(WITH_WEBNN) + include(cmake/OpenCVDetectWebNN.cmake) +endif() + # --- Inference Engine --- if(WITH_INF_ENGINE) include(cmake/OpenCVDetectInferenceEngine.cmake) @@ -1624,6 +1632,15 @@ if(WITH_VULKAN OR HAVE_VULKAN) endif() endif() +if(WITH_WEBNN OR HAVE_WEBNN) + status("") + status(" WebNN:" HAVE_WEBNN THEN "YES" ELSE "NO") + if(HAVE_WEBNN AND NOT EMSCRIPTEN) + status(" Include path:" WEBNN_HEADER_DIRS THEN "${WEBNN_HEADER_DIRS}" ELSE "NO") + status(" Link libraries:" WEBNN_LIBRARIES THEN "${WEBNN_LIBRARIES}" ELSE "NO") + endif() +endif() + if(WITH_OPENCL OR HAVE_OPENCL) ocv_build_features_string(opencl_features IF HAVE_OPENCL_SVM THEN "SVM" diff --git a/cmake/OpenCVDetectWebNN.cmake b/cmake/OpenCVDetectWebNN.cmake new file mode 100644 index 0000000000..e7ee687f5f --- /dev/null +++ b/cmake/OpenCVDetectWebNN.cmake @@ -0,0 +1,49 @@ +if(NOT EMSCRIPTEN) + if(WITH_WEBNN) + ocv_check_environment_variables(WEBNN_HEADER_DIRS) + ocv_check_environment_variables(WEBNN_INCLUDE_DIRS) + ocv_check_environment_variables(WEBNN_LIBRARIES) + if(NOT DEFINED WEBNN_HEADER_DIRS) + set(WEBNN_HEADER_DIRS "$ENV{WEBNN_NATIVE_DIR}/gen/src/include") + endif() + if(NOT DEFINED WEBNN_INCLUDE_DIRS) + set(WEBNN_INCLUDE_DIRS "$ENV{WEBNN_NATIVE_DIR}/../../src/include") + endif() + if(NOT DEFINED WEBNN_LIBRARIES) + set(WEBNN_LIBRARIES "$ENV{WEBNN_NATIVE_DIR}/libwebnn_native.so;$ENV{WEBNN_NATIVE_DIR}/libwebnn_proc.so") + endif() + endif() + try_compile(VALID_WEBNN + "${OpenCV_BINARY_DIR}" + SOURCES "${OpenCV_SOURCE_DIR}/cmake/checks/webnn.cpp" + "$ENV{WEBNN_NATIVE_DIR}/gen/src/webnn/webnn_cpp.cpp" + CMAKE_FLAGS "-DINCLUDE_DIRECTORIES:STRING=${WEBNN_INCLUDE_DIRS}\;${WEBNN_HEADER_DIRS}" + "-DLINK_LIBRARIES:STRING=${WEBNN_LIBRARIES}" + OUTPUT_VARIABLE TRY_OUT + ) +else() + try_compile(VALID_WEBNN + "${OpenCV_BINARY_DIR}" + SOURCES "${OpenCV_SOURCE_DIR}/cmake/checks/webnn.cpp" + OUTPUT_VARIABLE TRY_OUT + ) +endif() + +if(NOT VALID_WEBNN) + if(NOT EMSCRIPTEN) + message(WARNING "Can't use WebNN-native") + return() + else() + message(WARNING "Can't use WebNN") + return() + endif() +else() + set(HAVE_WEBNN ON) + message(STATUS "Set HAVE_WEBNN = ${HAVE_WEBNN}") +endif() + +if(NOT EMSCRIPTEN) + message(AUTHOR_WARNING "Use WebNN-native") +else() + message(AUTHOR_WARNING "Use WebNN") +endif() \ No newline at end of file diff --git a/cmake/checks/webnn.cpp b/cmake/checks/webnn.cpp new file mode 100644 index 0000000000..f1e365272e --- /dev/null +++ b/cmake/checks/webnn.cpp @@ -0,0 +1,23 @@ +#include +#include +#ifdef __EMSCRIPTEN__ +#include +#include +#include +#else +#include +#include +#endif + + +int main(int /*argc*/, char** /*argv*/) +{ +#ifdef __EMSCRIPTEN__ + ml::Context ml_context = ml::Context(emscripten_webnn_create_context()); +#else + WebnnProcTable backendProcs = webnn_native::GetProcs(); + webnnProcSetProcs(&backendProcs); + ml::Context ml_context = ml::Context(webnn_native::CreateContext()); +#endif + return 0; +} \ No newline at end of file diff --git a/doc/js_tutorials/js_assets/js_image_classification_webnn_polyfill.html b/doc/js_tutorials/js_assets/js_image_classification_webnn_polyfill.html new file mode 100644 index 0000000000..a8910c3fd3 --- /dev/null +++ b/doc/js_tutorials/js_assets/js_image_classification_webnn_polyfill.html @@ -0,0 +1,269 @@ + + + + + + Image Classification Example + + + + + +

Image Classification Example

+

+ This tutorial shows you how to write an image classification example with OpenCV.js.
+ To try the example you should click the modelFile button(and configFile button if needed) to upload inference model. + You can find the model URLs and parameters in the model info section. + Then You should change the parameters in the first code snippet according to the uploaded model. + Finally click Try it button to see the result. You can choose any other images.
+

+ +
+
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+
+
+ canvasInput +
+
+
+ modelFile +
+
+
+ configFile +
+
+
+ +
+

+
+ +
+

Help function

+

1.The parameters for model inference which you can modify to investigate more models.

+ +

2.Main loop in which will read the image from canvas and do inference once.

+ +

3.Load labels from txt file and process it into an array.

+ +

4.Get blob from image as input for net, and standardize it with mean and std.

+ +

5.Fetch model file and save to emscripten file system once click the input button.

+ +

6.The post-processing, including softmax if needed and get the top classes from the output vector.

+ +
+ +
+

Model Info:

+
+ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/doc/js_tutorials/js_assets/webnn-electron/js_image_classification_webnn_electron.html b/doc/js_tutorials/js_assets/webnn-electron/js_image_classification_webnn_electron.html new file mode 100644 index 0000000000..0da44330c9 --- /dev/null +++ b/doc/js_tutorials/js_assets/webnn-electron/js_image_classification_webnn_electron.html @@ -0,0 +1,268 @@ + + + + + + Image Classification Example + + + + +

Image Classification Example

+

+ This tutorial shows you how to write an image classification example with OpenCV.js.
+ To try the example you should click the modelFile button(and configFile button if needed) to upload inference model. + You can find the model URLs and parameters in the model info section. + Then You should change the parameters in the first code snippet according to the uploaded model. + Finally click Try it button to see the result. You can choose any other images.
+

+ +
+
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+
+
+ canvasInput +
+
+
+ modelFile +
+
+
+ configFile +
+
+
+ +
+

+
+ +
+

Help function

+

1.The parameters for model inference which you can modify to investigate more models.

+ +

2.Main loop in which will read the image from canvas and do inference once.

+ +

3.Load labels from txt file and process it into an array.

+ +

4.Get blob from image as input for net, and standardize it with mean and std.

+ +

5.Fetch model file and save to emscripten file system once click the input button.

+ +

6.The post-processing, including softmax if needed and get the top classes from the output vector.

+ +
+ +
+

Model Info:

+
+ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/doc/js_tutorials/js_assets/webnn-electron/main.js b/doc/js_tutorials/js_assets/webnn-electron/main.js new file mode 100644 index 0000000000..b73878b07e --- /dev/null +++ b/doc/js_tutorials/js_assets/webnn-electron/main.js @@ -0,0 +1,56 @@ +// Modules to control application life and create native browser window +const {app, BrowserWindow} = require('electron') +const path = require('path') + +// Keep a global reference of the window object, if you don't, the window will +// be closed automatically when the JavaScript object is garbage collected. +let mainWindow = {} + +function createWindow() { + // Create the browser window. + mainWindow = new BrowserWindow({ + width: 1220, + height: 840, + webPreferences: { + nodeIntegration: true, + contextIsolation: false, + preload: app.getAppPath()+"/node_setup.js" + } + }) + + // Load the index.html with 'numRunsParm' to run inference multiple times. + let url = `file://${__dirname}/js_image_classification_webnn_electron.html` + const numRunsParm = '?' + process.argv[2] + mainWindow.loadURL(url + numRunsParm) + + // Emitted when the window is closed. + mainWindow.on('closed', function() { + // Dereference the window object, usually you would store windows + // in an array if your app supports multi windows, this is the time + // when you should delete the corresponding element. + mainWindow = null + }) +} + +// This method will be called when Electron has finished +// initialization and is ready to create browser windows. +// Some APIs can only be used after this event occurs. +app.on('ready', createWindow) + +// Quit when all windows are closed. +app.on('window-all-closed', function() { + // On macOS it is common for applications and their menu bar + // to stay active until the user quits explicitly with Cmd + Q + if (process.platform !== 'darwin') app.quit() +}) + +app.on( + 'activate', + function() { + // On macOS it's common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (mainWindow === null) createWindow() + }) + + // In this file you can include the rest of your app's specific main process + // code. You can also put them in separate files and require them here. diff --git a/doc/js_tutorials/js_assets/webnn-electron/node_setup.js b/doc/js_tutorials/js_assets/webnn-electron/node_setup.js new file mode 100644 index 0000000000..c269b5ce94 --- /dev/null +++ b/doc/js_tutorials/js_assets/webnn-electron/node_setup.js @@ -0,0 +1,12 @@ +const cv = require('./opencv'); +const webnn = require(process.env.WEBNN_NATIVE_DIR+'/../../node/lib/webnn'); +// navigator is undefined in node.js, but defined in electron.js. +if (global.navigator === undefined) { + global.navigator = {}; +} +global.navigator.ml = webnn.ml; +global.MLContext = webnn.MLContext +global.MLGraphBuilder = webnn.MLGraphBuilder +global.MLGraph = webnn.MLGraph +global.MLOperand = webnn.MLOperand +global.cv = cv; \ No newline at end of file diff --git a/doc/js_tutorials/js_assets/webnn-electron/package.json b/doc/js_tutorials/js_assets/webnn-electron/package.json new file mode 100644 index 0000000000..e6a258ee40 --- /dev/null +++ b/doc/js_tutorials/js_assets/webnn-electron/package.json @@ -0,0 +1,14 @@ +{ + "name": "image_classification", + "version": "0.0.1", + "description": "An Electon.js example of image_classification using webnn-native", + "main": "main.js", + "author": "WebNN-native Authors", + "license": "Apache-2.0", + "scripts": { + "start": "electron ." + }, + "dependencies": { + "electron": "^15.1.2" + } +} diff --git a/doc/js_tutorials/js_assets/webnn-electron/utils_webnn_electron.js b/doc/js_tutorials/js_assets/webnn-electron/utils_webnn_electron.js new file mode 100644 index 0000000000..2c4e981715 --- /dev/null +++ b/doc/js_tutorials/js_assets/webnn-electron/utils_webnn_electron.js @@ -0,0 +1,159 @@ +function Utils(errorOutputId) { // eslint-disable-line no-unused-vars + let self = this; + this.errorOutput = document.getElementById(errorOutputId); + + const OPENCV_URL = 'opencv.js'; + this.loadOpenCv = async function(onloadCallback) { + if (cv.getBuildInformation) + { + console.log(cv.getBuildInformation()); + onloadCallback(); + } + else + { + // WASM + if (cv instanceof Promise) { + cv = await cv; + console.log(cv.getBuildInformation()); + onloadCallback(); + } else { + cv['onRuntimeInitialized']=()=>{ + console.log(cv.getBuildInformation()); + onloadCallback(); + } + } + } + }; + + this.createFileFromUrl = function(path, url, callback) { + let request = new XMLHttpRequest(); + request.open('GET', url, true); + request.responseType = 'arraybuffer'; + request.onload = function(ev) { + if (request.readyState === 4) { + if (request.status === 200) { + let data = new Uint8Array(request.response); + cv.FS_createDataFile('/', path, data, true, false, false); + callback(); + } else { + self.printError('Failed to load ' + url + ' status: ' + request.status); + } + } + }; + request.send(); + }; + + this.loadImageToCanvas = function(url, cavansId) { + let canvas = document.getElementById(cavansId); + let ctx = canvas.getContext('2d'); + let img = new Image(); + img.crossOrigin = 'anonymous'; + img.onload = function() { + canvas.width = img.width; + canvas.height = img.height; + ctx.drawImage(img, 0, 0, img.width, img.height); + }; + img.src = url; + }; + + this.executeCode = function(textAreaId) { + try { + this.clearError(); + let code = document.getElementById(textAreaId).value; + eval(code); + } catch (err) { + this.printError(err); + } + }; + + this.clearError = function() { + this.errorOutput.innerHTML = ''; + }; + + this.printError = function(err) { + if (typeof err === 'undefined') { + err = ''; + } else if (typeof err === 'number') { + if (!isNaN(err)) { + if (typeof cv !== 'undefined') { + err = 'Exception: ' + cv.exceptionFromPtr(err).msg; + } + } + } else if (typeof err === 'string') { + let ptr = Number(err.split(' ')[0]); + if (!isNaN(ptr)) { + if (typeof cv !== 'undefined') { + err = 'Exception: ' + cv.exceptionFromPtr(ptr).msg; + } + } + } else if (err instanceof Error) { + err = err.stack.replace(/\n/g, '
'); + } + this.errorOutput.innerHTML = err; + }; + + this.loadCode = function(scriptId, textAreaId) { + let scriptNode = document.getElementById(scriptId); + let textArea = document.getElementById(textAreaId); + if (scriptNode.type !== 'text/code-snippet') { + throw Error('Unknown code snippet type'); + } + textArea.value = scriptNode.text.replace(/^\n/, ''); + }; + + this.addFileInputHandler = function(fileInputId, canvasId) { + let inputElement = document.getElementById(fileInputId); + inputElement.addEventListener('change', (e) => { + let files = e.target.files; + if (files.length > 0) { + let imgUrl = URL.createObjectURL(files[0]); + self.loadImageToCanvas(imgUrl, canvasId); + } + }, false); + }; + + function onVideoCanPlay() { + if (self.onCameraStartedCallback) { + self.onCameraStartedCallback(self.stream, self.video); + } + }; + + this.startCamera = function(resolution, callback, videoId) { + const constraints = { + 'qvga': {width: {exact: 320}, height: {exact: 240}}, + 'vga': {width: {exact: 640}, height: {exact: 480}}}; + let video = document.getElementById(videoId); + if (!video) { + video = document.createElement('video'); + } + + let videoConstraint = constraints[resolution]; + if (!videoConstraint) { + videoConstraint = true; + } + + navigator.mediaDevices.getUserMedia({video: videoConstraint, audio: false}) + .then(function(stream) { + video.srcObject = stream; + video.play(); + self.video = video; + self.stream = stream; + self.onCameraStartedCallback = callback; + video.addEventListener('canplay', onVideoCanPlay, false); + }) + .catch(function(err) { + self.printError('Camera Error: ' + err.name + ' ' + err.message); + }); + }; + + this.stopCamera = function() { + if (this.video) { + this.video.pause(); + this.video.srcObject = null; + this.video.removeEventListener('canplay', onVideoCanPlay); + } + if (this.stream) { + this.stream.getVideoTracks()[0].stop(); + } + }; +}; diff --git a/doc/js_tutorials/js_setup/js_setup/js_setup.markdown b/doc/js_tutorials/js_setup/js_setup/js_setup.markdown index ad14185a35..9927477443 100644 --- a/doc/js_tutorials/js_setup/js_setup/js_setup.markdown +++ b/doc/js_tutorials/js_setup/js_setup/js_setup.markdown @@ -145,6 +145,12 @@ Building OpenCV.js from Source python ./platforms/js/build_js.py build_js --cmake_option="-DOPENCV_EXTRA_MODULES_PATH=opencv_contrib/modules" @endcode +-# [optional] To enable WebNN backend, append `--webnn` option. + + For example: + @code{.bash} + emcmake python ./opencv/platforms/js/build_js.py build_js --webnn + @endcode Running OpenCV.js Tests --------------------------------------- diff --git a/modules/dnn/CMakeLists.txt b/modules/dnn/CMakeLists.txt index 398ed5ba48..88a8546aa0 100644 --- a/modules/dnn/CMakeLists.txt +++ b/modules/dnn/CMakeLists.txt @@ -19,6 +19,10 @@ if(OPENCV_DNN_OPENCL AND HAVE_OPENCL) add_definitions(-DCV_OCL4DNN=1) endif() +if(WITH_WEBNN AND HAVE_WEBNN) + add_definitions(-DHAVE_WEBNN=1) +endif() + ocv_option(OPENCV_DNN_CUDA "Build with CUDA support" HAVE_CUDA AND HAVE_CUBLAS @@ -142,6 +146,16 @@ if(HAVE_TENGINE) list(APPEND libs -Wl,--whole-archive ${TENGINE_LIBRARIES} -Wl,--no-whole-archive) endif() +set(webnn_srcs "") +if(NOT EMSCRIPTEN) + if(HAVE_WEBNN) + list(APPEND include_dirs ${WEBNN_HEADER_DIRS}) + list(APPEND include_dirs ${WEBNN_INCLUDE_DIRS}) + list(APPEND libs -Wl,--whole-archive ${WEBNN_LIBRARIES} -Wl,--no-whole-archive) + list(APPEND webnn_srcs $ENV{WEBNN_NATIVE_DIR}/gen/src/webnn/webnn_cpp.cpp) + endif() +endif() + ocv_module_include_directories(${include_dirs}) if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") ocv_append_source_files_cxx_compiler_options(fw_srcs "-Wno-suggest-override") # GCC @@ -171,7 +185,7 @@ if(HAVE_NGRAPH) list(APPEND dnn_runtime_libs ngraph::ngraph) endif() -ocv_glob_module_sources(${sources_options} SOURCES ${fw_srcs}) +ocv_glob_module_sources(${sources_options} SOURCES ${fw_srcs} ${webnn_srcs}) ocv_create_module(${libs} ${dnn_runtime_libs}) ocv_add_samples() ocv_add_accuracy_tests(${dnn_runtime_libs}) diff --git a/modules/dnn/include/opencv2/dnn/dnn.hpp b/modules/dnn/include/opencv2/dnn/dnn.hpp index 8b4e5e21d4..d6b29cfcf3 100644 --- a/modules/dnn/include/opencv2/dnn/dnn.hpp +++ b/modules/dnn/include/opencv2/dnn/dnn.hpp @@ -74,6 +74,7 @@ CV__DNN_INLINE_NS_BEGIN DNN_BACKEND_OPENCV, DNN_BACKEND_VKCOM, DNN_BACKEND_CUDA, + DNN_BACKEND_WEBNN, #ifdef __OPENCV_BUILD DNN_BACKEND_INFERENCE_ENGINE_NGRAPH = 1000000, // internal - use DNN_BACKEND_INFERENCE_ENGINE + setInferenceEngineBackendType() DNN_BACKEND_INFERENCE_ENGINE_NN_BUILDER_2019, // internal - use DNN_BACKEND_INFERENCE_ENGINE + setInferenceEngineBackendType() @@ -307,6 +308,8 @@ CV__DNN_INLINE_NS_BEGIN virtual Ptr initVkCom(const std::vector > &inputs); + virtual Ptr initWebnn(const std::vector > &inputs, const std::vector >& nodes); + /** * @brief Returns a CUDA backend node * diff --git a/modules/dnn/src/dnn.cpp b/modules/dnn/src/dnn.cpp index 62607e247a..a76e81e8fb 100644 --- a/modules/dnn/src/dnn.cpp +++ b/modules/dnn/src/dnn.cpp @@ -45,6 +45,7 @@ #include "ie_ngraph.hpp" #include "op_vkcom.hpp" #include "op_cuda.hpp" +#include "op_webnn.hpp" #ifdef HAVE_CUDA #include "cuda4dnn/init.hpp" @@ -224,6 +225,13 @@ private: #endif #endif // HAVE_INF_ENGINE +#ifdef HAVE_WEBNN + if (haveWebnn()) + { + backends.push_back(std::make_pair(DNN_BACKEND_WEBNN, DNN_TARGET_CPU)); + } +#endif // HAVE_WEBNN + #ifdef HAVE_OPENCL if (cv::ocl::useOpenCL()) { @@ -1114,6 +1122,14 @@ static Ptr wrapMat(int backendId, int targetId, cv::Mat& m) return Ptr(new NgraphBackendWrapper(targetId, m)); #else CV_Error(Error::StsNotImplemented, "This OpenCV version is built without support of Inference Engine + nGraph"); +#endif + } + else if (backendId == DNN_BACKEND_WEBNN) + { +#ifdef HAVE_WEBNN + return Ptr(new WebnnBackendWrapper(targetId, m)); +#else + CV_Error(Error::StsNotImplemented, "This OpenCV version is built without support of WebNN"); #endif } else if (backendId == DNN_BACKEND_VKCOM) @@ -1259,6 +1275,12 @@ struct Net::Impl : public detail::NetImplBase { return wrapMat(preferableBackend, preferableTarget, host); } + else if (preferableBackend == DNN_BACKEND_WEBNN) + { +#ifdef HAVE_WEBNN + return wrapMat(preferableBackend, preferableTarget, host); +#endif + } else if (preferableBackend == DNN_BACKEND_VKCOM) { #ifdef HAVE_VULKAN @@ -1399,6 +1421,13 @@ struct Net::Impl : public detail::NetImplBase preferableTarget == DNN_TARGET_FPGA ); } +#endif +#ifdef HAVE_WEBNN + if (preferableBackend == DNN_BACKEND_WEBNN) + { + CV_Assert(preferableTarget == DNN_TARGET_CPU || + preferableTarget == DNN_TARGET_OPENCL); + } #endif CV_Assert(preferableBackend != DNN_BACKEND_VKCOM || preferableTarget == DNN_TARGET_VULKAN); @@ -1622,6 +1651,14 @@ struct Net::Impl : public detail::NetImplBase initNgraphBackend(blobsToKeep_); #else CV_Error(Error::StsNotImplemented, "This OpenCV version is built without support of Inference Engine + nGraph"); +#endif + } + else if (preferableBackend == DNN_BACKEND_WEBNN) + { +#ifdef HAVE_WEBNN + initWebnnBackend(blobsToKeep_); +#else + CV_Error(Error::StsNotImplemented, "This OpenCV version is built without support of WebNN"); #endif } else if (preferableBackend == DNN_BACKEND_VKCOM) @@ -2341,6 +2378,270 @@ struct Net::Impl : public detail::NetImplBase } #endif // HAVE_DNN_NGRAPH +#ifdef HAVE_WEBNN + void addWebnnOutputs(LayerData &ld) + { + CV_TRACE_FUNCTION(); + + Ptr layerNet; + auto it = ld.backendNodes.find(preferableBackend); + if (it != ld.backendNodes.end()) + { + Ptr node = it->second; + if (!node.empty()) + { + Ptr webnnNode = node.dynamicCast(); + CV_Assert(!webnnNode.empty()); CV_Assert(!webnnNode->net.empty()); + layerNet = webnnNode->net; + } + } + + for (int i = 0; i < ld.inputBlobsId.size(); ++i) + { + LayerData &inpLd = layers[ld.inputBlobsId[i].lid]; + Ptr inpNode = inpLd.backendNodes[preferableBackend]; + if (!inpNode.empty()) + { + Ptr webnnInpNode = inpNode.dynamicCast(); + CV_Assert(!webnnInpNode.empty()); CV_Assert(!webnnInpNode->net.empty()); + if (layerNet != webnnInpNode->net) + { + webnnInpNode->net->addOutput(webnnInpNode->name); + webnnInpNode->net->setUnconnectedNodes(webnnInpNode); + } + } + } + } + + void initWebnnBackend(const std::vector& blobsToKeep_) + { + CV_TRACE_FUNCTION(); + CV_Assert_N(preferableBackend == DNN_BACKEND_WEBNN, haveWebnn()); + + MapIdToLayerData::iterator it; + Ptr net; + + for (it = layers.begin(); it != layers.end(); ++it) + { + LayerData &ld = it->second; + if (ld.id == 0) + { + CV_Assert((netInputLayer->outNames.empty() && ld.outputBlobsWrappers.size() == 1) || + (netInputLayer->outNames.size() == ld.outputBlobsWrappers.size())); + for (int i = 0; i < ld.outputBlobsWrappers.size(); ++i) + { + Ptr wrapper = ld.outputBlobsWrappers[i].dynamicCast(); + std::string outputName = netInputLayer->outNames.empty() ? ld.name : netInputLayer->outNames[i]; + outputName = ld.outputBlobsWrappers.size() > 1 ? (outputName + "." + std::to_string(i)) : outputName; + wrapper->name = outputName; + } + } + else + { + for (int i = 0; i < ld.outputBlobsWrappers.size(); ++i) + { + Ptr wrapper = ld.outputBlobsWrappers[i].dynamicCast(); + std::string outputName = ld.outputBlobsWrappers.size() > 1 ? (ld.name + "." + std::to_string(i)) : ld.name; + wrapper->name = outputName; + } + } + } + + // Build WebNN networks from sets of layers that support this + // backend. Split a whole model on several WebNN networks if + // some of layers are not implemented. + for (it = layers.begin(); it != layers.end(); ++it) + { + LayerData &ld = it->second; + + if (ld.id == 0 && ld.skip) + continue; + + bool fused = ld.skip; + Ptr layer = ld.layerInstance; + if (!fused && !layer->supportBackend(preferableBackend)) + { + // For test use. when not using WebNN, the test case will fail + // with the following code. + CV_LOG_WARNING(NULL, "Layer " + ld.type + " name " + ld.name + " is unsupported by WebNN backend."); + + addWebnnOutputs(ld); + net = Ptr(); + layer->preferableTarget = DNN_TARGET_CPU; + + for (int i = 0; i < ld.inputBlobsId.size(); ++i) + { + LayerData &inpLd = layers[ld.inputBlobsId[i].lid]; + Ptr inpNode = inpLd.backendNodes[preferableBackend]; + if (!inpNode.empty()) { + Ptr webnnNode = inpNode.dynamicCast(); + CV_Assert(!webnnNode.empty()); + webnnNode->net->setUnconnectedNodes(webnnNode); + } + } + continue; + } + ld.skip = true; // Initially skip all WebNN supported layers. + + // Create a new network if one of inputs from different WebNN graph. + std::vector> inputNodes; + for (int i = 0; i < ld.inputBlobsId.size(); ++i) + { + // Layer_Test_ROIPooling.Accuracy has 2 inputs inpLD = 0, 0 -> has 4 inputNodes (input, rois, input, rois) + if (inputNodes.size() == ld.inputBlobsId.size()) { + break; + } + LayerData &inpLd = layers[ld.inputBlobsId[i].lid]; + Ptr inpNode = inpLd.backendNodes[preferableBackend]; + if (!inpNode.empty()) + { + Ptr webnnInpNode = inpNode.dynamicCast(); + CV_Assert(!webnnInpNode.empty()); CV_Assert(!webnnInpNode->net.empty()); + if (webnnInpNode->net == net && !fused) { + inputNodes.push_back(inpNode); + continue; + } + } + + if (net.empty()) { + net = Ptr(new WebnnNet()); + } + + if (!fused) { + std::vector inputNames; + std::vector inputs; + + auto curr_pos = inpLd.consumers.begin(); + auto compare = [&ld] (const LayerPin& lp) { return lp.lid == ld.id; }; + auto cons = curr_pos; + while ((cons = std::find_if(curr_pos, inpLd.consumers.end(), compare)) != + inpLd.consumers.end()) { + int cons_inp = cons->oid; + Ptr inpWrapper = inpLd.outputBlobsWrappers[cons_inp]. + dynamicCast(); + CV_Assert(!inpWrapper.empty()); + auto iter = std::find(inputNames.begin(), inputNames.end(), + inpWrapper->name); + if (iter == inputNames.end()) { + inputNames.push_back(inpWrapper->name); + inputs.push_back(inpLd.outputBlobs[cons_inp]); + } + curr_pos = cons + 1; + } + + auto inps = net->setInputs(inputs, inputNames); + for (auto& inp : inps) { + WebnnBackendNode* node = new WebnnBackendNode(inp); + node->net = net; + inputNodes.emplace_back(Ptr(node)); + } + } + } + + Ptr node; + if (!net.empty()) + { + if (fused) + { + bool inPlace = ld.inputBlobsId.size() == 1 && ld.outputBlobs.size() == 1 && + ld.inputBlobs[0]->data == ld.outputBlobs[0].data; + CV_Assert(inPlace); + node = layers[ld.inputBlobsId[0].lid].backendNodes[preferableBackend]; + ld.inputBlobsWrappers = layers[ld.inputBlobsId[0].lid].inputBlobsWrappers; + } + } + else { + net = Ptr(new WebnnNet()); + } + + if (!fused) + { + CV_Assert(ld.inputBlobsId.size() == inputNodes.size()); + for (int i = 0; i < ld.inputBlobsId.size(); ++i) + { + int lid = ld.inputBlobsId[i].lid; + int oid = ld.inputBlobsId[i].oid; + if (oid == 0 || lid == 0) + continue; + + auto webnnInpNode = inputNodes[i].dynamicCast(); + inputNodes[i] = Ptr(new WebnnBackendNode(webnnInpNode->operand)); + } + + if (layer->supportBackend(preferableBackend)) + { + if (ld.type == "Const") { + ml::Operand fake_operand; + Ptr fake_input_node = Ptr(new WebnnBackendNode(fake_operand)); + fake_input_node->net = net; + inputNodes.push_back(fake_input_node); + } + node = layer->initWebnn(ld.inputBlobsWrappers, inputNodes); + for (int i = 0; i < ld.outputBlobsWrappers.size(); ++i) + { + Ptr wrapper = ld.outputBlobsWrappers[i].dynamicCast(); + node.dynamicCast()->name = wrapper->name; + } + } + else + { + continue; + } + } + else if (node.empty()) + continue; + + ld.backendNodes[preferableBackend] = node; + + Ptr webnnNode = node.dynamicCast(); + CV_Assert(!webnnNode.empty()); + webnnNode->net = net; + + if (ld.consumers.empty()) { + // TF EAST_text_detection + webnnNode->net->setUnconnectedNodes(webnnNode); + } + for (const auto& pin : blobsToKeep_) + { + if (pin.lid == ld.id) + { + webnnNode->net->addOutput(webnnNode->name); + break; + } + } + net->addBlobs(ld.inputBlobsWrappers); + net->addBlobs(ld.outputBlobsWrappers); + addWebnnOutputs(ld); + } + + // Initialize all networks. + for (MapIdToLayerData::reverse_iterator it = layers.rbegin(); it != layers.rend(); ++it) + { + LayerData &ld = it->second; + auto iter = ld.backendNodes.find(preferableBackend); + if (iter == ld.backendNodes.end()) + continue; + + Ptr& node = iter->second; + if (node.empty()) + continue; + + Ptr webnnNode = node.dynamicCast(); + if (webnnNode.empty()) + continue; + + CV_Assert(!webnnNode->net.empty()); + + if (!webnnNode->net->isInitialized()) + { + webnnNode->net->setUnconnectedNodes(webnnNode); + webnnNode->net->createNet((Target)preferableTarget); + ld.skip = false; + } + } + } +#endif + void initVkComBackend() { CV_TRACE_FUNCTION(); @@ -3393,6 +3694,10 @@ struct Net::Impl : public detail::NetImplBase else if (preferableBackend == DNN_BACKEND_INFERENCE_ENGINE_NGRAPH) { forwardNgraph(ld.outputBlobsWrappers, node, isAsync); + } + else if (preferableBackend == DNN_BACKEND_WEBNN) + { + forwardWebnn(ld.outputBlobsWrappers, node, isAsync); } else if (preferableBackend == DNN_BACKEND_VKCOM) { @@ -4830,6 +5135,7 @@ string Net::Impl::dump() case DNN_BACKEND_OPENCV: backend = "OCV/"; break; case DNN_BACKEND_VKCOM: backend = "VULKAN/"; break; case DNN_BACKEND_CUDA: backend = "CUDA/"; break; + case DNN_BACKEND_WEBNN: backend = "WEBNN/"; break; // don't use default: } out << "digraph G {\n"; @@ -5421,6 +5727,13 @@ Ptr Layer::initNgraph(const std::vector > & inp return Ptr(); } +Ptr Layer::initWebnn(const std::vector > & inputs, const std::vector >& nodes) +{ + CV_Error(Error::StsNotImplemented, "WebNN pipeline of " + type + + " layers is not defined."); + return Ptr(); +} + void Layer::applyHalideScheduler(Ptr& node, const std::vector &inputs, const std::vector &outputs, int targetId) const { diff --git a/modules/dnn/src/layers/batch_norm_layer.cpp b/modules/dnn/src/layers/batch_norm_layer.cpp index 57a95b15ee..d22a070805 100644 --- a/modules/dnn/src/layers/batch_norm_layer.cpp +++ b/modules/dnn/src/layers/batch_norm_layer.cpp @@ -15,6 +15,7 @@ Implementation of Batch Normalization layer. #include "../op_halide.hpp" #include "../op_inf_engine.hpp" #include "../ie_ngraph.hpp" +#include "../op_webnn.hpp" #include @@ -172,6 +173,7 @@ public: return (backendId == DNN_BACKEND_OPENCV) || backendId == DNN_BACKEND_CUDA || (backendId == DNN_BACKEND_HALIDE && haveHalide()) || + backendId == DNN_BACKEND_WEBNN || ((backendId == DNN_BACKEND_INFERENCE_ENGINE_NN_BUILDER_2019 || backendId == DNN_BACKEND_INFERENCE_ENGINE_NGRAPH) && haveInfEngine() && (preferableTarget == DNN_TARGET_CPU || dims == 4)); } @@ -421,6 +423,27 @@ public: return true; } +#ifdef HAVE_WEBNN + virtual Ptr initWebnn(const std::vector >& inputs, const std::vector >& nodes) CV_OVERRIDE + { + Ptr node = nodes[0].dynamicCast(); + auto& webnnInpOperand = node->operand; + auto& webnnGraphBuilder = node->net->builder; + std::vector weights_shape = webnn::getShape(weights_); + ml::Operand weights = webnn::BuildConstant(webnnGraphBuilder, weights_shape, weights_.data, weights_.total()*weights_.elemSize(), ml::OperandType::Float32); + std::vector shape(dims, 1); + shape[1] = weights_shape[1]; + ml::Operand weights_reshaped = webnnGraphBuilder.Reshape(weights, shape.data(), shape.size()); + ml::Operand mul_res = webnnGraphBuilder.Mul(webnnInpOperand, weights_reshaped); + std::vector bias_shape = webnn::getShape(bias_); + ml::Operand bias = webnn::BuildConstant(webnnGraphBuilder, bias_shape, bias_.data, bias_.total()*bias_.elemSize(), ml::OperandType::Float32); + shape[1] = bias_shape[1]; + ml::Operand bias_reshaped = webnnGraphBuilder.Reshape(bias, shape.data(), shape.size()); + ml::Operand add_res = webnnGraphBuilder.Add(mul_res, bias_reshaped); + return Ptr(new WebnnBackendNode(add_res)); + } +#endif + virtual int64 getFLOPS(const std::vector &inputs, const std::vector &outputs) const CV_OVERRIDE { diff --git a/modules/dnn/src/layers/concat_layer.cpp b/modules/dnn/src/layers/concat_layer.cpp index 536114fcd7..f620d66a39 100644 --- a/modules/dnn/src/layers/concat_layer.cpp +++ b/modules/dnn/src/layers/concat_layer.cpp @@ -47,6 +47,7 @@ #include "../op_inf_engine.hpp" #include "../ie_ngraph.hpp" #include "../op_vkcom.hpp" +#include "../op_webnn.hpp" #ifdef HAVE_OPENCL #include "opencl_kernels_dnn.hpp" @@ -117,6 +118,7 @@ public: (backendId == DNN_BACKEND_HALIDE && haveHalide() && axis == 1 && !padding) || // By channels (backendId == DNN_BACKEND_INFERENCE_ENGINE_NN_BUILDER_2019 && haveInfEngine() && !padding) || backendId == DNN_BACKEND_INFERENCE_ENGINE_NGRAPH || + (backendId == DNN_BACKEND_WEBNN && !padding) || (backendId == DNN_BACKEND_VKCOM && haveVulkan() && !padding); } @@ -408,6 +410,22 @@ public: params.set("padding_value", zeropoints[1][0]); return true; } + +#ifdef HAVE_WEBNN + virtual Ptr initWebnn(const std::vector >& inputs, const std::vector >& nodes) CV_OVERRIDE + { + Ptr node = nodes[0].dynamicCast(); + auto& webnnGraphBuilder = node->net->builder; + std::vector inputsOperand; + for (int i = 0; i < nodes.size(); i++) + { + inputsOperand.push_back(nodes[i].dynamicCast()->operand); + } + auto operand = webnnGraphBuilder.Concat(inputsOperand.size(), inputsOperand.data(), axis); + return Ptr(new WebnnBackendNode(operand)); + } +#endif + }; Ptr ConcatLayer::create(const LayerParams& params) diff --git a/modules/dnn/src/layers/const_layer.cpp b/modules/dnn/src/layers/const_layer.cpp index 18f190b36b..1f307b8fa6 100644 --- a/modules/dnn/src/layers/const_layer.cpp +++ b/modules/dnn/src/layers/const_layer.cpp @@ -10,6 +10,7 @@ #include "../op_cuda.hpp" #include "layers_common.hpp" #include "../ie_ngraph.hpp" +#include "../op_webnn.hpp" #ifdef HAVE_OPENCL #include "opencl_kernels_dnn.hpp" @@ -36,6 +37,7 @@ public: return backendId == DNN_BACKEND_OPENCV || backendId == DNN_BACKEND_INFERENCE_ENGINE_NN_BUILDER_2019 || backendId == DNN_BACKEND_INFERENCE_ENGINE_NGRAPH || + backendId == DNN_BACKEND_WEBNN || backendId == DNN_BACKEND_CUDA; } @@ -97,6 +99,16 @@ public: } #endif // HAVE_DNN_NGRAPH +#ifdef HAVE_WEBNN + virtual Ptr initWebnn(const std::vector >& inputs, const std::vector >& nodes) CV_OVERRIDE + { + ml::Operand operand = nullptr; + Ptr node = nodes[0].dynamicCast(); + auto& webnnGraphBuilder = node->net->builder; + operand = webnn::BuildConstant(webnnGraphBuilder, webnn::getShape(blobs[0]), blobs[0].data, blobs[0].total()*blobs[0].elemSize(), ml::OperandType::Float32); + return Ptr(new WebnnBackendNode(operand)); + } +#endif #ifdef HAVE_CUDA Ptr initCUDA( diff --git a/modules/dnn/src/layers/convolution_layer.cpp b/modules/dnn/src/layers/convolution_layer.cpp index bb6fd5f5cc..50b06a19c0 100644 --- a/modules/dnn/src/layers/convolution_layer.cpp +++ b/modules/dnn/src/layers/convolution_layer.cpp @@ -47,6 +47,7 @@ #include "../op_inf_engine.hpp" #include "../ie_ngraph.hpp" #include "../op_vkcom.hpp" +#include "../op_webnn.hpp" #include #include @@ -80,6 +81,9 @@ class BaseConvolutionLayerImpl : public ConvolutionLayer public: bool fusedWeights, fusedBias; std::vector weightsMultipliers; +#ifdef HAVE_WEBNN + int groups; +#endif BaseConvolutionLayerImpl(const LayerParams ¶ms) { setParamsFrom(params); @@ -87,6 +91,9 @@ public: numOutput = params.get("num_output"); int ngroups = params.get("group", 1); +#ifdef HAVE_WEBNN + groups = ngroups; +#endif CV_Assert(numOutput % ngroups == 0); if (kernel_size.size() == 2) { @@ -347,6 +354,17 @@ public: #ifdef HAVE_VULKAN if (backendId == DNN_BACKEND_VKCOM) return ksize == 2; +#endif +#ifdef HAVE_WEBNN + if (backendId == DNN_BACKEND_WEBNN) + { + if (ksize != 2) + { + CV_LOG_WARNING(NULL, "WebNN only supports Conv2d."); + return false; + } + return true; + } #endif return false; } @@ -896,6 +914,108 @@ public: } #endif // HAVE_DNN_NGRAPH +#ifdef HAVE_WEBNN + virtual Ptr initWebnn(const std::vector >& inputs, const std::vector >& nodes) CV_OVERRIDE + { + CV_Assert_N(inputs.size() >= 1, nodes.size() >= 1); + Ptr node = nodes[0].dynamicCast(); + auto& webnnInpOperand = node->operand; + auto& webnnGraphBuilder = node->net->builder; + ml::Operand webnnWeights = nodes.size() > 1 ? nodes[1].dynamicCast()->operand : nullptr; + if (nodes.size() > 1) + CV_Assert(webnnWeights); + const int inpCn = weightsMat.total()/(kernel_size[0]*kernel_size[1]*numOutput); + const int group = groups; + const int inpGroupCn = inpCn / group; + std::vector kernel_shape; + if (group != 1) + { + kernel_shape.push_back(group); + } + kernel_shape.push_back(numOutput / group); + kernel_shape.push_back(inpGroupCn); + std::copy(kernel_size.begin(), kernel_size.end(), back_inserter(kernel_shape)); + + if (nodes.size() == 1) + { + webnnWeights = webnn::BuildConstant(webnnGraphBuilder, webnn::getShape(blobs[0]), blobs[0].data, blobs[0].total()*blobs[0].elemSize(), ml::OperandType::Float32); + if (fusedWeights) + { + if (weightsMat.isContinuous()) + { + webnnWeights = webnn::BuildConstant(webnnGraphBuilder, webnn::getShape(weightsMat), weightsMat.data, weightsMat.total()*weightsMat.elemSize(), ml::OperandType::Float32); + } + else + { + Mat newWeights; + Mat cvWeights = weightsMat.colRange(0, blobs[0].total() / numOutput); + cvWeights.copyTo(newWeights); + webnnWeights = webnn::BuildConstant(webnnGraphBuilder, webnn::getShape(newWeights), newWeights.data, newWeights.total()*newWeights.elemSize(), ml::OperandType::Float32); + } + } + } + else + { + webnnWeights = webnnGraphBuilder.Reshape(webnnWeights, kernel_shape.data(), kernel_shape.size()); + } + + ml::AutoPad pad_type = ml::AutoPad::Explicit; + if (!padMode.empty()) + pad_type = padMode == "VALID" ? ml::AutoPad::Explicit : ml::AutoPad::SameUpper; + + ml::Conv2dOptions options = {}; + options.groups = group; + options.autoPad = pad_type; + std::vector Strides(strides.begin(), strides.end()); + if (!Strides.empty()) + { + options.stridesCount = Strides.size(); + options.strides = Strides.data(); + } + std::vector Padding; + if (padMode.empty()) + { + Padding = {static_cast(pads_begin[0]), + static_cast(pads_end[0]), + static_cast(pads_begin[1]), + static_cast(pads_end[1])}; + } + else if (padMode == "VALID") + { + Padding = {0, 0, 0, 0}; + } + if (!Padding.empty()) + { + options.paddingCount = Padding.size(); + options.padding = Padding.data(); + } + std::vector Dilations(dilations.begin(), dilations.end()); + if (!Dilations.empty()) + { + options.dilationsCount = Dilations.size(); + options.dilations = Dilations.data(); + } + ml::Operand operand = webnnGraphBuilder.Conv2d(webnnInpOperand, webnnWeights, &options); + + // ml::Operand result = operand; + if (hasBias() || fusedBias || nodes.size() == 3) + { + ml::Operand webnnBias = nullptr; + if (nodes.size() == 3) + { + std::vector bias_shape = {1, numOutput, 1, 1}; + webnnBias = webnnGraphBuilder.Reshape(nodes[2].dynamicCast()->operand, bias_shape.data(), bias_shape.size()); + } + else + { + webnnBias = webnn::BuildConstant(webnnGraphBuilder, {1, numOutput, 1, 1}, biasvec.data(), (numOutput) * sizeof(float), ml::OperandType::Float32); + } + operand = webnnGraphBuilder.Add(operand, webnnBias); + } + return Ptr(new WebnnBackendNode(operand)); + } +#endif // HAVE_WEBNN + class ParallelConv : public cv::ParallelLoopBody { public: diff --git a/modules/dnn/src/layers/elementwise_layers.cpp b/modules/dnn/src/layers/elementwise_layers.cpp index c95dbbc933..56e82cc3d1 100644 --- a/modules/dnn/src/layers/elementwise_layers.cpp +++ b/modules/dnn/src/layers/elementwise_layers.cpp @@ -47,8 +47,11 @@ #include "../op_inf_engine.hpp" #include "../ie_ngraph.hpp" #include "../op_vkcom.hpp" +#include "../op_webnn.hpp" #include +#include +#include #include #ifdef HAVE_OPENCL @@ -59,6 +62,7 @@ #include "../cuda4dnn/primitives/activation.hpp" using namespace cv::dnn::cuda4dnn; #endif +#include namespace cv { @@ -186,6 +190,17 @@ public: } #endif // HAVE_DNN_NGRAPH +#ifdef HAVE_WEBNN + virtual Ptr initWebnn(const std::vector >& inputs, const std::vector >& nodes) CV_OVERRIDE + { + Ptr node = nodes[0].dynamicCast(); + auto& webnnInpOperand = node->operand; + auto& webnnGraphBuilder = node->net->builder; + auto operand = func.initWebnnAPI(webnnGraphBuilder, webnnInpOperand); + return Ptr(new WebnnBackendNode(operand)); + } +#endif + virtual Ptr initVkCom(const std::vector >& inputs) CV_OVERRIDE { #ifdef HAVE_VULKAN @@ -319,6 +334,16 @@ struct ReLUFunctor : public BaseFunctor #ifdef HAVE_DNN_NGRAPH if (backendId == DNN_BACKEND_INFERENCE_ENGINE_NGRAPH) return true; +#endif +#ifdef HAVE_WEBNN + if (backendId == DNN_BACKEND_WEBNN) { + // TODO: support PRELU + if (slope != 0) + { + CV_LOG_WARNING(NULL, "PRELU is not supported now."); + } + return slope == 0; + } #endif return backendId == DNN_BACKEND_OPENCV || backendId == DNN_BACKEND_CUDA || @@ -441,6 +466,13 @@ struct ReLUFunctor : public BaseFunctor } #endif // HAVE_DNN_NGRAPH +#ifdef HAVE_WEBNN + ml::Operand initWebnnAPI(const ml::GraphBuilder& builder, const ml::Operand& input) + { + return builder.Relu(input); + } +#endif + #ifdef HAVE_VULKAN std::shared_ptr initVkCom() { @@ -491,6 +523,7 @@ struct ReLU6Functor : public BaseFunctor return backendId == DNN_BACKEND_OPENCV || backendId == DNN_BACKEND_CUDA || backendId == DNN_BACKEND_HALIDE || + backendId == DNN_BACKEND_WEBNN || backendId == DNN_BACKEND_INFERENCE_ENGINE_NN_BUILDER_2019 || backendId == DNN_BACKEND_INFERENCE_ENGINE_NGRAPH; } @@ -587,6 +620,18 @@ struct ReLU6Functor : public BaseFunctor } #endif // HAVE_DNN_NGRAPH + + +#ifdef HAVE_WEBNN + ml::Operand initWebnnAPI(const ml::GraphBuilder& builder, const ml::Operand& input) + { + ml::ClampOptions clampOptions; + clampOptions.minValue = minValue; + clampOptions.maxValue = maxValue; + return builder.Clamp(input, &clampOptions); + } +#endif + #ifdef HAVE_VULKAN std::shared_ptr initVkCom() { @@ -684,6 +729,15 @@ struct BaseDefaultFunctor : public BaseFunctor } #endif // HAVE_DNN_NGRAPH +#ifdef HAVE_WEBNN + ml::Operand initWebnnAPI(const ml::GraphBuilder& builder, const ml::Operand& input) + { + CV_Error(Error::StsNotImplemented, ""); + ml::Operand operand; + return operand; + } +#endif + #ifdef HAVE_VULKAN std::shared_ptr initVkCom() { @@ -742,6 +796,15 @@ struct TanHFunctor : public BaseDefaultFunctor } #endif // HAVE_DNN_NGRAPH +#ifdef HAVE_WEBNN + ml::Operand initWebnnAPI(const ml::GraphBuilder& builder, const ml::Operand& input) + { + CV_Error(Error::StsNotImplemented, ""); + ml::Operand operand; + return operand; + } +#endif + int64 getFLOPSPerElement() const { return 1; } }; @@ -845,6 +908,15 @@ struct MishFunctor : public BaseDefaultFunctor } #endif // HAVE_DNN_NGRAPH +#ifdef HAVE_WEBNN + ml::Operand initWebnnAPI(const ml::GraphBuilder& builder, const ml::Operand& input) + { + CV_Error(Error::StsNotImplemented, ""); + ml::Operand operand; + return operand; + } +#endif + int64 getFLOPSPerElement() const { return 3; } }; @@ -897,6 +969,15 @@ struct SigmoidFunctor : public BaseDefaultFunctor } #endif // HAVE_DNN_NGRAPH +#ifdef HAVE_WEBNN + ml::Operand initWebnnAPI(const ml::GraphBuilder& builder, const ml::Operand& input) + { + CV_Error(Error::StsNotImplemented, ""); + ml::Operand operand; + return operand; + } +#endif + int64 getFLOPSPerElement() const { return 3; } }; @@ -1007,6 +1088,15 @@ struct AbsValFunctor : public BaseDefaultFunctor } #endif // HAVE_DNN_NGRAPH +#ifdef HAVE_WEBNN + ml::Operand initWebnnAPI(const ml::GraphBuilder& builder, const ml::Operand& input) + { + CV_Error(Error::StsNotImplemented, ""); + ml::Operand operand; + return operand; + } +#endif + int64 getFLOPSPerElement() const { return 1; } }; @@ -1136,6 +1226,15 @@ struct LogFunctor : public BaseDefaultFunctor return log(x); } +#ifdef HAVE_WEBNN + ml::Operand initWebnnAPI(const ml::GraphBuilder& builder, const ml::Operand& input) + { + CV_Error(Error::StsNotImplemented, ""); + ml::Operand operand; + return operand; + } +#endif + #ifdef HAVE_CUDA Ptr initCUDA(int target, csl::Stream stream) { @@ -1233,6 +1332,15 @@ struct SqrtFunctor : public BaseDefaultFunctor } #endif // HAVE_DNN_NGRAPH +#ifdef HAVE_WEBNN + ml::Operand initWebnnAPI(const ml::GraphBuilder& builder, const ml::Operand& input) + { + CV_Error(Error::StsNotImplemented, ""); + ml::Operand operand; + return operand; + } +#endif + int64 getFLOPSPerElement() const { return 1; } }; @@ -1415,6 +1523,15 @@ struct PowerFunctor : public BaseFunctor } #endif // HAVE_DNN_NGRAPH +#ifdef HAVE_WEBNN + ml::Operand initWebnnAPI(const ml::GraphBuilder& builder, const ml::Operand& input) + { + CV_Error(Error::StsNotImplemented, ""); + ml::Operand operand; + return operand; + } +#endif + #ifdef HAVE_VULKAN std::shared_ptr initVkCom() { @@ -1517,6 +1634,15 @@ struct ExpFunctor : public BaseDefaultFunctor } #endif // HAVE_DNN_NGRAPH +#ifdef HAVE_WEBNN + ml::Operand initWebnnAPI(const ml::GraphBuilder& builder, const ml::Operand& input) + { + CV_Error(Error::StsNotImplemented, ""); + ml::Operand operand; + return operand; + } +#endif + int64 getFLOPSPerElement() const { return 3; } }; @@ -1649,6 +1775,15 @@ struct ChannelsPReLUFunctor : public BaseFunctor } #endif // HAVE_DNN_NGRAPH +#ifdef HAVE_WEBNN + ml::Operand initWebnnAPI(const ml::GraphBuilder& builder, const ml::Operand& input) + { + CV_Error(Error::StsNotImplemented, ""); + ml::Operand operand; + return operand; + } +#endif + #ifdef HAVE_VULKAN std::shared_ptr initVkCom() { diff --git a/modules/dnn/src/layers/fully_connected_layer.cpp b/modules/dnn/src/layers/fully_connected_layer.cpp index 28ea7f347f..1e8c9f5489 100644 --- a/modules/dnn/src/layers/fully_connected_layer.cpp +++ b/modules/dnn/src/layers/fully_connected_layer.cpp @@ -46,6 +46,7 @@ #include "../op_halide.hpp" #include "../op_inf_engine.hpp" #include "../ie_ngraph.hpp" +#include "../op_webnn.hpp" #include @@ -150,6 +151,7 @@ public: return backendId == DNN_BACKEND_OPENCV || backendId == DNN_BACKEND_CUDA || (backendId == DNN_BACKEND_HALIDE && haveHalide() && axis == 1) || + (backendId == DNN_BACKEND_WEBNN && axis == 1) || (((backendId == DNN_BACKEND_INFERENCE_ENGINE_NN_BUILDER_2019 && !blobs.empty()) || backendId == DNN_BACKEND_INFERENCE_ENGINE_NGRAPH) && axis == 1); } @@ -657,6 +659,40 @@ public: return true; } +#ifdef HAVE_WEBNN + virtual Ptr initWebnn(const std::vector >& inputs, const std::vector >& nodes) CV_OVERRIDE + { + Ptr node = nodes[0].dynamicCast(); + auto& webnnInpOperand = node->operand; + auto& webnnGraphBuilder = node->net->builder; + ml::GemmOptions gemmOptions = {}; + if (bias) + { + std::vector biasDims = {(int32_t)blobs[1].size[1]}; + ml::Operand bias = webnn::BuildConstant(webnnGraphBuilder, biasDims, blobs[1].data, blobs[1].total()*blobs[1].elemSize(), ml::OperandType::Float32); + gemmOptions.c = bias; + } + ml::Operand result = nullptr; + if (nodes.size() == 2) + { + auto& inp2 = nodes[1].dynamicCast()->operand; + result = webnnGraphBuilder.Gemm(webnnInpOperand, inp2, &gemmOptions); + } + else + { + std::vector input_shape(2, -1); + input_shape[1] = blobs[0].size[1]; + ml::Operand webnnInpOperand_reshaped = webnnGraphBuilder.Reshape(webnnInpOperand, input_shape.data(), input_shape.size()); + std::vector weight_shape = {(int32_t)blobs[0].size[0], (int32_t)blobs[0].size[1]}; + // std::cout<<"weight size: "<(new WebnnBackendNode(result)); + } +#endif // HAVE_WEBNN + virtual int64 getFLOPS(const std::vector &inputs, const std::vector &outputs) const CV_OVERRIDE { diff --git a/modules/dnn/src/layers/permute_layer.cpp b/modules/dnn/src/layers/permute_layer.cpp index 77c2469c05..9e66eb6a64 100644 --- a/modules/dnn/src/layers/permute_layer.cpp +++ b/modules/dnn/src/layers/permute_layer.cpp @@ -46,6 +46,7 @@ #include "../op_inf_engine.hpp" #include "../ie_ngraph.hpp" #include "../op_vkcom.hpp" +#include "../op_webnn.hpp" #include #include @@ -119,6 +120,7 @@ public: #endif return backendId == DNN_BACKEND_OPENCV || backendId == DNN_BACKEND_CUDA || + backendId == DNN_BACKEND_WEBNN || ((backendId == DNN_BACKEND_INFERENCE_ENGINE_NN_BUILDER_2019 || backendId == DNN_BACKEND_INFERENCE_ENGINE_NGRAPH) && haveInfEngine()) || (backendId == DNN_BACKEND_VKCOM && haveVulkan()); } @@ -439,6 +441,20 @@ public: } #endif // HAVE_DNN_NGRAPH +#ifdef HAVE_WEBNN + virtual Ptr initWebnn(const std::vector >& inputs, const std::vector >& nodes) CV_OVERRIDE + { + Ptr node = nodes[0].dynamicCast(); + auto& webnnInpOperand = node->operand; + auto& webnnGraphBuilder = node->net->builder; + std::vector permutation(_order.begin(), _order.end()); + ml::TransposeOptions options; + options.permutation = permutation.data(); + options.permutationCount = permutation.size(); + auto operand = webnnGraphBuilder.Transpose(webnnInpOperand, &options); + return Ptr(new WebnnBackendNode(operand)); + } +#endif #ifdef HAVE_CUDA Ptr initCUDA( diff --git a/modules/dnn/src/layers/pooling_layer.cpp b/modules/dnn/src/layers/pooling_layer.cpp index 7653e53668..0b9b94fa57 100644 --- a/modules/dnn/src/layers/pooling_layer.cpp +++ b/modules/dnn/src/layers/pooling_layer.cpp @@ -46,6 +46,7 @@ #include "../op_cuda.hpp" #include "../op_halide.hpp" #include "../op_inf_engine.hpp" +#include "../op_webnn.hpp" #ifdef HAVE_DNN_NGRAPH #include "../ie_ngraph.hpp" @@ -85,6 +86,7 @@ typedef int HALIDE_DIFF_T; #include "../cuda4dnn/primitives/max_unpooling.hpp" using namespace cv::dnn::cuda4dnn; #endif +#include namespace cv @@ -246,6 +248,51 @@ public: (type == MAX || type == AVE); return false; } + else if (backendId == DNN_BACKEND_WEBNN) + { + if (kernel_size.empty() || kernel_size.size() == 2) + { + if (!haveWebnn()) + { + return false; + } + else + { + if (!ceilMode) + { + CV_LOG_WARNING(NULL, "ceilMode is not supported by WebNN backend."); + return false; + } + if (computeMaxIdx) + { + CV_LOG_WARNING(NULL, "Mask is not supported by WebNN backend."); + return false; + } + if (type != MAX && type != AVE) + { + if (type == STOCHASTIC) + { + CV_LOG_WARNING(NULL, "Stochastic Pooling is not supported by WebNN backend."); + } + if (type == SUM) + { + CV_LOG_WARNING(NULL, "Sum Pooling is not supported by WebNN backend."); + } + if (type == ROI) + { + CV_LOG_WARNING(NULL, "ROI Pooling is not supported by WebNN backend."); + } + if (type == PSROI) + { + CV_LOG_WARNING(NULL, "Position-sensitive ROI Pooling is not supported by WebNN backend."); + } + CV_LOG_WARNING(NULL, "WebNN backend only supports MaxPooling and AveragePooling currently."); + return false; + } + } + return true; + } + } return false; } @@ -607,6 +654,45 @@ public: } #endif // HAVE_DNN_NGRAPH +#ifdef HAVE_WEBNN + virtual Ptr initWebnn(const std::vector >& inputs, const std::vector >& nodes) CV_OVERRIDE + { + // std::cout << "Use WebNN Pooling Layer's Implementation." << std::endl; + Ptr node = nodes[0].dynamicCast(); + auto& webnnInpOperand = node->operand; + auto& webnnGraphBuilder = node->net->builder; + webnn::Pool2dOptions options; + std::vector kernelSize(kernel_size.begin(), kernel_size.end()); + std::vector Strides(strides.begin(), strides.end()); + std::vector Padding; + if (padMode.empty()) { + Padding = {static_cast(pads_begin[0]), + static_cast(pads_end[0]), + static_cast(pads_begin[1]), + static_cast(pads_end[1])}; + } else if (padMode == "VALID") { + Padding = {0, 0, 0, 0}; + } else if (padMode == "SAME") { + options.autoPad = ml::AutoPad::SameUpper; + } + // std::cout << "padMode: " << padMode << std::endl; + options.windowDimensions = kernelSize; + options.strides = Strides; + options.padding = Padding; + if (type == MAX) + { + auto operand = webnnGraphBuilder.MaxPool2d(webnnInpOperand, options.AsPtr()); + return Ptr(new WebnnBackendNode(operand)); + } + else if (type == AVE) + { + auto operand = webnnGraphBuilder.AveragePool2d(webnnInpOperand, options.AsPtr()); + return Ptr(new WebnnBackendNode(operand)); + } else { + CV_Error(Error::StsNotImplemented, "Unsupported pooling type"); + } + } +#endif // HAVE_WEBNN class PoolingInvoker : public ParallelLoopBody { diff --git a/modules/dnn/src/layers/reshape_layer.cpp b/modules/dnn/src/layers/reshape_layer.cpp index 4c10d155c8..0ba3abf047 100644 --- a/modules/dnn/src/layers/reshape_layer.cpp +++ b/modules/dnn/src/layers/reshape_layer.cpp @@ -45,6 +45,7 @@ #include "../op_cuda.hpp" #include "../op_inf_engine.hpp" #include "../ie_ngraph.hpp" +#include "../op_webnn.hpp" #include @@ -203,6 +204,7 @@ public: { return backendId == DNN_BACKEND_OPENCV || backendId == DNN_BACKEND_CUDA || + backendId == DNN_BACKEND_WEBNN || ((backendId == DNN_BACKEND_INFERENCE_ENGINE_NN_BUILDER_2019 || backendId == DNN_BACKEND_INFERENCE_ENGINE_NGRAPH) && haveInfEngine()); } @@ -330,6 +332,17 @@ public: } #endif // HAVE_DNN_NGRAPH +#ifdef HAVE_WEBNN + virtual Ptr initWebnn(const std::vector >& inputs, const std::vector >& nodes) CV_OVERRIDE + { + Ptr node = nodes[0].dynamicCast(); + auto& webnnInpOperand = node->operand; + auto& webnnGraphBuilder = node->net->builder; + const std::vector out(outShapes[0].begin(), outShapes[0].end()); + auto operand = webnnGraphBuilder.Reshape(webnnInpOperand, out.data(), out.size()); + return Ptr(new WebnnBackendNode(operand)); + } +#endif #ifdef HAVE_CUDA Ptr initCUDA( diff --git a/modules/dnn/src/layers/scale_layer.cpp b/modules/dnn/src/layers/scale_layer.cpp index 003f78dc1d..fcee451556 100644 --- a/modules/dnn/src/layers/scale_layer.cpp +++ b/modules/dnn/src/layers/scale_layer.cpp @@ -15,6 +15,7 @@ Implementation of Scale layer. #include "../op_halide.hpp" #include "../op_inf_engine.hpp" #include "../ie_ngraph.hpp" +#include "../op_webnn.hpp" #include #include @@ -32,6 +33,10 @@ namespace dnn class ScaleLayerImpl CV_FINAL : public ScaleLayer { public: +#ifdef HAVE_WEBNN + mutable int dims; + mutable int numChannels; +#endif ScaleLayerImpl(const LayerParams& params) { setParamsFrom(params); @@ -47,6 +52,15 @@ public: std::vector &internals) const CV_OVERRIDE { outputs.assign(1, inputs[0]); +#ifdef HAVE_WEBNN + dims = inputs[0].size(); + numChannels = 1; + if (inputs.size() > 1) + { + for (const size_t& dim : inputs[1]) + numChannels *= dim; + } +#endif return true; } @@ -68,7 +82,8 @@ public: backendId == DNN_BACKEND_CUDA || backendId == DNN_BACKEND_HALIDE || (backendId == DNN_BACKEND_INFERENCE_ENGINE_NN_BUILDER_2019 && axis == 1 && !blobs.empty()) || - (backendId == DNN_BACKEND_INFERENCE_ENGINE_NGRAPH && axis > 0); + (backendId == DNN_BACKEND_INFERENCE_ENGINE_NGRAPH && axis > 0) || + (backendId == DNN_BACKEND_WEBNN && axis >0); } template @@ -375,6 +390,48 @@ public: } #endif // HAVE_DNN_NGRAPH +#ifdef HAVE_WEBNN + virtual Ptr initWebnn(const std::vector >& inputs, const std::vector >& nodes) CV_OVERRIDE + { + Ptr node = nodes[0].dynamicCast(); + auto& webnnInpOperand0 = node->operand; + auto& webnnGraphBuilder = node->net->builder; + auto webnnInpOperand1 = nodes.size() > 1 ? nodes[1].dynamicCast()->operand : nullptr; + auto webnnInpOperand2 = nodes.size() > 2 ? nodes[1].dynamicCast()->operand : nullptr; + std::vector shape(dims, 1); + + size_t channels = 1; + if (blobs.empty()) + channels = numChannels; + else + channels = blobs[0].total(); + + int cAxis = normalize_axis(axis, shape.size()); + shape[cAxis] = channels; + + ml::Operand operand = webnnInpOperand0; + if (hasWeights) + { + ml::Operand webnnWeights = blobs.empty() ? webnnInpOperand1 : webnn::BuildConstant(webnnGraphBuilder, webnn::getShape(blobs[0]), blobs[0].data, blobs[0].total()*blobs[0].elemSize(), ml::OperandType::Float32); + webnnWeights = webnnGraphBuilder.Reshape(webnnWeights, shape.data(), shape.size()); + operand = webnnGraphBuilder.Mul(operand, webnnWeights); + } + if (hasBias) + { + ml::Operand webnnBias; + if(!hasWeights) + webnnBias = blobs.empty() ? webnnInpOperand1 : webnn::BuildConstant(webnnGraphBuilder, webnn::getShape(blobs.back()), blobs.back().data, blobs.back().total()*blobs.back().elemSize(), ml::OperandType::Float32); + else + webnnBias = blobs.empty() ? webnnInpOperand2 : webnn::BuildConstant(webnnGraphBuilder, webnn::getShape(blobs.back()), blobs.back().data, blobs.back().total()*blobs.back().elemSize(), ml::OperandType::Float32); + webnnBias = webnnGraphBuilder.Reshape(webnnBias, shape.data(), shape.size()); + operand = webnnGraphBuilder.Add(operand, webnnBias); + } + + return Ptr(new WebnnBackendNode(operand)); + } +#endif + + void getScaleShift(Mat& scale, Mat& shift) const CV_OVERRIDE { scale = (hasWeights && !blobs.empty()) ? blobs[0] : Mat(); diff --git a/modules/dnn/src/layers/softmax_layer.cpp b/modules/dnn/src/layers/softmax_layer.cpp index e937e98f8c..db2951808f 100644 --- a/modules/dnn/src/layers/softmax_layer.cpp +++ b/modules/dnn/src/layers/softmax_layer.cpp @@ -47,9 +47,11 @@ #include "../op_inf_engine.hpp" #include "../ie_ngraph.hpp" #include "../op_vkcom.hpp" +#include "../op_webnn.hpp" #include #include +#include using std::max; #ifdef HAVE_OPENCL @@ -97,6 +99,16 @@ public: virtual bool supportBackend(int backendId) CV_OVERRIDE { +#ifdef HAVE_WEBNN + if (backendId == DNN_BACKEND_WEBNN) { + // TODO: support logSoftMax + if (logSoftMax) + { + CV_LOG_WARNING(NULL, "logSoftMax is not supported by WebNN backend.") + } + return !logSoftMax; + } +#endif return backendId == DNN_BACKEND_OPENCV || backendId == DNN_BACKEND_CUDA || (backendId == DNN_BACKEND_HALIDE && haveHalide() && axisRaw == 1) || @@ -390,6 +402,18 @@ public: return true; } +#ifdef HAVE_WEBNN + virtual Ptr initWebnn(const std::vector >& inputs, const std::vector >& nodes) CV_OVERRIDE + { + Ptr node = nodes[0].dynamicCast(); + auto& webnnInpOperand = node->operand; + auto& webnnGraphBuilder = node->net->builder; + auto operand = webnnGraphBuilder.Softmax(webnnInpOperand); + return Ptr(new WebnnBackendNode(operand)); + } + +#endif + int64 getFLOPS(const std::vector &inputs, const std::vector &outputs) const CV_OVERRIDE { diff --git a/modules/dnn/src/op_webnn.cpp b/modules/dnn/src/op_webnn.cpp new file mode 100644 index 0000000000..4dba55bcbe --- /dev/null +++ b/modules/dnn/src/op_webnn.cpp @@ -0,0 +1,249 @@ +// 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. + +#include +#include "op_webnn.hpp" + +#include +#include + +#include "opencv2/core/utils/filesystem.hpp" +#include "opencv2/core/utils/filesystem.private.hpp" + +#include + +namespace cv { namespace dnn { + +#ifdef HAVE_WEBNN + +namespace webnn { +ml::Operand BuildConstant(const ml::GraphBuilder& builder, + const std::vector& dimensions, + const void* value, + size_t size, + ml::OperandType type) { + ml::OperandDescriptor desc; + desc.type = type; + desc.dimensions = dimensions.data(); + desc.dimensionsCount = (uint32_t)dimensions.size(); + ml::ArrayBufferView resource; + resource.buffer = const_cast(value); + resource.byteLength = size; + return builder.Constant(&desc, &resource); + } +} + +static std::string kDefaultInpLayerName = "opencv_webnn_empty_inp_layer_name"; + +static std::vector > +webnnWrappers(const std::vector >& ptrs) +{ + std::vector > wrappers(ptrs.size()); + for (int i = 0; i < ptrs.size(); ++i) + { + CV_Assert(!ptrs[i].empty()); + wrappers[i] = ptrs[i].dynamicCast(); + CV_Assert(!wrappers[i].empty()); + } + return wrappers; +} + +// WebnnNet +WebnnNet::WebnnNet() +{ + hasNetOwner = false; + device_name = "CPU"; + +#ifdef __EMSCRIPTEN__ + context = ml::Context(emscripten_webnn_create_context()); +#else + WebnnProcTable backendProcs = webnn_native::GetProcs(); + webnnProcSetProcs(&backendProcs); + context = ml::Context(webnn_native::CreateContext()); +#endif + builder = ::ml::CreateGraphBuilder(context); + namedOperands = ::ml::CreateNamedOperands(); +} + +void WebnnNet::addOutput(const std::string& name) +{ + requestedOutputs.push_back(name); +} + +void WebnnNet::createNet(Target targetId) { + init(targetId); +} + +void WebnnNet::init(Target targetId) +{ + switch (targetId) + { + case DNN_TARGET_CPU: + device_name = "CPU"; + break; + case DNN_TARGET_OPENCL: + device_name = "GPU"; + break; + default: + CV_Error(Error::StsNotImplemented, "Unknown target"); + }; + + graph = builder.Build(namedOperands); + CV_Assert(graph!=nullptr); + isInit = true; +} + +std::vector WebnnNet::setInputs(const std::vector& inputs, + const std::vector& names) { + CV_Assert_N(inputs.size() == names.size()); + std::vector current_inp; + for (size_t i = 0; i < inputs.size(); i++) + { + auto& m = inputs[i]; + + std::vector dimensions = webnn::getShape(m); + ml::OperandDescriptor descriptor; + descriptor.dimensions = dimensions.data(); + descriptor.dimensionsCount = dimensions.size(); + if (m.type() == CV_32F) + { + descriptor.type = ml::OperandType::Float32; + } + else + { + CV_Error(Error::StsNotImplemented, format("Unsupported data type %s", typeToString(m.type()).c_str())); + } + ml::Operand inputOperand = builder.Input(names[i].c_str(), &descriptor); + current_inp.push_back(std::move(inputOperand)); + } + inputNames = names; + return current_inp; +} + +void WebnnNet::setUnconnectedNodes(Ptr& node) { + outputNames.push_back(node->name); + namedOperands.Set(outputNames.back().c_str(), node->operand); +} + +bool WebnnNet::isInitialized() +{ + return isInit; +} + +void WebnnNet::reset() +{ + allBlobs.clear(); + isInit = false; +} + +void WebnnNet::addBlobs(const std::vector >& ptrs) +{ + auto wrappers = webnnWrappers(ptrs); + for (const auto& wrapper : wrappers) + { + std::string name = wrapper->name; + name = name.empty() ? kDefaultInpLayerName : name; + allBlobs.insert({name, wrapper}); + } +} + +void WebnnNet::forward(const std::vector >& outBlobsWrappers, bool isAsync) +{ + CV_LOG_DEBUG(NULL, "WebnnNet::forward(" << (isAsync ? "async" : "sync") << ")"); + ml::NamedInputs named_inputs = ::ml::CreateNamedInputs(); + std::vector inputs(inputNames.size()); + for (int i = 0; i < inputNames.size(); ++i) { + const std::string& name = inputNames[i]; + ml::Input& input = inputs[i]; + auto blobIt = allBlobs.find(name); + CV_Assert(blobIt != allBlobs.end()); + const Ptr wrapper = blobIt->second; + input.resource.buffer = wrapper->host->data; + input.resource.byteLength = wrapper->size; + named_inputs.Set(name.c_str(), &input); + } + std::vector > outs = webnnWrappers(outBlobsWrappers); + ml::NamedOutputs named_outputs = ::ml::CreateNamedOutputs(); + std::vector outputs(outs.size()); + for (int i = 0; i < outs.size(); ++i) { + const std::string& name = outs[i]->name; + ml::ArrayBufferView& output = outputs[i]; + output.buffer = outs[i]->host->data; + // std::cout<<"host data size: "<host->total()*outs[i]->host->elemSize()<size; + // std::cout<<"outs[i]->size: "<< outs[i]->size << std::endl; + named_outputs.Set(name.c_str(), &output); + } + ml::ComputeGraphStatus status = graph.Compute(named_inputs, named_outputs); + if (status != ::ml::ComputeGraphStatus::Success) { + CV_Error(Error::StsAssert, format("Failed to compute: %d", int(status))); + } +} + +// WebnnBackendNode +WebnnBackendNode::WebnnBackendNode(ml::Operand&& _operand) + : BackendNode(DNN_BACKEND_WEBNN), operand(std::move(_operand)) {} + +WebnnBackendNode::WebnnBackendNode(ml::Operand& _operand) + : BackendNode(DNN_BACKEND_WEBNN), operand(_operand) {} + +// WebnnBackendWrapper +WebnnBackendWrapper::WebnnBackendWrapper(int targetId, cv::Mat& m) + : BackendWrapper(DNN_BACKEND_WEBNN, targetId) +{ + size = m.total() * m.elemSize(); + // buffer.reset(new char[size]); + // std::memcpy(buffer.get(), m.data, size); + // dimensions = getShape(m); + // descriptor.dimensions = dimensions.data(); + // descriptor.dimensionsCount = dimensions.size(); + if (m.type() == CV_32F) + { + descriptor.type = ml::OperandType::Float32; + } + else + { + CV_Error(Error::StsNotImplemented, format("Unsupported data type %s", typeToString(m.type()).c_str())); + } + host = &m; +} + +WebnnBackendWrapper::~WebnnBackendWrapper() +{ + // nothing +} + +void WebnnBackendWrapper::copyToHost() +{ + CV_LOG_DEBUG(NULL, "WebnnBackendWrapper::copyToHost()"); + //CV_Error(Error::StsNotImplemented, ""); +} + +void WebnnBackendWrapper::setHostDirty() +{ + CV_LOG_DEBUG(NULL, "WebnnBackendWrapper::setHostDirty()"); + //CV_Error(Error::StsNotImplemented, ""); +} + +void forwardWebnn(const std::vector >& outBlobsWrappers, + Ptr& node, bool isAsync) +{ + CV_Assert(!node.empty()); + Ptr webnnNode = node.dynamicCast(); + CV_Assert(!webnnNode.empty()); + webnnNode->net->forward(outBlobsWrappers, isAsync); +} + + +#else +void forwardWebnn(const std::vector >& outBlobsWrappers, + Ptr& operand, bool isAsync) +{ + CV_Assert(false && "WebNN is not enabled in this OpenCV build"); +} + +#endif + +} +} \ No newline at end of file diff --git a/modules/dnn/src/op_webnn.hpp b/modules/dnn/src/op_webnn.hpp new file mode 100644 index 0000000000..5b77b10827 --- /dev/null +++ b/modules/dnn/src/op_webnn.hpp @@ -0,0 +1,171 @@ +// 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. + +#ifndef __OPENCV_DNN_OP_WEBNN_HPP__ +#define __OPENCV_DNN_OP_WEBNN_HPP__ + +#include "opencv2/core/cvdef.h" +#include "opencv2/core/cvstd.hpp" +#include "opencv2/dnn.hpp" + +#ifdef HAVE_WEBNN + +#include +#include +#ifdef __EMSCRIPTEN__ +#include +#include +#include +#else +#include +#include +#endif + +#include +#include + +#endif // HAVE_WEBNN + +namespace cv { namespace dnn { + +constexpr bool haveWebnn() { +#ifdef HAVE_WEBNN + return true; +#else + return false; +#endif +} + +#ifdef HAVE_WEBNN + +class WebnnBackendNode; +class WebnnBackendWrapper; + +namespace webnn { +inline std::vector getShape(const Mat& mat) +{ + std::vector result(mat.dims); + for (int i = 0; i < mat.dims; i++) + result[i] = (int32_t)mat.size[i]; + return result; +} + +ml::Operand BuildConstant(const ml::GraphBuilder& builder, + const std::vector& dimensions, + const void* value, + size_t size, + ml::OperandType type); + +struct Pool2dOptions { + public: + std::vector windowDimensions; + std::vector padding; + std::vector strides; + std::vector dilations; + ml::AutoPad autoPad = ml::AutoPad::Explicit; + ml::InputOperandLayout layout = ml::InputOperandLayout::Nchw; + + const ml::Pool2dOptions* AsPtr() { + if (!windowDimensions.empty()) { + mOptions.windowDimensionsCount = windowDimensions.size(); + mOptions.windowDimensions = windowDimensions.data(); + } + if (!padding.empty()) { + mOptions.paddingCount = padding.size(); + mOptions.padding = padding.data(); + } + if (!strides.empty()) { + mOptions.stridesCount = strides.size(); + mOptions.strides = strides.data(); + } + if (!dilations.empty()) { + mOptions.dilationsCount = dilations.size(); + mOptions.dilations = dilations.data(); + } + mOptions.layout = layout; + mOptions.autoPad = autoPad; + return &mOptions; + } + + private: + ml::Pool2dOptions mOptions; + }; +} + +class WebnnNet +{ +public: + WebnnNet(); + + void addOutput(const std::string& name); + + bool isInitialized(); + void init(Target targetId); + + void forward(const std::vector >& outBlobsWrappers, bool isAsync); + + std::vector setInputs(const std::vector& inputs, const std::vector& names); + + void setUnconnectedNodes(Ptr& node); + void addBlobs(const std::vector >& ptrs); + + void createNet(Target targetId); + // void setNodePtr(std::shared_ptr* ptr); + + void reset(); + + ml::GraphBuilder builder; + ml::Context context; + ml::Graph graph; + + std::unordered_map> allBlobs; + + bool hasNetOwner; + std::string device_name; + bool isInit = false; + + std::vector requestedOutputs; + + std::vector inputNames; + std::vector outputNames; + ml::NamedOperands namedOperands; +}; + +class WebnnBackendNode : public BackendNode +{ +public: + WebnnBackendNode(ml::Operand&& operand); + WebnnBackendNode(ml::Operand& operand); + + std::string name; + ml::Operand operand; + Ptr net; +}; + +class WebnnBackendWrapper : public BackendWrapper +{ +public: + WebnnBackendWrapper(int targetId, Mat& m); + ~WebnnBackendWrapper(); + + virtual void copyToHost() CV_OVERRIDE; + virtual void setHostDirty() CV_OVERRIDE; + + std::string name; + Mat* host; + std::unique_ptr buffer; + size_t size; + std::vector dimensions; + ml::OperandDescriptor descriptor; +}; + +#endif // HAVE_WebNN + +void forwardWebnn(const std::vector >& outBlobsWrappers, + Ptr& node, bool isAsync); + +}} // namespace cv::dnn + + +#endif // __OPENCV_DNN_OP_WEBNN_HPP__ diff --git a/modules/dnn/src/webnn/README.md b/modules/dnn/src/webnn/README.md new file mode 100644 index 0000000000..6c6544a65d --- /dev/null +++ b/modules/dnn/src/webnn/README.md @@ -0,0 +1,11 @@ +## Build Instructions + +### Build WebNN-native and set the environment variable + +Refer to [WebNN's build instructions](https://github.com/webmachinelearning/webnn-native) to complete the build of WebNN-native. + +Set environment variable `WEBNN_NATIVE_DIR` to enable native DNN_BACKEND_WEBNN build: `export WEBNN_NATIVE_DIR=${PATH_TO_WebNN}`. Please let `WEBNN_NATIVE_DIR` points the output directory of webnn-native build (e.g. webnn-native/out/Release). + +### Test native DNN_BACKEND_WEBNN backend +Add -DWITH_WEBNN=ON to the cmake command to build the WebNN module such as: +`cmake -DWITH_WEBNN=ON ../opencv` (according to the @ref tutorial_linux_install) \ No newline at end of file diff --git a/modules/dnn/test/test_common.hpp b/modules/dnn/test/test_common.hpp index 139f3d1671..f20aa507c1 100644 --- a/modules/dnn/test/test_common.hpp +++ b/modules/dnn/test/test_common.hpp @@ -135,7 +135,8 @@ testing::internal::ParamGenerator< tuple > dnnBackendsAndTarget bool withCpuOCV = true, bool withVkCom = true, bool withCUDA = true, - bool withNgraph = true + bool withNgraph = true, + bool withWebnn = true ); testing::internal::ParamGenerator< tuple > dnnBackendsAndTargetsIE(); diff --git a/modules/dnn/test/test_common.impl.hpp b/modules/dnn/test/test_common.impl.hpp index 3d56e6f308..c312474256 100644 --- a/modules/dnn/test/test_common.impl.hpp +++ b/modules/dnn/test/test_common.impl.hpp @@ -29,6 +29,7 @@ void PrintTo(const cv::dnn::Backend& v, std::ostream* os) case DNN_BACKEND_CUDA: *os << "CUDA"; return; case DNN_BACKEND_INFERENCE_ENGINE_NN_BUILDER_2019: *os << "DLIE"; return; case DNN_BACKEND_INFERENCE_ENGINE_NGRAPH: *os << "NGRAPH"; return; + case DNN_BACKEND_WEBNN: *os << "WEBNN"; return; } // don't use "default:" to emit compiler warnings *os << "DNN_BACKEND_UNKNOWN(" << (int)v << ")"; } @@ -247,7 +248,8 @@ testing::internal::ParamGenerator< tuple > dnnBackendsAndTarget bool withCpuOCV /*= true*/, bool withVkCom /*= true*/, bool withCUDA /*= true*/, - bool withNgraph /*= true*/ + bool withNgraph /*= true*/, + bool withWebnn /*= false*/ ) { #ifdef HAVE_INF_ENGINE @@ -302,6 +304,17 @@ testing::internal::ParamGenerator< tuple > dnnBackendsAndTarget } #endif +#ifdef HAVE_WEBNN + if (withWebnn) + { + for (auto target : getAvailableTargets(DNN_BACKEND_WEBNN)) { + targets.push_back(make_tuple(DNN_BACKEND_WEBNN, target)); + } + } +#else + CV_UNUSED(withWebnn); +#endif + { available = getAvailableTargets(DNN_BACKEND_OPENCV); for (std::vector< Target >::const_iterator i = available.begin(); i != available.end(); ++i) diff --git a/platforms/js/build_js.py b/platforms/js/build_js.py index f490eb58d5..64dc1a6c67 100644 --- a/platforms/js/build_js.py +++ b/platforms/js/build_js.py @@ -67,6 +67,8 @@ class Builder: self.options = options self.build_dir = check_dir(options.build_dir, create=True) self.opencv_dir = check_dir(options.opencv_dir) + print('-----------------------------------------------------------') + print('options.opencv_dir:', options.opencv_dir) self.emscripten_dir = check_dir(options.emscripten_dir) def get_toolchain_file(self): @@ -84,6 +86,7 @@ class Builder: "-DCMAKE_BUILD_TYPE=Release", "-DCMAKE_TOOLCHAIN_FILE='%s'" % self.get_toolchain_file(), "-DCPU_BASELINE=''", + "-DCMAKE_INSTALL_PREFIX=/usr/local", "-DCPU_DISPATCH=''", "-DCV_TRACE=OFF", "-DBUILD_SHARED_LIBS=OFF", @@ -136,10 +139,10 @@ class Builder: "-DBUILD_opencv_js=ON", "-DBUILD_opencv_python2=OFF", "-DBUILD_opencv_python3=OFF", - "-DBUILD_EXAMPLES=OFF", + "-DBUILD_EXAMPLES=ON", "-DBUILD_PACKAGE=OFF", - "-DBUILD_TESTS=OFF", - "-DBUILD_PERF_TESTS=OFF"] + "-DBUILD_TESTS=ON", + "-DBUILD_PERF_TESTS=ON"] if self.options.cmake_option: cmd += self.options.cmake_option if self.options.build_doc: @@ -162,6 +165,9 @@ class Builder: else: cmd.append("-DBUILD_WASM_INTRIN_TESTS=OFF") + if self.options.webnn: + cmd.append("-DWITH_WEBNN=ON") + flags = self.get_build_flags() if flags: cmd += ["-DCMAKE_C_FLAGS='%s'" % flags, @@ -184,6 +190,8 @@ class Builder: flags += "-msimd128 " if self.options.build_flags: flags += self.options.build_flags + if self.options.webnn: + flags += "-s USE_WEBNN=1 " return flags def config(self): @@ -243,6 +251,7 @@ if __name__ == "__main__": # Write a path to modify file like argument of this flag parser.add_argument('--config', default=os.path.join(os.path.dirname(os.path.abspath(__file__)), 'opencv_js.config.py'), help="Specify configuration file with own list of exported into JS functions") + parser.add_argument('--webnn', action="store_true", help="Enable WebNN Backend") args = parser.parse_args() diff --git a/platforms/js/opencv_js.config.py b/platforms/js/opencv_js.config.py index 1ed28af89f..20380fc970 100644 --- a/platforms/js/opencv_js.config.py +++ b/platforms/js/opencv_js.config.py @@ -129,7 +129,7 @@ video = { 'TrackerMIL_Params': [], } -dnn = {'dnn_Net': ['setInput', 'forward'], +dnn = {'dnn_Net': ['setInput', 'forward', 'setPreferableBackend'], '': ['readNetFromCaffe', 'readNetFromTensorflow', 'readNetFromTorch', 'readNetFromDarknet', 'readNetFromONNX', 'readNet', 'blobFromImage']} diff --git a/samples/dnn/classification.cpp b/samples/dnn/classification.cpp index 769d6874be..51893666ab 100644 --- a/samples/dnn/classification.cpp +++ b/samples/dnn/classification.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include @@ -17,6 +18,7 @@ std::string keys = "{ std | 0.0 0.0 0.0 | Preprocess input image by dividing on a standard deviation.}" "{ crop | false | Preprocess input image by center cropping.}" "{ framework f | | Optional name of an origin framework of the model. Detect it automatically if it does not set. }" + "{ needSoftmax | false | Use Softmax to post-process the output of the net.}" "{ classes | | Optional path to a text file with names of classes. }" "{ backend | 0 | Choose one of computation backends: " "0: automatically (by default), " @@ -24,7 +26,8 @@ std::string keys = "2: Intel's Deep Learning Inference Engine (https://software.intel.com/openvino-toolkit), " "3: OpenCV implementation, " "4: VKCOM, " - "5: CUDA }," + "5: CUDA, " + "6: WebNN }" "{ target | 0 | Choose one of target computation devices: " "0: CPU target (by default), " "1: OpenCL, " @@ -70,6 +73,9 @@ int main(int argc, char** argv) String framework = parser.get("framework"); int backendId = parser.get("backend"); int targetId = parser.get("target"); + bool needSoftmax = parser.get("needSoftmax"); + std::cout<<"mean: "< layersTimes; - double freq = getTickFrequency() / 1000; - double t = net.getPerfProfile(layersTimes) / freq; - std::string label = format("Inference time: %.2f ms", t); + timeRecorder.reset(); + for(int i = 0; i < 200; i++) { + //! [Make forward pass] + timeRecorder.start(); + prob = net.forward(); + timeRecorder.stop(); + + //! [Get a class with a highest score] + Point classIdPoint; + minMaxLoc(prob.reshape(1, 1), 0, &confidence, 0, &classIdPoint); + classId = classIdPoint.x; + //! [Get a class with a highest score] + + // Put efficiency information. + // std::vector layersTimes; + // double freq = getTickFrequency() / 1000; + // t = net.getPerfProfile(layersTimes) / freq; + // t_sum += t; + } + if (needSoftmax == true) + { + float maxProb = 0.0; + float sum = 0.0; + Mat softmaxProb; + + maxProb = *std::max_element(prob.begin(), prob.end()); + cv::exp(prob-maxProb, softmaxProb); + sum = (float)cv::sum(softmaxProb)[0]; + softmaxProb /= sum; + Point classIdPoint; + minMaxLoc(softmaxProb.reshape(1, 1), 0, &confidence, 0, &classIdPoint); + classId = classIdPoint.x; + } + std::string label = format("Inference time of 1 round: %.2f ms", t1); + std::string label2 = format("Average time of 200 rounds: %.2f ms", timeRecorder.getTimeMilli()/200); putText(frame, label, Point(0, 15), FONT_HERSHEY_SIMPLEX, 0.5, Scalar(0, 255, 0)); + putText(frame, label2, Point(0, 35), FONT_HERSHEY_SIMPLEX, 0.5, Scalar(0, 255, 0)); // Print predicted class. label = format("%s: %.4f", (classes.empty() ? format("Class #%d", classId).c_str() : classes[classId].c_str()), confidence); - putText(frame, label, Point(0, 40), FONT_HERSHEY_SIMPLEX, 0.5, Scalar(0, 255, 0)); + putText(frame, label, Point(0, 55), FONT_HERSHEY_SIMPLEX, 0.5, Scalar(0, 255, 0)); imshow(kWinName, frame); }