logo

OpenCV 图像处理

王哲峰 / 2022-03-10


目录

OpenCV 简介

OpenCV 基本信息

OpenCV (Open Source Computer Vision Library),一个开源的计算机视觉库, 官方网站为 http://opencv.org。它提供了很多函数, 这些函数非常高效地实现了计算机视觉算法,从最基本的滤波到高级的物体检测皆有涵盖。

1999 年,Gary Bradski(加里·布拉德斯基)当时在英特尔任职, 怀着通过为计算机视觉和人工智能的从业者提供稳定的基础架构并以此来推动产业发展的美好愿景, 他启动了 OpenCV 项目。

IPPICV 加速

OpenCV-Python

OpenCV-Python 是用于 OpenCV 的 Python API,结合了 OpenCV C++ API 和 Python 的最佳特性。

OpenCV-Python 依赖于 Numpy 库,所有的 OpenCV 数组结构都能够与 Numpy 数组相互转换, 这也使得使用 OpenCV 与 Numpy 的其他库的集成变得更加容易。

OpenCV-Python 依赖

OpenCV-Python 安装

使用预构建的二进制文件和源代码

使用非官方的 Python 预构建 OpenCV 包

在 macOS、Windows、Linux 环境中安装:

$ pip install opencv-python
$ pip install opencv-contrib-python

OpenCV-Python 调用

cv2 模块内采用的是面向对象的编程方式,而 cv 模块内更多采用的是面向过程的编程方式。

import cv2 as cv
print(cv.__version__)

OpenCV 图像基本操作

图像读取

API

示例

import cv2

img = cv2.imread(
    "D:/projects/computer_vision/cvproj/data/images/lena.jpg"
)

print(type(img))
print(img.shape)
print(img)
<class 'numpy.ndarray'>
(256, 256, 3)
[[[121 135 223]
  [123 137 225]
  [124 138 226]
  ...
  [132 144 238]
  [136 152 241]
  [120 137 224]]

 [[123 137 225]
  [122 139 226]
  [125 139 227]
  ...
  [134 142 235]
  [134 144 232]
  [114 124 211]]

 [[122 138 227]
  [120 138 227]
  [123 139 228]
  ...
  [148 149 239]
  [142 143 229]
  [119 121 205]]

 ...

 [[ 61  27  91]
  [ 66  32  96]
  [ 70  34 100]
  ...
  [ 74  51 136]
  [ 81  62 149]
  [ 84  67 154]]

 [[ 57  26  87]
  [ 62  28  92]
  [ 67  31  97]
  ...
  [ 82  60 148]
  [ 85  67 156]
  [ 85  69 157]]

 [[ 53  22  83]
  [ 58  24  88]
  [ 62  26  92]
  ...
  [ 94  75 162]
  [ 96  78 169]
  [ 92  75 166]]]

图像显示

API

  1. cv2.imshow(winname, mat)
  1. cv2.waitKey(delay)

cv2.destroyAllWindows()

Note

由于 OpenCV 和 Matplotlib 具有不同的原色顺序, OpenCV 以 BGR 的形式读取图像, Matplotlib 以 RGB 的形式读取图像, 因此为了正常显示图像,在用 Matplotlib 显示图像时, 需要将图像转换为 Matplotlib 的形式。

Jupyter Notebook

OpenCV 显示图像:

import cv2

# 读取图像
img_bgr = cv2.imread("img.jpg")

# 显示图像
cv2.imshow("image", img_bgr)
cv2.waitKey(0)
cv2.destroyAllWindows()

Matplotlib 显示图像:

import matplotlib.pyplot as plt
import cv2

# 读取图像
img_bgr = cv2.imread("img.jpg")

# 将 opencv 图像转换为 matplotlib 的形式
img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
plt.imshow("image", img_rgb)
cv2.waitKey(0)
cv2.destroyAllWindows()

Python Script

import cv2

img = cv2.imread("img.jpg")

while True:
    cv2.imshow("image", img)
    if cv2.waitKey(1) & 0xFF == 27:
        break
cv2.destroyAllWindows()

图像保存

API

cv2.imwrite()

示例

import cv2

img = cv2.imread("img.jpg")
cv2.imwrite("final_image.png", img)

像素处理

像素是图像构成的基本单位,像素处理是图像处理的基本操作,可以通过索引对图像内的元素进行访问和处理。

