1 SIFT
图像纹理是一个非常重要的特征维度。SIFT对图像纹理进行计算,提取不同尺度的图像关键点并生成描述子。如角点、边缘点、强对比点。具有旋转、尺度、亮度不变性。
大体步骤:
- 使用高斯模糊+下采样,得到高斯金字塔。注意的是每次下采样都使用相同大小、不同参数的高斯模糊处理,因此高斯金字塔可以分为若干组,每组都有相同大小的若干层。
高斯模糊:使用归一化的高斯核对图像进行卷积(使用分解卷积的方法更快)
- 高斯金字塔的每组内相邻两层相减,得到高斯差分金字塔DoG
- 极值检测,每个点和上下左右9+9+8+26个点比较,找到极值点,并清洗掉不好的特征点(如低对比度、不稳定边缘等)
- 求关键点梯度,然后考察关键点邻域像素梯度和方向,构造梯度直方图,将360°分成每10°一个方向,每个方向一个直方,直方值是该梯度方向的幅值和。
- 构建SIFT描述子。具体过程略,包括采样、旋转、直方图构建。每个关键点对4x4邻域采样,直方图考虑8个方向,于是可以用4x4x8=128维的SIFT特征矢量描述。
简单的sift特征提取和匹配代码如下:
### SIFT特征提取
# 1 将图片转为灰度图
gray= cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
# 2 创建sift特征
sift = cv2.SIFT()
# 3 检测关键点
kps = sift.detect(gray,None)
# 4 计算每个关键点的sift特征
sift.compute(gray,kps)
# 5 在灰度图中画出这些关键点
img=cv2.drawKeypoints(gray,kps)
### SIFT图像匹配
# 1 计算每个待匹配图像的关键点和sift特征
kp1, des1 = sift.detectAndCompute(img1, None)
kp2, des2 = sift.detectAndCompute(img2, None)
# 2 构建匹配器
bf = cv2.BFMatcher()
# 3 匹配
matches = bf.knnMatch(des1, des2, k=2)
# 4 获取能够匹配的关键点
good = [m for m, n in matches if m.distance < 0.5 * n.distance]
# 5 将待匹配的灰度图像、关键点、匹配结果画出来
img3 = cv2.drawMatchesKnn(img1, kp1, img2, kp2, good, None, flags=2)
SIFT计算还是比较复杂的,而且对平滑边缘比较无力。SURF特征对其进行了改进,计算速度大大提高。
2 轮廓
2.1 轮廓提取
轮廓是另一个重要的图像特征。有很多方法来提取轮廓,canny是经典的边缘检测算法。
- 高斯模糊
- 使用边缘差分算子(如sobel)计算梯度,可以得到初步的边缘点
- 非极大值抑制。上边计算出来的结果会得到很粗的边缘,这里进行处理,得到1个像素宽的边缘
- 双阈值对点进行标记,然后计算梯度小的点周围有没有梯度大的,没有则认为是噪声,过滤掉
opencv中一般直接用findcontours提取轮廓。一般流程如下:
# 1 将图像进行高斯模糊去除噪声(模糊后一些轮廓可能连到一起,具体问题具体分析)
dst=cv2.GaussianBlur(img,(3,3),0)
# 2 转化为灰度图像
gray=cv2.cvtColor(dst,cv2.COLOR_RGB2GRAY)
# 3 二值化
# 这里cv2.THRESH_OTSU是采用大津法的二值化方法,和自适应阈值有点像,一般比纯粹的cv2.THRESH_BINARY效果更好
ret,binary=cv2.threshold(gray,0,255,cv2.THRESH_BINARY|cv2.THRESH_OTSU)
# 4 提取轮廓cv2.findContours
# @params
# image:输入的二值图像,提取完轮廓后会改变,所以如果后边还要用到输入图像的话就传进来拷贝
# mode:轮廓的提取模式
# cv2.RETR_EXTERNAL:只提取外轮廓,孔洞的轮廓不计,没有层级
# cv2.RETR_CCOMP:外轮廓一个层级,内轮廓一个层级
# cv2.RETR_LIST:提取所有轮廓,没有层级
# cv2.RETR_TREE:提取所有轮廓,并建立轮廓的树状层级结构
# method:轮廓的近似方法
# cv2.CHAIN_APPROX_NONE:存储所有点
# cv2.CHAIN_APPROX_SIMPLE:存储关键点
# cv2.CHAIN_APPROX_TC89_L1,CV_CHAIN_APPROX_TC89_KCOS:也是关键点,采样方法和SIMPLE不同
# @returns
# image:处理过的二值图像
# contours:提取出来的轮廓list
# hierarchy:轮廓的层次,每个轮廓都有4个值:contours[i]-->hierachy[i][0~4],分别是前一个轮廓、后一个轮廓、父轮廓、内轮廓的索引,没有就是负值
bimg,contours,hieriachy = cv2.findContours(binary,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
# 绘制轮廓(注意只绘制点,所以如果轮廓只存储了关键点而不是所有点,是看不到连起来的轮廓线的
# @params(部分)
# image:绘制的目标图像
# contours:要绘制的轮廓list
# contourIdx:要绘制的轮廓索引,负值则绘制所有轮廓
# color:轮廓颜色
# thickness:轮廓粗细,负值则轮廓内部被填充
cv2.drawContours(bimg, contours, -1, color, thickness)
2.2 轮廓特征
提取出来的轮廓可以进一步处理,如求周长、面积、最包围矩形、最小包围矩形、最小外接圆等等。
# 都是对单个轮廓c的计算
# 单个轮廓是点的list。所以如果算多个轮廓的特征,把点都放到一个list就可以(周长面积不能这么做)
# 周长,True为闭合长度,False为不闭合长度
perimeter = cv2.arcLength(c,True)
# 面积(也叫0阶矩)
area = cv2.contourArea(c)
# 包围矩形AABB的左上顶点、宽高
x, y, w, h = cv2.boundingRect(c)
# 最小包围矩形的中心、宽高、旋转角(单位是度)
rect = cv2.minAreaRect(c)
# 获取最小包围矩形的顶点
box = cv2.boxPoints(rect)
# 最小外接圆的圆心、半径
(x, y), radius = cv2.minEnclosingCircle(c)
# 凸性检测的布尔值
k=cv2.isContourConvex(c)
# 凸包的点
hull = cv2.convexHull(c)
2.3 轮廓矩和轮廓匹配
前面写到面积是0阶矩,实际上可以用M=cv2.moments(c)
计算一个轮廓的各阶矩,返回一个字典。各阶矩之间的运算可以得到很多特征,例如轮廓的重心可以用以下方式求得:
cX = int(M["m10"] / M["m00"])
cY = int(M["m01"] / M["m00"])
Hu矩是归一化中心矩的线性组合,可以用来衡量两个轮廓的相似性,具有平移T、旋转R、尺度S、镜像M不变性。
除了Hu矩以外,常用的还有Zernike矩,也具有RST不变性,它比Hu矩描述了更多细节,因此对一些稍微复杂的轮廓描述能力更强一些,但计算较为复杂。OpenCV中没有Zernike矩的实现,这里不再赘述。
# Hu矩匹配
# @params(部分)
# method:CV_CONTOURS_MATCH_I1, CV_CONTOURS_MATCH_I2, CV_CONTOURS_MATCH_I3,Hu矩匹配的计算参数,不同的值匹配结果略有差别
r = cv2.matchShapes(c1,c2,cv2.cv.CV_CONTOURS_MATCH_I1)
虽然还有轮廓树、成对几何直方图的匹配方法,但相对Hu矩匹配来说效果都要差一些。Hu矩匹配对形状比较复杂、有较多大凹陷的轮廓匹配效果不太好。
2.4 傅里叶描述子Fourier Descriptor
和矩一样,傅里叶描述子也可以作为轮廓的描述子。它的思路就是将轮廓点(x,y)表示为复数s=x+iy,然后对s进行傅里叶变换,取前几个模/第一个模,形成傅里叶描述子。傅里叶描述子具有RST不变性。numpy可以很容易地对数据进行快速傅里叶变换:
# 使用np的fft2对轮廓进行傅里叶变换并求模(取前7阶)
f = np.abs(np.fft.fft(c, 7))
# 计算傅里叶描述子
fd = [i/f[0] for i in f]
3 HOG
方向梯度直方图HOG是物体检测的一种重要特征。HOG+SVM是图像识别的经典算法。HOG实际上是图像中梯度的统计信息。HOG的大体流程如下:
- 灰度化
- gamma校正。这一步是为了避免阴影光照等的影响。不过对于被识别的对象内部纹理变化较为剧烈(如动物的花纹)的则不太适合用gamma校正
- 计算每个像素的梯度。一般都是用检测边缘的核进行卷积
- 图像划分为细胞cell,每个细胞内统计梯度直方图得到描述子
直方图的构建在SIFT中已有过描述,就是360°分成若干个角度区域、统计落在对应角度的梯度,直方的值是落在该角度内梯度的幅值之和
- 几个细胞组成一个块block,每个块内的细胞的描述子归一化后串联起来,得到该块的HOG。block之间是有重叠的
- 图像的所有块的HOG串联起来,得到最终HOG
例如对于一个3x3个细胞的块,直方图维度9通道(9个角度区域)则一块的HOG特征有3x3x9维。
HOG最初是用来做行人检测的,是一个Object Detection的场景。OPENCV提取hog特征时,一般也是先滑窗获取ROI,再对ROI进行HOG特征提取。如果不设置ROI相关参数,就是对整张图片计算HOG了。
# HOG描述子构造函数
# @params(部分)
# winSize:滑窗ROI大小
# blockSize:块大小
# blockStride:块步长
# cellSize:细胞大小
# nbins:直方图维度
hog = cv2.HOGDescriptor()
# HOG提取
# @params(部分)
# img:输入图像
# winStride:滑窗步长(s,s)
hist = hog.compute(img,winStride)
# 提取到的HOG特征hist就可以用于后续的任务
4 LightField Descriptor(LFD)
最后简单提一下3D物体的描述子光场描述子LFD,是对物体进行正20面体投影得到20个2D图像。那么这20个图像就可以用上述的各种方法进行特征提取,得到LFD。当然,在深度学习如此流行的今天,20个图直接喂进CNN效率更高。