1.问题概述

人工智能带火了计算机视觉的人才需求,作为计算机视觉应用开发框架OpenCV也越来越受到欢迎,市场需求大增。因此,在学习Python的基础上,进行Opencv技术的学习是十分重要且有必要的。该项技术不仅有着成熟的学习框架,更有广泛的应用。学习一定的Opencv技术对于大学生来说,有着不同寻常的意义所在。

2.课程设计基本要求

设计报告的基本内容至少包括封面、正文、参考文献三部分。

1.封面

封面按照模版填写,不得随意更改。

2.正文

正文是设计报告的主体,具体由以下几部分组成:

(1)背景介绍、问题描述

描述要求编程解决的问题。

(2) 基本要求

给出程序要达到的具体的要求。

(3)需求分析
  以无歧义的陈述说明程序设计的任务,强调的是程序要做什么?并明确规定:
   输入的形式和输入值的范围;
   输出的形式;
   程序所能达到的功能;
   测试数据:包括正确的输入及其输出结果和含有错误的输入及其输出结果。
(4)概要设计
  说明本程序中主程序的流程以及各程序模块之间的层次(调用)关系。
(5)详细设计
  实现概要设计中定义的所有数据类型,给出关键部分源程序的清单,要求程序有充分的注释语句,至少要注释每个函数参数的含义和函数返回值的含义。
(6)调试分析
  内容包括:调试过程中遇到的问题是如何解决的以及对设计与实现的回顾讨论和分析;  
(7)用户使用说明
  说明如何使用你编写的程序,详细列出每一步的操作步骤。
(8)测试结果
设计测试数据,或具体给出测试数据。要求测试数据完整和严格,能全面地测试所设计程序的功能。
(9) 总结
(10)参考文献
  列出参考的相关资料和书籍。

3.Opencv识别设计需求分析

在理论研究中,物体识别在最近的十几年中一直是计算机视觉领域中的热门研究点。在每年的顶尖会议和期刊中,有很多这个方向的相关工作。在某些细化的应用场景,如人脸识别和指纹识别已经有了比较成熟的应用。本文通过研究物体识别的相关理论,基于OpenCV设计实现了一个多彩隔空画图,来试图解决一些颜色路径识别,完成的主要工作如下: 基于颜色特征的物体识别方案: 本文首先的研究内容是颜色识别相关的算法,其中主要包含两大部分:特征的提取和特征的匹配。深入地理解特征检测算法有助于在实际应用时针对特定的场景选取合适的算法并进行优化。

4.多彩隔空画图设计概要分析

本项目主要是为了实现,通过摄像头读取展示给摄像头的视觉信息,从中分析出需要识别的颜色,通过圆点标志出颜色,并连续不断的进行圆点绘制,以达到画图的最终效果。其中可能会用到窗口的调用恢复,窗口反转;腐蚀,消除噪声斑点;膨胀,连通被噪声、阴影分割的区域;开运算,先腐蚀后膨胀;闭运算,先膨胀后腐蚀;形态梯度; “礼貌” ; “黑帽” ;尺寸的调整;窗口合并;按钮添加进行参数调节;阀值化等等的技术。通过不同的模块的学习,将其整合为一个总体的项目。
本项目主要根据Opencv在Github的官方文档中不同章节涉及的Opencv知识进行学习以及完成该项目。

4.1多彩隔空画图开发环境

电脑型号:机械革命X6Ti版本
系统OS:Windows 10 专业版
版本号 20H2
安装日期 2020/9/10
操作系统版本 19042.610
体验 Windows Feature Experience Pack 120.2212.31.0
使用软件:PyCharm 2020.1 x64,VSCode,Python3.6

4.2Opencv技术

