Robotics Lab1 —— 基于颜色特征的目标识别与追踪实验

Robotics Lab1 —— 基于颜色特征的目标识别与追踪实验

由于第一次接触OpenCV和图像处理,阅读代码和进行实验时还是有一定的难度。通过他人的讲解和查阅相关的资料,有了初步的认识。感觉实验一已经融合了一些知识、算法和编程技巧,接收起来很零散。是否有基础的读物或逐步进阶的知识层,来学习图像处理领域的内容。

1. 环境的安装和配置

  • Ubuntu16.04系统,计算机摄像头,OpenCV-2.4.13;
  • 由于网络问题,没有通过远程请求来下载。解压OpenCV-2.4.13压缩包,运用Cmake和make命令进行编译和安装,安装过程比较顺利,没有出现什么问题。
  • 需要注意的是,如果Ubuntu系统之前没有安装过ROS,则在安装OpenCV需要安装一定的依赖包。以防万一,还是先执行一遍该操作,否则卸载和重装很麻烦,会遇到很多未知的问题。
  • 安装依赖包的命令如下:
# 安装编译工具
sudo apt-get install build-essential
# 安装依赖包
sudo apt-get install cmake git libgtk2.0-dev pkg-config libavcodec-dev libavformat-dev libswscale-dev
# 安装可选包
sudo apt-get install python-dev python-numpy libtbb2 libtbb-dev libjpeg-dev libpng-dev libtiff-dev libjasper-dev libdc1394-22-dev
  • 开启终端,进入实验代码文件夹,使用python ./video,py命令运行摄像头测试程序,能够顺利的捕获视频。

2. 相关概念和原理理解

从实验代码中涉及到的一些相关概念和算法入手,查阅资料,理解学习。如有错误,请指正。

灰度图

[理解] 图像处理中,用RGB三个分量[0,255]表示真彩色,也就是神经网络里处理图像数据时经常说的:3通道。一个像素点的颜色是由RGB三个值来表现的。已知图片的尺寸信息w*h(像素),则一个像素点矩阵对应三个颜色向量矩阵(R,G,B),分别为w*h大小的矩阵。

而对于灰度图,三通道的取值均相同(R=B=G),都为0时为黑色,都为255时为白色,0-255区间的值代表不同的亮度等级(灰度值),直观上看图像只有黑白两色。

[作用]

  1. 图像识别中,需要进行特征提取,梯度是其中最关键的因素,因为其意味着边缘。而灰度图可以理解为图像的强度,常用于计算梯度。常用的特征提取方法如HOG、LBP、SIFT等,本质都是统计梯度的信息。
  2. 同类物体的颜色易受光照等因素的影响,从而产生不同的变化,所以RGB图像难以提供关键信息;而灰度图综合了真彩色位图的RGB各通道的信息,与彩色图对图像的描述一致,能够提供有用信息。所以在图像识别中,通常将彩色图灰度化,再进行处理和分析。
  3. 灰度图存储量小,可以用较小的内存存储大量的特征信息。
  4. 有时,在图像处理和识别中,可视化的效果是RGB图像,但实际输入和处理的都是灰度图像。

[灰度化] 一张图片由一个像素点矩阵构成, 对图像的处理实际是对像素点矩阵的操作。用矩阵索引的方法表示某个像素点的位置,通过对其三个变量赋值,可以改变像素点的颜色。将图像灰度化,实际是让像素点矩阵中的每一个像素点的三变量值相等,此时的值成为灰度值[0-255之间的值]。通常有两种处理方法:

  1. 灰度化后的R=(处理前的R + 处理前的G +处理前的B)/ 3

    灰度化后的G=(处理前的R + 处理前的G +处理前的B)/ 3

    灰度化后的B=(处理前的R + 处理前的G +处理前的B)/ 3

  2. 在YUV的颜色空间中,Y分量表示点的亮度,而亮度Y与R、G、B三个颜色分量对应:Y=0.3R+0.59G+0.11B,以这个亮度值来表示图像的灰度值。

    灰度化后的R = 处理前的R * 0.3+ 处理前的G * 0.59 +处理前的B * 0.11

    灰度化后的G = 处理前的R * 0.3+ 处理前的G * 0.59 +处理前的B * 0.11

    灰度化后的B = 处理前的R * 0.3+ 处理前的G * 0.59 +处理前的B * 0.11

