opencv学习笔记1:图像基础、图像操作、直方图均衡化详解
目录
一.图像基础理论知识
1.灰度图像
2.像素、颜色、通道、图像和颜色空间
3.OpenCV中的坐标系
4.OpenCV中的通道顺序
二.图像操作
1.opencv读图像、显示图像操作
(1)打印当前路径
(2)imread() 函数读图像
(2)imshow函数用于在窗口中显示图像
(3)waitkey等待窗口击键
2.彩色图像访问和操作OpenCV中的像素
(1)读取像素值
(2)修改像素值
①修改单个像素
②修改区域像素
(3)获取图像属性 shape、size、dtype
3.灰度图像访问和操作OpenCV中的像素
三.直方图均衡化
1.图像的直方图是什么?
详细解释
2.什么是直方图均衡化?
3.直方图均衡化的作用
4.直方图均衡化的原理
5.直方图均衡化的python代码实现
详解:
(1)定义函数
(2)统计像素值的个数
(3)计算累积分布函数(使用前缀和算法优化)
(4)计算映射关系,建立查找表
(5)查表修改原像素值
6.完整、简洁的直方图均衡化代码
一.图像基础理论知识
1.灰度图像
灰度图像是指图像中每个像素点的颜色仅用亮度值表示,没有色彩信息,通常用0(黑色)到255(白色)的数值范围来量化。
(下面部分资料原作者为盼小辉丶,本人仅用于自学并加入相应注释,方便零基础学习)
2.像素、颜色、通道、图像和颜色空间
在表示图像时,有多种不同的颜色模型,但最常见的是红、绿、蓝 (RGB) 模型。
RGB
模型是一种加法颜色模型,其中原色 (在RGB模型中,原色是红色 R、绿色 G 和蓝色 B) 混合在一起就可以用来表示广泛的颜色范围。
每个原色 (R, G, B) 通常表示一个通道,其取值范围为[0, 255]内的整数值。因此,每个通道有共256个可能的离散值,其对应于用于表示颜色通道值的总比特数 ( 2 8 = 256 2^8=256 28=256)。此外,由于有三个不同的通道,使用 RGB 模型表示的图像称为24位色深图像:
在上图中,可以看到 RGB
颜色空间的“加法颜色”属性:
- 红色加绿色会得到黄色
- 蓝色加红色会得到品红
- 蓝色加绿色会得到青色
- 三种原色加在一起得到白色
因此,如前所述,RGB
颜色模型中,特定颜色可以由红、绿和蓝值分量合成表示,将像素值表示为 RGB
三元组 (r, g, b)
。典型的 RGB
颜色选择器如下图所示:
分辨率为 800×1200 的图像是一个包含800列和1200行的网格,每个网格就是称为一个像素,因此其中包含 800×1200=96 万像素。应当注意,图像中有多少像素并不表示其物理尺寸(一个像素不等于一毫米)。相反,像素的大小取决于为该图像设置的每英寸像素数 (Pixels Per Inch, PPI)
。图像的 PPI
一般设置在 [200-400]
范围内。
计算PPI的基本公式如下:
- PPI=宽度(像素) / 图像宽度(英寸)
- PPI=高度(像素) / 图像高度(英寸)
例如,一个4×6英寸图像,图像分辨率为 800×1200,则PPI是200。
彩色图像也可以用同样的方式表示,只是我们需要定义三个函数来分别表示红色、绿色和蓝色值。这三个单独的函数中的每一个都遵循与为灰度图像定义的 f ( x , y )函数相同的公式。我们将这三个函数的子索引 R、G 和 B 分别表示为 fR ( x , y )、fG(x, y)、fB(x,y)。
同样,黑白图像也可以表示为相同的形式,其仅需要一个函数来表示图像,且 f ( x , y )只能取两个值。通常,0 表示黑色、1 表示白色。
———————————————————————————————————————————
解释:①“离散” 体现的是数值变化是不连续的。就好比楼梯,你只能踩在一级一级的台阶上,而不能停留在两级台阶之间。在 8 位的红色通道中,数值只能以 1 为单位递增或者递减,像 241.5 这种中间值是不存在的。
②灰度图:每个像素只有黑白色的函数值 f(x,y),数值为0~255,代表黑白色的亮度
彩色图:每个像素有三个颜色的函数值 fR ( x , y )、fG(x, y)、fB(x,y),数值为0~255
黑白图:每个像素只有黑白色的函数值 f(x,y),数值为0或1,非黑即白。
———————————————————————————————————————————
下图显示了三种不同类型的图像(彩色图像、灰度图像和黑白图像):
3.OpenCV中的坐标系
为了更好的展示 OpenCV 中的坐标系以及如何访问各个像素,查看以下低分辨率图像为例:
这个图片的尺寸是 32×41 像素,也就是说,这个图像有 1312 个像素。为了进一步说明,我们可以在每个轴上添加像素计数,如下图所示:
现在,我们来看看 ( x , y ) (x, y) (x,y) 形式的像素索引。请注意,像素索引起始值为零,这意味着左上角位于 ( 0 , 0 ) (0, 0) (0,0),而不是 ( 1 , 1 ) (1, 1) (1,1)。下面的图像,索引了 4 个单独的像素,图像的左上角是原点的坐标:
单个像素的信息可以从图像中提取,方法与 Python 中引用数组的单个元素相同。
4.OpenCV中的通道顺序
在 OpenCV 使用中,使用的颜色通道顺序为 BGR 颜色格式而不是 RGB 格式。可以在下图中看到三个通道的顺序:
BGR 图像的像素结构如下图所示,作为演示,图示详细说明了如何访问pixel(y=n, x=1):
Tips:OpenCV 的最初开发人员选择了 BGR 颜色格式(而不是 RGB 格式),是因为当时 BGR 颜色格式在软件供应商和相机制造商中非常流行,因此选择 BGR 是出于历史原因。
此外,也有其他 Python 包使用的是 RGB 颜色格式(例如,Matplotlib 使用 RGB 颜色格式,Matplotlib 是最流行的 2D Python 绘图库,提供多种绘图方法,可以查看 Python-Matplotlib 可视化获取更多详细信息)。因此,我们需要知道如何将图像从一种格式转换为另一种格式。
当我们掌握了将图像从一种格式转换为另一种格式的方法后,就可以选择使用 OpenCV
进行图像处理,同时利用 Matplotlib
包提供的函数来显示图像,接下来,让我们看看如何处理两个库采用的不同颜色格式。
二.图像操作
1.opencv读图像、显示图像操作
官网历程
import cv2 as cv # 导入OpenCV库并简称为cv,直接import cv2也是可以的
import sys # 导入sys模块用于程序退出# 读取指定路径的图像文件
img = cv.imread("headshot.jpg")# 检查图像是否成功读取
if img is None:sys.exit("Could not read the image.") # 读取失败时退出程序并显示错误信息# 创建窗口并显示图像
cv.imshow("Display window", img)# 等待用户按键(0表示无限等待)
k = cv.waitKey(0)# 根据用户按键执行不同操作
if k == ord("s"): # 如果按下的是s键cv.imwrite("headshot.png", img) # 将图像保存为PNG格式
(1)打印当前路径
导入os
模块,os
模块提供了与操作系统进行交互的功能,包括文件和目录操作、进程管理、环境变量访问等。通过导入 os
模块,你可以在代码中使用该模块提供的各种函数和属性。
import os
print(os.getcwd())
输出:Y:\pycharm\pythonProject
便于imread填入相对路径
(2)imread()
函数读图像
首先,我们使用 cv.imread()
函数加载图像:
img = cv.imread("headshot.jpg")
imread()
函数:
①第一个参数:指定文件路径加载图像(该路径可以是绝对路径,也可以是相对路径。需要注意的是,如果指定路径的文件不存在,函数不会抛出异常,而是会返回None
)。
如果是相对路径,必须用双反斜杠、路径中不能带中文,例如:Y:\\pycharm\\lion.png
②第二个参数是可选的,它指定了我们想要图像的格式。这可能是:
cv.IMREAD_COLOR
(默认值,也可用1
表示 ):以彩色模式读取图像,会忽略图像的 alpha 通道(透明度通道 ),读取后的图像是 3 通道(BGR 顺序 )。cv.IMREAD_GRAYSCALE
(可用0
表示 ):以灰度模式读取图像,读取后图像为单通道,每个像素用 0 - 255 的灰度值表示。cv.IMREAD_UNCHANGED
(可用-1
表示 ):读取完整图像,包括 alpha 通道(若图像有该通道 ),适合读取带透明度信息的图像(如 PNG 格式含透明背景的图 )。
(2)imshow
函数用于在窗口中显示图像
cv2.imshow(winname, mat)
第一个参数是窗口的标题,第二个参数是将要显示的cv::Mat对象。
imshow
需要配合cv.waitKey()
使用才能正常显示窗口。
(3)waitkey等待窗口击键
cv.imshow("Display window", img)
k = cv.waitKey(0)
因为我们希望我们的窗口一直显示到用户按下一个键(否则程序会结束得太快),所以我们使用 cv::waitKey 函数,它的唯一参数是它应该等待用户输入多长时间(以毫秒为单位)。零意味着永远等待。返回值是按下的键。
2.彩色图像访问和操作OpenCV中的像素
现在,我们来看看如何在 OpenCV 中处理BGR图像。如上所述,OpenCV 加载彩色图像时,蓝色通道是第一个,绿色通道是第二个,红色通道是第三个。
(1)读取像素值
在 OpenCV(通过 cv2
库操作)中,图像数组的索引顺序是 [行(row), 列(column)]
,对应到图像坐标系里,行对应 y 轴(纵向)、列对应 x 轴(横向)
import cv2
img=cv2.imread('headshot.jpg')
cv2.imshow('headshot',img)
cv2.waitKey(0)(b,g,r)=img[6,40] #获取 (x=40,y=6)像素值
print(b,g,r)
b1=img[6,40,0] #获取 (x=40,y=6)蓝色像素值
g1=img[6,40,1] #获取 (x=40,y=6)绿色像素值
r1=img[6,40,2] #获取 (x=40,y=6)红色像素值
print(b1,g1,r1)cv2.destroyAllWindows() #关闭并释放所有窗口
输出:220 185 145
220 185 145
如果是灰度图就返回单个像素值
(2)修改像素值
①修改单个像素
像素值也可以以相同的方式进行修改。例如,要将像素 (x=40, y=6) 处设置为红色:
img[6, 40] = (0, 0, 255)
②修改区域像素
有时,需要处理某个区域而不是一个像素。在这种情况下,应该提供值的范围(也称切片),而不是单个值。例如,要获取图像的左上角:
top_left_corner = img[0:50, 0:50]
变量 top_left_corner
可以看做是另一个图像(比img小),但是我们可以用同样的方法处理它。
修改区域像素:
import cv2
img=cv2.imread('headshot.jpg')img[0:50, 0:50] =(0,0,255) #修改区域像素为红色cv2.imshow('headshot',img)
cv2.waitKey(0)
cv2.destroyAllWindows()
(3)获取图像属性 shape、size、dtype
图像加载到 img
后,可以获得图像的一些属性。我们要从加载的图像中提取的第一个属性是 shape
,它将告诉我们行、列和通道的数量(如果图像是彩色的)。我们将此信息存储在 dimensions
变量中:
dimensions = img.shape
第二个属性是图像的大小(img.size=图像高度 × 图像宽度 × 图像通道数):
total_number_of_elements= img.size
第三个属性是图像数据类型,可以通过 img.dtype
获得。因为像素值在 [0-255] 范围内,所以图像数据类型是 uint8 (unsigned char):
image_dtype = img.dtype
总代码:
import cv2
img=cv2.imread('headshot.jpg')dimension=img.shape
total_number_of_elements= img.size
image_dtype = img.dtypeprint(dimension,total_number_of_elements,image_dtype)
输出:(1080, 1080, 3) 3499200 uint8
3.灰度图像访问和操作OpenCV中的像素
总代码(下面一点一点讲解):
import cv2
gray_img=cv2.imread('headshot.jpg',cv2.IMREAD_GRAYSCALE)dimension=gray_img.shape #获取图像尺寸
print(dimension)i=gray_img[6,40] #获取像素值,只有一个值:白色强度
print(i)gray_img[0:50,0:50]=0 #修改左上角区域为黑色cv2.imshow('gray_headshot',gray_img)
cv2.waitKey(0)
输出:(1080, 1080)
177
灰度图像只有一个通道。因此,在处理这些图像时会引入一些差异。我们将在这里重点介绍这些差异,相同的部分不再赘述。
同样,我们将使用 cv2.imread() 函数来读取图像。在这种情况下,需要第二个参数,因为我们希望以灰度加载图像。第二个参数是一个标志位,指定读取图像的方式。以灰度加载图像所需的值是 cv2.IMREAD_grayscale:
gray_img = cv2.imread('headshot.png', cv2.IMREAD_GRAYSCALE)
在这种情况下,我们将图像存储在gray_img变量中。如果我们打印图像的尺寸(使用 gray_img.shape ),只能得到两个值,即行和列。在灰度图像中,不提供通道信息:
dimensions = gray_img.shape
shape将以元组形式返回图像的维度 ——(1080, 1080)。
像素值可以通过行和列坐标来访问。在灰度图像中,只获得一个值(通常称为像素的强度)。例如,如果我们想得到像素 ( x = 40 , y = 6 ) (x=40, y=6) (x=40,y=6) 处的像素强度:
i = gray_img[6, 40]
图像的像素值也可以以相同的方式修改。例如,如果要将像素 ( x = 40 , y = 6 ) (x=40, y=6) (x=40,y=6) 处的值更改为黑色(强度等于0):
gray_img[6, 40] = 0
三.直方图均衡化
1.图像的直方图是什么?
图像直方图,是指对整个图像在灰度范围内的像素值 (0~255) 统计出现频率次数,据此生成的直方图,称为图像直方图。直方图反映了图像灰度的分布情况。是图像的统计学特征。
简单来说:直方图是图像中像素强度分布的图形表达方式,它统计了每一个强度值所具有的像素个数。
例如下面这张图片,左图为灰度图,右图统计了这张图的所有像素值(0~255)对应的像素个数
详细解释
更形象的来说,将下面像素格子对等为如上图的图像
假设有该图像数据 8x8,像素值范围 0~14 共 15 个灰度等级,统计得到各个等级出现次数及直方图如下图所示:
则对上面抽象出来的图像(像素格子)进行像素与出现次数的统计得到下图左侧的表格,做出频率图如右图所示:
2.什么是直方图均衡化?
是一种提高图像对比度的方法,拉伸图像灰度值范围。
简单来说, 以上面狗狗的的直方图为例, 你可以看到像素主要集中在中间的一些强度值上。直方图均衡化要做的就是 拉伸 这个范围。就是下面蓝框框出来的范围就是像素主要几种区间。
见下图:绿圈 圈出了 像素分布率较低像素值,对其应用均衡化后(将中间蓝框像素分布较高的区间拉伸), 得到了中间图所示的直方图。均衡化的图像见下面右图.
3.直方图均衡化的作用
因为直方图均衡化处理之后,原来比较少像素的灰度会被分配到别的灰度去,像素相对集中, 处理后灰度范围变大,对比度变大,清晰度变大,所以能有效增强图像。
直方图均衡化是图像处理领域中利用图像直方图对对比度进行调整的方法。这种方法通常用来增加许多图像的局部对比度,尤其是当图像的有用数据的对比度相当接近的时候。通过这种方法,亮度可以更好地在直方图上分布。这样就可以用于增强局部的对比度而不影响整体的对比度,直方图均衡化通过有效地扩展常用的亮度来实现这种功能。
总的来说,直方图均衡化是用来增强对比度的
4.直方图均衡化的原理
直方图均衡化其实会用到一些简单的概率论知识。我尽量以比较通俗的方法来叙述,所以其中的一些表述可能不是很严谨,不过仅用于理解本例是足够了。
ps:以下仅为理论部分的叙述,实际代码实现时还会有一些不同的地方,但本质是一样的,只不过先熟悉原理会更好理解。
首先第一步,肯定就是要统计出所有像素值分别有多少个,这个其实就是一个图像遍历的过程。做完上一步后,我们需要求取每一个像素值的概率密度函数 PDF ,什么是概率密度函数呢?概率密度函数就是图像中像素值为x的点的个数对于图像像素点总数的比重。因为这里是离散的情况,所以非常好求。假设像素值为k的点在图像(假设图像总共有MN个像素)中有r个,那么该点的概率密度函数,或者说这些点所占据的个数比重就是
之后还需要介绍一下累积分布函数(CDF)的概念,这个概念跟PDF对照起来就很好理解。
PDF是针对一个特定的点计算的,也就是说是一对一的关系。
而CDF是指(在本例中),假如一个像素值x,求它的CDF,就是所有像素值≤x的点的PDF之和。
即:
=所有 像素值小于等于x 的点 的PDF之和
举一个例子,假设我现在要求像素值为5的PDF,即,那么结果就是像素值为5的点在所有点中占据的比重。
而如果现在我要求 ,根据上面的定义,结果就是所有像素值≤5的PDF之和。由于这里是离散的情况,且像素值的最低边界是0,所以这里计算起来并没有很复杂,无非就是
(计算公式1)
不难知道,CDF是非递减的,也就是说,
(计算公式2、个人推导)计算公式也可以是:假设像素值为k的点在图像(假设图像总共有MN个像素)中有
个
(计算公式3、个人推导)类似于递归的公式
得到CDF以后,就可以引出最核心的步骤了,也就是直方图均衡化的核心公式:
- 其中,L表示图像的像素等级数,对于256级的灰度图L就是256。
是映射的值,表示的是原图中像素值为x的点需要映射到这个值,其实就是一种变化关系。这个公式跟概率论有关,如果不理解的话直接记忆即可。
利用这个公式,将原图中的每个像素点进行映射输出以后,就得到了直方图均衡化的结果。可以发现,直方图均衡化其实是一种线性变化,所以这种方法也被叫做直方图线性变化。
5.直方图均衡化的python代码实现
上面说到,代码实现跟理论是有一些差别的。直接给出直方图均衡化函数代码
def equalize_self(src_img): """ param src_img: 待处理图像 return: 直方图均衡化后的新图像 """ height, width = src_img.shape[:2] # r_dict列表统计每个值像素的个数 统计r_k r_dict = [0 for i in range(256)] for r in range(height): for c in range(width): r_dict[src_img[r, c]] += 1 # 计算累积分布函数(使用前缀和算法优化) cdf = [0 for i in range(256)] cdf[0] = r_dict[0] for i in range(1, 256): #cdf x 为所有 像素值≤x 的点的PDF之和cdf[i] = cdf[i - 1] + r_dict[i] #求像素值x的cdf=前一个cdf值+x处的pdf值# 计算映射关系,建立查找表。 即:把256个像素的映射值依次加入列表中s = [] for i in range(256): # 累积分布函数的最小值不一定是0,直接采取书上的公式会导致图片偏灰 # 对于每一个cdf,计算时将其减去cdf_min使最小的像素映射到0,得到的图片效果更接近库方法 s.append((cdf[i] - cdf[0]) * 255.0 / (height * width - cdf[0])) # 查表修改原像素值 dst_img = np.zeros((height, width), dtype=np.uint8) for r in range(height): for c in range(width): dst_img[r, c] = round(s[src_img[r, c]]) return dst_img
详解:
(1)定义函数
传入的参数 src_img 为待处理图像,return返回 直方图均衡化后的新图像
def equalize_self(src_img):
(2)统计像素值的个数
- height, width = src_img.shape[:2] ——得到图像src_img的高和宽,2的意思为shape函数的前两个值 即:高和宽,不包含通道数。虽然对于灰度图shape默认没有第三个通道数,但是此处有可能传入彩色图像。虽然原函数
equalize_self
直接传入彩色图会因后续逻辑(如r_dict
索引)报错,但shape[:2]
的存在依然有意义。
height, width = src_img.shape[:2] # r_dict列表统计每个值像素的个数 r_dict = [0 for i in range(256)] for r in range(height): for c in range(width): r_dict[src_img[r, c]] += 1
- r_dict = [0 for i in range(256)] ——初始化 r_dict 列表,把256个元素全初始化为0,用于每个像素值的个数
- 后面的遍历就是统计src_img原图的各个像素值的数量
(3)计算累积分布函数(使用前缀和算法优化)
初始化 cdf 列表,把256个元素全初始化为0,根据公式
公式2::
公式3:
此处是用公式3递归相加了像素个数,只是还没有除MN总像素个数
# 计算累积分布函数(使用前缀和算法优化) cdf = [0 for i in range(256)] cdf[0] = r_dict[0] for i in range(1, 256): #cdf x 为所有 像素值≤x 的点的PDF之和cdf[i] = cdf[i - 1] + r_dict[i] #求像素值x的cdf=前一个cdf值+x处的pdf值
(4)计算映射关系,建立查找表
先建立一个s空列表作为映射关系查找表,遍历把256个像素的映射值依次加入列表中
# 计算映射关系,建立查找表。 即:把256个像素的映射值依次加入列表中s = [] for i in range(256): # 累积分布函数的最小值不一定是0,直接采取书上的公式会导致图片偏灰 # 对于每一个cdf,计算时将其减去cdf_min使最小的像素映射到0,得到的图片效果更接近库方法 s.append((cdf[i] - cdf[0]) * 255.0 / (height * width - cdf[0]))
s.append((cdf[i] - cdf[0]) * 255.0 / (height * width - cdf[0])) 解释:
① append内的参数其实就是上面所给的直方图均衡化的映射公式。
② L-1=256-1=255
③ cdf[i]-cdf[0]:有趣的是,会发现这里的cdf[i]在参与运算时,还减去了cdf[0],这是为什么呢?其实,这是为了正确调整归一化的比例范围,确保映射结果的动态范围合理。
实际图像中的像素值并不总是从 0 开始,图像的最小像素值可能不是 0。因此,累积分布函数的最小值也不一定是 0。
如果不减去,则累积分布函数 CDF 中的最小值不会映射为 0,而是映射为一个正值,导致图像中没有真正的黑色区域,会让图像看起来灰蒙蒙的,缺少足够的对比度。因此,通过减去
,我们可以将图像中的最小灰度值映射为 0,从而增强对比度,最大化使用图像的动态范围。
④ (height * width - cdf[0])) :分母是像素总数MN-图像的最小像素值。这其实是一个运算简化的过程。在前面计算CDF时,我并没有先计算PDF,这是因为PDF都会除以一个公共项,那就是图像的像素总数。因此我们可以先算分子的部分,最后只进行一次除法就可以了。不过由于上述我们减去 ,累积分布函数的起点变成 0,但其终点也相应地缩小了。因此,整个映射范围也需要缩小,以确保新的 CDF 被正确地归一化到 0 和 255 之间,即像素总数也要减去
。还有一个值得提的点,计算CDF时,可以使用前缀和算法优化,这样可以帮我们省去很多的重复计算,关于前缀和算法可以自行查阅相关资料了解。
(5)查表修改原像素值
# 查表修改原像素值 dst_img = np.zeros((height, width), dtype=np.uint8) for r in range(height): for c in range(width): dst_img[r, c] = round(s[src_img[r, c]]) return dst_img
(1)dst_img = np.zeros((height, width), dtype=np.uint8) 解释:np.zeros 是 NumPy 库中用于创建全零数组的函数。
-
参数1:(height, width)
根据原图像的高度和宽度创建数组,确保新图像与原图尺寸一致。 -
参数2:dtype=np.uint8
明确数组元素为uint8
类型,这是存储图像像素值的标准类型(0-255)。
(2)dst_img[r, c] = round(s[src_img[r, c]])
从[0,0]逐步遍历原图像src_img的每一个像素,从 s映射关系查找表 中找到每一个像素对应的映射值,round函数作用是四舍五入(append处为浮点值计算,可能产生小数),最后依次赋值给dst_img,得到直方图均衡化后的新图像。
6.完整、简洁的直方图均衡化代码
import cv2
import numpy as npdef equalize_self(src_img):# 统计像素个数height, width = src_img.shape[:2]r_dict = [0 for i in range(256)]for i in range(height):for j in range(width):r_dict[src_img[i,j]]+=1# 累积分布函数cdf = [0 for i in range(256)]cdf[0] = r_dict[0]for i in range(1,256):cdf[i]=cdf[i-1]+r_dict[i]#映射s=[]for i in range(256):s.append((cdf[i]-cdf[0])*255.0/(height*width-cdf[0]))#赋值新图dst_img=np.zeros((height,width), dtype=np.uint8)#print(type(dst_img[1][1])) 测试类型,可以不写for i in range(height):for j in range(width):dst_img[i,j]=round(s[src_img[i,j]])return dst_imgimg=cv2.imread("Y:\\pycharm\\lion.png",cv2.IMREAD_GRAYSCALE)
img2=equalize_self(img)cv2.imshow("lion_picture",img2)
cv2.waitKey(0)