OpenCV是一个基于BSD许可(开源)发行的跨平台计算机视觉和机器学习软件库,可以运行在Linux、Windows、Android和Mac OS操作系统上。 [1] 它轻量级而且高效——由一系列 C 函数和少量 C++ 类构成,同时提供了Python、Ruby、MATLAB等语言的接口,实现了图像处理和计算机视觉方面的很多通用算法。
OpenCV用C++语言编写,它具有C ++,Python,Java和MATLAB接口,并支持Windows,Linux,Android和Mac OS,OpenCV主要倾向于实时视觉应用,并在可用时利用MMX和SSE指令, 如今也提供对于C#、Ch、Ruby,GO的支持。
OpenCV可以在Windows,Android,Maemo,FreeBSD,OpenBSD,iOS,Linux 和Mac OS等平台上运行。使用者可以在 SourceForge 获得官方版本,或者从 SVN 获得开发版本。OpenCV也是用CMake。
在Windows上编译OpenCV中与摄像输入有关部分时,需要DirectShow SDK中的一些基类。该SDK可以从预先编译的Microsoft Platform SDK(or DirectX SDK 8.0 to 9.0c / DirectX Media SDK prior to 6.0)的子目录Samples\Multimedia\DirectShow\BaseClasses获得。
Python是由Guido van Rossum发起的通用编程语言,很快就非常流行,主要是因为它的简单性和代码可读性。它使程序员可以用较少的代码行表达想法,而不会降低可读性。
与C/C++之类的语言相比,Python速度较慢。也就是说,可以使用C/C++轻松扩展Python,这使我们能够用C/C++编写计算密集型代码并创建可用作Python模块的Python包装器。这给我们带来了两个好处:首先,代码与原始C/C++代码一样快(因为它是在后台运行的实际C++代码),其次,在Python中比C/C++编写代码更容易。OpenCV-Python是原始OpenCV C++实现的Python包装器。
OpenCV-Python利用了Numpy,这是一个高度优化的库,用于使用MATLAB样式的语法进行数值运算。所有OpenCV数组结构都与Numpy数组相互转换。这也使与使用Numpy的其他库(例如SciPy和Matplotlib)的集成变得更加容易。

4.3多彩隔空画图的总体结构设计

图1 多彩隔空画图的总体结构设计

图1为多彩隔空画图总体结构设计,总体分为摄像头读取图像框、获取颜色、基本单位圆点、实现绘制路径四个模块。其中摄像头读取图像框模块主要是在屏幕中创建一个窗口,在这个窗口中实现画图路径的绘制;在获取颜色模块,主要借用cv2中腐蚀,消除噪声斑点;膨胀等函数,进行所需识别颜色的获取与识别;在基本单位模块中,主要是绘制出一个带有颜色的圆形,这个圆形就是绘制路径的基本单位,通过不断的重复这个圆形,从而画出路径;在实现绘制路径模块中,主要通过函数,不断地重复基本单元圆点,以实现路径的绘制。

5.多彩隔空画图的详细设计

在四个模块中,一开始为摄像头读取图形框设计,然后是进行简单的识别,之后通过添加调节按钮来实现移动按钮以获取最佳的识别效果,从而获取到需要识别颜色的RGB值。通过获得到的RGB值,可以再调用Opencv函数,来实现基本单位圆形的绘制,再将其连起来,从而实现路径绘制的实现。过程中需要多次实验调试,每个模块可能有多次修改,从初步的设计到代码的书写,算法的编写,再到算法代码的优化。

5.1摄像头读取图像框设计

我们需要找到我们的颜色,并且需要使用网络摄像头,然后我们可以在找到颜色的任何地方放置不同的点来创建绘画示例。
所以首先我们需要的是网络摄像头,所以我们要做的是,根据官方文档的第一章内容,调用我们的网络摄像头。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import cv2

frameWidth = 640 # chapter1
frameHeight = 480 # chapter1
cap = cv2.VideoCapture(1) # chapter1
cap.set(3, frameWidth) # chapter1
cap.set(4, frameHeight) # chapter1
cap.set(10, 150) # chapter1

while True:
success, img = cap.read()
cv2.imshow("Result", img)
if cv2.waitKey(1) & 0xFF == ord('q'):
break

图2 调用网络摄像头代码

首先我们需要导入库,通过import cv2导入Opencv库。我们设置两个变量,一个是窗口的宽为640,一个是窗口的高为480,通过frameWidth = 640和frameHeight = 480实现。然后为窗口的宽和高分别设置ID号,分别为3号和4号。同时我们设置窗口的亮度,在经过测试后,我们选定150的亮度为窗口屏幕亮度。VideoCapture函数为调用摄像头的函数,参数为1代表不调用,参数为0代表调用,开始我们设置为1,在需要调用时,我们再设置为0。

然后我们循环获取图像的位置,使用imshow功能函数,并且将窗口名称设置为Result。为了有退出的功能,我们设置按键盘上的“Q”进行退出。

此时我们进行测试,预期效果为现实摄像头。

运行程序,进行摄像头测试,结果摄像头读取成功,此处展示四个手指,在屏幕上也能看到四个手指,测试成功。

然后进行程序退出功能的测试,按键盘上的Q进行退出,测试结果为成功退出,在输入法为中文模式时,需要先改变成英文模式再摁Q进行退出即可。

