OpenCV-Python Tutorials - OpenCV-Python 教程

Gui Features in OpenCV - OpenCV的Gui特性

(在这里,您将学习如何显示和保存图像和视频,控制鼠标事件和创建跟踪栏。)

OpenCV-Python 图像处理入门

上一篇教程: Building OpenCV for Tegra with CUDA
下一篇教程: Writing documentation for OpenCV

原作者 Ana Huamán
兼容性 OpenCV >= 3.4.4

警告

​ 本教程可能包含过时的信息。

目标

在本教程中,您将学习如何:

源代码
  • 可下载的源代码: Click here

  • 代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import cv2 as cv
    import sys

    img = cv.imread(cv.samples.findFile("starry_night.jpg"))

    if img is None:
    sys.exit("Could not read the image./找不到图像.")

    cv.imshow("Display window", img)
    k = cv.waitKey(0)

    if k == ord("s"):
    cv.imwrite("starry_night.png", img)

解释

作为第一步,导入OpenCV python库。正确的方法是额外为其分配名称 cv,该名称在下面的代码中用于引用库。

1
2
import cv2 as cv
import sys

现在,让我们分析一下主代码。作为第一步,我们从 OpenCV 样本读取图像”starry_night.ipg”。为了做到这一点,调用 cv::imread 函数使用第一个参数指定的文件路径加载图像。第二个参数是可选的,它指定了我们想要的图像格式。这可能是:

  • IMREAD_COLOR 以 BGR 8位格式加载图像. 这是这里使用的默认值
  • IMREAD_UNCHANGED 按原样加载图像(包括alpha通道,如果它存在的话)
  • IMREAD_GRAYSCALE 以灰度图形式加载图像

读取后的图像数据将存储在 cv::Mat 对象中。

1
img = cv.imread(cv.samples.findFile("starry_night.jpg"))

注意

OpenCV 提供了对 Windows位图(bmp)图像格式,便携式图像格式(pbm, pgm, ppm) 和 Sun raster(sr, ras)的支持。您也可以借助插件 (你需要在自行构建库时指定使用它们,不过在我们提供的包中,这些功能默认是包含的) 来加载如 JPEG (jpeg, jpg, jpe), JPEG 2000 (jp2 - codenamed in the CMake as Jasper), TIFF files (tiff, tif) 以及 portable network graphics (png) 一类的图像格式。 此外,OpenEXR也是一种可能性。

之后,如果映像加载正确,则执行检查。

1
2
if img is None:
sys.exit("Could not read the image.")

然后,调用 cv::imshow 函数以显示图像。第一个参数是窗口的标题,第二个参数是将要显示的 cv::Mat 对象。

因为我们想要让我们的窗口直到用户按下任意按键前一直显示(否则这个项目会结束得太快),我们使用 cv::waitKey 函数,它的唯一参数是等待用户输入的时间(以毫秒为单位)。0表示永远等待。返回值是按下的键。

1
2
cv.imshow("Display window", img)
k = cv.waitKey(0)

最后,如果按下的键是“s”键,则将图像写入文件。为此 cv::imwrite 函数被调用,其中包含文件路径和 作为一个参数的 cv::Mat 函数

1
2
if k == ord("s"):
cv.imwrite("starry_night.png", img)

[^注]: 本文翻译最初编辑于 2024.08.26,最终编辑于 2024.09.07


OpenCV-Python 视频处理入门

目标
从摄像机捕捉视频

通常,我们必须用摄像机捕捉实时视频流。OpenCV 提供了一个非常简单的接口来做到这一点。让我们从摄像机中捕捉一段视频 (我用的是笔记本电脑的内置摄像头),转换成灰度视频并显示。这只是一个简单的开始任务。

要捕获视频,需要创建一个 VideoCapture 对象。它的参数可以是设备索引或视频文件的名称。设备索引只是指定哪个相机的数字,通常会连接一台相机(就像我的情况一样),所以我只用参数0(或-1)。您可以用参数1来选择第二个摄像机,以此类推。之后,您可以逐帧捕获。但最后,别忘了释放捕捉到的画面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import numpy as np
import cv2 as cv

cap = cv.VideoCapture(0)
if not cap.isOpened():
print("Cannot open camera")
exit()
while True:
# Capture frame-by-frame / 获取一帧
ret, frame = cap.read()

# if frame is read correctly ret is True / 如果frame被正确读取,ret为True
if not ret:
print("Can't receive frame (stream end?). Exiting ...")
break
# Our operations on the frame come here / 我们对坐标系的操作到这里
gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY)
# Display the resulting frame
cv.imshow('frame', gray)
if cv.waitKey(1) == ord('q'):
break

# When everything done, release the capture / 当一切完成后,释放捕获
cap.release()
cv.destroyAllWindows()

cap.read() 返回一个bool值 (True/False)。如果帧读取正确该值将变为 True. 所以你可以通过检查这个返回值来检查视频是否结束。

有时,cap可能没有初始化捕获。在这种情况下,这段代码显示了一个错误。你可以通过 **cap.isOpened() **方法来检查它是否已经初始化。如果返回值为 True, 那么OK。否则使用 cap.open() 打开它。

您还可以使用 cap.get(propId) 方法访问本视频的一些功能,propld 为0 ~ 18之间的数字。每个数字代表视频的一个属性 (如果它适用于该视频)。完整的细节可以在这里看到: cv::VideoCapture::get()。其中一些值可以使用 cap.set(propId, value) 修改。Value is the new value you want.

