OpenCV 图像基础操作

OpenCV 是计算机视觉中经典的专用库,其支持多语言、跨平台,功能强大。

OpenCV-Python 为OpenCV 提供了Python接口,使得使用者在Python 中能够调用 C/C++ ,在保证易读性和运行效率的前提下,实现所需的功能。

文章目录

  • OpenCV 图像基础操作
    • @[TOC]
    • 一、 图像入门
      • 1. 读取图像
    • 二、利用 OpenCV 画图
      • 1. 绘制线段
      • 2. 绘制矩阵
      • 3. 画圆
      • 4. 绘制多边形
      • 5. 绘制文字
    • 三、做一个RGB调色板
      • 1. 创建滑动条
      • 2. 获取滑动条数据
      • 3. 像素操作
      • 4. 拆分和合并RGB通道
    • 5. 为图像设置边框
    • 三、图像算法运算
      • 1. 图像加法
      • 2. 图像融合
      • 3. 图像兴趣区ROI
        • 3.1 位运算
          • 3.1.1 位运算基础
          • 3.1.2 掩码MASK
      • 4. 阈值函数
        • 4.1 简单阈值
            • 4.1.1`THRESH_BINARY`
            • 4.1.2 `THRESH_BINARY_INV`
            • 4.1.3 `THRESH_BTRUNC`
            • 4.1.4 `THRESH_TOZERO`
            • 4.1.5 `THRESH_TOZERO_INV`
        • 4.2 自适应阈值
        • 4.3 Otsu的二值化
      • 四、图像几何变换
        • 1. 缩放
        • 2. 图像平移
        • 3. 旋转
        • 4. 仿射变换
          • 4.1 变换矩阵形式
          • 4.2 变换矩阵理解
        • 5. 透视变换

一、 图像入门
1. 读取图像

使用 cv2.imread(src, flag)函数读取图像。

  • src: 图像路径
  • flag: 读取图像的方式
  • cv.IMREAD_COLOR: 加载彩色图像。任何图像的透明度都会被忽视
  • cv.IMREAD_GRAYSCALE: 以灰度模式加载图像
  • cv.IMREAD_UNCHANGED: 加载图像,包括alpha通道

在使用cv2.imread()时,即使路径错误,也不会引发报错,但会返回一个 None 值

import numpy as np
import cv2 as cv
import matplotlib.pyplot as plt
%matplotlib inline

# 读入彩色图像,忽视透明度(忽略alpha通道),默认参数
# cv.IMREAD_COLOR = 1
img1 = cv.imread(r'E:/Computer/Desktop/keras_00.jpg', cv.IMREAD_COLOR)
print(img1.shape)
plt.imshow(img1)
(3969, 5613, 3)

# 读入灰度图像
# cv.IMREAD_GRAYSCALE = 0
img1 = cv.imread(r'E:/Computer/Desktop/keras_00.jpg', cv.IMREAD_GRAYSCALE)
print(img1.shape)
plt.imshow(img1)
(3969, 5613)

# 读入彩色图像(完整图片),包括alpha通道
# cv.IMREAD_UNCHANGED = -1
img1 = cv.imread(r'E:/Computer/Desktop/keras_00.jpg', cv.IMREAD_UNCHANGED)
print(img1.shape)
plt.imshow(img1)
(3969, 5613, 3)

可以看到选择 IMREAD_GRAYSCALE 比其他两种参数少了三个维度(RGB三通道)

二、利用 OpenCV 画图

创建一个黑色图像,在该图像上绘制各种形状

1. 绘制线段

cv.line(img, pt1, pt2, color, thickness, lineType, shift)

    img: 进行绘图的图像
    pt1: 线段起点坐标
    pt2: 线段终点坐标
    color: 线段颜色
    thickness: 线段粗细
    lineType: 线段类型
    shift: 坐标精确到小数点后几位
# 创建一个纯黑的图像
img = np.zeros((512, 512, 3), np.uint8)
# 绘制一条厚度为5的对角线
cv.line(img, (0, 0), (511, 511), (255, 255, 0), 5)
plt.imshow(img)

2. 绘制矩阵

cv.rectangle(img, pt1, pt2, color, thickness, lineType, shift)

    其用法和 cv.line() 基本相同
    pt1,pt2 表示矩阵的对角点
    thickness: 矩阵边框粗细,若为 cv.FILLED 则为实心矩阵
cv.rectangle(img, (100, 100), (150, 150), (0, 255, 0), 5)
plt.imshow(img)

3. 画圆

cv.circle(img, center, radius, color, thickness, lineType, shift)

    center: 圆心
    radius: 半径
    其他参数同上
cv.circle(img, (300, 300), 150, (255, 0, 0), 5)
plt.imshow(img)

