欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

精通使用真实世界的计算机视觉项目 OpenCV: 1~5

最编程 2024-03-17 13:22:29
...

一、Android 的卡通化器和换肤器

本章将向您展示如何为 Android 智能手机和平板电脑编写一些图像处理过滤器,该过滤器首先针对台式机(使用 C/C++)编写,然后移植到 Android(使用相同的 C/C++ 代码,但使用 Java GUI), 这是为移动设备开发时的推荐方案。 本章将涵盖:

  • 如何将真实图像转换为草图
  • 如何转换为绘画并叠加草图来生成卡通
  • 一种可怕的“邪恶”模式,用于创建坏角色而不是好角色
  • 基本的皮肤检测器和皮肤颜色更改器,可为某人提供绿色的“异形”皮肤
  • 如何将项目从桌面应用转换为移动应用

以下屏幕快照显示了在 Android 平板电脑上运行的最终 Cartoonifier 应用:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xgKeZM4T-1681871753484)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/master-opencv-prac-cv-proj/img/7829_1_1.jpg)]

我们想要使真实世界的相机帧看起来像真的是动画片。 基本思想是用一些颜色填充扁平零件,然后在坚固的边缘上绘制粗线。 换句话说,平坦区域应该变得更加平坦,边缘应该变得更加清晰。 我们将检测边缘并平滑平坦的区域,然后在顶部绘制增强的边缘以产生卡通或漫画效果。

开发移动计算机视觉应用时,最好先构建一个完全正常运行的桌面版本,然后再将其移植到移动设备上,因为开发和调试桌面程序比移动应用容易得多! 因此,本章将以完整的 Cartoonifier 桌面程序开始,您可以使用自己喜欢的 IDE 创建该程序(例如 Visual Studio,XCode , Eclipse, QtCreator 等)。 在桌面上正常运行后,最后一部分将说明如何使用 Eclipse 将其移植到 Android(或可能的 iOS)。 由于我们将创建两个不同的项目,这些项目大多使用不同的图形用户界面共享相同的源代码,因此您可以创建一个由两个项目链接的库,但为简单起见,我们将桌面和 Android 项目彼此相邻并设置 Android 项目以访问Desktop文件夹中的某些文件(cartoon.cppcartoon.h ,其中包含所有图像处理代码)。 例如:

  • C:\Cartoonifier_Desktop\cartoon.cpp
  • C:\Cartoonifier_Desktop\cartoon.h
  • C:\Cartoonifier_Desktop\main_desktop.cpp
  • C:\Cartoonifier_Android\...

桌面应用使用 OpenCV GUI 窗口,初始化摄像头,并通过每个摄像头框架调用cartoonifyImage()函数,该函数包含本章中的大部分代码。 然后,它将在 GUI 窗口上显示处理后的图像。 同样,Android 应用使用 Android GUI 窗口,使用 Java 初始化摄像头,并且每个摄像头框架都调用与前面提到的完全相同的 C++ cartoonifyImage()函数,但是具有 Android 菜单和手指触摸输入。 本章将解释如何从头开始创建桌面应用,以及如何从一个 OpenCV Android 示例项目中创建 Android 应用。 因此,首先您应该在自己喜欢的 IDE 中创建一个桌面程序,并使用main_desktop.cpp文件来保存以下各节中提供的 GUI 代码,例如主循环,网络摄像头功能和键盘输入,然后创建在项目之间共享的cartoon.cpp文件。 您应该将本章的大部分代码作为称为cartoonifyImage()的函数放入cartoon.cpp中。

访问网络摄像头

要访问计算机的网络摄像头或摄像头设备,只需在cv::VideoCapture对象(OpenCV 访问摄像头设备的方法)上调用open(),然后将0作为默认摄像头 ID 号。 某些计算机连接了多个摄像机,或者它们不作为默认摄像机0起作用; 因此,通常的做法是允许用户在希望尝试使用 1 号,2 号或 -1 号摄像机的情况下,将所需的摄像机号作为命令行参数传递。 我们还将尝试使用cv::VideoCapture::set() 将摄像机分辨率设置为640 x 480,以便在高分辨率摄像机上更快地运行。

注意

根据您的相机模型,驱动程序或系统,OpenCV 可能不会更改相机的属性。 对于这个项目而言,这并不重要,所以请放心,如果它不适用于您的相机。

您可以将此代码放入main_desktop.cppmain()函数中:

int cameraNumber = 0;
if (argc > 1)
  cameraNumber = atoi(argv[1]);
