一 实验目的
通过对图像捕捉和颜色特征提取,了解机器视觉的一般工作流程,掌握 OpenCV 的使用及基本图像处理算法。
二 实验内容
1. 图像传感器驱动应用
2. 图像直方图生成
3. 颜色特征设定及目标识别
4. 基于颜色特征的应用扩展
三 实验设备
平台一: 树莓派, Linux OS, OpenCV 开发库
平台二: 个人计算机, Linux OS,USB 摄像头, OpenCV 开发库
四 预备知识
1. 数字图像处理基础
2. C++、 python 编程基础
程序文件:
其中 video.py 和 common.py 是 opencv 自带的 simple 文件, 在 test.py 中会调用这两个文件,
blue.jpg、green.jpg、red.jpg是参考图像文件用于学习目标的颜色特征, .pyc 文件是程序中间生成文件。
五 实验步骤
5.1 图像传感器驱动应用
将提供的 USB相机连接至树莓派主板上任一 USB 端口,能够实时捕获图像帧并显示。
- 本实验选择树莓派为实验平台, 连接图像传感器至树莓派主板,实验采用图像传感器为山狗相机,如下图所示;
树莓派主板型号是树莓派第三代B型,操作系统为 ubuntuMate, 在摄像头插上以后便可直接使用,无需驱动,可以使用 video.py 测试摄像头。 整体连线见下图效果。
测试方式: 在当前文件夹路径下执行 ./video.py, 如设备正常则生成一个图像窗口并实时显示摄像头画面。
5.2 图像直方图生成
了解直方图的概念,理解像素灰度值的概率分布函数,根据提供的 OpenCV 框架, 获得直方图结果, 并显示当前图像直方图。
实验文件夹下有样本图像 blue.jpg, 从该图像上获取颜色概率用于图像颜色目标搜索,并绘制该图像的颜色直方图。
(1) 为了打开样本图像, 首先创建一个窗口对象,在 OpenCV 中通过对窗口命名的方式
来创建一个对象, 如:
cv2.namedWindow('camshift') # cv2窗口对话框名称
self.hist_roi = None
self.selection = None
self.tracking_state = 0
self.hide_background = False # 是否需要隐藏背景,默认显示
if color == 'red':
self.flag = 'red'
self.roi = cv2.imread('red.jpg') # 读取red.jpg作为region of interest
elif color == 'green':
self.flag = 'green'
self.roi = cv2.imread('green.jpg') # 读取green.jpg作为region of interest
elif color == 'hhh':
self.flag = 'hhh'
self.roi = cv2.imread('hhh.png') # 读取hhh.jpg作为region of interest 这个图片是用来测试其他颜色的追踪效果的
else:
# detect blue by default
self.flag = 'blue'
self.roi = cv2.imread('blue.jpg') # 读取blue.jpg作为region of interest
这里,创建了一个窗口,并命名为“camshift”,为后续窗口访问提供资源, 程序中
self.flag 为一全局 string 型变量,根据目标颜色取相应数值, 如目标颜色为红色时则置self.flag 为“red” 并读入red.jpg, 将得到的数据传递给变量 self.roi。
(2) 计算并显示颜色概率直方图
图像直方图为图像中不同颜色和亮度的像素点的统计分布, 横轴为颜色和亮度
纵轴为该颜色和亮度下像素点的个数,有一维直方图(针对灰度图像) 和二维直方图(针对彩色图像)。
由于该样本图像已经目标图像都为彩色的, 故需要二维直方图, 计算直方图的函数为
cv2:calcHist(images; channels;mask; histSize; ranges[; hist[; accumulate]])
1. images: 原图像(图像格式为 uint8 或 float32)。当传入函数时应该
用中括号 [] 括起来,例如: [img]。
2. channels: 同样需要用中括号括起来,它会告诉函数我们要统计那幅图
像的直方图。如果输入图像是灰度图,它的值就是 [0];如果是彩色图像
的话,传入的参数可以是 [0], [1], [2] 它们分别对应着通道 B, G, R。
3. mask: 掩模图像。要统计整幅图像的直方图就把它设为 None。但是如
果你想统计图像某一部分的直方图的话,你就需要制作一个掩模图像,并
使用它。
4. histSize:BIN 也就是像素某一灰度值范围内的像素点数目。也应该用中括号括起来,
例如: [256]。
5. ranges: 像素值范围,通常为 [0, 256];
注意参数最后 accumulate]]表示 channel、 range 和 histSize 可以通过中括号写入两个数值,以此来表示两个不同颜色通道。
实验中采用彩色图像,因此要考虑每个像素的颜色(Hue)和饱和度(Saturation), 需要将图像的颜色空间从 BGR 转换到 HSV, 根据 S、 V 这两个要素绘制 2D 直方图,相应地, calcHist()函数的参数也要有两个通道的表示,例如:
• channels=[0, 1] 因为我们需要同时处理 H 和 S 两个通道。
• bins=[180, 256]H 通道为 180, S 通道为 256。
• range=[0, 180, 0, 256]H 的取值范围在 0 到 180, S 的取值范围
在 0 到 256。
示例代码如下:channels 只有一个参数Hue 色调,范围是0-180?
为什么OpenCV里的图像的色调(hue)值的范围是0~180?
In the case of HSV or HLS representations, hue is normally represented as a value from |
意思就是8bit不方便表示0-360,而255和360对应起来没有180和360对应起来方便。
# 选定搜索区域后,设置当前窗口,开始检测。
# 这时获得示例图的HSV图像和设置掩模(与视频帧的掩模相对应)。。。
# [辅助处理] 为了增强图像的对比度,方便检测和追踪,
# 此时绘制例图的H通道信息的一维直方图,并进行均衡化。
# 若开启检测
if self.selection:
x0, y0, x1, y1 = self.selection
self.track_window = (x0, y0, x1, y1) # 追踪子区域
# 对ROI进行颜色格式转换和阈值限制
# 例图的HSV格式图像
hsv_roi = cv2.cvtColor(roi, cv2.COLOR_BGR2HSV)
# 例图的掩膜(与当前视频帧的掩膜对应)
mask_roi = self.get_mask(hsv_roi, color=self.flag)
# 绘制ROI图像的一维直方图
hist_roi = cv2.calcHist([hsv_roi], [0], mask_roi, [16], [0, 180])
# 对hist做归一化
# 直方图均衡化,[0,255]表示均衡后的范围
cv2.normalize(hist_roi, hist_roi, 0, 255, cv2.NORM_MINMAX)
# 将hist向量reshape为1列并存入self.hist中
self.hist_roi = hist_roi.reshape(-1)
self.show_hist()
# 可见区域
vis_roi = vis[y0:y1, x0:x1]
cv2.bitwise_not(vis_roi, vis_roi) # 对每个像素进行二进制取反操作
# 在vis中,置mask中为0的对应位置也为0
vis[mask == 0] = 0
# 如果检测到了图像中存在的感兴趣颜色区域
代码中引用了掩膜来进行图像区域限定,变量为 mask_roi, np.array(0.,60.,32.)表示,统计好的直方图进行显示时, 先定义每个颜色范围的像素点个数,然后通过矩形表示该像素点总数数值,最后生成一个显示窗口,显示矩形图像, self.show_hist()代码如下:
def show_hist(self):
# 展示图片的直方图
bin_count = self.hist_roi.shape[0]
# 直方图的柱子宽度为24像素
bin_w = 24
img = np.zeros((256, bin_count * bin_w, 3), np.uint8)
for i in range(bin_count):
h = int(self.hist_roi[i])
cv2.rectangle(img, (i * bin_w + 2, 255), ((i + 1) * bin_w - 2, 255 - h),
(int(180.0 * i / bin_count), 255, 255), -1)
# 颜色空间转换
img = cv2.cvtColor(img, cv2.COLOR_HSV2BGR)
cv2.imshow('hist', img)
5.3 颜色特征设定及目标识别
(1) 通过 self.cam.read()读取视频帧
while True:
# 读取视频帧
ret, self.frame = self.cam.read()
vis = self.frame.copy()
hsv = cv2.cvtColor(self.frame, cv2.COLOR_BGR2HSV) # 将当前帧从RGB格式转换为HSV格式
# 获得当前视频帧hsv图像的掩膜
"""
掩膜的概念:图像处理中,选定一块图像、图形或物体,
对处理的图像(全部或局部)进行遮挡,
来控制图像处理的区域或处理过程
这里遮挡处理的图像是当前摄像头获取的视频帧
"""
# 三个维度对应H、S、V,numpy数组设定了三个维度分别的范围,
# 在mask区域里设为255,其它区域设为0,实际上变为黑白图;
# 这里避免由于低光引起的错误值,使用cv.inRange()函数丢弃低光值;
# 即选定的范围是掩模,是后续要进行处理的区域。
mask = self.get_mask(hsv, color=self.flag)
(2) 通过参考图片颜色概率的反向投影函数在视频帧中找到目标颜色,通过 camshift 函数得到目标颜色在帧中的坐标信息并存入 track_box, 持续跟踪该物体。
# 如果检测到了图像中存在的感兴趣颜色区域
if self.tracking_state == 1:
# 追踪到对象之后,不用再设置搜索区域
self.selection = None # 取消ROI模板
"""
反向投影:重置视频帧的像素点的值后,最亮的即值最大的,
在例图的灰度直方图中出现的个数最多,极有可能是要追踪的目标;
最后相当于得到一个概率图
"""
# 区分颜色的是H(范围为0-180),以例图的直方图作反向投影,得到颜色概率分布图
prob = cv2.calcBackProject([hsv], [0], self.hist_roi, [0, 180], 1) # 反向投影法
# 使要寻找的颜色区域更亮
prob &= mask # 与mask进行与运算 得到所求颜色的直方图概率分布
# 迭代到10次或误差小于1,搜索结束
criteria_term = (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 1) # CamShift算法的停止条件
"""
CamShift算法:对meanShift进行改进,可以自适应的调整椭圆的大小和角度
追踪目标颜色质心
"""
# 运用CamShift算法对track_window内的图像进行prob检测
track_box, self.track_window = cv2.CamShift(prob, self.track_window, criteria_term)
(3) 通过 cv2.ellipse 画椭圆将图中的目标物体标识出来,参数中 track_box 为目标颜色坐标值, (0,0,255)选择 BGR 模式中的红色, 2 为椭圆线圈像素宽度。
# 绘制红色的椭圆标记
cv2.ellipse(vis, track_box, (0, 0, 255), 2) # 在track_box内部绘制椭圆图像
print ('center: %.2f\taxes: %.2f\tangle: %.2f\tstart_angle: %.2f\tend_angle: %.2f') % (
track_box[0][0], track_box[0][1], track_box[1][0], track_box[1][1], track_box[2]
)
(4) 当目标物体变得太远或移出画面时, track_box 值将无法计算, 判断 track_box 数值小于 1 时, 需要重置算法,重新搜索并跟踪
if track_box[1][1] <= 1:
# 如果没有检测到 重置检测状态
self.start()
当 track_box 范围小于一个阈值时,便把跟踪状态置为 0, 重新 start, 即给 track_box 重新赋值为整个图像范围, 从新的范围开始搜索。
5.4 基于颜色特征的应用扩展
在前 3 步的基础上, 尝试修改样本图像,或者修改代码, 进一步地进行机器视觉处理,如识别红色、 绿色或橙色特征区域等。
测试结论,对红黄蓝颜色的追踪较为准确,对复色光的追踪容易被干扰
六 实验思考题
6.1 什么是图像的直方图?
如果将图像中像素亮度(灰度级别)看成是一个随机变量, 则其分布情况就反映了图像的统计特性,这可用Probability Density Function (PDF)来刻画和描述,表现为灰度直方图(Histogram)。
灰度直方图是灰度级的函数,它表示图像中具有某种灰度级的像素的个数,反映了图像中每种灰度出现的频率,它是图像最基本的统计特征。
直方图显示图像数据时会以左暗又亮的分布曲线形式呈现出来,而不是显示原图像数据,并且可以通过算法来对图像进行按比例缩小,且具有图像平移、旋转、缩放不变性等众多优点。直方图在进行图像计算处理时代价较小,例如一个1024x1024的图像,当转换成直方图时会进行其分组,列如分为255组,那么这1024x1024个像素会被叠加分到这255组中去,即处理起虽然值变大了,但是像素点变少了,不需要去拆分rgb(hsv除外)三色,不需要单步计算等额外的工作,只需要通过特定的算法对这255组进行计算即可所以经常用于图像处理。直方图的显示方式是左暗又亮,左边用于描述图像的暗度,右边用于描述图像的亮度,如下图所示:
左右不同亮度的图片以及他们的直方图对比:
6.2 HSV 空间通过哪几个维度表达颜色分布?
1、HSV是一种将RGB色彩空间中的点在倒圆锥体中的表示方法。HSV由色相(Hue)、饱和度(Saturation)、明度(Value)三个维度组成,又称HSB(B即Brightness)。色相是色彩的基本属性,就是平常说的颜色的名称,如红色、黄色等。饱和度(S)是指色彩的纯度,越高色彩越纯,低则逐渐变灰,取0-100%的数值。明度(V),取0-max(计算机中HSV取值范围和存储的长度有关)。HSV颜色空间可以用一个圆锥空间模型来描述。圆锥的顶点处,V=0,H和S无定义,代表黑色。圆锥的顶面中心处V=max,S=0,H无定义,代表白色。
2、RGB颜色空间中,三种颜色分量的取值与所生成的颜色之间的联系并不直观。而HSV颜色空间,更类似于人类感觉颜色的方式,封装了关于颜色的信息:“这是什么颜色?深浅如何?明暗如何?”
3、RGB和HSV转换
(1)从RGB到HSV
设max等于r、g和b中的最大者,min为最小者。对应的HSV空间中的(h,s,v)值为:
h在0到360°之间,s在0到100%之间,v在0到max之间。
(2)从HSV到RGB
6.3 Meanshift 和 Camshift
1、Meanshift 算法的基本原理是和很简单的。假设我们有一堆点(比如直方图反向投影得到的点),和一个小的圆形窗口,我们要完成的任务就是将这个窗口移动到最大灰度密度处(或者是点最多的地方)。如下图所示:
初始窗口是蓝色的“C1”,它的圆心为蓝色方框“C1_o”,而窗口中所有点质心却是“C1_r”(小的蓝色圆圈),很明显圆心和点的质心没有重合。所以移动圆心 C1_o 到质心 C1_r,这样我们就得到了一个新的窗口。这时又可以找到新窗口内所有点的质心,大多数情况下还是不重合的,所以重复上面的操作:将新窗口的中心移动到新的质心。就这样不停的迭代操作直到窗口的中心和其所包含点的质心重合为止(或者有一点小误差)。按照这样的操作我们的窗口最终会落在像素值(和)最大的地方。如上图所示“C2”是窗口的最后位址,我们可以看出来这个窗口中的像素点最多。整个过程如下图所示:
通常情况下我们要使用直方图方向投影得到的图像和目标对象的起始位置。当目标对象的移动会反映到直方图反向投影图中。就这样, meanshift 算法就把我们的窗口移动到图像中灰度密度最大的区域了。
要在 OpenCV 中使用 Meanshift 算法首先我们要对目标对象进行设置,计算目标对象的直方图,这样在执行 meanshift 算法时我们就可以将目标对象反向投影到每一帧中去了。另外我们还需要提供窗口的起始位置。在这里我们值计算 H( Hue)通道的直方图,同样为了避免低亮度造成的影响,我们使用函数 cv2.inRange() 将低亮度的值忽略掉。
2. Camshift
你认真看上面的结果了吗?这里面还有一个问题。我们的窗口的大小是固定的,而汽车由远及近(在视觉上)是一个逐渐变大的过程,固定的窗口是不合适的。所以我们需要根据目标的大小和角度来对窗口的大小和角度进行修订。OpenCVLabs 为我们带来的解决方案( 1988 年):一个被叫做 CAMshift 的算法。
这个算法首先要使用 meanshift, meanshift 找到(并覆盖)目标之后,再去调整窗口的大小, s = 2x√M 00/256。它还会计算目标对象的最佳外接椭圆的角度,并以此调节窗口角度。然后使用更新后的窗口大小和角度来在原来的位置继续进行 meanshift。重复这个过程知道达到需要的精度。与 Meanshift 基本一样,但是返回的结果是一个带旋转角度的矩形(这是我们的结果),以及这个矩形的参数(被用到下一次迭代过程中)并且初始位置是感觉上一帧的位置开始的。对上面三帧图像分析的结果如下:
可以改变大小和角度的框感觉会更好一些