5.2获取颜色设计

在窗口创建成功之后,就是颜色的获取。我们要做的就是找到我们的颜色,以便找到我们的颜色。我们需要引入颜色检测的代码,这一部分的内容在官方文档的第7章有涉及介绍,我们可以阅读相关内容进行函数学习,以及代码的编写。
我们需要检测上限阀值和下限阀值,同时我们还需要将窗口转化为HSV空间。
在代码中,我们需要定义一个函数来查找我们的颜色,具体代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def findColor(img, myColors, myColorValues):  # 查找颜色函数
imgHSV = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # chapter2
count = 0 # 计数器来计算实际计数多少次
newPoints = []
for color in myColors:
lower = np.array(color[0:3]) # chapter2 这里改为取前三个值
upper = np.array(color[3:6]) # chapter2 这里改为取3-6的值
mask = cv2.inRange(imgHSV, lower, upper) # chapter2
x, y = getContours(mask) # 调用获取轮廓的函数
# 绘制圆,中心点由x和y坐标确定,半径定为8,获取识别的颜色,然后进行圆的填充
cv2.circle(imgResult, (x, y), 8, myColorValues[count], cv2.FILLED)
# 在计数前,如果x和y不等于0,每次记录新的点进行追加
if x != 0 and y != 0:
newPoints.append([x, y, count])
count += 1
# cv2.imshow(str(color[0]),mask)
return newPoints

图3 查找颜色代码

在第一步,我们先进行检测测试,将函数的输入设置为一个ing值即可,之后需要将其转化为HSV空间。为了检测是否正常工作,我们加入cv2.imshow()来进行测试。

1
2
3
4
5
6
def findColor(img):  # 查找颜色函数
imgHSV = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
lower = np.array([h_min,s_min,v_min])
upper = np.array([h_max,s_max,v_max])
mask = cv2.inRange(imgHSV,lower,upper)
cv2.imshow("img",mask)

图4 查找颜色初步测试代码

在此处的cv2.imshow(“img”,mask)只是为了测试,在之后会将其进行删除。

我们不想只找到一种颜色,我们想找到不同的颜色,所以当我们调用findColor()函数时,我们想找到不同类型的颜色以及任何存在的颜色,并且将其作为输出。所以我们可以在函数上面,先以列表的形式定义一些内容,例如颜色的最小值和最大值。我们定义一个名为myColors的列表,这里基本上就是我们要检测的颜色列表。所以我们需要在赋值的时候,给出最小和最大色相饱和值。此处我们选择一些特定的颜色,例如红色,蓝色,绿色等颜色,实现给出颜色预订值。这可以帮助我们使用网络摄像头选择正确的色相饱和度值。

下方为检测预订设置颜色代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import cv2
import numpy as np

frameWidth = 640
frameHeight = 480
cap = cv2.VideoCapture(1)
cap.set(3, frameWidth)
cap.set(4, frameHeight)
cap.set(10,150)

def empty(a):
pass

cv2.namedWindow("HSV")
cv2.resizeWindow("HSV",640,240)
cv2.createTrackbar("HUE Min","HSV",0,179,empty)
cv2.createTrackbar("SAT Min","HSV",0,255,empty)
cv2.createTrackbar("VALUE Min","HSV",0,255,empty)
cv2.createTrackbar("HUE Max","HSV",179,179,empty)
cv2.createTrackbar("SAT Max","HSV",255,255,empty)
cv2.createTrackbar("VALUE Max","HSV",255,255,empty)

while True:

_, img = cap.read()
imgHsv = cv2.cvtColor(img,cv2.COLOR_BGR2HSV)

h_min = cv2.getTrackbarPos("HUE Min","HSV")
h_max = cv2.getTrackbarPos("HUE Max", "HSV")
s_min = cv2.getTrackbarPos("SAT Min", "HSV")
s_max = cv2.getTrackbarPos("SAT Max", "HSV")
v_min = cv2.getTrackbarPos("VALUE Min", "HSV")
v_max = cv2.getTrackbarPos("VALUE Max", "HSV")
print(h_min)

lower = np.array([h_min,s_min,v_min])
upper = np.array([h_max,s_max,v_max])
mask = cv2.inRange(imgHsv,lower,upper)
result = cv2.bitwise_and(img,img, mask = mask)

mask = cv2.cvtColor(mask, cv2.COLOR_GRAY2BGR)
hStack = np.hstack([img,mask,result])
#cv2.imshow('Original', img)
#cv2.imshow('HSV Color Space', imgHsv)
#cv2.imshow('Mask', mask)
#cv2.imshow('Result', result)
cv2.imshow('Horizontal Stacking', hStack)
if cv2.waitKey(1) & 0xFF == ord('q'):
break