// Get access to the camera.
cv::VideoCapture camera;
camera.open(cameraNumber);
if (!camera.isOpened()) {
  std::cerr << "ERROR: Could not access the camera or video!" <<
  std::endl;
  exit(1);
}
// Try to set the camera resolution.
camera.set(cv::CV_CAP_PROP_FRAME_WIDTH, 640);
camera.set(cv::CV_CAP_PROP_FRAME_HEIGHT, 480);

初始化网络摄像头后,您可以将当前的摄像机图像作为cv::Mat对象(OpenCV 的图像容器)获取。 您可以使用 C++ 流运算符从cv::VideoCapture对象捕获到cv::Mat对象中,从而抓住每个摄像机帧,就像从控制台获取输入一样。

注意

OpenCV 使加载视频文件(例如 AVI 或 MPG 文件)并使用它代替网络摄像头非常容易。 与您的代码唯一的不同是,您应该使用视频文件名(例如camera.open("my_video.avi"))而不是摄像机编号(例如camera.open(0))创建cv::VideoCapture对象。 两种方法均会创建可以以相同方式使用的cv::VideoCapture对象。

桌面应用的主摄像头处理循环

如果要使用 OpenCV 在屏幕上显示 GUI 窗口,请为每个图像调用cv::imshow() ,但还必须每帧调用一次cv::waitKey() , 否则,您的 Windows 将根本不会更新! 调用cv::waitKey(0)会无限期地等待,直到用户敲击窗口中的某个键为止,但是正数(例如waitKey(20)或更高版本)将至少等待那么多毫秒。

将此主循环放在main_desktop.cpp中,作为您的实时摄像头应用的基础:

while (true) {
  // Grab the next camera frame.
  cv::Mat cameraFrame;
 camera >> cameraFrame;
  if (cameraFrame.empty()) {
    std::cerr << "ERROR: Couldn't grab a camera frame." <<
    std::endl;
    exit(1);
  }
  // Create a blank output image, that we will draw onto.
  cv::Mat displayedFrame(cameraFrame.size(), cv::CV_8UC3);
  // Run the cartoonifier filter on the camera frame.
 cartoonifyImage(cameraFrame, displayedFrame);
  // Display the processed image onto the screen.
  imshow("Cartoonifier", displayedFrame);
  // IMPORTANT: Wait for at least 20 milliseconds,
  // so that the image can be displayed on the screen!
  // Also checks if a key was pressed in the GUI window.
  // Note that it should be a "char" to support Linux.
 char keypress = cv::waitKey(20);  // Need this to see anything!
  if (keypress == 27) {   // Escape Key
  // Quit the program!
  break;
  }
}//end while

生成黑白草图

要获得相机帧的草图(黑白图),我们将使用边缘检测过滤器; 而要获得彩色绘画,我们将使用边缘保留过滤器(双边过滤器)进一步平滑平坦区域,同时保持边缘完整。 通过将素描图覆盖在彩色绘画的顶部,我们获得了卡通效果,如最终应用的屏幕截图中所示。

有许多不同的边缘检测过滤器,例如 Sobel, Scharr,拉普拉斯过滤器或 Canny 边缘检测器。 我们将使用 Laplacian 边缘过滤器,因为它产生的边缘与索贝尔或 Scharr 相比看起来与手绘草图最为相似,并且与 Canny 边缘检测器相比非常一致,后者产生的线条非常清晰,但受到随机噪声的影响更大。 因此,相机镜架中的“线条”和“线条图”通常会在镜架之间发生巨大变化。

尽管如此,在使用拉普拉斯边缘过滤器之前,我们仍然需要减少图像中的噪声。 我们将使用中值过滤器,因为它可以在消除噪声的同时保持边缘清晰; 而且,它不如双边过滤器慢。 由于拉普拉斯过滤器使用灰度图像,因此我们必须将 OpenCV 的默认 BGR 格式转换为灰度。 在空文件cartoon.cpp中,将此代码放在顶部,这样您就可以访问 OpenCV 和标准 C++ 模板,而无需在任何地方键入cv::std::

// Include OpenCV's C++ Interface
#include "opencv2/opencv.hpp"
using namespace cv;
using namespace std;

将此代码和所有其余代码放入cartoon.cpp文件的cartoonifyImage()函数中:

Mat gray;
cvtColor(srcColor, gray, CV_BGR2GRAY);
const int MEDIAN_BLUR_FILTER_SIZE = 7;
medianBlur(gray, gray, MEDIAN_BLUR_FILTER_SIZE);
Mat edges;
const int LAPLACIAN_FILTER_SIZE = 5;
Laplacian(gray, edges, CV_8U, LAPLACIAN_FILTER_SIZE);