例如,我可以通过 cap.get(cv.CAP_PROP_FRAME_WIDTH)cap.get(cv.CAP_PROP_FRAME_HEIGHT) 检查帧的宽度和高度。它默认是 640x480。但我想将它改为 320x240,只需使用 ret = cap.set(cv.CAP_PROP_FRAME_WIDTH,320)ret = cap.set(cv.CAP_PROP_FRAME_HEIGHT,240).

注意

如果你的代码报错,可能是你的相机正在使用任何其他相机应用程序工作 (比如 Linux 中的 Cheese)。

从文件播放视频

从文件中播放视频和从相机中捕获视频是一样的,只需将相机索引更改为视频文件名。在显示框架时也是如此,为 cv.waitKey() 选择适当的时间。如果它太小,视频将非常快,如果它太高,视频将很慢 (嗯,这就是如何用慢动作显示视频的方法)。在正常情况下,25毫秒就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import numpy as np
import cv2 as cv

cap = cv.VideoCapture('vtest.avi')

while cap.isOpened():
ret, frame = cap.read()

# if frame is read correctly ret is True
if not ret:
print("Can't receive frame (stream end?). Exiting ...")
break
gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY)

cv.imshow('frame', gray)
if cv.waitKey(1) == ord('q'):
break

cap.release()
cv.destroyAllWindows()

注意

确保安装了正确版本的 ffmpeg 或 gstreamer。有时使用视频捕捉是一件令人头疼的事情,主要是由于错误安装 ffmpeg/gstreamer。

保存视频

所以我们捕捉一段视频并逐帧处理,然后我们想保存这个视频。对于图像,它非常简单:只需使用 cv.imwrite()。 这里,还需要再多做一点工作。

这次我们创建一个 VideoWriter 对象。我们应该指定输出文件名 (如:output.avi)。然后我们应该指定 FourCC 代码 (详情见下一段)。然后应该传递每秒帧数(fps)和帧大小。最后一个是 isColor 标志。 如果它的值为 True,编码器期望彩色帧,否则它适用于灰度帧。

FourCC 是一个用于指定视频编解码器的 4-byte 代码。可用代码的列表可以在 fourcc.org 上找到。它依赖于平台。以下编解码器对我来说很好。

  • In Fedora: DIVX, XVID, MJPG, X264, WMV1, WMV2. (XVID is more preferable. MJPG results in high size video. X264 gives very small size video)
  • In Windows: DIVX (More to be tested and added)
  • In OSX: MJPG (.mp4), DIVX (.avi), X264 (.mkv).

