整理一下以前学习 OpenCV 时的一些基础实例。基于 OpenCV 3 (截至目前,最新的是4)和 Python。安装 OpenCV 的过程就省略了,毕竟网络已经有太多太多了。
安装完 OpenCV 后,在 Python 的 REPL 环境中可以加载出 cv2 模块,就算安装成功了。
>>> import cv2
这大概就是 OpenCV 的 Hello World了:
import cv2
img = cv2.imread('./images/cat.jpg')
cv2.imshow('Input image', img)
cv2.waitKey()
OpenCV 使用 NumPy 来存储图片。换句话说,要在 Python 中使用 OpenCV,需要先安装 NumPy 作为依赖。
>>> import cv2
>>> img = cv2.imread('./images/cat.jpg')
>>> type(img)
<class 'numpy.ndarray'>
cv2.waitKey()
用来绑定键盘,它接受一个表示毫秒的参数。基本上,这个函数被用来等待一个键盘事件。在这里就是用来等待键盘输入任意字符,然后就退出。如果没有传递参数或者传递了 0 作为参数,它会一直等待,直到键盘输入。
OpenCV 提供了很多加载图片的模式,比如计算机视觉中最常用的灰度图片:
import cv2
gray_img = cv2.imread('./images/cat.jpg', cv2.IMREAD_GRAYSCALE)
cv2.imshow('Grayscale', gray_img)
cv2.waitKey()
当然,还有很多的读取模式。
比如,把 .jpg 的图片转换成 .png 格式的(不是把文件后缀名改了这么简单的):
import cv2
img = cv2.imread('./images/cat.jpg')
cv2.imwrite('./images/output_cat.png', img, [cv2.IMWRITE_PNG_COMPRESSION])
显然,可以通过 ImwriteFlag
的参数改变图片的格式或者图片的质量等等。
色彩空间实际就是颜色模型和映射函数这两个的组合。有很多不同的色彩空间,最著名的就是 RGB,除此之外,像 YUV、HSV、Lab 等等也是很有名的。
考虑所有的颜色,OpenCV 大概提供了 190 种转换选项,可以在任意两种色彩空间中进行转换。如果你想要看所有的转换类型,可以在 Python 的 REPL 中输入下面的代码:
>>> import cv2
>>> print([x for x in dir(cv2) if x.startswith('COLOR_')])
比如,把彩色图片转换成灰度图:
import cv2
img = cv2.imread('./images/cat.jpg', cv2.IMREAD_COLOR)
gray_img = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
cv2.imshow('Grayscale', gray_img)
cv2.waitKey()
效果和上面那张灰度图是一样的,就不再截图了。
当然,也可以将图片转换成 YUV:
yuv_img = cv2.cvtColor(img, cv2.COLOR_BGR2YUV)
图片现在看起来是这样的:
看起来,好像图片有点反人类。这样说,也没错。但我们可以把它的通道分开:
# 第一种写法
y, u, v = cv2.split(yuv_img)
cv2.imshow('Y channel', y)
cv2.imshow('U channel', u)
cv2.imshow('V channel', v)
当然,利用 NumPy 切片语法,还有第二种写法,而且一般而言,更加快速。
cv2.imshow('Y channel', yuv_img[:,:,0])
cv2.imshow('U channel', yuv_img[:,:,1])
cv2.imshow('V channel', yuv_img[:,:,2])
前面已经介绍了 YUV,假如只有 Y 通道,它就是一张灰度图:
尝试一下将图片划分到不同的通道,再通过不同的组合合并通道。
import cv2
img = cv2.imread('./images/cat.jpg', cv2.IMREAD_COLOR)
g, b, r = cv2.split(img)
rbg_img = cv2.merge((r,b,g))
rbr_img = cv2.merge((r,b,r))
cv2.imshow('Original', img)
cv2.imshow('RBG', rbg_img)
cv2.imshow('RBR', rbr_img)
cv2.waitKey()
原始照片就不贴上了,RBG 是这样的:
RBR 是这样的:
嗯,都充满一股神秘的色彩。
接下来,看一看怎么在参考系中平移图像。
import cv2
import numpy as np
img = cv2.imread('./images/cat.jpg')
num_rows, num_cols = img.shape[:2]
translation_matrix = np.float32([[1, 0, 70], [0, 1, 110]])
img_translation = cv2.warpAffine(img, translation_matrix, (num_cols, num_rows), cv2.INTER_LINEAR)
cv2.imshow('Translation', img_translation)
cv2.waitKey()
平移,意味着在原有的 x x x 和 y y y 坐标的基础上加上/减去某个 x x x 和 y y y。要完成这个操作,我们需要有一个变换矩阵:
T = [ 1 0 t x 0 1 t y ] T = \left[ \begin{matrix} 1 & 0 & t_x \\ 0 & 1 & t_y \\ \end{matrix} \right] T=[1001txty]
这里的 t x t_x tx 和 t y t_y ty 是平移的值。只要创建好类似这样的矩阵,就可以使用函数 warpAffine
应用到我们的图像。第三个参数是结果图像的行和列数,接下来的参数 InterpolationFlags
定义了插值的方法。
因为效果图中行和列数与原来的图像一致,导致图像被剪切了一部分。为了避免剪切,可以为结果图像留出足够的空间。
img_translation = cv2.warpAffine(img, translation_matrix, \
(num_cols + 70, num_rows + 110), cv2.INTER_LINEAR)
如果说想要把图像放到更大的图像框架的的中间,可以这样做:
import cv2
import numpy as np
img = cv2.imread('./images/cat.jpg')
num_rows, num_cols = img.shape[:2]
translation_matrix = np.float32([[1, 0, 70], [0, 1, 110]])
img_translation = cv2.warpAffine(img, translation_matrix, \
(num_cols + 70, num_rows + 110), cv2.INTER_LINEAR)
translation_matrix = np.float32([[1, 0, -30], [0, 1, -50]])
img_translation = cv2.warpAffine(img_translation, translation_matrix,\
(num_cols + 70 + 30, num_rows + 110 + 50), cv2.INTER_LINEAR)
cv2.imshow('Translation', img_translation)
cv2.waitKey()
除此之外,还有 borderMode
和 borderValue
值可以用来填充空的边框。
import cv2
import numpy as np
img = cv2.imread('./images/cat.jpg')
num_rows, num_cols = img.shape[:2]
translation_matrix = np.float32([[1, 0, 70], [0, 1, 110]])
img_translation = cv2.warpAffine(img, translation_matrix, \
(num_cols, num_rows), cv2.INTER_LINEAR, cv2.BORDER_WRAP, 1)
cv2.imshow('Translation', img_translation)
cv2.waitKey()
细心一点,可以看到原本黑色的边框被填充了。
通过一个确定角度来旋转图像,可以这样做:
import cv2
import numpy as np
img = cv2.imread('images/cat.jpg')
num_rows, num_cols = img.shape[:2]
# 0.7 收缩 30%
rotation_matrix = cv2.getRotationMatrix2D((num_cols / 2, num_rows / 2), 30, 0.7)
img_rotation = cv2.warpAffine(img, rotation_matrix, (num_cols, num_rows))
cv2.imshow('img_rotation', img_rotation)
cv2.waitKey()
同样,表示旋转的方法是用旋转矩阵:
T = [ cos θ − sin θ sin θ cos θ ] T = \left[ \begin{matrix} \cos\theta & -\sin\theta \\ \sin\theta & \cos\theta \\ \end{matrix} \right] T=[cosθsinθ−sinθcosθ]
OpenCV 提供的 getRotationMatrix2D
就是用来获取旋转矩阵。只需要指定以某一个点(第1个参数)进行旋转的角度(第2个参数)还有放缩的因子(第3个参数)。
可以看到,旋转后的图片同样因为框架的尺寸不够而被剪切,所以,应该提供足够大的空间来输出图像:
import cv2
import numpy as np
img = cv2.imread('./images/cat.jpg')
num_rows, num_cols = img.shape[:2]
translation_matrix = np.float32([[1, 0, int(0.5 * num_cols)], \
[0, 1, int(0.5 * num_rows)]])
rotation_matrix = cv2.getRotationMatrix2D((num_cols, num_rows), 30, 1)
img_translation = cv2.warpAffine(img, translation_matrix, \
(2 * num_cols, 2 * num_rows))
img_rotation = cv2.warpAffine(img_translation, rotation_matrix, \
(num_cols * 2, num_rows * 2))
cv2.imshow('img_rotation', img_rotation)
cv2.waitKey()
调整图像的大小无疑也是很常见的计算机图像操作。
import cv2
img = cv2.imread('./images/cat.jpg')
img_scaled = cv2.resize(img, None, fx=1.2, fy=1.2, interpolation=cv2.INTER_LINEAR)
cv2.imshow('Scaling - Linear Interpolation', img_scaled)
img_scaled = cv2.resize(img, None, fx=1.2, fy=1.2, interpolation=cv2.INTER_CUBIC)
cv2.imshow('Scaling - Cubic Interpolation', img_scaled)
img_scaled = cv2.resize(img, (450, 400), interpolation=cv2.INTER_AREA)
cv2.imshow('Scaling - Skewed Size', img_scaled)
cv2.waitKey()
放大图像的时候,需要有某种方法填充像素位置之间的像素值。缩小图像的时候,需要找到最有代表性的像素值。进行非整数倍的缩放时,需要通过某种插值方法适当地进行插值,保证图像的质量。
如果是想要放大一张图片,插值的方法可以选择 linear(线性) 或者 cubic (三次)插值。如果是缩小一张图片,建议选择基于 area 的的插值。
当然,三次插值的计算比线性插值更复杂,因此比线性插值慢。不过,图像的质量将更高。
线性插值的效果:
三次插值的效果:
吐槽:可能我眼力不够,肉眼看不出明显的区别。
指定放缩后的图片大小:
讨论一下广义的几何变换。在前面已经用过了 warpAffine
,是时候了解一下发生了什么。在谈论仿射变换之前,先了解一下什么是欧几里得变换(欧氏变换)。欧氏变换保持长度和角度不变,它看起来可能是旋转后的,或者是平移后的,但是基本结构不会发生改变。
从技术上讲,欧氏变换后,直线仍然是直线,平面仍然是平面,方形依旧是方形,圆形依旧是圆形。回到仿射变换,可以说仿射变换是欧氏变换的推广。仿射变换中,直线仍然是直线,但正方形可能变成矩形或者平行四边形。基本上,仿射变换不保持长度和角度。
为了建立仿射变换的矩阵,需要定义控制点。一旦有了这些控制点,就可以决定把它映射到哪里。
import cv2
import numpy as np
img = cv2.imread('./images/cat.jpg')
rows, cols = img.shape[:2]
src_points = np.float32([[0, 0], [cols - 1, 0], [0, rows - 1]])
dst_points = np.float32([[0, 0], [int(0.6 * (cols - 1)), 0], [int(0.4 * (cols - 1)), rows - 1]])
affine_matrix = cv2.getAffineTransform(src_points, dst_points)
img_output = cv2.warpAffine(img, affine_matrix, (cols, rows))
cv2.imshow('Input', img)
cv2.imshow('Output', img_output)
cv2.waitKey()
也可以从输入图片中创建镜像图片:
import cv2
import numpy as np
img = cv2.imread('./images/cat.jpg')
rows, cols = img.shape[:2]
src_points = np.float32([[0, 0], [cols - 1, 0], [0, rows - 1]])
dst_points = np.float32([[cols - 1, 0], [0, 0], [cols - 1, rows - 1]])
affine_matrix = cv2.getAffineTransform(src_points, dst_points)
img_output = cv2.warpAffine(img, affine_matrix, (cols, rows))
cv2.imshow('Input', img)
cv2.imshow('Output', img_output)
cv2.waitKey()
仿射变换虽然不错,但它加了某些限制。射影变换,给了我们更大的自由。先大概了解一下射影变换是怎么样的。比如你站在一张纸的前面,上面画了一个正方形。现在倾斜那张纸,正方形就会越来越像一个梯形。射影变换就是使我们能够用一种很好的数学方法来捕捉这种动态。这些变换既不保留大小,也不保留角度。
import cv2
import numpy as np
img = cv2.imread('./images/cat.jpg')
rows, cols = img.shape[:2]
src_points = np.float32([[0, 0], [cols - 1, 0], \
[0, rows - 1], [cols - 1, rows - 1]])
dst_points = np.float32([[0, 0], [cols - 1, 0], \
[int(0.33 * cols), rows - 1], [int(0.66 * cols), rows - 1]])
projective_matrix = cv2.getPerspectiveTransform(src_points, dst_points)
img_output = cv2.warpPerspective(img, projective_matrix, (cols, rows))
cv2.imshow('Input', img)
cv2.imshow('Output', img_output)
cv2.waitKey()
射影变换和仿射变换一样,只不过是从源图像中选择4个控制点,映射到目标图像。可以改变一下控制点:
src_points = np.float32([[0, 0], [0, rows - 1], \
[cols / 2, 0], [cols / 2, rows - 1]])
dst_points = np.float32([[0, 100], [0, rows - 101], \
[cols / 2, 0], [cols / 2, rows - 1]])
虽然说射影变换还算自由,但它还是有一定的约束。如果要自己进行随机的变换,那也是可以的,只需要写好映射的规则。
最后一个例子,换张图片吧,不和喵星人过不去了。
import cv2
import numpy as np
import math
def main():
img = cv2.imread('./images/lenna.jpg', cv2.IMREAD_GRAYSCALE)
rows, cols = img.shape[:2]
#########
# Vertical wave
img_output = np.zeros(img.shape, dtype=img.dtype)
for i in range(rows):
for j in range(cols):
offset_x = int(25.0 * math.sin(2 * 3.14 * i / 180))
offset_y = 0
if j + offset_x < rows:
img_output[i, j] = img[i, (j + offset_x) % cols]
else:
img_output[i, j] = 0
cv2.imshow('Input', img)
cv2.imshow('Vertical wave', img_output)
#########
# Horizontal wave
img_output = np.zeros(img.shape, dtype=img.dtype)
for i in range(rows):
for j in range(cols):
offset_x = 0
offset_y = int(16.0 * math.sin(2 * 3.14 * j / 150))
if i + offset_y < rows:
img_output[i, j] = img[(i + offset_y) % rows, j]
else:
img_output[i, j] = 0
cv2.imshow('Horizontal wave', img_output)
##########
# Both horizontal and vertical
img_output = np.zeros(img.shape, dtype=img.dtype)
for i in range(rows):
for j in range(cols):
offset_x = int(20.0 * math.sin(2 * 3.14 * i / 150))
offset_y = int(20.0 * math.cos(2 * 3.14 * i / 150))
if i + offset_y < rows and j + offset_x < cols:
img_output[i, j] = img[(i + offset_y) % rows, (j + offset_x) % cols]
else:
img_output[i, j] = 0
cv2.imshow('Multidirectional wave', img_output)
###########
# Concave effect
img_output = np.zeros(img.shape, dtype=img.dtype)
for i in range(rows):
for j in range(cols):
offset_x = int(128.0 * math.sin(2 * 3.14 * i / (2 * cols)))
offset_y = 0
if j + offset_x < cols:
img_output[i, j] = img[i, (j + offset_x) % cols]
else:
img_output[i, j] = 0
cv2.imshow('Concave', img_output)
cv2.waitKey()
if __name__ == '__main__':
main()
下次估计写的大概是边缘检测和图像滤波。