C++ 机器人编程实用指南(二)

原文:zh.annas-archive.org/md5/E72C92D0A964D187E23464F49CAD88BE

译者:飞龙

协议:CC BY-NC-SA 4.0

第五章:使用笔记本电脑控制机器人

使用计算机控制机器人是一件迷人的事情。计算机成为遥控器,机器人根据键盘提供的命令移动。在本章中,我们将介绍使用笔记本电脑无线控制机器人的两种技术。

我们将涵盖以下主题:

  • 安装ncurses

  • 使用ncurses控制 LED 和蜂鸣器

  • 使用笔记本电脑键盘控制一辆漫游车(RPi 机器人)

  • 安装和设置 QT5

  • 使用 GUI 按钮控制 LED

  • 使用 QT5 在笔记本电脑上控制漫游车

技术要求

您需要此项目的主要硬件组件如下:

  • 两个 LED

  • 一个蜂鸣器

  • 一个 RPi 机器人

本章的代码文件可以从github.com/PacktPublishing/Hands-On-Robotics-Programming-with-Cpp/tree/master/Chapter05下载。

安装ncurses

New cursesncurses)是一个编程库,允许开发人员创建基于文本的用户界面。它通常用于创建基于 GUI 的应用程序或软件。ncurses库的一个关键特性是我们可以用它来从键盘键获取输入,并在输出端控制硬件设备。我们将使用ncurses库编写程序来检测键以相应地控制我们的机器人。例如,如果我们按上箭头,我们希望我们的机器人向前移动。如果我们按左箭头,我们希望我们的机器人向左转。

要安装ncurses库,我们首先必须打开命令窗口。要安装ncurses,请输入以下命令并按Enter

sudo apt-get install libncurses5-dev libncursesw5-dev 

接下来,您将被问及是否要安装该库。输入Y(表示是)并按Enterncurses库将需要大约三到五分钟的时间下载并安装到您的 RPi 中。

确保您的 RPi 靠近 Wi-Fi 路由器,以便库文件可以快速下载。

ncurses 函数

安装ncurses库后,让我们探索一些属于该库的重要函数:

  • initscr(): initscr()函数初始化屏幕。它设置内存,并清除命令窗口屏幕。

  • refresh(): 刷新函数刷新屏幕。

  • getch(): 此函数将检测用户的触摸,并返回该特定键的 ASCII 编号。然后将 ASCII 编号存储在整数变量中,以供后续比较使用。

  • printw(): 此函数用于在命令窗口中打印字符串值。

  • keypad(): 如果键盘函数设置为 true,则我们还可以从功能键和箭头键中获取用户的输入。

  • break: 如果程序在循环中运行,则使用此函数退出程序。

  • endwin(): endwin()函数释放内存,并结束ncurses

整个ncurses程序必须在initscr()endwin()函数之间编写:

#include <ncurses.h>
...
int main()
{
...
initscr();
...
...
endwin();
return 0;
}

使用ncurses编写 HelloWorld 程序

现在让我们编写一个简单的ncurses程序来打印Hello World。我将这个程序命名为HelloWorld.cppHelloWorld.cpp程序可以从 GitHub 存储库的Chapter05文件夹中下载:

#include <ncurses.h>
#include <stdio.h>

int main()
{
initscr(); //initializes and clear the screen
int keypressed = getch(); 
if(keypressed == 'h' || keypressed == 'H')
{
printw("Hello World"); //will print Hello World message
}
getch();
refresh(); 

endwin(); // frees up memory and ends ncurses
return 0;
}

使用ncurses库编译和运行 C++程序的程序与其他程序不同。首先,我们需要理解程序。之后,我们将学习如何编译和运行它。

在上面的代码片段中,我们首先声明了ncurses库和wiringPi库。接下来,我们执行以下步骤:

  1. main函数中,我们声明initscr()函数来初始化和清除屏幕。

  2. 接下来,当用户按下一个键时,将调用getch函数,并将该键的 ASCII 数字存储在keypressed变量中,该变量是int类型。

  3. 之后,使用for循环,我们检查按下的键是否为'h'或(||)'H'。确保将字母 H 放在单引号中。当我们将字母放在单引号中时,我们会得到该字符的 ASCII 数字。例如,'h'返回 ASCII 数字104,而'H'返回 ASCII 数字72。您也可以写入hH键按下的 ASCII 数字,分别为 104 和 72。这将如下所示:if(keypressed == 72 || keypressed == 104)。数字不应该在引号内。

  4. 然后,如果您按下'h''H'键,Hello World将在命令窗口内打印出来:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 如果要在下一行上打印Hello World,您可以在Hello World文本之前简单地放置\n。这将如下所示:printw("\nHello World")

  2. 之后,当您按下一个键时,在if条件下方的getch()函数将被调用,程序将终止。

编译和运行程序

要编译和运行HelloWorld.cpp程序,请打开终端窗口。在终端窗口内,输入ls并按Enter。现在您将看到您的 RPi 内所有文件夹名称的列表:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

HelloWorld.cpp存储在Cprograms文件夹中。要打开Cprograms文件夹,输入cd(更改目录)后跟文件夹名称,然后按Enter

cd Cprograms

可以看到上一个命令的输出如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

接下来,要查看Cprograms文件夹的内容,我们将再次输入ls

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Cprograms文件夹中,有一个Data文件夹和一些.cpp程序。我们感兴趣的程序是HelloWorld.cpp程序,因为我们想要编译和构建这个程序。要执行此操作,请输入以下命令并按Enter

gcc -o HelloWorld -lncurses HelloWorld.cpp 

以下屏幕截图显示编译成功:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

对于任何使用ncurses库的代码进行编译,代码如下:

gcc -o Programname -lncurses Programname.cpp

之后,输入./HelloWorld并按Enter运行代码:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

按下Enter后,整个终端窗口将被清除:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

接下来,按下hH键,Hello World文本将在终端窗口中打印出来。要退出终端窗口,请按任意键:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在我们已经创建了一个简单的HelloWorld程序,并测试了ncurses库在终端窗口内的工作,让我们编写一个程序来控制 LED 和蜂鸣器。

使用 ncurses 控制 LED 和蜂鸣器

在编译和测试您的第一个ncurses程序之后,让我们编写一个程序,通过从键盘提供输入来控制 LED 和蜂鸣器。

接线连接

对于这个特定的例子,我们将需要两个 LED 和一个蜂鸣器。LED 和蜂鸣器与 RPi 的接线连接如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们可以从连接图中看到以下内容:

  • 第一个 LED 的正极(阳极)引脚连接到 wiringPi 引脚号 15,负极(阴极)引脚连接到物理引脚号 6(地引脚)。

  • 第二个 LED 的正极引脚连接到 wiringPi 引脚号 4,负极引脚连接到物理引脚号 14(地引脚)。

  • 蜂鸣器的一根引脚连接到 wiringPi 引脚号 27,另一根引脚连接到物理引脚号 34(地引脚)。

编写 LEDBuzzer.cpp 程序

我们的程序名为LEDBuzzer.cppLEDBuzzer.cpp程序可以从 GitHub 存储库的Chapter05文件夹中下载。LEDBuzzer程序如下:

#include <ncurses.h>
#include <wiringPi.h>
#include <stdio.h>
int main()
{
 wiringPiSetup();

 pinMode(15,OUTPUT); //LED 1 pin
 pinMode(4, OUTPUT); //LED 2 pin
 pinMode(27,OUTPUT); //Buzzer pin

for(;;){

initscr();

int keypressed = getch();

if(keypressed=='L' || keypressed=='l')
{
 digitalWrite(15,HIGH);
 delay(1000);
 digitalWrite(15,LOW);
 delay(1000);
}

if(keypressed== 69 || keypressed=='e')       // 69 is ASCII number for E.
{
 digitalWrite(4,HIGH);
 delay(1000);
 digitalWrite(4,LOW);
 delay(1000);
}

if(keypressed=='D' || keypressed=='d')
{
 digitalWrite(15,HIGH);
 delay(1000);
 digitalWrite(15,LOW);
 delay(1000);
 digitalWrite(4,HIGH);
 delay(1000);
 digitalWrite(4,LOW);
 delay(1000);
}

if(keypressed=='B' || keypressed== 98)        //98 is ASCII number for b
{
 digitalWrite(27,HIGH);
 delay(1000);
 digitalWrite(27,LOW);
 delay(1000);
 digitalWrite(27,HIGH);
 delay(1000);
 digitalWrite(27,LOW);
 delay(1000);
}

if(keypressed=='x' || keypressed =='X')
{
break; 
}

refresh();
}
endwin(); // 
return 0; 
}

编写程序后,让我们看看它是如何工作的:

  1. 在上述程序中,我们首先声明了ncurseswiringPi库,以及stdio C 库

  2. 接下来,引脚编号1547被声明为输出引脚

  3. 现在,当按下Ll键时,LED 1 将分别在一秒钟内变为HIGHLOW

  4. 同样,当按下Ee键时,LED 2 将分别在一秒钟内变为HIGHLOW

  5. 如果按下Dd键,LED 1 将分别在一秒钟内变为HIGHLOW,然后 LED 2 将分别在一秒钟内变为HIGHLOW

  6. 如果按下bB键,蜂鸣器将响两次

  7. 最后,如果按下xX键,C++程序将被终止

在编译代码时,您还必须包括wiringPi库的名称,即lwiringPi。最终的编译命令如下:

gcc -o LEDBuzzer -lncurses -lwiringPi LEDBuzzer.cpp

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

编译代码后,键入./LEDBuzzer来运行它:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

接下来,按下LEDB键,LED 和蜂鸣器将相应地打开和关闭。

使用笔记本键盘控制一辆漫游车

在控制 LED 和蜂鸣器之后,让我们编写一个程序,从笔记本控制我们的漫游车(机器人):

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我保持了与第三章中相同的接线连接,编程机器人

  • wiringPi 引脚编号 0 和 2 连接到电机驱动器的IN1IN2引脚

  • wiringPi 引脚编号 3 和 4 连接到IN3IN4引脚

  • 左电机引脚连接到电机驱动器的OUT1OUT2引脚

  • 右电机引脚连接到电机驱动器的OUT3OUT4引脚

  • 树莓派的引脚 6 连接到电机驱动器的地线插座

构建一个由笔记本控制的漫游车程序

如果您已经理解了前两个程序,那么现在您可能已经找到了我们笔记本控制的漫游车代码。在这个程序中,我们将使用上、下、左和右箭头键以及ASXWD键将机器人向前、向后、向左和向右移动。为了识别来自箭头键的输入,我们需要在程序中包含keypad()函数。Laptop_Controlled_Rover.cpp程序可以从GitHub存储库的Chapter05文件夹中下载:


int main()
{
...
for(;;)
{
initscr(); 
keypad(stdscr,TRUE);
refresh(); 
int keypressed = getch(); 
if(keypressed==KEY_UP || keypressed == 'W' || keypressed == 'w') 
//KEY_UP command is for UP arrow key
{
printw("FORWARD");
digitalWrite(0,HIGH);
digitalWrite(2,LOW);
digitalWrite(3,HIGH);
digitalWrite(4,LOW);
}
if(keypressed==KEY_DOWN || keypressed == 'X' || keypressed == 'x')
//KEY_DOWN is for DOWN arrow key
{
printw("BACKWARD")
digitalWrite(0,LOW);
digitalWrite(2,HIGH);
digitalWrite(3,LOW);
digitalWrite(4,HIGH);
}

if(keypressed==KEY_LEFT || keypressed == 'A' || keypressed == 'a')
{
//KEY_LEFT is for LEFT arrow key
printw("LEFT TURN");
digitalWrite(0,LOW);
digitalWrite(2,HIGH);
digitalWrite(3,HIGH);
digitalWrite(4,LOW);
}

if(keypressed==KEY_RIGHT || keypressed == 'D' || keypressed == 'd')
{
//KEY_RIGHT is for right arrow keys
printw("RIGHT TURN");
digitalWrite(0,HIGH);
digitalWrite(2,LOW);
digitalWrite(3,LOW);
digitalWrite(4,HIGH);
}

if(keypressed=='S' || keypressed=='s')
{
printw("STOP");
digitalWrite(0,HIGH);
digitalWrite(2,HIGH);
digitalWrite(3,HIGH);
digitalWrite(4,HIGH);
}

if(keypressed=='E' || keypressed=='e')
{
break; 
}
}
endwin(); 
return 0; 
}

上述程序可以解释如下:

  1. 在上述程序中,如果按下上箭头键,这将被if条件内的KEY_UP代码识别。如果条件为TRUE,机器人将向前移动,并且终端中将打印FORWARD。类似地,如果按下Ww键,机器人也将向前移动。

  2. 如果按下下箭头键(KEY_DOWN)或Xx键,机器人将向后移动,并且终端中将打印BACKWARD

  3. 如果按下左箭头键(KEY_LEFT)或Aa键,机器人将向左转,终端中将打印LEFT TURN

  4. 如果按下右箭头键(KEY_RIGHT)或Dd键,机器人将向右转,终端中将打印RIGHT TURN

  5. 最后,如果按下Ss键,机器人将停止,并且终端中将打印STOP

  6. 要终止代码,我们可以按下Ee键。由于我们没有提供任何时间延迟,机器人将无限期地保持移动,除非您使用Ss键停止机器人。