一般来说第二种方法效果较好。

[二值化]灰度化后图像的三通道值虽然相等,但可以有0-255区间的任意值,而二值化就是让每个像素点的灰度值只有0和255两个值,直观上看图像只有黑与白两种效果。

对灰度图进行二值化,对于灰度值究竟转化为0还是255的问题,引入了阈值。常用的方法有3种:

  1. 对于所有的图片,均取阈值为127[0-255的中位数],将灰度值<=127的变为0(黑色),灰度值>127的变为255(白色)。优点:计算量小,速度快;缺点:不同的图片,颜色分布差别很大,不一定适用于中值。
  2. 先对所有像素点的灰度值求平均(avg),然后将每个像素点的灰度值依次与平均值比较,<=avg变为0,>avg变为255
  3. 直方图法[双峰法]直方图认为图像由前景背景组成,直观看灰度图,图像的前景背景都在其上形成高峰,而双峰之间的最低谷处为阈值,再将每个像素点的灰度值与阈值比较。
  • [注] 每张图片都分前景和背景,一般认为靠近相机的为前景。前景目标图像,涉及目标跟踪问题,一般流程是检测——识别——跟踪,对于目标跟踪,主要是根据实际问题建立最适合的模型再选取最优的算法。
    一般线性问题可以用卡尔曼滤波算法粒子滤波算法meanshift(利用颜色直方图,较简便),但对于遮挡情况处理效果都不是很好;此外还有光流估计等算法。

直方图

[理解]

  • 直方图实际上是对图像的另一种解释,其引用了统计学中显示数据分布情况的二维统计图表,直观的显示图像中灰度的分布情况。
  • 直方图的横轴表示灰度的等级(由左至右为由暗到亮),以灰度图为例,纵轴表示该灰度级的像素点的个数[一般是两个灰度值区间内的像素点个数]。直方图在某个亮度区间的凸起越高,即表示在这个亮度区间内的像素越多。[如:如果某个直方图的凸起主要集中在左侧,则说明这张图像的亮度整体偏低]。
  • 直方图的横轴等级还可以选择彩色图像R、G、B任意颜色的信息。
  • 一般会将直方图的纵坐标归一化到[0,1]区间内,即将灰度级出现的像素个数除以图像中像素的总数,得到灰度级出现的频率信息。

[统计直方图——不加掩模]

  • BINS 对灰度等级进行分组,即划定等级区间。BINS为组数,直观显示在直方图上为矩形块的个数。OpenCV中用histSize表示。
  • DIMS 表示对图像数据进行处理时使用的参数数目。如只考虑灰度值,则为1。
  • RANGE 要统计的灰度值范围,一般为[0-256],即所有的灰度值。
  • OpenCV中,得到的直方图为一维数组,索引表示颜色值,对应的值表示像素点个数。若为灰度图,则仅含一个灰度值参数,若为RGB图像,则可以通过for循环分别得到每个通道的灰度图,再显示到同一个二维坐标图中。
  1. OpenCV方式[较快]
    OpenCV中统计一幅图像的直方图函数为:
cv2.calcHist(images; channels; mask; histSize; ranges[, hist[, accumulate]])

其中,mask(掩模)参数用以选择图像的某一部分的直方图,值为None时表示统计整幅图像。

  1. Numpy方式
# img.ravel() 将图像转成一维数组,这里没有中括号。
hist,bins = np.histogram(img.ravel(), 256, [0,256])
# 一维直方图常用,速度快
hist=np.bincount(img.ravel(), minlength=256)

[绘制直方图]

  1. Matplotlib方式[简单,方便绘制多通道(BGR)的直方图]
import cv2
import numpy as np
from matplotlib import pyplot as plt
img = cv2.imread('home.jpg',0)
plt.hist(img.ravel(),256,[0,256]);
plt.show()
  1. OpenCV方式[复杂]