4. 绘制多边形

cv.polylines(img, pts, isClosed, color, thickness, lineType, shift)

    pts: 包含多边形点的数组
    isClosed: 决定绘制多边形是否闭合,若为True,则画若干个闭合多边形,若为False,则画一条连接所有点的折线
lst = []
for i in range(5):
    x = np.random.randint(img.shape[1])
    y = np.random.randint(img.shape[0])
    lst.append([x, y])
    
pts = np.array([lst], np.int32).reshape((-1, 1, 2))
cv.polylines(img, [pts], True, (0, 0, 255), 5)
plt.imshow(img)

在转化为数组(np.array)及最后画图(cv.polylines)时,要在将参数加上中括号,否则会报错 error: (-215:Assertion failed) p.checkVector(2, CV_32S) >= 0 in function 'cv::polylines'
报错原因: 数组形状不匹配

5. 绘制文字

cv.putText(img, text, org, fontFace, fontScale, color, thickness, bottomLeftOrign)

    text: 要添加的文字
    org: 文字位置
    fontFace: 字体类型
    fontScale: 字体大小
    bottomLeftOrign: 是否标识原点位置
cv.putText(img, 'XuJiu', (100, 500), cv.FONT_HERSHEY_SIMPLEX, 4, (255, 255, 255), 5, cv.LINE_AA)
plt.imshow(img)

三、做一个RGB调色板
1. 创建滑动条

cv.createTrackbar(trackbarname, winname, value, count, onChange, userdata)

    trackbarname: 跟踪条名称
    winname: 绑定窗口名称
    value: 初始滑块位置
    count: 滑动最大值
    onChange: 回调函数
    userdata: 数据,可选

2. 获取滑动条数据

cv.getTrackbarPos(trackbarname, winname)

img2 = np.zeros((300, 512, 3), np.uint8)

def func():
    pass

cv.namedWindow('RGB')
cv.createTrackbar('R', 'RGB', 0, 255, func)
cv.createTrackbar('G', 'RGB', 0, 255, func)
cv.createTrackbar('B', 'RGB', 0, 255, func)

switch='0: OFFn 1: ON'
cv.createTrackbar(switch, 'RGB', 0, 1, func)

while True:
    cv.imshow('RGB', img2)
    k = cv.waitKey(1) & 0xFF
    if k == 27:
        break
    r = cv.getTrackbarPos('R', 'RGB')
    g = cv.getTrackbarPos('G', 'RGB')
    b = cv.getTrackbarPos('B', 'RGB')
    s = cv.getTrackbarPos(switch, 'RGB')
    if s == 0:
        img2[:] = 0
    else:
        img2[:] = [b, g, r]
cv.destroyAllWindows()

3. 像素操作

jpg = cv.imread('E:/Computer/Desktop/keras_00.jpg')

# 访问某一像素的值
px = jpg[100, 100]
# 仅访问蓝色元素
blue = px[0]
print(px, blue)
[124 122 128] 124

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

注意 上面的方法通常用于选择数组的区域,例如前5行和后3列。对于单个像素访问,Numpy数组方法array.item()和array.itemset())被认为更好,但是它们始终返回标量。如果要访问所有B,G,R值,则需要分别调用所有的array.item()。

# 访问红色元素
jpg.item(100, 100, 2)
128
# 修改红色元素
jpg.itemset((100, 100, 2), 100)
jpg.item(100, 100, 2)
100
# 图像属性
print(jpg.shape)
print(jpg.size)
print(jpg.dtype)
(3969, 5613, 3)
66833991
uint8

利用图像像素属性可以实现图像局部平移等操作

4. 拆分和合并RGB通道

b, g, r = cv.split(jpg)
jpg = cv.merge((b, g, r))
b = jpg[:, :, 0]

cv.split()是一项耗时的操作(就时间而言)。因此,仅在必要时才这样做。否则请进行Numpy索引。


5. 为图像设置边框

如果要在图像周围设置边框,则可以使用 cv.copyMakeBorder(),它在卷积运算,零填充方面有更多的应用

cv.copyMakeBorder(src, top, bottom, left, right, borderType, value)

  • src: 输入的图片
  • top, bottom, left, right: 相应方向上的相框宽度
  • borderTyoe: 边框类型,包括:
    • cv.BORDER_CONSTANT: 添加的边界框像素值为常数(需要额外给定一个参数)
    • cv.BORDER_REFLECT: 倒映,添加的边框像素将是边界元素的镜面反射
    • cv.BORDER_REFLECT_101: 和上面类似,但在倒映时,会把边界空开
    • cv.BORDER_REPLICATE: 直接用边界的颜色填充
    • cv.WRAP
  • value: 如果为 cv.CONSTANT时填充的常数