cap.release()
cv2.destroyAllWindows()

图5 预订颜色检测代码

在此处的代码中,主要涉及官方文档当中第七章的内容进行代码编写。主要是将原本手动的调节功能集成到按钮当中,当我们调用按钮进行移动的时候,可以将画面进行HSV空间转化,较为方便地得出颜色的特征样本。在图3代码的基础上,添加按钮集成,分别设置按钮上限和下限值,以便调节具有一定的范围。

我们将cap = cv2.VideoCapture(1)设置为cap = cv2.VideoCapture(0),然后运行代码进行测试。

上方为检测窗口,下面左侧为颜色RGB值得输出,右侧为按钮调节。

首先我们进行红色的检测,然后进行蓝色和绿色的检测,红色和蓝色主要采用红笔和蓝色,绿色主要采用修正带上面的绿色部件。

红色检测结果,我需要将红色在画面中保持白色,尽量将其他部分保持为黑色。

我们可以看到,在画面中,我已经通过调节参数,将其保持为白色,最后输出为0,HUE Min值为0,SAT Min值为91,VALUE Min值为0,HUE Max为12,SAT Max和VALUE Max的值都为255。

但是手上的颜色被检测到了。所以我们需要调节HUE Max的值,我们将其调节为9,则手的颜色得到了有效的删减。

所以我们记录下了我们需要的值,0,91,0,9,255,255。所以在myColor列表中添加这些值。

蓝色检测结果,我需要将蓝色在画面中保持白色,尽量将其他部分保持为黑色。

我们可以看到,在画面中,我已经通过调节参数,将其保持为白色,最后输出为97,HUE Min值为97,SAT Min值为107,VALUE Min值为36,HUE Max为179,SAT Max值为255,VALUE Max的值为193。

但是附近的环境颜色被检测到了,我们还需要进行调整使其尽量优化。

经过调整,我们得到最后的值为104,100,33,174,255,226。将其添加到myColor列表中。

绿色检测结果,我需要将绿色在画面中保持白色,尽量将其他部分保持为黑色。

绿色的参数调节较为容易,不需要进行二次优化,我们的得到参数为77,150,0,91,255,255。将其添加到myColor列表中。

然后,我们的myColor列表中得到了三种颜色的最小和最大色相和饱和度值。

1
2
3
myColors = [[0, 91, 0, 9, 255, 255], 
[104, 100, 33, 174, 255, 226],
[77, 150, 0, 91, 255, 255]]

图6 预设颜色最小和最大色相和饱和度值

现在我们可以简单创建蒙版了,首先,我们需要导入Numpy库,通过import numpy as np将numpy库名称设置为np。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import cv2
import numpy as np

frameWidth = 640 # chapter1
frameHeight = 480 # chapter1
cap = cv2.VideoCapture(0) # chapter1
cap.set(3, frameWidth) # chapter1
cap.set(4, frameHeight) # chapter1
cap.set(10, 150) # chapter1

myColors = [[0, 91, 0, 9, 255, 255], # 要拥有最小和最大色相和饱和度值
[104, 100, 33, 174, 255, 226],
[77, 150, 0, 91, 255, 255]]


def findColor(img): # 查找颜色函数
imgHSV = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
lower = np.array([h_min, s_min, v_min])
upper = np.array([h_max, s_max, v_max])
mask = cv2.inRange(imgHSV, lower, upper)
cv2.imshow("img", mask)


while True:
success, img = cap.read()
cv2.imshow("Result", img)
if cv2.waitKey(1) & 0xFF == ord('q'):
break

图7 导入numpy库

然后我们需要将需要的值放入到lower,upper和mask三个值当中去。Lower代表最小色相和饱和度值,是myColor每个元素的前三个值,upper代表最大色相饱和值,是是myColor每个元素的后三个值。
因此我们只需要读取myColor的各个部分即可,例如我们首先进行红色的读取,那么lower设置就要如下设置,lower = np.array(myColors[0][0:3]),读取myColor第一个元素第一个值到第三个值,然后我们需要将myColor作为findColor函数的输入值以便函数内进行调用,设置def findColor(img,myColors)。
同样的,我们需要在upper中进行相同的设置,如upper = np.array(myColors[0][3:6])。
最后需要在while循环中调用findColor函数。

