Opencv实战 | 用摄像头自动化跟踪特定颜色物体
点击上方“小白学视觉”,选择加"星标"或“置顶”
重磅干货,第一时间送达
本文转自:新机器视觉
在之前的某个教程里,我们探讨了如何控制Pan/Tilt Servo设备来安置一个PiCam(树莓派的相机)。这次,我们将使用你的设备来帮助相机自动地跟踪某种颜色的物体,像下边的动图里那样:
尽管这是我第一次使用OpenCV,但我必须承认,我已经爱上了这个“开源计算机视觉库”。
OpenCV对学术用途和商业用途都免费。它有C++、C、Python和Java的接口,并且支持Windows、Linux、MacOS、iOS和Android系统。在我的OpenCV教程系列中,我们将专注于使用树莓派(当然,操作系统就是Raspbian了)和Python。OpenCV为高效计算而生,极大地专注于实时应用。因此,它对于物理计算(即使用可以感知和响应模拟世界的软件和硬件来构建交互式物理系统)项目来说,简直再适合不过了!
我使用的是安装着目前最新的Raspbian版本(Stretch)的树莓派V3。安装OpenCV最好的办法就是按照Adrian Rosebrock的这篇极棒的教程:Raspbian Stretch: Install OpenCV 3 + Python on your Raspberry Pi。
我在我的树莓派上试了好几种不同的OpenCV安装教程,其中Adrian的是最棒的一篇。我建议各位读者一步一步按照这篇教程的步骤做。
当你完成了Adrian的教程后,你的树莓派应该已经安装好了OpenCV的虚拟环境,并且可以进行我们的实验了。
让我们再次检查一下虚拟环境并确认OpenCV 3已经正确安装了。
Adrian建议每次打开新的终端都执行一次“source”命令,从而确保你的系统变量已经正确设置:
source ~/.profile
接下来,进入我们的虚拟环境:
workon cv
如果你看到你的命令提示符之前多了个(cv),那说明你已经进入虚拟环境“cv”了。
(cv) pi@raspberry:~$
Adrian强调,Python虚拟环境“cv”是和Raspbian Stretch系统自带的Python版本完全独立的。也就是说,系统Python的site-packages目录中的那些库在虚拟环境“cv”中并不能使用——同样,这个虚拟环境中的包在系统全局的Python版本中也是无法使用的。
现在,Python翻译器,启动!
python
同时,请确认你是用的是Python 3.5版本或者更高版本。
在翻译器中(应该会有“>>>”提示符),导入OpenCV库:
import cv2
如果没有出现任何错误信息,说明OpenCV在你的虚拟环境中已经正确安装~
既然你的树莓派已经安装好OpenCV了,那就先测试一下你的相机是否正常工作吧~(假设你已经在你的树莓派上安装PiCam了)
在你的IDE中输入以下代码:
import numpy as np
import cv2
cap = cv2.VideoCapture(0)
while(True):
ret, frame = cap.read()
frame = cv2.flip(frame, -1) # Flip camera vertically
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
cv2.imshow('frame', frame)
cv2.imshow('gray', gray)
if cv2.waitKey(1) & 0xFF == ord('q'):
breakcap.release()
cv2.destroyAllWindows()
上述代码将捕获你的PiCam的视频流并使用BGR三色模式和灰度模式显示。
请注意,我的相机在组装过程中是上下颠倒的,所以我把得到的图片垂直翻转了。如果你并没有我的情况,请删掉frame = cv2.flip(frame, -1)那一行。
或者,你可以直接从我的GitHub下载该代码:simpleCamTest.py
要执行我的代码,运行:
python simpleCamTest.py
要结束程序,请按键盘上的[q]键或[Ctrl]+[C]键。
下图是我的结果:
要学习OpenCV的更多知识,可以参考以下教程:loading -video-python-opencv-tutorial
我们想做的一件事情就是检测并跟踪某种颜色的物体。为此,我们必须理解一点OpenCV是如何翻译颜色的。
关于颜色检测,Henri Dang写了一篇很棒的教程:Color Detection in Python with OpenCV。
通常,我们的相机是使用RGB颜色模式工作的。RGB颜色模式可以这样认为:我们看到的所有可能的颜色都可以被三种颜色的光(红,绿,蓝)组成。然而,这里我们使用的OpenCV默认是BGR颜色模式,也就是将RGB的顺序进行了调整。
正如以上所述,使用BGR颜色模式,每一个像素可以由三个参数——蓝、绿、红组成。每个参数通常是一个0~255之间的值(或者十六进制下0x00到0xFF)。比如,电脑屏幕上的纯蓝色的BGR值分别为:蓝255,绿0,红0。
OpenCV还使用一种RGB模型的替代——HSV(Hue色相,Saturation色度,Value色值)颜色模型,它是70年代的计算机图形学研究者为了更好地与人类视觉对颜色属性的感知方式相匹配而提出的。
好。如果你想要使用OpenCV跟踪某一种确定的颜色,你必须使用HSV模型定义它。
比如说,我想要跟踪下图中的黄色塑料盒。首先要做的就是找出它的BGR值。你可以用很多办法采样(这里我用的是PowerPoint)。
我这里找到的是:
蓝色:71
绿色:234
红色:213
下面,我们需要将BGR模型(71, 234, 213)转换为HSV模型,这将被定义为上下界取值范围的形式。让我们执行以下代码:
import sys
import numpy as np
import cv2
blue = sys.argv[1]
green = sys.argv[2]
red = sys.argv[3]
color = np.uint8([[[blue, green, red]]])
hsv_color = cv2.cvtColor(color, cv2.COLOR_BGR2HSV)
hue = hsv_color[0][0][0]
print("Lower bound is :"),
print("[" + str(hue-10) + ", 100, 100]\n")
print("Upper bound is :"),
print("[" + str(hue + 10) + ", 255, 255]")
你也可以到GitHub下载我的这段代码:bgr_hsv_converter.py
要执行我的脚本,运行以下命令并把BGR值作为参数:
python bgr_hsv_converter.py 71 234 213
这个程序将会计算我们目标物体HSV值的上下界。给定以上参数会得到:
lower bound: [24, 100, 100]
以及
upper bound: [44, 255, 255]
以上结果将显示在终端中。
最后让我们看看OpenCV如何根据给出的颜色来选择出我们的物体。
import cv2
import numpy as np
# Read the picure - The 1 means we want the image in BGR
img = cv2.imread('yellow_object.JPG', 1)
# resize imag to 20% in each axis
img = cv2.resize(img, (0,0), fx=0.2, fy=0.2)
# convert BGR image to a HSV image
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
# NumPy to create arrays to hold lower and upper range
# The “dtype = np.uint8” means that data type is an 8 bit integer
lower_range = np.array([24, 100, 100], dtype=np.uint8)
upper_range = np.array([44, 255, 255], dtype=np.uint8)
# create a mask for image
mask = cv2.inRange(hsv, lower_range, upper_range)
# display both the mask and the image side-by-side
cv2.imshow('mask',mask)
cv2.imshow('image', img)
# wait to user to press [ ESC ]
while(1):
k = cv2.waitKey(0)
if(k == 27):
breakcv2.destroyAllWindows()
你也可以到GitHub下载我的这段代码: colorDetection.py
要执行我的脚本,运行以下命令并把图片名作为参数(我这里的图片为yellow_object.JPG):
python colorDetection.py
这个脚本将显示原图(“image”窗口)和OpenCV使用颜色范围过滤后的掩膜(“mask”窗口)。
既然我们已经知道了如何用掩膜来选择出我们的物体,那就让我们用相机来实时跟踪他的移动吧。为此,我基于Adrian Rosebrock的OpenCV小球目标跟踪教程写了我的代码。
我强烈建议你详细阅读Adrian的教程。
首先,请确认你已经安装了imutils库。它是Adrian基于OpenCV自制的图像处理基本任务(如修改尺寸、翻转等)的易用函数集合。如果你还没有安装,请在你的Python虚拟环境中运行下面的命令安装:
pip install imutils
下面,从我的GitHub下载ball_tracking.py代码并用下面的命令执行:
python ball_traking.py
你将会看到类似于下面的gif的结果:
总体而言,我与Adrian的代码除了“视频垂直翻转”之外没有什么不同:
frame = imutils.rotate(frame, angle=180)
请注意,这里使用的颜色掩膜的边界值是我们在上一步得到的。
现在我们已经搞定OpenCV的基础了,是时候给树莓派装个LED来试一下通用IO了。
请按照上图的电路做:LED的负极接到GPIO 21口,正极接一个220Ω的电阻再连接GND。
现在使用我们的Python虚拟环境测试一下这个LED吧!
请注意,有可能你的Python虚拟环境还没有安装树莓派的RPi.GPIO。如果还没有的话,运行下面的命令即可使用pip安装(请先确定自己在虚拟环境“cv”中):
pip install RPi.GPIO
现在用一个Python脚本来做个简单的测试:
import sys
import time
import RPi.GPIO as GPIO
# initialize GPIO and variables
redLed = int(sys.argv[1])
freq = int(sys.argv[2])
GPIO.setmode(GPIO.BCM)
GPIO.setup(redLed, GPIO.OUT)
GPIO.setwarnings(False)
print("\n [INFO] Blinking LED (5 times) connected at GPIO {0} \
at every {1} second(s)".format(redLed, freq))
for i in range(5):
GPIO.output(redLed, GPIO.LOW)
time.sleep(freq)
GPIO.output(redLed, GPIO.HIGH)
time.sleep(freq)
# do a bit of cleanup
print("\n [INFO] Exiting Program and cleanup stuff \n")
GPIO.cleanup()
上边的代码需要一个GPIO端口号和一个LED闪烁频率作为参数。LED闪烁5次后程序结束。结束之前记得释放GPIO。
也就是说,运行脚本时要给出两个参数:“LED GPIO”和frequency。举个例子:
python LED_simple_test.py 21 1
上边的指令意味着使用“GPIO 21”上连接的LED灯,并且每1秒闪烁一次,总共闪烁五次。
同样,上边这段代码也可以在GitHub下载:GPIO_LED_test.py
上边的图片显示了我的程序结果。至于LED灯亮不亮,就要各位自己去检验啦。
好,下面让我们把OpenCV和基本GPIO操作一起耍起来~
让我们开始集成 OpenCV 代码和 GPIO 进行交互。我们会从 最后的OpenCV 代码开始,并且我们将会把 GPIO_RPI 库集成到代码中,其目的是在摄像头检测到我们的着色物体时,能使红色LED常亮。 这一步骤使用的代码是基于 Adrian 写得非常不错的教程OpenCV, RPi.GPIO, and GPIO Zero on the Raspberry Pi
第一件需要做的事情是:”创建“我们的LED对象,目的是为了连接上指定的GPIO。
import RPi.GPIO as GPIO
redLed = 21
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
GPIO.setup(redLed, GPIO.OUT)
第二,我们必须初始化LED(关灯状态):
GPIO.output(redLed, GPIO.LOW)
ledOn = False
现在,在代码循环体中,当物体被检测到,”圆“被创建时,我们会把LED灯打开
GPIO.output(redLed, GPIO.HIGH)
ledOn = True
你可以在我的GitHub库中下载到完整的代码:object_detection_LED.py
运行代码使用到的命令行:
python object_detection_LED.py
下面的图片就是实现的效果。提示:当物体被检测到时,在图片左下方的LED灯就会亮着。
试试不同颜色,不同形式的物体,你会发现一旦颜色和掩码范围内匹配的话,LED灯就会亮起来。
下面的视频显示了一些经验。要注意的是,只有在色值一定范围内的黄色物体才会被检测到,LED等会亮起来。而其他不同颜色的物体则会被略过。
正如最后一步解释的那样,我们只是用到了LED灯。但是在视频中,摄像头却集成了云台(Pan Tilt:指摄像头可全方位左右/上下移动),所以不妨先忽略它。我们会下一步骤中实现云台机制。
现在我们已经用上了基本的 OpenCV 和 GPIO,那么接下来我们升级一下云台机制。
获取更多细节,请查看我的教程:Pan-Tilt-Multi-Servo-Control
伺服(servo:一种微型电子与机械产品的合体转置)需要连接额外的 5V 电力供应模块,并且这些伺服使用它们的数据插口(在我这边,它们是黄色的布线)连接草莓派的 GPIO,连接方式如下:
GPIO 17 ==> 倾斜伺服
GPIO 27 ==> 水平伺服
不要忘记了将 GND(GND:ground,接地端)引脚 也连到一起 ==> 草莓派——伺服——额外电力供应模块
你有个可选项:在草莓派 GPIO 和 服务端的数据输入引脚之间串联一个 1K 欧姆的电阻。这个举措可以在伺服发生问题时保护你的草莓派。
让我们一起用这个机会在 虚拟 Python 环境中测试一下我们的伺服。
我们执行 Python 脚本来测试一下驱动器。
from time import sleep
import RPi.GPIO as GPIO)
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)def setServoAngle(servo, angle):
pwm = GPIO.PWM(servo, 50)
pwm.start(8)
dutyCycle = angle / 18. + 3.
pwm.ChangeDutyCycle(dutyCycle)
sleep(0.3)
pwm.stop()
if __name__ == '__main__':
import sys
servo = int(sys.argv[1])
GPIO.setup(servo, GPIO.OUT)
setServoAngle(servo, int(sys.argv[2]))
GPIO.cleanup()
上面代码的核心是 setServoAngle(servo, angle)方法。这个方法会接收的参数有:一个 GPIO 数字,一个伺服被定位的角度值。一旦把角度值输入到这个方法中,我们必须将其转换到等效的工作周期中(duty cycle:指伺服进入角度变化的时间段)。
执行脚本时,你要输入两个参数值:GPIO 伺服对应的端口以及角度值。
例如:
python angleServoCtrl.py 17 45
上面的命令行会将在 连接在GPIO 17端口的伺服(倾斜伺服)定位到45度的”海拔“上。
angleServoCtrl.py 文件可以在我的GitHub上下载到。
将物体定位到屏幕中央的想法会使用到云台机制。实现这个想法坏消息是 我们必须实时地定位到物体的位置,但好消息是 如果我们已经知道了物体中心坐标点,这将会很容易。
首先,我们使用之前用过的”object_detect_LED“代码,以及修改该代码,以打印出检测物体的 x,y坐标点。
代码可以从我的GitHub中下载到:objectDetectCoord.py
代码核心逻辑是:在检测到的物体区域画出一个圆,并且在圆的中心画一个红点。
# only proceed if the radius meets a minimum size
if radius > 10:
# draw the circle and centroid on the frame,
# then update the list of tracked points
cv2.circle(frame, (int(x), int(y)), int(radius),
(0, 255, 255), 2)
cv2.circle(frame, center, 5, (0, 0, 255), -1)
# print center of circle coordinates
mapObjectPosition(int(x), int(y))
# if the led is not already on, turn the LED on
if not ledOn:
GPIO.output(redLed, GPIO.HIGH)
ledOn = True
我们输出中心点坐标到 mapObjectPosition(int(x), int(y)) 方法中,目的是打印这些坐标点。方法如下:
def mapObjectPosition (x, y):
print ("[INFO] Object Center coordinates at \
X0 = {0} and Y0 = {1}".format(x, y))
在跑这个程序时,我们会看到在命令行终端上输出的 (x,y)坐标点,如下图所示:
太好了!我们可以使用这些坐标点作为云台追踪系统的开始点。
我们想要目标始终在屏幕的中央,我们来定义一下,例如:假如满足下方条件,我们就认为物体在中央:
220 < x < 280
160 < y < 210
而在这个界限之外的话,我们就需要通过移动云台装置来修正偏差。基于这个逻辑,我们可以构建如下方法mapServoPosition(x, y)。需要注意的是,该方法中使用到的”x“和”y“是和我们之前打印出来的中心位置一样的。
# position servos to present object at center of the frame
def mapServoPosition (x, y):
global panAngle
global tiltAngle
if (x < 220):
panAngle += 10
if panAngle > 140:
panAngle = 140
positionServo (panServo, panAngle)
if (x > 280):
panAngle -= 10
if panAngle < 40:
panAngle = 40
positionServo (panServo, panAngle)
if (y < 160):
tiltAngle += 10
if tiltAngle > 140:
tiltAngle = 140
positionServo (tiltServo, tiltAngle)
if (y > 210):
tiltAngle -= 10
if tiltAngle < 40:
tiltAngle = 40
positionServo (tiltServo, tiltAngle)
基于这些(x,y)坐标点,并使用方法positionServo(servo, angle) ,伺服位置命令已经产生了。举个例子:假如”y“的位置是”50“,这就意味着我们的物体几乎在屏幕的顶部,也就是说 摄像头的视野是往下的(比如说倾斜装置处于120°上),所以要调低倾斜装置的角度(比如说调到100°),如此一来,摄像头的视野将会抬高,进而使得物体在屏幕上就会往下方移动(比如 y坐标提高到190的位置)。
上面的图例在几何上解释了举的例子。
思考一下水平装置上的摄像头如何移动的。要注意的是 屏幕并不是镜像映射的,也就是说,当你面对着摄像头时,如果你将物体移动到”你的左边“,但在屏幕上看,物体却会在”你的右边“移动。
positionServo(servo, angle)方法可以写成这样:
def positionServo (servo, angle):
os.system("python angleServoCtrl.py " + str(servo) + " " +
str(angle))
print("[INFO] Positioning servo at GPIO {0} to {1} \
degrees\n".format(servo, angle))
上面的代码中,我们将会调用之前展示的伺服移动脚本。
注意: angleServoCtrl.py一定要和 objectDetectTrac.py 在同一个目录下。
完整的代码可以从我的GitHub上下载:objectDetectTrack.py
下面的gif 展示了我们的项目运行的效果:
我一如既往地希望这个项目能帮助其他人找到进入激动人心的电子世界的入口。
想要获取项目细节以及最终的代码,可以浏览我的GitHub仓库:OpenCV-Object-Face-Tracking 。
交流群
欢迎加入公众号读者群一起和同行交流,目前有SLAM、三维视觉、传感器、自动驾驶、计算摄影、检测、分割、识别、医学影像、GAN、算法竞赛等微信群(以后会逐渐细分),请扫描下面微信号加群,备注:”昵称+学校/公司+研究方向“,例如:”张三 + 上海交大 + 视觉SLAM“。请按照格式备注,否则不予通过。添加成功后会根据研究方向邀请进入相关微信群。请勿在群内发送广告,否则会请出群,谢谢理解~