二值图像及灰度图像

OpenCV 中的最小的数据类型是无符号的 8 位二进制数,其最小值是 0,最大值是 255。 其使用 8 位二进制数的最小值 0 表示二值图像中的黑色, 使用 8 位二进制数的最大值 255 表示二值图像中的白色。 因此,可以将二值图像理解为特殊的灰度图像,其像素值仅有最大值 255 和最小值 0。 因此,下文仅考虑灰度图像的读取和修改等,不再单独对二值图像进行讨论。

可以将图像理解为一个矩阵,在面向 Python 的 OpenCV 中,图像就是 Numpy 库中的数组(numpy.ndarray), 一个灰度图象是一个二位数组,可以通过索引访问其中的像素值。

示例 1:

为了方便理解,首先使用 Numpy 库来生成一个 $8 \times 8$ 大小的数组,用来模拟一个黑色图像,并对其及进行简单处理。

import numpy as np
import cv2

# 生成 8×8 的黑色图像
img = np.zeros((8, 8), dtype = np.uint8)
print(f"img=\n{img}")
cv2.imshow("one", img)

print(f"读取像素点 img[0, 3]={img[0, 3]}")
img[0, 3] = 255
print(f"修改后 img=\n{img}")

print(f"读取修改后像素点 img[0, 3]={img[0, 3]}")
cv2.imshow("two", img)
cv2.waitKey()
cv2.destroyAllWindows()
img=
[[0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0]]

img

读取像素点 img[0, 3]=0

修改后 img=
[[  0   0   0 255   0   0   0   0]
 [  0   0   0   0   0   0   0   0]
 [  0   0   0   0   0   0   0   0]
 [  0   0   0   0   0   0   0   0]
 [  0   0   0   0   0   0   0   0]
 [  0   0   0   0   0   0   0   0]
 [  0   0   0   0   0   0   0   0]
 [  0   0   0   0   0   0   0   0]]

读取修改后像素点 img[0, 3]=255

img

示例 2:

img = cv2.imread("D:/projects/computer_vision/cvproj/data/images/lena_gray.bmp", 0)
cv2.imshow("before", img)

print(f"img 尺寸:{img.shape}")
print(f"img[50, 90] 原始值:{img[50, 90]}")
img[10:100, 80:100] = 255
print(f"img[50, 90] 修改值:{img[50, 90]}")
cv2.imshow("after", img)

cv2.waitKey()
cv2.destroyAllWindows()

img 尺寸:(512, 512)
img[50, 90] 原始值:105
img[50, 90] 修改值:255

彩色图像

OpenCV 在处理 RGB 模式的彩色图像时,会按照行方向依次分别读取该 RGB 图像像素点的 B 通道、 G 通道、R 通道的像素值,并将像素值以行为单位存储在 ndarray 的列中。

例如,有一幅大小为 R 行 $\times$ C 列的原始 RGB 图像,其在 OpenCV 内以 BGR 模式的三维数组形式存储, 可以使用表达式访问数组内的值。可以使用 image[0, 0, 0] 访问 image 图像 B 通道内第 0 行第 0 列上的像素点, 其中:

示例:

import cv2

img = cv2.imread("D:/projects/computer_vision/cvproj/data/images/lena.png")
cv2.imshow("before", img)

print(f"访问 img[0, 0] = {img[0, 0]}")
print(f"访问 img[0, 0, 0] = {img[0, 0, 0]}")
print(f"访问 img[0, 0, 1] = {img[0, 0, 1]}")
print(f"访问 img[0, 0, 2] = {img[0, 0, 2]}")
print(f"访问 img[50, 0] = {img[50, 0]}")
print(f"访问 img[100, 0] = {img[100, 0]}")

# 区域 1:白色
img[0:50, 0:100, 0:3] = 255

# 区域 2:灰色
img[50:100, 0:100, 0:3] = 128

# 区域 3:黑色
img[100:150, 0:100, 0:3] = 0

# 区域 4:红色
img[150:200, 0:100] = [0, 0, 255]

cv2.imshow("after", img)
print(f"修改后 img[0, 0] = {img[0, 0]}")
print(f"修改后 img[0, 0, 0] = {img[0, 0, 0]}")
print(f"修改后 img[0, 0, 1] = {img[0, 0, 1]}")
print(f"修改后 img[0, 0, 2] = {img[0, 0, 2]}")
print(f"修改后 img[50, 0] = {img[50, 0]}")
print(f"修改后 img[100, 0] = {img[100, 0]}")