1
2
3
4
5
6
while True:
success, img = cap.read()
findColor(img, myColors)
cv2.imshow("Result", img)
if cv2.waitKey(1) & 0xFF == ord('q'):
Break

图8 调用findColor函数

然后运行程序,进行测试。

我们可以看到红色检测正常,不过这仅仅是红色的检测,我们还需要检测列表中所有的颜色,为此我们需要添加一个for循环,使变量color在myColor中进行遍历,然后用color[0:3]和color[3:6]代替原来的值,最后由于是多个不同的窗口,因此窗口名称需要不同,所以我们将窗口名称用str函数设置为myColor值中每个元素的第一个值,分别是0,104和77。

1
2
3
4
5
6
7
def findColor(img,myColors):  # 查找颜色函数
imgHSV = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
for color in myColors:
lower = np.array(color[0:3])
upper = np.array(color[3:6])
mask = cv2.inRange(imgHSV, lower, upper)
cv2.imshow(str(color[0]), mask)

图9 显示所有颜色

运行代码,进行测试。

可以看到,我们同时展示三种颜色,从左至右的窗口分别为红色,蓝色和绿色。

如果我们需要添加更多的颜色,我们可以直接将其色相饱和度值添加到myColor列表中,然后便可以直接进行识别。

现在部分测试已经结束,我们可以将cv2.imshow(str(color[0]),mask)采用注释的方式进行删除。

5.3基本单位圆点设计

我们现在需要在图像中找到需要检测的对象在哪里,为了实现这一点,我们需要得到轮廓,所以需要近似周围的边界框,这样我们就可以找到对象的位置。这里参考官方文档第八章对于边界框获取函数的知识点。

1
2
3
4
5
6
7
8
9
10
11
def getContours(img):  # chapter8
contours, hierarchy = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) # chapter8
x, y, w, h = 0, 0, 0, 0
for cnt in contours:
area = cv2.contourArea(cnt)
if area > 500:
# cv2.drawContours(imgResult, cnt, -1, (255, 0, 0), 3)
peri = cv2.arcLength(cnt, True)
approx = cv2.approxPolyDP(cnt, 0.02 * peri, True) # chapter8
x, y, w, h = cv2.boundingRect(approx) # chapter8
return x + w // 2, y # 返回尖端的最高点和中心点,如果大于500未检测到,仍需要返回一些值

图10 获取轮廓函数代码

我们需要在while循环中创建一个新图像,用在getContours()函数中进行调用。

1
2
3
4
5
6
7
while True:
success, img = cap.read()
imgResult = img.copy()
findColor(img, myColors)
cv2.imshow("Result", img)
if cv2.waitKey(1) & 0xFF == ord('q'):
Break

图11 创建复制新图像

现在我们可以测试是否进行正确的打印,在findColor函数中,我们需要查找轮廓,因此我们需要在findColor函数中调用getContours函数,并且将参数设置为mask。

1
2
3
4
5
6
7
8
def findColor(img, myColors):  # 查找颜色函数
imgHSV = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
for color in myColors:
lower = np.array(color[0:3])
upper = np.array(color[3:6])
mask = cv2.inRange(imgHSV, lower, upper)
getContours(mask)
# cv2.imshow(str(color[0]), mask)

图12 在findColor函数中调用getContours函数

运行程序进行测试

因此测试不会有任何的效果,我们需要找到错误所在

错误为,在最后的while循环输出中,我们并没有将复制的新图像imgResult进行输出,而是将img进行输出,而img又因为我们将cv2.imshow(str(color[0]), mask)进行删除而无法显示。因此,我们需要将img改为imgResult,然后再进行代码运行测试。

红色轮廓检测

蓝色轮廓检测

绿色轮廓检测

我们需要围绕着颜色的边界框,现在我们需要发送这些值,可以发送中心点,但是我们要从笔尖绘制而不是从检测对象的中心,所以我们将发送中心。因此,我们需要返回x+w//2以及y值,这样将使我们获得尖端的最高点,并且它的中心。为了一些意外情况的出现,例如area未大于500,我们还需要返回一些值,所以我们需要将x,y,w,h的初值赋为0。

1
2
3
4
5
6
7
8
9
10
11
def getContours(img):  # chapter8
contours, hierarchy = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) # chapter8
x, y, w, h = 0, 0, 0, 0
for cnt in contours:
area = cv2.contourArea(cnt)[]
if area > 500:
cv2.drawContours(imgResult, cnt, -1, (255, 0, 0), 3)
peri = cv2.arcLength(cnt, True)
approx = cv2.approxPolyDP(cnt, 0.02 * peri, True) # chapter8
x, y, w, h = cv2.boundingRect(approx) # chapter8
return x + w // 2, y