在测试代码时,将树莓派连接到移动电源,这样你的机器人就完全无线,可以自由移动。

追踪一个正方形路径

在将机器人移动到不同方向后,让我们让机器人追踪一个正方形路径。为此,我们的机器人将按以下方式移动:向前->右转->向前->右转->向前->右转->向前->停止:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

LaptopControlRover程序中,我们将创建另一个if条件。在这个if条件内,我们将编写一个程序来使机器人追踪一个正方形路径。if条件将如下所示:

if(keypressed == 'r' || keypressed == 'R')
{
forward(); //first forward movement
delay(2000);
rightturn(); //first left turn
delay(500); //delay needs to be such that the robot takes a perfect 90º right turn

forward(); //second forward movement
delay(2000);
rightturn(); //second right turn
delay(500);

forward(); //third forward movement
delay(2000);
rightturn(); //third and last left turn
delay(500);

forward(); //fourth and last forward movement
delay(2000);
stop(); //stop condition
}

为了追踪正方形路径,机器人将向前移动四次。它将右转三次,最后停下来。在main函数之外,我们需要创建forward()rightturn()stop()函数,这样,我们可以简单地调用必要的函数,而不是在主函数中多次编写digitalWrite代码。

向前条件右转停止

|

void forward()
{
digitalWrite(0,HIGH);
 digitalWrite(2,LOW);
 digitalWrite(3,HIGH);
 digitalWrite(4,LOW);
}

|

void rightturn()
{
digitalWrite(0,HIGH); 
 digitalWrite(2,LOW); 
 digitalWrite(3,LOW); 
 digitalWrite(4,HIGH);
}

|

void stop()
{
digitalWrite(0,HIGH); 
 digitalWrite(2,HIGH); 
 digitalWrite(3,HIGH); 
 digitalWrite(4,HIGH);
}

|

这是我们如何使用笔记本电脑控制机器人,借助键盘按键的帮助。接下来,让我们看看第二种技术,我们将使用 QT5 创建 GUI 按钮。当按下这些按钮时,机器人将朝不同的方向移动。

安装和设置 QT5

QT 是一个跨平台应用程序框架,通常用于嵌入式图形用户界面。QT 的最新版本是 5,因此也被称为 QT5。要在我们的 RPi 内安装 QT5 软件,打开终端窗口并输入以下命令:

sudo apt-get install qt5-default

上述命令的输出如下截图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这个命令将下载在后台运行的必要的qt5文件。接下来,要下载和安装 QT5 IDE,输入以下命令:

sudo apt-get install qtcreator

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

QT5 IDE 的安装将需要大约 10 到 15 分钟,具体取决于您的互联网速度。如果在安装 QT5 时遇到任何问题,请尝试更新和升级您的 RPi。要做到这一点,请在终端窗口中输入以下命令:

sudo apt-get update
sudo apt-get upgrade -y

设置 QT5

在 QT5 中编写任何程序之前,我们首先需要设置它,以便它可以运行 C++程序。要打开 QT5,点击树莓图标,转到“编程”,然后选择“Qt Creator”:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

QT5 在 RPi 上运行速度较慢,因此打开 IDE 需要一些时间。点击工具,然后选择“选项…”:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在“选项…”中,点击设备,确保类型设置为桌面。名称应为“本地 PC”,这是指 RPi:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

之后,点击“构建和运行”选项。接下来,选择“工具包”选项卡,点击“桌面”(默认)选项:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

选择“构建和运行”选项后,我们需要进行一些修改:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

让我们逐步看修改:

  1. 保持名称为“桌面”。

  2. 将文件系统的名称设置为RPi

  3. 在设备类型中,选择桌面选项。

  4. 系统根(系统根)默认设置为/home/pi,这意味着当我们创建新的 QT5 应用程序时,它将被创建在pi文件夹内。现在,我们将在pi文件夹内创建一个名为QTPrograms的新文件夹,而不是在pi文件夹中创建我们的 QT 项目。要更改文件夹目录,点击“浏览”按钮。之后,点击文件夹选项。将此文件夹命名为QTPrograms,或者您想要的任何其他名称。选择QTPrograms文件夹,然后选择“选择”按钮:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 接下来,我们必须将编译器设置为 GCC。要做到这一点,点击编译器选项卡。在里面,点击“添加”下拉按钮。转到 GCC 并选择 C++选项:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在,在 C++选项下,您将看到 GCC 编译选项:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

之后,点击 Apply 按钮应用更改,然后点击 OK 按钮。接下来,再次点击 Tools,打开 Options。在 Build and run 选项内,选择 Kits 选项卡,再次选择 Desktop 选项。这次,在 C++选项旁边,您将看到一个下拉选项。点击这个选项,选择 GCC 编译器:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 接下来,检查调试器选项。它应该设置为位于/usr/bin/gdb 的 System GDB。

  2. 最后,检查 QT5 版本。目前,我正在使用最新版本的 QT,即 5.7.1。当您阅读到这一章时,最新版本可能已经更新。

进行这些更改后,点击 Apply,然后点击 OK。在设置 QT5 之后,让我们编写我们的第一个程序,使用 GUI 按钮来打开和关闭 LED。

使用 GUI 按钮控制 LED

在本节中,我们将创建一个简单的 QT5 程序,通过 GUI 按钮来控制 LED 的开关。对于这个项目,您将需要两个 LED:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

LED 的接线与LEDBuzzer项目中的完全相同:

  • 第一个 LED 的阳极(正极)引脚连接到 wiringPi 引脚号 0,阴极(负极)引脚连接到物理引脚号 9(地线引脚)

  • 第二个 LED 的阳极引脚连接到 wiringPi 引脚号 2,阴极引脚连接到物理引脚号 14(地线引脚)

创建 QT 项目

用于打开和关闭 LED 的 QT5 项目称为LedOnOff。您可以从 GitHub 存储库的Chapter05文件夹中下载此项目。下载LedOnOff项目文件夹后,打开LedOnOff.pro文件以在 QT5 IDE 中查看项目。

按照以下步骤在 QT5 IDE 中创建项目:

  1. 点击 File 选项,然后点击 New File or Project…:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 接下来,选择 QT Widgets Application 选项,然后点击 Choose 按钮:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 之后,给您的项目命名。我将我的项目命名为LEDOnOff。之后,将目录更改为QTPrograms,以便在此文件夹中创建项目,然后点击 Next:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 保持 Desktop 选项选中,然后点击 Next:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 现在您应该看到某些文件名,这些是项目的一部分。保持名称不变,然后点击 Next:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 最后,您将看到一个摘要窗口,其中将显示将要创建的所有文件的摘要。我们不需要在此窗口中进行任何更改,因此点击 Finish 创建项目:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在 IDE 的左侧,您将看到设计、C++和头文件。首先,我们将打开LEDOnOff.pro文件并添加wiringPi库的路径。在文件底部,添加以下代码:

LIBS += -L/usr/local/lib -lwiringPi

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

接下来,打开Forms文件夹内的mainwindow.ui文件。mainwindow.ui文件是设计文件,我们将在其中设计 GUI 按钮:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

mainwindow.ui文件将在 Design 选项卡中打开。在 Design 选项卡的左侧是小部件框,其中包含按钮、列表视图和布局等小部件。中间是设计区域,我们将在其中拖动 UI 组件。在右下角,显示所选 UI 组件的属性:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

接下来,要创建 GUI 按钮,将 Push Button 小部件拖到设计区域内。双击按钮,将文本更改为ON。之后,选中 Push Button,将 objectName(在属性窗口内)更改为on

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

之后,添加两个按钮。将一个按钮的名称设置为OFFobjectName设置为off。将另一个按钮的名称设置为ON / OFFobjectName设置为onoff

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们可以使用两种不同类型的按钮函数来打开和关闭 LED:

  • clicked(): clicked按钮函数将在按钮被点击时立即执行。

  • pressed()released(): pressed按钮函数会在您按住或按住按钮时一直执行。当我们使用pressed函数时,我们还必须使用released()函数。释放的函数包含指示按钮释放时应发生的操作的代码。

我们将把clicked()函数链接到ONOFF按钮,并将pressed()released()函数链接到ON/OFF按钮。接下来,要将clicked()函数链接到ON按钮,右键单击ON按钮,选择 Go to slot…选项,然后选择clicked()函数。然后,按下 OK:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在,一旦您选择clicked()函数,mainwindow.cpp文件(此文件位于Sources文件夹中)中将创建一个名为on_on_clicked()on_buttonsobjectname_clicked)的点击函数。在此函数中,我们将编写打开 LED 的程序。但在此之前,我们需要在mainwindow.h文件中声明wiringPi库和引脚。此文件位于Headers文件夹中:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们还需要声明QMainWindow库,它将创建一个包含我们按钮的窗口。接下来,我已将led1引脚设置为引脚0,将led2引脚设置为引脚2。之后,再次打开mainwindow.cpp文件。然后我们将执行以下操作:

  1. 首先,我们将声明wiringPiSetup();函数

  2. 接下来,我们将把led1led2设置为OUTPUT引脚

  3. 最后,在on_on_clicked()函数中,将led1led2引脚设置为HIGH

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

接下来,要关闭 LED 灯,再次打开mainwindow.ui文件,右键单击关闭按钮,选择 Go to slot…,然后再次选择clicked()函数。在mainwindow.cpp文件中,将创建一个名为on_off_clicked的新函数。在此函数中,我们将编写关闭 LED 灯的程序。

要编程 ON/OFF 按钮,右键单击它,选择 Go to slot…,然后选择pressed()函数。将在mainwindow.ui文件中创建一个名为on_onoff_pressed()的新函数。接下来,右键单击ON/OFF按钮,选择 Go to slot…,然后选择released()函数。现在将创建一个名为on _onoff_released()的新函数。

on_onoff_pressed()函数中,我们将编写一个程序来打开 LED 灯。在on_onoff_released()函数中,我们将编写一个程序来关闭 LED 灯:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在运行代码之前,单击文件,然后单击全部保存。接下来,要构建和运行代码,请单击构建,然后单击运行选项。MainWindow 出现需要大约 30 到 40 秒,在主窗口中,您将看到以下 GUI 按钮:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在,当您点击 ON 按钮时,LED 将打开。当您点击 OFF 按钮时,LED 将关闭。最后,当您按住ON / OFF按钮时,LED 将一直打开,直到您松开为止,然后它们将关闭。

处理错误

在控制台中,您可能会看到一些次要错误。如果主窗口已打开,您可以忽略这些错误:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

当您打开 Qt Creator IDE 时,GCC 编译器可能会不断重置。因此,在运行项目后,您将收到以下错误:

Error while building/deploying project LEDOnOff (kit: Desktop)
 When executing step "qmake"

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如果您遇到此错误,请转到工具,然后选项,并将 C++编译器设置为 GCC,如“设置 QT5”部分的步骤 5中所示。

使用 QT5 控制笔记本电脑的小车

现在我们可以控制 LED 灯,让我们看看如何使用 QT5 控制小车。在 Qt Creator IDE 中,创建一个新项目并命名为QTRover。您可以从本章的 GitHub 存储库中下载QTRover项目文件夹。我们现在可以使用clicked()函数和pressed()released()函数来创建这个QTRover项目。为此,我们有以下选项:

  1. 如果我们只使用clicked()函数创建这个项目,我们需要创建五个按钮:前进、后退、左转、右转和停止。在这种情况下,我们需要每次按下停止按钮来停止机器人。

  2. 如果我们只使用pressed()released()函数创建这个项目,我们只需要创建四个按钮:前进、后退、左转和右转。在这种情况下,我们不需要停止按钮,因为当按钮释放时,小车会停止。

  3. 或者,我们也可以使用clicked()pressed()released()函数的组合,其中前进、后退和停止按钮将链接到clicked()函数,左右按钮将链接到pressed()released()函数。

在这个项目中,我们将选择第三个选项,即clicked()pressed()released()函数的组合。在创建这个项目之前,我们将关闭LEDOnOff项目,因为如果LEDOnOffQTRover项目都保持打开状态,有可能如果您在一个项目中进行 UI 更改,代码可能会在另一个项目中更改,从而影响到您的两个项目文件。要关闭LEDOnOff项目,请右键单击它,然后选择关闭项目LEDOnOff选项。

接下来,在QTRover.pro文件中添加wiringPi库路径:

LIBS += -L/usr/local/lib -lwiringPi

之后,打开mainwindow.ui文件并创建五个按钮。将它们标记为FORWARDBACKWARDLEFTRIGHTSTOP

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

