深度学习以外的视觉算法
尽管深度学习为基础的计算机视觉技术攻克了很多传统算法的难题,但了解这些传统的视觉算法依然是有必要的,因为它们提供了很多不同的理解计算机视觉的视角。并且相比于深度神经网络来说,计算机视觉的很多算法学起来还是很有挑战性的,但是如果你真正花时间去攻克它们,就会发现这些算法背后如此的巧夺天工,以至于你会由衷的为这些算法的发明者的智慧发出赞叹!
Quote from www.learnopencv.com
A lot many things look difficult and mysterious. But once you take the time to deconstruct them, the mystery is replaced by mastery and that is what we are after. If you are a beginner and are finding Computer Vision hard and mysterious, just remember the following
Q : How do you eat an elephant ?
A : One bite at a time!
Spacial Coherent Data
Computer Vision 这门学科处理的信号不仅仅包含传统意义上的相机产生的照片信息,还包括其他类型的传感器产生的信息,包括声学信息,这些信息在空间之内传播和变化,因此被称为 Spacial Coherent Data,通过对这些信息进行捕捉和感知,可以让机器获得空间感知能力。CV 的几个典型的应用场景包括自动驾驶、医学影像分析、图像标注和人脸识别等等。
Cognitive and Emotional Intelligence
认知智能 Cognitive Intelligence 即通常所说的 IQ,情感智能 Emotional Intelligence 即通常所说的 EQ,通过计算机视觉技术 Affectiva 使得计算机可以识别与之交互的人类的面部表情和肢体语言,进而了解此时与之交互的人的情感状态,从而建立更加丰富的情感交互能力。
计算机视觉应用的通用工作流
对于上述表情功能的实现,以及任何一个计算机视觉任务,通常都需要经过以下几个大致的过程:
输入数据的预处理:包括降噪、缩放、和变更颜色空间等,其最重要的目的在于对于多个输入图片的标准化 Standardization
定位目标区域:目标检测和图像分割,例如在情感识别中,需要定位面部重要的特征点
特征提取:提取特征数据,例如下文中提到的 HOG,FAST 等
目标识别:对于新的输入执行目标识别和特征匹配,进而对于被检测的对象的状态给予一个预测
图像的表征
在计算机的世界里,图像通常被表示为一系列的网格状的数字(像素)矩阵,这一表示形式是大多数图像处理技术的基础。我们可以通过坐标位置来确定某个像素点的位置,并通过更改该点的像素的值来更改图像的显示样式。
色彩空间 Color Spaces
同人类理解世界一样,对于计算机来说同样有“知识的表示形式决定了学习的难易程度”,为了更好的表示图像信息,除了常用的 RGB 通道外,还有两种常用的颜色空间表示方法:
RGB:Red,Green,Blue,这个空间中的 RGB 分布取值范围都在 [0, 255],呈均匀分布
HSV: 色调 Hue,饱和度 Saturation,亮度 Value,这个空间中的颜色分布呈现为一个圆柱体,在不同的软件中这三者的取值范围不同,在使用中需要注意转换。在 OpenCV 中 Hue 通道的取值范围为 [0, 179],Saturation 通道的取值范围是 [0, 255],亮度 Value 通道的取值范围为 [0, 255]。由于色调通道在不同的光照条件下变化范围不大,而亮度通道则在不同的光照条件下变化明显,因此可以通过调整色调通道的值来更好的选择目标区域而避免光照条件的影响。
HLS:Hue, Lightness, Saturation
由于在大部分计算机视觉应用中,光照条件对于算法的识别能力是有影响的,因此后两种颜色空间表示方法考虑了亮度信息,因此可以用于图像的光照条件的分辨。
通过 OpenCV 将图片从 RGB 空间转到 HSV 空间的代码如下:
cv2.cvtColor(input_image, cv2.COLOR_BGR2HSV)
这里需要注意的是在 OpenCV 中默认的颜色顺序是 BGR 而非 RGB,如果需要做这一转换,则可以通过如下代码进行:
cv2.cvtColor(input_image, cv2.COLOR_BGR2RGB)
由于大部分人对于 RGB 空间的色彩更熟悉,所以如果想知道某一个 RGB 空间下的色彩对应的 HSV 的值,可以通过如下代码来实现:
green = np.uint8([[[0,255,0 ]]])
hsv_green = cv2.cvtColor(green, cv2.COLOR_BGR2HSV)
print(hsv_green)
目标区域的选取
在既定的色彩空间下,可以通过 cv2.inRange()
选择取值在一定范围内的像素来对目标区域进行选取,并且由于图像被以数组的形式进行存储,因此可以通过简单的数组加法来对多个图像进行叠加。由于纯色的背景更加容易通过这一操作来编辑,因此在 Udacity 的课程中讲者背后的背景通常都是蓝色的。
# Define the masked area
lower_blue = np.array([0, 0, 230])
upper_blue = np.array([50, 50, 255])
mask = cv2.inRange(image_copy, lower_blue, upper_blue)
# Mask the image to let the pizza show through
masked_image = np.copy(image_copy)
masked_image[mask != 0] = [0, 0, 0]
# Display it!
plt.imshow(masked_image)
边缘检测和图像信息的过滤
在图像中除了简单的 RGB 或 HSV 通道信息外,图像本身的模式变化也是一个非常有用的信息,例如我们在图像中看到的物体和背景的边缘 Edges,其最明显的特征就是边缘两侧具有明显的色彩强度 Intensity 变化。
Intensity is a measure of light and dark similar to brightness, and we can use this knowledge to detect other areas or objects of interest. For example, you can often identify the edges of an object by looking at an abrupt change in intensity which happens when an image changes from a very dark to light area.
进一步地,在很多情况下图像中的模式变化是有一定规律可循的,衡量这一变化模式的一个重要指标就是变化的频率,而研究频率最为有效的办法之一则是伟大的傅立叶变换。所谓的高频图像或者图像当中的高频部分就是指其强度数值变化频率较高的部分,而低频图像或者图片中的低频部分则是指图片上信息一致或者渐变的部分。
Similar to sound, frequency in images is a rate of change. But, what does it means for an image to change? Well, images change in space, and a high frequency image is one where the intensity changes a lot. And the level of brightness changes quickly from one pixel to the next. A low frequency image may be one that is relatively uniform in brightness or changes very slowly. Most images have both high-frequency and low-frequency components.
The Fourier Transform (FT) is an important image processing tool which is used to decompose an image into its frequency components. The output of an FT represents the image in the frequency domain, while the input image is the spatial domain (x, y) equivalent. In the frequency domain image, each point represents a particular frequency contained in the spatial domain image. So, for images with a lot of high-frequency components (edges, corners, and stripes), there will be a number of points in the frequency domain at high frequency values.
通过傅立叶变换将图像上的信息分解为不同的频率组成部分之后,就可以为了过滤掉不需要的信息或者突出感兴趣的信息而使用不同类型的滤波器 Filter 来对图像进行处理。
在图像处理的语境中,高通滤波器 High-pass filter 的目的在于锐化 Sharpen 图像中的高频部分,其通过类似 Sobel 滤波器(每一个滤波器只检测一个方向的边缘)这样的边缘检测卷积核来实现。而低通滤波器则主要用于降噪 Denoising 或图像模糊 Blur,其通过平均化卷积核区域内的所有像素的信息,如 Gaussian Blur 来实现。
# High pass filter further intensify the center element if it already stand out
# All elements should add up to 0
high_pass = np.array([[-1, -1, -1],
[-1, 8, 1],
[-1, -1, -1]])
high_pass
array([[-1, -1, -1],
[-1, 8, 1],
[-1, -1, -1]])
# Low pass filter averaging and smoothing the target area
low_pass = np.ones((3, 3)) / 9
low_pass
array([[ 0.11111111, 0.11111111, 0.11111111],
[ 0.11111111, 0.11111111, 0.11111111],
[ 0.11111111, 0.11111111, 0.11111111]])
我们所讲的滤波器 Filter 在很多情况下又被称为核 kernel,通过采用特定的核和滤波器对于图像进行过滤操作,可以突出图像中感兴趣的部分而忽略不感兴趣的部分。在实际使用中,通常先将图片进行低通滤波,再进行高通滤波以避免产生无意义的干扰信息 Noise。
A kernel is a set of weights that are applied to a region in a source image to generate a single pixel in the destination image.
对于执行边缘检测的核(高通)来说,由于需要通过计算的结果来反应图像上每一个位置的强度变化情况,因此每一个核中的数字加总的结果应该是 0,此时如果在某一个位置得到的结果为 0,则意味这在这个地区没有强度变化,也即没有边缘过渡。核中的参数加总不为 0 的操作会对图像进行加亮或暗化。由计算过程可知,执行边缘检测的过滤计算的结果的大小即代表了核覆盖的区域内的边缘过渡是否强烈,结果越大则边缘越明显,而核中的数字也称为权重的原因正是其决定了相应位置的像素在最终计算结果中的权重值。
Canny Edge Detection
在边缘检测过程中,常见的两个问题是:
多大程度的强度改变应该被定义为一个边缘?
我们如何能够一致性的展示出细微和宽厚的边缘?
这就要求我们能够拥有更加高性能的边缘检测器 - Canny Edge Detection,其实现过程如下:
通过 Gaussian Blur 来完成低通滤波
通过多个 Sobel 滤波器来识别出边缘的粗细和方向
采用非极大抑制来分离出较粗的线条,并将它们以一个像素的宽度来重新展示,经过处理后的图像是一个 binary image
Use Hysteresis to isolate the best edges,在选择的过程中需要设定一个强度梯度的最小和最大的阈值,并且只保留处于最低阈值之上的与其他确定边缘连接的线条,以保留有意义的边界信息并去掉可能的噪声,其中阈值最小和最大的值的比例一般选择 1:2 或 1:3
gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
lower = 100
upper = 300
edges = cv2.Canny(gray, lower, upper)
plt.imshow(edges, cmap='gray')
在建立的基本的边缘检测工具后,下一步就是要了解如果进一步地在图像中检测特定的形状。
Hough Transform
Hough Transform 是一个非常重要的形状检测工具,这个方法的实现原理比较复杂,尝试理解如下:
在直角坐标系下的一条直线可以被表示为 y = mx + b,相应的这条直线在极坐标系下则可以用一个点(ρ, θ)来表示,其中 ρ 代表原点到这条直线的距离,而 θ 则表示这条直线和 x 轴的夹角。并且对于指定的点 (x, y),如果再指定直线与坐标轴的夹角 θ,则 ρ 可以通过 ρ = xcosθ + ysinθ 来唯一确定。由于在直角坐标系下通过直线上某个点可以绘制出无数条直线,这些直线所对应的极坐标下的点则构成一条正弦曲线。在实际检测过程中,算法将建立一个二维的数组,称作 Accumulator,其横轴为 ρ ,纵轴为 θ,对于任意一个点 (x, y) 和它相邻的点,算法会计算是否在这个点附近有直线存在。
在实际使用中,在执行 Hough 变换之前需要将图像转化为灰度图像,并通过 Canny 边缘检测器进行滤波,函数的输入结果为 binary image。
gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
# Define our parameters for Canny
low_threshold = 50
high_threshold = 100
edges = cv2.Canny(gray, low_threshold, high_threshold)
# Parameters for Hough Transform
rho = 1
theta = np.pi/180
threshold = 60
min_line_length = 100
max_line_gap = 5
# Creating an image copy to draw lines on
line_image = np.copy(image)
# Run Hough on the edge-detected image
lines = cv2.HoughLinesP(edges, rho, theta, threshold, np.array([]),
min_line_length, max_line_gap)
# Iterate over the output "lines" and draw lines on the image copy
for line in lines:
for x1,y1,x2,y2 in line:
cv2.line(line_image, (x1, y1), (x2, y2), (255, 0, 0), 5)
plt.imshow(line_image)
Haar Cascade Face Detection
Haar Cascade 算法通过一系列包含和不包含脸部的图片来完成训练,这个利用 Haar feature 进行脸部识别的算法在计算的过程中会逐步消除掉判断为非脸部的图片位置,进而减小检测范围,因此算法性能十分高效,可以实时进行面部检测。
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# load in cascade classifier
face_cascade = cv2.CascadeClassifier('detector_architectures/haarcascade_frontalface_default.xml')
# run the detector on the grayscale image
faces = face_cascade.detectMultiScale(gray, 4, 6)
img_with_detections = np.copy(image) # make a copy of the original image to plot rectangle detections ontop of
# loop over our detections and draw their corresponding boxes on top of our original image
for (x, y, w, h) in faces:
# draw next detection as a red rectangle on top of the original image.
# Note: the fourth element (255,0,0) determines the color of the rectangle,
# and the final argument (here set to 5) determines the width of the drawn rectangle
cv2.rectangle(img_with_detections, (x, y), (x+w, y+h), (255, 0, 0), 5)
Corner Detection
在很多情况下,图像中的成角部分能够提供很多特别有效的特征信息,在 OpenCV 中一个较为常用的方法是 cv2.cornerHarris。
gray = cv2.cvtColor(image_copy, cv2.COLOR_RGB2GRAY)
gray = np.float32(gray)
# Detect corners
dst = cv2.cornerHarris(gray, 2, 3, 0.04)
# Dilate corner image to enhance corner points
dst = cv2.dilate(dst, None)
# Try changing this free parameter, 0.1, to be larger or smaller ans see what happens
thresh = 0.1*dst.max()
# Create an image copy to draw corners on
corner_image = np.copy(image_copy)
# Iterate through all the corners and draw them on the image (if they pass the threshold)
for j in range(0, dst.shape[0]):
for i in range(0, dst.shape[1]):
if(dst[j,i] > thresh):
# image, center pt, radius, color, thickness
cv2.circle( corner_image, (i, j), 1, (0, 255, 0), 1)
plt.imshow(corner_image)
Image Segmentation
在完成了前述一系列的基本特征检测以后,一个重要的应用就是对图像进行分割,在图像分割之前需要进一步完成的是轮廓提取(在 OpenCV 中当背景是黑色,而物体是白色时最容易实现轮廓提取,这就是为何很多处理算法的结果都是 binary image)。
# Convert to grayscale
gray = cv2.cvtColor(image,cv2.COLOR_RGB2GRAY)
# Create a binary thresholded image
retval, binary = cv2.threshold(gray, 225, 255, cv2.THRESH_BINARY_INV)
# Find contours from thresholded, binary image
retval, contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
# Draw all contours on a copy of the original image
contours_image = np.copy(image)
contours_image = cv2.drawContours(contours_image, contours, -1, (0,255,0), 3)
plt.imshow(contours_image)