图13 返回需要绘制的点

我们需要在findColor函数中获取x,y函数,所以我们将getContours(mask)获得的一个点的值赋给x和y值。
x和y值相当于一个圆心,所以我们可以开始画一个圆,我们在结果imgResult图像上绘制它,中心点是x和y,然后我们设值半径为8,这样的圆不是太大,正好可以作为路径绘制的基本单位。然后先自定义一个颜色,用cv2.FILLED进行圆点内部的颜色填充。例如,我们首先将颜色设置为(255,0,0),这里采用的颜色模式我BGR,所以我们应该会在结果中看到一个蓝色的实心小圆点。

1
2
3
4
5
6
7
8
9
def findColor(img, myColors):  # 查找颜色函数
imgHSV = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
for color in myColors:
lower = np.array(color[0:3])
upper = np.array(color[3:6])
mask = cv2.inRange(imgHSV, lower, upper)
x, y = getContours(mask)
cv2.circle(imgResult, (x, y), 8, (255, 0, 0), cv2.FILLED)
# cv2.imshow(str(color[0]), mask)

图14 测试绘制圆点代码

红色

蓝色

绿色

测试结果成功绘制出三种颜色下的圆点。

这里需要注意区分,因为我们输入的点是图像顶端的中心,所以当我们图像角度倾斜不大的时候,我们的圆点会在物体上,当发生倾斜时,圆点会在物体外部。
无倾斜时:

有倾斜时:

要解决这个点,目前来说比较棘手,所以我们不打算讨论过多细节,现在我么保持笔直即可。现在需要改变的是,我需要显示的圆点颜色不应该是我们自定义的颜色,而应该是物体本身地颜色。
现在轮廓已经检测正确,我们不再需要轮廓,因此可以将其先删除。

1
2
3
4
5
6
7
8
9
10
11
def getContours(img):  # chapter8
contours, hierarchy = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) # chapter8
x, y, w, h = 0, 0, 0, 0
for cnt in contours:
area = cv2.contourArea(cnt)
if area > 500:
#cv2.drawContours(imgResult, cnt, -1, (255, 0, 0), 3)
peri = cv2.arcLength(cnt, True)
approx = cv2.approxPolyDP(cnt, 0.02 * peri, True) # chapter8
x, y, w, h = cv2.boundingRect(approx) # chapter8
return x + w // 2, y

图15 删除图像轮廓

我们需要更改颜色,所以我们需要定义颜色值,例如检测到红色,在绘图上应该是什么颜色,所以我们将其命名为myColorValues,还需要在myColorValues中创建一个列表,在这个列表中,我们需要定义所有想要的颜色。这里我们暂时有三个颜色,所以我们定义三个列表值。我们可以根据对应的色相饱和度值进行转换,转化的网站为https://www.rapidtables.com/web/color/RGB_Color.html。
红色

蓝色

绿色

然后我们用BGR的模式将颜色写入到myColorValues列表中。

1
2
3
myColorValues = [[0, 0, 255],  # 红色
[255, 0, 0], # 蓝色
[0, 255, 0]] # 绿色

图16 写入颜色BGR值

这些就是我们现在的颜色,我们可以做的是绘制基于这些颜色值得实心圆圈。我们需要myColorValues的值作为输入。所以需要将myColorValues作为findColor的输入参数,以及在while循环中将myColorValues作为findColor的输入参数。

在findColor函数中,我们需要一个计数器来实际计数多少次,所以我们定义一个count变量,并且将其初值赋为0。在findColor的for循环中,每次进行圆的绘制时,count就进行加一操作。在绘制圆中,我们需要将原来预设的颜色值改为为myColorValues的值,将原来的(255, 0, 0)改为myColorValues[count],这样每次都能得到需要检测颜色的BGR值。

运行程序,进行测试。
红色

蓝色

绿色

5.4实现绘制路径设计

我们现在得到正确的颜色和正确的值,我们需要做的是绘制这些点,以便绘制出运动的路线。我们将创建一个点列表,然后我们将每次都显示它,循环它,所以在顶部,首先创建一个列表,称为myPoints,第一个值应该为x,第二个应该为y,第三个应该为colorid。

1
myPoints = []  ## [x , y , colorId ]

图17 创建myPoints列表

