本文主要內容來自於 OpenCV-Python 教程 的 OpenCV 中的圖像處理 部分,這個部分的主要內容如下:
-
學習在不同色彩空間之間改變圖像。另外學習跟蹤視頻中的彩色對象。
-
學習對圖像應用不同的幾何變換,比如旋轉、平移等。
-
學習使用全局閾值、自適應閾值、Otsu 的二值化等將圖像轉換爲二值圖像。
-
學習模糊圖像,使用自定義內核過濾圖像等。
-
瞭解形態學變換,如侵蝕、膨脹、開放、閉合等。
-
學習尋找圖像漸變、邊緣等。
-
學習通過 Canny 邊緣檢測尋找邊緣。
-
學習關於圖像金字塔的內容,以及如何使用它們進行圖像混合。
-
所有關於 OpenCV 中的輪廓的內容。
-
所有關於 OpenCV 中的直方圖的內容。
-
在 OpenCV 中遇到不同的圖像變換,如傅里葉變換、餘弦變換等。
-
學習使用模板匹配在圖像中搜索對象。
-
學習在一幅圖像中探測線。
-
學習在一幅圖像中探測圓。
-
學習使用分水嶺分割算法分割圖像。
-
學習使用 GrabCut 算法提取前景
目標
- 學習對圖像應用不同的幾何變換,比如平移,旋轉和仿射變換等等。
- 我們將看到這些函數:cv.getPerspectiveTransform。
變換
OpenCV 提供了兩個變換函數,cv.warpAffine 和 cv.warpPerspective,通過它們我們可以執行所有種類的變換。cv.warpAffine 接收一個 2x3 的變換矩陣,而 cv.warpPerspective 則接收一個 3x3 的變換矩陣作爲參數。
放縮
放縮只是改變圖像的大小。OpenCV 有一個函數 cv.resize() 用於完成這個操作。圖像的大小可以手動指定,或者可以指定放縮因子。放縮時可以使用不同的插值方法。用於縮小的首選插值方法是 cv.INTER_AREA,用於放大的是 cv.INTER_CUBIC (slow) & cv.INTER_LINEAR。默認情況下,cv.INTER_LINEAR 插值方法用於所有的調整大小。我們可以通過如下的方法改變一幅輸入圖像的大小:
def scaling():
cv.samples.addSamplesDataSearchPath("/media/data/my_multimedia/opencv-4.x/samples/data")
img = cv.imread(cv.samples.findFile('messi5.jpg'))
res = cv.resize(img, None, fx=2, fy=2, interpolation=cv.INTER_CUBIC)
cv.imshow('frame', res)
# OR
height, width = img.shape[:2]
res = cv.resize(img, (2 * width, 2 * height), interpolation=cv.INTER_CUBIC)
cv.waitKey()
cv.destroyAllWindows()
放縮操作對許多以多幅圖像爲參數的操作比較有意義。當某個操作的輸入爲多幅圖像,且對輸入的圖像的大小有一定的限制,而實際的輸入圖像又難以滿足這種限制時,放縮可以幫我們改變部分圖像的大小,以滿足目標操作的輸入要求。比如多幅圖像的相加操作,圖像的水平拼接和豎直拼接等。
平移
平移是移動一個物體的位置。如果知道了 (x,y) 方向的偏移,並使其爲 ($t_x$,$t_y$),則我們可以創建如下的轉換矩陣: $$ \begin{bmatrix} 1&0&t_x \ 0&1&t_x \end{bmatrix} $$
$$ dst(x,y) = src(M_{11} ∗ x+M_{12}∗ y+M_{13}, M_{21}∗ x+M_{22}∗ y+M_{23}) \ = src(1∗x+0∗y+t_x, 0∗x+1∗y+t_y) $$
我們可以把它放入 np.float32 類型的 Numpy 數組並將其傳遞給 cv.warpAffine() 函數。可以參考下面移動 (100,50) 的例子:
def translation():
cv.samples.addSamplesDataSearchPath("/media/data/my_multimedia/opencv-4.x/samples/data")
img = cv.imread(cv.samples.findFile('messi5.jpg'))
rows, cols, _ = img.shape
M = np.float32([[1, 0, 100], [0, 1, 50]])
dst = cv.warpAffine(img, M, (cols, rows))
dst = cv.hconcat([img, dst])
cv.imshow('frame', dst)
cv.waitKey()
cv.destroyAllWindows()
cv.warpAffine() 函數的第三個參數是輸出圖像的大小,它的形式應該是 (width, height)。記得 width = 列數,而 height = 行數。
看到的結果應該像下面這樣:
旋轉
將一幅圖像旋轉角度 θ,可以通過如下的轉換矩陣實現: $$ M = \begin{bmatrix} cosθ&−sinθ \ sinθ&cosθ \end{bmatrix} $$
但是 OpenCV 提供了帶有可調節旋轉中心的放縮旋轉,因此我們可以在任何喜歡的位置旋轉。修改後的變換矩陣由下式給出
$$ \begin{bmatrix} α&β&(1−α)⋅center.x−β⋅center.y \ −β&α&β⋅center.x+(1−α)⋅center.y \end{bmatrix} $$
其中: $$ α=scale⋅cosθ, \ β=scale⋅sinθ $$
爲了獲得這個旋轉矩陣,OpenCV 提供了一個函數,cv.getRotationMatrix2D。檢查如下的例子,它將圖像相對於中心旋轉 120 度且放大到 1.2 倍。
def rotation():
cv.samples.addSamplesDataSearchPath("/media/data/my_multimedia/opencv-4.x/samples/data")
img = cv.imread(cv.samples.findFile('messi5.jpg'))
rows, cols, _ = img.shape
# cols-1 and rows-1 are the coordinate limits.
M = cv.getRotationMatrix2D(((cols - 1) / 2.0, (rows - 1) / 2.0), 120, 1.2)
dst = cv.warpAffine(img, M, (cols, rows))
dst = cv.hconcat([img, dst])
cv.imshow('frame', dst)
cv.waitKey()
cv.destroyAllWindows()
來看下結果:
放射變換
在仿射變換中,原始圖像中所有的平行線在輸出的圖像中依然是平行的。爲了找到變換矩陣,我們需要輸入圖像中的三個點,以及它們在輸出圖像中對應的位置。cv.getAffineTransform 將創建一個 2x3 的矩陣,它將被傳給 cv.warpAffine。
檢查下面的例子,我們也將看到選中的點(它們用綠色標記):
def affine_transformation():
img = np.zeros((512, 512, 3), np.uint8)
cv.rectangle(img, (0, 0), (512, 512), (255, 255, 255), -1)
cv.line(img, (0, 50), (512, 50), (0, 0, 0), 3)
cv.line(img, (0, 150), (512, 150), (0, 0, 0), 3)
cv.line(img, (0, 300), (512, 300), (0, 0, 0), 3)
cv.line(img, (0, 450), (512, 450), (0, 0, 0), 3)
cv.line(img, (100, 0), (100, 512), (0, 0, 0), 3)
cv.line(img, (256, 0), (256, 512), (0, 0, 0), 3)
cv.line(img, (412, 0), (412, 512), (0, 0, 0), 3)
cv.rectangle(img, (60, 170), (430, 400), (0, 0, 0), 3)
# img, center, radius, color, thickness=None
cv.circle(img, (60, 50), 8, (0, 255, 0), -1)
cv.circle(img, (280, 50), 8, (0, 255, 0), -1)
cv.circle(img, (60, 270), 8, (0, 255, 0), -1)
rows, cols, ch = img.shape
pts1 = np.float32([[50, 50], [200, 50], [50, 200]])
pts2 = np.float32([[10, 100], [200, 50], [100, 250]])
M = cv.getAffineTransform(pts1, pts2)
dst = cv.warpAffine(img, M, (cols, rows))
plt.subplot(121), plt.imshow(img), plt.title('Input')
plt.subplot(122), plt.imshow(dst), plt.title('Output')
plt.show()
if __name__ == "__main__":
affine_transformation()
可以看到下面的結果:
透視變換
對於透視變換,我們需要一個 3x3 的變換矩陣。即使在轉換之後,直線也將保持筆直。要找到這個變換矩陣,我們需要輸入圖像上的 4 個點和輸出圖像上的對應點。這 4 個點中,有 3 個不應該共線。然後可以通過函數 cv.getPerspectiveTransform 找到變換矩陣。然後用這個 3x3 變換矩陣應用 cv.warpPerspective。
可以看下如下的代碼:
def perspective_transformation():
cv.samples.addSamplesDataSearchPath("/media/data/my_multimedia/opencv-4.x/samples/data")
img = cv.imread(cv.samples.findFile('sudoku.png'))
rows, cols, ch = img.shape
pts1 = np.float32([[70, 80], [490, 70], [30, 510], [515, 515]])
pts2 = np.float32([[0, 0], [515, 0], [0, 515], [515, 515]])
M = cv.getPerspectiveTransform(pts1, pts2)
dst = cv.warpPerspective(img, M, (515, 515))
cv.line(img, (0, int(rows / 2)), (cols, int(rows / 2)), (0, 255, 0), 3)
cv.line(img, (int(cols / 2), 0), (int(cols / 2), rows), (0, 255, 0), 3)
cv.circle(img, (70, 80), 8, (0, 255, 0), -1)
cv.circle(img, (490, 70), 8, (0, 255, 0), -1)
cv.circle(img, (30, 510), 8, (0, 255, 0), -1)
cv.circle(img, (515, 515), 8, (0, 255, 0), -1)
plt.subplot(121), plt.imshow(img), plt.title('Input')
cv.line(dst, (0, int(rows / 2)), (cols, int(rows / 2)), (0, 255, 0), 3)
cv.line(dst, (int(cols / 2), 0), (int(cols / 2), rows), (0, 255, 0), 3)
plt.subplot(122), plt.imshow(dst), plt.title('Output')
plt.show()
if __name__ == "__main__":
perspective_transformation()
最終的結果如下圖:
其它資源
- "Computer Vision: Algorithms and Applications", Richard Szeliski
參考文檔
Geometric Transformations of Images
Done.