constant = cv.copyMakeBorder(img, 10, 10, 10, 10, cv.BORDER_CONSTANT, value=[255, 0, 0])
reflect = cv.copyMakeBorder(img, 10, 10, 10, 10, cv.BORDER_REFLECT)
reflect_101 = cv.copyMakeBorder(img, 10, 10, 10, 10, cv.BORDER_REFLECT_101)
replicate = cv.copyMakeBorder(img, 10, 10, 10, 10, cv.BORDER_REPLICATE)
wrap = cv.copyMakeBorder(img, 10, 10, 10, 10, cv.BORDER_WRAP)

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


三、图像算法运算
1. 图像加法

可以通过 cv.add() 或者 res = img1 + img2添加两个图像,两个图像应该具有相同的深度和类型,或第二个图像为一个标量值

x = np.uint8([255])
y = np.uint8([10])
res1 = cv.add(x, y)
print(res1)  # 255 + 10 = 266 => 255
print(x + y)  # 255 + 10 = 265 % 256 = 9
[[255]]
[9]

OpenCV 加法是饱和运算,而Numpy是模运算

2. 图像融合

也是图像加法,但是对图像赋予了不同的权重,以使其具有融合或透明的效果,根据以下等式添加图像:

G

(

x

)

=

(

1

α

)

f

0

(

x

)

+

α

f

1

(

x

)

G(x) = (1 - alpha)f_0(x) + alpha f_1(x)

G(x)=(1−α)f0​(x)+αf1​(x)

通过

α

alpha

α 从

0

1

0 to 1

0→1 更改,
cv.addWeighted() 在图像上应用以下公式

d

s

t

=

α

i

m

g

1

+

β

i

m

g

2

+

γ

dst = alpha cdot img1 + beta cdot img2 + gamma

dst=α⋅img1+β⋅img2+γ
在这里,

γ

gamma

γ 视为0

dst = cv.addWeighted(img, 0.2, img, 0.2, 0)
plt.imshow(dst)

3. 图像兴趣区ROI

有时候,不得不处理一些特定区域的图像。例如眼睛检测,首先对整个图像进行人脸检测。在获取人脸图像时,我们只选择人脸区域,搜索其中的眼睛,而不是搜索整个图像。以提高准确性(眼睛总是在面部上)和性能(搜索区域变小)

可以使用Numpy索引在此获得ROI

3.1 位运算

包括按位 AND、OR、NOT、XOR操作。它们在提取图像的任何部分、定义和处理非矩形ROI等方面非常有用,如下:

img_test = cv.imread('E:/Computer/Desktop/ec4c-iyhvyuz4791207.png', cv.IMREAD_COLOR)
plt.imshow(img_test)

rows, cols, channels = img.shape
roi = img_test[0:rows, 0:cols]
img2gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
ret, mask = cv.threshold(img2gray, 10, 255, cv.THRESH_BINARY)
mask_inv = cv.bitwise_not(mask)
img1_bg = cv.bitwise_and(roi, roi, mask = mask_inv)
img2_fg = cv.bitwise_and(img, img, mask = mask)
dst = cv.add(img1_bg, img2_fg)
img_test[0:rows, :cols] = dst
plt.imshow(img_test)

3.1.1 位运算基础

运算 运算结果 类比 举例
AND 相同-真,不同-假 交集 8 and 9 = 8:8 = 0b1000, 9 = 0b1001 0b1000&0b1001 = 0b1000
OR 有一个不为0-真 并集 8 OR 9 = 9
XOR 两个数不同-真 N/A 8 XOR 9 = 1
NOT 反转数组的每一位 N/A NOT 9 = -10

数组经过 bitwise_not 反转后每一位与转换前相加都为-1.

3.1.2 掩码MASK

Mask及图与掩码的按位与运算。

原图中的每个像素和掩码中的每个对应像素进行按位与运算,如果为真,结果是原图的值。

  • mask 只能是二维矩阵,与原图 shape[:2]维数相同;
  • 只能是单通道矩阵,图片表现是灰度值;
  • 不管mask的值是10还是255,如果为真,mask结果都是原图的值

mask的最大作用:让我们只关注我们感兴趣的图像部分。

4. 阈值函数
4.1 简单阈值

问题直截了当。对于每个像素,应用相同的阈值。如果像素值小于阈值,则将其设置为0,否则设置为最大值。

cv.threshold(src, thresh, maxval, type)

  • src: 输入图,只能输入灰度图
  • dst: 输出图
  • thresh: 阈值
  • maxval: 当像素超过了阈值或小于阈值(由type决定),所赋予的值
  • type: 二值操作的类型,包括cv.THRESH_BINARY,cv.THRESH_BINARY_INV,cv.THRESH_TRUNC,cv.THRESH_TOZERO,cv.THRESH_TOZERO_INV