我们需要创建一个函数,循环myPoints列表,检查x,y的值并绘制。
我们可以做的是创建一个名为drawOnCanvas的新函数,需要将myPoints和myColorValues作为传入的参数。
在for循环中,用point变量遍历myPoints列表,然后将findColor中写过的绘制圆圈语句放入这个for循环中,将原来的x用point[0]代替,将原来的y用point[1]代替,将count用point[2]代替即可。
我们需要在while循环中,将findColor的值进行新的赋值,赋值为newPoints。

1
2
3
4
5
6
7
while True:
success, img = cap.read()
imgResult = img.copy()
newPoints = findColor(img, myColors, myColorValues)
cv2.imshow("Result", imgResult)
if cv2.waitKey(1) & 0xFF == ord('q'):
Break

图18 findColor函数进行赋值

因此当我们检测到颜色时,我们需要在findColor函数中返回该点的值,如果没有检测到x,y的值,那么x,y不需要返回,只需要添加即可。
在count进行计数之前,我们可以加入一个判断语句,如果x和y不等于0,那么我们只需要使用append进行添加即可,这里我们需要一个新的列表变量newPoints,其中每个元素都有x,y和count三个值。每次当x和y都不等于0的时候,就进行添加每个识别出来的点,通过newPoints.append([x, y, count])实现,每个点连起来,就形成了一条线,但是这条线不是连续的,还会有一些空点,在下面还需要进行优化。同样的,我们这个缺少一个newPoints列表变量的初始化,所以在初始化count变量的时候,我同样对newPoints进行初始化,每次循环都会定义新点,因此每次点都会改变,所以初始化的时候,将newPoints列表定义为空的列表。
最后我们将newPoints列表值进行返回即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def findColor(img, myColors, myColorValues):  # 查找颜色函数
imgHSV = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
count = 0
newPoints = []
for color in myColors:
lower = np.array(color[0:3])
upper = np.array(color[3:6])
mask = cv2.inRange(imgHSV, lower, upper)
x, y = getContours(mask)
cv2.circle(imgResult, (x, y), 8, myColorValues[count], cv2.FILLED)
if x != 0 and y != 0:
newPoints.append([x, y, count])
count += 1
# cv2.imshow(str(color[0]), mask)
return newPoints

图19 findColor函数细节优化代码

因此每次加上这些点,一旦我们将这些新点发送到while循环的newPoints变量中时。我们可以检查newPoints是否真的存在。通过判断newPoints的长度是否等于0。
如果不等于0,我们就可以采用一个for循环,将其加入到newPoints连到一起。

1
2
3
4
5
6
7
8
9
10
11
12
while True:
success, img = cap.read()
imgResult = img.copy()
newPoints = findColor(img, myColors, myColorValues)

if len(newPoints)!=0:
for newP in newPoints:
myPoints.append(newP)

cv2.imshow("Result", imgResult)
if cv2.waitKey(1) & 0xFF == ord('q'):
Break

图20 将点连续的代码

在这里放上for循环的原因是,我们再findColor函数中返回的是一个列表变量,所以无法在列表中放置一个列表。获取到了列表,但是我需要的是点,而不是列表。我们需要将列表分解为点,所以需要在后面进行添加。
如果newPoints长度不为0,那么我们需要绘制它们,以便在画布上绘制。这里我们调用drawOnCanvas函数,进行调用,传入的两个参数分别为myPoints和myColorValues。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
while True:
success, img = cap.read()
imgResult = img.copy()
newPoints = findColor(img, myColors, myColorValues)

if len(newPoints) != 0:
for newP in newPoints:
myPoints.append(newP)

if len(myPoints) != 0:
drawOnCanvas(myPoints, myColorValues)

cv2.imshow("Result", imgResult)
if cv2.waitKey(1) & 0xFF == ord('q'):
Break

图21 将点连成线

6.系统的调试分析

在本报告中,摄像头读取图像框、获取颜色、基本单位圆点这两个模块的代码测试和分析在详细设计中一起进行了分类模块调试测试功能。目前实现绘制路径模块的编写完成,代表整个项目的编写成功,因此让我们来调试整个项目。
运行代码进行调试。当三种颜色出现在摄像头前时,应该会出现对应颜色的点,当物体进行移动时,对应的点会连成线,呈现出一条线,从而实现隔空画图。
单独测试
红色

蓝色

绿色

放在一起进行测试

6.1运行过程

我们使用Pycharm进行代码编写和调试,点击运行按钮,然后进行图像绘制。

6.2系统调试过程中遇到的问题

