上一教程: 基于 G-API 的各向异性图像分割
简介
在本教程中你将学习到
- 示例美颜算法的基本原理;
- 如何在 G-API 流水线中推断不同的网络;
- 如何在视频流上运行 G-API 流水线。
先决条件
此示例需要
- 带有 GNU/Linux 或 Microsoft Windows 的 PC(支持 Apple macOS,但未经测试);
- 使用英特尔® OpenVINO™ 工具包构建的 OpenCV 4.2 或更高版本(使用 英特尔® TBB 构建是加分项);
- OpenVINO™ 工具包 Open Model Zoo 中的以下拓扑
face-detection-adas-0001
;
facial-landmarks-35-adas-0002
.
美颜算法
我们将使用现代深度学习技术和传统计算机视觉的组合来实现一个简单的美颜算法。该算法背后的基本思想是让脸部皮肤更光滑,同时保留面部特征,如眼睛或嘴巴的对比度。该算法使用 DNN 推断识别面部部位,对找到的部位应用不同的滤镜,然后使用基本的图像算法将其组合成最终结果
该算法简要描述如下
- 输入图像 \(I\) 传递到锐化掩模和双边滤镜(分别为 \(U\) 和 \(L\));
- 输入图像 \(I\) 传递到基于 SSD 的人脸检测器;
- SSD 结果(一个 \([1 \times 1 \times 200 \times 7]\) blob)被解析并转换为人脸数组;
- 每个人脸都传递到关键点检测器;
- 基于为每张人脸找到的地标信息生成三个图像掩码
- 一个背景掩码 \(b\) – 表示从原始图像中按原样保留哪些区域;
- 一个面部部件掩码 \(p\) – 识别要保留(锐化)的区域。
- 一个面部皮肤掩码 \(s\) – 识别要模糊的区域;
- 最终结果 \(O\) 是上面计算的特征的组合,即 \(O = b*I + p*U + s*L\)。
基于有限的特征集(每张人脸仅 35 个,包括所有部件)生成面部元素掩码并不是一件很简单的事,其具体描述如下。
构建 G-API 管道
声明深度学习拓扑
此示例使用两个 DNN 检测器。每个网络接受一个输入并生成一个输出。在 G-API 中,使用宏 G_API_NET() 定义网络
GMat 类表示图中的图像或张量数据。
定义 gmat.hpp:68
#define G_API_NET(Class, API, Tag)
定义 infer.hpp:452
如需了解更多信息,请参阅“人脸分析管道”教程中介绍的 声明深度学习拓扑。
描述处理流程图
以下代码将为上述算法生成一个流程图
{
cv::GMat faceOut = cv::gapi::infer<custom::FaceDetector>(gimgIn);
GArrayROI garRects = custom::GFacePostProc::on(faceOut, gimgIn, config::kConfThresh);
std::tie(garElems, garJaws) = custom::GLandmPostProc::on(landmOut, garRects);
std::tie(garElsConts, garFaceConts) = custom::GGetContours::on(garElems, garJaws);
cv::GMat mskSharp = custom::GFillPolyGContours::on(gimgIn, garElsConts);
config::kGSigma);
cv::GMat mskBlur = custom::GFillPolyGContours::on(gimgIn, garFaceConts);
config::kGSigma);
cv::GMat mskFacesGaussed = mskBlurFinal + mskSharpG;
cv::GMat gimgBilat = custom::GBilatFilter::on(gimgIn, config::kBSize,
config::kBSigmaCol, config::kBSigmaSp);
cv::GMat gimgSharp = custom::unsharpMask(gimgIn, config::kUnshSigma,
config::kUnshStrength);
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;
garFaceConts,
garElsConts,
garRects));
});
模板类 cv::GArray<T> 表示图中类 T 对象的列表。
定义 garray.hpp:366
GComputation 类表示一个捕获的计算图。GComputation 对象形成边界的 ...
定义 gcomputation.hpp:121
GMat gaussianBlur(const GMat &src, const Size &ksize, double sigmaX, double sigmaY=0, int borderType=BORDER_DEFAULT, const Scalar &borderValue=Scalar(0))
使用高斯滤镜模糊图像。
GMat mask(const GMat &src, const GMat &mask)
向矩阵应用掩码。
GMat threshold(const GMat &src, const GScalar &thresh, const GScalar &maxval, int type)
向每个矩阵元素应用固定级别的阈值。
GMat bitwise_not(const GMat &src)
反转数组的每个位。
@ THRESH_BINARY
定义 imgproc.hpp:325
GProtoInputArgs GIn(Ts &&... ts)
定义 gproto.hpp:96
GProtoOutputArgs GOut(Ts &&... ts)
定义 gproto.hpp:101
生成图是 G-API 标准操作、用户定义操作(名称空间 custom::
)和 DNN 推断的混合。通用函数 cv::gapi::infer<>()
允许在管道内触发推断;要推断的网络指定为模板参数。示例代码使用两个版本的 cv::gapi::infer<>()
- 面向帧的一种用于检测输入帧上的人脸。
- 面向 ROI 列表的一种用于对人脸列表执行标记推断 - 此版本为每张人脸生成一个标记数组。
在“Face Analytics 管道”中对此进行了更多介绍(构建 GComputation 部分)。
G-API 中的锐化掩码
图像 \(I\) 的锐化掩码 \(U\) 定义为
\[U = I - s * L(M(I)),\]
其中 \(M()\) 是中值滤镜,\(L()\) 是拉普拉斯算子,\(s\) 是强度系数。虽然 G-API 并未开箱即用地提供此函数,但它使用现有的 G-API 操作自然地表达
const int sigma,
const float strength)
{
return (src - (laplacian * strength));
}
#define CV_8U
定义 interface.h:73
GMat medianBlur(const GMat &src, int ksize)
模糊使用中值滤波器的图像。
注意以上代码是使用 G-API 类型定义的常规 C++ 函数。用户可以编写此类函数来简化图形构造;被调用时,此函数只需将相关节点放到其所在的管道中即可。
自定义操作
美化面部图形广泛使用自定义操作。本小节重点介绍最有意思的内核;请参阅 G-API Kernel API 以了解有关在 G-API 中定义操作和实现内核的一般信息。
人脸检测后处理
将人脸检测器输出转换为含有人脸的数组,使用以下内核
using VectorROI = std::vector<cv::Rect>;
{
static void run(
const cv::Mat &inDetectResult,
const float faceConfThreshold,
VectorROI &outFaces)
{
const int kObjectSize = 7;
const int imgCols = inFrame.
size().width;
const int imgRows = inFrame.
size().height;
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];
如果 (faceId < 0.f)
{
结束;
}
常量 浮点 faceConfidence = data[i * kObjectSize + 2];
如果 (faceConfidence > faceConfThreshold)
{
常量 浮点 left = data[i * kObjectSize + 3];
常量 浮点 top = data[i * kObjectSize + 4];
常量 浮点 right = data[i * kObjectSize + 5];
常量 浮点 bottom = data[i * kObjectSize + 6];
toIntRounded(top * imgRows));
toIntRounded(bottom * imgRows));
outFaces.push_back(
cv::Rect(tl, br) & 边界);
}
}
}
};
MatSize 大小
定义 mat.hpp:2160
uchar * ptr(int i0=0)
返回指向指定矩阵行的指针。
#define GAPI_OCV_KERNEL(名称,API)
定义 gcpukernel.hpp:488
面部特征后处理
算法使用通用面部特征检测器(详细信息),从 OpenVINO™ Open Model Zoo 中推断面部元素(如眼睛、嘴巴以及头部轮廓本身)的位置。但是,如此检测到的地标不足以生成掩码 - 此操作要求由闭合轮廓表示的面部感兴趣区域,因此,应用了一些内插法来获取这些区域。以下内核执行此地标处理和内插法
{
静态 void run(常量 std::vector<Landmarks> &vctPtsFaceElems,
常量 std::vector<Contour> &vctCntJaw,
std::vector<Contour> &vctElemsContours,
std::vector<Contour> &vctFaceContours)
{
size_t numFaces = vctCntJaw.size();
CV_Assert(numFaces == vctPtsFaceElems.size());
vctElemsContours.reserve(numFaces * 4);
vctFaceContours.reserve(numFaces);
Contour cntFace, cntLeftEye, cntRightEye, cntNose, cntMouth;
cntNose.reserve(4);
for (size_t i = 0ul; i < numFaces; i++)
{
cntLeftEye = getEyeEllipse(vctPtsFaceElems[i][1], vctPtsFaceElems[i][0]);
cntLeftEye.insert(cntLeftEye.end(), {vctPtsFaceElems[i][12], vctPtsFaceElems[i][13],
vctPtsFaceElems[i][14]});
cntRightEye = getEyeEllipse(vctPtsFaceElems[i][2], vctPtsFaceElems[i][3]);
cntRightEye.insert(cntRightEye.end(), {vctPtsFaceElems[i][15], vctPtsFaceElems[i][16],
vctPtsFaceElems[i][17]});
cntNose.clear();
cntNose.insert(cntNose.end(), {vctPtsFaceElems[i][4], vctPtsFaceElems[i][7],
vctPtsFaceElems[i][5], vctPtsFaceElems[i][6]});
cntMouth = getPatchedEllipse(vctPtsFaceElems[i][8], vctPtsFaceElems[i][9],
vctPtsFaceElems[i][10], vctPtsFaceElems[i][11]);
vctElemsContours.insert(vctElemsContours.end(), {cntLeftEye, cntRightEye, cntNose, cntMouth});
cntFace = getForeheadEllipse(vctCntJaw[i][0], vctCntJaw[i][16], vctCntJaw[i][8]);
std::copy(vctCntJaw[i].crbegin(), vctCntJaw[i].crend(), std::back_inserter(cntFace));
vctFaceContours.push_back(cntFace);
}
}
};
#define CV_Assert(expr)
在运行时检查条件,如果失败则抛出异常。
定义 base.hpp:342
内核采用两个数组的非规范化地标坐标,并返回一个元素闭合轮廓的数组和一个面部闭合轮廓的数组;换句话说,输出是:第一,要锐化的图像区域轮廓的数组,第二,另一个要平滑的图像区域轮廓的数组。
此处和下方Contour
是点的向量。
获取眼睛轮廓
眼睛轮廓使用以下函数估算
inline int custom::getLineInclinationAngleDegrees(
const cv::Point &ptLeft,
const cv::Point &ptRight)
{
if (residual.
y == 0 && residual.
x == 0)
return 0;
else
return toIntRounded(atan2(toDouble(residual.
y), toDouble(residual.
x)) * 180.0 /
CV_PI);
}
_Tp y
点的 y 坐标
Definition types.hpp:202
_Tp x
点的 x 坐标
Definition types.hpp:201
#define CV_PI
Definition cvdef.h:380
{
Contour cntEyeBottom;
const cv::Point ptEyeCenter((ptRight + ptLeft) / 2);
const int angle = getLineInclinationAngleDegrees(ptLeft, ptRight);
const int axisX = toIntRounded(
cv::norm(ptRight - ptLeft) / 2.0);
const int axisY = axisX / 3;
static constexpr int kAngEyeStart = 0;
static constexpr int kAngEyeEnd = 180;
cntEyeBottom);
返回 cntEyeBottom;
}
用于指定图像或矩形大小的模板类。
定义 types.hpp:335
double norm(InputArray src1, int normType=NORM_L2, InputArray mask=noArray())
计算数组的绝对范数。
void ellipse2Poly(Point center, Size axes, int angle, int arcStart, int arcEnd, int delta, std::vector< Point > &pts)
将椭圆弧近似为折线。
简而言之,此函数根据左右眼角的两个点,通过半椭圆来重建眼睛的底部。实际上,cv::ellipse2Poly()
用于逼近眼睛区域,而且此函数仅基于两个点来定义椭圆参数
- 椭圆中心和由两个眼睛点计算的 X 半轴;
- 根据眼睛平均宽度为其长度的 1/3 的假设计算 Y 半轴;
- 起始角度和结束角度为 0 和 180(请参阅
cv::ellipse()
文档);
- 角度增量:一条轮廓中产生的点数;
- 轴的倾斜角。
函数 custom::getLineInclinationAngleDegrees()
中使用 atan2()
而不只是 atan()
至关重要,因为它可以根据 x 和 y 的符号返回负值,因此即使在面部上下颠倒放置的情况下(当然,如果按正确的顺序放置点),我们也能获得正确的角度。
获取额头轮廓
该函数对额头轮廓进行逼近
内联 轮廓 custom::getForeheadEllipse(
const cv::Point &ptJawLeft,
{
轮廓 cntForehead;
const cv::Point ptFaceCenter((ptJawLeft + ptJawRight) / 2);
const int angFace = getLineInclinationAngleDegrees(ptJawLeft, ptJawRight);
const double jawWidth =
cv::norm(ptJawLeft - ptJawRight);
const int axisX = toIntRounded(jawWidth / 2.0);
const double jawHeight =
cv::norm(ptFaceCenter - ptJawLower);
const int axisY = toIntRounded(jawHeight * 2 / 3.0);
static constexpr int kAngForeheadStart = 180;
static constexpr int kAngForeheadEnd = 360;
config::kAngDelta, cntForehead);
return cntForehead;
}
由于在检测到的关键点中只包含下颌点,我们必须根据下颌的三个点(最左、最右和最低点)获取一个半椭圆。下颌宽度假设等于额头宽度,额头宽度使用左右点进行计算。关于 \(Y\) 轴,我们没有可用于直接获取它的点,而是假设额头高度约为下颌高度的 \(2/3\),该高度可从脸部中心点(左右点之间的中点)和最低下颌点算出。
绘制蒙版
当我们拥有所有需要的轮廓时,就能够绘制蒙版
cv::GMat mskSharp = custom::GFillPolyGContours::on(gimgIn, garElsConts);
config::kGSigma);
cv::GMat mskBlur = custom::GFillPolyGContours::on(gimgIn, garFaceConts);
config::kGSigma);
cv::GMat mskFacesGaussed = mskBlurFinal + mskSharpG;
获取蒙版所需的步骤如下
- 计算“锐化”蒙版
- 填充应该锐化的轮廓;
- 模糊它以获取“锐化”蒙版 (
mskSharpG
);
- 计算“双边”蒙版
- 完全填充所有脸部轮廓;
- 对它进行模糊处理;
- 减去与“锐化”蒙版相交的区域,得到“双边”蒙版 (
mskBlurFinal
);
- 计算背景蒙版
配置和运行管道
图表得到充分表示后,我们最终可以编译它,并在真实数据上运行。G-API 图表编译是 G-API 框架实际上了解应该使用哪些内核和网络的阶段。这种配置通过 G-API 编译参数来完成。
DNN 参数
本示例使用 OpenVINO™ 工具包推理引擎后端来进行 DL 推理, 配置如下
{
faceXmlPath,
faceBinPath,
faceDevice
};
{
landmXmlPath,
landmBinPath,
landmDevice
};
此结构提供的函数将填充 OpenVINO Toolkit 模型的推理参数。
定义 ie.hpp:144
每个 cv::gapi::ie::Params<>
对象都与其模板参数中指定的网络相关。我们应在那儿传递我们在本教程的开头部分中 G_API_NET()
中定义的网络类型。
然后网络参数被封装在 cv::gapi::NetworkPackage
中
cv::gapi::GNetPackage networks(Args &&... args)
定义 infer.hpp:703
更多详细信息在“人脸分析管道”中(配置管道部分)。
内核包
在此示例中,我们使用了大量自定义内核,此外,我们还使用 Fluid 后端来优化 G-API 标准内核(在适用情况下)的内存。最终的内核包按如下所示形成
custom::GCPULaplacian,
custom::GCPUFillPolyGContours,
custom::GCPUPolyLines,
custom::GCPURectangle,
custom::GCPUFacePostProc,
custom::GCPULandmPostProc,
custom::GCPUGetContours>();
customKernels);
GKernelPackage kernels()
创建一个内核包对象,其中包含可变模板 ... 中指定的内核和转换。
定义 gkernel.hpp:678
cv::GKernelPackage kernels()
cv::GKernelPackage combine(const cv::GKernelPackage &lhs, const cv::GKernelPackage &rhs)
编译流式处理管道
以“流式处理”模式编译时,G-API 会针对视频流优化执行。
表示为流式处理而编译的计算(图)
定义 gstreaming.hpp:156
GCompileArgs compile_args(Ts &&... args)
将参数包的一系列参数包装到编译参数的向量(cv::GCompileArg)中...
定义 gcommon.hpp:214
有关其他详情,请参见“面部分析管道”(管道配置部分)。
运行流式处理管道
为了运行 G-API 流式处理管道,我们需要指定输入视频源,调用cv::GStreamingCompiled::start()
,然后获取管道处理结果
if (parser.has("input"))
{
stream.
setSource(cv::gapi::wip::make_src<cv::gapi::wip::GCaptureSource>(parser.get<
cv::String>(
"input")));
}
void setSource(GRunArgs &&ins)
为 GStreamingCompiled 指定要处理的输入数据,通用版本。
std::string String
定义 cvstd.hpp:151
auto out_vector =
cv::gout(imgBeautif, imgShow, vctFaceConts,
vctElsConts, vctRects);
avg.start();
{
if (!stream.
try_pull(std::move(out_vector)))
{
else continue;
}
frames++;
if (flgLandmarks == true)
{
config::kClrYellow);
config::kClrYellow);
}
如果 (flgBoxes == 真)
对于 (自动 rect : vctRects)
cv::rectangle(imgShow, rect, config::kClrGreen);
cv::imshow(config::kWinFaceBeautification, imgBeautif);
}
GAPI_WRAP bool running() const
测试管道是否正在运行。
GAPI_WRAP void start()
启动管道执行。
bool try_pull(cv::GRunArgsP &&outs)
尝试从管道获取下一个处理后的帧。
void imshow(const String &winname, InputArray mat)
在指定窗口中显示图像。
int waitKey(int delay=0)
等待按下一个键。
void polylines(InputOutputArray img, InputArrayOfArrays pts, bool isClosed, const Scalar &color, int thickness=1, int lineType=LINE_8, int shift=0)
绘制多个多边形曲线。
与磁盘上的文件关联的文件存储的“黑匣子”展示。
定义 core.hpp:102
GRunArgsP gout(Ts &... args)
定义 garg.hpp:280
一旦结果准备就绪并能从管道中拉出,我们在屏幕上显示结果并处理 GUI 事件。
有关更多详细信息,请参见“人脸分析管道”教程中的 运行管道 部分。
结论
本教程有两个目标:展示 OpenCV 4.2 中引入的 G-API 全新功能的使用,并对示例人脸美化算法进行基本了解。
算法应用的结果
人脸美化示例
在测试机器(英特尔® 酷睿™ i7-8700)上,G-API 优化的视频管道以 2.7 倍的性能优于串行(非管道)版本——这意味着对于此类非平凡图,正确的管道可将性能提升将近 3 倍。