深度學(xué)習(xí)以外的視覺算法
盡管深度學(xué)習(xí)為基礎(chǔ)的計(jì)算機(jī)視覺技術(shù)攻克了很多傳統(tǒng)算法的難題,但了解這些傳統(tǒng)的視覺算法依然是有必要的,因?yàn)樗鼈兲峁┝撕芏嗖煌睦斫庥?jì)算機(jī)視覺的視角。并且相比于深度神經(jīng)網(wǎng)絡(luò)來說,計(jì)算機(jī)視覺的很多算法學(xué)起來還是很有挑戰(zhàn)性的,但是如果你真正花時(shí)間去攻克它們,就會發(fā)現(xiàn)這些算法背后如此的巧奪天工,以至于你會由衷的為這些算法的發(fā)明者的智慧發(fā)出贊嘆!
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 這門學(xué)科處理的信號不僅僅包含傳統(tǒng)意義上的相機(jī)產(chǎn)生的照片信息,還包括其他類型的傳感器產(chǎn)生的信息,包括聲學(xué)信息,這些信息在空間之內(nèi)傳播和變化,因此被稱為 Spacial Coherent Data,通過對這些信息進(jìn)行捕捉和感知,可以讓機(jī)器獲得空間感知能力。CV 的幾個(gè)典型的應(yīng)用場景包括自動(dòng)駕駛、醫(yī)學(xué)影像分析、圖像標(biāo)注和人臉識別等等。
Cognitive and Emotional Intelligence
認(rèn)知智能 Cognitive Intelligence 即通常所說的 IQ,情感智能 Emotional Intelligence 即通常所說的 EQ,通過計(jì)算機(jī)視覺技術(shù) Affectiva 使得計(jì)算機(jī)可以識別與之交互的人類的面部表情和肢體語言,進(jìn)而了解此時(shí)與之交互的人的情感狀態(tài),從而建立更加豐富的情感交互能力。
計(jì)算機(jī)視覺應(yīng)用的通用工作流
對于上述表情功能的實(shí)現(xiàn),以及任何一個(gè)計(jì)算機(jī)視覺任務(wù),通常都需要經(jīng)過以下幾個(gè)大致的過程:
輸入數(shù)據(jù)的預(yù)處理:包括降噪、縮放、和變更顏色空間等,其最重要的目的在于對于多個(gè)輸入圖片的標(biāo)準(zhǔn)化 Standardization
定位目標(biāo)區(qū)域:目標(biāo)檢測和圖像分割,例如在情感識別中,需要定位面部重要的特征點(diǎn)
特征提取:提取特征數(shù)據(jù),例如下文中提到的 HOG,F(xiàn)AST 等
目標(biāo)識別:對于新的輸入執(zhí)行目標(biāo)識別和特征匹配,進(jìn)而對于被檢測的對象的狀態(tài)給予一個(gè)預(yù)測
圖像的表征
在計(jì)算機(jī)的世界里,圖像通常被表示為一系列的網(wǎng)格狀的數(shù)字(像素)矩陣,這一表示形式是大多數(shù)圖像處理技術(shù)的基礎(chǔ)。我們可以通過坐標(biāo)位置來確定某個(gè)像素點(diǎn)的位置,并通過更改該點(diǎn)的像素的值來更改圖像的顯示樣式。
色彩空間 Color Spaces
同人類理解世界一樣,對于計(jì)算機(jī)來說同樣有“知識的表示形式?jīng)Q定了學(xué)習(xí)的難易程度”,為了更好的表示圖像信息,除了常用的 RGB 通道外,還有兩種常用的顏色空間表示方法:
RGB:Red,Green,Blue,這個(gè)空間中的 RGB 分布取值范圍都在 [0, 255],呈均勻分布
HSV: 色調(diào) Hue,飽和度 Saturation,亮度 Value,這個(gè)空間中的顏色分布呈現(xiàn)為一個(gè)圓柱體,在不同的軟件中這三者的取值范圍不同,在使用中需要注意轉(zhuǎn)換。在 OpenCV 中 Hue 通道的取值范圍為 [0, 179],Saturation 通道的取值范圍是 [0, 255],亮度 Value 通道的取值范圍為 [0, 255]。由于色調(diào)通道在不同的光照條件下變化范圍不大,而亮度通道則在不同的光照條件下變化明顯,因此可以通過調(diào)整色調(diào)通道的值來更好的選擇目標(biāo)區(qū)域而避免光照條件的影響。
HLS:Hue, Lightness, Saturation
由于在大部分計(jì)算機(jī)視覺應(yīng)用中,光照條件對于算法的識別能力是有影響的,因此后兩種顏色空間表示方法考慮了亮度信息,因此可以用于圖像的光照條件的分辨。
通過 OpenCV 將圖片從 RGB 空間轉(zhuǎn)到 HSV 空間的代碼如下:
cv2.cvtColor(input_image, cv2.COLOR_BGR2HSV)
這里需要注意的是在 OpenCV 中默認(rèn)的顏色順序是 BGR 而非 RGB,如果需要做這一轉(zhuǎn)換,則可以通過如下代碼進(jìn)行:
cv2.cvtColor(input_image, cv2.COLOR_BGR2RGB)
由于大部分人對于 RGB 空間的色彩更熟悉,所以如果想知道某一個(gè) RGB 空間下的色彩對應(yīng)的 HSV 的值,可以通過如下代碼來實(shí)現(xiàn):
green = np.uint8([[[0,255,0 ]]])
hsv_green = cv2.cvtColor(green, cv2.COLOR_BGR2HSV)
print(hsv_green)
目標(biāo)區(qū)域的選取
在既定的色彩空間下,可以通過 cv2.inRange() 選擇取值在一定范圍內(nèi)的像素來對目標(biāo)區(qū)域進(jìn)行選取,并且由于圖像被以數(shù)組的形式進(jìn)行存儲,因此可以通過簡單的數(shù)組加法來對多個(gè)圖像進(jìn)行疊加。由于純色的背景更加容易通過這一操作來編輯,因此在 Udacity 的課程中講者背后的背景通常都是藍(lán)色的。