cv2.waitKey()
cv2.destroyAllWindows()

img

访问 img[0, 0] = [121 135 223]
访问 img[0, 0, 0] = 121
访问 img[0, 0, 1] = 135
访问 img[0, 0, 2] = 223
访问 img[50, 0] = [ 70  49 151]
访问 img[100, 0] = [ 87  70 181]

img

修改后 img[0, 0] = [255 255 255]
修改后 img[0, 0, 0] = 255
修改后 img[0, 0, 1] = 255
修改后 img[0, 0, 2] = 255
修改后 img[50, 0] = [128 128 128]
修改后 img[100, 0] = [0 0 0]

通道处理

RGB 图像是由 R 通道、G 通道、B 通道三个通道构成的。 需要注意的是,OpenCV 中的通道是按照 B->G->R 通道的顺序存储的。 在图像处理过程中,可以根据需要对通道进行拆分和合并。

通道拆分

针对 RGB 图像,可以分别拆分出其 R 通道、G 通道、B 通道。 在 OpenCV 中,通过索引可以直接将各个通道从图像内提出来。 例如,针对 OpenCV 内的 BGR 图像 img, 可用如下语句分别从中提取 B 通道、G 通道、R 通道:

b = img[:, :, 0]
g = img[:, :, 1]
r = img[:, :, 2]

示例 1:演示图像通道拆分及通道改变对彩色图像的影响。

import cv2

lena = cv2.imread("D:/projects/computer_vision/cvproj/data/images/lena.png")
cv2.imshow("lena", lena)

# 通道拆分
b = lena[:, :, 0]
g = lena[:, :, 1]
r = lena[:, :, 2]

cv2.imshow("b", b)
cv2.imshow("g", g)
cv2.imshow("r", r)

img

# 通道值改变
lena[:, :, 0] = 0
cv2.imshow("lenab0", lena)
lena[:, :, 1] = 0
cv2.imshow("lenab0g0", lena)

示例 2:除了使用索引,还可以使用函数 cv2.split() 来拆分图像的通道:

b, g, r = cv2.split(img)

通道合并

通道合并是通道拆分的逆过程,通过合并通道可以将三个通道的灰度图像合并成一副彩色图像。 函数 cv2.merge() 可以实现通道合并。例如,使用函数 cv2.merge() 将 B 通道图像 b、 G 通道图像 b 和 R 通道图像 r 这三幅通道图像合并为一幅 RGB 的三通道彩色图像。 实现语句为:

bgr = cv2.merge([b, g, r])

示例:

import cv2

lena = cv2.imread("D:/projects/computer_vision/cvproj/data/images/lena.png")

# 通道拆分
b, g, r = cv2.split(lena)

# 通道合并
bgr = cv2.merge([b, g, r])
rgb = cv2.merge([r, g, b])

cv2.imshow("lena", lena)
cv2.imshow("bgr", bgr)
cv2.imshow("rgb", rgb)

cv2.waitKey()
cv2.destroyAllWindows()

调整图像大小

OpenCV 使用函数 cv2.resize() 实现对图像的缩放,该函数的具体语法为:

dst = cv2.resize(src, dsize[, fx[, fy[, interpolation]]])

其中:

掩模

色彩处理

滤波处理

形态学

图像变换

图像变换包括:

图像翻转

import matplotlib.pyplot as plt
import cv2

img = cv2.imread("img.jpg")
cv2.flip()

在图像上绘图

基本流程

  1. 读取或创建一个图像作为模板
import numpy as np
import matplotlib.pyplot as plt

# 创建简单图像
img_blank = np.zeros(shape = (512, 512, 3), dtype = np.int16)
plt.imshow(img_blank)
plt.show()

# 读取图像
img_bgr = cv2.imread("img.jpg")
img_rgb = cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
plt.imshow(img_rgb)
plt.show()

img

  1. 功能与属性

在图像上绘制形状的通用函数是 cv2.shape():

import cv2

cv2.shape(img, pt1, pt2, color, thickness, lineType)

参数:

直线

import matplotlib.pyplot as plt
import cv2