将按钮对象的名称设置如下:

  • FORWARD按钮对象名称设置为 forward

  • BACKWARD按钮对象名称设置为 backward

  • LEFT按钮对象名称设置为 left

  • RIGHT按钮对象名称设置为 right

  • STOP按钮对象名称设置为 stop

之后,右键单击前进、后退和停止按钮,并将clicked()函数添加到这三个按钮。同样,右键单击左和右按钮,并将pressed()released()函数添加到这些按钮。

接下来,打开mainwindow.h文件并声明wiringPiQMainWindow库。还要声明四个wiringPi引脚号。在我的情况下,我使用引脚号0234

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

mainwindow.cpp文件内,我们将有三个on_click函数来向前移动(on_forward_clicked)、向后移动(on_backward_clicked)和停止(on_stop_clicked)。

我们还有两个on_pressedon_released函数用于左(on_left_pressedon_left_released)和右(on_right_pressedon_right_released)按钮。

以下步骤描述了移动机器人在不同方向上所需的步骤:

  1. on_forward_clicked()函数内,我们将编写程序来使机器人向前移动:
digitalWrite(leftmotor1, HIGH);
digitalWrite(leftmotor2, LOW);
digitalWrite(rightmotor1, HIGH);
digitalWrite(rightmotor2, LOW);
  1. 接下来,在on_backward_clicked()函数内,我们将编写程序来使机器人向后移动:
digitalWrite(leftmotor1, HIGH);
digitalWrite(leftmotor2, LOW);
digitalWrite(rightmotor1, HIGH);
digitalWrite(rightmotor2, LOW);
  1. 之后,在on_left_pressed()函数内,我们将编写程序来进行轴向左转或径向左转:
digitalWrite(leftmotor1, LOW);
digitalWrite(leftmotor2, HIGH);
digitalWrite(rightmotor1, HIGH);
digitalWrite(rightmotor2, LOW);
  1. 然后,在on_right_pressed()函数内,我们将编写程序来进行轴向右转或径向右转:
digitalWrite(leftmotor1, HIGH);
digitalWrite(leftmotor2, LOW);
digitalWrite(rightmotor1, LOW);
digitalWrite(rightmotor2, HIGH);
  1. on_stop_clicked()函数内,我们将编写程序来停止机器人:
digitalWrite(leftmotor1, HIGH);
digitalWrite(leftmotor2, HIGH);
digitalWrite(rightmotor1, HIGH);
digitalWrite(rightmotor2, HIGH);

完成代码后,保存所有文件。之后,运行程序并测试最终输出。运行代码后,您将看到带有向前、向后、向左、向右和停止按钮的主窗口。按下每个 GUI 按钮以使机器人朝所需方向移动。

总结

在本章中,我们看了两种不同的技术来使用笔记本电脑控制机器人。在第一种技术中,我们使用ncurses库从键盘接收输入,以相应地移动机器人。在第二种技术中,我们使用 QT Creator IDE 创建 GUI 按钮,然后使用这些按钮来使机器人朝不同方向移动。

在下一章中,我们将在树莓派上安装 OpenCV 软件。之后,我们将使用树莓派摄像头记录图片和视频。

问题

  1. ncurses程序应该在哪两个函数之间编写?

  2. initscr()函数的目的是什么?

  3. 如何在终端窗口中编译ncurses代码?

  4. 我们在 QT Creator 中使用了哪个 C++编译器?

  5. 你会使用哪个按钮功能或功能来在按下按钮时使机器人向前移动?

第三部分:人脸和物体识别机器人

在本节中,您将使用 OpenCV 来检测人脸和现实世界中的物体。然后,我们将扩展 OpenCV 的功能,以识别不同的人脸,并在检测到正确的人脸时移动机器人。

本节包括以下章节:

  • 第六章,使用 OpenCV 访问 RPi 相机

  • 第七章,使用 OpenCV 构建一个物体跟随机器人

  • 第八章,使用 Haar 分类器进行人脸检测和跟踪

第六章:使用 OpenCV 访问 RPi 相机

我们可以使用树莓派连接到外部 USB 网络摄像头或树莓派相机RPi 相机)来识别对象和人脸,这是树莓派最令人兴奋的事情之一。

为了处理来自相机的输入,我们将使用 OpenCV 库。由于安装 OpenCV 需要很长时间并涉及多个步骤,本章将专门用于让您开始运行。

在本章中,您将探索以下主题:

  • 在树莓派上安装 OpenCV 4.0.0

  • 启用并连接 RPi 相机到 RPi

  • 使用 RPi 相机捕获图像和视频

  • 使用 OpenCV 读取图像

技术要求

在本章中,您将需要以下内容:

  • 树莓派相机模块-截至 2019 年,最新的 RPi 相机模块称为RPi 相机 V2 1080P

  • 树莓派相机外壳(安装支架)

本章的代码文件可以从github.com/PacktPublishing/Hands-On-Robotics-Programming-with-Cpp/tree/master/Chapter06下载。

在树莓派上安装 OpenCV 4.0.0

开源计算机视觉库OpenCV)是一个开源的计算机视觉和机器学习库。OpenCV 库包括 2500 多个计算机视觉和机器学习算法,可用于识别对象、检测颜色和跟踪现实生活中或视频中的运动物体。OpenCV 支持 C++、Python 和 Java 编程语言,并可以在 Windows、macOS、Android 和 Linux 上运行。

在树莓派上安装 OpenCV 是一个耗时且冗长的过程。除了 OpenCV 库,我们还必须安装多个库和文件,以使其正常工作。安装 OpenCV 的步骤将在我运行 Raspbian Stretch 的树莓派 3B+型号上执行。我们要安装的 OpenCV 版本是 OpenCV 4.0.0。

在安装 OpenCV 时,我们将下载多个文件。如果您住在大房子里,请确保您坐在 Wi-Fi 路由器附近,以便 RPi 接收良好的信号强度。如果 RPi 离 Wi-Fi 很远,下载速度可能会受到影响,安装 OpenCV 可能需要更长的时间。我在我的 RPi 3B+上安装 OpenCV 大约花了 3 个小时,下载速度大约为 500-560 Kbps。

卸载 Wolfram 和 LibreOffice

如果您使用 32GB 的 microSD 卡,Raspbian Stretch 将只占用存储空间的 15%,但如果您使用 8GB 的 microSD 卡,它将占用 50%的空间。如果您使用 8GB 的 microSD 卡,您需要释放一些空间。您可以通过卸载一些未使用的应用程序来实现。其中两个应用程序是 Wolfram 引擎和 LibreOffice。

在 Raspbian Stretch 上卸载应用程序很容易。您只需要在终端窗口中输入一个命令。让我们从卸载 Wolfram 引擎开始:

sudo apt-get purge wolfram-engine -y

接下来,使用相同的命令卸载 LibreOffice:

sudo apt-get purge libreoffice* -y

卸载两个软件后,我们可以使用两个简单的命令进行清理:

sudo apt-get clean
sudo apt-get autoremove -y

现在我们已经释放了一些空间,让我们更新 RPi。

更新您的 RPi

更新您的 RPi 涉及一些简单的步骤:

  1. 打开终端窗口,输入以下命令:
sudo apt-get update 
  1. 通过输入以下命令升级 RPi:
sudo apt-get upgrade -y
  1. 重新启动 RPi:
sudo shutdown -r now

一旦您的 RPi 重新启动,再次打开终端窗口。

在终端窗口运行某些命令时,您可能会收到提示,询问您是否要继续。在此过程的命令中,我们已经添加了-y命令(在行的末尾),它将自动应用yes命令到提示。

安装 cmake、image、video 和 gtk 软件包

cmake是一个配置实用程序。使用cmake,我们可以在安装后配置不同的 OpenCV 和 Python 模块。要安装cmake软件包,请输入以下命令:

sudo apt-get install build-essential cmake pkg-config -y

接下来,要安装图像 I/O 软件包,请输入以下命令:

sudo apt-get install libjpeg-dev libtiff5-dev libjasper-dev libpng12-dev -y

之后,我们将通过输入以下命令安装两个视频 I/O 软件包:

sudo apt-get install libavcodec-dev libavformat-dev libswscale-dev libv4l-dev -y
sudo apt-get install libxvidcore-dev libx264-dev -y

接下来,我们将下载并安装Gimp ToolkitGTK)软件包。此工具包用于为我们的程序制作图形界面。我们将执行以下命令来下载和安装 GTK 软件包:

sudo apt-get install libgtk2.0-dev libgtk-3-dev -y
sudo apt-get install libatlas-base-dev gfortran -y

下载和解压 OpenCV 4.0 及其贡献存储库

安装了这些软件包后,我们可以继续进行 OpenCV。让我们开始下载 Open CV 4.0:

  1. 在终端窗口中输入以下命令:
wget -O opencv.zip https://github.com/opencv/opencv/archive/4.0.0.zip
  1. 下载包含一些附加模块的opencv_contrib存储库。输入以下命令:
wget -O opencv_contrib.zip https://github.com/opencv/opencv_contrib/archive/4.0.0.zip

步骤 1步骤 2中的命令都是单行命令。

  1. 使用以下命令解压opencv.zip文件:
unzip opencv.zip
  1. 解压opencv_contrib.zip文件:
unzip opencv_contrib.zip

解压opencvopencv_contrib后,您应该在pi文件夹中看到opencv-4.0.0opencv_contrib-4.0.0文件夹。

安装 Python

接下来,我们将安装 Python 3 及其一些支持工具。即使我们将使用 C++编程 OpenCV,安装并链接 Python 包与 OpenCV 仍然是一个好主意,这样您就可以选择使用 OpenCV 编写或编译 Python 代码。

要安装 Python 及其开发工具,请输入以下命令:

sudo apt-get install python3 python3-setuptools python3-dev -y
wget https://bootstrap.pypa.io/get-pip.py
sudo python3 get-pip.py
sudo pip3 install numpy

安装 Python 软件包后,我们可以编译和构建 OpenCV。

编译和安装 OpenCV

要编译和安装 OpenCV,我们需要按照以下步骤进行:

  1. 进入opencv-4.0.0文件夹。使用以下命令更改目录到opencv-4.0.0文件夹:
cd opencv-4.0.0
  1. 在此文件夹中创建一个build文件夹。为此,请输入以下命令:
mkdir build
  1. 要打开build目录,请输入以下命令:
cd build
  1. 更改目录到build后,输入以下命令:
cmake -D CMAKE_BUILD_TYPE=RELEASE \
-D CMAKE_INSTALL_PREFIX=/usr/local \
-D BUILD_opencv_java=OFF \
-D BUILD_opencv_python2=OFF \
-D BUILD_opencv_python3=ON \
-D PYTHON_DEFAULT_EXECUTABLE=$(which python3) \
-D INSTALL_C_EXAMPLES=ON \
-D INSTALL_PYTHON_EXAMPLES=ON \
-D BUILD_EXAMPLES=ON\
-D OPENCV_EXTRA_MODULES_PATH=~/opencv_contrib-4.0.0/modules \
-D WITH_CUDA=OFF \
-D BUILD_TESTS=OFF \
-D BUILD_PERF_TESTS= OFF ..

在输入此命令时,请确保在终端窗口中输入两个点..

  1. 要启用 RPi 的所有四个内核,请在 nano 编辑器中打开swapfile文件:
sudo nano /etc/dphys-swapfile
  1. 在此文件中,搜索CONF_SWAPSIZE=100代码,并将值从100更改为1024

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 按下Ctrl + O保存此文件。您将在文件底部收到提示,询问您是否要保存此文件。按Enter,然后按*Ctrl *+ X退出。

  2. 要应用这些更改,请输入以下两个命令:

sudo /etc/init.d/dphys-swapfile stop
sudo /etc/init.d/dphys-swapfile start
  1. 要使用 RPi 的所有四个内核编译 OpenCV,请输入以下命令:
make -j4

这是最耗时的步骤,需要 1.5 到 2 小时。如果在编译时遇到任何错误,请尝试使用单个内核进行编译。

要使用单个内核进行编译,请输入以下命令:

sudo make install
make

只有在使用make -j4命令时遇到错误时才使用前面的两个命令。

  1. 要安装 OpenCV 4.0.0,请输入以下命令:
sudo make install
sudo ldconfig 

我们现在已经编译并安装了 OpenCV。让我们将其连接到 Python。

将 OpenCV 链接到 Python

让我们按照以下步骤将 OpenCV 链接到 Python:

  1. 打开python 3.5文件夹(/usr/local/python/cv2/python-3.5):

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在此文件夹中,您应该看到一个名为cv2.socv2.cpython-35m-arm-linux-gnueabihf.so的文件。如果文件名是cv2.so,则无需进行任何更改。如果文件名是cv2.cpython-35m-arm-linux-gnueabihf.so,则必须将其重命名为cv2.so。要重命名此文件,请输入以下命令更改目录到python 3.5

cd /usr/local/python/cv2/python-3.5

