此OpenCV教程仅供初学者开始学习基础知识。在本指南中,您将学习使用Python使用OpenCV库进行的基本图像处理操作。
在本教程中,我们将创建两个Python脚本来帮助您学习OpenCV基础知识:
目录
1. 加载并显示图像
2. 访问单个像素
3. 数组切片和裁剪
4. 调整图像大小
5. 旋转图像
6 平滑图像
7. 在图像上绘图
8 图像简单物体计数
8.1 将图像转换为灰度图
8.2 边缘检测
8.3 图像阈值化
8.4 检测和绘制轮廓
8.5 侵蚀和膨胀
8.6 掩膜和按位运算
1 # import the necessary packages
2 import imutils
3 import cv2
4
5 # load the input image and show its dimensions, keeping in mind that
6 # images are represented as a multi-dimensional NumPy array with
7 # shape no. rows (height) x no. columns (width) x no. channels (depth)
8 image = cv2.imread("jp.png")
9 (h, w, d) = image.shape
10 print("width={}, height={}, depth={}".format(w, h, d))
11
12 # display the image to our screen -- we will need to click the window
13 # open by OpenCV and press a key on our keyboard to continue execution
14 cv2.imshow("Image", image)
15 cv2.waitKey(0)
在第2行和第3行,我们同时导入了imutils和cv2。 cv2是OpenCV软件包,尽管写的是cv2,但实际上可以是OpenCV 3(或可能是OpenCV 4)。 imutils软件包是我的一系列便利功能。
现在,我们已经导入所需的软件包,现在将图像从磁盘加载到内存中。
我们调用cv2.imread(“ jp.png”)。 正如您在第8行看到的那样,我们将结果分配给image。 我们的图像实际上只是一个NumPy数组。
在此脚本的后面,我们将需要高度和宽度。 因此,在第9行中,我调用image.shape来提取高度,宽度和深度。
高度先于宽度似乎让人感到困惑,但是以这种方式思考:
因此,表示为NumPy数组的图像的尺寸实际上表示为(高度,宽度,深度)。
深度是通道数-在我们的例子中是3,因为我们正在使用3种颜色通道:蓝色,绿色和红色。
第10行显示的打印命令会将值输出到终端:
width=600, height=322, depth=3
为了使用OpenCV在屏幕上显示图像,我们在第14行使用cv2.imshow(“ Image”,image)。下一行等待按键(第15行)。 这一点很重要,否则我们的图像显示和消失的速度将比我们看到图像还要快。
注意:您实际上需要单击由OpenCV打开的活动窗口,然后按键盘上的某个键以前进脚本。 OpenCV无法监视您的终端的输入,因此如果您在终端中按一个键,OpenCV不会注意到。 同样,您将需要单击屏幕上活动的OpenCV窗口,然后按键盘上的一个键。
首先,您可能会问:
什么是像素?
所有图像都由像素组成,这些像素是图像的原始构建块。图像由网格中的像素组成。640 x 480图像具有640列(宽度)和480行(高度)。有640 * 480 = 307200 具有这些尺寸的图像中的像素。
灰度图像中的每个像素都有一个代表灰度的值。在OpenCV中,有256种灰度(从0到255)。因此,灰度图像将具有与每个像素关联的灰度值。
彩色图像中的像素具有其他信息。在学习图像处理时,您很快就会熟悉几种色彩空间。为简单起见,我们仅考虑RGB颜色空间。
在OpenCV彩色图像中,RGB(红色,绿色,蓝色)颜色空间中的每个像素都有一个三元组: (B,G,R ) 。
注意,顺序是BGR而不是RGB。这是因为多年前首次开发OpenCV时,标准是BGR订购。多年来,标准现已成为RGB,但OpenCV仍保持这种“传统” BGR顺序以确保不存在现有代码中断。
BGR 3元组中的每个值的范围为 [ 0 ,255 ] 。OpenCV中RGB图像中的每个像素有多少种颜色可能性?这很容易:256 * 256 * 256 = 16777216 。
既然我们确切地知道什么是像素,让我们看看如何检索图像中单个像素的值:
17 # access the RGB pixel located at x=50, y=100, keepind in mind that
18 # OpenCV stores images in BGR order rather than RGB
19 (B, G, R) = image[100, 50]
20 print("R={}, G={}, B={}".format(R, G, B))
如前所示,我们的图像尺寸为 宽度= 600 ,高度= 322 ,深度= 3 。我们可以通过指定坐标来访问数组中的各个像素值,只要它们在最大宽度和高度之内即可。
编码, 图像[ 100 ,50 ] ,从位于的像素产生3元组的BGR值 x = 50 和 y = 100 (同样,记住的高度是多少行和宽度是多少列 -现在进行第二次说服自己这是真的)。如上所述,OpenCV以BGR顺序存储图像(例如,不同于Matplotlib)。看看提取第19行上像素的颜色通道值有多么简单 。
最终像素值显示在此处的终端上:
R=41, G=49, B=37
提取“感兴趣区域”(ROI)是图像处理的一项重要技能。
举例来说,您正在识别电影中的人脸。首先,您将运行人脸检测算法以查找正在使用的所有帧中人脸的坐标。然后,您需要提取面部ROI,然后保存它们或对其进行处理。
现在,让我们手动提取ROI。这可以通过数组切片来实现。
22 # extract a 100x100 pixel square ROI (Region of Interest) from the
23 # input image starting at x=320,y=60 at ending at x=420,y=160
24 roi = image[60:160, 320:420]
25 cv2.imshow("ROI", roi)
26 cv2.waitKey(0)
数组切片在 第24行 ,格式为: 图片[ startY:endY,startX:endX ] 。 然后在第25行显示 。就像上次一样,我们显示直到按下某个键(第26行)。
调整图像大小很重要,原因有很多。首先,您可能需要调整大图像的大小以适合屏幕。在较小的图像上,图像处理也更快,因为要处理的像素更少。在深度学习的情况下,我们经常忽略宽高比来调整图像的大小,以使体积适合网络,这要求图像是正方形且具有一定尺寸。
让我们将原始图像调整为200 x 200像素:
28 # resize the image to 200x200px, ignoring aspect ratio
29 resized = cv2.resize(image, (200, 200))
30 cv2.imshow("Fixed Resizing", resized)
31 cv2.waitKey(0)
在 第29行,我们调整了忽略宽高比的图像的大小。 显示图像已调整大小,但由于我们未考虑长宽比,因此现在已失真。
让我们计算原始图像的长宽比,并使用它来调整图像的大小,以使其看起来不会变形和变形:
33 # fixed resizing and distort aspect ratio so let's resize the width
34 # to be 300px but compute the new height based on the aspect ratio
35 r = 300.0 / w
36 dim = (300, int(h * r))
37 resized = cv2.resize(image, dim)
38 cv2.imshow("Aspect Ratio Resize", resized)
39 cv2.waitKey(0)
回想一下该脚本的第9行,我们在其中提取了图像的宽度和高度。
假设我们要拍摄600像素宽的图像,并将其大小调整为300像素宽,同时保持宽高比。
在第35行,我们计算新宽度与旧宽度的比率(恰好是0.5)。
在第36行,我们指定新图像的尺寸dim。 我们知道我们想要一个300像素宽的图像,但是我们必须使用h乘以r(分别是原始高度和比率)来计算新高度。
向cv2.resize函数中输入dim(我们的尺寸)后,我们现在获得了一个名为resize的新图像,该图像不会失真(第37行)。
我们使用第38行的代码显示图像:
但是,我们是否可以使调整大小时,保持宽高比的过程变得更加容易?
是!
每次我们要调整图像大小时,都要计算纵横比有点繁琐,因此我将代码包装在imutils中的函数中。
这是使用imutils.resize的方法
41 # manually computing the aspect ratio can be a pain so let's use the
42 # imutils library instead
43 resized = imutils.resize(image, width=300)
44 cv2.imshow("Imutils Resize", resized)
45 cv2.waitKey(0)
在一行代码中,我们保留了宽高比并调整了图像的大小。
简单吧?
您只需要提供目标宽度或目标高度作为关键字参数即可(第43行)。
结果如下:
47 # let's rotate an image 45 degrees clockwise using OpenCV by first
48 # computing the image center, then constructing the rotation matrix,
49 # and then finally applying the affine warp
50 center = (w // 2, h // 2)
51 M = cv2.getRotationMatrix2D(center, -45, 1.0)
52 rotated = cv2.warpAffine(image, M, (w, h))
53 cv2.imshow("OpenCV Rotation", rotated)
54 cv2.waitKey(0)
围绕中心点旋转图像,需要首先计算图像的中心(x,y)坐标(第50行)。
注意:我们使用 // 进行整数数学运算(即没有浮点值)。
(第51行)我们计算出旋转矩阵M。 -45表示我们将图像顺时针旋转45度。 回忆一下中学/中学几何课中关于单位圆的信息,您将可以提醒自己,正角是逆时针方向,负角是顺时针方向。
我们使用第52行的矩阵(有效旋转)使图像变形。
旋转的图像显示在第52行的屏幕上,如下图所示:
现在,让我们使用imutils在单行代码中执行相同的操作
56 # rotation can also be easily accomplished via imutils with less code
57 rotated = imutils.rotate(image, -45)
58 cv2.imshow("Imutils Rotation", rotated)
59 cv2.waitKey(0)
由于我不必像调整图像大小一样旋转图像,因此我觉得旋转过程更难记。 因此,我在imutils中创建了一个函数来为我们处理它。 在一行代码中,我可以完成将图像顺时针旋转45度(第57行)。
此时,您必须要考虑:
为什么图像被剪切?
关键是,OpenCV不在乎旋转后我们的图像是否被裁剪。 我觉得这很麻烦,因此这是我的imutils版本,它将使整个图像保持可见。 我称它为rotate_bound:
61 # OpenCV doesn't "care" if our rotated image is clipped after rotation
62 # so we can instead use another imutils convenience function to help
63 # us out
64 rotated = imutils.rotate_bound(image, 45)
65 cv2.imshow("Imutils Bound Rotation", rotated)
66 cv2.waitKey(0)
结果如下图所示
在许多图像处理管道中,我们必须对图像进行模糊处理以减少高频噪声,从而使我们的算法更容易检测和理解图像的实际内容。 在OpenCV中,对图像进行模糊处理非常容易,并且有多种方法可以完成图像处理。
我经常使用GaussianBlur函数:
68 # apply a Gaussian blur with a 11x11 kernel to the image to smooth it,
69 # useful when reducing high frequency noise
70 blurred = cv2.GaussianBlur(image, (11, 11), 0)
71 cv2.imshow("Blurred", blurred)
72 cv2.waitKey(0)
在第70行,我们使用11 x 11内核执行高斯模糊,其结果如图10所示。
较大的内核将产生更模糊的图像。 较小的内核将创建较少的模糊图像。
在本部分中,我们将在输入图像上绘制矩形,圆形和直线。 我们还将在图像上覆盖文字。
在继续使用OpenCV在图像上进行绘制之前,请注意,在图像上进行绘制操作是就地执行的。 因此,在每个代码块的开头,我们制作原始图像的副本,并将副本存储为输出。 然后,我们继续在原位输出图像上进行绘制,以免破坏原始图像。
让我们在图像上绘制一个矩形:
74 # draw a 2px thick red rectangle surrounding the face
75 output = image.copy()
76 cv2.rectangle(output, (320, 60), (420, 160), (0, 0, 255), 2)
77 cv2.imshow("Rectangle", output)
78 cv2.waitKey(0)
首先,出于刚才说明的原因,我们在第75行上复制了图像。
然后我们继续绘制矩形。
在OpenCV中绘制矩形再简单不过了。 使用预先计算的坐标,我向第76行的cv2.rectangle函数提供了以下参数:
由于我们使用的是OpenCV的函数而不是NumPy的操作,因此我们可以按(x,y)的顺序而不是(y,x)的顺序提供坐标,因为我们没有直接操作或访问NumPy数组。
这是我们的结果
现在,让我们在图像上放置一个蓝色实心圆圈:
80 # draw a blue 20px (filled in) circle on the image centered at
81 # x=300,y=150
82 output = image.copy()
83 cv2.circle(output, (300, 150), 20, (255, 0, 0), -1)
84 cv2.imshow("Circle", output)
85 cv2.waitKey(0)
要绘制圆,您需要向cv2.circle提供以下参数:
这是结果
接下来,我们将绘制一条红线。
如果仔细查看方法参数并将它们与矩形的参数进行比较,您会发现它们是相同的:
87 # draw a 5px thick red line from x=60,y=20 to x=400,y=200
88 output = image.copy()
89 cv2.line(output, (60, 20), (400, 200), (0, 0, 255), 5)
90 cv2.imshow("Line", output)
91 cv2.waitKey(0)
就像在矩形中一样,我们提供两个点,一个颜色和一个线宽。 OpenCV的后端完成其余的工作。
结果如下:
通常,您会发现您想要在图像上覆盖文字以进行显示。 如果您要进行人脸识别,则可能需要在该人的面部上方绘制该人的名字。 或者,如果您在计算机视觉事业中取得进步,则可以构建图像分类器或对象检测器。 在这些情况下,您会发现要绘制包含类别名称和概率的文本。
让我们看看OpenCV的putText函数如何工作:
93 output = image.copy()
94 cv2.putText(output, "OpenCV + Jurassic Park!!!", (10, 25),
95 cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
96 cv2.imshow("Text", output)
97 cv2.waitKey(0)
OpenCV的putText函数负责在图像上绘制文本。 让我们看一下所需的参数:
第95和96行的代码将绘制文本“ OpenCV + Jurassic Park !!!”。
现在,我们要介绍第二个脚本。
接下来,我们将学习如何使用创建简单的Python + OpenCV脚本来计算下图中的Tetris块数:
在该脚本中,我们将:
1 # import the necessary packages
2 import argparse
3 import imutils
4 import cv2
5
6 # construct the argument parser and parse the arguments
7 ap = argparse.ArgumentParser()
8 ap.add_argument("-i", "--image", required=True,
9 help="path to input image")
10 args = vars(ap.parse_args())
在2-4行上,我们导入了包。 在每个Python脚本的开始都需要这样做。 对于第二个脚本,我导入了argparse -所有安装的Python随附的命令行参数解析包。
快速浏览7-10行。 这些行使我们可以在运行时,从终端内部向程序提供其他信息。
我们有一个必需的命令行参数--image,如第8和9行所定义。
我们将在下面介绍如何使用必需的命令行参数运行脚本。 现在,只要知道脚本中遇到args [“ image”]的地方,我们就是指输入图像的路径。
12 # load the input image (whose path was supplied via command line
13 # argument) and display the image to our screen
14 image = cv2.imread(args["image"])
15 cv2.imshow("Image", image)
16 cv2.waitKey(0)
17
18 # convert the image to grayscale
19 gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
20 cv2.imshow("Gray", gray)
21 cv2.waitKey(0)
我们将图像加载到第14行的内存中。cv2.imread函数的参数是包含在args字典中的路径,该字典以“ image”键args [“ image”]引用。
从那里开始显示图像,直到遇到第一次按键(第15和16行)。
我们将很快对图像进行边缘化和检测。 因此,我们通过调用cv2.cvtColor并提供图像和cv2.COLOR_BGR2GRAY标志,在第19行将图像转换为灰度。
再次,我们显示图像并等待按键(第20和21行)。
我们转换为灰度的结果如下图所示。
边缘检测对于查找图像中对象的边界很有用,对于分割很有用。
让我们执行边缘检测以查看该过程如何工作:
23 # applying edge detection we can find the outlines of objects in
24 # images
25 edged = cv2.Canny(gray, 30, 150)
26 cv2.imshow("Edged", edged)
27 cv2.waitKey(0)
使用流行的Canny算法(由John F. Canny在1986年开发),我们可以找到图像中的边缘。
我们为cv2.Canny函数提供了三个参数:
最小和最大阈值的不同值将返回不同的边缘图。
在下面的图中,请注意如何显示Tetris块本身的边缘以及构成Tetris块的子块:
图像阈值化是图像处理管道的重要中间步骤。 阈值处理可以帮助我们去除较亮或较暗的图像区域和轮廓。
我强烈建议您尝试阈值化。 我通过反复试验(以及经验)对以下代码进行了调整,使其适用于我们的示例:
29 # threshold the image by setting all pixel values less than 225
30 # to 255 (white; foreground) and all pixel values >= 225 to 255
31 # (black; background), thereby segmenting the image
32 thresh = cv2.threshold(gray, 225, 255, cv2.THRESH_BINARY_INV)[1]
33 cv2.imshow("Thresh", thresh)
34 cv2.waitKey(0)
在一行(第32行)中,我们是:
用二进制图像从背景中分割前景对于找到轮廓至关重要。
请注意,在图上中,前景对象为白色,背景为黑色。
36 # find contours (i.e., outlines) of the foreground objects in the
37 # thresholded image
38 cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL,
39 cv2.CHAIN_APPROX_SIMPLE)
40 cnts = imutils.grab_contours(cnts)
41 output = image.copy()
42
43 # loop over the contours
44 for c in cnts:
45 # draw each contour on the output image with a 3px thick purple
46 # outline, then display the output contours one at a time
47 cv2.drawContours(output, [c], -1, (240, 0, 159), 3)
48 cv2.imshow("Contours", output)
49 cv2.waitKey(0)
在第38和39行,我们使用cv2.findContours来检测图像中的轮廓。 请注意参数标志,但现在让我们保持简单-我们的算法是在thresh.copy()图像中找到所有前景(白色)像素。
第40行非常重要,这说明了cv2.findContours实现在OpenCV 2.4,OpenCV 3和OpenCV 4之间发生了变化。无论涉及轮廓,此兼容性行都存在于博客中。
我们在第41行上制作了原始图像的副本,以便可以在后续的第44-49行上绘制轮廓。
在第47行,我们使用适当命名的cv2.drawContours从图像的cnts列表中绘制每个c。 我选择了以元组(240,0,159)表示的紫色。
使用我们在此博客文章中先前学到的知识,让我们在图像上覆盖一些文本:
51 # draw the total number of contours found in purple
52 text = "I found {} objects!".format(len(cnts))
53 cv2.putText(output, text, (10, 25), cv2.FONT_HERSHEY_SIMPLEX, 0.7,
54 (240, 0, 159), 2)
55 cv2.imshow("Contours", output)
56 cv2.waitKey(0)
第52行构建了一个文本字符串,其中包含许多形状轮廓。 计算此图像中的对象总数就像检查轮廓列表len(cnts)的长度一样简单。
结果如下图所示:
侵蚀和膨胀通常用于减少二进制图像中的噪声(阈值的副作用)。
为了减少前景对象的大小,我们可以通过多次迭代来侵蚀掉像素:
58 # we apply erosions to reduce the size of foreground objects
59 mask = thresh.copy()
60 mask = cv2.erode(mask, None, iterations=5)
61 cv2.imshow("Eroded", mask)
62 cv2.waitKey(0)
在第59行中,我们在将脱thresh图像复制并命名为mask。
然后,利用cv2.erode,通过5次迭代来减少轮廓大小(第60行)。
如下图所示,从俄罗斯方块轮廓生成的蒙版略小:
同样,我们可以在蒙版中扩大前景区域,只需使用cv2.dilate:
64 # similarly, dilations can increase the size of the ground objects
65 mask = thresh.copy()
66 mask = cv2.dilate(mask, None, iterations=5)
67 cv2.imshow("Dilated", mask)
68 cv2.waitKey(0)
掩膜允许我们“遮蔽”我们不感兴趣的图像区域。我们称它们为“掩膜”是因为它们将隐藏我们不关心的图像区域。
70 # a typical operation we may want to apply is to take our mask and
71 # apply a bitwise AND to our input image, keeping only the masked
72 # regions
73 mask = thresh.copy()
74 output = cv2.bitwise_and(image, image, mask=mask)
75 cv2.imshow("Output", output)
76 cv2.waitKey(0)
掩膜mask是通过复制二进制thresh图像生成的(第73行)。
我们使用cv2.bitwise_and将两个图像的像素按位与。
结果是下图,现在我们仅显示/突出显示Tetris块。
在今天的博客文章中,您学习了使用Python编程语言进行图像处理和OpenCV的基础知识。
现在,您准备开始使用这些图像处理操作作为“构建块”,可以将它们链接在一起以构建实际的计算机视觉应用程序-这样的项目的一个很好的例子是我们通过计算轮廓创建的基本对象计数器。
我希望本教程可以帮助您学习OpenCV!
下一章:python open-cv 基础知识总结(二)