拉普拉斯过滤器产生的边缘具有变化的亮度,因此为了使边缘看起来更像草图,我们应用二进制阈值使边缘为白色或黑色:

Mat mask;
const int EDGES_THRESHOLD = 80;
threshold(edges, mask, EDGES_THRESHOLD, 255, THRESH_BINARY_INV);

在下图中,您可以看到原始图像(左侧)和生成的边缘遮罩(右侧),看起来与草图相似。 生成彩色绘画(稍后说明)后,我们可以将此边缘遮罩放在黑色线条画的顶部:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JF5o0PBr-1681871753486)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/master-opencv-prac-cv-proj/img/7829_1_2.jpg)]

生成彩色绘画和卡通

强大的双边过滤器使边缘平滑的同时保持边缘清晰,因此非常适合作为自动卡通化器或绘画过滤器,但它非常慢(即以秒甚至数分钟而不是毫秒为单位! )。 因此,我们将使用一些技巧来获得仍然可以以可接受的速度运行的漂亮的卡通化器。 我们可以使用的最重要的技巧是以较低的分辨率执行双边过滤。 它具有与全分辨率相似的效果,但运行速度更快。 让我们将像素总数减少四倍(例如,一半宽度和一半高度):

Size size = srcColor.size();
Size smallSize;
smallSize.width = size.width/2;
smallSize.height = size.height/2;
Mat smallImg = Mat(smallSize, CV_8UC3);
resize(srcColor, smallImg, smallSize, 0,0, INTER_LINEAR);

与其应用大型双边过滤器,不如应用许多小型双边过滤器,以在更短的时间内产生强烈的卡通效果。 我们将截断过滤器(请参见下图),以便代替执行整个过滤器(例如,当钟形曲线为 21 像素宽时,过滤器的尺寸为21 x 21),而仅使用过滤器所需的最小过滤器尺寸。 令人信服的结果(例如,即使钟形曲线的宽度为 21 像素,过滤器大小也仅为9 x 9)。 该截断的过滤器将应用过滤器的主要部分(灰色区域),而不会浪费时间在过滤器的次要部分(曲线下方的白色区域)上,因此它将运行几倍:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YVuKbFnt-1681871753486)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/master-opencv-prac-cv-proj/img/7829_1_3.jpg)]

我们有四个参数来控制双边过滤器:颜色强度,位置强度,大小和重复计数。 我们需要一个临时Mat,因为bilateralFilter() 无法覆盖其输入(称为“原地处理”),但是我们可以应用一个存储临时Mat的过滤器,另一个存储返回到输入的过滤器:

Mat tmp = Mat(smallSize, CV_8UC3);
int repetitions = 7;  // Repetitions for strong cartoon effect.
for (int i=0; i<repetitions; i++) {
  int ksize = 9;     // Filter size. Has a large effect on speed.
  double sigmaColor = 9;    // Filter color strength.
  double sigmaSpace = 7;    // Spatial strength. Affects speed.
 bilateralFilter(smallImg, tmp, ksize, sigmaColor, sigmaSpace);
 bilateralFilter(tmp, smallImg, ksize, sigmaColor, sigmaSpace);
}

记住这是应用于缩小的图像,因此我们需要将图像扩展回原始大小。 然后,我们可以覆盖之前发现的边缘遮罩。 要将边缘遮罩“素描”覆盖到双边过滤器“绘画”(下图的左侧),我们可以从黑色背景开始,复制“素描”中不是边缘的“绘画”像素:

Mat bigImg;
resize(smallImg, bigImg, size, 0,0, INTER_LINEAR);
dst.setTo(0);
bigImg.copyTo(dst, mask);

结果是原始照片的卡通版本,如右图所示,其中“素描”遮罩覆盖在“绘画”上:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Yi5PrRTh-1681871753486)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/master-opencv-prac-cv-proj/img/7829_1_4.jpg)]

使用边缘过滤器生成“邪恶”模式

卡通和漫画总是有好有坏的角色。 使用边缘过滤器的正确组合,最无辜的人可能会生成可怕的图像! 诀窍是使用小边缘过滤器,它将在整个图像中找到许多边缘,然后使用小中值过滤器合并边缘。

我们将在具有一定降噪效果的灰度图像上执行此操作,因此应再次使用前面的代码将原始图像转换为灰度并应用7 x 7中值过滤器(下图中的第一幅图像显示了灰度的输出) 中值模糊)。 如果我们沿 x 和 y 应用3 x 3 Scharr 梯度过滤器(图中的第二个图像),然后应用具有非常高的二值阈值,则不用拉普拉斯过滤器和二进制阈值跟随它,就可以得到更恐怖的外观。 低截止(图中的第三幅图像)和7 x 7中值模糊,从而产生最终的“邪恶”遮罩(图中的第四幅图像):

