diff --git a/doc/installation.md b/doc/installation.md index a3c9097e..b568b84c 100644 --- a/doc/installation.md +++ b/doc/installation.md @@ -113,7 +113,7 @@ Any problem installing OpenPose? Check [doc/faq.md](./faq.md) and/or post a GitH ### Prerequisites 1. Ubuntu - **Anaconda should not be installed** on your system. Anaconda includes a Protobuf version that is incompatible with Caffe. Either you uninstall anaconda and install protobuf via apt-get, or you compile your own Caffe and link it to OpenPose. 2. Download and install CMake GUI: - - Ubuntu: run the command `sudo apt-get install cmake-qt-gui`. Note: If you prefer to use CMake through the command line, see [Cmake Command Line Build](#cmake-command-line-build-ubuntu-only). + - Ubuntu: run the command `sudo apt-get install cmake-qt-gui`. Note: If you prefer to use CMake through the command line, see [CMake Command Line Configuration (Ubuntu Only)](#cmake-command-line-configuration-ubuntu-only). - Windows: download and install the latest CMake win64-x64 msi installer from the [CMake website](https://cmake.org/download/), called `cmake-X.X.X-win64-x64.msi`. - Mac: `brew cask install cmake`. 3. Windows - **Microsoft Visual Studio (VS) 2015 Enterprise Update 3**: diff --git a/include/openpose/core/macros.hpp b/include/openpose/core/macros.hpp index 6df6038d..c6a5f068 100644 --- a/include/openpose/core/macros.hpp +++ b/include/openpose/core/macros.hpp @@ -7,6 +7,7 @@ #include #include // std::this_thread #include +#include // cv::Mat, check OpenCV version // OpenPose name and version const std::string OPEN_POSE_NAME_STRING = "OpenPose"; @@ -85,7 +86,16 @@ namespace boost } // Compabitility for OpenCV 4.0 while preserving 2.4.X and 3.X compatibility -#if (defined(CV_MAJOR_VERSION) && CV_MAJOR_VERSION == 4) +// Note: +// - CV_VERSION: 2.4.9.1 | 4.0.0-beta +// - CV_MAJOR_VERSION: 2 | 4 +// - CV_MINOR_VERSION: 4 | 0 +// - CV_SUBMINOR_VERSION: 9 | 0 +// - CV_VERSION_EPOCH: 2 | Not defined +#if (defined(CV_MAJOR_VERSION) && CV_MAJOR_VERSION > 3) + #define OPEN_CV_IS_4_OR_HIGHER +#endif +#ifdef OPEN_CV_IS_4_OR_HIGHER #define CV_BGR2GRAY cv::COLOR_BGR2GRAY #define CV_CALIB_CB_ADAPTIVE_THRESH cv::CALIB_CB_ADAPTIVE_THRESH #define CV_CALIB_CB_NORMALIZE_IMAGE cv::CALIB_CB_NORMALIZE_IMAGE diff --git a/include/openpose/net/headers.hpp b/include/openpose/net/headers.hpp index 93fceae0..997c26a1 100644 --- a/include/openpose/net/headers.hpp +++ b/include/openpose/net/headers.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include diff --git a/include/openpose/net/netOpenCv.hpp b/include/openpose/net/netOpenCv.hpp new file mode 100644 index 00000000..173d0b50 --- /dev/null +++ b/include/openpose/net/netOpenCv.hpp @@ -0,0 +1,34 @@ +#ifndef OPENPOSE_NET_NET_OPEN_CV_HPP +#define OPENPOSE_NET_NET_OPEN_CV_HPP + +#include +#include + +namespace op +{ + class OP_API NetOpenCv : public Net + { + public: + NetOpenCv(const std::string& caffeProto, const std::string& caffeTrainedModel, const int gpuId = 0); + + virtual ~NetOpenCv(); + + void initializationOnThread(); + + void forwardPass(const Array& inputNetData) const; + + boost::shared_ptr> getOutputBlob() const; + + private: + // PIMPL idiom + // http://www.cppsamples.com/common-tasks/pimpl.html + struct ImplNetOpenCv; + std::unique_ptr upImpl; + + // PIMP requires DELETE_COPY & destructor, or extra code + // http://oliora.github.io/2015/12/29/pimpl-and-rule-of-zero.html + DELETE_COPY(NetOpenCv); + }; +} + +#endif // OPENPOSE_NET_NET_OPEN_CV_HPP diff --git a/include/openpose/utilities/keypoint.hpp b/include/openpose/utilities/keypoint.hpp index 3ed1ed38..cd6a0f77 100644 --- a/include/openpose/utilities/keypoint.hpp +++ b/include/openpose/utilities/keypoint.hpp @@ -48,10 +48,10 @@ namespace op const T threshold); template - float getKeypointsROI(const Array& keypoints, const int personA, const int personB, const T threshold); + float getKeypointsRoi(const Array& keypoints, const int personA, const int personB, const T threshold); template - float getKeypointsROI(const Array& keypointsA, const int personA, const Array& keypointsB, const int personB, + float getKeypointsRoi(const Array& keypointsA, const int personA, const Array& keypointsB, const int personB, const T threshold); } diff --git a/python/openpose/_openpose.cpp b/python/openpose/_openpose.cpp index 363a33cd..ea7dd716 100644 --- a/python/openpose/_openpose.cpp +++ b/python/openpose/_openpose.cpp @@ -73,7 +73,7 @@ public: #ifdef USE_CUDA caffe::Caffe::set_mode(caffe::Caffe::GPU); caffe::Caffe::SetDevice(mGpuID); -#elif USE_OPENCL +#elif defined USE_OPENCL caffe::Caffe::set_mode(caffe::Caffe::GPU); std::vector devices; const int maxNumberGpu = op::OpenCL::getTotalGPU(); @@ -217,7 +217,7 @@ public: std::vector*> peaksBlobs{ peaksBlob.get() }; #ifdef USE_CUDA resizeAndMergeCaffe->Forward_gpu(caffeNetOutputBlobs, heatMapsBlobs); // ~5ms -#elif USE_OPENCL +#elif defined USE_OPENCL resizeAndMergeCaffe->Forward_ocl(caffeNetOutputBlobs, heatMapsBlobs); // ~5ms #else resizeAndMergeCaffe->Forward_cpu(caffeNetOutputBlobs, heatMapsBlobs); // ~5ms @@ -226,7 +226,7 @@ public: nmsCaffe->setThreshold((float)poseExtractorCaffe->get(op::PoseProperty::NMSThreshold)); #ifdef USE_CUDA nmsCaffe->Forward_gpu(heatMapsBlobs, peaksBlobs);// ~2ms -#elif USE_OPENCL +#elif defined USE_OPENCL nmsCaffe->Forward_ocl(heatMapsBlobs, peaksBlobs);// ~2ms #else nmsCaffe->Forward_cpu(heatMapsBlobs, peaksBlobs);// ~2ms diff --git a/src/openpose/core/cvMatToOpInput.cpp b/src/openpose/core/cvMatToOpInput.cpp index 7a2af322..b8a01cb4 100644 --- a/src/openpose/core/cvMatToOpInput.cpp +++ b/src/openpose/core/cvMatToOpInput.cpp @@ -1,3 +1,4 @@ +// #include #include #include #include @@ -31,13 +32,21 @@ namespace op std::vector> inputNetData(numberScales); for (auto i = 0u ; i < inputNetData.size() ; i++) { - inputNetData[i].reset({1, 3, netInputSizes.at(i).y, netInputSizes.at(i).x}); - std::vector scaleRatios(numberScales, 1.f); cv::Mat frameWithNetSize; resizeFixedAspectRatio(frameWithNetSize, cvInputData, scaleInputToNetInputs[i], netInputSizes[i]); // Fill inputNetData[i] + inputNetData[i].reset({1, 3, netInputSizes.at(i).y, netInputSizes.at(i).x}); uCharCvMatToFloatPtr(inputNetData[i].getPtr(), frameWithNetSize, (mPoseModel == PoseModel::BODY_19N ? 2 : 1)); + // // OpenCV equivalent + // const auto scale = 1/255.; + // const cv::Scalar mean{128,128,128}; + // const cv::Size outputSize{netInputSizes[i].x, netInputSizes[i].y}; + // // cv::Mat cvMat; + // cv::dnn::blobFromImage( + // // frameWithNetSize, cvMat, scale, outputSize, mean); + // frameWithNetSize, inputNetData[i].getCvMat(), scale, outputSize, mean); + // // log(cv::norm(cvMat - inputNetData[i].getCvMat())); // ~0.25 } return inputNetData; } diff --git a/src/openpose/net/CMakeLists.txt b/src/openpose/net/CMakeLists.txt index bddb58e0..66026658 100644 --- a/src/openpose/net/CMakeLists.txt +++ b/src/openpose/net/CMakeLists.txt @@ -7,6 +7,7 @@ set(SOURCES_OP_NET maximumBase.cu maximumCaffe.cpp netCaffe.cpp + netOpenCv.cpp nmsBase.cpp nmsBase.cu nmsBaseCL.cpp diff --git a/src/openpose/net/netCaffe.cpp b/src/openpose/net/netCaffe.cpp index e051e9c5..ed7c86dd 100644 --- a/src/openpose/net/netCaffe.cpp +++ b/src/openpose/net/netCaffe.cpp @@ -205,9 +205,8 @@ namespace op #elif defined USE_OPENCL auto* gpuImagePtr = upImpl->upCaffeNet->blobs().at(0)->mutable_gpu_data(); cl::Buffer imageBuffer = cl::Buffer((cl_mem)gpuImagePtr, true); - OpenCL::getInstance(upImpl->mGpuId)->getQueue().enqueueWriteBuffer(imageBuffer, true, 0, - inputData.getVolume() * sizeof(float), - inputData.getConstPtr()); + OpenCL::getInstance(upImpl->mGpuId)->getQueue().enqueueWriteBuffer( + imageBuffer, true, 0, inputData.getVolume() * sizeof(float), inputData.getConstPtr()); #else auto* cpuImagePtr = upImpl->upCaffeNet->blobs().at(0)->mutable_cpu_data(); std::copy(inputData.getConstPtr(), inputData.getConstPtr() + inputData.getVolume(), cpuImagePtr); diff --git a/src/openpose/net/netOpenCv.cpp b/src/openpose/net/netOpenCv.cpp new file mode 100644 index 00000000..455d1b06 --- /dev/null +++ b/src/openpose/net/netOpenCv.cpp @@ -0,0 +1,151 @@ +// Note: OpenCV only uses CPU or OpenCL (for Intel GPUs). Used CUDA for following blobs (Resize + NMS) +#include // OPEN_CV_IS_4_OR_HIGHER +#ifdef USE_CAFFE + #include +#endif +#if defined(USE_CAFFE) && defined(USE_CUDA) && defined(OPEN_CV_IS_4_OR_HIGHER) + #define OPEN_CV_DNN_AVAILABLE + #include + #include +#endif +#include // std::accumulate +#include +#include + +namespace op +{ + struct NetOpenCv::ImplNetOpenCv + { + #ifdef OPEN_CV_DNN_AVAILABLE + // Init with constructor + const int mGpuId; + const std::string mCaffeProto; + const std::string mCaffeTrainedModel; + // OpenCV DNN + cv::dnn::Net mNet; + cv::Mat mNetOutputBlob; + // std::shared_ptr> spOutputBlob; + boost::shared_ptr> spOutputBlob; + + ImplNetOpenCv(const std::string& caffeProto, const std::string& caffeTrainedModel, const int gpuId) : + mGpuId{gpuId}, + mCaffeProto{caffeProto}, + mCaffeTrainedModel{caffeTrainedModel}, + mNet{cv::dnn::readNetFromCaffe(caffeProto, caffeTrainedModel)}, + // spOutputBlob{std::make_shared>(1,1,1,1)} + spOutputBlob{new caffe::Blob(1,1,1,1)} + { + const std::string message{".\nPossible causes:\n\t1. Not downloading the OpenPose trained models." + "\n\t2. Not running OpenPose from the same directory where the `model`" + " folder is located.\n\t3. Using paths with spaces."}; + if (!existFile(mCaffeProto)) + error("Prototxt file not found: " + mCaffeProto + message, __LINE__, __FUNCTION__, __FILE__); + if (!existFile(mCaffeTrainedModel)) + error("Caffe trained model file not found: " + mCaffeTrainedModel + message, + __LINE__, __FUNCTION__, __FILE__); + + // Set GPU + mNet.setPreferableTarget(cv::dnn::DNN_TARGET_CPU); // 1.7 sec at -1x160 + // mNet.setPreferableTarget(cv::dnn::DNN_TARGET_OPENCL); // 1.2 sec at -1x160 + // mNet.setPreferableTarget(cv::dnn::DNN_TARGET_OPENCL_FP16); + // mNet.setPreferableTarget(cv::dnn::DNN_TARGET_MYRIAD); + // mNet.setPreferableTarget(cv::dnn::DNN_TARGET_VULKAN); + // // Set backen + // mNet.setPreferableBackend(cv::dnn::DNN_BACKEND_DEFAULT); + // mNet.setPreferableBackend(cv::dnn::DNN_BACKEND_HALIDE); + // mNet.setPreferableBackend(cv::dnn::DNN_BACKEND_INFERENCE_ENGINE); + // mNet.setPreferableBackend(cv::dnn::DNN_BACKEND_OPENCV); + // mNet.setPreferableBackend(cv::dnn::DNN_BACKEND_VKCOM); + } + #endif + }; + + #ifdef OPEN_CV_DNN_AVAILABLE + inline void reshapeNetOpenCv(caffe::Net* caffeNet, const std::vector& dimensions) + { + try + { + caffeNet->blobs()[0]->Reshape(dimensions); + caffeNet->Reshape(); + #ifdef USE_CUDA + cudaCheck(__LINE__, __FUNCTION__, __FILE__); + #endif + } + catch (const std::exception& e) + { + error(e.what(), __LINE__, __FUNCTION__, __FILE__); + } + } + #endif + + NetOpenCv::NetOpenCv(const std::string& caffeProto, const std::string& caffeTrainedModel, const int gpuId) + #ifdef OPEN_CV_DNN_AVAILABLE + : upImpl{new ImplNetOpenCv{caffeProto, caffeTrainedModel, gpuId}} + #endif + { + try + { + #ifndef OPEN_CV_DNN_AVAILABLE + UNUSED(caffeProto); + UNUSED(caffeTrainedModel); + UNUSED(gpuId); + error("OpenPose must be compiled with the `USE_CAFFE` macro definition in order to use this" + " functionality.", __LINE__, __FUNCTION__, __FILE__); + #endif + } + catch (const std::exception& e) + { + error(e.what(), __LINE__, __FUNCTION__, __FILE__); + } + } + + NetOpenCv::~NetOpenCv() + { + } + + void NetOpenCv::initializationOnThread() + { + } + + void NetOpenCv::forwardPass(const Array& inputData) const + { + try + { + #ifdef OPEN_CV_DNN_AVAILABLE + upImpl->mNet.setInput(inputData.getConstCvMat()); + upImpl->mNetOutputBlob = upImpl->mNet.forward(); // 99% of the runtime here + std::vector outputSize(upImpl->mNetOutputBlob.dims,0); + for (auto i = 0u ; i < outputSize.size() ; i++) + outputSize[i] = upImpl->mNetOutputBlob.size[i]; + upImpl->spOutputBlob->Reshape(outputSize); + auto* gpuImagePtr = upImpl->spOutputBlob->mutable_gpu_data(); + cudaMemcpy(gpuImagePtr, (float*)upImpl->mNetOutputBlob.data, + upImpl->spOutputBlob->count() * sizeof(float), + cudaMemcpyHostToDevice); + #else + UNUSED(inputData); + #endif + } + catch (const std::exception& e) + { + error(e.what(), __LINE__, __FUNCTION__, __FILE__); + } + } + + boost::shared_ptr> NetOpenCv::getOutputBlob() const + { + try + { + #ifdef OPEN_CV_DNN_AVAILABLE + return upImpl->spOutputBlob; + #else + return nullptr; + #endif + } + catch (const std::exception& e) + { + error(e.what(), __LINE__, __FUNCTION__, __FILE__); + return nullptr; + } + } +} diff --git a/src/openpose/net/nmsCaffe.cpp b/src/openpose/net/nmsCaffe.cpp index c17ccc78..209eb591 100644 --- a/src/openpose/net/nmsCaffe.cpp +++ b/src/openpose/net/nmsCaffe.cpp @@ -149,7 +149,7 @@ namespace op #ifdef USE_CUDA Forward_gpu(bottom, top); // OpenCL - #elif USE_OPENCL + #elif defined USE_OPENCL Forward_ocl(bottom, top); // CPU #else diff --git a/src/openpose/net/resizeAndMergeCaffe.cpp b/src/openpose/net/resizeAndMergeCaffe.cpp index 81d0ab1f..d7f96559 100644 --- a/src/openpose/net/resizeAndMergeCaffe.cpp +++ b/src/openpose/net/resizeAndMergeCaffe.cpp @@ -1,13 +1,13 @@ #ifdef USE_CAFFE #include #endif -#include -#include -#include #ifdef USE_OPENCL #include #include #endif +#include +#include +#include namespace op { @@ -135,7 +135,7 @@ namespace op #ifdef USE_CUDA Forward_gpu(bottom, top); // OpenCL - #elif USE_OPENCL + #elif defined USE_OPENCL Forward_ocl(bottom, top); // CPU #else diff --git a/src/openpose/pose/poseExtractorCaffe.cpp b/src/openpose/pose/poseExtractorCaffe.cpp index d3bcc105..bc8ec758 100644 --- a/src/openpose/pose/poseExtractorCaffe.cpp +++ b/src/openpose/pose/poseExtractorCaffe.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -23,13 +24,13 @@ namespace op struct PoseExtractorCaffe::ImplPoseExtractorCaffe { #ifdef USE_CAFFE - // Used when increasing spCaffeNets + // Used when increasing spNets const PoseModel mPoseModel; const int mGpuId; const std::string mModelFolder; const bool mEnableGoogleLogging; // General parameters - std::vector> spCaffeNets; + std::vector> spNets; std::shared_ptr> spResizeAndMergeCaffe; std::shared_ptr> spNmsCaffe; std::shared_ptr> spBodyPartConnectorCaffe; @@ -114,7 +115,7 @@ namespace op } } - void addCaffeNetOnThread(std::vector>& netCaffe, + void addCaffeNetOnThread(std::vector>& net, std::vector>>& caffeNetOutputBlob, const PoseModel poseModel, const int gpuId, const std::string& modelFolder, const bool enableGoogleLogging) @@ -122,16 +123,23 @@ namespace op try { // Add Caffe Net - netCaffe.emplace_back( - std::make_shared(modelFolder + getPoseProtoTxt(poseModel), - modelFolder + getPoseTrainedModel(poseModel), - gpuId, enableGoogleLogging) - ); + net.emplace_back( + std::make_shared( + modelFolder + getPoseProtoTxt(poseModel), + modelFolder + getPoseTrainedModel(poseModel), + gpuId, enableGoogleLogging)); + // net.emplace_back( + // std::make_shared( + // modelFolder + getPoseProtoTxt(poseModel), + // modelFolder + getPoseTrainedModel(poseModel), + // gpuId)); + // UNUSED(enableGoogleLogging); // Initializing them on the thread - netCaffe.back()->initializationOnThread(); - caffeNetOutputBlob.emplace_back(netCaffe.back()->getOutputBlob()); + net.back()->initializationOnThread(); + caffeNetOutputBlob.emplace_back(((NetCaffe*)net.back().get())->getOutputBlob()); + // caffeNetOutputBlob.emplace_back(((NetOpenCv*)net.back().get())->getOutputBlob()); // Sanity check - if (netCaffe.size() != caffeNetOutputBlob.size()) + if (net.size() != caffeNetOutputBlob.size()) error("Weird error, this should not happen. Notify us.", __LINE__, __FUNCTION__, __FILE__); // Cuda check #ifdef USE_CUDA @@ -188,7 +196,7 @@ namespace op // Logging log("Starting initialization on thread.", Priority::Low, __LINE__, __FUNCTION__, __FILE__); // Initialize Caffe net - addCaffeNetOnThread(upImpl->spCaffeNets, upImpl->spCaffeNetOutputBlobs, upImpl->mPoseModel, + addCaffeNetOnThread(upImpl->spNets, upImpl->spCaffeNetOutputBlobs, upImpl->mPoseModel, upImpl->mGpuId, upImpl->mModelFolder, upImpl->mEnableGoogleLogging); #ifdef USE_CUDA cudaCheck(__LINE__, __FUNCTION__, __FILE__); @@ -231,8 +239,8 @@ namespace op // Resize std::vectors if required const auto numberScales = inputNetData.size(); upImpl->mNetInput4DSizes.resize(numberScales); - while (upImpl->spCaffeNets.size() < numberScales) - addCaffeNetOnThread(upImpl->spCaffeNets, upImpl->spCaffeNetOutputBlobs, upImpl->mPoseModel, + while (upImpl->spNets.size() < numberScales) + addCaffeNetOnThread(upImpl->spNets, upImpl->spCaffeNetOutputBlobs, upImpl->mPoseModel, upImpl->mGpuId, upImpl->mModelFolder, false); // Process each image @@ -240,7 +248,7 @@ namespace op { // 1. Caffe deep network // ~80ms - upImpl->spCaffeNets.at(i)->forwardPass(inputNetData[i]); + upImpl->spNets.at(i)->forwardPass(inputNetData[i]); // Reshape blobs if required // Note: In order to resize to input size to have same results as Matlab, uncomment the commented @@ -387,7 +395,7 @@ namespace op // Re-Process image // 1. Caffe deep network - upImpl->spCaffeNets.at(0)->forwardPass(inputNetDataRoi); + upImpl->spNets.at(0)->forwardPass(inputNetDataRoi); std::vector>> caffeNetOutputBlob{upImpl->spCaffeNetOutputBlobs[0]}; // Reshape blobs if (!vectorsAreEqual(upImpl->mNetInput4DSizes.at(0), inputNetDataRoi.getSize())) @@ -461,7 +469,7 @@ namespace op for (auto person2 = 0 ; person2 < poseKeypoints.getSize(0) ; person2++) { // Get ROI - const auto currentRoi = getKeypointsROI( + const auto currentRoi = getKeypointsRoi( mPoseKeypoints, person, poseKeypoints, person2, nmsThreshold); // Update person if (personRoi < currentRoi @@ -527,7 +535,7 @@ namespace op // if (person != person2) // { // // Get ROI - // const auto currentRoi = getKeypointsROI( + // const auto currentRoi = getKeypointsRoi( // mPoseKeypoints, person, person2, nmsThreshold); // // Update person // if (currentRoi > 0.f) diff --git a/src/openpose/tracking/pyramidalLK.cu b/src/openpose/tracking/pyramidalLK.cu index 26327bd8..e88c7c41 100644 --- a/src/openpose/tracking/pyramidalLK.cu +++ b/src/openpose/tracking/pyramidalLK.cu @@ -8,7 +8,7 @@ #if (defined(CV_VERSION_EPOCH) && CV_VERSION_EPOCH == 2) #include #define cvCuda cv::gpu - // OpenCV 3.X + // OpenCV > 2 #else #include #include diff --git a/src/openpose/utilities/keypoint.cpp b/src/openpose/utilities/keypoint.cpp index a9cc61d2..c1bf3b4f 100644 --- a/src/openpose/utilities/keypoint.cpp +++ b/src/openpose/utilities/keypoint.cpp @@ -496,11 +496,11 @@ namespace op const double threshold); template - float getKeypointsROI(const Array& keypoints, const int personA, const int personB, const T threshold) + float getKeypointsRoi(const Array& keypoints, const int personA, const int personB, const T threshold) { try { - return getKeypointsROI(keypoints, personA, keypoints, personB, threshold); + return getKeypointsRoi(keypoints, personA, keypoints, personB, threshold); } catch (const std::exception& e) { @@ -508,13 +508,13 @@ namespace op return 0.f; } } - template OP_API float getKeypointsROI( + template OP_API float getKeypointsRoi( const Array& keypoints, const int personA, const int personB, const float threshold); - template OP_API float getKeypointsROI( + template OP_API float getKeypointsRoi( const Array& keypoints, const int personA, const int personB, const double threshold); template - float getKeypointsROI(const Array& keypointsA, const int personA, const Array& keypointsB, const int personB, + float getKeypointsRoi(const Array& keypointsA, const int personA, const Array& keypointsB, const int personB, const T threshold) { try @@ -560,10 +560,10 @@ namespace op return 0.f; } } - template OP_API float getKeypointsROI( + template OP_API float getKeypointsRoi( const Array& keypointsA, const int personA, const Array& keypointsB, const int personB, const float threshold); - template OP_API float getKeypointsROI( + template OP_API float getKeypointsRoi( const Array& keypointsA, const int personA, const Array& keypointsB, const int personB, const double threshold); }