mirror of https://github.com/opencv/opencv.git
Open Source Computer Vision Library
https://opencv.org/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
783 lines
32 KiB
783 lines
32 KiB
// This file is part of OpenCV project. |
|
// It is subject to the license terms in the LICENSE file found in the top-level |
|
// directory of this distribution and at http://opencv.org/license.html. |
|
// |
|
// Copyright (C) 2018-2019 Intel Corporation |
|
|
|
#include <opencv2/gapi.hpp> |
|
#include <opencv2/gapi/core.hpp> |
|
#include <opencv2/gapi/imgproc.hpp> |
|
#include <opencv2/gapi/fluid/core.hpp> |
|
#include <opencv2/gapi/infer.hpp> |
|
#include <opencv2/gapi/infer/ie.hpp> |
|
#include <opencv2/gapi/cpu/gcpukernel.hpp> |
|
#include "opencv2/gapi/streaming/cap.hpp" |
|
|
|
#include <opencv2/videoio.hpp> |
|
#include <opencv2/highgui.hpp> |
|
#include <iomanip> |
|
|
|
namespace config |
|
{ |
|
constexpr char kWinFaceBeautification[] = "FaceBeautificator"; |
|
constexpr char kWinInput[] = "Input"; |
|
const cv::Scalar kClrWhite (255, 255, 255); |
|
const cv::Scalar kClrGreen ( 0, 255, 0); |
|
const cv::Scalar kClrYellow( 0, 255, 255); |
|
|
|
constexpr float kConfThresh = 0.7f; |
|
|
|
const cv::Size kGKernelSize(5, 5); |
|
constexpr double kGSigma = 0.0; |
|
constexpr int kBSize = 9; |
|
constexpr double kBSigmaCol = 30.0; |
|
constexpr double kBSigmaSp = 30.0; |
|
constexpr int kUnshSigma = 3; |
|
constexpr float kUnshStrength = 0.7f; |
|
constexpr int kAngDelta = 1; |
|
constexpr bool kClosedLine = true; |
|
|
|
const size_t kNumPointsInHalfEllipse = 180 / config::kAngDelta + 1; |
|
} // namespace config |
|
|
|
namespace |
|
{ |
|
using VectorROI = std::vector<cv::Rect>; |
|
using GArrayROI = cv::GArray<cv::Rect>; |
|
using Contour = std::vector<cv::Point>; |
|
using Landmarks = std::vector<cv::Point>; |
|
|
|
|
|
// Wrapper function |
|
template<typename Tp> inline int toIntRounded(const Tp x) |
|
{ |
|
return static_cast<int>(std::lround(x)); |
|
} |
|
|
|
template<typename Tp> inline double toDouble(const Tp x) |
|
{ |
|
return static_cast<double>(x); |
|
} |
|
|
|
std::string getWeightsPath(const std::string &mdlXMLPath) // mdlXMLPath = |
|
// "The/Full/Path.xml" |
|
{ |
|
size_t size = mdlXMLPath.size(); |
|
CV_Assert(mdlXMLPath.substr(size - 4, size) // The last 4 symbols |
|
== ".xml"); // must be ".xml" |
|
std::string mdlBinPath(mdlXMLPath); |
|
return mdlBinPath.replace(size - 3, 3, "bin"); // return |
|
// "The/Full/Path.bin" |
|
} |
|
} // anonymous namespace |
|
|
|
|
|
|
|
namespace custom |
|
{ |
|
using TplPtsFaceElements_Jaw = std::tuple<cv::GArray<Landmarks>, |
|
cv::GArray<Contour>>; |
|
using TplFaces_FaceElements = std::tuple<cv::GArray<Contour>, |
|
cv::GArray<Contour>>; |
|
|
|
// Wrapper-functions |
|
inline int getLineInclinationAngleDegrees(const cv::Point &ptLeft, |
|
const cv::Point &ptRight); |
|
inline Contour getForeheadEllipse(const cv::Point &ptJawLeft, |
|
const cv::Point &ptJawRight, |
|
const cv::Point &ptJawMiddle, |
|
const size_t capacity); |
|
inline Contour getEyeEllipse(const cv::Point &ptLeft, |
|
const cv::Point &ptRight, |
|
const size_t capacity); |
|
inline Contour getPatchedEllipse(const cv::Point &ptLeft, |
|
const cv::Point &ptRight, |
|
const cv::Point &ptUp, |
|
const cv::Point &ptDown); |
|
|
|
// Networks |
|
G_API_NET(FaceDetector, <cv::GMat(cv::GMat)>, "face_detector"); |
|
G_API_NET(LandmDetector, <cv::GMat(cv::GMat)>, "landm_detector"); |
|
|
|
// Function kernels |
|
G_TYPED_KERNEL(GBilatFilter, |
|
<cv::GMat(cv::GMat,int,double,double)>, |
|
"custom.faceb12n.bilateralFilter") |
|
{ |
|
static cv::GMatDesc outMeta(cv::GMatDesc in, int,double,double) |
|
{ |
|
return in; |
|
} |
|
}; |
|
|
|
G_TYPED_KERNEL(GLaplacian, |
|
<cv::GMat(cv::GMat,int)>, |
|
"custom.faceb12n.Laplacian") |
|
{ |
|
static cv::GMatDesc outMeta(cv::GMatDesc in, int) |
|
{ |
|
return in; |
|
} |
|
}; |
|
|
|
G_TYPED_KERNEL(GFillPolyGContours, |
|
<cv::GMat(cv::GMat,cv::GArray<Contour>)>, |
|
"custom.faceb12n.fillPolyGContours") |
|
{ |
|
static cv::GMatDesc outMeta(cv::GMatDesc in, cv::GArrayDesc) |
|
{ |
|
return in.withType(CV_8U, 1); |
|
} |
|
}; |
|
|
|
G_TYPED_KERNEL(GPolyLines, |
|
<cv::GMat(cv::GMat,cv::GArray<Contour>,bool,cv::Scalar)>, |
|
"custom.faceb12n.polyLines") |
|
{ |
|
static cv::GMatDesc outMeta(cv::GMatDesc in, cv::GArrayDesc,bool,cv::Scalar) |
|
{ |
|
return in; |
|
} |
|
}; |
|
|
|
G_TYPED_KERNEL(GRectangle, |
|
<cv::GMat(cv::GMat,GArrayROI,cv::Scalar)>, |
|
"custom.faceb12n.rectangle") |
|
{ |
|
static cv::GMatDesc outMeta(cv::GMatDesc in, cv::GArrayDesc,cv::Scalar) |
|
{ |
|
return in; |
|
} |
|
}; |
|
|
|
G_TYPED_KERNEL(GFacePostProc, |
|
<GArrayROI(cv::GMat,cv::GMat,float)>, |
|
"custom.faceb12n.faceDetectPostProc") |
|
{ |
|
static cv::GArrayDesc outMeta(const cv::GMatDesc&,const cv::GMatDesc&,float) |
|
{ |
|
return cv::empty_array_desc(); |
|
} |
|
}; |
|
|
|
G_TYPED_KERNEL_M(GLandmPostProc, |
|
<TplPtsFaceElements_Jaw(cv::GArray<cv::GMat>,GArrayROI)>, |
|
"custom.faceb12n.landmDetectPostProc") |
|
{ |
|
static std::tuple<cv::GArrayDesc,cv::GArrayDesc> outMeta( |
|
const cv::GArrayDesc&,const cv::GArrayDesc&) |
|
{ |
|
return std::make_tuple(cv::empty_array_desc(), cv::empty_array_desc()); |
|
} |
|
}; |
|
|
|
G_TYPED_KERNEL_M(GGetContours, |
|
<TplFaces_FaceElements(cv::GArray<Landmarks>, |
|
cv::GArray<Contour>)>, |
|
"custom.faceb12n.getContours") |
|
{ |
|
static std::tuple<cv::GArrayDesc,cv::GArrayDesc> outMeta( |
|
const cv::GArrayDesc&,const cv::GArrayDesc&) |
|
{ |
|
return std::make_tuple(cv::empty_array_desc(), cv::empty_array_desc()); |
|
} |
|
}; |
|
|
|
|
|
// OCV_Kernels |
|
// This kernel applies Bilateral filter to an input src with default |
|
// "cv::bilateralFilter" border argument |
|
GAPI_OCV_KERNEL(GCPUBilateralFilter, custom::GBilatFilter) |
|
{ |
|
static void run(const cv::Mat &src, |
|
const int diameter, |
|
const double sigmaColor, |
|
const double sigmaSpace, |
|
cv::Mat &out) |
|
{ |
|
cv::bilateralFilter(src, out, diameter, sigmaColor, sigmaSpace); |
|
} |
|
}; |
|
|
|
// This kernel applies Laplace operator to an input src with default |
|
// "cv::Laplacian" arguments |
|
GAPI_OCV_KERNEL(GCPULaplacian, custom::GLaplacian) |
|
{ |
|
static void run(const cv::Mat &src, |
|
const int ddepth, |
|
cv::Mat &out) |
|
{ |
|
cv::Laplacian(src, out, ddepth); |
|
} |
|
}; |
|
|
|
// This kernel draws given white filled contours "cnts" on a clear Mat "out" |
|
// (defined by a Scalar(0)) with standard "cv::fillPoly" arguments. |
|
// It should be used to create a mask. |
|
// The input Mat seems unused inside the function "run", but it is used deeper |
|
// in the kernel to define an output size. |
|
GAPI_OCV_KERNEL(GCPUFillPolyGContours, custom::GFillPolyGContours) |
|
{ |
|
static void run(const cv::Mat &, |
|
const std::vector<Contour> &cnts, |
|
cv::Mat &out) |
|
{ |
|
out = cv::Scalar(0); |
|
cv::fillPoly(out, cnts, config::kClrWhite); |
|
} |
|
}; |
|
|
|
// This kernel draws given contours on an input src with default "cv::polylines" |
|
// arguments |
|
GAPI_OCV_KERNEL(GCPUPolyLines, custom::GPolyLines) |
|
{ |
|
static void run(const cv::Mat &src, |
|
const std::vector<Contour> &cnts, |
|
const bool isClosed, |
|
const cv::Scalar &color, |
|
cv::Mat &out) |
|
{ |
|
src.copyTo(out); |
|
cv::polylines(out, cnts, isClosed, color); |
|
} |
|
}; |
|
|
|
// This kernel draws given rectangles on an input src with default |
|
// "cv::rectangle" arguments |
|
GAPI_OCV_KERNEL(GCPURectangle, custom::GRectangle) |
|
{ |
|
static void run(const cv::Mat &src, |
|
const VectorROI &vctFaceBoxes, |
|
const cv::Scalar &color, |
|
cv::Mat &out) |
|
{ |
|
src.copyTo(out); |
|
for (const cv::Rect &box : vctFaceBoxes) |
|
{ |
|
cv::rectangle(out, box, color); |
|
} |
|
} |
|
}; |
|
|
|
// A face detector outputs a blob with the shape: [1, 1, N, 7], where N is |
|
// the number of detected bounding boxes. Structure of an output for every |
|
// detected face is the following: |
|
// [image_id, label, conf, x_min, y_min, x_max, y_max]; all the seven elements |
|
// are floating point. For more details please visit: |
|
// https://github.com/opencv/open_model_zoo/blob/master/intel_models/face-detection-adas-0001 |
|
// This kernel is the face detection output blob parsing that returns a vector |
|
// of detected faces' rects: |
|
GAPI_OCV_KERNEL(GCPUFacePostProc, GFacePostProc) |
|
{ |
|
static void run(const cv::Mat &inDetectResult, |
|
const cv::Mat &inFrame, |
|
const float faceConfThreshold, |
|
VectorROI &outFaces) |
|
{ |
|
const int kObjectSize = 7; |
|
const int imgCols = inFrame.size().width; |
|
const int imgRows = inFrame.size().height; |
|
const cv::Rect borders({0, 0}, inFrame.size()); |
|
outFaces.clear(); |
|
const int numOfDetections = inDetectResult.size[2]; |
|
const float *data = inDetectResult.ptr<float>(); |
|
for (int i = 0; i < numOfDetections; i++) |
|
{ |
|
const float faceId = data[i * kObjectSize + 0]; |
|
if (faceId < 0.f) // indicates the end of detections |
|
{ |
|
break; |
|
} |
|
const float faceConfidence = data[i * kObjectSize + 2]; |
|
if (faceConfidence > faceConfThreshold) |
|
{ |
|
const float left = data[i * kObjectSize + 3]; |
|
const float top = data[i * kObjectSize + 4]; |
|
const float right = data[i * kObjectSize + 5]; |
|
const float bottom = data[i * kObjectSize + 6]; |
|
cv::Point tl(toIntRounded(left * imgCols), |
|
toIntRounded(top * imgRows)); |
|
cv::Point br(toIntRounded(right * imgCols), |
|
toIntRounded(bottom * imgRows)); |
|
outFaces.push_back(cv::Rect(tl, br) & borders); |
|
} |
|
} |
|
} |
|
}; |
|
|
|
// This kernel is the facial landmarks detection output Mat parsing for every |
|
// detected face; returns a tuple containing a vector of vectors of |
|
// face elements' Points and a vector of vectors of jaw's Points: |
|
GAPI_OCV_KERNEL(GCPULandmPostProc, GLandmPostProc) |
|
{ |
|
static void run(const std::vector<cv::Mat> &vctDetectResults, |
|
const VectorROI &vctRects, |
|
std::vector<Landmarks> &vctPtsFaceElems, |
|
std::vector<Contour> &vctCntJaw) |
|
{ |
|
// There are 35 landmarks given by the default detector for each face |
|
// in a frame; the first 18 of them are face elements (eyes, eyebrows, |
|
// a nose, a mouth) and the last 17 - a jaw contour. The detector gives |
|
// floating point values for landmarks' normed coordinates relatively |
|
// to an input ROI (not the original frame). |
|
// For more details please visit: |
|
// https://github.com/opencv/open_model_zoo/blob/master/intel_models/facial-landmarks-35-adas-0002 |
|
static constexpr int kNumFaceElems = 18; |
|
static constexpr int kNumTotal = 35; |
|
const size_t numFaces = vctRects.size(); |
|
CV_Assert(vctPtsFaceElems.size() == 0ul); |
|
CV_Assert(vctCntJaw.size() == 0ul); |
|
vctPtsFaceElems.reserve(numFaces); |
|
vctCntJaw.reserve(numFaces); |
|
|
|
Landmarks ptsFaceElems; |
|
Contour cntJaw; |
|
ptsFaceElems.reserve(kNumFaceElems); |
|
cntJaw.reserve(kNumTotal - kNumFaceElems); |
|
|
|
for (size_t i = 0; i < numFaces; i++) |
|
{ |
|
const float *data = vctDetectResults[i].ptr<float>(); |
|
// The face elements points: |
|
ptsFaceElems.clear(); |
|
for (int j = 0; j < kNumFaceElems * 2; j += 2) |
|
{ |
|
cv::Point pt = |
|
cv::Point(toIntRounded(data[j] * vctRects[i].width), |
|
toIntRounded(data[j+1] * vctRects[i].height)) |
|
+ vctRects[i].tl(); |
|
ptsFaceElems.push_back(pt); |
|
} |
|
vctPtsFaceElems.push_back(ptsFaceElems); |
|
|
|
// The jaw contour points: |
|
cntJaw.clear(); |
|
for(int j = kNumFaceElems * 2; j < kNumTotal * 2; j += 2) |
|
{ |
|
cv::Point pt = |
|
cv::Point(toIntRounded(data[j] * vctRects[i].width), |
|
toIntRounded(data[j+1] * vctRects[i].height)) |
|
+ vctRects[i].tl(); |
|
cntJaw.push_back(pt); |
|
} |
|
vctCntJaw.push_back(cntJaw); |
|
} |
|
} |
|
}; |
|
|
|
// This kernel is the facial landmarks detection post-processing for every face |
|
// detected before; output is a tuple of vectors of detected face contours and |
|
// facial elements contours: |
|
GAPI_OCV_KERNEL(GCPUGetContours, GGetContours) |
|
{ |
|
static void run(const std::vector<Landmarks> &vctPtsFaceElems, |
|
const std::vector<Contour> &vctCntJaw, |
|
std::vector<Contour> &vctElemsContours, |
|
std::vector<Contour> &vctFaceContours) |
|
{ |
|
size_t numFaces = vctCntJaw.size(); |
|
CV_Assert(numFaces == vctPtsFaceElems.size()); |
|
CV_Assert(vctElemsContours.size() == 0ul); |
|
CV_Assert(vctFaceContours.size() == 0ul); |
|
// vctFaceElemsContours will store all the face elements' contours found |
|
// on an input image, namely 4 elements (two eyes, nose, mouth) |
|
// for every detected face |
|
vctElemsContours.reserve(numFaces * 4); |
|
// vctFaceElemsContours will store all the faces' contours found on |
|
// an input image |
|
vctFaceContours.reserve(numFaces); |
|
|
|
Contour cntFace, cntLeftEye, cntRightEye, cntNose, cntMouth; |
|
cntNose.reserve(4); |
|
|
|
for (size_t i = 0ul; i < numFaces; i++) |
|
{ |
|
// The face elements contours |
|
// A left eye: |
|
// Approximating the lower eye contour by half-ellipse |
|
// (using eye points) and storing in cntLeftEye: |
|
cntLeftEye = getEyeEllipse(vctPtsFaceElems[i][1], |
|
vctPtsFaceElems[i][0], |
|
config::kNumPointsInHalfEllipse + 3); |
|
// Pushing the left eyebrow clock-wise: |
|
cntLeftEye.insert(cntLeftEye.cend(), {vctPtsFaceElems[i][12], |
|
vctPtsFaceElems[i][13], |
|
vctPtsFaceElems[i][14]}); |
|
// A right eye: |
|
// Approximating the lower eye contour by half-ellipse |
|
// (using eye points) and storing in vctRightEye: |
|
cntRightEye = getEyeEllipse(vctPtsFaceElems[i][2], |
|
vctPtsFaceElems[i][3], |
|
config::kNumPointsInHalfEllipse + 3); |
|
// Pushing the right eyebrow clock-wise: |
|
cntRightEye.insert(cntRightEye.cend(), {vctPtsFaceElems[i][15], |
|
vctPtsFaceElems[i][16], |
|
vctPtsFaceElems[i][17]}); |
|
// A nose: |
|
// Storing the nose points clock-wise |
|
cntNose.clear(); |
|
cntNose.insert(cntNose.cend(), {vctPtsFaceElems[i][4], |
|
vctPtsFaceElems[i][7], |
|
vctPtsFaceElems[i][5], |
|
vctPtsFaceElems[i][6]}); |
|
// A mouth: |
|
// Approximating the mouth contour by two half-ellipses |
|
// (using mouth points) and storing in vctMouth: |
|
cntMouth = getPatchedEllipse(vctPtsFaceElems[i][8], |
|
vctPtsFaceElems[i][9], |
|
vctPtsFaceElems[i][10], |
|
vctPtsFaceElems[i][11]); |
|
// Storing all the elements in a vector: |
|
vctElemsContours.insert(vctElemsContours.cend(), {cntLeftEye, |
|
cntRightEye, |
|
cntNose, |
|
cntMouth}); |
|
|
|
// The face contour: |
|
// Approximating the forehead contour by half-ellipse |
|
// (using jaw points) and storing in vctFace: |
|
cntFace = getForeheadEllipse(vctCntJaw[i][0], vctCntJaw[i][16], |
|
vctCntJaw[i][8], |
|
config::kNumPointsInHalfEllipse + |
|
vctCntJaw[i].size()); |
|
// The ellipse is drawn clock-wise, but jaw contour points goes |
|
// vice versa, so it's necessary to push cntJaw from the end |
|
// to the begin using a reverse iterator: |
|
std::copy(vctCntJaw[i].crbegin(), vctCntJaw[i].crend(), |
|
std::back_inserter(cntFace)); |
|
// Storing the face contour in another vector: |
|
vctFaceContours.push_back(cntFace); |
|
} |
|
} |
|
}; |
|
|
|
// GAPI subgraph functions |
|
inline cv::GMat unsharpMask(const cv::GMat &src, |
|
const int sigma, |
|
const float strength); |
|
inline cv::GMat mask3C(const cv::GMat &src, |
|
const cv::GMat &mask); |
|
} // namespace custom |
|
|
|
|
|
// Functions implementation: |
|
// Returns an angle (in degrees) between a line given by two Points and |
|
// the horison. Note that the result depends on the arguments order: |
|
inline int custom::getLineInclinationAngleDegrees(const cv::Point &ptLeft, |
|
const cv::Point &ptRight) |
|
{ |
|
const cv::Point residual = ptRight - ptLeft; |
|
if (residual.y == 0 && residual.x == 0) |
|
return 0; |
|
else |
|
return toIntRounded(atan2(toDouble(residual.y), toDouble(residual.x)) |
|
* 180.0 / M_PI); |
|
} |
|
|
|
// Approximates a forehead by half-ellipse using jaw points and some geometry |
|
// and then returns points of the contour; "capacity" is used to reserve enough |
|
// memory as there will be other points inserted. |
|
inline Contour custom::getForeheadEllipse(const cv::Point &ptJawLeft, |
|
const cv::Point &ptJawRight, |
|
const cv::Point &ptJawLower, |
|
const size_t capacity = 0) |
|
{ |
|
Contour cntForehead; |
|
cntForehead.reserve(std::max(capacity, config::kNumPointsInHalfEllipse)); |
|
// The point amid the top two points of a jaw: |
|
const cv::Point ptFaceCenter((ptJawLeft + ptJawRight) / 2); |
|
// This will be the center of the ellipse. |
|
|
|
// The angle between the jaw and the vertical: |
|
const int angFace = getLineInclinationAngleDegrees(ptJawLeft, ptJawRight); |
|
// This will be the inclination of the ellipse |
|
|
|
// Counting the half-axis of the ellipse: |
|
const double jawWidth = cv::norm(ptJawLeft - ptJawRight); |
|
// A forehead width equals the jaw width, and we need a half-axis: |
|
const int axisX = toIntRounded(jawWidth / 2.0); |
|
|
|
const double jawHeight = cv::norm(ptFaceCenter - ptJawLower); |
|
// According to research, in average a forehead is approximately 2/3 of |
|
// a jaw: |
|
const int axisY = toIntRounded(jawHeight * 2 / 3.0); |
|
|
|
// We need the upper part of an ellipse: |
|
static constexpr int kAngForeheadStart = 180; |
|
static constexpr int kAngForeheadEnd = 360; |
|
cv::ellipse2Poly(ptFaceCenter, cv::Size(axisX, axisY), angFace, |
|
kAngForeheadStart, kAngForeheadEnd, config::kAngDelta, |
|
cntForehead); |
|
return cntForehead; |
|
} |
|
|
|
// Approximates the lower eye contour by half-ellipse using eye points and some |
|
// geometry and then returns points of the contour; "capacity" is used |
|
// to reserve enough memory as there will be other points inserted. |
|
inline Contour custom::getEyeEllipse(const cv::Point &ptLeft, |
|
const cv::Point &ptRight, |
|
const size_t capacity = 0) |
|
{ |
|
Contour cntEyeBottom; |
|
cntEyeBottom.reserve(std::max(capacity, config::kNumPointsInHalfEllipse)); |
|
const cv::Point ptEyeCenter((ptRight + ptLeft) / 2); |
|
const int angle = getLineInclinationAngleDegrees(ptLeft, ptRight); |
|
const int axisX = toIntRounded(cv::norm(ptRight - ptLeft) / 2.0); |
|
// According to research, in average a Y axis of an eye is approximately |
|
// 1/3 of an X one. |
|
const int axisY = axisX / 3; |
|
// We need the lower part of an ellipse: |
|
static constexpr int kAngEyeStart = 0; |
|
static constexpr int kAngEyeEnd = 180; |
|
cv::ellipse2Poly(ptEyeCenter, cv::Size(axisX, axisY), angle, kAngEyeStart, |
|
kAngEyeEnd, config::kAngDelta, cntEyeBottom); |
|
return cntEyeBottom; |
|
} |
|
|
|
//This function approximates an object (a mouth) by two half-ellipses using |
|
// 4 points of the axes' ends and then returns points of the contour: |
|
inline Contour custom::getPatchedEllipse(const cv::Point &ptLeft, |
|
const cv::Point &ptRight, |
|
const cv::Point &ptUp, |
|
const cv::Point &ptDown) |
|
{ |
|
// Shared characteristics for both half-ellipses: |
|
const cv::Point ptMouthCenter((ptLeft + ptRight) / 2); |
|
const int angMouth = getLineInclinationAngleDegrees(ptLeft, ptRight); |
|
const int axisX = toIntRounded(cv::norm(ptRight - ptLeft) / 2.0); |
|
|
|
// The top half-ellipse: |
|
Contour cntMouthTop; |
|
const int axisYTop = toIntRounded(cv::norm(ptMouthCenter - ptUp)); |
|
// We need the upper part of an ellipse: |
|
static constexpr int angTopStart = 180; |
|
static constexpr int angTopEnd = 360; |
|
cv::ellipse2Poly(ptMouthCenter, cv::Size(axisX, axisYTop), angMouth, |
|
angTopStart, angTopEnd, config::kAngDelta, cntMouthTop); |
|
|
|
// The bottom half-ellipse: |
|
Contour cntMouth; |
|
const int axisYBot = toIntRounded(cv::norm(ptMouthCenter - ptDown)); |
|
// We need the lower part of an ellipse: |
|
static constexpr int angBotStart = 0; |
|
static constexpr int angBotEnd = 180; |
|
cv::ellipse2Poly(ptMouthCenter, cv::Size(axisX, axisYBot), angMouth, |
|
angBotStart, angBotEnd, config::kAngDelta, cntMouth); |
|
|
|
// Pushing the upper part to vctOut |
|
cntMouth.reserve(cntMouth.size() + cntMouthTop.size()); |
|
std::copy(cntMouthTop.cbegin(), cntMouthTop.cend(), |
|
std::back_inserter(cntMouth)); |
|
return cntMouth; |
|
} |
|
|
|
inline cv::GMat custom::unsharpMask(const cv::GMat &src, |
|
const int sigma, |
|
const float strength) |
|
{ |
|
cv::GMat blurred = cv::gapi::medianBlur(src, sigma); |
|
cv::GMat laplacian = custom::GLaplacian::on(blurred, CV_8U); |
|
return (src - (laplacian * strength)); |
|
} |
|
|
|
inline cv::GMat custom::mask3C(const cv::GMat &src, |
|
const cv::GMat &mask) |
|
{ |
|
std::tuple<cv::GMat,cv::GMat,cv::GMat> tplIn = cv::gapi::split3(src); |
|
cv::GMat masked0 = cv::gapi::mask(std::get<0>(tplIn), mask); |
|
cv::GMat masked1 = cv::gapi::mask(std::get<1>(tplIn), mask); |
|
cv::GMat masked2 = cv::gapi::mask(std::get<2>(tplIn), mask); |
|
return cv::gapi::merge3(masked0, masked1, masked2); |
|
} |
|
|
|
|
|
int main(int argc, char** argv) |
|
{ |
|
cv::CommandLineParser parser(argc, argv, |
|
"{ help h || print the help message. }" |
|
|
|
"{ facepath f || a path to a Face detection model file (.xml).}" |
|
"{ facedevice |GPU| the face detection computation device.}" |
|
|
|
"{ landmpath l || a path to a Landmarks detection model file (.xml).}" |
|
"{ landmdevice |CPU| the landmarks detection computation device.}" |
|
|
|
"{ input i || a path to an input. Skip to capture from a camera.}" |
|
"{ boxes b |false| set true to draw face Boxes in the \"Input\" window.}" |
|
"{ landmarks m |false| set true to draw landMarks in the \"Input\" window.}" |
|
); |
|
parser.about("Use this script to run the face beautification" |
|
" algorithm on G-API."); |
|
if (argc == 1 || parser.has("help")) |
|
{ |
|
parser.printMessage(); |
|
return 0; |
|
} |
|
|
|
cv::namedWindow(config::kWinFaceBeautification, cv::WINDOW_NORMAL); |
|
cv::namedWindow(config::kWinInput, cv::WINDOW_NORMAL); |
|
|
|
// Parsing input arguments |
|
const std::string faceXmlPath = parser.get<std::string>("facepath"); |
|
const std::string faceBinPath = getWeightsPath(faceXmlPath); |
|
const std::string faceDevice = parser.get<std::string>("facedevice"); |
|
|
|
const std::string landmXmlPath = parser.get<std::string>("landmpath"); |
|
const std::string landmBinPath = getWeightsPath(landmXmlPath); |
|
const std::string landmDevice = parser.get<std::string>("landmdevice"); |
|
|
|
// The flags for drawing/not drawing face boxes or/and landmarks in the |
|
// \"Input\" window: |
|
const bool flgBoxes = parser.get<bool>("boxes"); |
|
const bool flgLandmarks = parser.get<bool>("landmarks"); |
|
// To provide this opportunity, it is necessary to check the flags when |
|
// compiling a graph |
|
|
|
// Declaring a graph |
|
// Streaming-API version of a pipeline expression with a lambda-based |
|
// constructor is used to keep all temporary objects in a dedicated scope. |
|
cv::GComputation pipeline([=]() |
|
{ |
|
cv::GMat gimgIn; |
|
// Infering |
|
cv::GMat faceOut = cv::gapi::infer<custom::FaceDetector>(gimgIn); |
|
GArrayROI garRects = custom::GFacePostProc::on(faceOut, gimgIn, |
|
config::kConfThresh); |
|
cv::GArray<Landmarks> garElems; |
|
cv::GArray<Contour> garJaws; |
|
cv::GArray<cv::GMat> landmOut = cv::gapi::infer<custom::LandmDetector>( |
|
garRects, gimgIn); |
|
std::tie(garElems, garJaws) = custom::GLandmPostProc::on(landmOut, |
|
garRects); |
|
cv::GArray<Contour> garElsConts; |
|
cv::GArray<Contour> garFaceConts; |
|
std::tie(garElsConts, garFaceConts) = custom::GGetContours::on(garElems, |
|
garJaws); |
|
// Masks drawing |
|
// All masks are created as CV_8UC1 |
|
cv::GMat mskSharp = custom::GFillPolyGContours::on(gimgIn, |
|
garElsConts); |
|
cv::GMat mskSharpG = cv::gapi::gaussianBlur(mskSharp, |
|
config::kGKernelSize, |
|
config::kGSigma); |
|
cv::GMat mskBlur = custom::GFillPolyGContours::on(gimgIn, |
|
garFaceConts); |
|
cv::GMat mskBlurG = cv::gapi::gaussianBlur(mskBlur, |
|
config::kGKernelSize, |
|
config::kGSigma); |
|
// The first argument in mask() is Blur as we want to subtract from |
|
// BlurG the next step: |
|
cv::GMat mskBlurFinal = mskBlurG - cv::gapi::mask(mskBlurG, |
|
mskSharpG); |
|
cv::GMat mskFacesGaussed = mskBlurFinal + mskSharpG; |
|
cv::GMat mskFacesWhite = cv::gapi::threshold(mskFacesGaussed, 0, 255, |
|
cv::THRESH_BINARY); |
|
cv::GMat mskNoFaces = cv::gapi::bitwise_not(mskFacesWhite); |
|
cv::GMat gimgBilat = custom::GBilatFilter::on(gimgIn, |
|
config::kBSize, |
|
config::kBSigmaCol, |
|
config::kBSigmaSp); |
|
cv::GMat gimgSharp = custom::unsharpMask(gimgIn, |
|
config::kUnshSigma, |
|
config::kUnshStrength); |
|
// Applying the masks |
|
// Custom function mask3C() should be used instead of just gapi::mask() |
|
// as mask() provides CV_8UC1 source only (and we have CV_8U3C) |
|
cv::GMat gimgBilatMasked = custom::mask3C(gimgBilat, mskBlurFinal); |
|
cv::GMat gimgSharpMasked = custom::mask3C(gimgSharp, mskSharpG); |
|
cv::GMat gimgInMasked = custom::mask3C(gimgIn, mskNoFaces); |
|
cv::GMat gimgBeautif = gimgBilatMasked + gimgSharpMasked + |
|
gimgInMasked; |
|
// Drawing face boxes and landmarks if necessary: |
|
cv::GMat gimgTemp; |
|
if (flgLandmarks == true) |
|
{ |
|
cv::GMat gimgTemp2 = custom::GPolyLines::on(gimgIn, garFaceConts, |
|
config::kClosedLine, |
|
config::kClrYellow); |
|
gimgTemp = custom::GPolyLines::on(gimgTemp2, garElsConts, |
|
config::kClosedLine, |
|
config::kClrYellow); |
|
} |
|
else |
|
{ |
|
gimgTemp = gimgIn; |
|
} |
|
cv::GMat gimgShow; |
|
if (flgBoxes == true) |
|
{ |
|
gimgShow = custom::GRectangle::on(gimgTemp, garRects, |
|
config::kClrGreen); |
|
} |
|
else |
|
{ |
|
// This action is necessary because an output node must be a result of |
|
// some operations applied to an input node, so it handles the case |
|
// when it should be nothing to draw |
|
gimgShow = cv::gapi::copy(gimgTemp); |
|
} |
|
return cv::GComputation(cv::GIn(gimgIn), |
|
cv::GOut(gimgBeautif, gimgShow)); |
|
}); |
|
// Declaring IE params for networks |
|
auto faceParams = cv::gapi::ie::Params<custom::FaceDetector> |
|
{ |
|
faceXmlPath, |
|
faceBinPath, |
|
faceDevice |
|
}; |
|
auto landmParams = cv::gapi::ie::Params<custom::LandmDetector> |
|
{ |
|
landmXmlPath, |
|
landmBinPath, |
|
landmDevice |
|
}; |
|
auto networks = cv::gapi::networks(faceParams, landmParams); |
|
// Declaring custom and fluid kernels have been used: |
|
auto customKernels = cv::gapi::kernels<custom::GCPUBilateralFilter, |
|
custom::GCPULaplacian, |
|
custom::GCPUFillPolyGContours, |
|
custom::GCPUPolyLines, |
|
custom::GCPURectangle, |
|
custom::GCPUFacePostProc, |
|
custom::GCPULandmPostProc, |
|
custom::GCPUGetContours>(); |
|
auto kernels = cv::gapi::combine(cv::gapi::core::fluid::kernels(), |
|
customKernels); |
|
// Now we are ready to compile the pipeline to a stream with specified |
|
// kernels, networks and image format expected to process |
|
auto stream = pipeline.compileStreaming(cv::GMatDesc{CV_8U,3, |
|
cv::Size(1280,720)}, |
|
cv::compile_args(kernels, |
|
networks)); |
|
// Setting the source for the stream: |
|
if (parser.has("input")) |
|
{ |
|
stream.setSource(cv::gapi::wip::make_src<cv::gapi::wip::GCaptureSource> |
|
(parser.get<cv::String>("input"))); |
|
} |
|
else |
|
{ |
|
stream.setSource(cv::gapi::wip::make_src<cv::gapi::wip::GCaptureSource> |
|
(0)); |
|
} |
|
// Declaring output variables |
|
cv::Mat imgShow; |
|
cv::Mat imgBeautif; |
|
// Streaming: |
|
stream.start(); |
|
while (stream.running()) |
|
{ |
|
auto out_vector = cv::gout(imgBeautif, imgShow); |
|
if (!stream.try_pull(std::move(out_vector))) |
|
{ |
|
// Use a try_pull() to obtain data. |
|
// If there's no data, let UI refresh (and handle keypress) |
|
if (cv::waitKey(1) >= 0) break; |
|
else continue; |
|
} |
|
cv::imshow(config::kWinInput, imgShow); |
|
cv::imshow(config::kWinFaceBeautification, imgBeautif); |
|
} |
|
return 0; |
|
}
|
|
|