目录
2.4.1 函数cv2.getRotationMatrix2D()
零、写在前面的话
这一部分的原理相较之前更难了,涉及到的线性代数与概率论等高等数学知识较多,对于反复学习都无法理解的内容,我们可以选择暂时跳过这些晦涩的原理部分,可以待后面有时间了再来捡起。针对这些前置基础,这里推荐一些学习资源,有需要的可以自行收藏。
【熟肉】线性代数的本质 - 01 - 向量究竟是什么?_哔哩哔哩_bilibili
《概率论与数理统计》教学视频全集(宋浩)_哔哩哔哩_bilibili
一、图像二值化处理
- 将图像中每个像素的灰度值(0~255)转化为 0(黑) 或 255(白),根据设定的阈值进行判断。
- 二值化操作的图像必须是灰度图 。
1.1 二值化的实际意义
- 简化图像内容,去掉冗余细节,降低计算量和计算需求,加快处理速度。
- 突出物体轮廓或结构,利于图像分割。
- 二值化常作为边缘检测的预处理步骤,可为后续的图像识别、轮廓提取、OCR(线条图的扫描识别)等任务提供清晰的输入。
- 节约内存空间,二值图像占用空间远小于彩色图。
1.2 阈值法处理
关于阈值法:
_,binary = cv2.threshold(img,thresh,maxval,type)
img
:输入图像,要进行二值化处理的灰度图。
thresh
:设定的阈值。当像素值大于(或小于,取决于阈值类型)thresh
时,该像素被赋予的值。
type
:阈值处理的类型。返回值:
第一个值(通常用下划线表示):计算出的阈值,若使用自适应阈值法,会根据算法自动计算出这个值。
第二个值(binary):二值化后的图像矩阵。与输入图像尺寸相同。
1.2.1 全局阈值法
通过设置一个阈值,将灰度图中的每一个像素值与该阈值进行比较,小于等于阈值的像素就被设置为0(通常代表背景),大于阈值的像素就被设置为maxval(通常代表前景)。对于我们的8位图像(0~255)来说,通常是设置为255。
1.2.2 反阈值法
THRESH_BINARY_INV
反阈值法是当灰度图的像素值大于阈值时,该像素值将会变成0(黑),当灰度图的像素值小于等于阈值时,该像素值将会变成maxval。(即与上相反)
1.2.3 截断阈值法
THRESH_TRUNC
截断阈值法,指将灰度图中的所有像素与阈值进行比较,像素值大于阈值的部分将会被修改为阈值,小于等于阈值的部分不变。 由截断阈值法处理过的二值化图中的最大像素值就是阈值。
1.2.4 低阈值零处理
THRESH_TOZERO
即像素值小于等于阈值的部分被置为0(也就是黑色),大于阈值的部分不变。
1.2.5 超阈值零处理
THRESH_TOZERO_INV
超阈值零处理就是将灰度图中的每个像素与阈值进行比较,像素值大于阈值的部分置为0(也就是黑色),像素值小于等于阈值的部分不变。
1.2.6 OTSU计算阈值(重难点)
(1)概念:
cv2.THRESH_OTSU 并不是一个有效的阈值类型或标识。THRESH_OTSU
本身并不是一个独立的阈值化方法,而是与 OpenCV 中的二值化方法结合使用的一个标志。 默认情况下它会与 THRESH_BINARY
结合使用。
(2)双峰图:
如下的直方图是对灰度图中每个像素值的点的个数的统计图,双峰图片就是指灰度图的直方图上有两个峰值。
直方图说明:
- 直方图是一个柱状图,其中 x 轴表示灰度级(从 0 到 255),y 轴表示对应灰度级在图像中出现的次数(频率)。
- 每个柱子的高度代表该灰度级在图像中出现的像素数量。
(3)OTSU算法
OTSU算法是通过一个值将这张图分前景色和背景色(也就是灰度图中小于这个值的是一类,大于这个值的是一类。例如,如果你设置阈值为128,则所有大于128的像素点可以被视作前景,而小于等于128的像素点则被视为背景。),通过统计学方法(最大类间方差)来验证该值的合理性,当根据该值进行分割时,使用最大类间方差计算得到的值最大时,该值就是二值化算法中所需要的阈值。通常该值是从灰度图中的最小值加1开始进行迭代计算,直到灰度图中的最大像素值减1,然后把得到的最大类间方差值进行比较,来得到二值化的阈值。
(4)算法解释:
假设有一张4*4 像素的图片:
参数解释如下:
以上根据阈值1分为了前景(像素为2的部分)和背景(像素为0)的部分,并且计算出了OTSU算法所需要的各个数据,现引入计算类间方差的公式:
g就是前景与背景两类之间的方差,这个值越大,说明前景和背景的差别就越大,效果就越好。
OTSU算法就是在灰度图的像素值范围内遍历阈值T,使得g最大,基本上双峰图片的阈值T在两峰之间的谷底。
注意:使用OTSU算法计算阈值时,组件中的thresh参数将不再有任何作用。
1.2.7 代码演示整理
import cv2 as cv
flower = cv.imread('../images/flower.png')
flower = cv.resize(flower, (360, 360))
# 灰度化处理
gray = cv.cvtColor(flower, cv.COLOR_BGR2GRAY)
cv.imshow('gray', gray)
# 全局阈值法
thresh, binary = cv.threshold(gray, 127, 255, cv.THRESH_BINARY)
cv.imshow('binary', binary)
# 反阈值法,_ 表示不接受返回的阈值,以下同理
_, binary_inv = cv.threshold(gray, 127, 255, cv.THRESH_BINARY_INV)
cv.imshow('binary_inv', binary_inv)
# 截断阈值法, 将大于阈值的像素值设置为设定值
_, binary_trunc = cv.threshold(gray, 170, 255, cv.THRESH_TRUNC)
cv.imshow('binary_trunc', binary_trunc)
# 低阈值法, 将小于阈值的像素值设置为设定值
_, binary_tozero = cv.threshold(gray, 127, 255, cv.THRESH_TOZERO)
cv.imshow('binary_tozero', binary_tozero)
# 高阈值法, 将小于阈值的像素值设置为设定值
_, binary_tozero_inv = cv.threshold(gray, 127, 255, cv.THRESH_TOZERO_INV)
cv.imshow('binary_tozero_inv', binary_tozero_inv)
# OTSU阈值法,默认结合了阈值法
# 阈值是OTSU计算得出的
aaa, binary_otsu = cv.threshold(gray, 0, 255, cv.THRESH_BINARY + cv.THRESH_OTSU)
cv.imshow('binary_otsu', binary_otsu)
print('otsu阈值为:', aaa)
cv.waitKey(0)
cv.destroyAllWindows()
1.3 自适应二值化
1.3.1 概念与意义
- 自适应二值化(Adaptive Thresholding)的核心思想就是为图像中的每个像素点计算一个局部阈值。这种方法与全局阈值化不同,全局阈值化是对整个图像使用同一个固定的阈值,而在自适应二值化中每个像素的阈值是基于其周围邻域内的像素值动态确定的。
- 与二值化算法相比,自适应二值化更加适合用在明暗分布不均的图片,因为图片的明暗不均,导致图片上的每一小部分都要使用不同的阈值进行二值化处理,这时候传统的二值化算法就无法满足我们的需求了,于是就出现了自适应二值化。
- 自适应二值化方法会对图像中的所有像素点计算其各自的阈值,这样能够更好的保留图片里的一些信息。
1.3.2 方法介绍
cv2.adaptiveThreshold(image_np_gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 7, 10)
关于cv2.adaptiveThreshold参数解释:
- image_np_gray: 输入图像,这里必须是灰度图像(单通道)。
- 255: 输出图像的最大值。在二值化后,超过自适应阈值的像素会被设置为该最大值,通常为255表示白色;未超过阈值的像素将被设置为0,表示黑色。
- cv2.ADAPTIVE_THRESH_GAUSSIAN_C: 自适应阈值类型。在这个例子中,使用的是高斯加权的累计分布函数(CDF),并添加一个常数 C 来计算阈值。另一种可选类型是 cv2.ADAPTIVE_THRESH_MEAN_C,它使用邻域内的平均值加上常数 C 计算阈值。
- cv2.THRESH_BINARY: 输出图像的类型。这意味着输出图像将会是一个二值图像(binary image),其中每个像素要么是0要么是最大值(在这里是255)。另外还有其他选项如 cv2.THRESH_BINARY_INV 会得到相反的二值图像。
- 7是blockSize参数,表示计算每个像素阈值时所考虑的7x7邻域大小(正方形区域的宽度和高度),其值必须是奇数。
- 10即C参数,常数值,在计算自适应阈值时与平均值或高斯加权值相加。正值增加阈值,负值降低阈值,具体效果取决于应用场景。对阈值的自定义调整。
maxval
:最大阈值,一般为255adaptiveMethod
:小区域阈值的计算方式:ADAPTIVE_THRESH_MEAN_C
:小区域内取均值ADAPTIVE_THRESH_GAUSSIAN_C
:小区域内加权求和,权重是个高斯核thresholdType
:二值化方法,只能使用THRESH_BINARY、THRESH_BINARY_INV,也就是阈值法和反阈值法blockSize
:选取的小区域的面积,如7就是7*7的小块。c
:自定义调整参数,最终阈值等于小区域计算出的阈值再减去此值
1.3.3 取均值法
可能直接看图会比文字叙述更直观清晰,这里直接放图算了:
如图,假设使用的小区域为3*3,从左上角开始,根据区域内所有块的值来取平均,计算出阈值。
如果处于边缘地区就会对边界进行填充,填充值就是边界的像素点。
1.3.4 高斯加权法(重难点)
对小区域内的像素进行加权求和得到新的阈值,其权重值来自于高斯分布。
(1)高斯分布
高斯分布,通过概率密度函数来定义高斯分布,一维高斯概率分布函数为:
通过改变函数中和的值,我们可以得到如下图像:
此时我们拓展到二维图像,一般情况下我们使x轴和y轴的相等并且,此时我们可以得到二维高斯函数的表达式为:
(2)高斯核运用
如上的公式其实就是高斯核,高斯核是来源于高斯函数(Gaussian function)的一种二维滤波模板,是图像处理中最常见的卷积核之一。
特点:
- σ(\sigma):标准差,决定核的“模糊程度”。
- 中心权重最大,越远的权重越小。
- 整个核的权重和为 1(归一化后)。
我们的目的是通过这个高斯核,对图片中的每个像素去计算其阈值,并将该阈值减去固定值得到最终阈值,然后根据二值化规则进行二值化。
在opencv里,当kernel(小区域)的尺寸为1、3、5、7并且用户没有设置sigma的时候(sigma <= 0),核值就会取固定的系数,这是一种默认的值是高斯函数的近似。
举例:
比如kernel的尺寸为3*3时,使用
进行矩阵的乘法,就会得到如下的权重值,其他的类似。
而当kernels尺寸超过7的时候,如果sigma设置合法(用户设置了sigma),则按照高斯公式计算.当sigma不合法(用户没有设置sigma),则按照如下公式计算sigma的值:
(3)演算示例图
如图某像素点的阈值计算过程如下图所示:
首先还是对边界进行填充,然后计算原图中的左上角(也就是162像素值的位置)的二值化阈值,其计算过程如上图所示,再然后根据选择的二值化方法对左上角的像素点进行二值化,之后核向右继续计算第二个像素点的阈值,第三个像素点的阈值…直到右下角(也就是155像素值的位置)为止。
(4)代码示例部分
import cv2 as cv
flower = cv.imread('../images/flower.png')
flower = cv.resize(flower, (360, 360))
# 灰度化处理
img = cv.cvtColor(flower, cv.COLOR_BGR2GRAY)
cv.imshow('img', img)
# 阈值法用于对比
ret, img_thresh = cv.threshold(img, 127, 255, cv.THRESH_BINARY)
cv.imshow('thresh', img_thresh)
print(ret)
# 取均值法,自适应二值化
img_mean = cv.adaptiveThreshold(img, 255, cv.ADAPTIVE_THRESH_MEAN_C, cv.THRESH_BINARY, 11, 2)
cv.imshow('img_mean', img_mean)
# 加权求和法,自适应二值化,高斯核
img_gaussian = cv.adaptiveThreshold(img, 255, cv.ADAPTIVE_THRESH_GAUSSIAN_C, cv.THRESH_BINARY, 11, 2)
cv.imshow('img_gaussian', img_gaussian)
cv.waitKey(0)
cv.destroyAllWindows()
二、图像的仿射变换
仿射变换(Affine Transformation)是一种线性变换,保持了点之间的相对距离不变。
2.1 基本翻转
作为缓冲,这里先插入一个基础方法,是用于图像的镜像翻转的
cv2.flip(img,flipcode)
参数:
im 要翻转的图像
flipcode: 指定翻转类型的标志
flipcode=0: 垂直翻转,图片像素点沿x轴翻转
flipcode>0: 水平翻转,图片像素点沿y轴翻转
flipcode<0: 水平垂直翻转,水平翻转和垂直翻转的结合
简单练习一下:
import cv2 as cv
# ps:这里你可以读取你自己的图片
img = cv.imread('../images/face.png')
# 以图片中心为轴,0垂直翻转,大于0水平翻转,小于0垂直水平翻转
flip_0 = cv.flip(img, 0)
flip_1 = cv.flip(img, 1)
flip_2 = cv.flip(img, -1)
cv.imshow('img', img)
cv.imshow('flip_0', flip_0)
cv.imshow('flip_1', flip_1)
cv.imshow('flip_2', flip_2)
cv.waitKey(0)
cv.destroyAllWindows()
2.2 仿射变换概念
2.2.1 仿射变换的基本性质
- 保持直线
- 保持平行
- 比例不变性
- 不保持角度和长度
2.2.2 常见的仿射变换类型
- 旋转:绕着某个点或轴旋转一定角度。
- 平移:仅改变物体的位置,不改变其形状和大小。
- 缩放:改变物体的大小。
- 剪切:使物体发生倾斜变形。
2.2.3 仿射变换的基本原理
仿射变换(Affine Transformation)是一种线性变换,保持了点之间的相对距离不变。
二维空间中,图像点坐标为(x,y),仿射变换的目标是将这些点映射到新的位置 (x', y')。
为了实现这种映射,通常会使用一个矩阵乘法的形式:
(类似于y=kx+b)
a,b,c,d 是线性变换部分的系数,控制旋转、缩放和剪切。
t_x,t_y 是平移部分的系数,控制图像在平面上的移动。
输入点的坐标被扩展为齐次坐标形式[x,y,1],以便能够同时处理线性变换和平移
2.2.4 仿射变换函数 cv2.warpAffine()
cv2.warpAffine(img,M,dsize)
img:输入图像。
M:2x3的变换矩阵,类型为
np.float32
。dsize:输出图像的尺寸,形式为
(width,height)
。
2.3 单点旋转
以单个点的旋转作为引入,进而了解图像旋转的原理
2.3.1 概念引入
首先我们以最简单的一个点的旋转为例子,且以最简单的情况举例,令旋转中心为坐标系中心O(0,0),假设有一点P[0](x[0],y[0]),P[0]离旋转中心O的距离为r,OP[0]与坐标轴x轴的夹角为\alpha,P[0]绕O顺时针旋转\theta角后对应的点为P(x,y)。
可以得到如下关系:
2.3.2 旋转矩阵
以上用矩阵来表示就是:
然而,在OpenCV中,旋转时是以图像的左上角为旋转中心,且以逆时针为正方向,因此上面的例子中角\theta其实是个负值,所以该矩阵可写为:(简单推导即可得)
其中:
也被称作旋转矩阵。
2.3.3 平移矩阵
现在我们了解了旋转矩阵,然而我们所要的不仅仅是可以围绕图像左上角进行旋转,而是可以围绕任意点进行旋转。那么我们可以将其转化成绕原点的旋转,其过程为:
- 首先将旋转点移到原点
- 按照上面的旋转矩阵进行旋转得到新的坐标点
- 再将得到的旋转点移回原来的位置
也就是说,在以任意点为旋转中心时,除了要进行旋转之外,还要进行平移操作。那么当点经过平移后得到P点时,如下图所示:
那么我们就可以得到:
写成矩阵的形式为:
其中
也被叫做平移矩阵。 ... ... ... ... (1)
相反的,从P移到点时,其平移矩阵为:记为(2)
我们将原始的旋转矩阵也扩展到3*3的形式:
从平移和旋转的矩阵可以看出,3x3矩阵的前2x2部分是和旋转相关的,第三列与平移相关。有了上面的表达式之后,我们就可以得到二维空间中绕任意点旋转的旋转矩阵了,只需要将旋转矩阵先左乘 (1) 式子,再右乘(2)即可得到最终的矩阵:
(这里理解即可,千万不要死记硬背!不理解的话。。。直接跳过就是了。)
于是我们就可以根据这个矩阵计算出图像中任意一点绕某点旋转后的坐标了,这个矩阵学名叫做仿射变换矩阵,而仿射变换是一种二维坐标到二维坐标之间的线性变换,也就是只涉及一个平面内二维图形的线性变换,图像旋转就是仿射变换的一种。
2.4 图像旋转
旋转图像,即将图像绕着某个点旋转一定的角度。
2.4.1 函数cv2.getRotationMatrix2D()
获取旋转矩阵(会返回一个二维的矩阵) :
cv2.getRotationMatrix2D(center,angle,scale)
center:旋转中心点的坐标,格式为
(x,y)
。angle:旋转角度,单位为度,正值表示逆时针旋转负值表示顺时针旋转。
scale:缩放比例,若设为1,则不缩放。
返回值:M,2x3的旋转矩阵。
2.4.2 代码示例
import cv2 as cv
img = cv.imread('../images/1.jpg')
# img = cv.resize(img, (360, 360))
# 获取旋转矩阵(会返回一个二维的矩阵) 旋转中心,旋转角度(顺时针负),缩放比例(1不变)
M = cv.getRotationMatrix2D((img.shape[1] / 2, img.shape[0] / 2), -45, 1)
# 仿射变换,M是旋转矩阵(float32),img是原图,dsize是输出图片的大小(宽在前高在后)
img_rotate = cv.warpAffine(img, M, (img.shape[1], img.shape[0]))
cv.imshow('img', img)
cv.imshow('img_rotate', img_rotate)
cv.waitKey(0)
cv.destroyAllWindows()
2.5 图像平移缩放
2.5.1 图像平移
- 平移操作可以将图像中的每个点沿着某个方向移动一定的距离。
假设我们有一个点 P(x,y),希望将其沿x轴方向平移t_x*个单位,沿y轴方向平移t_y个单位到新的位置P′(x′,y′),那么平移公式如下:
x′ = x + tx
y′ = y + ty
在矩阵形式下,该变换可以表示为:
这里的t_x和t_y分别代表在x轴和y轴上的平移量。
2.5.2 图像缩放
- 缩放操作可以改变图片的大小。
假设要把图像的宽高分别缩放为0.5和0.8,那么对应的缩放因子sx=0.5,sy=0.8。
点P(x,y)对应到新的位置P'(x',y'),缩放公式为:
x′ = s_x * x
y′ = s_y * y
sx和sy分别表示在x轴和y轴方向上的缩放因子。
在矩阵形式下,该变换可以表示为:
相较于图像旋转中只能等比例的缩放,图像缩放更加灵活,可以在指定方向上进行缩放。
2.5.3 代码演示
import cv2 as cv
import numpy as np
img = cv.imread('../images/1.jpg')
img = cv.resize(img, (520, 520))
# 定义平移量
tx, ty = 80, 120
# 获取平移矩阵
M = np.float32([[1, 0, tx], [0, 1, ty]])
# print(M)
dst_p = cv.warpAffine(img, M, (img.shape[1], img.shape[0]))
# 定义缩放量
sx = 0.5
sy = 0.5
# 获取缩放矩阵
M1 = np.float32([[sx, 0, 0], [0, sy, 0]])
dst_s = cv.warpAffine(img, M1, (img.shape[1], img.shape[0]))
# 整合,同时缩放和平移
M2 = np.float32([[sx, 0, tx], [0, sy, ty]])
dst_sp = cv.warpAffine(img, M2, (img.shape[1], img.shape[0]))
print(M2)
# 显示
cv.imshow('img', img)
cv.imshow('dst_p', dst_p)
cv.imshow('dst_s', dst_s)
cv.imshow('dst_sp', dst_sp)
cv.waitKey(0)
cv.destroyAllWindows()
2.6 图像剪切
2.6.1 概念初识
图像剪切(Shearing)是仿射变换(Affine Transformation)中的一种基本操作,它会使图像在x方向或y方向上发生倾斜变形。在数学上,剪切变换可以看作是将图像的像素坐标沿着一个轴向按比例偏移。
2.6.2 数学表示
用矩阵表示:
水平剪切(x方向剪切)
[ 1 shx 0 ]
[ 0 1 0 ]
[ 0 0 1 ]
垂直剪切(y方向剪切)
[ 1 0 0 ]
[shy 1 0 ]
[ 0 0 1 ]
也即
放个图方便理解:
2.6.3 OpenCV中的实现
可以使用 warpAffine()
函数结合剪切变换矩阵来实现图像剪切
import cv2 as cv
import numpy as np
# 读取图像
img = cv.imread("../images/1.jpg")
# 定义剪切参数
shx = 0.5 # 水平剪切因子
shy = 0.3 # 垂直剪切因子
# 构建剪切变换矩阵
M = np.float32([[1, shx, 0], [shy, 1, 0]])
# 获取图像尺寸
rows, cols = img.shape[:2]
# 应用仿射变换
sheared_img = cv.warpAffine(img, M, (int(cols * (1 + abs(shx))), int(rows * (1 + abs(shy)))))
# 显示结果
cv.imshow("Sheared Image", sheared_img)
cv.imshow("Original Image", img)
cv.waitKey(0)
cv.destroyAllWindows()