该方法返回两个输出,第一个是使用的阈值,第二个输出是阈值后的图像

l = [i for i in range(0, 256)] * 256
arr = np.array(l, dtype=np.uint8).reshape((256, 256))

ret,thresh1 = cv.threshold(arr,127,255,cv.THRESH_BINARY)
ret,thresh2 = cv.threshold(arr,127,255,cv.THRESH_BINARY_INV)
ret,thresh3 = cv.threshold(arr,127,255,cv.THRESH_TRUNC)
ret,thresh4 = cv.threshold(arr,127,255,cv.THRESH_TOZERO)
ret,thresh5 = cv.threshold(arr,127,255,cv.THRESH_TOZERO_INV)

titles = ['UNCHANGED','BINARY','BINARY_INV','TRUNC','TOZERO','TOZERO_INV']
images = [arr, thresh1, thresh2, thresh3, thresh4, thresh5]
for i in range(6):
    plt.subplot(2,3,i+1),plt.imshow(images[i],'gray')
    plt.title(titles[i])
    plt.xticks([]),plt.yticks([])

4.1.1THRESH_BINARY

d

s

t

(

x

,

y

)

=

{

m

a

x

v

a

l

u

e

if src(x, y) > thresh

0

otherwise

dst(x, y) = begin{cases} maxvalue & text{if src(x, y) > thresh} \ 0 & text{otherwise} end{cases}

dst(x,y)={maxvalue0​if src(x, y) > threshotherwise​

4.1.2 THRESH_BINARY_INV

d

s

t

(

x

,

y

)

=

{

0

if src(x, y) > thresh

m

a

x

v

a

l

u

e

otherwise

dst(x, y) = begin{cases} 0 & text{if src(x, y) > thresh} \ maxvalue & text{otherwise} end{cases}

dst(x,y)={0maxvalue​if src(x, y) > threshotherwise​

4.1.3 THRESH_BTRUNC

d

s

t

(

x

,

y

)

=

{

t

h

r

e

s

h

o

l

d

if src(x, y) > thresh

s

r

c

(

x

,

y

)

otherwise

dst(x, y) = begin{cases} threshold & text{if src(x, y) > thresh} \ src(x, y) & text{otherwise} end{cases}

dst(x,y)={thresholdsrc(x,y)​if src(x, y) > threshotherwise​

4.1.4 THRESH_TOZERO

d

s

t

(

x

,

y

)

=

{

s

r

c

(

x

,

y

)

if src(x, y) > thresh

0

otherwise

dst(x, y) = begin{cases} src(x, y) & text{if src(x, y) > thresh} \ 0 & text{otherwise} end{cases}

dst(x,y)={src(x,y)0​if src(x, y) > threshotherwise​

4.1.5 THRESH_TOZERO_INV

d

s

t

(

x

,

y

)

=

{

0

if src(x, y) > thresh

s

r

c

(

x

,

y

)

otherwise

dst(x, y) = begin{cases} 0 & text{if src(x, y) > thresh} \ src(x, y) & text{otherwise} end{cases}

dst(x,y)={0src(x,y)​if src(x, y) > threshotherwise​

4.2 自适应阈值

使用全局值作为阈值时,并非在所有情况下都能取得很好的效果,例如,如果图像在不同区域具有不同的光照条件,这种情况下,自适应阈值阈值化可以提供帮助。在此,算法基于像素周围的小区域确定像素的阈值。因此,对于同一图像的不同区域,可以获得不同的阈值,这为光照度变化的图像提供了更好的结果。

cv.adaptiveThreshold(src, maxValue, adaptiveMethod, thresholdType, blockSize, C, dst=None)

  • src: 需要进行二值化的一张灰度图像
  • maxValue: 满足条件的像素点需要设置的灰度值
  • adaptiveMethod: 自适应阈值算法,可选ADAPTIVE_THRESH_MEAN_C 或 ADAPTIVE_THRESH_GAUSSIAN_C
  • thresholdType: openCV 提供的二值化方法,只能 THRESH_BINARY或THRESH_BINARY_INV
  • blockSize: 要分成的区域大小,一般取奇数
  • C: 常数,每个区域计算出的阈值的基础上减去这个常数
  • dst: 输出图像,可选

adaptiveMethod 的两个选项分别为局部邻域块的平均值(类似于KNN),和局部邻域块的高斯加权和。该算法是在区域周围的像素根据高斯函数按照他们离中心点的距离进行加权计算。

for i in range(6):
    blockSize = 2 * (i + 1) + 1
    plt.subplot(2, 3, i + 1), plt.imshow(cv.adaptiveThreshold(cv.cvtColor(src, cv.COLOR_BGR2GRAY), 127, cv.ADAPTIVE_THRESH_MEAN_C, cv.THRESH_BINARY, blockSize, 1))
    plt.title('MEAN_C blockSize=' + str(blockSize))
    plt.xticks([]), plt.yticks([])

可以看出 blockSize越大,参与计算阈值的区域就越大,细节轮廓就变得少,整体轮廓越粗越明显。

这种二值化有点类似 canny 边缘检测,用来找轮廓或者特征点是一个不错的选择。

4.3 Otsu的二值化

在全局阈值化中,使用任意选择的值作为阈值。Otsu的方法避免必须选择一个值并自动确定它的情况。

它是根据图像的灰度特性,将图像分为前景和背景两个部分。当取最佳阈值时,两部分之间的差别应该是最大的,在OTSU算法中所采用的衡量差别的标准就是较为常见的最大类间方差。前景和背景之间的类间方差如果越大,就说明构成图像的两个部分之间的差别越大,当部分目标被错分为背景或部分背景被错分为目标,都会导致两部分差别变小,当所取阈值的分割使类间方差最大时就意味着错分概率最小。

记T为前景与背景的分割阈值,前景点数占图像比例为

w

0

w_0

w0​,平均灰度为u0;背景点数占图像比例为

w

1

w_1

w1​,平均灰度为

u

1

u_1

u1​,图像的总平均灰度为

u

u

u,前景和背景图象的方差

g

g

g,则有:

u

=

w

0

×

u

0

+

w

1

×

u

1

g

=

w

0

×

(

u

0

u

)

2

+

w

1

×

(

u

1

u

)

2

begin{aligned} u&= w_0 times u_0 + w_1 times u_1 \ g&= w_0 times (u_0 -u)^2 + w_1 times (u_1 - u)^2 end{aligned}

ug​=w0​×u0​+w1​×u1​=w0​×(u0​−u)2+w1​×(u1​−u)2​

g

=

w

0

×

w

1

×

(

u

0

u

1

)

2

g

=

w

0

1

w

0

×

(

u

0

u

)

2

begin{aligned} g &= w_0 times w_1 times (u_0 - u_1)^2 \ 或 \ g &= frac{w_0}{1-w_0} times (u_0 - u)^2 end{aligned}

g或g​=w0​×w1​×(u0​−u1​)2=1−w0​w0​​×(u0​−u)2​

当方差g最大时,可以认为此时前景和背景差异最大,此时的灰度T是最佳阈值。类间方差法对噪声以及目标大小十分敏感,它仅对类间方差为单峰的图像产生较好的分割效果。当目标与背景的大小比例悬殊时(例如受光照不均、反光或背景复杂等因素影响),类间方差准则函数可能呈现双峰或多峰,此时效果不好.


四、图像几何变换

OpenCV 提供了两个转换函数: cv.warpAffine 和 cv.warpPerspective,可以使用题目进行各种转换,cv.warpAffine 采用

2

×

3

2 times 3

2×3 矩阵,cv.warpPerspective 采用

3

×

3

3 times 3

3×3 矩阵作为输入。

cv.warpAffine(src, M, dsize, flags, borderMode, borderValue)

  • src: 输入图像
  • M: 2 X 3 的变换矩阵,一般反应平移或者旋转的关系
  • dsize: 输出图像的大小
  • flags: 插值方法的组合(int型),默认为 cv.INTER_LINEAR
  • borderMode: 边界像素模式(int型)
  • borderValue: 边界像素填充值,默认为0(黑色)

1. 缩放

缩小图像称为下采样(subsampled)或降采样(downsampled),放大图像称为上采样,主要目的是得到更高分辨率的图像。

缩放只是调整图像的大小,OpenCV带有一个函数cv.resize(src, dsize[, dft[, fx[, fy[, interpolation]]]])

  • src: 输入图像
  • dsize: 输出图像的尺寸,与下面的比例因子二选一
  • fx: 沿水平轴的比例因子
  • fy: 沿垂直轴的比例因子
  • interpolation: 插值方法,默认为cv2.INTER_NEAREST(最近邻插值),其他的还包括 cv2.INTER_CUBIC和 cv.INTER_LINEAR等
src = cv.imread('E:/Computer/Desktop/ec4c-iyhvyuz4791207.png')
plt.imshow(src)

res = cv.resize(src, None, fx=2, fy=2, interpolation=cv.INTER_CUBIC)
plt.imshow(res)

2. 图像平移

平移是物体的位置移动(将图像中所有的点按照指定的平移量水平或者垂直移动)

(

x

0

,
  

y

0

)

(x_0,;y_0)

(x0​,y0​) 为原图像上的一点,图像水平平移量为

T

x

T_x

Tx​,垂直平移量为

T

y

T_y

Ty​,则评议后的点坐标

(

x

1

,
  

x

2

)

(x_1, ; x_2)

(x1​,x2​) 变为:

x

1

=

x

0

+

T

x

y

1

=

y

0

+

T

y

x1 = x_0 + T_x \ y1 = y_0 + T_y

x1=x0​+Tx​y1=y0​+Ty​
或创建矩阵

M

M

M:

M

=

[

1

0

t

x

0

1

t

y

]

M = begin{bmatrix} 1 & 0 & t_x \ 0 & 1 & t_y end{bmatrix}

M=[10​01​tx​ty​​]
可以将其放入 np.float32 类型的 Numpy 数组中, 并将其传递给 cv.warpAffine() 函数

rows, cols = src.shape[:2]
M = np.float32([[1, 0, 100], [0, 1, 50]])
dst = cv.warpAffine(src, M, (cols, rows))
plt.imshow(dst)

3. 旋转

以图像的中心为原点,旋转一定的角度,也就是图像上的所有像素都旋转一个相同的角度。旋转后的图像大小一般会改变,即可以把超出显示区域的图像截去,或者扩大图像范围来显示图像所有部分。

图像旋转角度为

θ

theta

θ,通过以下形式的变换矩阵实现:

M

=

[

c

o

s

θ

s

i

n

θ

s

i

n

θ

c

o

s

θ

]

M = begin{bmatrix} cos theta & -sintheta \ 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 & (1-alpha) cdot center.x - beta cdot center.y \ -beta & alpha & beta cdot center.x + (1-alpha) cdot center.y end{bmatrix}

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

α

=

s

c

a

l

e

c

o

s

θ

β

=

s

c

a

l

e

s

i

n

θ

alpha = scale cdot costheta \ beta = scale cdot sintheta

α=scale⋅cosθβ=scale⋅sinθ
为了计算此转换矩阵,OpenCV 提供了一个函数cv.getRotationMatrix2D(center, angle, scale)

center: 图片的旋转中心

angle: 旋转角度

scale: 缩放比例,0.5表示缩小到原来一半,还能表示旋转方向,正为逆时针,负为顺时针

M = cv.getRotationMatrix2D(((cols -1) / 2.0, (rows - 1) / 2.0), 90, 1)
dst = cv.warpAffine(src, M, (rows, cols))
plt.imshow(dst)

4. 仿射变换

平移和旋转的组合称之为欧式变换或刚体变化。

缩放可进一步分为

u

n

i

f

o

r

m
  

s

c

a

l

i

n

g

uniform;scaling

uniformscaling和

n

o

n

u

n

i

f

o

r

m
  

s

c

a

l

i

n

g

non-uniform;scaling

non−uniformscaling,前者每个坐标轴放缩系数相同(各同向性),后者不同;如果放缩系数为负,则会叠加上 反射(

r

e

f

l

e

c

t

i

o

n

reflection

reflection),

r

e

f

l

e

c

t

i

o

n

reflection

reflection可以看作是特殊的

s

c

a

l

i

n

g

scaling

scaling。

刚体变化 +

u

n

i

f

o

r

m
  

s

c

a

l

i

n

g

uniform ; scaling

uniformscaling 称之为相似变换(

s

i

m

i

l

a

r

i

t

y
  

t

r

a

n

s

f

o

r

m

a

t

i

o

n

similarity; transformation

similaritytransformation),即平移+旋转+各同向性 的放缩。

剪切变换(

s

h

e

a

r
  

m

a

p

p

i

n

g

shear ; mapping

shearmapping) 将所有点沿某一指定方向成比例的平移,变换关系如下图:

4.1 变换矩阵形式

没有平移量或者平移量为0的所有仿射变换可以用如下变换矩阵描述:

[

x

y

]

=

[

a

b

c

d

]

[

x

y

]

begin{bmatrix} x' \ y' end{bmatrix} = begin{bmatrix} a & b \ c & d end{bmatrix} begin{bmatrix} x \ y end{bmatrix}

[x′y′​]=[ac​bd​][xy​]

不同变换对应的

a

,

b

,

c

,

d

a, b, c, d

a,b,c,d 约束不同,排除了平移变换的所有仿射变换为线性变换(

l

i

n

e

a

r

t

r

a

n

s

f

o

r

m

m

a

t

i

o

n

linear transformmation

lineartransformmation),其涵盖的变换如上图所示,其特点是 原点位置不变,多次线性变换的结果仍是线性变换
为了涵盖平移,引入 齐次坐标,在原有2维3坐标的基础上,增广一个维度,如下:

[

x

y

1

]

=

[

a

b

c

d

e

f

0

0

1

]

[

x

y

1

]

begin{bmatrix} x' \ y' \ 1 end{bmatrix} = begin{bmatrix} a & b & c\ d & e & f \ 0 & 0 & 1 end{bmatrix} begin{bmatrix} x \ y \ 1 end{bmatrix}

⎣⎡​x′y′1​⎦⎤​=⎣⎡​ad0​be0​cf1​⎦⎤​⎣⎡​xy1​⎦⎤​
所以,仿射变换的变换矩阵统一用

[

a

b

c

d

e

f

0

0

1

]

begin{bmatrix} a & b & c\ d & e & f \ 0 & 0 & 1 end{bmatrix}

⎣⎡​ad0​be0​cf1​⎦⎤​ 来描述,不同基础变换的约束不同,如下:

旋转和平移相乘得到刚体变换的变换矩阵,如下,由三个自由度

(

θ

,

t

x

,

t

y

)

(theta, t_x, t_y)

(θ,tx​,ty​)

[

c

o

s

(

θ

)

s

i

n

(

θ

)

t

x

s

i

n

(

θ

)

c

o

s

(

θ

)

t

y

0

0

1

]

[

x

y

1

]

=

[

x

y

1

]

begin{bmatrix} cos(theta) & -sin(theta) & t_x \ sin(theta) & cos(theta) & t_y \ 0 & 0 & 1 end{bmatrix} begin{bmatrix} x \ y \ 1 end{bmatrix} = begin{bmatrix} x' \ y' \ 1 end{bmatrix}

⎣⎡​cos(θ)sin(θ)0​−sin(θ)cos(θ)0​tx​ty​1​⎦⎤​⎣⎡​xy1​⎦⎤​=⎣⎡​x′y′1​⎦⎤​
再乘上

u

n

i

f

o

r

m
  

s

c

a

l

i

n

g

uniform ; scaling

uniformscaling 得到相似变换,有四个自由度

(

s

,

θ

,

t

x

,

t

y

)

(s, theta, t_x, t_y)

(s,θ,tx​,ty​)

[

s
  

c

o

s

(

θ

)

s
  

s

i

n

(

θ

)

t

x

s
  

s

i

n

(

θ

)

s
  

c

o

s

(

θ

)

t

y

0

0

1

]

[

x

y

1

]

=

[

x

y

1

]

begin{bmatrix} s;cos(theta) & -s;sin(theta) & t_x \ s;sin(theta) & s;cos(theta) & t_y \ 0 & 0 & 1 end{bmatrix} begin{bmatrix} x \ y \ 1 end{bmatrix} = begin{bmatrix} x' \ y' \ 1 end{bmatrix}

⎣⎡​scos(θ)ssin(θ)0​−ssin(θ)scos(θ)0​tx​ty​1​⎦⎤​⎣⎡​xy1​⎦⎤​=⎣⎡​x′y′1​⎦⎤​

4.2 变换矩阵理解

坐标系 由 坐标原点 和 基向量 决定,坐标向量 和 基向量 确定了,坐标系也就确定了。

对于坐标系中的位置

(

x

,

y

)

(x, y)

(x,y),其相对坐标原点在

[

1

,

0

]

[1, 0]

[1,0] 方向上的投影为

x

x

x,在

[

0

,

1

]

[0, 1]

[0,1] 方向上的投影维

y

y

y,这里投影的意思是过

(

x

,

y

)

(x,y)

(x,y)做坐标轴的平行线与坐标轴的交点到原点的距离,即

(

x

y

)

(x,y)

(x,y)实际为:

[

x

y

]

=

x

[

1

0

]

+

y

[

0

1

]

=

[

1

0

0

1

]

[

x

y

]

begin{bmatrix} x \ y end{bmatrix}= xbegin{bmatrix} 1 \ 0 end{bmatrix} + y begin{bmatrix} 0 \ 1 end{bmatrix}= begin{bmatrix} 1 & 0 \ 0 & 1 end{bmatrix} begin{bmatrix} x \ y end{bmatrix}

[xy​]=x[10​]+y[01​]=[10​01​][xy​]

当坐标系变化,坐标系中的点也跟着变化,但点相对新坐标系

(

x

y

)

(x'-y'坐标系)

(x′−y′坐标系) 的位置不变,仍为

(

x

,

y

)

(x, y)

(x,y) 以旋转变换为例,新坐标轴的基向量变为

[

c

o

s

θ

,

s

i

n

θ

]

[costheta, sintheta]

[cosθ,sinθ] 和

[

s

i

n

θ

,

c

o

s

θ

]

[-sintheta, costheta]

[−sinθ,cosθ],所以点变化到新位置为

[

x

y

]

=

x

[

c

o

s

(

θ

)

s

i

n

(

θ

)

]

+

y

[

s

i

n

(

θ

)

       

c

o

s

(

θ

)

]

=

[

c

o

s

(

θ

)

s

i

n

(

θ

)

s

i

n

(

θ

)

       

c

o

s

(

θ

)

]

[

x

y

]

begin{bmatrix} x' \ y' end{bmatrix}= xbegin{bmatrix} cos(theta) \ sin(theta) end{bmatrix} + ybegin{bmatrix} -sin(theta) \ ;;,;cos(theta) end{bmatrix}= begin{bmatrix} cos(theta) & -sin(theta) \ sin(theta) &;;;, cos(theta) end{bmatrix} begin{bmatrix} x \ y end{bmatrix}

[x′y′​]=x[cos(θ)sin(θ)​]+y[−sin(θ)cos(θ)​]=[cos(θ)sin(θ)​−sin(θ)cos(θ)​][xy​]
新位置和新基向量是相对绝对坐标系

(

x

y

)

(x-y坐标系)

(x−y坐标系)而言的,其他变换矩阵同理。

  • 所有变换矩阵只需要关注一点:坐标系的变化,即基向量和原点的变化
  • 坐标系变化到哪里,坐标系中的点也跟着做同样的变化
  • 坐标系的变化分为 基向量的变化 以及 坐标原点的变化,在仿射变换矩阵

    [

    a

    b

    c

    d

    e

    f

    0

    0

    1

    ]

    begin{bmatrix} a & b & c \ d & e & f \ 0 & 0 & 1end{bmatrix}

    ⎣⎡​ad0​be0​cf1​⎦⎤​ 中,

    [

    a

    d

    ]

    begin{bmatrix} a \ d end{bmatrix}

    [ad​]和

    [

    b

    e

    ]

    begin{bmatrix}b \ e end{bmatrix}

    [be​]为新的基向量,

    [

    c

    f

    ]

    begin{bmatrix} c \ f end{bmatrix}

    [cf​] 为新的坐标原点,先变化基向量,再变化坐标原点

OpenCV提供了cv.getAffineTransform(src, dst)函数来求解仿射变换的变换矩阵

  • src: 原始图像中三个点的坐标
  • dst: 变换后的这三个点的对应坐标

5. 透视变换

透视变换

(

P

e

r

s

p

e

c

t

i

v

e
  

T

r

a

n

s

f

o

r

m

a

t

i

o

n

)

(Perspective; Transformation)

(PerspectiveTransformation)的本质是将图像投影到一个新的视平面

(

V

i

e

w

i

n

g
  

P

l

a

n

e

)

(Viewing ; Plane)

(ViewingPlane)。与仿射变换类似,OpenCV提供了一个求透视变换矩阵的函数cv.getPerspectiveTransform(src, dst, solveMehtod),以及进行透视变换操作的函数cv.warpPerspective(src, M, dsize, flags, borderMode, borderValue)

src: 透视变换前的四个点的位置

dst: 透视变换后的四个点的位置

通用的变换公式为

[

x

y

w

]

=

[

u

v

w

]

[

a

11

a

12

a

13

a

21

a

22

a

23

a

31

a

32

a

33

]

begin{bmatrix} x' & y' & w' end{bmatrix}= begin{bmatrix} u & v & w end{bmatrix} begin{bmatrix} a_{11} & a_{12} & a_{13} \ a_{21} & a_{22} & a_{23} \ a_{31} & a_{32} & a_{33} end{bmatrix}

[x′​y′​w′​]=[u​v​w​]⎣⎡​a11​a21​a31​​a12​a22​a32​​a13​a23​a33​​⎦⎤​

变换矩阵可以拆成四部分,

[

a

11

a

12

a

21

a

22

]

begin{bmatrix} a_{11} & a_{12} \ a_{21} & a_{22} end{bmatrix}

[a11​a21​​a12​a22​​] 表示线性变换,比如

s

c

a

l

i

n

g

scaling

scaling,

s

h

e

a

r

i

n

g

shearing

shearing 和

r

a

t

o

t

i

o

n

ratotion

ratotion。

[

a

31

a

32

]

begin{bmatrix} a_{31} & a_{32} end{bmatrix}

[a31​​a32​​] 用于平移,

[

a

13

a

23

]

T

begin{bmatrix} a_{13} & a_{23} end{bmatrix}^T

[a13​​a23​​]T 产生透视变换。

d1 = np.float32([[0, 0], [0, rows], [cols, 0], [rows, cols]])
d2 = np.float32([[0, 0], [0, rows / 2], [cols / 2, 0], [rows / 3, cols / 3]])
M = cv.getPerspectiveTransform(d1, d2)
dst = cv.warpPerspective(src, M, (cols, rows))
plt.imshow(dst)