为后续项目学习做准备,我们需要了解LinuxOpenCV、Mediapipe、ROS、QT等知识。
一、图像显示与保存
1、基本原理
1.1 图像像素存储形式
首先得了解下图像在计算机中存储形式:(为了方便画图,每列像素值都写一样了)。对于只有黑白颜色的灰度图,为单通道,一个像素块对应矩阵中一个数字,数值为0到255, 其中0表示最暗(黑色) ,255表示最亮(白色)
对于采用RGB模式的彩色图片,为三通道图,Red、Green、Blue三原色,按不同比例相加,一个像素块对应矩阵中的一个向量, 如[24,180, 50],分别表示三种颜色的比列, 即对应深度上的数字,如下图所示:
需要注意的是,由于历史遗留问题,opencv采用BGR模式,而不是RGB
2、API说明
API名: cv2.destroyAllWindows( ) | 删除所有我们建立的窗口 |
API名: cv2.destroyWindow(指定窗口名) | 删除特定的窗口可以使用,在括号内输入你想删 除的窗口名。 |
API名: cv2.imshow('窗口名', 图像变量): | 用“窗口名”显示图像 |
API名:cv2.resizeWindow(‘窗口名’, 宽, 高) | 调整窗口大小 |
API名:cv2.imwrite('图像文件地址',图像变量) | 将图像变量保存到指定位置的指定文件中 |
API名:cv.namedWindow(winname, flags=None) | |
参数:winname | 窗口名字 |
flags窗口标志 | |
标志参数 | 作用 |
WINDOW_NORMAL | 显示图像后,允许用户随意调整窗口大小 |
WINDOW_AUTOSIZE | 根据图像大小显示窗口,不允许用户调整大小 |
WINDOW_FREERATIO | 窗口大小自适应比例 |
WINDOW_KEEPRATIO | 保持图像的比例 |
1、具体代码展示标志效果 1.1 窗口大小可变 cv.namedWindow("show Image",cv.) 或者 cv.namedWindow("show Image",cv.WINDOW_GUI_NORMAL) 或者 cv.namedWindow("show Image",0) 此时的图片是可以自由拉伸改变大小的 1.2 窗口大小不可变,自动适应图片大小(默认) cv.namedWindow("show Image",cv.WINDOW_AUTOSIZE) 或者 cv.namedWindow("show Image",1) 1.3 窗口大小自适应比例 cv.namedWindow("show Image",cv.WINDOW_FREERATIO) 1.4 窗口大小跟随图片保持其比例 cv.namedWindow("show Image",cv.WINDOW_KEEPRATIO) | |
说明:如果在imshow()之前加上namedWindow()方法来显示一张图片的话,该窗口显示的图片是可交互的。 1、这里使用imshow()和namedWindow()方法时候窗口的标识名称(传递的第一个参数)要一样。 2、namedWindow()方法要写在imshow()方法之前才可以。 | |
API名:cv.namedWindow(winname, flags=None) | |
参数:winname | 窗口名字 |
flags窗口标志 | |
标志参数 | 作用 |
WINDOW_NORMAL | 显示图像后,允许用户随意调整窗口大小 |
WINDOW_AUTOSIZE | 根据图像大小显示窗口,不允许用户调整大小 |
WINDOW_FREERATIO | 窗口大小自适应比例 |
WINDOW_KEEPRATIO | 保持图像的比例 |
1、具体代码展示标志效果 1.1 窗口大小可变 cv.namedWindow("show Image",cv.) 或者 cv.namedWindow("show Image",cv.WINDOW_GUI_NORMAL) 或者 cv.namedWindow("show Image",0) 此时的图片是可以自由拉伸改变大小的 1.2 窗口大小不可变,自动适应图片大小(默认) cv.namedWindow("show Image",cv.WINDOW_AUTOSIZE) 或者 cv.namedWindow("show Image",1) 1.3 窗口大小自适应比例 cv.namedWindow("show Image",cv.WINDOW_FREERATIO) 1.4 窗口大小跟随图片保持其比例 cv.namedWindow("show Image",cv.WINDOW_KEEPRATIO) | |
说明:如果在imshow()之前加上namedWindow()方法来显示一张图片的话,该窗口显示的图片是可交互的。 1、这里使用imshow()和namedWindow()方法时候窗口的标识名称(传递的第一个参数)要一样。 2、namedWindow()方法要写在imshow()方法之前才可以。 |
三、代码示例
使用 Matplotlib显示图片 |
import cv2 from matplotlib import pyplot as plt
if __name__ == "__main__": img = cv2.imread('1.jpg',0) plt.imshow(img, cmap = 'gray', interpolation = 'bicubic') plt.xticks([]), plt.yticks([]) # to hide tick values on X and Y axis plt.show() |
使用 Matplotlib显示图片并进行一些操作 |
import cv2 from matplotlib import pyplot as plt
if __name__ == "__main__": img = cv2.imread('2.jpg') edges = cv2.Canny(img,100,200)
plt.subplot(121),plt.imshow(img,cmap = 'gray') plt.title('Original Image'), plt.xticks([]), plt.yticks([]) plt.subplot(122), plt.imshow(edges,cmap = 'gray') plt.title('Edge Image'), plt.xticks([]), plt.yticks([]) plt.show() |
说明:plt.imshow()函数负责对图像进行处理,并显示其格式,而plt.show()则是将plt.imshow()处理后的函数显示出来。 |
二、图像处理基础
1.1、获取图像属性
所谓获取图像属性,实际上就说当我们把一幅图像读入内存后,这幅图像就如同一个三维或二维数组,那既然是个numpy数组,必然可以获取它的shape、它的size以及它的dtype。乃至于,你可以像操作numpy数组那样,在图像上做诸如求平均值、方差之类的各种数学操作。因此,实际上获取图像的属性操作也很简单,比如下面这个例子就分别展示了如何获得图像的shape(指示是三维还是二维图像,或者说彩色图还是灰度图)、size(图像中像素的总数,依据这个总数以及每个像素的dtype就可以获得这幅图像当前总共占据多少内存存储空间)、dtype(每个像素值的数据类型:float32还是int等等):
img = cv2.imread('test.jpg')
获取图像的属性 | |
img.shape | 可以获取图像的形状。他的返回值是:一个包含(行数(高),列数(宽), 通道数)的元组。 |
img.size | 返回图像的像素数目 |
img.dtype | 返回返回图像的数据类型 |
API名:cv2.imwrite('图像文件地址',图像变量) | 将图像变量保存到指定位置的指定文件中 |
先查看原始图像的shape、size、dtype:(675, 1200, 3) 2430000 uint8
再查看颜色模式为HSV后的图像的shape、size、dtype:(675, 1200, 3) 2430000 uint8
最后查看变成灰度图后图像的shape、size、dtype:(675, 1200) 810000 uint8
从结果来看,当彩色图变成灰度图之后,通道数没有了,意味着图像的像素总数也会大幅减少。
1.2 图像像素更改
图像像素的更改行为,往往在机器视觉中是最最简单的一种行为,其操作起来也如同我们更改一个list或者一个numpy数组中的元素那样简单。要知道,任何一幅图像实际上读到内存之后就是一个numpy数组,因此,图像像素的修改就如同我们在修改一个n维数组中的某个元素那样简单。所不同的是,修改图像像素,我们需要指定图像像素的坐标:
如果是RGB格式的,坐标就分别是R、G、B三个通道的值来共同定位一个像素;如果是HSV或别的什么颜色模式下的图像,也如同像RGB那样指定出三维坐标系各个坐标轴上的坐标值即可。
如果是一个灰度图,则如同一个平面坐标系那般简单,只需要指定横纵坐标的值即可圈定一个像素。
当然,通常情况下,我们只需要给出像素的行和列坐标值即可获得像素内的值。此时,对于RGB或HSV等三维颜色格式的图像来说,返回的是图像的三维坐标的值,比如RGB就是返回R、G、B;HSV的就是返回H、S、V;对于灰度图来说则是返回的是灰度值。
同时,给定像素的行、列坐标值就可以依据像操作numpy二维数组那样来给指定的像素点修改像素值。
获取像素值并修改 参数含义:y坐标x:x坐标 bgr:BGR通道,0:B通道,1:G通道,2:R通道 | |
获取像素值 img[y,x] | 获取(x,y)处的通道值,返回列表
# 获取像素值px = img[100, 200] |
img[y,x,bgr] | 获取(x,y)处一个通道值 获取像素中的B通道颜色值blue = img[100, 200, 0] |
修改像素值 img[y,x]=[b,g,r] | 将一个颜色赋值给一个像素点 # 修改像素值img[100, 200] = [0, 123, 155] |
比如下面这段小代码就很好地解释了如何获取不同颜色格式下的像素值内容:
import numpy as npimport cv2#读入原始图片,并将其进行颜色空间转换为HSV和灰度图#再分别读出相同的行、列坐标值下的像素值以加深对像素值更操作的理解
import numpy as npimport cv2#读入原始图片,并将其进行颜色空间转换为HSV和灰度图#再分别读出相同的行、列坐标值下的像素值以加深对像素值更操作的理解
img = cv2.imread(r'E:\tmp\maerdaifu.jpg',cv2.IMREAD_UNCHANGED)
hsv = cv2.cvtColor(img,cv2.COLOR_BGR2HSV)
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
print('以下为不同颜色模式下获取像素值的输出结果')
print(img[100,100])
print(hsv[100,100])
print(gray[100,100])
img[100,100]=[192, 212, 222]
hsv[100,100]=[198, 218, 228]
gray[100,100]=255print('以下为不同颜色模式下修改像素值的输出结果')
print(img[100,100])
print(hsv[100,100])
print(gray[100,100])
以下为不同颜色模式下获取像素值的输出结果
[217 121 55][108 190 217]
以下为不同颜色模式下修改像素值的输出结果[192 212 222][198 218 228]
1.3 获取图像的感兴趣区域(ROI)
图像的感兴趣区域(ROI:region of interest),是指在机器视觉、图像处理中,从被处理的图像以方框、圆、椭圆、不规则多边形等方式勾勒出需要处理的区域,称为感兴趣区域。在图像处理领域,感兴趣区域是从图像中选择的一个图像区域,这个区域是你的图像分析所关注的重点。圈定该区域以便进行进一步处理。使用ROI圈定你想读的目标,可以减少处理时间,增加精度。感兴趣区是图像的一部分,它通过在图像上选择或使用诸如设定阈值(thresholding) 或者从其他文件(如矢量> 转换获得等方法生成。感趣区可以是点、线、面不规则的形状,通常用来作为图像分类的样本、掩膜、裁剪区或及其他操作。
比如,我想要抠出下面这幅图中的房子:
图1 马尔代夫风景图
那么首先我需要知道图中的房子大约在哪些像素坐标范围之内,也即其行坐标范围和列坐标范围。这里我们需要事先弄清楚感兴趣区域在图像中的坐标范围:即行列坐标的坐标值范围。
确定坐标范围后,我们利用切片操作,像在Numpy数组中获取一定范围内的数据那样,即可获得我们感兴趣的区域内的目标对象——房子。整个获取ROI的代码如下所示:
import cv2
#读入原始图片
img = cv2.imread(r'E:\tmp\maerdaifu.jpg',cv2.IMREAD_UNCHANGED)
#扣除我们感兴趣的房子区域并予以单独显示
house = img[126:353,316:908]
cv2.imshow('HouseTest1', house)
#我们再把抠出来的房子放至原图像的其他区域,再显示操作过后的原始图像img[326:553,500:1092]=house
cv2.imshow('HouseTest2', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
其运行结果如下:
图2 抠出的ROI图像
图3 将ROI图像重新放置回原始图中的运行效果图
从以上两幅图来看,我们大体上知道了如何来获取一个ROI图像。
需要注意的是:图像在内存中的存储格式是一个numpy数组,因此,当你给定行列坐标值范围时,一定要把列坐标范围放置在切片操作的第一个参数值上,行坐标值范围为切片操作的第二个参数。这是大家在实际利用numpy数组开展切片操作或其他需要用到行列坐标参与运算时最最容易犯的错误之一。
1.4 图像通道的拆分、合并操作
当我们需要单独对图像的通道进行操作,比如R、G、B三个通道都有不同的操作时,我们就需要用到通道的拆分。当拆分后的通道被处理结束之后,可能需要将其复原时,又需要用到通道的合并操作。由于我们再三强调过读入一幅图像到内存后,其实质就是读入一个numpy数组,因此,在numpy的基础知识讲解中,如何拆分和合并不同的维度,这里就可以用同样的API来操作,也即利用split()来拆分通道,利用merge()来合并通道,
拆分通道1: b,g,r = cv2.split(m,mv) m: 图像imgmv: 默认为None | # 分离颜色通道 b, g, r = cv2.split(img) cv2.imshow('Blue', b) cv2.imshow('Green', g) cv2.imshow('Red', r) |
拆分通道2: b=img[:,:,bgr] | # 其他方法 b1 = img[:, :, 0] # 仅取B通道 cv2.imshow('Blue-1', b1) b2 = cv2.imread('test.jpg') b2[:, :, 1:3] = 0 # 将0赋值给G、R通道 cv2.imshow('Blue-2', b2) |
合并通道 img = cv2.merge([X,Y,Z]) | mergeImg = cv2.merge([b, g, r]) cv2.imshow('merge', mergeImg ) mergeImg2 = cv2.merge([h, s, v]) cv2.imshow('merge', mergeImg2 ) |
import cv2#读入原始图片
img = cv2.imread(r'E:\tmp\maerdaifu.jpg',cv2.IMREAD_UNCHANGED)
r,g,b = cv2.split(img) #请大家注意一下merge函数的参数给定形式,如果不以list 或tuple方式来给定的话,必然会出错
img = cv2.merge([r,g,b])
cv2.imshow('Test', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
其运行结果如下:
图4 通道拆分合并效果图
可以看出,通道经拆分再合并后得到的图像与原始图像并无二致。
#通道的分离与合并以及某个通道值的修改
import cv2 as cv
src=cv.imread('E:\imageload\example.png')
cv.namedWindow('first_image', cv.WINDOW_AUTOSIZE)
cv.imshow('first_image', src)
#三通道分离形成单通道图片
b, g, r =cv.split(src)
cv.imshow("second_blue", b)
cv.imshow("second_green", g)
cv.imshow("second_red", r)
# 其中cv.imshow("second_red", r)可表示为r = cv2.split(src)[2]
#三个单通道合成一个三通道图片
src = cv.merge([b, g, r])
cv.imshow('changed_image', src)
#修改多通道里的某个通道的值
src[:, :, 2] = 0
cv.imshow('modify_image', src)
cv.waitKey(0)
cv.destroyAllWindows()
5、添加边界(padding)/为图像扩边
cv2.copyMakeBorder(src,top, bottom, left, right,borderType,value) | |
src: 输入图像 • top, bottom, left, right 对应边界的像素数目。 value: 边界颜色,如果borderType为cv2.BORDER_CONSTANT时,传入的边界颜色值,如[0,255,0] | borderType: 要添加那种类型的边界,类型如下 –cv2.BORDER_CONSTANT 添加有颜色的常数值边界,还需要下一个参数(value)。 – cv2.BORDER_REFLECT边界元素的镜像。比如: fedcba|abcdefgh|hgfedcb – cv2.BORDER_REFLECT_101 or cv2.BORDER_DEFAULT 跟上面一样,但稍作改动。例如: gfedcb|abcdefgh|gfedcba – cv2.BORDER_REPLICATE重复最后一个元素。例如: aaaaaa| abcdefgh|hhhhhhh – cv2.BORDER_WRAP 不知道怎么说了, 就像这样: cdefgh| abcdefgh|abcdefg |
import cv2
import numpy as np
from matplotlib import pyplot as plt
BLUE = [255, 0, 0]
img = cv2.imread('test.jpg')
replicate = cv2.copyMakeBorder(img, 30, 30, 30, 30, cv2.BORDER_REPLICATE)
reflect = cv2.copyMakeBorder(img, 30, 30, 30, 30, cv2.BORDER_REFLECT)
reflect101 = cv2.copyMakeBorder(img, 30, 30, 30, 30, cv2.BORDER_REFLECT_101)
wrap = cv2.copyMakeBorder(img, 30, 30, 30, 30, cv2.BORDER_WRAP)
constant = cv2.copyMakeBorder(img, 30, 30, 30, 30, cv2.BORDER_CONSTANT, value=BLUE)
plt.subplot(231),
plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB), 'gray'), plt.title('ORIGINAL')
plt.subplot(232),
plt.imshow(cv2.cvtColor(replicate,cv2.COLOR_BGR2RGB),'gray'), plt.title('REPLICATE')
plt.subplot(233),
plt.imshow(cv2.cvtColor(reflect, cv2.COLOR_BGR2RGB), 'gray'), plt.title('REFLECT')
plt.subplot(234),
plt.imshow(cv2.cvtColor(reflect101, cv2.COLOR_BGR2RGB), 'gray'), plt.title('REFLECT_101')
plt.subplot(235),
plt.imshow(cv2.cvtColor(wrap, cv2.COLOR_BGR2RGB), 'gray'), plt.title('WRAP')
plt.subplot(236),
plt.imshow(cv2.cvtColor(constant, cv2.COLOR_BGR2RGB), 'gray'), plt.title('CONSTANT')
plt.show()
- 使用示例:
import cv2 as cv
import matplotlib.pyplot as plt
img2 = cv.imread(r"C:\Users\Administrator\Desktop\dog.jpg")
img = cv.cvtColor(img2,cv.COLOR_BGR2RGB) #matplotlib的图像为RGB格式
constant = cv.copyMakeBorder(img,20,20,20,20,cv.BORDER_CONSTANT,value=[0,255,0]) #绿色
reflect = cv.copyMakeBorder(img,20,20,20,20,cv.BORDER_REFLECT)
reflect01 = cv.copyMakeBorder(img,20,20,20,20,cv.BORDER_REFLECT_101)
replicate = cv.copyMakeBorder(img,20,20,20,20,cv.BORDER_REPLICATE)
wrap = cv.copyMakeBorder(img,20,20,20,20,cv.BORDER_WRAP)
titles = ["constant","reflect","reflect01","replicate","wrap"]
images = [constant,reflect,reflect01,replicate,wrap]
for i in range(5):
plt.subplot(2,3,i+1),plt.imshow(images[i]),plt.title(titles[i])
plt.xticks([]),plt.yticks([])
plt.show()