# 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 通道信息外,圖像本身的模式變化也是一個(gè)非常有用的信息,例如我們在圖像中看到的物體和背景的邊緣 Edges,其最明顯的特征就是邊緣兩側(cè)具有明顯的色彩強(qiáng)度 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.
進(jìn)一步地,在很多情況下圖像中的模式變化是有一定規(guī)律可循的,衡量這一變化模式的一個(gè)重要指標(biāo)就是變化的頻率,而研究頻率最為有效的辦法之一則是偉大的傅立葉變換。所謂的高頻圖像或者圖像當(dāng)中的高頻部分就是指其強(qiáng)度數(shù)值變化頻率較高的部分,而低頻圖像或者圖片中的低頻部分則是指圖片上信息一致或者漸變的部分。
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 來對圖像進(jìn)行處理。
在圖像處理的語境中,高通濾波器 High-pass filter 的目的在于銳化 Sharpen 圖像中的高頻部分,其通過類似 Sobel 濾波器(每一個(gè)濾波器只檢測一個(gè)方向的邊緣)這樣的邊緣檢測卷積核來實(shí)現(xiàn)。而低通濾波器則主要用于降噪 Denoising 或圖像模糊 Blur,其通過平均化卷積核區(qū)域內(nèi)的所有像素的信息,如 Gaussian Blur 來實(shí)現(xiàn)。
# 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,通過采用特定的核和濾波器對于圖像進(jìn)行過濾操作,可以突出圖像中感興趣的部分而忽略不感興趣的部分。在實(shí)際使用中,通常先將圖片進(jìn)行低通濾波,再進(jìn)行高通濾波以避免產(chǎn)生無意義的干擾信息 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.
對于執(zhí)行邊緣檢測的核(高通)來說,由于需要通過計(jì)算的結(jié)果來反應(yīng)圖像上每一個(gè)位置的強(qiáng)度變化情況,因此每一個(gè)核中的數(shù)字加總的結(jié)果應(yīng)該是 0,此時(shí)如果在某一個(gè)位置得到的結(jié)果為 0,則意味這在這個(gè)地區(qū)沒有強(qiáng)度變化,也即沒有邊緣過渡。核中的參數(shù)加總不為 0 的操作會對圖像進(jìn)行加亮或暗化。由計(jì)算過程可知,執(zhí)行邊緣檢測的過濾計(jì)算的結(jié)果的大小即代表了核覆蓋的區(qū)域內(nèi)的邊緣過渡是否強(qiáng)烈,結(jié)果越大則邊緣越明顯,而核中的數(shù)字也稱為權(quán)重的原因正是其決定了相應(yīng)位置的像素在最終計(jì)算結(jié)果中的權(quán)重值。
Canny Edge Detection
在邊緣檢測過程中,常見的兩個(gè)問題是:
多大程度的強(qiáng)度改變應(yīng)該被定義為一個(gè)邊緣?
我們?nèi)绾文軌蛞恢滦缘恼故境黾?xì)微和寬厚的邊緣?
這就要求我們能夠擁有更加高性能的邊緣檢測器 - Canny Edge Detection,其實(shí)現(xiàn)過程如下:
通過 Gaussian Blur 來完成低通濾波
通過多個(gè) Sobel 濾波器來識別出邊緣的粗細(xì)和方向
采用非極大抑制來分離出較粗的線條,并將它們以一個(gè)像素的寬度來重新展示,經(jīng)過處理后的圖像是一個(gè) binary image
Use Hysteresis to isolate the best edges,在選擇的過程中需要設(shè)定一個(gè)強(qiáng)度梯度的最小和最大的閾值,并且只保留處于最低閾值之上的與其他確定邊緣連接的線條,以保留有意義的邊界信息并去掉可能的噪聲,其中閾值最小和最大的值的比例一般選擇 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')
在建立的基本的邊緣檢測工具后,下一步就是要了解如果進(jìn)一步地在圖像中檢測特定的形狀。
Hough Transform
Hough Transform 是一個(gè)非常重要的形狀檢測工具,這個(gè)方法的實(shí)現(xiàn)原理比較復(fù)雜,嘗試?yán)斫馊缦拢?/p>
在直角坐標(biāo)系下的一條直線可以被表示為 y = mx + b,相應(yīng)的這條直線在極坐標(biāo)系下則可以用一個(gè)點(diǎn)(ρ, θ)來表示,其中 ρ 代表原點(diǎn)到這條直線的距離,而 θ 則表示這條直線和 x 軸的夾角。并且對于指定的點(diǎn) (x, y),如果再指定直線與坐標(biāo)軸的夾角 θ,則 ρ 可以通過 ρ = xcosθ + ysinθ 來唯一確定。由于在直角坐標(biāo)系下通過直線上某個(gè)點(diǎn)可以繪制出無數(shù)條直線,這些直線所對應(yīng)的極坐標(biāo)下的點(diǎn)則構(gòu)成一條正弦曲線。在實(shí)際檢測過程中,算法將建立一個(gè)二維的數(shù)組,稱作 Accumulator,其橫軸為 ρ ,縱軸為 θ,對于任意一個(gè)點(diǎn) (x, y) 和它相鄰的點(diǎn),算法會計(jì)算是否在這個(gè)點(diǎn)附近有直線存在。
在實(shí)際使用中,在執(zhí)行 Hough 變換之前需要將圖像轉(zhuǎn)化為灰度圖像,并通過 Canny 邊緣檢測器進(jìn)行濾波,函數(shù)的輸入結(jié)果為 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 算法通過一系列包含和不包含臉部的圖片來完成訓(xùn)練,這個(gè)利用 Haar feature 進(jìn)行臉部識別的算法在計(jì)算的過程中會逐步消除掉判斷為非臉部的圖片位置,進(jìn)而減小檢測范圍,因此算法性能十分高效,可以實(shí)時(shí)進(jìn)行面部檢測。
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 中一個(gè)較為常用的方法是 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
在完成了前述一系列的基本特征檢測以后,一個(gè)重要的應(yīng)用就是對圖像進(jìn)行分割,在圖像分割之前需要進(jìn)一步完成的是輪廓提?。ㄔ?OpenCV 中當(dāng)背景是黑色,而物體是白色時(shí)最容易實(shí)現(xiàn)輪廓提取,這就是為何很多處理算法的結(jié)果都是 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)