Mat gray;
cvtColor(srcColor, gray, CV_BGR2GRAY);
const int MEDIAN_BLUR_FILTER_SIZE = 7;
medianBlur(gray, gray, MEDIAN_BLUR_FILTER_SIZE);
Mat edges, edges2;
Scharr(srcGray, edges, CV_8U, 1, 0);
Scharr(srcGray, edges2, CV_8U, 1, 0, -1);
edges += edges2;     // Combine the x & y edges together.
const int EVIL_EDGE_THRESHOLD = 12;
threshold(edges, mask, EVIL_EDGE_THRESHOLD, 255, THRESH_BINARY_INV);
medianBlur(mask, mask, 3);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QSnfJ7Cs-1681871753487)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/master-opencv-prac-cv-proj/img/7829_1_5.jpg)]

现在,我们有了一个“邪恶”遮罩,可以像使用常规“素描”边缘遮罩那样,将该遮罩叠加到卡通化的“绘画”图像上。 最终结果显示在下图的右侧:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K5qJCnSM-1681871753487)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/master-opencv-prac-cv-proj/img/7829_1_6.jpg)]

使用皮肤检测生成“异物”模式

现在我们具有素描模式,卡通模式(绘画+素描遮罩)和邪恶模式(绘画+邪恶遮罩),为了好玩,让我们尝试更复杂的东西:“异形”模式, 检测脸部的皮肤区域,然后将皮肤颜色更改为绿色。

皮肤检测算法

从使用 RGB红绿蓝)或 HSV色相饱和度值)的简单颜色阈值,或颜色直方图的计算和重新投影,到需要在 CIELab 颜色空间中进行摄像机校准,和进行离线训练的混合模型的复杂机器学习算法中,有许多用于检测皮肤区域的技术。 但是,即使是复杂的方法也不一定能在各种相机,照明条件和皮肤类型下正常运行。 由于我们希望皮肤检测无需任何校准或训练就可以在移动设备上运行,并且我们仅将“有趣”的图像过滤器用于皮肤检测,因此我们只需使用简单的皮肤- 检测方法。 但是,来自移动设备中微小的摄像头传感器的颜色响应往往会发生很大变化,并且我们希望支持任何肤色的人的皮肤检测,而无需任何校准,因此我们需要比简单的颜色阈值更强大的功能。

例如,如果一个简单的 HSV 皮肤检测器的色相相当红色,饱和度相当高但不是很高,并且其亮度不是太暗或太亮,则可以将任何像素视为皮肤。 但是移动相机的白平衡通常很差,因此一个人的皮肤看起来可能略带蓝色,而不是红色,依此类推,这对于简单的 HSV 阈值来说将是一个主要问题。

一种更强大的解决方案是使用 Haar 或 LBP 级联分类器执行人脸检测(如第 8 章,“使用 EigenFace 进行人脸识别”所示),然后查看检测到的面部中间像素的颜色,因为您知道这些像素应该是实际人物的皮肤像素。 然后,您可以扫描整个图像或附近区域中与脸部中心颜色相似的像素。 这具有的优点是,无论他们的肤色是什么,或者即使他们的皮肤在相机图像中显得有些蓝色或红色,也很有可能找到任何被检测到的人的至少某些真实皮肤区域。

不幸的是,在当前的移动设备上,使用级联分类器进行人脸检测的速度相当慢,因此该方法对于某些实时移动应用可能不太理想。 另一方面,我们可以利用以下事实:对于移动应用,可以假设用户将相机从近处直接朝向人脸握持,并且由于用户握住了相机可以轻松移动,因此要求用户将脸部放置在特定的位置和距离,而不是尝试检测脸部的位置和大小是很合理的。 这是许多移动电话应用的基础,其中该应用要求用户将其脸部放置在某个位置,或者手动在屏幕上拖动点以显示其脸角在照片中的位置。 因此,我们只需在屏幕*绘制一个脸部轮廓,然后让用户将其脸部移动到所示位置和大小即可。

向用户显示放置脸部的位置

首次启动外星人模式时,我们将在相机框的顶部绘制脸部轮廓,以便用户知道将脸部放置在何处。 我们将绘制一个大椭圆,覆盖图像高度的 70%,并且纵横比固定为 0.72,以便根据相机的纵横比,面部不会变得太瘦或太胖:

