从像素到二值化:OpenCV图像处理实战入门指南
目录
- 前言
- 一、像素
- 二、计算机中图像的存储
- 三、opencv
- 四、计算机眼中的图像
- 五、颜色选择器
- 六、 灰度实验
- 6.1 最大值法
- 6.2 平均值法
- 6.3 加权平均法
- 七、二值化
- 7.1 阈值法(THRESH_BINARY)
- 7.2 反阈值法(THRESH_BINARY_INV)
- 7.3 截断阈值法(THRESH_TRUNC)
- 7.4 低阈值零处理(THRESH_TOZERO)
- 7.5 超阈值零处理(THRESH_TOZERO_INV)
- 7.6 OTSU阈值法
- 总结
前言
机器视觉属于一个新的阶段:机器学习。
本质上机器视觉就是学习OpenCV库,使用Python代码对图像进行处理:
数学理论
主要用于面试加分,不要过度陷入其中,例如很多公式都是前人总结的,直接使用即可,部分公式简单推导。
图像理论
目标图像的指向性,只有清楚图像理论,才能让代码的产出符合预期。
代码编程
重点是理解代码的步骤和含义,以及参数如何调节能达到预期的效果。
一、像素
像素是图像的基本单元,每个像素存储着图像的颜色、亮度和其他特征。一系列像素组合到一起就形成了完整的图像,在计算机中,图像以像素的形式存在并采用二进制格式进行存储。根据图像的颜色不同,每个像素可以用不同的二进制数表示。
日常生活中常见的图像是RGB三原色图。RGB图上的每个点都是由红(R)、绿(G)、蓝(B)三个颜色按照一定比例混合而成的,几乎所有颜色都可以通过这三种颜色按照不同比例调配而成。在计算机中,RGB三种颜色被称为RGB三通道,根据这三个通道存储的像素值,来对应不同的颜色。
RGB每个通道的范围是0-255,8位,255表示颜色打满,0表示黑色,(255,0,0)表示纯红色,(0,0,0)表示纯黑色。
二、计算机中图像的存储
在本次学习的过程中,图像处理使用OpenCV库实现,OpenCV使用一个三维数组(矩阵)存储一张图片的数据。
一个RGB图像放在内存中是一个三维数组,其中第一维存储的是图像的高度height(行数rows),第二维存储的是图像的宽度width(列数cols),第三维存储的是每个像素点RGB的数值,计算机中处理图像的本质就是对三维数组进行计算。
三、opencv
官方文档查询
OpenCV: OpenCV moduleshttps://docs.opencv.org/4.5.2/
OpenCV的源代码是C++实现的,Python可以调用C++源代码。基础使用库Python语言比较方便,但是做到行业顶尖需要C++。
四、计算机眼中的图像
预期图案
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 设置matplotlib显示中文字体
plt.rcParams['font.sans-serif']=['SimHei'] # 设置字体为黑体以正常显示中文
plt.rcParams['axes.unicode_minus']=False # 解决负号显示问题
"""
创建纯黑图像
参数:
size: 图像尺寸(700,700,3), 表示700x700像素的3通道(BGR)图像
dtype: np.uint8, 表示像素值范围为0-255的无符号8位整数
返回值:
全零的黑色图像矩阵
"""
image = np.zeros((700,700,3), np.uint8)
# 定义每个小方块的大小
block_size = 100
"""
绘制棋盘格图案
算法说明:
1. 遍历图像每个block_size间隔的像素点
2. 在主对角线或副对角线上且不在第一行/最后一行时绘制红色方块
3. 其他情况绘制白色边框方块
"""
for i in range(0,700,block_size):
for j in range(0,700,block_size):
top_left = (j,i) # 当前方块左上角坐标
bottom_right = (j+block_size,i+block_size) # 当前方块右下角坐标
# 判断是否在主/副对角线上且不在边界行
if (i//block_size == j//block_size or i//block_size + j//block_size==6) and (i != 0 and i!= 600):
# 绘制填充红色矩形(-1表示填充)
cv2.rectangle(image,top_left,bottom_right,(0,0,255),-1)
else:
# 绘制白色边框矩形(2表示边框粗细)
cv2.rectangle(image,top_left,bottom_right,(255,255,255),2)
# 将BGR格式转换为RGB格式以便matplotlib正确显示
image_rgb = cv2.cvtColor(image,cv2.COLOR_BGR2RGB)
# 显示图像
plt.imshow(image_rgb)
plt.title('红色X图案')
plt.axis('off') # 隐藏坐标轴
plt.show()
"""
-------拆分通道-------
函数说明:
cv2.split() - 将多通道图像拆分为单通道数组
参数:
image: 输入的多通道图像
返回值:
多个单通道数组(按B,G,R顺序)
"""
b,g,r = cv2.split(image)
# 创建全零矩阵用于存储单通道数据
blue_channel = np.zeros((700,700,3),np.uint8)
blue_channel[:,:,0] = b # 蓝色通道
green_channel = np.zeros((700,700,3),np.uint8)
green_channel[:,:,1] = g # 绿色通道
red_channel = np.zeros((700,700,3),np.uint8)
red_channel[:,:,2] = r # 红色通道
# 显示各通道图像
blue_channel_rgb = cv2.cvtColor(blue_channel,cv2.COLOR_BGR2RGB)
plt.subplot(1,3,1)
plt.imshow(blue_channel_rgb)
plt.title('蓝色通道')
plt.axis('off')
green_channel_rgb = cv2.cvtColor(green_channel,cv2.COLOR_BGR2RGB)
plt.subplot(1,3,2)
plt.imshow(green_channel_rgb)
plt.title('绿色通道')
plt.axis('off')
red_channel_rgb = cv2.cvtColor(red_channel,cv2.COLOR_BGR2RGB)
plt.subplot(1,3,3)
plt.imshow(red_channel_rgb)
plt.title('红色通道')
plt.axis('off')
plt.show()
五、颜色选择器
在线颜色选择器 | RGB颜色查询对照表https://tools.jb51.net/static/colorpicker/
六、 灰度实验
彩色图是由RGB三个通道组成,灰度图只有一个通道,也成为单通道图像,所以彩色图转换为灰度图的过程本质就是将RGB三通道合并成一个通道的过程。
实验中介绍三种合并方法:
- 最大值法
- 平均值法
- 加权平均值法
6.1 最大值法
6.2 平均值法
6.3 加权平均法
import cv2
import numpy as np
import matplotlib.pyplot as plt
def show_image(image,name):
"""显示图像函数
Args:
image: 要显示的图像(numpy数组格式)
name: 图像窗口标题
"""
# 设置matplotlib显示中文字体
plt.rcParams['font.sans-serif']=['SimHei'] # 设置字体为黑体以正常显示中文
plt.rcParams['axes.unicode_minus']=False # 解决负号显示问题
image = cv2.cvtColor(image,cv2.COLOR_BGR2RGB) # 将BGR格式转换为RGB格式
plt.imshow(image) # 显示图像
plt.title(name) # 设置标题
plt.axis('off') # 关闭坐标轴
if __name__ == '__main__':
#图片输入
path = ".\image_1.jpg" # 图片路径
image_np = cv2.imread(path) # 读取图片
show_image(image_np,"原图") # 显示原图
plt.show()
#加权平均法
image_np_gray = np.zeros_like(image_np) # 创建与原图相同大小的全零数组
wr = 0.299 # 红色通道权重
wg = 0.587 # 绿色通道权重
wb = 0.114 # 蓝色通道权重
for j in range(image_np.shape[0]): # 遍历图像高度
for i in range(image_np.shape[1]): # 遍历图像宽度
# 计算灰度值(加权平均)
mean = image_np[j,i][0]*wb + image_np[j,i][1]*wg + image_np[j,i][2]*wr
image_np_gray[j,i] = mean # 赋值灰度值
#最大值法
image_np_max = np.zeros_like(image_np)
for j in range(image_np.shape[0]):
for i in range(image_np.shape[1]):
max = np.max([image_np[j,i][0],image_np[j,i][1],image_np[j,i][2]])
image_np_max[j,i] = max # 赋值灰度值
#平均值法
image_np_mean = np.zeros_like(image_np)
for j in range(image_np.shape[0]):
for i in range(image_np.shape[1]):
mean = (int(image_np[j,i][0]) + int(image_np[j,i][1]) + int(image_np[j,i][2])) / 3
image_np_mean[j,i] = mean # 赋值灰度值
plt.subplot(1,3,1)
show_image(image_np_gray,"加权平均法")
plt.subplot(1,3,2)
show_image(image_np_max,"最大值法")
plt.subplot(1,3,3)
show_image(image_np_mean,"平均值法")
plt.show()
七、二值化
二值化就是将某张图片的像素改成只有两种值(最常见的是纯黑和纯白),二值化操作的图像必须是灰度图,二值化的过程实际上是将一张灰度图上的像素根据某种规则修改为两种数值(0和maxval),使图像呈现出更为强烈的黑白效果,可以帮助后续处理图像轮廓或边缘等特征。
7.1 阈值法(THRESH_BINARY)
阈值法就是通过设定一个阈值thresh,将灰度图的每一个像素与该阈值进行比较,小于等于阈值的像素被设置为0(黑),大于阈值的像素被设置为maxval。
# 导入必要的库
import cv2 # OpenCV库,用于图像处理
import numpy as np # NumPy库,用于数值计算
import matplotlib.pyplot as plt # Matplotlib库,用于图像显示
def show_image(image, name):
"""
显示图像的函数,适配中文标题并调整图像通道顺序
:param image: 输入图像(BGR格式)
:param name: 图像标题
"""
# 设置Matplotlib中文字体(避免中文乱码)
plt.rcParams['font.sans-serif'] = ['SimHei'] # 设置中文字体
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
# 将BGR格式转换为RGB格式(Matplotlib需要RGB格式)
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
# 显示图像
plt.imshow(image)
plt.title(name) # 设置标题
plt.axis('off') # 关闭坐标轴
if __name__ == '__main__':
# -------------------- 第一部分:图像加载与原始显示 --------------------
plt.subplot(1,2,1) # 创建1行2列的子图,当前操作第1个
# 读取图像(注意路径可能需要根据实际文件位置调整)
path = "./image_1.jpg"
image_np = cv2.imread(path) # 使用OpenCV读取图像(BGR格式)
# 显示原始图像
show_image(image_np, "原图")
img_shape = image_np.shape # 获取图像维度(高度, 宽度, 通道数)
# -------------------- 第二部分:灰度化处理 --------------------
plt.subplot(1,2,2) # 切换到第2个子图位置
# 创建与原始图像相同尺寸的全零矩阵(用于存储灰度结果)
image_np_shap = np.zeros_like(image_np)
# 定义RGB转灰度的权重系数(标准转换系数)
wr = 0.299 # 红色通道权重
wg = 0.587 # 绿色通道权重
wb = 0.114 # 蓝色通道权重
# 手动实现灰度化(存在索引顺序问题,见注释说明)
# 注意:此处双重循环效率较低,实际应用建议使用向量化操作
# 警告:i,j循环顺序可能导致索引越界(当宽度 > 高度时会报错)
# 正确方式应该是 for j in range(height): for i in range(width)
for j in range(img_shape[0]): # 遍历高度维度
for i in range(img_shape[1]): # 遍历宽度维度
# 灰度值计算(BGR通道分别乘对应权重)
# 注意:此处image_np[i,j]的索引顺序可能错误,应为image_np[j,i]
vag = image_np[i,j][0]*wb + image_np[i,j][1]*wg + image_np[i,j][2]*wr
image_np_shap[i,j] = vag # 存储灰度值(三个通道填充相同值)
# 显示手动灰度化结果
show_image(image_np_shap, "灰度化")
plt.show()
# -------------------- 第三部分:二值化处理 --------------------
thresh = 127 # 二值化阈值
maxval = 255 # 最大值
# 方法1:手动阈值处理
for i in range(img_shape[0]): # 遍历高度
for j in range(img_shape[1]): # 遍历宽度
if image_np_shap[i, j][0] <= thresh:
image_np_shap[i, j] = 0 # 低于阈值设为0(黑色)
else:
image_np_shap[i, j] = maxval # 高于阈值设为255(白色)
plt.subplot(1,2,1)
show_image(image_np_shap, "二值化")
# 方法2:OpenCV内置函数实现(更高效)
plt.subplot(1,2,2)
# 调用cv2.threshold函数(注意:此处输入应为单通道图像)
ret, image_np_shap = cv2.threshold(image_np_shap, thresh, maxval, cv2.THRESH_BINARY)
show_image(image_np_shap, "二值化简易方法")
plt.show()
7.2 反阈值法(THRESH_BINARY_INV)
反阈值法与阈值法相反,当灰度图的像素值大于阈值时,像素值会被设置为0;当灰度图的像素值小于等于阈值时,像素值会被设置为最大值。
7.3 截断阈值法(THRESH_TRUNC)
截断阈值法是将灰度图中所有像素值与阈值比较,当像素值大于阈值时被修改为为阈值,小于等于阈值的部分不变。
7.4 低阈值零处理(THRESH_TOZERO)
低阈值零处理表示像素值小于等于阈值的部分被设置为0,大于阈值的部分不变。
7.5 超阈值零处理(THRESH_TOZERO_INV)
超阈值零处理表示像素值大于阈值的部分被设置为0,小于等于阈值的部分不变。
# 导入必要的库
import cv2 # OpenCV库,用于图像处理
import numpy as np # NumPy库,用于数值计算
import matplotlib.pyplot as plt # Matplotlib库,用于绘图和显示图像
def show_image(image, name):
"""
显示图像的函数(适配中文标题)
参数:
image: 要显示的图像(BGR格式)
name: 显示时的图像标题
"""
# 设置matplotlib中文字体(解决中文显示问题)
plt.rcParams['font.sans-serif'] = ['SimHei'] # 指定默认字体
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示异常
# 将BGR格式转换为RGB格式(Matplotlib需要RGB格式)
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
# 显示图像
plt.imshow(image)
plt.title(name) # 设置标题
plt.axis('off') # 关闭坐标轴显示
if __name__ == '__main__':
# ------------------- 1. 图片输入 -------------------
path = 'image_1.jpg' # 图像文件路径
image_np = cv2.imread(path) # 读取BGR格式图像
# ------------------- 2. 灰度化处理 -------------------
image_np_gray = cv2.cvtColor(image_np, cv2.COLOR_BGR2GRAY) # 转换为灰度图
# ------------------- 3. 二值化处理 -------------------
thresh = 127 # 手动设定的阈值(0-255)
maxval = 255 # 像素最大值
# 不同二值化方法对比:
# (1) 反二值化(THRESH_BINARY_INV)
# 大于阈值设为0,小于等于阈值设为maxval
ret_1, image_np_INV = cv2.threshold(
image_np_gray,
thresh,
maxval,
cv2.THRESH_BINARY_INV
)
# (2) 截断阈值化(THRESH_TRUNC)
# 大于阈值的像素截断为阈值,其他像素保持不变
ret_2, image_np_TRUNC = cv2.threshold(
image_np_gray,
thresh,
maxval,
cv2.THRESH_TRUNC
)
# (3) 阈值化为零(THRESH_TOZERO)
# 小于阈值的像素设为0,其他保持不变
ret_3, image_np_TOZERO = cv2.threshold(
image_np_gray,
thresh,
maxval,
cv2.THRESH_TOZERO
)
# (4) 反阈值化为零(THRESH_TOZERO_INV)
# 大于阈值的像素设为0,其他保持不变
ret_4, image_np_TOZERO_INV = cv2.threshold(
image_np_gray,
thresh,
maxval,
cv2.THRESH_TOZERO_INV
)
# (5) OTSU自动阈值(大津算法)
# 自动计算最佳阈值(此时thresh参数无效)
ret_5, image_np_OSTU = cv2.threshold(
image_np_gray,
0, # 当使用OTSU时,此参数会被忽略
maxval,
cv2.THRESH_BINARY + cv2.THRESH_OTSU # 组合使用二值化和OTSU
)
# ------------------- 可视化结果 -------------------
# 2行3列布局显示对比结果
# 第一行
plt.subplot(2, 3, 1) # 位置1(左上角)
show_image(image_np, '原图')
plt.subplot(2, 3, 2) # 位置2(第一行中间)
show_image(image_np_INV, '反二值化')
plt.subplot(2, 3, 3) # 位置3(第一行右边)
show_image(image_np_TRUNC, '截断阈值化')
# 第二行
plt.subplot(2, 3, 4) # 位置4(第二行左边)
show_image(image_np_TOZERO, '阈值化为零')
plt.subplot(2, 3, 5) # 位置5(第二行中间)
show_image(image_np_TOZERO_INV, '反阈值化为零')
plt.subplot(2, 3, 6) # 位置6(第二行右边)
show_image(image_np_OSTU, 'OTSU自动阈值')
plt.tight_layout() # 自动调整子图间距
plt.show() # 显示所有图像
7.6 OTSU阈值法
以上的二值化方法都需要手动设置阈值,但是在不同的环境下,摄像头拍摄的图像可能存在差异,导致手动设置的阈值并不适用于所有图像,这可能会使二值化的效果不理想。因此需要一种自动计算图片阈值的二值化方法,可以提高图像处理的准确性和鲁棒性。
使用OTSU阈值法可以找到一张图片的阈值,需要注意的是,这种方法仅用于找到阈值,并不能二值化,是在二值化之前增加的算法。
双峰图片就是指灰度图的直方图上有两个峰值,直方图是对灰度图的每个像素值点的个数的统计图(横轴是灰度从0到255,纵轴是像素点的频数)
OTSU算法就是通过一个值把图片分为前景色和背景色,通过统计学的方法来验证该值的合理性:最大类间方差。
根据上述计算的结果套用OTSU算法公式:
这个点在直方图上基本上就是双峰之间的谷底。
通过OTSU算法得到阈值T之后,就可以结合之前的任意一种二值化方法进行操作。
总结
图像处理本质是对像素矩阵的数学操作,掌握RGB通道特性与灰度转换原理是基础,二值化作为特征提取的关键步骤需灵活选择阈值策略,OpenCV库的高效封装简化了复杂算法实现,建议重点理解颜色空间转换逻辑、阈值分割原理及OTSU算法思想,通过代码复现强化三维矩阵操作能力,为后续边缘检测、目标识别等高级任务奠定基础。