前言
书接上文
自适应二值化与形态学变换在图像颜色识别与替换中的应用解析-CSDN博客文章浏览阅读852次,点赞15次,收藏6次。本文系统解析图像处理中的自适应二值化、形态学变换与颜色操作技术,涵盖动态阈值计算、腐蚀膨胀原理、HSV颜色空间及掩膜应用,通过代码实例演示如何实现复杂光照图像的细节保留、噪声消除及精准颜色替换,为图像分析提供高效方法。https://blog.csdn.net/qq_58364361/article/details/146901473?spm=1011.2415.3001.10575&sharefrom=mp_manage_link
一、ROI切割
ROI(Region of Interest,感兴趣区域)指的是从原始图像中定义处特定的区域,该区域对于后续的分析、处理具有特别的意义。
比如对于一个人的照片,加入算法要检测眼睛,因为眼睛肯定在脸上,所以只要对脸部感兴趣,其他区域无视,可以通过ROI切割把人脸截取出来,这样的节省计算量,提高程序的运行速度。
本质上是通过Numpy数组的切片操作完成
1.1 切割数据
参数如下:
- x_min:ROI区域最小的横坐标
- x_max:ROI区域最大的横坐标
- y_min:ROI区域最小的纵坐标
- y_max:ROI区域最大的纵坐标
之前已经学过在OpenCV中坐标的x轴正方向是水平向右的,y轴的正方向是垂直向下的。
另外Numpy存储图像是一个三维数组:
- 第一个维度(0):高度(行数)
- 第二个维度(1):宽度(列数)
- 第三个维度(2):三个通道BGR的像素值
OpenCV中一个点的坐标是先x后y,这一点与Numpy相反。
1.2代码
使用ROI切割,截取坤坤的篮球。
import cv2
import numpy as np
import matplotlib.pyplot as pltdef show_image(image, name):"""显示图像Args:image: 输入图像(numpy数组)name: 图像标题(str)"""plt.rcParams['font.sans-serif'] = ['SimHei'] # 设置中文字体plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题if len(image.shape) == 3: # 彩色图像image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) # BGR转RGBelse: # 灰度图像image = cv2.cvtColor(image, cv2.COLOR_GRAY2RGB) # 灰度转RGBplt.imshow(image) # 显示图像plt.title(name) # 设置标题plt.axis('off') # 关闭坐标轴# 图像路径
path = "kunkun.jpg"
# 因为图像容易越界,所以加入异常处理
try:# 读取图像image = cv2.imread(path)# 创建画布,设置dpi为300plt.figure(dpi=300)# 第一个子图:原始图像plt.subplot(1, 3, 1)show_image(image, "原始图像")# 获得图像尺寸(h, w,_ )= image.shape# 设置切割范围x_min, x_max, y_min, y_max = 500, 607, 269, 368# 判断ROI区域是否合理if not x_min >= 0 and x_max <= w and y_min >= 0 and y_max <= h:raise Exception("ROI区域越界")if x_min >= x_max or y_min >= y_max:raise Exception("ROI区域最值不合法")# 在图像上画红色矩形框line_width = 2cv2.rectangle(image, (x_min-line_width, y_min-line_width), (x_max+line_width, y_max+line_width), (0, 0, 255), line_width)# 第二个子图:带矩形框的图像plt.subplot(1, 3, 2)show_image(image, "切割图像画框")# 切片获取ROI区域ROI_img = image[y_min:y_max, x_min:x_max]# 第三个子图:ROI区域图像plt.subplot(1, 3, 3)show_image(ROI_img, "切片图像")# 显示所有子图plt.show()
except Exception as e:print("图像读取失败!", e)
二、图像旋转
图像旋转指的是图像以某一点为中心,将图像中所有的像素点围绕该点旋转一定的角度,并且旋转后像素点组成的图像与原图像相同。
2.1 仿射变换
旋转 镜像 缩放 平移 剪切 不均匀缩放
2.1.1 单点旋转
首先先以一个单点旋转的案例进行讲解,如下图所示。旋转中心为坐标系中心O(0,0),假设有一个点,距离旋转中心的距离为r,
与x轴的夹角为α,
绕O点顺时针旋转的角度为θ,旋转后的点为P(x,y)。
可以得到如下关系:
只看红框内容,使用线性代数的计算方式构建矩阵表示:
上面的旋转是左下角原点,以顺时针方向旋转的,但是OpenCV中原点在左上角且正方向是逆时针,因此需要修改上面的公式:
其中被称为旋转矩阵。
2.1.2 平移
上面的旋转是以原点进行的,图像旋转不仅仅是围绕图像左上角进行旋转,而是可以围绕任意点进行旋转,可以把围绕任意点旋转的问题转换为围绕原点旋转的问题。
求过程为:
1. 首先将旋转点移动到原点
2. 按照1.1节推理的公式,使用旋转矩阵对图像的各个像素点进行旋转,得到每个像素点的新坐标。
3. 再将得到的旋转点移动会原来的位置。
因此在以任意点为旋转中心时,除了要进行旋转外,还要进行平移操作,平移操作如下所示:
那么可以得到
把上面的计算规则拓展为矩阵表示,在计算中图形学中需要给(x,y)再补充一个1,(x,y,1)为齐次坐标或投影坐标:
于是被叫做平移矩阵,相反的如果从P移动到,其平移矩阵为:
2.1.3 复合矩阵
把1.1和1.2节结合的操作被称为仿射变换,中间的计算矩阵被称为复合矩阵。
先把1.1节的原始的旋转矩阵拓展到1.2节平移矩阵的3*3的格式:
回到操作的三个过程:
1. 先从旋转点移动到原点 → 平移 → 平移矩阵
2. 基于进行旋转 → 单点旋转 → 旋转矩阵
3. 从原点移动回旋转点 → 逆平移 → 逆平移矩阵
需要注意的是,矩阵的乘法是有顺序的,先进行的操作要在左边,因此把上面三个过程相乘的到复合矩阵。
结果如下:
这个矩阵学名叫做仿射变换矩阵,仿射变换是一种二维坐标到二维坐标的线性变换,也就是只涉及一个平面内的二维图形,图像旋转是仿射变换的一种,它保持了二维图形变换的性质:
- 平直性
直线在经过变换后还是直线。
- 平行性
平行线在经过变换后还是平行线。
2.2 差值方法
在之前的旋转过程中可能会出现下面的问题:
1. 取整丢失精度
由于三角函数的值是小数,其乘积也是小数,但那是OpenCV在计算的过程中会进行取整操作,因为像素点旋转之后的取整结果可能有重合,会导致转换后的图像丢失一部分像素信息。
2. 缩放丢失精度。
尝试更改图像旋转节点的scale参数(为了好观察,angle先设置为0):
例如原图的分辨率是10*10,放大之后变为20*20,图像由100个像素点变为400个像素点,那么多余的300个像素点数据如何而来???
再如原图的分辨率是20*20,缩小之后变为10*10,需要丢弃300个像素点,如何丢弃才能保证图像的正常显示???
使用插值法来解决上述出现的问题,插值是一种通过已知的数据点之前的关系推断或估计来获取新的数据点的方法,常用于仿射变换相关操作,包括但不限于图像放大、缩小、旋转变形等。
原始图像
本次学习提供了五种常见的插值算法:
- 最近邻插值
速度最快,但是可能会导致图像出现锯齿状边缘(马赛克)和失真。
- 双线性插值
相比于最近邻插值计算速度稍慢,但是效果大幅提升,适用于绝大多数场景。
- 像素区域插值
更适合缩小图像且保持质量时使用。
- 双三次插值
速度很慢,但是效果很好。
- Lanczos(兰克索斯)插值
速度最慢,效果最好。
2.2.1 最近邻插值
以图片放大为例,dst表示放大后的目标图像,src表示放大前的原图。
上面的公式的目的是让dst图像的每个像素值都对应到src原图的某个像素值,例如从一个2*2的图像放大到4*4的过程如下:
根据公式可以计算放大之后的dst图像点对应的原图src像素点为:
上面的运算结果表示对应的原图src像素点为
接下来计算dst图中点对应的原图src像素点,仍然套用公式:
对应的是原图src的(0.5, 0)像素点,OpenCV固定向下取整,取整后为。
全图像素点依次计算,得到完整的dst图像。
2.2.2双线性插值
双线性插值算法是一种比较好的图像缩放算法,它充分利用了原图src中虚拟点四周的四个真实像素点来推算dst图中的一个像素点的值。因此效果比较好,不会出现不连续的情况。
2.2.2.1 线性插值
假如已知两个点(x0,y0)和(x1,y1),计算两个点之间的某个位置x在直线上的y值,过程为:
已知x求y:
上面的过程就是一个线性插值,双线性插值本质上就是在两个方向上进行线性插值。
2.2.2.2 双线性插值
假设根据上述公式计算出了dst中某个点对应的点P(通常是个小数),所以在周围会有四个原始图像的像素点(左上Q12、右上Q22、左下Q11、右下Q21)包围。
第一个方向的单线性插值:
先根据Q12和Q22计算单线性插值R2的像素值,再计算Q11和Q21单线性插值R1的像素值。
第二个方向的单线性插值:
根据上一步计算出的R1和R2,再进行一次单线性插值,得到P点的像素值。
2.2.2.3 双线性插值的问题
假设原图src是3*3的,目标图像dst是4*4的,在插值过程中存在以下问题:
1. 不同位置的像素点处理不公平
无论采取什么坐标系,都会发现src的原点会直接复制到dst中;而有些边界的像素点相当于只进行了1次单线性插值。
2. 整体图像的相对位置会发生变化
如下图所示,左侧为src原图,右侧为dst目标图像,源图像的几何中心像素点坐标为(1,1),但是目标图像的几何中心点为(2,2)。根据点和点的对应关系,目标图像的集合中心点应该为(1.2,1.2)。
按照之前的坐标系,原图与目标图像应该是原点重合,因此目标图像的几何中心点相对于原始图像的几何中心点偏向于右下角。
因此在OpenCV中,为了解决上面的问题,对公式进行了优化,如下:
OpenCV接口内部使用上述方式解决存在的问题。
2.2.3 像素区域差值
像素区域插值主要分为两种情况:缩小图像和放大图像的原理并不相同。
1. 缩小图像
内部会使用一个均值滤波器(滤波器就是一个核,后续实验会介绍),其工作原理可以理解为对一个区域内的像素考虑核之后进行平均。
2. 放大图像
- 如果放大的是整数倍
与最近邻插值类似
- 如果放大的不是整数倍
直接使用双线性插值
2.2.4 双三次插值
与双线性插值类似,该方法也是通过映射,在映射点的邻域内通过加权来的到放大图像的像素值。不同的是双三次插值法需要源图像src中近邻的16个像素点来进行加权。
以上图为例,假设源图像src的大小为m*n,缩放之后的目标图像dst大小为M*N,其中src的每个像素点是已知的,目标图像dst是未知的,需要求出dst中每个像素点(X,Y)的值。
必须先找出像素B(X,Y)在源图像中对应的像素(x,y),再根据(x,y)在原图src中最近的16个像素点作为计算(X,Y)的参数,利用BiCubic基函数求出16个像素点的权重,dst中(X,Y)的值就是src中16个像素点的加权求和。
BiCubic函数:
假设上图中的P点就是dst在(X,Y)处根据公式计算出的对应源图像src的位置,P的坐标(x+u,y+v)会出现小数部分,x,y表示整数部分,u,v表示小数部分,利用a00、a01......表示需要加权的16个像素点。
需要把16个像素点带入到BiCubic函数中,求出每个像素点的权重W,需要注意的是变量a经过试验通常取-0.5或-0.75。
剩下的变量只有x,但是a00等点的坐标是二维,需要把a00等点的x和y分别拆开带入BiCubic函数中计算。
例如,计算a00点的权重:
先把x=0带入BiCubic函数中,求出x方向权重W(i);
再把y=0带入BiCubic函数总,求出y方向的权重W(j)。
相乘得到最终权重,计算所有16个点的权重,公式如下:
2.2.5 Lanczos插值
Lanczos插值方法与双三次插值的思想是一样,不同的是近邻的像素点范围变成了8*8,并且换了一个公式来计算像素点的权重。
权重公式换为:
其中a通常取2或者3,当a=2时,该算法适合图像缩小;当a=3时,该算法适合图像放大。
其他的计算过程与双三次插值完全相同,最终的权重公式为:
2.3 边缘填充
下图中左边为原图src,右边为旋转后的目标图像dst。可以看到dst中四个角没有对应的像素值,因此使用了(0,0,0)黑色填充,除此之外还可以使用其他的方式填充。
2.3.1 边界复制 BORDER_REPLICATE
边界复制会将边界处的像素值进行复制,然后作为边界填充的像素值,如下图所示,可以看到四周的像素值都一样。
2.3.2 边界反射 BORDER_REFLECT
2.3.3 边界反射101 BORDER_REFLECT_101
与边界反射的区别是,不再反射边缘像素的点。
2.3.4 边界常数 BORDER_CONSTANT
边界常数表示补充一个自定义的像素值,下图虚拟仿真节点中的参数为补充的像素的BGR通道值。
2.3.5 边界包裹 BORDER_WRAP
如下图所示,与边界反射的区别是不再进行轴对称。
2.4 代码
import cv2
import numpy as np
import matplotlib.pyplot as pltdef show_image(image, name):"""显示图像Args:image: 输入图像(numpy数组)name: 图像标题(str)"""plt.rcParams['font.sans-serif'] = ['SimHei'] # 设置中文字体plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题if len(image.shape) == 3: # 彩色图像image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) # BGR转RGBelse: # 灰度图像image = cv2.cvtColor(image, cv2.COLOR_GRAY2RGB) # 灰度转RGBplt.imshow(image) # 显示图像plt.title(name) # 设置标题plt.axis('off') # 关闭坐标轴path = "nvren.png" # 图像文件路径
image = cv2.imread(path) # 读取图像
image_shape = image.shape # 获取图像形状(高度,宽度,通道数)
plt.figure(dpi=300) # 创建画布,设置DPI
plt.subplot(1, 2, 1) # 创建子图1
show_image(image, "原始图像") # 显示原始图像#旋转图像
angle = 45 # 旋转角度
scaled = 0.5 # 缩放比例
"""
获取旋转矩阵
参数:center: 旋转中心点坐标angle: 旋转角度(正值表示逆时针)scale: 缩放比例
"""
M = cv2.getRotationMatrix2D((image_shape[1] // 2, image_shape[0] // 2), angle, scaled) # TODO 仿射变换"""
执行仿射变换
参数:src: 输入图像M: 2x3变换矩阵dsize: 输出图像尺寸flags: 插值方法(这里使用LANCZOS4)borderMode: 边界填充模式(这里使用BORDER_WRAP)
"""
image_rotated = cv2.warpAffine(image, M, (image_shape[1], image_shape[0]),flags=cv2.INTER_LANCZOS4,borderMode=cv2.BORDER_WRAP)
plt.subplot(1, 2, 2) # 创建子图2
show_image(image_rotated, "旋转图像") # 显示旋转后的图像
总结
本文深入探讨了OpenCV中ROI区域切割与图像旋转的核心技术,通过Numpy数组切片实现篮球目标的精准提取,详细解析了仿射变换矩阵的数学原理与坐标转换机制,对比五种插值算法在图像旋转中的优劣表现,结合边界填充模式实现旋转后图像的无损处理,并提供了完整的Python代码实现与可视化演示,为计算机视觉开发中的图像预处理提供完整解决方案。