// Draw the color face onto a black background.
Mat faceOutline = Mat::zeros(size, CV_8UC3);
Scalar color = CV_RGB(255,255,0);    // Yellow.
int thickness = 4;
// Use 70% of the screen height as the face height.
int sw = size.width;
int sh = size.height;
int faceH = sh/2 * 70/100;  // "faceH" is the radius of the ellipse.
// Scale the width to be the same shape for any screen width. int faceW = faceH * 72/100;
// Draw the face outline.
ellipse(faceOutline, Point(sw/2, sh/2), Size(faceW, faceH),
 0, 0, 360, color, thickness, CV_AA);

为了更清楚地表明它是一张脸,让我们绘制两个眼睛轮廓。 与其将眼睛绘制为椭圆,不如通过将截断的椭圆绘制为眼睛的顶部,并将截断的椭圆绘制为底部的椭圆来使其更加逼真(请参见下图) 眼睛,因为我们可以在使用ellipse()绘制时指定起始和终止角度:

// Draw the eye outlines, as 2 arcs per eye.
int eyeW = faceW * 23/100;
int eyeH = faceH * 11/100;
int eyeX = faceW * 48/100;
int eyeY = faceH * 13/100;
Size eyeSize = Size(eyeW, eyeH);
// Set the angle and shift for the eye half ellipses.
int eyeA = 15; // angle in degrees.
int eyeYshift = 11;
// Draw the top of the right eye.
ellipse(faceOutline, Point(sw/2 - eyeX, sh/2 – eyeY),
 eyeSize, 0, 180+eyeA, 360-eyeA, color, thickness, CV_AA);
// Draw the bottom of the right eye.
ellipse(faceOutline, Point(sw/2 - eyeX, sh/2 - eyeY – eyeYshift),
 eyeSize, 0, 0+eyeA, 180-eyeA, color, thickness, CV_AA);
// Draw the top of the left eye.
ellipse(faceOutline, Point(sw/2 + eyeX, sh/2 - eyeY),
 eyeSize, 0, 180+eyeA, 360-eyeA, color, thickness, CV_AA);
// Draw the bottom of the left eye.
ellipse(faceOutline, Point(sw/2 + eyeX, sh/2 - eyeY – eyeYshift),
 eyeSize, 0, 0+eyeA, 180-eyeA, color, thickness, CV_AA);

我们可以使用相同的方法绘制嘴的下唇:

// Draw the bottom lip of the mouth.
int mouthY = faceH * 48/100;
int mouthW = faceW * 45/100;
int mouthH = faceH * 6/100;
ellipse(faceOutline, Point(sw/2, sh/2 + mouthY), Size(mouthW,
 mouthH), 0, 0, 180, color, thickness, CV_AA);

为了使用户将脸部放在显示的位置更加明显,让我们在屏幕上写一条消息!

// Draw anti-aliased text.
int fontFace = FONT_HERSHEY_COMPLEX;
float fontScale = 1.0f;
int fontThickness = 2;
char *szMsg = "Put your face here";
putText(faceOutline, szMsg, Point(sw * 23/100, sh * 10/100),
 fontFace, fontScale, color, fontThickness, CV_AA);

现在我们已经绘制了人脸轮廓,我们可以通过使用 alpha 混合将卡通化的图像与此绘制的轮廓相结合,将其叠加到显示的图像上:

addWeighted(dst, 1.0, faceOutline, 0.7, 0, dst, CV_8UC3);

它导致下图的轮廓,向用户显示了将脸放在哪里,因此我们无需检测脸部位置:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6K5VNAF0-1681871753487)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/master-opencv-prac-cv-proj/img/7829_1_7.jpg)]

换肤器的实现

我们可以使用 OpenCV 的floodFill()而不是先检测肤色,然后再检测具有该肤色的区域,这与许多图像编辑程序中的存储桶填充工具类似。 我们知道屏幕中间的区域应该是皮肤像素(因为我们要求用户将其脸部放在中间),因此要将整个脸部更改为绿色皮肤,我们只需在屏幕上应用绿色填充中心像素即可,它将始终将脸部的至少某些部分着色为绿色。 实际上,脸部的不同部分的颜色,饱和度和亮度可能会有所不同,因此,除非阈值太低以至于也覆盖了脸部之外的多余像素,否则泛色填充将很少覆盖脸部的所有皮肤像素。 面对。 因此,与其在图像的中心应用单个泛洪填充,不如在脸部周围六个不同的点(应该是皮肤像素)上应用泛洪填充。

OpenCV 的