将此文件从cv2.cpython-35m-arm-linux-gnueabihf.so重命名为cv2.so,输入以下命令:

sudo mv /usr/local/python/cv2/python3.5/cv2.cpython-35m-arm-linux-gnueabihf.so cv2.so
  1. 使用以下命令将此文件移动到dist-package文件夹(/usr/local/lib/python3.5/dist-packages/):
sudo mv /usr/local/python/cv2/python-3.5/cv2.so /usr/local/lib/python3.5/dist-packages/cv2.so
  1. 要测试 OpenCV 4.0.0 是否正确链接到 Python 3,请在终端窗口中输入cd ~进入pi目录。接下来,输入python3

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 您应该看到一个三角括号。输入import cv2

  2. 要检查 OpenCV 版本,请输入cv2.__version__。如果看到opencv 4.0.0,这意味着 OpenCV 已成功安装并与 Python 软件包链接:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 输入exit()并按Enter

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

安装 OpenCV 后,我们需要将CONF_SWAPSIZE重置为100

  1. 打开swapfile
sudo nano /etc/dphys-swapfile
  1. CONF_SWAPSIZE更改为100

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 要应用这些更改,请输入以下命令:
sudo /etc/init.d/dphys-swapfile stop
sudo /etc/init.d/dphys-swapfile start

您已成功在树莓派上安装了 OpenCV 4.0.0。我们现在准备将 RPi 相机连接到 RPi。

启用并连接 RPi 相机到 RPi

在连接 RPi 相机到 RPi 之前,我们需要从 RPi 配置中启用相机选项:

  1. 打开一个终端窗口并输入sudo raspi-config打开 RPi 配置。

  2. 选择“高级选项”并按Enter打开它:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 选择相机选项并按Enter打开它:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 选择“是”并按Enter启用相机选项:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 选择确定并按Enter

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 退出 RPi 配置并关闭 RPi。

在连接 RPi 相机到 RPi 时,请确保 RPi 已关闭。

现在我们已经完成了设置,让我们连接相机。

连接 RPi 相机到 RPi

连接 RPi 相机到 RPi 是一个简单但又微妙的过程。RPi 相机有一根连接的带线。我们必须将这根带线插入 RPi 的相机插槽中,该插槽位于 LAN 端口和 HDMI 端口之间:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

RPi 相机上的带线由前面的蓝色条组成,后面没有:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在我们了解了组件和端口,让我们开始连接它们:

  1. 轻轻抬起相机插槽的盖子:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 将相机带插入插槽,确保带子上的蓝色胶带面向 LAN 端口。

  2. 按下盖子锁定相机带线:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

就是这样——您的 RPi 相机现在已准备好拍照和录制视频。

安装 RPi 相机在机器人上

让我们在机器人上安装 RPi 相机;您需要一个 RPi 相机盒子。在amazon.com上快速搜索RPi 相机盒子将显示以下情况:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我不推荐这个特定的情况,因为它没有正确安装我的 RPi 相机模块。当盒子关闭时,我的 RPi 相机的镜头没有正确对齐这个相机盒子的小孔。

由于我住在印度,在亚马逊印度网站(www.amazon.in)上找不到好的 RPi 相机盒子,而且可用的盒子价格昂贵。我最终使用的盒子来自一个名为www.robu.in的印度电子商务网站,只花了我 90 卢比(不到 2 美元)。在从电子商务网站购买相机盒子或相机支架之前,请检查评论以确保它不会损坏您的 RPi 相机。

我使用的 RPi 相机盒子的图像显示在以下图像中。我从一个名为www.robu.in的印度网站购买了这个盒子。在这个网站上,搜索树莓派相机支架模块以找到这个相机支架:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

尽管此摄像头支架包含四个小螺母和螺栓将 RPi 摄像头固定到摄像头支架上,但我发现螺母和螺栓的螺纹不准确,并且将 RPi 摄像头固定到摄像头支架上非常困难。因此,我使用了四小块双面胶带,并将其粘贴到 RPi 摄像头的孔中:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

接下来,我将 RPi 摄像头安装到摄像头支架上。在下图中,RPi 摄像头被倒置安装。因此,当我们捕获图像时,图像将呈倒置状态,为了正确查看图像,我们需要翻转它(在 OpenCV 中解释了在第七章中水平和垂直翻转图像的过程,使用 OpenCV 构建对象跟随机器人):

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

之后,我使用双面胶带在 RPi 外壳顶部安装了摄像头支架,从而将 RPi 摄像头安装在机器人上:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在我们已经将摄像头外壳安装到机器人上,让我们看看如何使用 RPi 摄像头捕获图像和视频。

使用 RPi 摄像头捕获图像和视频

让我们看看如何在 RPi 上拍照和录制视频。打开终端窗口,输入以下命令:

raspistill -o image1.jpg

在此命令中,我们使用raspistill拍摄静态图片,并将其保存为image1.jpg

由于终端窗口指向pi目录,因此此图像保存在pi文件夹中。要打开此图像,请打开pi文件夹,在其中您将看到image1.jpg。使用 RPi 摄像头捕获的图像具有本机分辨率为 3,280 x 2,464 像素:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

image1的输出如下截图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如果我们想水平翻转图像,可以添加-hf命令,如果要垂直翻转图像,可以在raspistill代码中添加-vf命令:

raspistill -hf -vf -o image2.jpg

image2.jpg文件也保存在pi文件夹中,其输出如下截图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在我们已经使用 RPi 摄像头捕获了图像,让我们录制并查看视频。

使用 RPi 摄像头录制视频

现在我们知道如何使用 RPi 摄像头拍照,让我们看看如何录制视频。录制视频剪辑的命令如下:

raspivid -o video1.h264 -t 5000 

如下截图所示,上述命令不会产生任何输出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在我们的命令中,我们使用raspivid录制视频,并将其命名为video1。我们以h264格式录制了视频。数字5000代表 5000 毫秒,也就是说,我们录制了一个 5 秒的视频。您可以打开pi文件夹,双击视频文件以打开它:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在我们知道如何拍照和录制视频,让我们安装v4l2驱动程序,以便 OpenCV 库可以检测到 RPi 摄像头。

安装 v4l2 驱动程序

OpenCV 库默认可以识别连接到 RPi USB 端口的 USB 摄像头,但无法直接检测 RPi 摄像头。要识别我们的 RPi 摄像头,我们需要在模块文件中加载v4l2驱动程序。要打开此文件,请在终端窗口中输入以下命令:

sudo nano /etc/modules

要加载v4l2驱动程序,请在以下文件中添加bcm2835-v4l2

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

按下Ctrl + O,然后按Enter保存此文件,按下Ctrl + X退出文件,然后重新启动您的 RPi。重新启动后,OpenCV 库将识别 RPi 摄像头。

使用 OpenCV 读取图像

在 RPi 相机上玩了一会儿之后,让我们使用 OpenCV 函数编写一个简单的 C++程序来显示图像。在这个程序中,我们首先从一个特定的文件夹中读取图像,然后在新窗口中显示这个图像:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

要显示图像,我们首先需要一张图像。在pi文件夹中,我创建了一个名为Data的新文件夹,在其中,我复制了一张名为Car.png的图像。在同一个文件夹中,我创建了DisplayImage.cpp文件,我们将在其中编写显示图像的程序。DisplayImage.cpp程序可以从本书的 GitHub 存储库的Chapter06文件夹中下载。代码如下:

#include <iostream>
#include <stdio.h>
#include <opencv2/opencv.hpp>

using namespace cv;
using namespace std;
int main()
{

Mat img;

img = imread("Car.jpg");

imshow("Car Image", img);

waitKey(0);

return 0;
}

在上述代码中,我们首先声明了opencv.hpp库,以及基本的 C++库。然后声明了cv命名空间,它是 OpenCV 库的一部分。在main函数内部,我们声明了一个名为img的矩阵(Mat)变量。

接下来,使用imread()函数读取Car.jpg图像,并将值存储在img变量中。如果图像和.cpp文件在同一个文件夹中,只需在imread()函数中写入图像名称。如果图像在不同的文件夹中,则应在imread函数中提及图像的位置。

imshow()函数用于在新窗口中显示汽车图像。imshow()函数接受两个参数作为输入。第一个参数是窗口文本("Car Image"),第二个参数是要显示的图像的变量名(img)。

waitKey(0)函数用于创建无限延迟,也就是说,waitKey(0)将无限地显示汽车图像,直到您按下任意键。按下键后,将执行下一组代码。由于在waitKey(0)函数之后没有任何代码,程序将终止,汽车图像窗口将关闭。

要在 RPi 内部编译和构建 OpenCV 代码,我们需要在编译和构建框内添加以下行:

  1. 单击构建选项,然后选择设置构建命令。在编译框内,输入以下命令:
g++ -Wall $(pkg-config --cflags opencv) -c "%f" -lwiringPi
  1. 在构建框内,输入以下命令,然后单击“确定”:
g++ -Wall $(pkg-config --libs opencv) -o "%e" "%f" -lwiringPi
  1. 单击编译按钮编译代码,然后单击构建按钮测试输出。在输出中,将创建一个新窗口,在其中将显示汽车图像:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 如果按任意键,程序将终止,汽车图像窗口将关闭。

总结

在本章中,我们专注于在树莓派上安装 OpenCV。您已经了解了 RPi 相机模块。设置 RPi 相机后,您使用 RPi 相机拍摄了照片并录制了一个短视频剪辑。

在下一章中,我们将使用 OpenCV 库编写 C++程序。您将学习不同的图像处理概念,以便可以扫描、阈值化和识别对象。在识别对象之后,我们将为机器人编写程序,使其跟随该对象。

问题

  1. OpenCV 的全称是什么?

  2. RPi 相机拍摄的图像分辨率是多少?

  3. 使用 RPi 相机拍摄图像的命令是什么?

  4. 使用 RPi 相机录制视频的命令是什么?

  5. Raspbian OS 在 8GB 和 32GB SD 卡上占用的内存百分比是多少?

第七章:使用 OpenCV 构建一个目标跟随机器人

在上一章中安装了 OpenCV 之后,现在是时候使用 OpenCV 库执行图像处理操作了。在本章中,我们将涵盖以下主题:

  • 使用 OpenCV 进行图像处理

  • 查看来自 Pi 摄像头的视频源

  • 构建一个目标跟随机器人

技术要求

对于本章没有新的技术要求,但是您需要以下内容来执行示例:

  • 用于检测红色、绿色或蓝色的球

  • 安装在机器人上的 Pi 摄像头和超声波传感器

本章的代码文件可以从github.com/PacktPublishing/Hands-On-Robotics-Programming-with-Cpp/tree/master/Chapter07下载。

使用 OpenCV 进行图像处理

在本节中,我们将查看 OpenCV 库的重要函数。之后,我们将使用 OpenCV 库编写一个简单的 C++程序,并对图像执行不同的图像处理操作。

OpenCV 中的重要函数

在编写任何 OpenCV 程序之前,了解 OpenCV 中的一些主要函数以及这些函数可以给我们的输出是很重要的。让我们从查看这些函数开始:

  • imread(): imread()函数用于从 Pi 摄像头或网络摄像头读取图像或视频。在imread()函数内部,我们必须提供图像的位置。如果图像和程序文件在同一个文件夹中,我们只需要提供图像的名称。但是,如果图像存储在不同的文件夹中,那么我们需要在imread函数内提供图像的完整路径。我们将从imread()函数中存储的图像值存储在一个矩阵(Mat)变量中。

如果图像和.cpp文件在同一个文件夹中,代码如下所示:

Mat img = imread("abcd.jpg"); //abcd.jpg is the image name

如果图像和.cpp文件在不同的文件夹中,代码如下所示:

Mat img = imread("/home/pi/abcd.jpg"); //abcd image is in 
                                      // the Pi folder

  • imshow(): imshow()函数用于显示或查看图像:
imshow("Apple Image", img);

imshow()函数包括两个参数,如下:

    • 第一个参数是窗口文本
  • 第二个参数是要显示的图像的变量名

imshow()函数的输出如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • resize(): resize()函数用于调整图像的尺寸。当用户同时使用多个窗口时,通常会使用此函数:
resize(img, rzimg, cvSize(400,400));  //new width is 400 
                                     //and height is 400

此函数包括三个参数:

    • 第一个参数是要调整大小的原始图像(img)的变量名。
  • 第二个参数是将调整大小的新图像(rzimg)的变量名。

  • 第三个参数是cvSize,在其中输入新宽度高度值

resize()函数的输出如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • flip(): 此函数用于水平翻转、垂直翻转或同时进行两者:
flip(img, flipimage, 1)

此函数包括三个参数:

    • 第一个参数(img)是原始图像的变量名。
  • 第二个参数(flipimage)是翻转后的图像的变量名。

  • 第三个参数是翻转类型;0表示垂直翻转,1表示水平翻转,-1表示图像应同时水平和垂直翻转。