[掩模(mask)]

  • 掩模图像的矩阵形状和原图像相同,为二维数组,通过全局或局部遮挡来控制图像的处理区域。
  • 掩模实际上是两幅图像间进行的各种位运算操作。
  • 在图像基本运算的操作函数中,若参数中含有掩模,则原始图像在运算完之后会与掩模图像运算。
  • 掩模的作用主要有提取感兴趣区屏蔽某些区域结构特征提取。本次实验用到了掩模的第一个作用:选定感兴趣区域(ROI)——
  1. 构建掩模图像,选定感兴趣区域:其为二维数组,设置要统计的区域值为255(白色),其余部分为0(黑色)。这时,仅得到另一个黑白的掩模图像。将这个掩模图像传给统计直方图的函数。
  2. 与运算:用预先制作的感兴趣区掩模与待处理图像作与运算,使得感兴趣区域的像素值不变(有时统一为白色),区域外的像素值为0此时,得到的是划定了一块区域后的原始图像,区域外的内容均变为了黑色(类似图片剪切,但保留了剪切的部分)

[1D直方图与2D直方图]

  • 绘制灰度图的直方图,仅考虑了图像的灰度值一个特征,所得的直方图为1D直方图
  • 而对于彩色图像的直方图,即2D直方图,需要考虑两个图像特征——颜色(Hue)饱和度(Saturation);其横轴代表S值,纵轴代表H值。
  • 绘制彩色2D直方图,首先需要将图像的颜色空间从BGR转换到HSV。[因为在HSV颜色空间要比在BGR空间中更容易表示一个特定颜色]。函数cv2.calcHist()的参数要进行相应的修改:
  1. channels=[0,1],同时处理H和S两个通道。
  2. bins=[180,256],按每个灰度值来算时,H通道为180组,S通道为256组。
  3. range=[0,180,0,256],H的取值范围为0-180,S的取值范围为0-256.

直方图均衡化

[理解] 一张图片的亮度等级可能集中分布在[0-255]区间的某个子区间,而通过某种转化,重新分配像素值,使一定灰度范围内的像素数量(在算法实现上用概率表示)大致相同,即将其像素点的个数均匀分布在0-255这个更大的区间(视觉上直方图呈现拉伸和压平的效果),会增强图像的对比度。
图像对比度增强有直接和间接两种方式,直方图拉伸直方图均衡化是两种最常见的间接对比度增强方法。
直方图拉伸通过对比度拉伸对直方图进行调整,从而“扩大”前景和背景灰度的差别,以达到增强对比度的目的,实现方法有线性和非线性两种。
直方图均衡化通过使用累积函数对灰度值进行调整以实现对比度的增强。

[算法原理] 直方图均衡化实际上是要找到一个累积变换函数,利用这个函数对原图像的像素个数重新分配区间。该变换函数需要满足两个条件:

  1. f(x)在 0<=x<=L−1 上单调递增(不要求严格单调递增),其中x,L表示灰度级(L=256);保证灰度级次序,即原本暗的仍然暗,原本亮的仍然亮,防止黑白颠倒。
  2. f(x)的范围是 [0,L−1]使输入和输出在同一个区间范围内变换,方便比较;有时也用[0,1]。

不同问题有不同的直方图均衡化变换函数,但函数的生成可以“自动化”,即有一套既成的公式,对于不同的值进行套用,下图描述了公式的推导[要知道数学上离散是连续的一种特例]:

直方图均衡化推导

由上述过程可知,对于一个输入图像,只需统计它之中各灰度值出现的概率Pf,然后生成映射函数 均衡化映射函数(连续形式
).png

  • 图像中,灰度值是离散的,则可以用求和代替积分,差分代替微分,则函数定义改为 原图灰度值概率定义.png定义2.png ,即图像的累积直方图CDF
    变换函数为 均衡化映射函数(离散).png ,其中h(xi)表示直方图中每个灰度级像素的个数, w和h分别表示图像的宽和高。
  • 在灰度值离散的情况下,原始和均衡化后的直方图灰度级均为整数,则必须对映射结果取整,这导致变换后的图像中各灰度值出现的概率未必相等。但是可以确定的是: g 的灰度级在一定程度上比 f 更分散了。