img_line = cv2.line(
    img = img_blank, 
    pt1 = (0, 0), 
    pt2 = (511, 511), 
    color = (255, 0, 0), 
    thickness = 5, 
    lineType = 8
)
plt.imshow(img_line)
plt.show()

img

矩形

import matplotlib.pyplot as plt
import cv2

img_rectangle = cv2.rectangle(
    img = img_blank, 
    pt1 = (384, 0),
    pt2 = (510, 128), 
    color = (0, 0, 255), 
    thickness = 5,
    lineType = 8,
)
plt.imshow(img_rectangle)
plt.show()

img

圆圈

import matplotlib.pyplot as plt
import cv2

img_circle = cv2.circle(
    img = img_blank, 
    center = (447, 63), 
    radius = 63, 
    color = (0, 0, 255), 
    thickness = -1,
    lineType = 8,
)
plt.imshow(img_circle)
plt.show()

img

在图像上写文字

API

cv2.putText(
    img, 
    text, 
    org, 
    fontFace, 
    fontScale, 
    color, 
    thickness, 
    lineType
)

主要参数:

示例

import matplotlib.pyplot as plt
import cv2

font = cv2.FONT_HERSHEY_SCRIPT_SIMPLEX
img_text = cv2.putText(
    img = img_blank,             # 图像
    text = "OpenCV",             # 文字内容
    org = (150, 200),              # 文字坐标
    fontFace = font,             # 文字字体
    fontScale = 2,               # 文字比例
    color = (255, 255, 255),     # 文字颜色
    thickness = 5,               # 文字字体粗细
    lineType = cv2.LINE_AA,      # 文字线条类型
)
plt.imshow(img_text)
plt.show()

img

OpenCV 收集图片数据集

通过视频截取图像来创建图像数据集是收集和格式化图像数据的最简单方法之一。 建议在有一面空白墙壁来收集数据,以确保框架中没有外部噪音

初始化

创建一个 VideoCapture 对象,该对象从系统的网络摄像头实时捕获视频

示例

import os
import cv2

# 创建 VideoCapture 对象
cap = cv2.VideoCapture(0)

flag_collecting = False
images_collected = 0
images_required = 50

# 创建项目目录
directory = os.path.join(os.path.dirname(__file__), "demo")
if not os.path.exists(directory):
    os.mkdir(directory)

# 收集和格式化图像数据集
while True:
    # 
    ret, frame = cap.read()
    frame = cv2.flip(frame, 1)

    # 设置收集图像的数据量    
    if images_collected == images_required:
        break

    # 绘制矩形    
    cv2.rectangle(frame, (380, 80), (620, 320), (0, 0, 0), 3)

    if flag_collecting == True:
        sliced_frame = frame[80:320, 380:620]
        save_path = os.path.join(directory, f'{images_collected + 1}.jpg')
        cv2.imwrite(save_path, sliced_frame)
        images_collected += 1
    
    cv2.putText(
        frame, 
        f"Saved Images: {images_collected}", 
        (400, 50), 
        cv2.FONT_HERSHEY_SIMPLEX, 
        0.7,
        (0, 0, 0),
        2
    )
    cv2.imshow("Data Collection", frame)

    k = cv2.waitKey(10)
    if k == ord("s"):
        flag_collecting = not flag_collecting
    if k == ord("q"):
        break

print(images_collected, "images saved to directory")
cap.release()
cv2.destroyAllWindows()

参考

OpenCV 图像投影变换

投影变换(仿射变换)

在数学中,线性变换是将一个向量空间映射到另一个向量空间的函数,通常由矩阵实现。 如果映射保留向量加法和标量乘法,则映射被认为是线性变换

要将线性变换应用于向量(即,一个点的坐标,在图像中就是像素的 $x$$y$值), 需要将该向量乘以表示线性变换的矩阵。作为输出,将获得一个坐标转换后的向量

投影变换矩阵

投影变换可以用下面的矩阵表示

$$ \left[ \begin{array}{c} a_{1} & a_{2} & b_{1}\\ a_{3} & a_{4} & b_{2}\\ c_{1} & c_{2} & 1\\ \end{array} \right]$$

其中:

$$ \left[ \begin{array}{c} a_{1} & a_{2}\\ a_{3} & a_{4}\\ \end{array} \right]$$

是一个旋转矩阵,该矩阵定义了将要执行的变换类型: 缩放、旋转等