flip()函数的输出如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • cvtColor(): 此函数用于将普通的 RGB 彩色图像转换为灰度图像:
cvtColor(img, grayimage, COLOR_BGR2GRAY)

此函数包括三个参数:

    • 第一个参数(img)是原始图像的变量名
  • 第二个参数(grayimage)是将转换为灰度的新图像的变量

  • 第三个参数,COLOR_BGR2GRAY,是转换类型;BGR 是 RGB 倒过来写的

cvtColor()函数的输出如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • threshold(): 阈值化方法用于分离代表对象的图像区域。简单来说,阈值化用于识别图像中的特定对象。阈值化方法接受源图像(src)、阈值和最大阈值(255)作为输入。它通过比较源图像的像素值与阈值来生成输出图像(thresimg):
threshold(src, thresimg, threshold value, max threshold value, threshold type);

阈值函数由五个参数组成:

    • 第一个参数(src)是要进行阈值化的图像的变量名。
  • 第二个参数(thresimg)是阈值化图像的变量名。

  • 第三个参数(阈值)是阈值(从 0 到 255)。

  • 第四个参数(最大阈值)是最大阈值(255)。

  • 第五个参数(阈值类型)是阈值化类型。

一般来说,有五种类型的阈值化,如下所示:

    • 0-二进制:二进制阈值化是阈值化的最简单形式。在这种阈值化中,如果源图像(src)上的任何像素值大于阈值,则在输出图像(thresimg)中,该像素将被设置为最大阈值(255),并且将变为白色。另一方面,如果源图像上的任何像素值小于阈值,则在输出图像中,该像素值将被设置为0,并且将变为黑色。

例如,在以下代码中,阈值设置为85,最大阈值为255,阈值类型为用数字0表示的二进制:

threshold(src, thresimg,85, 255, 0);

因此,如果苹果图像源图像上的任何像素值大于阈值(即大于85),那么这些像素将在输出图像中变为白色。同样,源图像上值小于阈值的像素将在输出图像中变为黑色。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

二进制阈值化

    • 1-二进制反转:二进制反转阈值化正好与二进制阈值化相反。在这种类型的阈值化中,如果源图像的像素值大于阈值,则输出图像的像素将变为黑色(0),如果源图像的像素值小于阈值,则输出图像的像素将变为白色(255):

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

二进制反转阈值化

    • 2-截断 阈值化:在截断阈值化中,如果src源图像上的任何像素值大于阈值,则在输出图像中,该像素将被设置为阈值。另一方面,如果src源图像上的任何像素值小于阈值,则在输出图像中,该像素将保留其原始颜色值:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

截断阈值化

    • 3-阈值为零:在这种阈值化中,如果src源图像上的任何像素值大于阈值,则在输出图像中,该像素将保留其原始颜色值。另一方面,如果src源图像上的任何像素值小于阈值,则在输出图像中,该像素将被设置为0(即黑色):

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

阈值为零

    • 4-阈值为零反转:在这种阈值化中,如果src上的任何像素值大于阈值,则在输出图像中,该像素将被设置为0。如果src上的任何像素值小于阈值,则在输出图像中,该像素将保留其原始颜色值:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

阈值为零反转

  • inRange(): inRange()函数是阈值函数的高级形式。在这个函数内部,我们必须输入我们想要识别的对象的最小和最大 RGB 颜色值。inRange()函数由四个参数组成:

  • 第一个参数(img)是要进行阈值处理的图像的变量名。

  • 有两个Scalar函数。在第一个Scalar函数中的第二个参数中,我们必须输入对象的最小 RGB 颜色。

  • 在第三个参数中,也就是第二个Scalar函数中,我们将输入对象的最大 RGB 颜色值。

  • 第四个参数(thresImage)代表阈值图像的输出:

inRange(img, Scalar(min B,min G,min R), Scalar(max B,max G,max R),thresImage)

图像矩——图像矩的概念源自,它在力学和统计学中用于描述一组点的空间分布。在图像处理或计算机视觉中,图像矩用于找到形状的质心,即形状中所有点的平均值。简单来说,图像矩用于在我们从整个图像中分割出对象后找到任何对象的中心。例如,在我们的情况下,我们可能想要找到苹果的中心。从图像计算对象的中心的图像矩公式如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    • x代表图像的宽度
  • y代表图像的高度

  • M10代表图像中所有x值的总和

  • M01代表图像中所有y值的总和

  • M00代表图像的整个区域

  • circle: 正如其名,这个函数用于画圆。它有五个参数作为输入:

  • 第一个参数(img)是你要在其上画圆的图像的变量名。

  • 第二个参数(point)是圆的中心(xy位置)点。

  • 第三个参数(radius)是圆的半径。

  • 第四个参数(Scalar(B,G,R))是为圆着色的;我们使用Scalar()函数来做到这一点。

  • 第五个参数(thickness)是圆的厚度:

circle(img, point, radius, Scalar(B,G,R),thickness);

使用 OpenCV 进行对象识别

现在我们已经了解了 OpenCV 的重要功能,让我们编写一个程序来从图像中检测一个有颜色的球。在我们开始之前,我们必须做的第一件事是拍摄球的合适照片。你可以用任何球来做这个项目,但要确保球是单色的(红色、绿色或蓝色的球是强烈推荐的),并且不是多色的。我在这个项目中使用了一个绿色的球:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

拍摄图像

为了捕捉你的球的图像,把它放在一些黑色的表面上。我把我的绿色球放在一个黑色的手机壳上:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如果你的球是黑色,或者颜色较暗,你可以把球放在一个颜色较浅的表面上。这是为了确保球的颜色和背景的颜色之间有很高的对比度,这将有助于我们后面的阈值处理。

在拍摄图像时,确保球上没有白色斑块,因为这可能会在后面的阈值处理中造成问题:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

左边的照片有一个大的白色区域,因为光线太亮。右边,球被适当照亮。

一旦你对拍摄的图像满意,将其传输到你的笔记本电脑上。

找到 RGB 像素值

现在我们将通过以下步骤检查球上不同点的 RGB 像素值来找到球的 RGB 像素值:

  1. 打开画图并打开保存的球的图像,如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 接下来,使用取色器工具,在球的任何位置单击取样颜色:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

颜色 1 框将显示被点击的颜色的样本。在我的情况下,这是绿色:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 如果您点击“编辑颜色”选项,您将看到该像素的 RGB 颜色值。在我的情况下,绿色像素的 RGB 颜色值为红色:61,绿色:177,蓝色:66。记下这些值,以备后用:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 现在,再次选择取色器选项,点击球的另一个彩色区域,找出该像素的 RGB 颜色值。再次记录这个值。重复 13 到 14 次,确保包括球上最浅和最暗的颜色:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我已经记录了球边缘六个点的 RGB 值,球周围随机位置的四个点的 RGB 值,以及颜色为浅绿色或深绿色的六个点的 RGB 值。找到 RGB 值后,突出显示最低的红色、绿色和蓝色值,以及最高的红色、绿色和蓝色值。我们将在程序中稍后使用这些值来对图像进行阈值处理。

  1. 现在,您需要将这个图像传输到您的 RPi。我通过Google Drive传输了我的图像。我通过将图像上传到 Google Drive,然后在我的 RPi 内打开默认的 Chromium 网络浏览器,登录我的 Gmail 账户,打开 Google Drive,并下载图像来完成这一步。

物体检测程序

用于检测绿色球的程序名为ObjectDetection.cpp,我将其保存在OpenCV_codes文件夹中。我还将greenball.png图像复制到了这个文件夹中。您可以从 GitHub 存储库的Chapter07文件夹中下载ObjectDetection.cpp程序。因此,用于检测绿色球的程序如下:

#include <iostream>
#include<opencv2/opencv.hpp>
#include<opencv2/core/core.hpp>
#include<opencv2/highgui/highgui.hpp>
#include<opencv2/imgproc/imgproc.hpp>

using namespace cv;
using namespace std;

int main()
{

 Mat img, resizeimg,thresimage;
 img = imread("greenball.png");
 imshow("Green Ball Image", img);
 waitKey(0);

 resize(img, resizeimg, cvSize(640, 480));
 imshow("Resized Image", resizeimg);
 waitKey(0);

 inRange(resizeimg, Scalar(39, 140, 34), Scalar(122, 245, 119), thresimage);
 imshow("Thresholded Image", thresimage);
 waitKey(0);

 Moments m = moments(thresimage,true);
 int x,y;
 x = m.m10/m.m00;
 y = m.m01/m.m00;
 Point p(x,y);
 circle(img, p, 5, Scalar(0,0,200), -1);
 imshow("Image with center",img);
 waitKey(0);

 return 0;
}

在前面的程序中,我们导入了四个 OpenCV 库,它们是opencv.hppcore.hpphighgui.hppimgproc.hpp。然后我们声明了 OpenCV 库的cv命名空间。

以下是前面程序的解释:

  1. main函数内,我们声明了三个矩阵变量,分别为imgresizeimgthresimage

  2. 接下来,imread()函数读取greenball.png文件,并将其存储在img变量中。

  3. imshow("Green Ball Image", img)行将在新窗口中显示图像,如下面的屏幕截图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 之后,waitKey(0)函数将等待键盘输入。然后执行下一组代码。一旦按下任意键,将执行调整图像大小的下两行代码。

  2. resize函数将调整图像的宽度和高度,使得图像的新宽度为640,高度为480

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 然后使用inRange函数执行阈值处理操作。在第一个Scalar函数内,我输入了我的球的绿色的最小 RGB 值,在第二个Scalar函数内,我输入了最大 RGB 值。阈化后的图像存储在thresimage变量中。

Scalar函数内,我们首先输入蓝色值,然后是绿色,最后是红色。

  1. 阈值处理后,球的颜色将变为白色,图像的其余部分将变为黑色。球中间的一些部分将呈现为黑色,这是正常的。如果白色内部出现大面积黑色,这意味着阈值处理没有正确进行。在这种情况下,您可以尝试修改Scalar函数内的 RGB 值:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 接下来,使用moments函数,我们找到对象的中心。

  2. moments(thresimage,true)行,我们将thresimage变量作为输入。

  3. 在接下来的三行代码中,我们找到白色区域的中心并将该值存储在点变量p中。

  4. 之后,为了显示球的中心,我们使用circle函数。在圆函数内部,我们使用img变量,因为我们将在原始图像上显示圆点。接下来,点变量p告诉函数我们在哪里显示点。圆形点的宽度设置为5,圆形点的颜色将是红色,因为我们只填充了Scalar函数的最后一个参数,表示颜色为红色。如果要设置其他颜色,可以更改Scalar函数内的颜色值:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 按任意键再次按下waitKey(0)函数,将关闭除终端窗口之外的所有窗口。要关闭终端窗口,请按Enter

通过上述程序,我们已经学会了如何调整大小、阈值处理,并在绿色球的图像上生成一个点(红点)。在下一节中,我们将对实时视频反馈执行一些图像识别操作。

OpenCV 相机反馈程序

现在,我们将编写一个简单的 C++程序来查看来自 Pi 相机的视频反馈。视频查看程序如下。该程序名为Camerafeed.cpp,您可以从 GitHub 存储库的Chaper07文件夹中下载:

int main()
{
 Mat videoframe;

VideoCapture vid(0);

if (!vid.isOpened())
 {
cout<<"Error opening camera"<<endl;
 return -1;
 }
 for(;;)
 {
 vid.read(videoframe);
 imshow("Frame", videoframe);
 if (waitKey(1) > 0) break;
 }
 return 0;
}

OpenCV 库和命名空间声明与先前程序类似:

  1. 首先,在main函数内部,我们声明了一个名为videoframe的矩阵变量。

  2. 接下来,使用VideoCapture数据类型从 Pi 相机捕获视频反馈。它有一个名为vid(0)的变量。vid(0)变量内的0数字表示相机的索引号。目前,由于我们只连接了一个相机到 RPi,Pi 相机的索引将为0。如果您将 USB 相机连接到树莓派,那么 USB 相机的索引将为1。通过更改索引号,您可以在 Pi 相机和 USB 相机之间切换。

  3. 接下来,我们指定如果相机无法捕获任何视频反馈,则应调用!vid.isOpened()条件。在这种情况下,终端将打印出"Error opening camera"消息。

  4. 之后,vid.read(videoframe)命令将读取相机反馈。

  5. 使用imshow("Video output", videoframe)行,我们现在可以查看相机反馈。

  6. waitKey命令将等待键盘输入。一旦按下任意键,它将退出代码。

这就是您可以使用 Pi 相机查看视频反馈的方法。

构建一个目标跟踪机器人

在对图像进行阈值处理并从 Pi 相机查看视频反馈之后,我们将结合这两个程序来创建我们的目标跟踪机器人程序。

在本节中,我们将编写两个程序。在第一个程序中,我们将球放在相机前面,并通过在球的中心创建一个点(使用矩形)来追踪它。接下来,我们将移动球,并记录相机上不同位置的点值。