[作用]

  1. 直方图均衡化可以增强图像的对比度,常用来增强局部对比度。
  2. 直方图均衡化对于背景和前景太亮或太暗的图像处理效果更好。
  3. 直方图均衡化通常用在训练图片的预处理中,来使所有图片具有相同的亮度条件。
  4. 直方图均衡化对处理的数据不加选择,其可能增强噪音,而降低有用信号的对比度。

直方图反向投影

[理解——△重要]

OpenCV中文网站 对反向投影的讲解比较详细,也比较容易理解。

  • 直方图反向投影通常应用在对比两幅图像中,[如在测试图像中找到含示例(模型)图像颜色的区域]。对比两幅图像,实际上是提取图像中的某个特征,提取特征的过程就是反向投影的过程。
  • 有示例图像和目标图像,分别统计出它们的直方图,[一般使用颜色直方图,即用H分量作直方图。因为这更易于图像分割],再由直方图得到反向投影矩阵。
  • 对原来的直方图进行反向投影,实际上是将原图像简单化:首先对亮度值0-255划分区间(注意模型和目标的直方图划分区间要相同),找到目标图像的像素值所在的区间,再去模型直方图对应区间找到BINS的值,用该值代替目标图像对应区间内的所有像素值;
    [这样就建立了测试图像和目标特征的联系]。最后得到的图像矩阵和原矩阵大小相同,但其中的值种类更少。[一般将0-255划分为几个区间,就有几个值]。
  • 由反向投影的过程可以看出,若在原图像的直方图区间内点越多,替换的像素值就越大,而越大的值在亮度等级上越高,表现在反向投影矩阵中就越亮。
  • 结果为需要寻找的特征区域在反向投影图中越亮,其他区域越暗。最后得到的颜色概率分布图是一个灰度图像。

[作用]

  • 反向投影一般给定某个特征,计算某一特征的直方图模型,然后使用模型去寻找测试图像中存在的该特征。
  • 可以通过直方图反向投影来实现图像分割,背景与对象分离,对已知对象位置进行定位等。
  • 常应用于模式匹配、对象识别、视频跟踪。

[算法实现]

OpenCV-Python-Toturial中文版

  • 不太理解用圆盘形卷积核对其做卷操作,最后使用阈值进行二值化的原因和含义。
  • OpenCV实现如下:
import cv2
import numpy as np
roi = cv2.imread('tar.jpg')
hsv = cv2.cvtColor(roi,cv2.COLOR_BGR2HSV)
target = cv2.imread('roi.jpg')
hsvt = cv2.cvtColor(target,cv2.COLOR_BGR2HSV)

# calculating object histogram
roihist = cv2.calcHist([hsv],[0, 1], None, [180, 256], [0, 180, 0, 256] )

# 归一化:原始图像,结果图像,映射到结果图像中的最小值,最大值,归一化类型
#cv2.NORM_MINMAX 对数组的所有值进行转化,使它们线性映射到最小值和最大值之间
# 归一化之后的直方图便于显示,归一化之后就成了 0 到 255 之间的数了。
cv2.normalize(roihist,roihist,0,255,cv2.NORM_MINMAX)
dst = cv2.calcBackProject([hsvt],[0,1],roihist,[0,180,0,256],1)

# 此处卷积可以把分散的点连在一起
disc = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(5,5))
dst=cv2.filter2D(dst,-1,disc)

# threshold and binary AND
ret,thresh = cv2.threshold(dst,50,255,0)

# 别忘了是三通道图像,因此这里使用 merge 变成 3 通道
thresh = cv2.merge((thresh,thresh,thresh))

# 按位操作
res = cv2.bitwise_and(target,thresh)
res = np.hstack((target,thresh,res))
cv2.imwrite('res.jpg',res)

# 显示图像
cv2.imshow('1',res)
cv2.waitKey(0)

CamShift算法

  • 典型的目标跟踪算法。CamShift算法是MeanShift算法的扩展,两者常常与直方图反向投影相结合,从反向投影得到颜色概率分布图入手。