FourCC code is passed as ‘cv.VideoWriter_fourcc(‘M’,’J’,’P’,’G’)or cv.VideoWriter_fourcc(*’MJPG’)` for MJPG.

下面的代码从摄像机捕获,在垂直方向翻转每一帧,并保存视频。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import numpy as np
import cv2 as cv

cap = cv.VideoCapture(0)

# Define the codec and create VideoWriter object
fourcc = cv.VideoWriter_fourcc(*'XVID')
out = cv.VideoWriter('output.avi', fourcc, 20.0, (640, 480))

while cap.isOpened():
ret, frame = cap.read()
if not ret:
print("Can't receive frame (stream end?). Exiting ...")
break
frame = cv.flip(frame, 0)

# write the flipped frame
out.write(frame)

cv.imshow('frame', frame)
if cv.waitKey(1) == ord('q'):
break

# Release everything if job is finished
cap.release()
out.release()
cv.destroyAllWindows()
额外的资源
练习

[^注]: 本文翻译最初编辑于 2024.08.27,最终编辑于 2024.09.07


OpenCV中的绘图函数

目标
代码

在上述所有函数中,您将看到一些常见的参数,如下所示:

  • img : 要绘制形状的图像
  • color :形状的颜色。对于 BGR,请将其作为元组传递,例如:(255,0,0) 表示蓝色。对于灰度而言,只需传递标量值。
  • thickness : 线或圆等的粗细。如果为闭合图形(如圆形)传递 -1,它将填充形状。默认厚度 = 1
  • lineType :线路类型,是否为 8-connected、抗锯齿线等。*默认情况下,它是 8-connected。cv.LINE_AA 提供抗锯齿线,非常适合曲线。
绘制直线

要绘制一条线,您需要设置线的起点和终点坐标。我们将创建一个黑色图像,并在其上从左上角到右下角绘制一条蓝线。

1
2
3
4
5
6
7
8
import numpy as np
import cv2 as cv

# Create a black image
img = np.zeros((512,512,3), np.uint8)

# Draw a diagonal blue line with thickness of 5 px
cv.line(img,(0,0),(511,511),(255,0,0),5)
绘制矩形

要绘制矩形,您需要矩形的左上角和右下角。这次我们将在图像的右上角绘制一个绿色矩形。

1
cv.rectangle(img,(384,0),(510,128),(0,255,0),3)
绘制圆

要绘制圆,您需要其中心坐标和半径。我们将在上面绘制的矩形内画一个圆。

1
cv.circle(img,(447,63), 63, (0,0,255), -1)
绘制椭圆

要绘制椭圆,我们需要设置多个参数。一个参数是中心位置 (x,y)。下一个参数是轴长度 (长轴长度、短轴长度)。angle 是椭圆逆时针方向的旋转角度。startAngle 和 endAngle 表示从长轴开始按顺时针方向测量的椭圆弧的起点和终点。即,给出值 0 和 360 得到完整的椭圆。有关更多详细信息,请查看 cv.ellipse() 的文档。以下示例在图像中心绘制一个半椭圆。

1
cv.ellipse(img,(256,256),(100,50),0,0,180,255,-1)
绘制多边形

要绘制多边形,首先需要顶点的坐标。将这些点制作成形状为 ROWSx1x2 的数组,其中 ROWS 是顶点数,其类型应为 int32。在这里,我们绘制一个带有四个黄色顶点的小多边形。

1
2
3
pts = np.array([[10,5],[20,30],[70,20],[50,10]], np.int32)
pts = pts.reshape((-1,1,2))
cv.polylines(img,[pts],True,(0,255,255))

注意

如果第三个参数为 False,您将获得连接所有点的多段线,而不是闭合形状。

cv.polylines() 可用于绘制多条线。只需创建一个包含您要绘制的所有线条的列表,并将其传递给函数即可。所有线条都将单独绘制。与为每行调用 cv.line() 相比,绘制一组行是一种更好、更快的方法。

在图像中添加文本:

要将文本放入图像中,您需要指定以下内容。

  • 要写入的文本数据
  • 将坐标放在你想要放置的位置(即数据开始的左下角)。
  • 字体类型(查看 cv.putText() 文档了解支持的字体)
  • 字体比例 (指定字体的大小)
  • 常规内容,如 color、thickness、lineType 等。为了更好地查看,建议使用 lineType = cv.LINE_AA

我们将在我们的图像上以白色书写 OpenCV

1
2
font = cv.FONT_HERSHEY_SIMPLEX
cv.putText(img,'OpenCV',(10,500), font, 4,(255,255,255),2,cv.LINE_AA)
结果

是时候看看我们绘图的最终结果了。正如您在前几篇文章中所学习的,请显示图像以查看它。

其他资源
  1. ellipse(椭圆) 函数中使用的角度不是我们的圆角。有关更多详细信息,请访问此讨论
习题
  1. 尝试使用 OpenCV 中提供的绘图功能创建 OpenCV 的徽标。

[^注]: 本文翻译最初编辑于 2024.09.07


鼠标作为画笔

目标
简单演示

在这里,我们创建了一个简单的应用程序,它在图像双击图像的任何位置都会画一个圆圈。

首先,我们创建一个鼠标回调函数,该函数在鼠标事件发生时执行。鼠标事件可以是与鼠标相关的任何内容,例如左键按下、左键向上、左键双击等。它为我们提供了每个鼠标事件的坐标 (x,y)。有了这个活动和地点,我们可以做任何我们想做的事。要列出所有可用的事件,请在 Python 终端中运行以下代码:

1
2
3
import cv2 as cv
events = [i for i in dir(cv) if 'EVENT' in i]
print( events )

创建鼠标回调函数具有特定的格式,该格式在任何地方都是相同的。它仅在函数的作用上有所不同。所以我们的鼠标回调函数只做了一件事,它在我们双击的地方画了一个圆圈。所以请看下面的代码。代码从注释中不言自明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import numpy as np
import cv2 as cv

# mouse callback function
def draw_circle(event,x,y,flags,param):
if event == cv.EVENT_LBUTTONDBLCLK:
cv.circle(img,(x,y),100,(255,0,0),-1)

# Create a black image, a window and bind the function to window
img = np.zeros((512,512,3), np.uint8)
cv.namedWindow('image')
cv.setMouseCallback('image',draw_circle)

while(1):
cv.imshow('image',img)
if cv.waitKey(20) & 0xFF == 27:
break
cv.destroyAllWindows()
更高级的演示

现在我们寻求更好的应用程序。在这种情况下,我们像在 Paint 应用程序中一样通过拖动鼠标来绘制矩形或圆形(取决于我们选择的模式)。所以我们的鼠标回调函数有两部分,一部分是画矩形,一部分是画圆。这个具体示例对于创建和理解一些交互式应用程序(如对象跟踪、图像分割等)非常有帮助。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import numpy as np
import cv2 as cv

drawing = False # true if mouse is pressed
mode = True # if True, draw rectangle. Press 'm' to toggle to curve
ix,iy = -1,-1

# mouse callback function
def draw_circle(event,x,y,flags,param):
global ix,iy,drawing,mode

if event == cv.EVENT_LBUTTONDOWN:
drawing = True
ix,iy = x,y

elif event == cv.EVENT_MOUSEMOVE:
if drawing == True:
if mode == True:
cv.rectangle(img,(ix,iy),(x,y),(0,255,0),-1)
else:
cv.circle(img,(x,y),5,(0,0,255),-1)

elif event == cv.EVENT_LBUTTONUP:
drawing = False
if mode == True:
cv.rectangle(img,(ix,iy),(x,y),(0,255,0),-1)
else:
cv.circle(img,(x,y),5,(0,0,255),-1)

接下来,我们必须将这个鼠标回调函数绑定到 OpenCV 窗口。在主循环中,我们应该为键 ‘m’ 设置一个键盘绑定,以便在矩形和圆形之间切换。

1
2
3
4
5
6
7
8
9
10
11
12
13
img = np.zeros((512,512,3), np.uint8)
cv.namedWindow('image')
cv.setMouseCallback('image',draw_circle)

while(1):
cv.imshow('image',img)
k = cv.waitKey(1) & 0xFF
if k == ord('m'):
mode = not mode
elif k == 27:
break

cv.destroyAllWindows()

其他资源

习题
  1. 在上一个例子中,我们绘制了填充矩形。修改代码以绘制未填充的矩形。

[^注]: 本文翻译最初编辑于 2024.09.07


Trackbar 作为调色板

目标
代码演示

在这里,我们将创建一个简单的应用程序,显示您指定的颜色。您有一个显示颜色的窗口和三个跟踪条来指定 B、G、R 颜色中的每一种。滑动跟踪条并相应地更改窗口颜色。默认情况下,初始颜色将设置为 Black。

对于 cv.createTrackbar() 函数,第一个参数是跟踪栏名称,第二个参数是它所附加的窗口名称,第三个参数是默认值,第四个参数是最大值,第五个参数是每次跟踪栏值更改时执行的回调函数。回调函数始终具有一个默认参数,即跟踪栏位置。在我们的例子中,function 什么都不做,所以我们简单地传递。

跟踪栏的另一个重要应用是将其用作按钮或开关。默认情况下,OpenCV 没有按钮功能。因此,您可以使用 trackbar 来获得此类功能。在我们的应用程序中,我们创建了一个开关,该应用程序仅在开关打开时工作,否则屏幕始终为黑色。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import numpy as np
import cv2 as cv

def nothing(x):
pass

# Create a black image, a window
img = np.zeros((300,512,3), np.uint8)
cv.namedWindow('image')

# create trackbars for color change
cv.createTrackbar('R','image',0,255,nothing)

cv.createTrackbar('G','image',0,255,nothing)
cv.createTrackbar('B','image',0,255,nothing)

# create switch for ON/OFF functionality
switch = '0 : OFF \n1 : ON'
cv.createTrackbar(switch, 'image',0,1,nothing)

while(1):
cv.imshow('image',img)
k = cv.waitKey(1) & 0xFF
if k == 27:
break

# get current positions of four trackbars
r = cv.getTrackbarPos('R','image')
g = cv.getTrackbarPos('G','image')
b = cv.getTrackbarPos('B','image')
s = cv.getTrackbarPos(switch,'image')

if s == 0:
img[:] = 0
else:
img[:] = [b,g,r]

cv.destroyAllWindows()

应用程序的屏幕截图如下所示:

image

习题
  1. 使用跟踪条创建具有可调整颜色和画笔半径的 Paint 应用程序。有关绘图,请参阅前面的鼠标操作教程。

[^注]: 本文翻译最初编辑于 2024.09.07


Core Operations - 核心业务

(在本节中,您将学习图像的基本操作,如像素编辑,几何变换,代码优化,一些数学工具等)

对图像的基本操作

(学习读取和编辑像素值,处理图像ROl等基本操作)

目标

学习:

  • 访问像素值并对其进行修改
  • 访问图像属性
  • 设置感兴趣区域 (ROI)
  • 拆分和合并图像

本节中几乎所有的操作都主要与 Numpy 有关,而不是 OpenCV。要使用 OpenCV 编写更好的优化代码,需要对 Numpy 有很好的了解。

( 示例将显示在 Python 终端中,因为它们中的大多数只是单行代码 )

访问和修改像素值

让我们先加载一张彩色图片:

1
2
3
4
5
>>> import numpy as np
>>> import cv2 as cv

>>> img = cv.imread('messi5.jpg')
>>> assert img is not None, "file could not be read, check with os.path.exists()"

您可以通过像素值的行和列坐标来访问像素值。对于 BGR 图像,它返回 Blue、Green、Red 值的数组。对于灰度图像,仅返回相应的强度。

1
2
3
4
5
6
7
8
>>> px = img[100,100]
>>> print( px )
[157 166 200]

# accessing only blue pixel
>>> blue = img[100,100,0]
>>> print( blue )
157

您可以用相同的方式修改像素值。

1
2
3
>>> img[100,100] = [255,255,255]
>>> print( img[100,100] )
[255 255 255]

警告

Numpy 是一个用于快速数组计算的优化库。因此,简单地访问每个像素值并对其进行修改将非常缓慢,并且不建议这样做。

注意

上述方法通常用于选择数组的区域,例如前 5 行和后 3 列。对于单个像素访问,Numpy 数组方法 array.item() 和 array.itemset() 被认为更好。但是,它们总是返回一个标量,因此如果您想访问所有 B,G,R 值,则需要为每个值单独调用 array.item()。

更好的像素访问和编辑方法:

1
2
3
4
5
6
7
8
# accessing RED value
>>> img.item(10,10,2)
59

# modifying RED value
>>> img.itemset((10,10,2),100)
>>> img.item(10,10,2)
100
访问图像属性

图像属性包括行数、列数和通道数;图像数据类型;像素数;等。

图像的形状由 img.shape 访问。它返回行数、列数和通道数的 Tuples(如果图像是彩色的):

1
2
>>> print( img.shape )
(342, 548, 3)

注意

如果图像是灰度图像,则返回的元组仅包含行数和列数,因此检查加载的图像是灰度还是彩色的好方法。

像素总数的访问方式为 :img.size

1
2
>>> print( img.size )
562248

Image 数据类型由 ‘img.dtype’ 获取:

1
2
>>> print( img.dtype )
uint8

注意

img.dtype 在调试时非常重要,因为 OpenCV-Python 代码中的大量错误都是由无效的数据类型引起的。

图像 ROI

有时,您将不得不处理图像的某些区域。对于图像中的眼睛检测,首先对整个图像进行人脸检测。获得人脸后,我们单独选择人脸区域并搜索其中的眼睛,而不是搜索整个图像。它提高了准确性(因为眼睛总是:D人脸上)和性能(因为我们在小区域内搜索)。

ROI 再次使用 Numpy 索引获得。在这里,我选择球并将其复制到图像中的另一个区域:

1
2
>>> ball = img[280:340, 330:390]
>>> img[273:333, 100:160] = ball

检查下面的结果:

image

拆分和合并图像通道

有时,您需要单独处理图像的 B、G、R 通道。在这种情况下,您需要将 BGR 图像拆分为单个通道。在其他情况下,您可能需要加入这些单独的通道以创建 BGR 图像。您可以简单地通过以下方式执行此操作:

1
2
>>> b,g,r = cv.split(img)
>>> img = cv.merge((b,g,r))

1
>>> b = img[:,:,0]

假设您要将所有红色像素设置为零 - 您无需先拆分通道。Numpy 索引更快:

1
>>> img[:,:,2] = 0

警告

cv.split() 是一个昂贵的操作(就时间而言)。因此,请仅在必要时使用它。否则选择 Numpy 索引。

为图像创建边框(填充)

如果你想在图像周围创建一个边框,比如相框,你可以使用 cv.copyMakeBorder()但它有更多的应用,用于卷积运算、零填充等。此函数采用以下参数:

  • src - 输入图像
  • topbottomleftright - 相应方向的边框宽度(以像素数为单位)
  • borderType - 定义要添加的边框类型的标志。它可以是以下类型:
  • value - 如果 border type 为 cv.BORDER_CONSTANT,则边框的颜色

下面是一个示例代码,演示了所有这些边框类型,以便更好地理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt

BLUE = [255,0,0]

img1 = cv.imread('opencv-logo.png')
assert img1 is not None, "file could not be read, check with os.path.exists()"

replicate = cv.copyMakeBorder(img1,10,10,10,10,cv.BORDER_REPLICATE)
reflect = cv.copyMakeBorder(img1,10,10,10,10,cv.BORDER_REFLECT)
reflect101 = cv.copyMakeBorder(img1,10,10,10,10,cv.BORDER_REFLECT_101)
wrap = cv.copyMakeBorder(img1,10,10,10,10,cv.BORDER_WRAP)
constant= cv.copyMakeBorder(img1,10,10,10,10,cv.BORDER_CONSTANT,value=BLUE)

plt.subplot(231),plt.imshow(img1,'gray'),plt.title('ORIGINAL')
plt.subplot(232),plt.imshow(replicate,'gray'),plt.title('REPLICATE')
plt.subplot(233),plt.imshow(reflect,'gray'),plt.title('REFLECT')
plt.subplot(234),plt.imshow(reflect101,'gray'),plt.title('REFLECT_101')
plt.subplot(235),plt.imshow(wrap,'gray'),plt.title('WRAP')
plt.subplot(236),plt.imshow(constant,'gray'),plt.title('CONSTANT')

plt.show()

请参阅下面的结果。(图像使用 matplotlib 显示。所以 RED 和 BLUE 通道将互换):

image

其他资源
习题

图像的算术运算

(对图像执行算术运算)

目标
  • 学习图像上的几种算术运算,如加法、减法、按位运算等。
  • 了解这些函数:cv.add()**, **cv.addWeighted(), 等。
图像添加

您可以使用 OpenCV 函数 cv.add() 添加两张图像,或者简单地通过 numpy 操作 res = img1 + img2。两个图像应具有相同的深度和类型,或者第二个图像只能是标量值。

注意

OpenCV 加法和 Numpy 加法是有区别的。OpenCV 加法是饱和运算,而 Numpy 加法是模运算。

例如,请考虑以下示例:

1
2
3
4
5
6
7
8
>>> x = np.uint8([250])
>>> y = np.uint8([10])

>>> print( cv.add(x,y) ) # 250+10 = 260 => 255
[[255]]

>>> print( x+y ) # 250+10 = 260 % 256 = 4
[4]

当您添加两张图片时,这将更加明显。坚持使用 OpenCV 函数,因为它们会提供更好的结果。

图像混合

这也是图像添加,但为了给人一种混合或透明的感觉,对图像赋予了不同的权重。根据以下公式添加图像:
$$
g(x)=(1-\alpha)f_0(x)+\alpha f_1(x)
$$
通过变化α从0→1中,您可以在一个图像到另一个图像之间执行很酷的过渡。

在这里,我拍了两张图片混合在一起。第一个图像的权重为 0.7,第二个图像的权重为 0.3。cv.addWeighted() 将以下公式应用于图像:
$$
dst=\alpha\cdot img1+\beta\cdot img2+\gamma
$$
这里γ视为零。

1
2
3
4
5
6
7
8
9
10
img1 = cv.imread('ml.png')
img2 = cv.imread('opencv-logo.png')
assert img1 is not None, "file could not be read, check with os.path.exists()"
assert img2 is not None, "file could not be read, check with os.path.exists()"

dst = cv.addWeighted(img1,0.7,img2,0.3,0)

cv.imshow('dst',dst)
cv.waitKey(0)
cv.destroyAllWindows()

检查下面的结果:

image

按位运算

这包括按位 AND、OR、NOT 和 XOR 运算。它们在提取图像的任何部分(我们将在后面的章节中看到)、定义和使用非矩形 ROI 等时非常有用。下面我们将看到如何更改图像特定区域的示例。

我想将 OpenCV 徽标放在图像上方。如果我添加两张图片,它会改变颜色。如果我混合它们,我会得到一个透明的效果。但我希望它是不透明的。如果它是一个矩形区域,我可以像上一章中那样使用 ROI。但 OpenCV 标志不是矩形。所以你可以用按位运算来实现,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# Load two images
img1 = cv.imread('messi5.jpg')
img2 = cv.imread('opencv-logo-white.png')
assert img1 is not None, "file could not be read, check with os.path.exists()"
assert img2 is not None, "file could not be read, check with os.path.exists()"

# I want to put logo on top-left corner, So I create a ROI
rows,cols,channels = img2.shape
roi = img1[0:rows, 0:cols]

# Now create a mask of logo and create its inverse mask also
img2gray = cv.cvtColor(img2,cv.COLOR_BGR2GRAY)
ret, mask = cv.threshold(img2gray, 10, 255, cv.THRESH_BINARY)
mask_inv = cv.bitwise_not(mask)

# Now black-out the area of logo in ROI
img1_bg = cv.bitwise_and(roi,roi,mask = mask_inv)

# Take only region of logo from logo image.
img2_fg = cv.bitwise_and(img2,img2,mask = mask)

# Put logo in ROI and modify the main image
dst = cv.add(img1_bg,img2_fg)
img1[0:rows, 0:cols ] = dst

cv.imshow('res',img1)
cv.waitKey(0)
cv.destroyAllWindows()

请参阅下面的结果。左图显示了我们创建的蒙版。右图显示最终结果。为了更好地理解,请显示上述代码中的所有中间图像,尤其是 img1_bg 和 img2_fg。

image

其他资源
习题
  1. 使用 cv.addWeighted 函数在文件夹中创建图像的幻灯片,并在图像之间平滑过渡

性能测量和改进技术

(找到解决方案很重要。但以最快的方式获得解决方案更为重要。学会检查代码的运行速度,优化代码等等。)

目标

在图像处理中,由于您每秒要处理大量操作,因此您的代码不仅必须提供正确的解决方案,而且还必须以最快的方式提供解决方案。因此,在本章中,您将学习:

除了 OpenCV,Python 还提供了一个模块 time,有助于测量执行时间。另一个模块配置文件有助于获取有关代码的详细报告,例如代码中每个函数花费的时间、函数被调用的次数等。但是,如果您使用的是 IPython,所有这些功能都以用户友好的方式集成。我们将看到一些重要的 VPN,有关更多详细信息,请查看 Additional Resources 部分中的链接。

使用 OpenCV 测量性能

cv.getTickCount 函数返回引用事件(如机器打开的那一刻)到调用此函数的时钟周期数。因此,如果您在函数执行之前和之后调用它,您将获得用于执行函数的 clock-cycles 数。

cv.getTickFrequency 函数返回 clock-cycles 的频率,或每秒的 clock-cycles 数。因此,要查找以秒为单位的执行时间,您可以执行以下操作:

1
2
3
4
e1 = cv.getTickCount()
# your code execution
e2 = cv.getTickCount()
time = (e2 - e1)/ cv.getTickFrequency()

我们将通过以下示例进行演示。以下示例对奇数大小(从 5 到 49)的内核应用中位数筛选。(不要担心结果会是什么样子 - 这不是我们的目标):

1
2
3
4
5
6
7
8
9
10
11
img1 = cv.imread('messi5.jpg')
assert img1 is not None, "file could not be read, check with os.path.exists()"

e1 = cv.getTickCount()
for i in range(5,49,2):
img1 = cv.medianBlur(img1,i)
e2 = cv.getTickCount()
t = (e2 - e1)/cv.getTickFrequency()
print( t )

# Result I got is 0.521107655 seconds

注意

你可以对 time 模块做同样的事情。使用 time.time() 函数,而不是 cv.getTickCount。然后取两次的差值。

OpenCV 中的默认优化

许多 OpenCV 函数都使用 SSE2、AVX 等进行了优化。它还包含未优化的代码。因此,如果我们的系统支持这些功能,我们应该利用它们(几乎所有现代处理器都支持它们)。默认情况下,它在编译时处于启用状态。因此,如果启用,OpenCV 将运行优化的代码,否则它将运行未优化的代码。您可以使用 cv.useOptimized() 来检查它是否被启用/禁用,并使用 cv.setUseOptimized() 来启用/禁用它。让我们看一个简单的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# check if optimization is enabled
In [5]: cv.useOptimized()
Out[5]: True

In [6]: %timeit res = cv.medianBlur(img,49)
10 loops, best of 3: 34.9 ms per loop

# Disable it
In [7]: cv.setUseOptimized(False)

In [8]: cv.useOptimized()
Out[8]: False

In [9]: %timeit res = cv.medianBlur(img,49)
10 loops, best of 3: 64.1 ms per loop

如您所见,优化的中值滤波比未优化的版本快 2 倍。如果检查其来源,您可以看到中值滤波是 SIMD 优化的。因此,您可以使用它来在代码顶部启用优化(请记住,默认情况下它是启用的)。

在 IPython 中测量性能

有时,您可能需要比较两个类似操作的性能。IPython 为您提供了一个神奇的命令 timeit 来执行此操作。它会多次运行代码以获得更准确的结果。同样,它适用于测量单行代码。

例如,您知道以下哪个加法运算更好吗,x = 5;y = x**2, x = 5;y = x*x, x = np.uint8([5]);y = x*x,还是 y = np.square(x)?我们将在 IPython shell 中使用 timeit 来找出答案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
In [10]: x = 5

In [11]: %timeit y=x**2
10000000 loops, best of 3: 73 ns per loop

In [12]: %timeit y=x*x
10000000 loops, best of 3: 58.3 ns per loop

In [15]: z = np.uint8([5])

In [17]: %timeit y=z*z
1000000 loops, best of 3: 1.25 us per loop

In [19]: %timeit y=np.square(z)
1000000 loops, best of 3: 1.16 us per loop

你可以看到, x = 5 ;y = x*x 是最快的,与 Numpy 相比,它快了大约 20 倍。如果您还考虑数组创建,它可能会快 100 倍。很酷,对吧?(Numpy 开发人员正在努力解决这个问题)

注意

Python 标量运算比 Numpy 标量运算更快。因此,对于包含一个或两个元素的操作,Python 标量优于 Numpy 数组。当数组的大小稍大时,Numpy 具有优势。

我们将再尝试一个例子。这一次,我们将比较 cv.countNonZero()np.count_nonzero() 对同一图像的性能。

1
2
3
4
5
In [35]: %timeit z = cv.countNonZero(img)
100000 loops, best of 3: 15.8 us per loop

In [36]: %timeit z = np.count_nonzero(img)
1000 loops, best of 3: 370 us per loop

你看,OpenCV 函数比 Numpy 函数快近 25 倍。

注意

通常,OpenCV 函数比 Numpy 函数更快。所以对于相同的操作,OpenCV 函数是首选。但是,可能会有例外,尤其是当 Numpy 使用视图而不是副本时。

更多 IPython 魔术命令

还有其他几个魔术命令来测量性能、分析、行分析、内存测量等。他们都有据可查。因此,此处仅提供指向这些文档的链接。建议有兴趣的读者尝试一下。

性能优化技术

有几种技术和编码方法可以充分利用 Python 和 Numpy 的最大性能。这里只注明相关的,并给出重要来源的链接。这里要注意的主要一点是,首先尝试以简单的方式实现算法。一旦它开始工作,就对其进行分析,找到瓶颈并对其进行优化。

  1. 尽量避免在 Python 中使用循环,尤其是双/三循环等。它们本质上是缓慢的。
  2. 尽可能地矢量化算法/代码,因为 Numpy 和 OpenCV 针对矢量运算进行了优化。
  3. 利用缓存一致性。
  4. 除非必要,否则切勿复制数组。尝试改用视图。数组复制是一项成本高昂的操作。

如果在执行所有这些操作后代码仍然很慢,或者如果不可避免地使用大循环,请使用 Cython 等其他库来使其更快。

其他资源
  1. Python 优化技术
  2. Scipy 讲义 - 高级 Numpy
  3. IPython 中的计时和分析
习题

Image Processing in OpenCV - OpenCV中的图像处理

(在本节中,您将学习OpencV中不同的图像处理函数。)

改变颜色

(学会在不同的色彩空间之间改变图像。另外学习在视频中跟踪有颜色的物体。)

目标
  • 在本教程中,您将学习如何将图像从一个色彩空间转换为另一个色彩空间,例如 BGR ↔ Gray,BGR ↔ HSV 等
  • 除此之外,我们还将创建一个应用程序来提取视频中的彩色对象
  • 您将学习以下函数:cv.cvtColor(), cv.inRange() 等。
更改色彩空间

OpenCV 中有 150 多种颜色空间转换方法。但我们只研究两个,它们是使用最广泛的:BGR ↔ Gray 和 BGR ↔ HSV。

对于颜色转换,我们使用函数 cv.cvtColor(input_image, flag),其中 flag 确定转换的类型。

对于 BGR → Gray 转换,我们使用 cv.COLOR_BGR2GRAY 标志。BGR → HSV 也是如此,我们使用 cv.COLOR_BGR2HSV 标志。要获取其他标志,只需在 Python 终端中运行以下命令:

1
2
3
>>> 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 图像阈值设置为蓝色范围
  • 现在单独提取蓝色对象,我们可以对该图像执行任何操作。

下面是详细注释的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import cv2 as cv
import numpy as np

cap = cv.VideoCapture(0)

while(1):

# Take each frame
_, frame = cap.read()

# Convert BGR to HSV
hsv = cv.cvtColor(frame, cv.COLOR_BGR2HSV)

# define range of blue color in HSV
lower_blue = np.array([110,50,50])
upper_blue = np.array([130,255,255])

# Threshold the HSV image to get only blue colors
mask = cv.inRange(hsv, lower_blue, upper_blue)

# Bitwise-AND mask and original image
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()

下图显示了对蓝色对象的跟踪:

image

注意

图像中有一些杂色。我们将在后面的章节中看到如何删除它。

这是对象跟踪中最简单的方法。一旦你学会了等值线的功能,你就可以做很多事情,比如找到对象的质心并使用它来跟踪对象,只需在相机前移动你的手就可以画图表,以及其他有趣的事情。

如何找到要跟踪的 HSV 值?

这是 stackoverflow.com 中的常见问题。这非常简单,您可以使用相同的函数 cv.cvtColor()。您无需传递图像,只需传递所需的 BGR 值。例如,要查找 Green 的 HSV 值,请在 Python 终端中尝试以下命令:

1
2
3
4
>>> 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 范围。

其他资源
习题
  1. 尝试找到一种方法来提取多个彩色对象,例如,同时提取红色、蓝色和绿色对象。

图像的几何变换

(学习应用不同的几何变换图像,如旋转,平移等。)

目标
  • 学习对图像应用不同的几何变换,如平移、旋转、仿射变换等。
  • 您将看到以下函数:cv.getPerspectiveTransform
转换

OpenCV 提供了两个变换函数,cv.warpAffine and cv.warpPerspective,**你可以使用它们执行各种变换。 **cv.warpAffine 采用 2x3 变换矩阵,而 cv.warpPerspective 采用 3x3 变换矩阵作为输入。

缩放

缩放只是调整图像的大小。为此 OpenCV 附带了一个函数 cv.resize()。可以手动指定图像的大小,也可以指定缩放因子。使用不同的插值方法。首选的插值方法是 cv.INTER_AREA 用于缩小,cv.INTER_CUBIC (慢速) & cv.INTER_LINEAR 用于缩放。默认情况下,插值方法 cv.INTER_LINEAR 用于所有大小调整目的。您可以使用以下任一方法调整输入图像的大小:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import numpy as np

import cv2 as cv



img = [cv.imread](https://docs.opencv.org/4.10.0/d4/da8/group__imgcodecs.html#gab32ee19e22660912565f8140d0f675a8)('messi5.jpg')

assert img is not None, "file could not be read, check with os.path.exists()"



res = [cv.resize](https://docs.opencv.org/4.10.0/da/d54/group__imgproc__transform.html#ga47a974309e9102f5f08231edc7e7529d)(img,None,fx=2, fy=2, interpolation = cv.INTER_CUBIC)



\#OR



height, width = img.shape[:2]

res = [cv.resize](https://docs.opencv.org/4.10.0/da/d54/group__imgproc__transform.html#ga47a974309e9102f5f08231edc7e7529d)(img,(2*width, 2*height), interpolation = cv.INTER_CUBIC)

平移

平移是物体位置的移动。如果你知道 (x,y) 方向的位移并设为 (tx,ty), you can create the transformation matrix M as follows:

M=[10tx01ty]

You can take make it into a Numpy array of type np.float32 and pass it into the cv.warpAffine() function. See the below example for a shift of (100,50):

import numpy as np

import cv2 as cv

img = cv.imread(‘messi5.jpg’, cv.IMREAD_GRAYSCALE)

assert img is not None, “file could not be read, check with os.path.exists()”

rows,cols = img.shape

M = np.float32([[1,0,100],[0,1,50]])

dst = cv.warpAffine(img,M,(cols,rows))

cv.imshow(‘img’,dst)

cv.waitKey(0)

cv.destroyAllWindows()

warning

The third argument of the cv.warpAffine() function is the size of the output image, which should be in the form of (width, height). Remember width = number of columns, and height = number of rows.

See the result below:

img

image

Rotation

Rotation of an image for an angle θ is achieved by the transformation matrix of the form

M=[cosθ−sinθsinθcosθ]

But OpenCV provides scaled rotation with adjustable center of rotation so that you can rotate at any location you prefer. The modified transformation matrix is given by

[αβ(1−α)⋅center.x−β⋅center.y−βαβ⋅center.x+(1−α)⋅center.y]

where:

α=scale⋅cos⁡θ,β=scale⋅sin⁡θ

To find this transformation matrix, OpenCV provides a function, cv.getRotationMatrix2D. Check out the below example which rotates the image by 90 degree with respect to center without any scaling.

img = cv.imread(‘messi5.jpg’, cv.IMREAD_GRAYSCALE)

assert img is not None, “file could not be read, check with os.path.exists()”

rows,cols = img.shape

# cols-1 and rows-1 are the coordinate limits.

M = cv.getRotationMatrix2D(((cols-1)/2.0,(rows-1)/2.0),90,1)

dst = cv.warpAffine(img,M,(cols,rows))

See the result:

img

image

Affine Transformation

In affine transformation, all parallel lines in the original image will still be parallel in the output image. To find the transformation matrix, we need three points from the input image and their corresponding locations in the output image. Then cv.getAffineTransform will create a 2x3 matrix which is to be passed to cv.warpAffine.

Check the below example, and also look at the points I selected (which are marked in green color):

img = cv.imread(‘drawing.png’)

assert img is not None, “file could not be read, check with os.path.exists()”

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()

See the result:

img

image

Perspective Transformation

For perspective transformation, you need a 3x3 transformation matrix. Straight lines will remain straight even after the transformation. To find this transformation matrix, you need 4 points on the input image and corresponding points on the output image. Among these 4 points, 3 of them should not be collinear. Then the transformation matrix can be found by the function cv.getPerspectiveTransform. Then apply cv.warpPerspective with this 3x3 transformation matrix.

See the code below:

img = cv.imread(‘sudoku.png’)

assert img is not None, “file could not be read, check with os.path.exists()”

rows,cols,ch = img.shape

pts1 = np.float32([[56,65],[368,52],[28,387],[389,390]])

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()

Result:

img

image

Additional Resources

  1. “Computer Vision: Algorithms and Applications”, Richard Szeliski

Exercises

Generated on Sun Jun 2 2024 21:52:14 for OpenCV by doxygen 1.9.8