特征類型
這圖里的大多數特征 或者說任意圖像的大多數特征,都逃不出三大類邊緣、角點和斑點。
- 邊緣: 圖像強度發生突變的區域,也稱為高強度梯度區域
- 角點:角點則是兩個邊緣相交的地方,起來像是個角或一個尖銳的點
-
斑點:按特征劃分的區域,可能是強度特別高或特別低的區域或是具備獨特紋理的區域
我們最想檢測的就是角點,因為角點是可重復性最高的特征,也就是說因為角點是可重復性最高的特征,給出關于同一景象的兩張或以上圖像 我們就能很輕易地識別出這類特征。
舉一個簡單的例子:
看這張蒙德里安的畫作 來看這三小塊A、B 和 C 有了這三小塊,告訴我它們位于圖像的哪個矩形區域嗎?
A 只是個簡單的色塊,能和許多這樣的矩形區域匹配,由于它不是獨一無二的 所以不是個好特征
B 是邊緣,因為從 B 的方向來看 B 與紅色矩形底部的邊緣相匹配,但我們還是可以左右移動這個邊緣 B 左右都能匹配,我們只能估測這個邊緣在圖像上的大概位置,但很難找出確切位置
C 則是個角點,實際上 C 包含了兩個角點,位置也很容易確定 就在右下角,這是因為角點代表兩個邊緣變化的交點。所以角點是最容易匹配的 是獨一無二的 因此是好特征。
角點檢測
圖像強度變化往往也稱為圖像梯度,要檢測角點 我們也可以靠這類梯度測量法來進行。
每個方向的梯度測量都會有一個幅值 即梯度強度的度量值和表示強度變化的方向。
這些值都能用 Sobel 算子計算出來,Sobel 算子會分別取 x 和 y 方向的強度變化或圖像梯度
這里我繪制出了山峰圖像的這兩個梯度,分別稱之為 Gx 和 Gy 其中 G 是梯度的英文首字母。
這兩張圖看起來和之前的卷積核有點不一樣,因為它們還沒有轉為二進制閥值圖像,這里不需要轉化。
計算出這兩個方向總梯度的幅值和方向,將這些值從圖像空間的xy坐標系轉換成以 ρ 表示幅值 θ 表示方向的極坐標系。
把 Gx 和 Gy 想象成梯度三角形兩邊的長,Gx 是底邊的長 Gy 則是右邊的長,所以梯度的總幅值 ρ 就是三角形的斜線,也就是這兩個梯度和的平方根。而梯度方向 θ則是 Gy 除以 Gx 的正切的倒數
注:下圖有誤 rho = sqrt(Gx^2 + Gy^2)
許多角點檢測器會取一個窗口,在梯度圖像不同區域里上下左右移動這個窗口,一旦遇到角點,窗口就會發現剛才計算出來的梯度方向和幅值有突變而識別出角點的存在。
Harris Corner Detection
復制圖像 將其轉為 RGB 顏色空間
# Import resources and display image
import matplotlib.pyplot as plt
import numpy as np
import cv2
%matplotlib inline
# Read in the image
image = cv2.imread('images/waffle.jpg')
# Make a copy of the image
image_copy = np.copy(image)
# Change color to RGB (from BGR)
image_copy = cv2.cvtColor(image_copy, cv2.COLOR_BGR2RGB)
plt.imshow(image_copy)
角點檢測靠的是強度變化,所以先把圖像轉為灰度圖像,然后將值轉化為浮點型,以便 Hrarris 角點檢測器使用。
接著創建角點檢測器 Harris
該函數需要輸入的參數有灰度浮點值、以及檢測潛在角點所需觀察的相鄰像素大小,2 表示 2 乘 2 像素方塊(由于在這個例子中 角點很明顯,所以這樣的小窗口就夠用了);然后輸入 Sobel 算子的大小,3 也就是典型的核大小。最后輸入一個常數 以便確定哪些點會被視為角點,通常設為 0.04,如果這個常數設得稍微小一些 那檢測出來的角點就會多一些。
函數的輸出圖像命名為 dst,這個圖像會把角點標亮,非角點則會標為較暗的像素,實際上我們很難看到這張圖里標亮的角點,所以我要再加一步操作來處理這些角點,這一步叫角點膨脹。使用 OpenCV 的函數 dilate 將其應用到檢測出來的角點上,在計算機視覺里 膨脹會放大明亮的區域,或是位于前景的區域 比如這些角點 以便我們更清楚地觀察它們。
# Convert to grayscale
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)
plt.imshow(dst, cmap='gray')
Extract and display strong corners
要選出最明亮的角點 我得定義一個閥值 以便角點通過,但這里我要設一個較低的閥值,也就是至少為最大角點檢測值的十分之一
- 創建圖像副本以便繪制角點
- 如果角點大于我們定義的閥值,那就把它繪制在副本上
- 在圖像副本上用小綠圈畫出強角點
- 可以看到多數角點都被檢測出來了,實際上少了幾個角點,可以把閥值調低試試,把閥值減少至角點最大值的 1% 再次將結果繪制出來
##Define a threshold for extracting strong corners
# This value vary depending on the image and how many corners you want to detect
# 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)
熟悉了一些簡單的特征類型,如何通過使用這些特征將圖像的不同部分組合在一起。
將圖像分組或分割成不同的部分稱為圖像分割。
圖像分割的最簡單情況是背景減法。在視頻和其他應用中,通常情況是人必須與靜態或移動背景隔離,因此我們必須使用分割方法來區分這些區域。圖像分割還用于各種復雜的識別任務,例如在對道路圖像中的每個像素進行分類時。
我們將介紹幾種分割圖像的方法:
- 使用輪廓繪制圖像不同部分的邊界
- 通過一些顏色或紋理相似性的度量來聚類圖像數據
圖像描廓(Image Contours)
邊緣檢測算法常用于檢測物體邊界,但檢測出來的邊緣往往不僅是物體邊界,還涉及一些有趣的特征和線條。而要進行圖像分割,要的只是那些完整的閉合邊界,因為這類邊界能切實標識出特定的圖像區域和物體,圖像描廓就可以實現這一點。
圖像輪廓就是位于已知邊界上的邊緣所形成的連續曲線,因此輪廓可用于圖像分割,能提供大量關于物體邊界形狀的信息。
在 OpenCV 里 如果物體是白色的 背景是黑色的,就可以得到最好的輪廓檢測效果。所以在識別圖像輪廓之前,我們要先為圖像創建二進制閥值,這樣才能用黑白像素將圖像里不同的物體區分開來,然后我們用這些物體的邊緣來形成輪廓。這種二值圖像通常只由一個閥值生成 ,或由 Canny 邊緣檢測器生成。
# Import resources and display image
import numpy as np
import matplotlib.pyplot as plt
import cv2
%matplotlib inline
# Read in the image
image = cv2.imread('images/thumbs_up_down.jpg')
# Change color to RGB (from BGR)
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
plt.imshow(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)
plt.imshow(binary, cmap='gray')
找到并畫出輪廓
CV 的函數 findContours,該函數要輸入的參數有我們的二值圖像、輪廓檢索模式這里用的是樹模式,以及輪廓近似方法 這里我就設為簡單的鏈近似了.
函數會輸出輪廓列表和輪廓層級,如果你有諸多輪廓彼此嵌套 那這個層級就能派上大用場,層級定義了輪廓之間的關系,詳情請見文檔
繪制輪廓, OpenCV 的函數 drawContours,輸入的參數有圖像副本,輪廓列表以及要顯示的輪廓,-1 指的是所有輪廓,輸入輪廓的顏色和大小。
# 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)
輪廓特征
每個輪廓都有許多可以計算的特征,包括輪廓的面積,它的方向(大部分輪廓指向的方向),它的周長,以及OpenCV documentation, here.中概述的許多其他屬性。
方向:
對象的方向是對象指向的角度。 要找到輪廓的角度,首先應找到適合輪廓的橢圓,然后從該形狀中提取角度。
# Fit an ellipse to a contour and extract the angle from that ellipse
(x,y), (MA,ma), angle = cv2.fitEllipse(selected_contour)
這些取向值以度為單位,從x軸測量。 值為零表示平直線,值為90表示輪廓指向直線!
因此,每個輪廓計算的方向角應該能夠告訴我們關于手的一般位置的信息。 用拇指向上的手應該比用拇指向下的手更高(接近90度)。
TODO: Find the orientation of each contour
## TODO: Complete this function so that
## it returns the orientations of a list of contours
## The list should be in the same order as the contours
## i.e. the first angle should be the orientation of the first contour
def orientations(contours):
"""
Orientation
:param contours: a list of contours
:return: angles, the orientations of the contours
"""
# Create an empty list to store the angles in
# Tip: Use angles.append(value) to add values to this list
angles = []
for selected_contour in contours:
(x,y), (MA,ma), angle = cv2.fitEllipse(selected_contour)
angles.append(angle)
return angles
# ---------------------------------------------------------- #
# Print out the orientation values
angles = orientations(contours)
print('Angles of each contour (in degrees): ' + str(angles))
邊界矩形-Bounding Rectangle
# Find the bounding rectangle of a selected contour
x,y,w,h = cv2.boundingRect(selected_contour)
# Draw the bounding rectangle as a purple box
box_image = cv2.rectangle(contours_image, (x,y), (x+w,y+h), (200,0,200),2)
#要裁剪圖像,請選擇要包含的圖像的正確寬度和高度。
# Crop using the dimensions of the bounding rectangle (x, y, w, h)
cropped_image = image[y: y + h, x: x + w]
TODO: Crop the image around a contou
## TODO: Complete this function so that
## it returns a new, cropped version of the original image
def left_hand_crop(image, selected_contour):
"""
Left hand crop
:param image: the original image
:param selectec_contour: the contour that will be used for cropping
:return: cropped_image, the cropped image around the left hand
"""
## TODO: Detect the bounding rectangle of the left hand contour
x,y,w,h = cv2.boundingRect(selected_contour)
## TODO: Crop the image using the dimensions of the bounding rectangle
# Make a copy of the image to crop
cropped_image = np.copy(image)
cropped_image = cropped_image[y: y + h, x: x + w]
return cropped_image
## TODO: Select the left hand contour from the list
## Replace this value
selected_contour = contours[1]
# ---------------------------------------------------------- #
# If you've selected a contour
if(selected_contour is not None):
# Call the crop function with that contour passed in as a parameter
cropped_image = left_hand_crop(image, selected_contour)
plt.imshow(cropped_image)
K-means 聚類
有種常用的圖像分割技術 叫 k 均值聚類,方法是把具相似特征的數據點聚類或分組到一起。
我們來看一個簡單的例子 更具體地探討 k 均值
這張圖很小 只有 34 乘 34 像素 是彩虹的一部分,我要用 k 均值 根據顏色將這張圖分為三簇
首先 我們知道這張圖里的每個像素都有一個 RGB 值,將各像素值當作 RGB 顏色空間的數據點繪制出來。
如果我讓 k 均值將這些圖像數據分成三簇,那么 k 均值就會觀察這些像素值 隨機猜測三個 RGB 點 將數據分成三簇。
- k 均值會分別取各簇所有 RGB 值的實際平均數 也就是均值,然后將三個中心點更新為相對應的均值
- 將之前猜測出來的中心點移動到簇均值的位置上
- 重復這個過程,根據調整后的新中心點
形成新簇然后再次計算簇均值 更新均值隨后再次更新中心點 - 基本上 每次迭代后 中心點的移動幅度都會變小,算法會不斷重復這個步驟直至收斂, 而收斂程度是由我們定義的:比如 10 次或根據每次迭代后中心點的移動幅度來確定是否要收斂
import numpy as np
import matplotlib.pyplot as plt
import cv2
%matplotlib inline
# Read in the image
## TODO: Check out the images directory to see other images you can work with
# And select one!
image = cv2.imread('images/monarch.jpg')
# Change color to RGB (from BGR)
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
plt.imshow(image)
- 重塑這張圖像使其變成一個二維數組 以便輸入 k 均值算法:這樣的數組維數應該是 m 乘 3,m 指像素數 3 則指顏色通道的數目。
- 將這些值轉為浮點型
# Reshape image into a 2D array of pixels and 3 color values (RGB)
pixel_vals = image.reshape((-1,3))
# Convert to float type
pixel_vals = np.float32(pixel_vals)
- 用函數 cv2.kmeans,該函數需要輸入的參數有我們剛創建的 m 乘 3 像素值數組、k 值 這里初始設為 2,還有我們想要的標簽但這里不需要 所以寫 none,還有終止條件,然后是迭代次數;然后是迭代次數。
- 標準要在調用這個函數之前定義它,標準會告訴算法何時應終止,這里用 ε 值或迭代最大次數來定義標準,迭代最大次數設為 10 而 ε 這個值我們曾略略提過,也就是在經過幾次迭代后 若簇移動的范圍小于該值則算法終止。
- 要顯示分割情況,需要將數據重新轉成一張 8 bit 的圖像,還要重塑分割好的數據 使其變回圖像副本原本的形狀
# define stopping criteria
# you can change the number of max iterations for faster convergence!
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1)
## TODO: Select a value for k
# then perform k-means clustering
k = 2
retval, labels, centers = cv2.kmeans(pixel_vals, k, None, criteria, 10, cv2.KMEANS_RANDOM_CENTERS)
# convert data into 8-bit values
centers = np.uint8(centers)
segmented_data = centers[labels.flatten()]
# reshape data into the original image dimensions
segmented_image = segmented_data.reshape((image.shape))
labels_reshape = labels.reshape(image.shape[0], image.shape[1])
plt.imshow(segmented_image)
實際上我可以把簇標簽可視化,需將他們逐一呈現 就像用掩膜一樣,來看等于 1 的標簽
## TODO: Visualize one segment, try to find which is the leaves, background, etc!
plt.imshow(labels_reshape==1, cmap='gray')
甚至還可以利用這些信息來對這部分圖像進行掩膜處理
# mask an image segment by cluster
cluster = 0 # the first cluster
masked_image = np.copy(image)
# turn the mask green!
masked_image[labels_reshape == cluster] = [0, 0, 0]
plt.imshow(masked_image)