[meanshift算法]
其过程为:

  1. 在颜色概率分布图中选择搜索窗口:窗口的初始位置;窗口的类型(均匀、多项式、指数或高斯类型);窗口的形状(对称的、歪斜的、可能旋转的、圆形或矩形);窗口的大小(超出窗口大小则被截去)。
  2. 计算窗口的重心:即x,y分别的一阶距与零阶距之比。
  3. 调整搜索窗的大小。
  4. 移动搜索窗的中心到计算出的质心。
  5. 返回第2步,直到搜索窗的中心与质心间的移动距离小于预设的固定阈值,或者循环运算的次数达到某一最大值,则停止计算。

[CamShift算法]

  • CamShift算法将meanshift算法扩展到连续图像序列,其将视频的所有帧做meanshift运算,并将上一帧的结果(搜索窗的大小和重心),作为下一帧meanshift算法搜索窗的初始值。
    迭代此过程,实现对目标的跟踪。其过程为:
  1. 初始化搜索窗;
  2. 计算搜索窗的颜色概率分布(反向投影);
  3. 执行meanshift算法,获取搜索窗新的大小和位置;
  4. 在下一帧视频图像中用3中的值重新初始化搜索窗的大小和位置,再跳转到2继续进行。

[算法性能]

优点: camshift能有效解决目标变形和遮挡的问题,对系统资源要求不高,时间复杂度低,在简单背景下能够取得良好的跟踪效果。

缺点: 当背景较为复杂,或者有许多与目标颜色相似像素干扰的情况下,会导致跟踪失败。因为它单纯的考虑颜色直方图,忽略了目标的空间分布特性,所以这种情况下需加入对跟踪目标的预测算法。

主要代码分析

关于函数的用法以及涉及的原理写到了代码的注释里,这里整理一下程序的实现思路。

  • Lab1是“关于颜色特征的目标识别和与追踪”,则根据示例图的颜色信息(纯色),从视频图像中选定的初始搜索区域内进行目标颜色的检测,识别到之后使用反向投影和CamShift算法控制搜索框移动到目标区域,绘制红色的椭圆框进行追踪。
  1. [预处理]首先,获取视频帧图像,将其从BGR颜色空间转到HSV颜色空间,这样可以忽略光照等亮度信息的影响,并设置视频帧图像的掩模。
roi = self.roi    # 获取感兴趣的例图
        self.start()      # 开启检测

        while True:  # 在输入错误的情况下仍然可以继续循环
#         for frame in self.cam.capture_continuous(self.rCa, format='bgr', use_video_port=True):
            ret, self.frame = self.cam.read()      # 读取视频帧
#             self.frame = frame.array
            vis = self.frame.copy()
#             vis = copy.deepcopy(self.frame)
            hsv = cv2.cvtColor(self.frame, cv2.COLOR_BGR2HSV)       # 将当前帧从RGB格式转换为HSV格式

            # 获得当前视频帧hsv图像的掩膜
            """
               掩膜的概念:图像处理中,选定一块图像、图形或物体,对处理的图像(全部或局部)进行遮挡,来控制图像处理的区域或处理过程
               这里遮挡处理的图像是当前摄像头获取的视频帧
            """
             # 三个维度对应H、S、V,numpy数组设定了三个维度分别的范围,在mask区域里设为255,其它区域设为0,实际上变为黑白图;
			 # 这里避免由于低光引起的错误值,使用cv.inRange()函数丢弃低光值;即选定的范围是掩模,是后续要进行处理的区域。
             mask = cv2.inRange(hsv, np.array((0., 60., 32.)), np.array((180., 255., 255.)))
  1. 选定搜索区域后,设置当前窗口,开始检测。这时获得示例图的HSV图像和设置掩模(与视频帧的掩模相对应)。[辅助处理] 为了增强图像的对比度,方便检测和追踪,此时绘制例图的H通道信息的一维直方图,并进行均衡化。
if self.selection:    # 若开启检测
#                 x0, y0, x1, y1 = 220, 110, 358, 245
               x0, y0, x1, y1 = self.selection     # 获取选择的区域四个坐标
               self.track_window = (x0, y0, x1-x0, y1-y0)     # 根据坐标设置当前跟踪窗口的形状