(1)在进行轮廓绘制时,我们并没有将复制的新图像imgResult进行输出,而是将img进行输出,而img又因为我们将cv2.imshow(str(color[0]), mask)进行删除而无法显示。
需要在while循环中将img改为imgResult,然后再进行代码运行测试。结果显示如预期所示。
(2)在圆点绘制时,这里需要注意区分,因为我们输入的点是图像顶端的中心,所以当我们图像角度倾斜不大的时候,我们的圆点会在物体上,当发生倾斜时,圆点会在物体外部。
要解决这个点,目前来说比较棘手,所以我们不打算讨论过多细节,现在我么保持笔直即可。现在需要改变的是,我需要显示的圆点颜色不应该是我们自定义的颜色,而应该是物体本身地颜色。
(3)在上述的测试,以及最后的图像输出中,我们得到的图像全都是正向,与我们的动作运行方向相反,这样不利于用户进行隔空绘图。因此我们需要将最后生产的整个窗口进行镜像翻转。具体代码如下。

1
2
if success == True:  # 用来实现输出的水平翻转
new_img = cv2.flip(imgResult, 180)

在摄像头确认读取成功之后,将imgResult进行水平180度的翻转,然后将图像赋给一个新的值,名为new_img,最后imshow显示结果也用new_img进行输出。
在解决这个问题之后,我们同时又解决了另一个问题,那就是线的绘制不是非常连续,只是单纯点的集合。这和图像没有镜像有一定关系。在新图像进行镜像之后,线也变得连续起来。
以下为测试效果
红色

蓝色

绿色

7.用户使用说明

首先,我们需要在Pycharm中找到运行按钮,位置在右上方,为绿色三角形。

点击绿色三角形,进行程序运行。等待一些时间,在下方会出现摄像头选框,点击选框在选框中进行绘制。

根据镜像,可以直接进行绘画,用不同的颜色,分别绘制出RGB三个字形状。

最后摁Q进行退出,如果是中文输入模式,请切换至英文模式进行退出。

8.测试结果

在测试中,我们发现,由于手的颜色比较接近橙色,所以在识别的时候,手部的颜色可能会被识别成红色,导致在不应该有的位置出现红色。这里的问题,需要在颜色识别模块,进行相应的优化。或者可以添加新的函数优化识别的精度。
此外,在运行过程中,一开始有颜色的物体进入窗口,会产生点点点,而不能立刻进行识别并且连成一条线,我们认为这一点也需要后续的优化。在本次报告中,进行简单的隔空画图实现即可。

9.总结

Python是一种面向对象、直译式计算机程序设计语言。我们觉得Python突出的地方是对字符串操作特别的灵活、采取缩进的方式简单明了,以及简单的语法。Python和C类似,是顺序进行的,不像visual c+是事件触发不同模块进行的。操作和mat lab相似,有编辑窗口,也有一个运行的窗口(交互式解释器),可以编写之后运行,也可以在命令行模式下一条条的完成。实验结束后,我学习Python基础,初步了解了Python的基本语法及知识点,也能编译出不错的运行模块。Python的实验让我对未来工作提升了信心,提高我们从事实际工作的能力。
对于Opencv的学习,我们认为,最好的方法永远是阅读官方文档,再进行许多开源代码的实验测试,从中学会某种函数或者某种算法的使用和原理,这对于初学者来说十分重要。我们通过官方文档的反复阅读,了解了需要采用的函数,以及该函数应该如何进行调用,再结合一些实例,进行代码的编写。过程中虽然遇到一些问题,一时半会无法解决,但是也通过积极找寻解决办法来将其解决,总体来说,本次课设带来的意义非凡。

10.参考文献

[1]http://python.jobbole.com/ Python基础教程网站.

[2]https://www.w3cschool.cn/opencv/ opencv官方文档.

[3]https://github.com/yuanxiaosc/DeepNude-an-Image-to-Image-technology 图像处理实例算法开源项目.

[4]https://github.com/Asabeneh/30-Days-Of-Python Python开源学习项目.

[5]刘瑞祯,于仕琪.OpenCV教程—基础篇[M]北京:北京航空航天大学出版社.2007.

[6]G. Bradski,The OpenCV Library[M].ResearchGate:CiNii,2000.

[7]张基温.C++程序设计基础[M].北京:高等教育出版社.2003.

[8]侯俊杰,深入浅出MFC[M].武汉:华中科技大学出版社.2001.

[9]冈萨雷斯·数字图像处理(第二版)[M].北京:电子工业出版社.2005.

[10]于仕琪译,学习OpenCV(中文版)[M].北京:清华大学出版社.2009.