$$ \left[ \begin{array}{c} b_{1}\\ b_{2}\\ \end{array} \right]$$

是一个平移向量,它只移动点

$$ \left[ \begin{array}{c} c_{1} & c_{2}\\ \end{array} \right]$$

是投影向量,对于仿射变换,该向量的所有元素始终等于 0

投影变换

如果 $x$$y$ 是一个点的坐标,则可以通过简单的乘法进行变换

$$ \left[ \begin{array}{c} a_{1} & a_{2} & b_{1}\\ a_{3} & a_{4} & b_{2}\\ c_{1} & c_{2} & 1\\ \end{array} \right] \times \left[ \begin{array}{c} x\\ y\\ 1\\ \end{array} \right] = \left[ \begin{array}{c} x^{'}\\ y^{'}\\ 1\\ \end{array} \right]$$

其中: $x^{'}$$y^{'}$ 是变换点的坐标

示例

  1. 读取源图像并获取源图像的大小
import numpy as np
import matplotlib.pyplot as plt
import cv2

# 源图像
img_src = cv2.imread("source_image.jpg", cv2.IMREAD_COLOR)

# 源图像大小参数
h, w, c = img_src.shape

# 获取源图像的参数: [[left,top], [left,bottom], [right, top], [right, bottom]]
img_src_coordinate = np.array([
    [0, 0],
    [0, h],
    [w, 0],
    [w, h],
])
  1. 读取目标图像
img_dest = cv2.imread("dest_image.jpg", cv2.IMREAD_COLOR)
img_dest_copy = img_dest.copy()
  1. 获取目标图像中的坐标,源图像将被粘贴到改坐标
def get_paste_position(img_dest_copy, event, x, y, flags, paste_coorinate_list):
    """
    点击目标图像中的四个点后,将四个点保存到一个列表中
    """
    cv2.imshow("collect coordinate", img_dest_copy)
    if event == cv2.EVENT_LBUTTONUP:
        # draw circle right in click position
        cv2.circle(img_dest_copy, (x, y), 2, (0, 0, 225), -1)
        # append new clicked coordinate to paste_coordinate_list
        paste_coordinate_list.append([x, y])

# 获取目标图像中的坐标
paste_coordinate = []
cv2.namedWindow("collect coordinate")
cv2.setMouseCallback(
    "collect coordinate", 
    get_paste_position, 
    paste_coordinate
)
while True:
    cv2.waitKey(1)
    if len(paste_coordinate) == 4:
        break
paste_coordinate = np.array(paste_coordinate)

# 计算投影变换矩阵
matrix, _ = cv2.findHomography(img_src_coordinate, paste_coordinate, 0)
print(f"matrix: {matrix}")

# 将源图像转换为具有目标图像大小的透视图像
perspective_img = cv2.warpPerspective(
    img_src, 
    matrix, 
    (img_dest.shape[1], img_dest.shape[0]),
)
cv2.imshow("img", perspective_img)

# 将源图像粘贴到目标图像中的选定坐标中
cv2.copyTo(
    src = perspective_img, 
    mask = np.tile(perspective_img, 1), 
    dst = img_dest
)
cv2.imshow("result", img_dest)

cv2.waitKey()
cv2.destroyAllWindows()

OpenCV BGR 像素强度图

图像属性、像素属性

import numpy as np
import matplotlib.pyplot as plt

import cv2

img = cv2.imread("./data/img.jpg")
print(f"Image shape: {img.shape}")
print(f"Pixels num: {img.size}")
print(f"First pixel: {img[0][0]}")

BGR 像素强度线(计数图)

colors = ("blue", "green", "red")
label = ("Blue", "Green", "Red")
for count, color in enumerate(colors):
    histogram = cv2.calcHist(
        images = [img], 
        channels = [count], 
        mask = None, 
        histSize = [256], 
        ranges = [0, 256]
    )
    plt.plot(
        histogram, 
        color = color, 
        label = label[count] + str(" Pixels")
    )
    plt.title("Histogram Showing Number Of Pixels Belonging To Respective Pixel Intensity", color = "crimson")
    plt.ylabel("Number Of Pixels", color="crimson")
    plt.xlabel("Pixel Intensity", color="crimson")
    plt.legend(numpoints=1, loc="best")
    plt.show()

参考