改变颜色空间
OpenCV中有超过150种颜色空间转换方法,其中两个最广泛使用的为BGR<->灰色、BGR<->HSV。
使用cvtColor(input_image, flag)
进行颜色转换,其中flag决定转换的类型。对于BGR<->灰色,使用标志cv.COLOR_BGR2GRAY
;对于BGR<->HSV,使用标志cv.COLOR_BGR2HSV。要获取其他标志,可使用以下代码:
import cv2 as cv
flags = [i for i in dir(cv) if i.startswith('COLOR_')]
print(flags)
注意:HSV的色相范围为[0, 179],饱和度范围为[0, 255],值范围为[0, 255],不同的软件使用不同的规模,因此要将OpenCV值和它们比较,需要将这些范围标准化。
对象追踪
知道如何将BGR图像转换为HSV,可以使用它来提取一个有颜色的对象。在HSV中比在BGR颜色空间中更容易表示颜色。在以下程序中,尝试提取一个蓝色的对象。方法如下:取视频的每一帧->转换从BGR到HSV颜色空间->对HSV图像设置蓝色范围的阈值->单独提取蓝色对象。
import cv2 as cv
import numpy as np
cap = cv.VideoCapture(0)
while(1):
#读取帧
_, frame = cap.read()
#转换颜色空间BGR到HSV
hsv = cv.cvtColor(frame, cv.COLOR_BGR2HSV)
#定义HSV中蓝色的范围
lower_blue = np.array([110, 50, 50])
upper_blue = np.array([130, 255, 255])
# 设置HSV的阈值使得只取蓝色
mask = cv.inRange(hsv, lower_blue, upper_blue)
# 将掩膜和图像逐像素相加
res = cv.bitwise_and(frame, frame, mask = mask)
cv.imshow('frame', frame)
cv.imshow('mask', mask)
cv.imshow('res', res)
k = cv.waitKey(5) & 0xff
if k == 27:
break
cv.destroyAllWindows
图像中的一些噪点,会在后续章节中学习如何删除它们。
如何找到要追踪的HSV值
使用cv.cvtColor()
,只需传递想要的BGR值,而不是传递图像。如要查找绿色的HSV值,可使用如下代码:
import cv2 as cv
import numpy as np
green = np.uint8([[[0, 255, 0]]])
hsv_green = cv.cvtColor(green, cv.COLOR_BGR2HSV)
print(hsv_green)
结果为:[[[ 60 255 255]]]
可以把[H - 10, 100, 100]和[H + 10, 255, 255]分别作为下界和上界。除了这种方法外,可以使用任何图像编辑工具(如GIMP或任何在线转换器)来查找这些值,但不要忘记调整HSV范围。
图像的几何变换
变换
OpenCV提供了两个转换函数cv.warpAffine
和cv.warpPerspective
,可以使用它们进行各种转换。cv.warpAffine
采用2x3转换矩阵、cv.warpPerspective
采用3x3转换矩阵作为输入。
缩放
缩放只是调整图像的大小。OpenCV带有一个函数cv.resize()
,图像的大小可以手动指定,也可以指定缩放比例,可以使用不同的插值方法。首选的插值方法是cv.INTER_AREA
用于缩小,cv.INTER_CUBIC
(慢)和cv.INTER_LINEAR
用于缩放。默认情况下,出于所有调整大小的目的,使用的插值方法为cv.INTET_LINEAR
。可使用以下方法调整输入图像的大小:
import cv2 as cv
import numpy as np
img = cv.imread('./OpenCV/fusi1.jpg')
# res = cv.resize(img, None, fx = 2, fy = 2, interpolation = cv.INTERcUBIC)
# 或者
height, width = img.shape[:2]
res = cv.resize(img, (2 * width, 2 * height), interpolation = cv.INTER_CUBIC)
cv.imshow('img', img)
cv.imshow('res', res) #缩放后
k = cv.waitKey(0) & 0xff
if k == 27:
cv.destroyAllWindows
平移
平移是物体位置的移动。若知道在(x, y)方向上的位移,则将其设为
(
t
x
,
t
y
)
,
(t_{x} , t_{y}) ,
(tx,ty),可以创建转换矩阵M,如下所示:
M
=
[
1
0
t
x
0
1
t
y
]
M=\begin{bmatrix} 1 & 0 & t_{x} \\ 0 & 1 & t_{y} \end{bmatrix}
M=[1001txty]
可以将其放入np.float32
类型的Numpy数组中,并将其传递给cv.warpAffine
函数。如下列示例代码偏移为(100, 50):
import cv2 as cv
import numpy as np
img = cv.imread('./OpenCV/fusi1.jpg', 0)
rows, cols = img.shape
M = np.float32([[1, 0, 100], [0, 1, 50]])
dst = cv.warpAffine(img, M, (cols, rows))
cv.imshow('img', dst)
k = cv.waitKey(0) & 0xff
if k == 27:
cv.destroyAllWindows
cv.warpAffine
函数的第三个参数是输出图像的大小,其形式应为(width, height),其中width = 列数,height = 行数。
旋转
图像旋转角度为
θ
\theta
θ 是通过以下形式的变换矩阵实现的:
M
=
[
cos
θ
−
sin
θ
sin
θ
cos
θ
]
M=\begin{bmatrix}\cos\theta & -\sin \theta \\ \sin \theta & \cos\theta \end{bmatrix}
M=[cosθsinθ−sinθcosθ]
但是OpenCV提供了可缩放的旋转以及可调整的旋转中心,因此可以在任何为位置旋转,修改后的变换矩阵为:
[
α
β
(
1
−
α
)
⋅
c
e
n
t
e
r
.
x
−
β
⋅
c
e
n
t
e
r
.
y
−
β
α
β
⋅
c
e
n
t
e
r
.
x
+
(
1
−
α
)
⋅
c
e
n
t
e
r
.
y
]
\begin{bmatrix} \alpha& \beta & \left(1-\alpha\right) \cdot center_{.}x - \beta \cdot center_{.} y \\ -\beta & \alpha & \beta\cdot center_{.}x + \left( 1-\alpha \right) \cdot center_{.}y \end{bmatrix}
[α−ββα(1−α)⋅center.x−β⋅center.yβ⋅center.x+(1−α)⋅center.y]
其中:
α
=
s
c
a
l
e
⋅
cos
θ
,
β
=
s
c
a
l
e
⋅
sin
θ
\alpha = scale \cdot \cos \theta ,\beta = scale \cdot \sin \theta
α=scale⋅cosθ,β=scale⋅sinθ
为了找到此转换矩阵,OpenCV提供了cv.getRotationMatrix2D
函数。以下示例代码将图像相对于中心旋转90度而没有任何缩放比例。
仿射变换
在仿射变换中,原始图像中的所有平行线在输出图像中仍将平行。为了找到变换矩阵,需要输入图像中的三个点及其在输出图像中的对应位置。然后cv.getAffineTransform
将创建一个2x3矩阵,该矩阵将传递给cv.warpAffine
。
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
img = cv.imread('./OpenCV/pane.jpg')
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()
透视变换
对于透视变换,需要3x3变换矩阵。即使在转换后,直线也将保持直线。要找到此变换矩阵,需要在输入图像上有4个点,在输出图像上需要相应的点;在这4个点中,其中3个不应共线;可以通过函数cv.getPerspectiveTransform
找到变换矩阵,然后将cv.warpPerspective
应用于此3x3转换矩阵。
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
img = cv.imread('./OpenCV/sudoku.png')
rows, cols, ch = img.shape
pts1 = np.float32([[46, 55], [320, 41], [18, 334], [341, 341]])
pts2 = np.float32([[0, 0], [300, 0], [0, 300], [300, 300]])
M = cv.getPerspectiveTransform(pts1, pts2)
dst = cv.warpPerspective(img, M, (300, 300))
plt.subplot(121), plt.imshow(img), plt.title('Input')
plt.subplot(122), plt.imshow(dst), plt.title('Output')
plt.show()
图像阈值
简单阈值
函数cv.threshold
用于应用阈值,第一个参数是源图像(灰度图像);第二个参数是阈值,用于对像素值进行分类;第三个参数是分配给超过阈值的像素值的最大值;第四个参数为OpenCV提供的不同类型的阈值。
所有简单的阈值类型为:cv.THRESH_BINARY
、cv.THRESH_BINARY_INV
、cv.THRESH_TRUNC
、cv.THRESH_TOZERO
、cv.THRESH_TOZERO_INV
函数cv.threshold
返回两个输出,第一个是使用的阈值,第二个输出是应用阈值后的图像。
下列代码比较了不同的简单阈值类型:
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
img = cv.imread('./OpenCV/gradient.jpg', 0)
ret, thresh1 = cv.threshold(img, 126, 255, cv.THRESH_BINARY)
ret, thresh2 = cv.threshold(img, 126, 255, cv.THRESH_BINARY_INV)
ret, thresh3 = cv.threshold(img, 126, 255, cv.THRESH_TRUNC)
ret, thresh4 = cv.threshold(img, 126, 255, cv.THRESH_TOZERO)
ret, thresh5 = cv.threshold(img, 126, 255, cv.THRESH_TOZERO_INV)
imgs = [img, thresh1, thresh2, thresh3, thresh4, thresh5]
titles = ['Original', 'BINARY', 'BINARY_INV', 'TRUNC', 'TOZERO', 'TOZERO_INV']
for i in range(6):
plt.subplot(2, 3, i + 1)
plt.imshow(imgs[i], 'gray')
plt.title(titles[i])
plt.xticks([]), plt.yticks([])
plt.show()
从结果可以看出,
cv.THRESH_BINARY
将小于阈值的像素值设置为0,大于阈值的像素值设置为255;
cv.THRESH_BINARY_INV
将小于阈值的像素值设置为255,大于阈值的像素值设置为0;
cv.THRESH_TRUNC
将小于阈值的像素值保持不变,大于阈值的像素值设置为阈值;
cv.THRESH_TOZERO
将小于阈值的像素值设置为0,大于阈值的像素值保持不变;
cv.THRESH_TOZERO_INV
将小于阈值的像素值保持不变,大于阈值的像素值设置为0。
自适应阈值
前面使用一个全局值作为阈值,但可能不是在所有情况下都很好;如图像在不同区域具有不同的光照条件,在这种情况下,自适应阈值阈值化可以;算法基于像素周围的小区域确定像素的阈值,为光照变化的图像提供了更好的结果。
cv.ADAPTIVE_THRESH_MEAN_C
:阈值是邻近区域的平均值减去常数C;
cv.ADAPTIVE_THRESH_GAUSSIAN_C
:阈值是邻域值的高斯加权总和减去常数C;
使用BLOCKSIZE确定附近区域的大小;
C是从邻域像素的平均或加权总和中减去一个常数。
Otsu的二值化
在全局阈值化中,使用任意选择的值作为阈值;而Otsu的方法避免了必须选择一个值并自动确定它的情况。考虑仅具有两个不同图像值的图像(双峰图像),其中直方图将仅包含两个峰;一个好的阈值应该在这两个值的中间;类似地,Otsu的方法从图像直方图中确定最佳全局阈值。
为此,使用cv.threshold
作为附加标志传递,阈值可以任意选择,然后算法找到最佳阈值,该阈值作为第一输出返回。
查看以下示例,输入图像为噪点图像,在第一种情况下,采用值为127的全局阈值;第二种情况下,直接采用Otsu阈值法;第三种情况下,首先使用5x5高斯核对图像进行滤波以去除噪声,
然后应用Otsu阈值处理,了解噪声滤波如何改善结果。
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
img = cv.imread('./OpenCV/noisy.jpg', 0)
# 全局阈值
ret1, th1 = cv.threshold(img, 127, 255, cv.THRESH_BINARY)
# Otsu阈值
ret2, th2 = cv.threshold(img, 0, 255, cv.THRESH_BINARY + cv.THRESH_OTSU)
# 高斯滤波后再采用Otsu阈值
blur = cv.GaussianBlur(img, (5, 5), 0)
ret3, th3 = cv.threshold(blur, 0, 255, cv.THRESH_BINARY + cv.THRESH_OTSU)
# 绘制所有图像及其直方图
imags = [img, 0, th1, img, 0, th2, blur, 0, th3]
titles = ['Original Noisy Image', 'Histogram', 'Global Thresholding(v = 127)',
'Original Noisy Image', 'Histogram', "Otsu's Thresholding",
'Gaussian filtered Image', 'Histogram', "Otsu's Thresholding"]
for i in range(3):
plt.subplot(3, 3, i * 3 + 1), plt.imshow(imags[i * 3], 'gray')
plt.title(titles[ i * 3]), plt.xticks([]), plt.yticks([])
plt.subplot(3, 3, i * 3 + 2), plt.hist(imags[i * 3].ravel(), 256)
plt.title(titles[ i * 3 + 1]), plt.xticks([]), plt.yticks([])
plt.subplot(3, 3, i * 3 + 3), plt.imshow(imags[i * 3 + 2], 'gray')
plt.title(titles[ i * 3 + 2]), plt.xticks([]), plt.yticks([])
plt.show()
noisy.jpg:
运行结果:
Otsu的二值化如何实现?
由于正在处理双峰图像,Otsu的算法尝试找到一个阈值(t),该阈值将由关系式给出的 加权类内方差 最小化:
它找到位于两个峰值之间的t值,以使两个类别的差异最小。在Python中的实现如下:
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
img = cv.imread('./OpenCV/noisy.jpg', 0)
blur = cv.GaussianBlur(img, (5, 5), 0)
# 寻找归一化直方图和对应的累积分布函数
hist = cv.calcHist([blur], [0], None, [256], [0, 356])
hist_norm = hist.ravel() / hist.max()
Q = hist_norm.cumsum()
bins = np.arange(256)
fn_min = np.inf
thresh = -1
for i in range(1, 256):
p1, p2 = np.hsplit(hist_norm, [i]) #概率
q1, q2 = Q[i], Q[255] - Q[i] #对类求和
b1, b2 = np.hsplit(bins, [i]) #权重
# 寻找均值和方差
m1, m2 = np.sum(p1 * b1) / q1, np.sum(p2 * b2) / q2
v1, v2 = np.sum(((b1 - m1) **2) * p1) / q1, np.sum(((b2 - m2) ** 2) * p2) / q2
# 计算最小化函数
fn = v1 * q1 + v2 * q2
if fn < fn_min:
fn_min = fn
thresh = i
# 使用OpenCV函数找到Otsu的阈值
ret, otsu = cv.threshold(blur, 0, 255, cv.THRESH_BINARY + cv.THRESH_OTSU)
print("{} {}".format(thresh, ret))
学习来源:OpenCV-Python中文文档