在第二个程序中,我们将使用这些点值作为输入,并使机器人跟随球对象。

使用矩形进行球追踪

在跟踪球之前,机器人应首先能够使用 Pi 相机追踪它。在编写程序之前,让我们看看我们将如何追踪球。

编程逻辑

首先,我们将相机分辨率调整为 640 x 480,如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

调整宽度和高度后,我们将相机屏幕水平分为三个相等的部分:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

从 0 到 214 的x 坐标值代表左侧部分。从 214 到 428 的x 坐标值代表前进部分,而从 428 到 640 的x 坐标值代表右侧部分。我们不需要编写任何特定的程序来将摄像头屏幕划分为这三个不同的部分,我们只需要记住每个部分的最小和最大x 点值

接下来,我们将对球对象进行阈值处理。之后,我们将使用矩和在球的中心生成一个点。我们将在控制台中打印点值,并检查屏幕特定部分的xy点值:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如果球在前进部分,x 坐标值必须在214428之间。由于我们不是垂直地划分屏幕,所以不需要考虑y值。现在让我们开始球追踪程序。

球追踪程序

BallTracing.cpp程序如下。您可以从 GitHub 存储库的Chapter07文件夹中下载此程序:

int main()
{
  Mat videofeed,resizevideo,thresholdvideo;
  VideoCapture vid(0);
  if (!vid.isOpened())
  {
    return -1;
  } 
  for (;;)
  { 
    vid.read(videofeed);
  resize(videofeed, resizevideo, cvSize(640, 480));
  flip(resizevideo, resizevideo, 1);

  inRange(resizevideo, Scalar(39, 140, 34), Scalar(122, 245, 119), thresholdvideo); 

  Moments m = moments(thresholdvideo,true);
  int x,y;
  x = m.m10/m.m00;
  y = m.m01/m.m00; 
  Point p(x,y);

  circle(resizevideo, p, 10, Scalar(0,0,128), -1);

  imshow("Image with center",resizevideo);
    imshow("Thresolding Video",thresholdvideo);

  cout<<Mat(p)<< endl;

  if (waitKey(33) >= 0) break;
  }
  return 0;
}

main函数内,我们有三个矩阵变量,名为videofeedresizevideothresholdvideo。我们还声明了一个名为vid(0)VideoCapture变量来捕获视频。

以下步骤详细说明了BallTracing.cpp程序:

  1. for循环中,vid.read(videofeed)代码将读取摄像头视频。

  2. 使用resize函数,我们将摄像头分辨率调整为 640 x 480。调整大小后的视频存储在resizevideo变量中。

  3. 然后,使用flip函数,我们水平翻转调整大小后的图像。翻转后的视频输出再次存储在resizevideo变量中。如果我们不水平翻转视频,当你向左移动时,球会看起来好像在右侧移动,反之亦然。如果您将树莓派相机倒置安装,则需要垂直翻转调整大小后的图像。要垂直翻转,将第三个参数设置为0

  4. 接下来,使用inRange函数,我们对视频进行阈值处理,使彩色球从图像的其余部分中脱颖而出。阈值化后的视频输出存储在thresholdvideo变量中。

  5. 使用moments,我们找到了存储在点变量p中的球的中心。

  6. 使用circle函数,在resizevideo视频中显示一个红点在球上。

  7. 第一个imshow函数将显示调整大小后的(resizedvideo)视频,而第二个imshow函数将显示阈值化后的(thresholdvideo)视频:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在上面的屏幕截图中,左窗口显示了resizevideo的视频,我们看到绿色球上的红点。右窗口显示了阈值视频,其中只有球的区域是白色的。

  1. 最后,cout<<Mat(p)<<endl;代码将在控制台内显示红点的xy点值。当您移动球时,红点也会随之移动,并且红点的xy位置将显示在控制台内。

从上面的屏幕截图中,方括号内的值[298 ; 213]是点值。因此,我的情况下红点的x值在 298 到 306 的范围内,y值在 216 到 218 的范围内。

设置物体跟随机器人

跟踪球的位置后,剩下的就是让我们的机器人跟随球。我们将使用xy坐标值作为输入。然而,在跟随球的同时,我们还必须确保机器人与球的距离适当,以免与球或拿着球的人发生碰撞。为此,我们还将把超声波传感器连接到我们的机器人上。对于这个项目,我已经通过电压分压电路将超声波传感器的trigger引脚连接到wiringPi pin no 12,将echo引脚连接到wiringPi pin no 13

物体跟随机器人程序

物体跟随机器人程序基本上是第四章中的避障程序和前面的球追踪程序的组合。该程序名为ObjectFollowingRobot.cpp,您可以从 GitHub 存储库的Chapter07文件夹中下载:

int main()
 { 
...
 float distance = (totalTime * 0.034)/2;

 if(distance < 15)
 {
 cout<<"Object close to Robot"<< " " << Mat(p)<< " " <<distance << " cm" << endl;
 stop();
 }

 else{ 
      if(x<20 && y< 20)
      {
      cout<<"Object not found"<< " " << Mat(p)<< " " <<distance << " cm" << endl;
      stop();
      }
      if(x > 20 && x < 170 && y > 20 )
      {
      cout<<"LEFT TURN"<< " " << Mat(p)<< " " <<distance << " cm" << endl;
      left();
      }
      if(x > 170 && x < 470)
      {
      cout<<"FORWARD"<< " " << Mat(p)<< " " <<distance << " cm" << endl;
      forward();
      }
      if(x > 470 && x < 640)
      {
      cout<<"RIGHT TURN"<< " " << Mat(p)<< " " <<distance << " cm" << endl;
      right();
      }

      }
      if (waitKey(33) >= 0) break;
      }
       return 0;
}

main函数中,计算距离、对视频进行阈值处理并将点放在球的中心后,让我们来看看程序的其余部分:

  1. 第一个if条件(if(distance < 15))将检查机器人距离物体是否为 15 厘米。如果距离小于 15 厘米,机器人将停止。前进、左转、右转和停止功能在main函数上方声明。

  2. stop()函数下,cout语句将首先打印消息"Object close to Robot"。之后,它将打印点(x,y)值(Mat(p)),然后是distance值。在每个if条件内,cout语句将打印区域(如LEFTFORWARDRIGHT),点值和distance值。

  3. 如果距离大于 15 厘米,将执行else条件。在else条件内,有三个if条件来找到球的位置(使用上面的红点作为参考)。

  4. 现在,一旦摄像头被激活,或者当球移出摄像头的视野时,红点(点)将重置到屏幕的极左上角的位置x:0y:0else块内的第一个if条件(if(x<20 && y< 20))将检查红点的位置在xy轴上是否都小于 20。如果是,机器人将停止。

  5. 如果x位置在 20 和 170 之间,y位置大于 20,红点将在LEFT区域,机器人将向LEFT转动。

  6. 在这个程序中,我已经减小了LEFTRIGHT区域的宽度,并增加了FORWARD区域的宽度,如下图所示。您可以根据需要修改每个区域的宽度:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 如果x位置在 170 和 470 之间,红点在FORWARD区域,机器人将向FORWARD移动。

  2. 如果x位置在 470 和 640 之间,红点在RIGHT区域,机器人将向RIGHT转动。

使用移动电源为您的机器人供电,以便它可以自由移动。接下来,编译程序并在您的 RPi 机器人上构建它。只要球不在机器人面前,红点将保持在屏幕的极左上角,机器人将不会移动。如果您将球移动到摄像头前,并且距离机器人 15 厘米,机器人将开始跟随球。

随着机器人跟随球,球的颜色会因外部因素(如阳光或房间内的光线)而变化。如果房间里的光线较暗,球对机器人来说会显得稍暗。同样,如果房间里的光线太亮,球的某些部分也可能显得白色。这可能导致阈值处理无法正常工作,这可能意味着机器人无法顺利跟随球。在这种情况下,您需要调整 RGB 值。

总结

在本章中,我们研究了 OpenCV 库中的一些重要函数。之后,我们对这些函数进行了测试,并从图像中识别出了一个物体。接下来,我们学习了如何从树莓派摄像头读取视频,如何对彩色球进行阈值处理,以及如何在球的顶部放置一个红点。最后,我们使用了树莓派摄像头和超声波传感器来检测球并跟随它。

在下一章中,我们将通过使用 Haar 级联来扩展我们的 OpenCV 知识,检测人脸。之后,我们将识别微笑并让机器人跟随人脸。

问题

  1. 从图像中分离出一个物体的过程叫什么?

  2. 垂直翻转图像的命令是什么?

  3. 如果 x>428 且 y>320,红点会在哪个区块?

  4. 用于调整摄像头分辨率的命令是什么?

  5. 如果物体不在摄像头前方,红点会放在哪里?

第八章:使用 Haar 分类器进行面部检测和跟踪

在上一章中,我们编程机器人来检测一个球体并跟随它。在本章中,我们将通过检测和跟踪人脸、检测人眼和识别微笑,将我们的检测技能提升到下一个水平。

在本章中,您将学习以下主题:

  • 使用 Haar 级联进行面部检测

  • 检测眼睛和微笑

  • 面部跟踪机器人

技术要求

在本章中,您将需要以下内容:

  • 三个 LED 灯

  • 一个树莓派(RPi)机器人(连接到 RPi 的树莓派摄像头模块)

本章的代码文件可以从github.com/PacktPublishing/Hands-On-Robotics-Programming-with-Cpp/tree/master/Chapter08下载。

使用 Haar 级联进行面部检测

Paul Viola 和 Micheal Jones 在他们的论文《使用增强级联简单特征的快速目标检测》中于 2001 年提出了基于 Haar 特征的级联分类器。Haar 特征的级联分类器是使用面部图像以及非面部图像进行训练的。Haar 级联分类器不仅可以检测正面人脸,还可以检测人的眼睛、嘴巴和鼻子。Haar 特征的分类器也被称为 Viola-Jones 算法。

Viola-Jones 算法的基本工作

因此,简而言之,Viola-Jones 算法使用 Haar 特征来检测人脸。Haar 通常包括两个主要特征:边缘特征线特征。我们将首先了解这两个特征,然后我们将看到这些特征如何用于检测人脸:

  • 边缘特征:通常用于检测边缘。边缘特征由白色和黑色像素组成。边缘特征可以进一步分为水平边缘特征和垂直边缘特征。在下图中,我们可以看到左侧块上的垂直边缘特征和右侧块上的水平边缘特征:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 线特征:通常用于检测线条。在线特征中,一个白色像素被夹在两个黑色像素之间,或者一个黑色像素被夹在两个白色像素之间。在下图中,您可以看到左侧的两个水平线特征,一个在另一个下方,以及右侧的垂直线特征,相邻在一起:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

面部检测始终在灰度图像上执行,但这意味着在灰度图像中,我们可能没有完全黑色和白色的像素。因此,让我们将白色像素称为较亮的像素,黑色像素称为较暗的像素。如果我们看下面的灰度人脸图片,额头区域较亮(较亮的像素)与眉毛区域(较暗的像素)相比:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

与眼睛和脸颊区域相比,鼻线区域更亮。同样,如果我们看口部区域,上唇区域较暗,牙齿区域较亮,下唇区域再次较暗:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这就是通过使用 Haar 级联的边缘和线特征,我们可以检测人脸中最相关的特征点,如眼睛、鼻子和嘴巴。

OpenCV 4.0 包括不同的预训练 Haar 检测器,可以用于检测人脸,包括眼睛、鼻子、微笑等。在Opencv-4.0.0文件夹中,有一个Data文件夹,在Data文件夹中,您会找到haarcascades文件夹。在这个文件夹中,您会找到不同的 Haar 级联分类器。对于正面人脸检测,我们将使用haarcascade_frontalface_alt2.xml检测器。在下面的截图中,您可以看到haarcascades文件夹的路径,其中包含不同的 Haar 级联分类器:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在我们了解了 Viola-Jones 特征的基础知识,我们将编写程序,使我们的机器人使用 Haar 级联检测人脸。

人脸检测程序

让我们编写一个程序来检测人脸。我将这个程序命名为FaceDetection.cpp,您可以从本书的 GitHub 存储库的Chapter08文件夹中下载。

由于我们将使用haarcascade_frontalface_alt2.xml来检测人脸,请确保FaceDetection.cpphaarcascade_frontalface_alt2.xml文件在同一个文件夹中。

要编写人脸检测程序,请按照以下步骤进行:

  1. FaceDetection.cpp程序中,使用CascadeClassifier类加载 Haar 的预训练正面脸 XML,如下面的代码片段所示:
CascadeClassifier faceDetector("haarcascade_frontalface_alt2.xml");
  1. 声明两个矩阵变量,称为videofeedgrayfeed,以及一个名为vid(0)VideoCapture变量,以从 RPi 相机捕获视频:
Mat videofeed, grayfeed;
VideoCapture vid(0);
  1. for循环内,读取相机视频。然后,水平翻转相机视频。使用cvtColor函数,我们可以将我们的videofeed转换为grayscale。如果您的 Pi 相机放置颠倒,将flip函数内的第三个参数设置为0grayscale输出存储在grayfeed变量中。以下代码显示了如何完成此步骤:
vid.read(videofeed);
flip(videofeed, videofeed, 1);
cvtColor(videofeed, grayfeed, COLOR_BGR2GRAY);
  1. 让我们执行直方图均衡化,以改善videofeed的亮度和对比度。直方图均衡化是必需的,因为有时在光线较暗时,相机可能无法检测到人脸。为了执行直方图均衡化,我们将使用equalizeHist函数:
equalizeHist(grayfeed, grayfeed);
  1. 让我们检测一些人脸。为此,使用detectMultiScale函数,如下所示:
detectMultiScale(image, object, scalefactor, min neighbors,flags, min size, max size);

在前面的代码片段中显示的detectMultiScale函数由以下七个参数组成:

    • image:表示输入视频源。在我们的情况下,它是grayfeed,因为我们将从灰度视频中检测人脸。
  • object:表示矩形的向量,其中每个矩形包含检测到的人脸。

  • scalefactor:指定图像大小必须缩小多少。比例因子的理想值在 1.1 和 1.3 之间。

  • flags:此参数可以设置为CASCADE_SCALE_IMAGECASCADE_FIND_BIGGEST_OBJECTCASCADE_DO_ROUGH_SEARCHCASCADE_DO_CANNY_PRUNING

  • CASCADE_SCALE_IMAGE:这是最流行的标志;它通知分类器,用于检测人脸的 Haar 特征应用于视频或图像。

  • CASCADE_FIND_BIGGEST_OBJECT:此标志将告诉分类器在图像或视频中找到最大的脸

  • CASCADE_DO_ROUGH_SEARCH:此标志将在检测到人脸后停止分类器。

  • CASCADE_DO_CANNY_PRUNNING:此标志通知分类器不要检测锐利的边缘,从而增加检测到人脸的机会。

  • min neighbors:最小邻居参数影响检测到的人脸的质量。较高的最小邻居值将识别较少的人脸,但无论它检测到什么都一定是人脸。较低的min neighbors值可能会识别多个人脸,但有时也可能识别不是人脸的对象。检测人脸的理想min neighbors值在 3 和 5 之间。

  • min size:最小尺寸参数将检测最小的人脸尺寸。例如,如果我们将最小尺寸设置为 50 x 50 像素,分类器将只检测大于 50 x 50 像素的人脸,忽略小于 50 x 50 像素的人脸。理想情况下,我们可以将最小尺寸设置为 30 x 30 像素。

  • max size:最大尺寸参数将检测最大的人脸尺寸。例如,如果我们将最大尺寸设置为 80 x 80 像素,分类器将只检测小于 80 x 80 像素的人脸。因此,如果您离相机太近,您的脸的尺寸超过了最大尺寸,分类器将无法检测到您的脸。

  1. 由于detectMultiScale函数提供矩形的向量作为其输出,我们必须声明一个Rect类型的向量。变量名为facescalefactor设置为1.1min neighbors设置为5,最小比例大小设置为 30 x 30 像素。最大大小在这里被忽略,因为如果您的脸部尺寸变得大于最大尺寸,您的脸部将无法被检测到。要完成此步骤,请使用以下代码:
vector<Rect> face;
 faceDetector.detectMultiScale(grayfeed, faces, 1.3, 5, 0 | CASCADE_SCALE_IMAGE, Size(30, 30));

检测到脸部后,我们将在检测到的脸部周围创建一个矩形,并在矩形的左上方显示文本,指示“检测到脸部”:

for (size_t f = 0; f < face.size(); f++) 
 {
rectangle(videofeed, face[f], Scalar(255, 0, 0), 2);
putText(videofeed, "Face Detected", Point(face[f].x, face[f].y), FONT_HERSHEY_PLAIN, 1.0, Scalar(0, 255, 0), 2.0);
}

for循环内,我们使用face.size()函数来确定检测到了多少张脸。如果检测到一张脸,face.size()等于1for循环就会满足条件。在for循环内,我们有矩形和putText函数。

矩形函数将在检测到的脸部周围创建一个矩形。它由四个参数组成:

  • 第一个参数表示我们要在其上绘制矩形的图像或视频源,在我们的例子中是videofeed

  • face[f]的第二个参数表示我们要在其上绘制矩形的检测到的脸部

  • 第三个参数表示矩形的颜色(在此示例中,我们将颜色设置为蓝色)

  • 第四个和最后一个参数表示矩形的厚度

putText函数用于在图像或视频源中显示文本。它由七个参数组成:

  • 第一个参数表示我们要在其上绘制矩形的图像或视频源。

  • 第二个参数表示我们要显示的文本消息。

  • 第三个参数表示我们希望文本显示的位置。face[f].xface[f].y函数表示矩形的左上点,因此文本将显示在矩形的左上方。

  • 第四个参数表示字体类型,我们设置为FONT_HERSHEY_PLAIN

  • 第五个参数表示文本的字体大小,我们设置为1

  • 第六个参数表示文本的颜色,设置为绿色(Scalar(0,255,0))。

  • 第七个和最后一个参数表示字体的厚度,设置为1.0

最后,使用imshow函数,我们将查看视频源,以及矩形和文本:

imshow("Face Detection", videofeed);

使用上述代码后,如果您已经编译和构建了程序,您将看到在检测到的脸部周围画了一个矩形:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

接下来,我们将检测人眼并识别微笑。一旦眼睛和微笑被识别出来,我们将在它们周围创建圆圈。

检测眼睛和微笑

用于检测眼睛和微笑的程序名为SmilingFace.cpp,您可以从本书的 GitHub 存储库的Chapter08文件夹中下载。

检测眼睛

SmilingFace.cpp程序基本上是FaceDetection.cpp程序的扩展,这意味着我们将首先找到感兴趣的区域,即脸部。接下来,使用 Haar 级联分类器检测眼睛,然后在它们周围画圆圈。

在编写程序之前,让我们首先了解不同的可用的眼睛CascadeClassifier。OpenCV 4.0 有三个主要的眼睛级联分类器:

  • haarcascade_eye.xml:此分类器将同时检测两只眼睛

  • haarcascade_lefteye_2splits.xml:此分类器将仅检测左眼

  • haarcascade_righteye_2splits.xml:此分类器将仅检测右眼

根据您的要求,您可以使用haarcascade_eye分类器来检测两只眼睛,或者您可以使用haarcascade_lefteye_2splits分类器仅检测左眼和haarcascade_righteye_2splits分类器仅检测右眼。在SmilingFace.cpp程序中,我们将首先使用haarcascade_eye分类器测试输出,然后我们将使用haarcascade_lefteye_2splitshaarcascade_righteye_2splits分类器测试输出。

使用haarcascade_eye进行眼睛检测

要测试haarcascade_eye的输出,观察以下步骤:

  1. 在我们的程序中加载这个分类器:
CascadeClassifier eyeDetector("haarcascade_eye.xml");
  1. 要检测眼睛,我们需要在图像(视频源)中找到脸部区域(感兴趣区域)。在脸部检测的for循环中,我们将创建一个名为faceroiMat变量。videofeed(face[f]),这将在videofeed中找到脸部并将它们存储在faceroi变量中:
Mat faceroi = videofeed(face[f]);
  1. 创建一个名为eyesRect类型的向量,然后使用detectMultiScale函数来检测眼睛区域:
vector<Rect> eyes;
eyeDetector.detectMultiScale(faceroi, eyes, 1.3, 5, 0 |CASCADE_SCALE_IMAGE,Size(30, 30));

detectMultiScale函数中,第一个参数设置为faceroi,这意味着我们只想从脸部区域检测眼睛,而不是从整个视频源检测。检测到的眼睛将存储在 eyes 变量中。

  1. 为了在眼睛周围创建圆圈,我们将使用一个for循环。让我们找到眼睛的中心。为了找到眼睛的中心,我们将使用Point数据类型,并且eyecenter变量中的方程将给出眼睛的中心:
for (size_t e = 0; e < eyes.size(); e++)
 {
 Point eyecenter(face[f].x + eyes[e].x + eyes[e].width/2, face[f].y + eyes[e].y + eyes[e].height/2);
 int radius = cvRound((eyes[e].width + eyes[e].height)*0.20);
 circle(videofeed, eyecenter, radius, Scalar(0, 0, 255), 2);
 }

这的结果可以在这里看到:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

使用radius变量,我们计算了圆的半径,然后使用circle函数在眼睛周围创建红色的圆圈。

使用haarcascade_lefteye_2splitshaarcascade_righteye_2splits进行眼睛检测

使用haarcascade_eye分类器检测两只眼睛后,让我们尝试仅使用haarcascade_lefteye_2splitshaarcascade_righteye_2splits分类器分别检测左眼或右眼。

检测左眼

要检测左眼,执行以下步骤:

  1. 在我们的程序中加载haarcascade_lefteye_2splits级联分类器:
CascadeClassifier eyeDetectorleft("haarcascade_lefteye_2splits.xml");
  1. 由于我们想要在脸部区域检测左眼,我们将创建一个名为faceroiMat变量,并在其中存储脸部区域的值:
Mat faceroi = videofeed(face[f]);
  1. 使用detectMultiScale函数创建一个名为lefteyeRect类型的向量来检测左眼区域。min neighbors参数设置为25,以便分类器只检测左眼。如果我们将min neighbors设置为低于 25,haarcascade_lefteye_2splits分类器也可能检测到右眼,这不是我们想要的。要完成此步骤,请使用以下代码:
vector<Rect> lefteye;
eyeDetectorleft.detectMultiScale(faceROI, lefteye, 1.3, 25, 0 |CASCADE_SCALE_IMAGE,Size(30, 30));
 for (size_t le = 0; le < lefteye.size(); le++)
 {
 Point center(face[f].x + lefteye[le].x + lefteye[le].width*0.5, face[f].y + lefteye[le].y + lefteye[le].height*0.5);
 int radius = cvRound((lefteye[le].width + lefteye[le].height)*0.20);
 circle(videofeed, center, radius, Scalar(0, 0, 255), 2);
 }

上述代码的输出如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

检测左右眼分开的for循环代码是SmilingFace.cpp程序的一部分,但是被注释掉了。要测试代码,首先注释掉同时检测两只眼睛的for循环,然后取消注释检测左眼和右眼的另外两个for循环。

检测右眼

检测右眼的编程逻辑与检测左眼非常相似。我们唯一需要改变的是分类器名称和一些变量名称,以区分左眼和右眼。要检测右眼,执行以下步骤:

  1. 加载haarcascade_righteye_2splits级联分类器:
CascadeClassifier eyeDetectorright("haarcascade_righteye_2splits.xml");
  1. 在脸部检测的for循环中,找到脸部区域。然后,使用detectMultiScale函数来检测右眼。使用circle函数在右眼周围创建一个绿色的圆圈。为此,请使用以下代码:
Mat faceroi = videofeed(face[f]); 
vector<Rect>  righteye;
eyeDetectorright.detectMultiScale(faceROI, righteye, 1.3, 25, 0 |CASCADE_SCALE_IMAGE,Size(30, 30));

for (size_t re = 0; re < righteye.size(); re++)
 {
 Point center(face[f].x + righteye[re].x + righteye[re].width*0.5, face[f].y + righteye[re].y + righteye[re].height*0.5);
 int radius = cvRound((righteye[re].width + righteye[re].height)*0.20);
 circle(videofeed, center, radius, Scalar(0, 255, 0), 2);
 }

上述代码的输出如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如果我们结合左眼和右眼的检测器代码,最终输出将如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

正如我们所看到的,图片中的左眼被红色圆圈包围,右眼被绿色圆圈包围。

识别微笑

在从面部区域检测到眼睛后,让我们编写程序来识别笑脸。当网络摄像头检测到嘴巴周围的黑白黑线特征时,即上下嘴唇通常比牙齿区域略暗时,网络摄像头将识别出一个微笑的脸:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

微笑识别的编程逻辑

微笑识别的编程逻辑与眼睛检测类似,我们还将在面部检测的for循环内编写微笑识别程序。要编写微笑识别程序,请按照以下步骤进行:

  1. 加载微笑CascadeClassifier
CascadeClassifier smileDetector("haarcascade_smile.xml");
  1. 我们需要检测面部区域,它位于面部区域内。面部区域再次是我们的感兴趣区域,为了从视频源中找到面部区域,我们将使用以下命令:
Mat faceroi = videofeed(face[f]);
  1. 声明一个smile变量,它是Rect类型的向量。然后使用detectMultiScale函数。在detectMultiScale函数中,将min neighbors设置为25,以便只有在人微笑时才创建一个圆圈(如果我们将最小邻居设置为低于 25,即使人没有微笑,也可能在嘴周围创建一个圆圈)。您可以在 25-35 之间变化min neighbors的值。接下来,在for循环内,我们编写了在嘴周围创建绿色圆圈的程序。要完成此步骤,请使用以下代码:
vector<Rect> smile; 
smileDetector.detectMultiScale(faceroi, smile, 1.3, 25, 0 |CASCADE_SCALE_IMAGE,Size(30, 30));
 for (size_t sm = 0; sm <smile.size(); sm++)
 {
 Point scenter(face[f].x + smile[sm].x + smile[sm].width*0.5, face[f].y + smile[sm].y + smile[sm].height*0.5);
 int sradius = cvRound((smile[sm].width + smile[sm].height)*0.20);
 circle(videofeed, scenter, sradius, Scalar(0, 255, 0), 2);
 }

前面代码的输出如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在接下来的部分中,当检测到眼睛和微笑时,我们将打开不同的 LED。当面部移动时,我们还将使我们的机器人跟随检测到的面部。

面部跟踪机器人

用于打开/关闭 LED 和跟踪人脸的程序称为Facetrackingrobot.cpp,您可以从本书的 GitHub 存储库的Chapter08文件夹中下载。

Facetrackingrobot程序中,我们将首先检测面部,然后是左眼、右眼和微笑。一旦检测到眼睛和微笑,我们将打开/关闭 LED。之后,我们将在面部矩形的中心创建一个小点,然后使用这个点作为移动机器人的参考。

接线

对于Facetrackingrobot程序,我们至少需要三个 LED:一个用于左眼,一个用于右眼,一个用于微笑识别。这三个 LED 显示在以下图表中:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

LED 和机器人的接线如下:

  • 对应左眼的左 LED 连接到wiringPi pin 0

  • 对应右眼的右 LED 连接到wiringPi pin 2

  • 对应微笑的中间 LED 连接到wiringPi pin 3

  • 电机驱动器的IN1引脚连接到wiringPi pin 24

  • 电机驱动器的IN2引脚连接到wiringPi pin 27

  • 电机驱动器的IN3引脚连接到wiringPi pin 25

  • 电机驱动器的IN4引脚连接到wiringPi pin 28

在我的机器人上,我已经把左右 LED 贴在机器人的顶部底盘上。第三个 LED(中间 LED)贴在机器人的底盘上。我使用绿色 LED 作为眼睛,红色 LED 作为微笑:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

编程逻辑

Facetrackingrobot程序中,将 wiringPi 引脚 0、2 和 3 设置为输出引脚:

 pinMode(0,OUTPUT);
 pinMode(2,OUTPUT);
 pinMode(3,OUTPUT);

从面部检测程序中,您可能已经注意到面部跟踪过程非常缓慢。因此,当您将脸部向左或向右移动时,必须确保电机不要移动得太快。为了减慢电机的速度,我们将使用softPwm.h库,这也是我们在第二章中使用的使用 wiringPi 实现眨眼

  1. softPwm.h库中,使用softPwmCreate函数声明四个电机引脚(24272528):
softPwmCreate(24,0,100); //pin 24 is left Motor pin
softPwmCreate(27,0,100); //pin 27 is left motor pin 
softPwmCreate(25,0,100); //pin 25 is right motor pin
softPwmCreate(28,0,100); //pin 28 is right motor pin

softPwmCreate函数中的第一个参数表示 RPi 的 wiringPi 引脚。第二个参数表示我们可以移动电机的最小速度,第三个参数表示我们可以移动电机的最大速度。

  1. 加载面部、左眼、右眼和微笑CascadeClassifiers
CascadeClassifier faceDetector("haarcascade_frontalface_alt2.xml");
CascadeClassifier eyeDetectorright("haarcascade_righteye_2splits.xml");
CascadeClassifier eyeDetectorleft("haarcascade_lefteye_2splits.xml");
CascadeClassifier smileDetector("haarcascade_smile.xml");
  1. for循环内,声明三个布尔变量,称为lefteyedetectrighteyedetectisSmiling。将这三个变量都设置为false。使用这三个变量,我们将检测左眼、右眼和微笑是否被检测到。声明facexfacey变量,用于找到脸部矩形的中心。要完成此步骤,请使用以下代码:
bool lefteyedetect = false;
bool righteyedetect = false;
bool isSmiling = false;
int facex, facey;
  1. 使用detectMultiScale函数检测面部,然后在for循环内编写程序创建检测到的面部周围的矩形:
vector<Rect> face;
faceDetector.detectMultiScale(grayfeed, face, 1.1, 5, 0 | CASCADE_SCALE_IMAGE,Size(30, 30)); 
 for (size_t f = 0; f < face.size(); f++) 
 {
 rectangle(videofeed, face[f], Scalar(255, 0, 0), 2);

 putText(videofeed, "Face Detected", Point(face[f].x, face[f].y), FONT_HERSHEY_PLAIN, 1.0, Scalar(0, 255, 0), 1.0); 

facex = face[f].x +face[f].width/2;
facey = face[f].y + face[f].height/2; 

Point facecenter(facex, facey);
circle(videofeed,facecenter,5,Scalar(255,255,255),-1);

face[f].x + face[f].width/2将返回矩形的x中心值,face[f].y + face[f].height/2将返回矩形的y中心值。 x中心值存储在facex变量中,y中心值存储在facey变量中。

  1. 提供facexfacey作为Point变量的输入,以找到矩形的中心,称为facecenter。在圆函数中,使用facecenter点变量作为输入,在脸部矩形的中心创建一个点:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 当检测到左眼时,我们将在其周围创建一个红色圆圈,并将lefteyedetect变量设置为true
eyeDetectorleft.detectMultiScale(faceroi, lefteye, 1.3, 25, 0 |CASCADE_SCALE_IMAGE,Size(30, 30));
 for (size_t le = 0; le < lefteye.size(); le++)
 {
 Point center(face[f].x + lefteye[le].x + lefteye[le].width*0.5, face[f].y + lefteye[le].y + lefteye[le].height*0.5);
 int radius = cvRound((lefteye[le].width + lefteye[le].height)*0.25);
 circle(videofeed, center, radius, Scalar(0, 0, 255), 2);
 lefteyedetect = true;
 }
  1. 当检测到右眼时,我们将在其周围创建一个浅蓝色圆圈,并将righteyedetect变量设置为true
 eyeDetectorright.detectMultiScale(faceroi, righteye, 1.3, 25, 0 |CASCADE_SCALE_IMAGE,Size(30, 30));
 for (size_t re = 0; re < righteye.size(); re++)
 {
 Point center(face[f].x + righteye[re].x + righteye[re].width*0.5, face[f].y + righteye[re].y + righteye[re].height*0.5);
 int radius = cvRound((righteye[re].width + righteye[re].height)*0.25);
 circle(videofeed, center, radius, Scalar(255, 255, 0), 2);
 righteyedetect = true;
 }
  1. 当检测到微笑时,我们将在嘴周围创建一个绿色圆圈,并将isSmiling设置为true
 smileDetector.detectMultiScale(faceroi, smile, 1.3, 25, 0 |CASCADE_SCALE_IMAGE,Size(30, 30));
 for (size_t sm = 0; sm <smile.size(); sm++)
 {
 Point scenter(face[f].x + smile[sm].x + smile[sm].width*0.5, face[f].y + smile[sm].y + smile[sm].height*0.5);
 int sradius = cvRound((smile[sm].width + smile[sm].height)*0.25);
 circle(videofeed, scenter, sradius, Scalar(0, 255, 0), 2, 8, 0);
 isSmiling = true;
 }

在下面的屏幕截图中,您可以看到左眼周围画了一个红色圆圈,右眼周围画了一个浅蓝色圆圈,嘴周围画了一个绿色圆圈,并且在围绕脸部的蓝色矩形的中心有一个白点:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

使用三个if条件,我们将检查lefteyedetectrighteyedetectisSmiling变量何时为true,并在它们为true时打开它们各自的 LED:

  • 当检测到左眼时,lefteyedetect变量将为true。当检测到左眼时,我们将打开连接到 wiringPi 引脚 0 的机器人上的左 LED,如下面的代码所示:
if(lefteyedetect == true){
digitalWrite(0,HIGH);
}
else
{
digitalWrite(0,LOW);
}
  • 当检测到右眼时,righteyedetect变量将为true。当检测到右眼时,我们将打开连接到 wiringPi 引脚 2 的机器人上的右 LED:
if(righteyedetect == true){
digitalWrite(2,HIGH);
}
else
{
digitalWrite(2,LOW);
}
  • 最后,当识别到微笑时,isSmiling变量将为 true。当识别到微笑时,我们将打开连接到 wiringPi 引脚 3 的中间 LED:
if(isSmiling == true){
 digitalWrite(3,HIGH);
 }
 else
 {
 digitalWrite(3,LOW);
 }

接下来,我们将使用脸部矩形上的白点(点)将机器人向左和向右移动。

使用脸部三角形上的白点移动机器人

与第七章类似,使用 OpenCV 构建一个目标跟踪机器人,我们将摄像头屏幕分为三个部分:左侧部分、中间部分和右侧部分。当白点位于左侧或右侧部分时,我们将向左或向右转动机器人,从而跟踪脸部。即使我没有调整videofeed的大小,videofeed的分辨率设置为 640 x 480(宽度为 640,高度为 480)。

您可以根据需要变化范围,但如下图所示,左侧部分设置为 x 范围从 0 到 280,中间部分设置为 280-360 的范围,右侧部分设置为 360 到 640 的范围:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

当我们移动我们的脸时,脸部矩形将移动,当脸部矩形移动时,矩形中心的白点也会移动。当点移动时,facexfacey的值将发生变化。将摄像头屏幕分为三个部分时,我们将使用facex变量作为参考,然后我们将使用三个 if 条件来检查白点位于哪个部分。用于比较facex值的代码如下:

if(facex > 0 && facex < 280)
 {
 putText(videofeed, "Left", Point(320,10), FONT_HERSHEY_PLAIN, 1.0, CV_RGB(0, 0, 255), 2.0); 
 softPwmWrite(24, 0);
 softPwmWrite(27, 30);
 softPwmWrite(25, 30);
 softPwmWrite(28, 0); 
 } 

 if(facex > 360 && facex < 640)
 {
 putText(videofeed, "Right", Point(320,10), FONT_HERSHEY_PLAIN, 1.0, CV_RGB(0, 0, 255), 2.0); 
 softPwmWrite(24, 30);
 softPwmWrite(27, 0);
 softPwmWrite(25, 0);
 softPwmWrite(28, 30);

 }
 if(facex > 280 && facex < 360)
 {
 putText(videofeed, "Middle", Point(320,10), FONT_HERSHEY_PLAIN, 1.0, CV_RGB(0, 0, 255), 2.0); 
 softPwmWrite(24, 0);
 softPwmWrite(27, 0);
 softPwmWrite(25, 0);
 softPwmWrite(28, 0);
 }

如果满足第一个if条件,这意味着白点位于 0 到 280 之间。在这种情况下,我们在videofeed上打印Left文本,然后使用softPwmWrite函数,使机器人进行轴向左转。在softPwmWrite函数内,第一个参数代表引脚号,第二个参数代表我们的电机移动的速度。由于 wiringPi 引脚 24 设置为 0(低),wiringPi 引脚 27 设置为 30,左电机将以 30 的速度向后移动。同样,由于 wiringPi 引脚 25 设置为 30,wiringPi 引脚 28 设置为 0(低),右电机将以 30 的速度向前移动。

30 的速度值在 0 到 100 的范围内,我们在softPwmCreate函数中设置。您也可以改变速度值。

如果白点位于 360 到 640 之间,将打印Right文本,并且机器人将以 30 的速度进行轴向右转。

最后,当白点位于 280 到 360 之间时,将打印Middle文本,机器人将停止移动。

这就是我们如何让机器人跟踪脸部并跟随它。

摘要

在本章中,我们使用 Haar 面部分类器从视频源中检测面部,然后在其周围画一个矩形。接下来,我们从给定的面部检测眼睛和微笑,并在眼睛和嘴周围画圈。之后,利用我们对面部、眼睛和微笑检测的知识,当检测到眼睛和微笑时,我们打开和关闭机器人的 LED。最后,通过在脸部矩形中心创建一个白点,我们使机器人跟随我们的脸。

在下一章中,我们将学习如何使用我们的声音控制机器人。我们还将创建一个 Android 应用程序,用于识别我们所说的内容。当 Android 应用程序检测到特定关键词时,Android 智能手机的蓝牙将向树莓派蓝牙发送数据位。一旦我们的机器人识别出这些关键词,我们将使用它们来使机器人朝不同方向移动。

问题

  1. 我们用于检测面部的分类器的名称是什么?

  2. 当我们张开嘴时,会创建哪种类型的特征?

  3. 哪个级联可以用于仅检测左眼?

  4. 从面部检测眼睛时,该区域通常被称为什么?

  5. equalizeHist函数的用途是什么?

  • 8
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值