#                 hsv_roi = hsv[y0:y1, x0:x1]
#                 mask_roi = mask[y0:y1, x0:x1]
               hsv_roi = cv2. cvtColor(roi,cv2. COLOR_BGR2HSV)     # 例图的HSV格式图像
               mask_roi = cv2.inRange(hsv_roi, np.array((0., 60., 32.)), np.array((180., 255., 255.)))   # 例图的掩膜(与当前视频帧的掩膜对应)

               #仅以Hue值为特征,绘制例图的一维直方图,分为16个区间
               hist = cv2.calcHist( [hsv_roi], [0], mask_roi, [16], [0, 180] )    # mask_roi用来标记例图中要查找的颜色的区域(此示例不必要,整张图颜色相同)

       #二维直方图
#                 hist = cv2.calcHist( [hsv_roi], [0,2],None, [180,256], [0, 180,0 , 255] )

               cv2.normalize(hist, hist, 0, 255, cv2.NORM_MINMAX);    # 直方图均衡化,[0,255]表示均衡后的范围

               # 新数组的shape属性应与原来的数组一致,即新数组元素数量与原数组元素数量要相等。
       # 当其中一个参数为-1时,函数会根据另一个参数的维度计算出数组的另外一个shape属性值。
       # 这里表示将hist矩阵变为一列(按行展开)。
       self.hist = hist.reshape(-1)

               #二维直方图显示
#                 plt.imshow(hist,interpolation = 'nearest')
#                 plt.show()
               self.show_hist()

       # 我的理解是这一部分使视频图像中感兴趣的区域为白色,其余为黑色。
               vis_roi = vis[y0:y1, x0:x1]
               cv2.bitwise_not(vis_roi, vis_roi)
               vis[mask == 0] = 0
  1. 接下来到了追踪目标的核心代码,使用例图的直方图对视频图像做反向投影,得到颜色概率分布图。CamShift算法从这个颜色概率分布图开始,进行一系列计算,根据得到的向量控制搜索框向目标区域移动。
    每一帧根据上一帧的结果动态调整绘制的椭圆追踪框的大小、形状和方向。
if self.tracking_state == 1:    # 如果检测到了图像中存在的感兴趣颜色区域
                self.selection = None       # 追踪到对象之后,不用再设置搜索区域
                """
                   反向投影:重置视频帧的像素点的值后,最亮的即值最大的,在例图的灰度直方图中出现的个数最多,极有可能是要追踪的目标;
                            最后相当于得到一个概率图
                """
                prob = cv2.calcBackProject([hsv], [0], self.hist, [0, 180], 1)     # 区分颜色的是H(范围为0-180),以例图的直方图作反向投影,得到颜色概率分布图

                prob &= mask     # 使要寻找的颜色区域更亮

                term_crit = ( cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 1 )    # 迭代到10次或误差小于1,搜索结束

                """
                   CamShift算法:对meanShift进行改进,可以自适应的调整椭圆的大小和角度
                                 追踪目标颜色质心
                """
                track_box, self.track_window = cv2.CamShift(prob, self.track_window, term_crit)
#                 if track_box[0][1] <= 240:
#             self.ser.write(str(int(track_box[0][0])-320) + " " + str(int(track_box[0][1])-240))
#             print str(int(track_box[0][0])-320) + " " + str(int(track_box[0][1])-240)
                if track_box[1][1] <= 1:   # 目标距离太远,重置算法,重新搜索
                    self.tracking_state = 0
                    self.start()
                else:
                    if self.show_backproj:
                        vis[:] = prob[...,np.newaxis]
                    try:
					    '''
                        track_box: [[center, axes], [angle, startAngle], endAngle]
                        '''
                        cv2.ellipse(vis, track_box, (0, 0, 255), 2)   # 绘制红色的椭圆标记
#                         print track_box
                        a = str(track_box[0][0])+" "+str(track_box[0][1])+" "+str(round(track_box[1][0],2))\
                                       +" "+str(round(track_box[1][1],2))+" "+str(round(track_box[2],2))+"\r\n"
                        print a
#                         self.ser.write(a)
                    except: print track_box

posted @ 2019-01-24 21:26  Sandrammm  Views(2564)  Comments(0Edit  收藏  举报