python+opencv3.3视频教学基础入门
求知讲堂2020python+人工智能 99天完整版
OpenCV贾志刚学习笔记
Python标准库
OpenCV官方文档
OpenCV中文文档
matplotlib官方文档
python下载链接
注意:勾选add to path,这样可以添加Python到环境变量里
最终要添加的路径:
Pycharm下载链接
注意:全部勾选这样可以添加环境变量
使用教育邮箱可以获得专业版
汉化教程链接
pycharm使用笔记3-自动生成文件注释和函数注释
命令行里执行
pip install opencv-python
可以使用指令
python3 -m pip install --user --upgrade pip
可以参考链接:
python实现opencv学习一:安装、环境配置、工具
命令行里执行
pip install matplotlib
直接cmd用命令行输入:pip install +要安装的第三方库,
比如pip install requests
安装完库之后,可能会存在找不到包路径的情况,可以让Pycharm直接使用Python安装路径下的site-packages
直接用pycharm安装
File-Settings,点击Project: 在Project Interpreter里点击右上角的+来安装
参考链接:
参考链接:
读取一张图片
def imread(filename: Any,flags: Any = None) -> retval
保存一张图片
imwrite(filename, img[, params]) -> retval
创建GUI
def namedWindow(winname: Any,flags: Any = None) -> None
显示图片
def imshow(winname: Any,mat: Any) -> None
在一个给定的时间内(单位ms)等待用户按键触发,0表示无限制等待用户按键触发
waitKey([, delay]) -> retval
在cv2.imshow()之后要跟着一句cv2.waitkey()。
源码注释如下:
This function should be followed by cv::waitKey function which displays the image for specified . milliseconds. Otherwise, it won’t display the image.
This function is the only method in HighGUI that can fetch and handle events, so it needs to be .
这个函数是HighGUI窗口中唯一的获取和处理事件的方法,因此它必须存在。
实际上,waitkey控制着imshow的持续时间,当imshow之后不跟waitkey时,相当于没有给imshow提供时间展示图像,所以只有一个空窗口一闪而过。添加了waitkey后,哪怕仅仅是cv2.waitkey(1),我们也能截取到一帧的图像。所以cv2.imshow后边是必须要跟cv2.waitkey的。
尤其是在显示视频时,如果使用 waitKey(0) 则只会显示第一帧视频。
src = cv.imread("C:/Users/22164/Desktop/zhou.jpg")
gray = cv.cvtColor(src, cv.COLOR_BGR2GRAY)
cv.namedWindow("import image", cv.WINDOW_AUTOSIZE) # 创建GUI
cv.imshow("import image", src)
cv.waitKey()
cv.imwrite("C:/Users/22164/Desktop/zhouCopy.jpg", src) # 保存到桌面
cv.imwrite("C:/Users/22164/Desktop/zhouCopyGray.jpg", gray) # 保存灰度图到桌面
video_demo()
get_image_info(src)
cv.destroyAllWindows()
复制一份图片
注意: 使用图像前可以复制一份
返回图片的高,宽,通道数
视频文件的读取
参数是0,表示打开笔记本的内置摄像头,参数是视频文件路径则打开视频
图像翻转
flip(src, flipCode[, dst]) -> dst
def video_demo():
# capture = cv.VideoCapture(0)
# 要用这个,之前那个有点报错。0是打开摄像头,也可以是输入视频文件的路径
capture = cv2.VideoCapture(0, cv2.CAP_DSHOW)
while 1:
ret, frame = capture.read()
frame = cv.flip(frame, 1) # 图像翻转
cv.imshow("video", frame)
c = cv.waitKey(50)
if c == 27: # 表示键盘输入的是ESC
break
# 计算gaussian_noise的运行时间
t1 = cv.getTickCount()
gaussian_noise(src)
t2 = cv.getTickCount()
time = (t2 - t1)/cv.getTickFrequency()
print("time consume: %s"%(time * 1000)) # 单位ms
返回一个全为0的数组
def zeros(shape: Union[int, Iterable, tuple[int]],
dtype: Optional[object] = None,
order: Optional[str] = 'C',
*args: Any,
**kwargs: Any) -> ndarray
同numpy.zeros
返回一个对角线全为1的数组
def creatArray():
"""创建一个新数组"""
new_array = np.ones([3, 3], np.uint8)
new_array.fill(120) # 全部赋值为120
print(new_array)
new_array2 = new_array.reshape([1, 9])
print(new_array2)
GARY色彩空间(灰度图像)通常指8位灰度图,具有256个灰度级,像素值的范围是[0,255]。不同数值表示不同程度的灰色。像素值越低,灰色越深。0表示纯黑色,255表示纯白色。注意这个值不是RGB里的任何一个元素。
GARY色彩空间为单通道,所以通常用二维数组表示一幅灰度图像。
其中二值图像:只有0和255两种像素值的灰度图像
OpenCV中通道排序为BGR
RGB是我们接触最多的颜色空间,由三个通道表示一幅图像,分别为红色®,绿色(G)和蓝色(B)。这三种颜色的不同组合可以形成几乎所有的其他颜色。
RGB色彩空间还可以用一个三维的立方体来描述。当三基色分量都为0(最弱)时混合为黑色光;当三基色都为k(最大,值由存储空间决定)时混合为白色光。
F = r [ R ] + r [ G ] + r [ B ] F=r[R]+r[G]+r[B] F=r[R]+r[G]+r[B]
RGB 颜色空间利用三个颜色分量的线性组合来表示颜色,任何颜色都与这三个分量有关,而且这三个分量是高度相关的,所以连续变换颜色时并不直观,想对图像的颜色进行调整需要更改这三个分量才行。
自然环境下获取的图像容易受自然光照、遮挡和阴影等情况的影响,即对亮度比较敏感。而RGB颜色空间的三个分量都与亮度密切相关,即只要亮度改变,三个分量都会随之相应地改变,而没有一种更直观的方式来表达。
RGB颜色空间适合于显示系统,却并不适合于图像处理在HSV颜色空间下,比BGR更容易跟踪某种颜色的物体,常用于分割指定颜色的物体。
OpenCV中通道排序为HSV
HSV是一种将RGB色彩空间中的点在倒圆锥体中的表示方法。色相是色彩的基本属性,就是平常说的颜色的名称,如红色、黄色等。饱和度(S)是指色彩的纯度,越高色彩越纯,低则逐渐变灰,取0-100%的数值。明度(V),取0-max(计算机中HSV取值范围和存储的长度有关)。HSV颜色空间可以用一个圆锥空间模型来描述。圆锥的顶点处,V=0,H和S无定义,代表黑色。圆锥的顶面中心处V=max,S=0,H无定义,代表白色。
在图像处理中使用较多的是HSV颜色空间,它比RGB更接近人们对彩色的感知经验。非常直观地表达颜色的色调、鲜艳程度和明暗程度,方便进行颜色的对比。
在HSV 颜色空间下,比BGR更容易跟踪某种颜色的物体,常用于分割指定颜色的物体。
HSL和HSV稍有区别,一般我们常用的是HSV模型。
HLS中的L分量为亮度,亮度为100,表示白色,亮度为0,表示黑色;HSV 中的 V 分量为明度,明度为100,表示光谱色,明度为0,表示黑色。
为什么HSV有圆锥和圆柱两种定义,参考链接如下:
参考链接:
def cvtColor(src: Any,code: Any,dst: Any = None,dstCn: Any = None) -> dst
色彩空间的转换
import cv2 as cv
#色彩空间的转换
def color_space_demo(image):
gray = cv.cvtColor(image, cv.COLOR_BGR2GRAY)#RGB转换为gray
cv.imshow("gray", gray)
hsv = cv.cvtColor(image, cv.COLOR_BGR2HSV)#RGB转换为hsv
cv.imshow("hsv", hsv)
yuv = cv.cvtColor(image, cv.COLOR_BGR2YUV)#RGB转换为yuv
cv.imshow("yuv", yuv)
显示No module named ‘cv2’
是因为缺少了OpenCV的cv2模块,需要导入这个包
将在两个阈值内的像素值设置为白色(255),而不在阈值区间内的像素值设置为黑色(0)
inRange(src, lowerb, upperb[, dst]) -> dst
注意:
在lower_red~upper_red之间的值变成255,其余的为0
从视频中提取指定颜色范围,并做二值化处理(黑白)
def extract_object():
"""从视频中获取提取指定颜色范围"""
capture = cv.VideoCapture("car.mp4")
while True:
ret, frame = capture.read()
if not ret:
break
hsv = cv.cvtColor(frame, cv.COLOR_RGB2HSV)
lower_hsv = np.array([35, 43, 46]) # hsv的最小值
upper_hsv = np.array([77, 255, 255]) # hsv的最大值
# 用inRange函数提取指定颜色范围,这里是对hsv来处理
mask = cv.inRange(hsv, lowerb=lower_hsv, upperb=upper_hsv)
cv.imshow("mask", mask)
keyboard = cv.waitKey(40) # 40ms一帧
if keyboard == 27:
break
分离通道
split(m[, mv]) -> mv
合并通道
merge(mv[, dst]) -> dst
对RGB通道进行拆分
def channels_split(image):
"""对图片三个通道颜色进行拆分"""
b, g, r = cv.split(image) # 拆分 b通道提取时,对该通道颜色保留,其余通道置为0
# cv.imshow("blue", b)
# cv.imshow("blue", g)
# cv.imshow("blue", r)
changed_image = image.copy()
changed_image[:, :, 0] = 0 # 将b通道颜色全部置为0
cv.imshow("changed_image", changed_image)
image_merge = cv.merge([b, g, r]) #合并
cv.imshow("image_merge", image_merge)
注意:
需要两张图片大小格式完全一样
两张图片相加
add(src1, src2[, dst[, mask[, dtype]]]) -> dst
注意:
大于255的使用255计数
两张图片相减
subtract(src1, src2[, dst[, mask[, dtype]]]) -> dst
两张图片相乘(点乘)
multiply(src1, src2[, dst[, scale[, dtype]]]) -> dst
图2:
相乘结果:
注意:
因为Linux这张图是抗锯齿的,边缘经过光滑处理。黑白图像边缘并不完全是黑白的。
两张图片相除
divide(src1, src2[, dst[, scale[, dtype]]]) -> dst
对像素的二进制数据进行“与”操作
bitwise_and(src1, src2[, dst[, mask]]) -> dst
对像素的二进制数据进行“或”操作
bitwise_or(src1, src2[, dst[, mask]]) -> dst
对像素的二进制数据进行“非”操作
bitwise_not(src[, dst[, mask]]) -> dst
注意:
bitwise_not()只需要三个参数
对像素的二进制数据进行“异或”操作
bitwise_xor(src1, src2[, dst[, mask]]) -> dst
图像掩膜,是用选定的图像、图形或物体,对处理的图像(全部或局部)进行遮挡,来控制图像处理的区域或处理过程。
数字图像处理中,掩模为二维矩阵数组,有时也用多值图像,图像掩模主要用于:
计算两个数组的加权和,将两个图片进行重叠操作
addWeighted(src1, alpha, src2, beta, gamma[, dst[, dtype]]) -> dst
d s t = s r c 1 ∗ a l p h a + s r c 2 ∗ b e t a + g a m m a dst = src1*alpha + src2*beta + gamma dst=src1∗alpha+src2∗beta+gamma
修改图片对比度、亮度
def contrast_brightness(image, constract, brightness):
"""
修改图片的对比度与亮度,参数分别为:第一张图,第一张图权重,第二张图,第二张图权重,增强的亮度
"""
h, w, ch = image.shape
mask = np.zeros([h, w, ch], image.dtype)
# 图像混合,参数分别为:第一张图,第一张图权重,第二张图,第二张图权重,增强的亮度
dst = cv.addWeighted(image, constract, mask, 1 - constract, brightness)
cv.imshow("contrast_brightness", dst)
结果:
从视频中提取指定颜色范围,彩色
def extract_object_color():
"""从视频中提取指定颜色范围,彩色"""
capture = cv.VideoCapture("car.mp4")
while True:
ret, frame = capture.read()
if not ret:
break
hsv = cv.cvtColor(frame, cv.COLOR_RGB2HSV)
lower_hsv = np.array([35, 43, 46]) # hsv的最小值
upper_hsv = np.array([77, 255, 255]) # hsv的最大值
# 用inRange函数提取指定颜色范围,这里是对hsv来处理
mask = cv.inRange(hsv, lowerb=lower_hsv, upperb=upper_hsv)
dst = cv.bitwise_and(frame, frame, mask=mask)
# cv.imshow("mask", mask)
cv.imshow("dst", dst)
keyboard = cv.waitKey(40) # 40ms一帧
if keyboard == 27:
break
mask = cv.inRange(hsv, lowerb=lower_hsv, upperb=upper_hsv)
dst = cv.bitwise_and(frame, frame, mask)
这样会出不来结果,原因是函数有四个参数,如果直接写mask,位置传参会错误,需要对mask进行说明。
如下:
mask = cv.inRange(hsv, lowerb=lower_hsv, upperb=upper_hsv)
dst = cv.bitwise_and(frame, frame, mask=mask)
参考链接:
region of interest,感兴趣区域。
机器视觉、图像处理中,从被处理的图像以方框、圆、椭圆、不规则多边形等方式勾勒出需要处理的区域,称为感兴趣区域,ROI。
OpenCV图像坐标是从左上角开始,起始坐标为(0,0),往下是x坐标,往右是Y坐标
def roi(image):
"""将图片的[60:360, 90:300]转换为灰色"""
face = image[60:360, 90:300]
# cv.imshow("face", face)
face_gray = cv.cvtColor(face, cv.COLOR_BGR2GRAY)
face_back = cv.cvtColor(face_gray, cv.COLOR_GRAY2BGR)
# cv.imshow("face", face_gray)
image[60:360, 90:300] = face_back
cv.imshow("face", image)
结果:
Flood Fill Algorithm
泛洪填充算法又称洪水填充算法是在很多图形绘制软件中常用的填充算法,最熟悉不过就是windows paint的油漆桶功能。算法的原理很简单,就是从一个点开始附近像素点,填充成新的颜色,直到封闭区域内的所有像素点都被填充新颜色为止。泛红填充实现最常见有四邻域像素填充法,八邻域像素填充法,基于扫描线的像素填充方法。根据实现又可以分为递归与非递归(基于栈)。
注意: 图像处理前可以先使用image.copy()复制一份
泛洪填充算法,也称漫水填充算法。填充一个对象内部区域。
floodFill(image, mask, seedPoint, newVal[, loDiff[, upDiff[, flags]]]) -> retval, image, mask, rect
填充区域:
s r c ( s e e d . x , s e e d . y ) − l o D i f f < = s r c ( x , y ) < = s r c ( s e e d . x , s e e d . y ) + u p D i f f src(seed.x, seed.y)-loDiff<=src(x, y)<=src(seed.x, seed.y)+upDiff src(seed.x,seed.y)−loDiff<=src(x,y)<=src(seed.x,seed.y)+upDiff
注意:
对图片进行泛洪填充,标记为FLOODFILL_FIXED_RANGE
def fill_color(image):
"""对图片进行泛洪填充,标记为FLOODFILL_FIXED_RANGE"""
copy_image = image.copy()
h, w = image.shape[:2]
mask = np.zeros([w + 2, h + 2], np.uint8)
# cv.floodFill(图片,遮盖层,起始位置,填充颜色,低值,高值,填充方法)
cv.floodFill(copy_image, mask, (30, 30), (0, 255, 255), (100, 100, 100), (50, 50, 50), cv.FLOODFILL_FIXED_RANGE)
cv.imshow("copy_image", copy_image)
对黑色的图片进行泛洪填充,标记为FLOODFILL_MASK_ONLY
def fill_binary():
"""对黑色的图片进行泛洪填充,标记为FLOODFILL_MASK_ONLY"""
image = np.zeros([400, 400, 3], np.uint8)
image[300, 300, 1] = 255
cv.imshow("fill_binary_1", image)
mask = np.ones([400 + 2, 400 + 2], np.uint8)
mask[101:301, 101:301] = 0 # mask中间矩形[101:301, 101:301]是0,旁边是1
# cv.floodFill(图片,遮盖层,起始位置,填充颜色,低值,高值,填充方法)
cv.floodFill(image, mask, (200, 200), (0, 255, 255), cv.FLOODFILL_MASK_ONLY)
cv.imshow("fill_binary_2", image)
图像处理中,常用的滤波算法有均值滤波、中值滤波以及高斯滤波等
滤波器种类 | 基本原理 | 特点 |
---|---|---|
均值滤波 | 使用模板内所有像素的平均值代替模板中心像素灰度值 | 易收到噪声的干扰,不能完全消除噪声,只能相对减弱噪声 |
中值滤波 | 计算模板内所有像素中的中值,并用所计算出来的中值体改模板中心像素的灰度值 | 对噪声不是那么敏感,能够较好的消除椒盐噪声,但是容易导致图像的不连续性 |
高斯滤波 | 对图像邻域内像素进行平滑时,邻域内不同位置的像素被赋予不同的权值 | 对图像进行平滑的同时,同时能够更多的保留图像的总体灰度分布特征 |
blur(src, ksize[, dst[, anchor[, borderType]]]) -> dst
均值模糊
def blurry(image):
"""均值模糊,对随机噪声去噪较好"""
# 均值模糊,卷积核为大小为5*5(第1位为x方向,第2位为y方向)
image_blurry = cv.blur(image, (5, 5))
cv.imshow("image_blurry", image_blurry)
medianBlur(src, ksize[, dst]) -> dst
中值模糊
def blurry_medium(image):
"""中值模糊,对椒盐噪声去噪较好"""
image_median = cv.medianBlur(image, 5)
cv.imshow("image_median", image_median)
使用自定义内核对图像进行卷积。该功能将任意线性滤波器应用于图像。支持就地操作。当光圈部分位于图像外部时,该功能会根据指定的边框模式插入异常像素值。
filter2D(src, ddepth, kernel[, dst[, anchor[, delta[, borderType]]]]) -> dst
自定义滤波器,可产生锐化等多种效果
def blurry_custom(image):
"""自定义滤波器"""
# kernel = np.ones([5, 5], np.float32) / 25 # 自定义内核大小
kernel = np.array([[0, -1, 0], [-1, 5, -1], [0, -1, 0]], np.float32) # 锐化算子
# filter2D(src, depth(图像深度,-1表示默认和src一样深度), kernel, dst=None, anchor=None(锚点,卷积核中心), delta=None, borderType=None)
image_custom = cv.filter2D(image, -1, kernel=kernel) # 二维滤波器
cv.imshow("blurry_custom", image_custom)
图像内核是一个小矩阵,用于应用您可能在Photoshop或Gimp中找到的效果,例如模糊,锐化,轮廓或浮雕。它们还用于机器学习中的“特征提取”,这是一种用于确定图像最重要部分的技术。在这种情况下,该过程更普遍地称为“卷积”。
常用的几种内核:
上述内核定义及效果参考链接:
高斯分布=正态分布
cv2.GaussianBlur( SRC,ksize,sigmaX [,DST [,sigmaY [,borderType ] ] ] ) →DST
注意:
卷积核和标准差sigma只需要填一个,另一个为0
高斯核可以看成是与中心距离负相关的权重。平滑时,调整σ实际是在调整周围像素对当前像素的影响程度,调大σ即提高了远处像素对中心像素的影响程度,滤波结果也就越平滑。高斯曲线随σ变化的曲线如下:
参考链接:
给图片加上高斯噪声
def clamp(pv):
"""确保随机数在0至255之间"""
if pv > 255:
return 255
elif pv < 0:
return 0
else:
return pv
def add_gaussian_noise(image):
"""给图片加上高斯噪声"""
h, w, c = image.shape
for row in range(h):
for col in range(w):
# normal(loc=0.0, scale=1.0, size=None),均值,标准差,大小
s = np.random.normal(0, 20, 3)
b = image[row, col, 0]
g = image[row, col, 1]
r = image[row, col, 2]
image[row, col, 0] = clamp(b + s[0])
image[row, col, 1] = clamp(g + s[1])
image[row, col, 2] = clamp(r + s[2])
cv.imshow("add_gaussian_noise", image)
高斯滤波
"""高斯滤波(模糊)"""
# 高斯滤波,卷积核大小为5*5,卷积核与标准差只需要填一个
frame_hsv_blur = GaussianBlur(frame_hsv, (5, 5), 0)
双边滤波(Bilateral filter)是一种非线性的滤波方法,是结合图像的空间邻近度和像素值相似度的一种折衷处理,同时考虑空域信息和灰度相似性,达到保边去噪的目的。具有简单、非迭代、局部的特点。
双边滤波器的好处是可以做边缘保存(edge preserving),一般用高斯滤波去降噪,会较明显地模糊边缘,对于高频细节的保护效果并不明显。双边滤波器顾名思义比高斯滤波多了一个高斯方差sigma-d,它是基于空间分布的高斯滤波函数,所以在边缘附近,离的较远的像素不会太多影响到边缘上的像素值,这样就保证了边缘附近像素值的保存。但是由于保存了过多的高频信息,对于彩色图像里的高频噪声,双边滤波器不能够干净的滤掉,只能够对于低频信息进行较好的滤波。
高斯双边滤波
bilateralFilter(src, d, sigmaColor, sigmaSpace[, dst[, borderType]]) -> dst
注意:
def bilateral(image):
"""
同时考虑空间与信息和灰度相似性,达到保边去噪的目的
双边滤波的核函数是空间域核与像素范围域核的综合结果:
在图像的平坦区域,像素值变化很小,对应的像素范围域权重接近于1,此时空间域权重起主要作用,相当于进行高斯模糊;
在图像的边缘区域,像素值变化很大,像素范围域权重变大,从而保持了边缘的信息。
"""
# bilateralFilter(src, d, sigmaColor, sigmaSpace, dst=None, borderType=None)
dst = cv.bilateralFilter(image, 0, 100, 15) # 高斯双边滤波
cv.imshow("bilateral ", dst)
均值漂移算法是一种通用的聚类算法,它的基本原理是:对于给定的一定数量样本,任选其中一个样本,以该样本为中心点划定一个圆形区域,求取该圆形区域内样本的质心,即密度最大处的点,再以该点为中心继续执行上述迭代过程,直至最终收敛。
这个函数严格来说并不是图像的分割,而是图像在色彩层面的平滑滤波,它可以中和色彩分布相近的颜色,平滑色彩细节,侵蚀掉面积较小的颜色区域。
pyrMeanShiftFiltering(src, sp, sr[, dst[, maxLevel[, termcrit]]]) -> dst
pyrMeanShiftFiltering函数的执行过程:
迭代空间构建:
以输入图像上src上任一点P0为圆心,建立物理空间上半径为sp,色彩空间上半径为sr的球形空间,物理空间上坐标2个—x、y,色彩空间上坐标3个—R、G、B(或HSV),构成一个5维的空间球体。
其中物理空间的范围x和y是图像的长和宽,色彩空间的范围R、G、B分别是0~255。
求取迭代空间的向量并移动迭代空间球体后重新计算向量,直至收敛:
在1中构建的球形空间中,求得所有点相对于中心点的色彩向量之和后,移动迭代空间的中心点到该向量的终点,并再次计算该球形空间中所有点的向量之和,如此迭代,直到在最后一个空间球体中所求得的向量和的终点就是该空间球体的中心点Pn,迭代结束。
更新输出图像dst上对应的初始原点P0的色彩值为本轮迭代的终点Pn的色彩值,如此完成一个点的色彩均值漂移。
对输入图像src上其他点,依次执行步骤1,、2、3,遍历完所有点位后,整个均值偏移色彩滤波完成,这里忽略对金字塔的讨论。
注意:
关键参数是sp和sr的设置,二者设置的值越大,对图像色彩的平滑效果越明显,同时函数耗时也越多。
均值迁移
def shift(image):
"""均值迁移"""
dst = cv.pyrMeanShiftFiltering(image, 10, 50)
cv.imshow("shift", dst)
参考链接:
直方图是对图像像素的统计分布,它统计了每个像素(0到L-1)的数量。
注意:
pycharm从2017.3版之后,将matplotlib的绘图的结果默认显示在SciView窗口中,而不是弹出独立的窗口。
修改方式见链接:
新版Pycharm中Matplotlib图像不在弹出独立的显示窗口
将多维数组降为一维
可以传入索引:‘C’, ‘F’, ‘A’, ‘K’
参考链接:
numpy 辨异 (五)—— numpy.ravel() vs numpy.flatten()
枚举
参考链接:
绘制直方图,一般用来绘制灰度直方图
def hist(
x, bins=None, range=None, density=False, weights=None,
cumulative=False, bottom=None, histtype='bar', align='mid',
orientation='vertical', rwidth=None, log=False, color=None,
label=None, stacked=False, *, data=None, **kwargs):
return gca().hist(
x, bins=bins, range=range, density=density, weights=weights,
cumulative=cumulative, bottom=bottom, histtype=histtype,
align=align, orientation=orientation, rwidth=rwidth, log=log,
color=color, label=label, stacked=stacked,
**({"data": data} if data is not None else {}), **kwargs)
注意: 此处还没有将图像转换为灰度图
from matplotlib import pyplot as plt
def plot(image):
"""画出image的直方图"""
# image.ravel()将图像展开为一维数组,256为bins数量,[0, 256]为数值范围(不包括256)
plt.hist(image.ravel(), 256, [0, 256])
plt.show() # 显示直方图
计算图像直方图
cv2.calcHist(images, channels, mask, histSize, ranges[, hist[, accumulate ]]) ->hist
注意:
绘制image的直方图(BGR三个通道)
def image_hist(image):
"""绘制image的直方图(三个通道)"""
color = ('blue', 'green', 'red')
for i, color in enumerate(color): # # enumerate 枚举,返回元素以及对应的索引
# 计算出直方图,calcHist(images, channels, mask, histSize(有多少个bin), ranges[, hist[, accumulate]]) -> hist
# hist 是一个 256x1 的数组,每一个值代表了与该灰度值对应的像素点数目。
hist = cv.calcHist(image, [i], None, [256], [0, 256])
# print(hist.shape)
plt.plot(hist, color=color) # 绘制函数曲线
plt.xlim([0, 256]) # 设置坐标轴刻度取值范围
plt.show()
问题一:
在按照https://blog.csdn.net/u010472607/article/details/82290159点击查看的操作,让图表单独显示后,产生了图框出现而内容不出现的问题,如下图所示:
原因:未知,个人猜测是交互模式和阻碍模式的问题(可能性高),或是线程阻碍产生的结果
解决方案:
第一种:
# cv.imshow("example", src1)
plot(src1)
第二种:
plt.show(False)
第三种(推荐):
第四种(推荐):
参考链接:
问题二:
在出现问题一并解决后,想要同时显示两张图表,却发现两张图只显示前一张
原因: 显示第一张的时候就用了plt.show
解决方案:
第一种:
第二种:
直方图均衡化就是将原始的直方图拉伸,使之均匀分布在全部灰度范围内,从而增强图像的对比度。
直方图均衡化的中心思想是把原始图像的的灰度直方图从比较集中的某个区域变成在全部灰度范围内的均匀分布。
如果一幅图像的灰度直方图几乎覆盖了整个灰度的取值范围,并且除了个别灰度值的个数较为突出,整个灰度值分布近似于均匀分布,那么这幅图像就具有较大的灰度动态范围和较高的对比度,同时图像的细节更为丰富。
均衡化步骤:
参考链接:
注意:
OpenCV里的直方图均衡化都是基于灰度图
直方图均衡化
equalizeHist(src[, dst]) -> dst
直方图均衡化
def equal_hist(image):
"""全局直方图均衡化,用于增强图像对比度,即黑的更黑,白的更白"""
gray = cv.cvtColor(image, cv.COLOR_BGR2GRAY)
dst = cv.equalizeHist(gray)
cv.imshow("equalHist", dst)
直方图均衡化可以可能到是一种全局意义上的均衡化,但是有的时候这种操作并不是很好,会把某些不该调整的部分给调整了。OpenCV中还有一种直方图均衡化,它是一种局部局部来的均衡化,也就是是说把整个图像分成许多小块(比如按10*10作为一个小块),那么对每个小块进行均衡化。这种方法主要对于图像直方图不是那么单一的(比如存在多峰情况)图像比较实用。
自适应均衡化图像
createCLAHE([, clipLimit[, tileGridSize]]) -> retval
注意:
局部直方图均衡化
def local_equal_hist(image):
"""局部直方图均衡化"""
gray = cv.cvtColor(image, cv.COLOR_BGR2GRAY)
# clipLimit颜色对比度的阈值,titleGridSize进行像素均衡化的网格大小,即在多少网格下进行直方图的均衡化操作
local_hist = cv.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) # 实例化均衡直方图函数
local_hist_dst = local_hist.apply(gray)
cv.imshow("local_equal_hist", local_hist_dst)
参考链接:
如果我们有两张图像,并且这两张图像的直方图一样,或者有极高的相似度,那么在一定程度上,我们可以认为这两幅图是一样的,这就是直方图比较的应用之一。
直方图比较
compareHist(H1, H2, method) -> retval
比较方式有:
==注意: ==
用巴氏距离和相关性比较好
参考链接:
利用直方图比较图像相似性
def create_rgbhist(image):
"""创建直方图"""
h, w, c = image.shape
rgbHist = np.zeros([16 * 16 * 16, 1], np.float32) # 每个通道的灰度值(0~255共256个值)分为16个刻度
bsize = 256 / 16 # 刻度宽度
for row in range(h):
for col in range(w): # 拆分图片通道
b = image[row, col, 0]
g = image[row, col, 1]
r = image[row, col, 2]
# 单一通道的灰度值依据刻度的宽度归到16个刻度内得到对应的值
# 像素点的BGR三通道分别依据得到的值,按权重取得索引(就是三通道的值排到一起了)
# index = np.int(b / bsize) * 16 * 16 + np.int(g / bsize) * 16 + np.int(r / bsize)
# rgbHist[np.int(index), 0] = rgbHist[np.int(index), 0] + 1 # 对应索引处的值(频数)+1
index = int(b / bsize) * 16 * 16 + int(g / bsize) * 16 + int(r / bsize)
rgbHist[int(index), 0] = rgbHist[int(index), 0] + 1 # 对应索引处的值(频数)+1
return rgbHist
def hist_compare(image1, image2):
"""利用直方图比较相似性"""
hist1 = create_rgb_demo(image1)
hist2 = create_rgb_demo(image2)
match1 = cv.compareHist(hist1, hist2, method=cv.HISTCMP_BHATTACHARYYA)
match2 = cv.compareHist(hist1, hist2, method=cv.HISTCMP_CORREL)
match3 = cv.compareHist(hist1, hist2, method=cv.HISTCMP_CHISQR)
print("巴式距离:%s, 相关性:%s, 卡方:%s" % (match1, match2, match3))
结果:
SRC1
SRC2
计算结果:
注意:
上述比较的代码只能用于两张大小一样的图片,如果需要比较两张不一样
大小的图片,需要进行归一化操作
把RGB颜色空间,想象成一个三维立体的坐标系,rgb对应xyz轴,每个颜色8 bins,对应xyz三个轴上,8个等分刻度,这样就得到一个8x8x8=512个小立方体构成的大立方体,要的直方图就是每个小立方体在大立方体中出现的概率分布。
参考链接:
按照教程调用hist_compare函数的时候显示:
`np.int` is a deprecated alias for the builtin `int`. To silence this warning, use `int` by itself. Doing this will not modify any behavior and is safe. When replacing `np.int`, you may wish to use e.g. `np.int64` or `np.int32` to specify the precision. If you wish to review your current use, check the release note link for additional information.
原因:Python不推荐使用np.int
解决方案:将np.int修改为int
一维直方图,需要从BGR转换为灰度,二维直方图,需要将图像从BGR转换为HSV。
def hist2d(image):
"""绘制图像的二维直方图"""
hsv = cv.cvtColor(image, cv.COLOR_BGR2HSV)
hist = cv.calcHist([hsv], [0, 1], None, [180, 360], [0, 180, 0, 256]) # 计算H和S通道的2D直方图
print(hist.shape)
# cv.imshow("hist2d", hist)
plt.imshow(hist, interpolation="nearest") # 直方图显示,插值方法为最近领域内插法
plt.title("2D Histogram")
plt.ioff()
plt.show()
结果:
注意:
interpolation='nearest’如果显示分辨率与图像分辨率不同(通常是这种情况),则只显示图像而不尝试在像素之间进行插值。它将生成一个像素显示为多个像素的正方形的图像。
参考链接:
归一化(矩阵的值通过某种方式变到某一个区间内)
normalize(src, dst[, alpha[, beta[, norm_type[, dtype[, mask]]]]]) -> dst
src-输入数组。
dst-与SRC大小相同的输出数组。
α-范数值在范围归一化的情况下归一化到较低的范围边界。
β-上限范围在范围归一化的情况下;它不用于范数归一化。
norm_type-规范化类型,见下面参考链接。
dType——当输出为负时,输出数组具有与SRC相同的类型;否则,它具有与SRC相同的信道数和深度=CVH-MatthAsHead(DyType)。
mask-可选的操作掩膜。
参考链接:
直方图反向投影
calcBackProject(images, channels, hist, ranges, scale[, dst]) -> dst
参数与cv2.calchist几乎相同
注意:
在传递给backproject函数之前,应该对对象直方图进行归一化。
def back_projection(sample, target):
"""直方图反向投影,sample是样本,target是需要寻找的输入图像"""
roi_hsv = cv.cvtColor(sample, cv.COLOR_BGR2HSV)
target_hsv = cv.cvtColor(target, cv.COLOR_BGR2HSV)
cv.imshow("sample", sample)
cv.imshow("target", target)
roiHist = cv.calcHist([roi_hsv], [0, 1], None, [180, 256], [0, 180, 0, 256])
# 归一化:原始图像,结果图像,映射到结果图像中的最小值,最大值,归一化类型
# cv.NORM_MINMAX对数组的所有值进行转化,使它们线性映射到最小值和最大值之间
# 归一化后的图像便于显示,归一化后到0,255之间了
cv.normalize(roiHist, roiHist, 0, 255, cv.NORM_MINMAX)
dst = cv.calcBackProject([target_hsv], [0, 1], roiHist, [0, 180, 0, 256], 1)
cv.imshow("backProjectionDemo", dst)
模板匹配就是在整个图像区域发现与给定子图像匹配的小块区域。
所以模板匹配首先需要一个模板图像T(给定的子图像),另外需要一个待检测的图像-源图像S。
工作方法:在带检测图像上,从左到右,从上向下计算模板图像与重叠子图像的匹配度,匹配程度越大,两者相同的可能性越大。
模板匹配
matchTemplate(image, templ, method[, result[, mask]]) -> result
注意: 如果使用cv.TM_SQDIFF作为比较方法,则最小值提供最佳匹配
寻找最值
minMaxLoc(src[, mask]) -> minVal, maxVal, minLoc, maxLoc
模板匹配
def template_match(sample, target):
"""模板匹配"""
target_copy = target.copy()
methods = [cv.TM_SQDIFF_NORMED, cv.TM_CCORR_NORMED, cv.TM_CCOEFF_NORMED] # 三种模板匹配方法
th, tw = sample.shape[:2] # 获取样本的行,列数
for md in methods:
print(md)
result = cv.matchTemplate(target_copy, sample, md) # 得到匹配结果
min_val, max_val, min_loc, max_loc = cv.minMaxLoc(result)
if md == cv.TM_SQDIFF_NORMED: # cv.TM_SQDIFF_NORMED最小时最相似,其他最大时最相似
tl = min_loc
else:
tl = max_loc
br = (tl[0] + tw, tl[1] + th)
cv.rectangle(target_copy, tl, br, (0, 0, 255), 2) # tl为左上角坐标,br为右下角坐标,从而画出矩形
cv.imshow("match-" + np.str(md), target_copy)
target_copy = target.copy()
结果:
match-1,3,5分别对应TM_SQDIFF_NORMED,TM_CCORR_NORMED和TM_CCOEFF_NORMED
注意: 多对象的模板匹配需要使用阈值化的方式,见链接:点击查看
多对象的模板匹配
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
img_rgb = cv.imread('mario.png')
img_gray = cv.cvtColor(img_rgb, cv.COLOR_BGR2GRAY)
template = cv.imread('mario_coin.png',0)
w, h = template.shape[::-1]
res = cv.matchTemplate(img_gray,template,cv.TM_CCOEFF_NORMED)
threshold = 0.8
loc = np.where( res >= threshold)
for pt in zip(*loc[::-1]):
cv.rectangle(img_rgb, pt, (pt[0] + w, pt[1] + h), (0,0,255), 2)
cv.imwrite('res.png',img_rgb)
参考链接:
简单阈值
threshold(src, thresh, maxval, type[, dst]) -> retval, dst
注意:
THRESH_BINARY 二进制阈值化 -> 大于阈值为1 小于阈值为0
THRESH_BINARY_INV 反二进制阈值化 -> 大于阈值为0 小于阈值为1
THRESH_TRUNC 截断阈值化 -> 大于阈值为阈值,小于阈值不变
THRESH_TOZERO 阈值化为0 -> 大于阈值的不变,小于阈值的全为0
THRESH_TOZERO_INV 反阈值化为0 -> 大于阈值为0,小于阈值不变
参考链接:
def image_binary(image):
"""图像二值化(简单阈值)"""
image_gray = cv.cvtColor(image, cv.COLOR_BGR2GRAY)
# 这个函数的第一个参数就是原图像,原图像应该是灰度图。
# 第二个参数就是用来对像素值进行分类的阈值。
# 第三个参数就是当像素值高于(有时是小于)阈值时应该被赋予的新的像素值
# 第四个参数指定阈值类型(图像处理方式 | cv.THRESH_OTSU、cv.THRESH_TRIANGLE表示使用OTSU、TRIANGLE的阈值计算方法)
ret, binary = cv.threshold(image_gray, 0, 255, cv.THRESH_BINARY | cv.THRESH_OTSU)
# ret, binary = cv.threshold(image_gray, 0, 255, cv.THRESH_BINARY | cv.THRESH_TRIANGLE)
# ret, binary = cv.threshold(image_gray, 0, 255, cv.THRESH_TOZERO | cv.THRESH_OTSU)
print("threshold value: %s" % ret)
cv.imshow("threshold_demo", binary)
cv.imshow("image", image)
def image_threshold(image):
"""显示多种图像二值化方法(简单阈值)"""
image_gray = cv.cvtColor(image, cv.COLOR_BGR2GRAY)
ret, thresh1 = cv.threshold(image_gray, 127, 255, cv.THRESH_BINARY)
ret, thresh2 = cv.threshold(image_gray, 127, 255, cv.THRESH_BINARY_INV)
ret, thresh3 = cv.threshold(image_gray, 127, 255, cv.THRESH_TRUNC)
ret, thresh4 = cv.threshold(image_gray, 127, 255, cv.THRESH_TOZERO)
ret, thresh5 = cv.threshold(image_gray, 127, 255, cv.THRESH_TOZERO_INV)
titles = ['Original Image', 'BINARY', 'BINARY_INV', 'TRUNC', 'TOZERO', 'TOZERO_INV']
images = [image, thresh1, thresh2, thresh3, thresh4, thresh5]
for i in range(6):
plt.subplot(2, 3, i + 1), plt.imshow(images[i], cmap='gray') # 将图像按2x3铺开,以灰度图的方式显示
plt.title(titles[i])
plt.xticks([]), plt.yticks([])
plt.show()
注意: plt.imshow默认是带点绿色的图,想要显示灰度图需要传参cmap=‘gray’。
参考链接:
在前面的部分我们使用是全局阈值,整幅图像采用同一个数作为阈值。
当时这种方法并不适应与所有情况,尤其是当同一幅图像上的不同部分的具有不同亮度时。这种情况下我们需要采用自适应阈值。此时的阈值是根据图像上的每一个小区域计算与其对应的阈值。
因此在同一幅图像上的不同区域采用的是不同的阈值,从而使我们能在亮度不同的情况下得到更好的结果。
自适应阈值
adaptiveThreshold(src, maxValue, adaptiveMethod, thresholdType, blockSize, C[, dst]) -> dst
adaptiveMethod:Int类型的,这里有两种选择,不过这两种方法最后得到的结果要减掉参数里面的C值。
thresholdType:同type,见threshold
blockSize:Int类型的,这个值来决定像素的邻域块有多大。
C:偏移值调整量,计算adaptiveMethod用到的参数。
注意: 这里的blockSize的值要为奇数,否则会给出这样的提示:
Assertion failed (blockSize % 2 == 1 && blockSize > 1) in cv::adaptiveThreshold
参考链接:
def threshold_adaptive(image):
"""图像二值化(自适应阈值)"""
img = cv.cvtColor(image, cv.COLOR_BGR2GRAY)
# 中值滤波
img = cv.medianBlur(img, 5)
ret, th1 = cv.threshold(img, 127, 255, cv.THRESH_BINARY)
# 11 为 Block size, 2 为 C 值
th2 = cv.adaptiveThreshold(img, 255, cv.ADAPTIVE_THRESH_MEAN_C, cv.THRESH_BINARY, 11, 2)
th3 = cv.adaptiveThreshold(img, 255, cv.ADAPTIVE_THRESH_GAUSSIAN_C, cv.THRESH_BINARY, 11, 2)
titles = ['Original Image', 'Global Threshold (v = 127)', 'Adaptive Mean Threshold', 'Adaptive Gaussian Threshold']
images = [img, th1, th2, th3]
for i in range(4):
plt.subplot(2, 2, i + 1), plt.imshow(images[i], cmap='gray')
plt.title(titles[i])
plt.xticks([]), plt.yticks([])
plt.show()
手动将图像二值化
def threshold_custom(image):
"""手动将图像二值化"""
gray = cv.cvtColor(image, cv.COLOR_BGR2GRAY)
h, w = gray.shape[:2]
m = np.reshape(gray, [1, w*h])
mean = m.sum() / (w*h) # 求出整个灰度图像的平均值
print("mean:", mean)
ret, binary = cv.threshold(gray, mean, 255, cv.THRESH_BINARY)
cv.imshow("threshold_custom", binary)
def big_image_threshold(image):
"""大图像二值化"""
print(image.shape)
cw = 256 # cw、ch定义分隔的小块的大小
ch = 256
h, w = image.shape[:2]
gray = cv.cvtColor(image, cv.COLOR_BGR2GRAY)
# cv.imshow("big_image_gray", gray)
for row in range(0, h, ch): # 分割图片
for col in range(0, w, cw):
roi = gray[row:row+ch, col:col+cw] # 获取ROI(坐标为row,col的256*256的小矩形)
# 对ROI区域进行图像二值化(自适应阈值),127是256/2,将ROI分割成四个小区域分别进行
dst = cv.adaptiveThreshold(roi, 255, cv.ADAPTIVE_THRESH_GAUSSIAN_C, cv.THRESH_BINARY, 127, 2)
gray[row:row + ch, col:col + cw] = dst
print(np.std(dst), np.mean(dst)) # 打印ROI区域的标准差和平均值
cv.imwrite("result_big_image.jpg", gray) # 保存图像
结果:
分辨率:1000 * 1398(在windows上显示的宽高,OpenMV里是1398 * 1000)
分辨率:2362 * 3425(在windows上显示的宽高,OpenMV里是3425 * 2362)
对该算法进行优化:
def big_image_threshold_pro(image):
"""优化大图像二值化"""
print(image.shape)
cw = 600 # cw、ch定义分隔的小块的大小
ch = 600
h, w = image.shape[:2]
gray = cv.cvtColor(image, cv.COLOR_BGR2GRAY)
# cv.imshow("big_image_gray", gray)
for row in range(0, h, ch): # 分割图片
for col in range(0, w, cw):
roi = gray[row:row + ch, col:col + cw] # 获取ROI(坐标为row,col的256*256的小矩形)
# 对ROI区域进行图像二值化(自适应阈值)
dst = cv.adaptiveThreshold(roi, 255, cv.ADAPTIVE_THRESH_GAUSSIAN_C, cv.THRESH_BINARY, 299, 2)
if np.std(dst) <= 60:
gray[row:row + ch, col:col + cw] = 255
else:
gray[row:row + ch, col:col + cw] = dst
print(np.std(dst), np.mean(dst)) # 打印ROI区域的标准差和平均值
cv.imwrite("result_big_imagePro.jpeg", gray) # 保存图像
图像金字塔最初用于机器视觉和图像压缩,一幅图像的金字塔是一系列以金字塔形状排列的分辨率逐步降低,且来源于同一张原始图的图像集合。其通过梯次向下采样获得,直到达到某个终止条件才停止采样。
金字塔的底部是待处理图像的高分辨率表示,而顶部是低分辨率的近似。我们将一层一层的图像比喻成金字塔,层级越高,则图像越小,分辨率越低。
两者的区别:高斯金字塔用来向下降采样图像,注意降采样其实是由金字塔底部向上采样,分辨率降低,它和我们理解的金字塔概念相反;而拉普拉斯金字塔则用来从金字塔底层图像中向上采样重建一个图像。
注意:
参考链接:
高斯金字塔是通过高斯平滑和亚采样获得一系列下采样图像,也就是说第K层高斯金字塔通过平滑、亚采样就可以获得K+1层高斯图像,高斯金字塔包含了一系列低通滤波器,其截至频率从上一层到下一层是以因子2逐渐增加,所以高斯金字塔可以跨越很大的频率范围。
对图像的向下取样操作,即缩小图像。
为了获取层级为 G_i+1 的金字塔图像,方法步骤如下:
得到的图像即为G_i+1的图像,显而易见,结果图像只有原图的四分之一。通过对输入图像G_i(原始图像)不停迭代以上步骤就会得到整个金字塔。同时我们也可以看到,向下取样会逐渐丢失图像的信息。以上就是对图像的向下取样操作,即缩小图像。
对图像的向上取样,即放大图像
方法步骤如下:
得到的图像即为放大后的图像,但是与原来的图像相比会发觉比较模糊,因为在缩放的过程中已经丢失了一些信息,如果想在缩小和放大整个过程中减少信息的丢失,这些数据形成了拉普拉斯金字塔。
拉普拉斯金字塔第i层的数学定义:
L i = G i − U P ( G i + 1 ) ⊗ G 5 × 5 式 中 的 G i 表 示 第 i 层 的 图 像 。 而 U P ( ) 操 作 是 将 源 图 像 中 位 置 为 ( x , y ) 的 像 素 映 射 到 目 标 图 像 的 ( 2 x + 1 , 2 y + 1 ) 位 置 , 即 在 进 行 向 上 取 样 。 符 号 ⊗ 表 示 卷 积 , G 5 × 5 为 5 × 5 的 高 斯 内 核 。 L_i=G_i-UP(G_{i+1})\otimes\mathcal{G}_{5×5} \\ 式中的G_i表示第i层的图像。而UP()操作是将源图像中位置为\\(x,y)的像素映射到目标图像的(2x+1,2y+1)位置,即在进行\\向上取样。符号\otimes表示卷积,\mathcal{G}_{5×5} 为{5×5} 的高斯内核。 Li=Gi−UP(Gi+1)⊗G5×5式中的Gi表示第i层的图像。而UP()操作是将源图像中位置为(x,y)的像素映射到目标图像的(2x+1,2y+1)位置,即在进行向上取样。符号⊗表示卷积,G5×5为5×5的高斯内核。
pryUp,就是在进行上面这个式子的运算。因此,可以直接用OpenCV进行拉普拉斯运算:
L i = G i − P y r U P ( G i + 1 ) L_i=G_i-PyrUP(G_{i+1}) Li=Gi−PyrUP(Gi+1)
将降采样之后的图像再进行上采样操作,然后与之前还没降采样的原图进行做差得到残差图!为还原图像做信息的准备。
也就是说,拉普拉斯金字塔是通过源图像减去先缩小后再放大的图像的一系列图像构成的。保留的是残差!
注意: 上采样和下采样是非线性处理,不可逆,有损的处理。
先对图像进行高斯平滑,然后再进行降采样
pyrDown(src[, dst[, dstsize[, borderType]]]) -> dst
{ ∣ d s t s i z e . w i d t h ∗ 2 − s r c . c o l s ∣ ≤ 2 ∣ d s t s i z e . h e i g h t ∗ 2 − s r c . r o w s ∣ ≤ 2 \begin{cases} |dstsize.width * 2 - src.cols| ≤ 2\\ |dstsize.height * 2 - src.rows| ≤ 2 \end{cases} {∣dstsize.width∗2−src.cols∣≤2∣dstsize.height∗2−src.rows∣≤2
先对图像进行升采样(将图像尺寸行和列方向增大一倍),然后再进行高斯平滑
pyrUp(src[, dst[, dstsize[, borderType]]]) -> dst
{ / / 如 果 w i d t h 是 偶 数 , 那 么 必 须 d s t s i z e . w i d t h 是 s r c . c o l s 的 2 倍 ∣ d s t s i z e . w i d t h − s r c . c o l s ∗ 2 ∣ ≤ ( d s t s i z e . w i d t h m o d 2 ) ; ∣ d s t s i z e . h e i g h t − s r c . r o w s ∗ 2 ∣ ≤ ( d s t s i z e . h e i g h t m o d 2 ) ; \begin{cases} //如果width是偶数,那么必须dstsize.width是src.cols的2倍\\ |dstsize.width - src.cols * 2| ≤ (dstsize.width mod 2); \\ |dstsize.height - src.rows * 2| ≤ (dstsize.height mod 2); \end{cases} ⎩⎪⎨⎪⎧//如果width是偶数,那么必须dstsize.width是src.cols的2倍∣dstsize.width−src.cols∗2∣≤(dstsize.widthmod2);∣dstsize.height−src.rows∗2∣≤(dstsize.heightmod2);
def pyramid_image(image):
"""高斯图像金字塔"""
level = 4
temp = image.copy() # 备份image
pyramid_images = []
for i in range(level):
dst = cv.pyrDown(temp) # PyrDown降采样
pyramid_images.append(dst)
cv.imshow("pyramid_down_" + str(i + 1), dst)
temp = dst.copy()
return pyramid_images
def laplace_image(image):
"""拉普拉斯图像金字塔"""
pyramid_images = pyramid_image(image)
level = len(pyramid_images)
for i in range(level - 1, -1, -1):
if i - 1 < 0:
expand = cv.pyrUp(pyramid_images[i], dstsize=image.shape[:2])
lpls = cv.subtract(image, expand)
cv.imshow("laplace_" + str(i), lpls)
else:
expand = cv.pyrUp(pyramid_images[i], dstsize=pyramid_images[i - 1].shape[:2])
lpls = cv.subtract(pyramid_images[i - 1], expand)
cv.imshow("laplace_" + str(i), lpls)
注意:
注意图像大小需要是n*n的
结果:
参考链接:
Sobel算子是高斯平滑加微分运算的联合运算,因此它更抗噪声。如果ksize = -1,则使用3x3 Scharr滤波器,比3x3 Sobel滤波器具有更好的结果。
Sobel算子:
k e r n e l = [ − 1 0 1 − 2 0 2 − 1 0 1 ] / / 水 平 kernel = \left[ {\begin{matrix} -1 & 0 & 1 \\ -2 & 0 & 2 \\ -1 & 0 & 1 \\ \end{matrix}} \right]//水平 kernel=⎣⎡−1−2−1000121⎦⎤//水平
k e r n e l = [ − 1 − 2 − 1 0 0 0 1 2 1 ] / / 垂 直 kernel = \left[ {\begin{matrix} -1 & -2 & -1 \\ 0 & 0 & 0 \\ 1 & 2 & 1 \\ \end{matrix}} \right]//垂直 kernel=⎣⎡−101−202−101⎦⎤//垂直
Scharr算子:
k e r n e l = [ − 3 0 3 − 10 0 10 − 3 0 3 ] / / 水 平 kernel = \left[ {\begin{matrix} -3 & 0 & 3 \\ -10 & 0 & 10 \\ -3 & 0 & 3 \\ \end{matrix}} \right]//水平 kernel=⎣⎡−3−10−30003103⎦⎤//水平
k e r n e l = [ − 3 − 10 − 3 0 0 0 3 10 3 ] / / 垂 直 kernel = \left[ {\begin{matrix} -3 & -10 & -3 \\ 0 & 0 & 0 \\ 3 & 10 & 3 \\ \end{matrix}} \right]//垂直 kernel=⎣⎡−303−10010−303⎦⎤//垂直
Sobel滤波器
Sobel(src, ddepth, dx, dy[, dst[, ksize[, scale[, delta[, borderType]]]]]) -> dst
注意: Sobel函数求完导数后会有负值,还有会大于255的值。而原图像是uint8,即8位无符号数,所以Sobel建立的图像位数不够,会有截断。第二个参数可以传cv.CV_32F。在经过处理后,要用convertScaleAbs()函数将其转回原来的uint8形式。否则将无法显示图像,而只是一副灰色的窗口。
在输入数组的每个元素上,函数convertScaleAbs依次执行三个操作:缩放,获取绝对值,转换为无符号的8位类型
convertScaleAbs(src[, dst[, alpha[, beta]]]) -> dst
Scharr(src, ddepth, dx, dy[, dst[, scale[, delta[, borderType]]]]) -> dst
参数与Sobel()基本一致
Scharr()函数提供了比标准Sobel函数更精确的计算结果。
def sobel_gradient(image):
"""sobel算子梯度滤波(一阶导数)"""
grad_x = cv.Sobel(image, cv.CV_32F, 1, 0) # x方向的
grad_y = cv.Sobel(image, cv.CV_32F, 0, 1) # y方向的
# grad_x = cv.Scharr(image, cv.CV_32F, 1, 0) # 采用Scharr边缘更突出
# grad_y = cv.Scharr(image, cv.CV_32F, 0, 1)
gradx = cv.convertScaleAbs(grad_x) # 由于算完的图像有正有负,所以对其取
绝对值并转换回uint8
grady = cv.convertScaleAbs(grad_y)
# 计算两个图像的权值和,dst = src1*alpha + src2*beta + gamma
gradxy = cv.addWeighted(gradx, 0.5, grady, 0.5, 0)
cv.imshow("gradx", gradx)
cv.imshow("grady", grady)
cv.imshow("gradient", gradxy)
结果:
参考链接:
计算了由关系
Δ s r c = δ 2 s r c δ x 2 + δ 2 s r c δ y 2 \Delta src = \frac{\delta^2 src}{\delta x^2}+\frac{\delta^2 src}{\delta y^2} Δsrc=δx2δ2src+δy2δ2src
给出的图像的拉普拉斯图,它是每一阶导数通过Sobel算子计算。如果ksize = 1,然后使用以下内核用于过滤:
k e r n e l = [ 0 1 0 1 − 4 1 0 1 0 ] / / 4 邻 域 式 , 默 认 的 是 这 个 kernel = \left[ {\begin{matrix} 0 & 1 & 0 \\ 1 & -4 & 1 \\ 0 & 1 & 0 \\ \end{matrix}} \right]//4邻域式,默认的是这个 kernel=⎣⎡0101−41010⎦⎤//4邻域式,默认的是这个
补充:
k e r n e l = [ 1 1 1 1 − 8 1 1 1 1 ] / / 8 邻 域 式 kernel = \left[ {\begin{matrix} 1 & 1 & 1 \\ 1 & -8 & 1 \\ 1 & 1 & 1 \\ \end{matrix}} \right]//8邻域式 kernel=⎣⎡1111−81111⎦⎤//8邻域式
Laplacian滤波器
Laplacian(src, ddepth[, dst[, ksize[, scale[, delta[, borderType]]]]]) -> dst
def laplace_gradient(image):
"""Laplacian算子梯度滤波(二阶导数)"""
dst = cv.Laplacian(image,cv.CV_32F)
lpls = cv.convertScaleAbs(dst)
cv.imshow("laplace_gradient", lpls)
第一步:使用高斯滤波器进行滤波,去除噪音点
第二步:使用sobel算子,计算出每个点的梯度大小和梯度方向
第三步:使用非极大值抑制(只有最大的保留),消除边缘检测带来的杂散效应
第四步:应用双阈值(磁滞阈值),来确定真实和潜在的边缘
第五步:通过抑制弱边缘来完成最终的边缘检测
canny边缘检测
Canny(image, threshold1, threshold2[, edges[, apertureSize[, L2gradient]]]) -> edges
注意: 一般来说,threshold1 : threshold2 = 1 : 3 / 1 : 2 (我也不知道为什么)
def canny(image):
"""canny边缘提取"""
blurred = cv.GaussianBlur(image, (3, 3), 0)
gray = cv.cvtColor(blurred, cv.COLOR_BGR2GRAY)
grad_x = cv.Sobel(gray, cv.CV_16SC1, 1, 0)
grad_y = cv.Sobel(gray, cv.CV_16SC1, 0, 1)
# image:要检测的图像,threshold1:阈值1(最小值),threshold2:阈值2(最大值),使用此参数进行明显的边缘检测,
# canny_output2 = cv.Canny(grad_x, grad_y, 30, 150)
canny_output1 = cv.Canny(gray, 50, 150) # 也可以直接传入gray
cv.imshow("image", image)
cv.imshow("Canny", canny_output1)
# cv.imshow("Canny2", canny_output2)
参考链接:
任何一条线都可以用(ρ,θ)这两个术语表示。因此,首先创建2D数组或累加器(以保存两个参数的值),并将其初始设置为0。让行表示ρ,列表示θ。阵列的大小取决于所需的精度。假设您希望角度的精度为1度,则需要180列。对于ρ,最大距离可能是图像的对角线长度。因此,以一个像素精度为准,行数可以是图像的对角线长度。
考虑一个100x100的图像,中间有一条水平线。取直线的第一点。您知道它的(x,y)值。现在在线性方程式中,将值θ= 0,1,2,… 180放进去,然后检查得到ρ。对于每对(ρ,θ),在累加器中对应的(ρ,θ)单元格将值增加1。所以现在在累加器中,单元格(50,90)= 1以及其他一些单元格。
现在,对行的第二个点。执行与上述相同的操作。递增(ρ,θ)对应的单元格中的值。这次,单元格(50,90)=2。实际上,您正在对(ρ,θ)值进行投票。您对线路上的每个点都继续执行此过程。在每个点上,单元格(50,90)都会增加或投票,而其他单元格可能会或可能不会投票。这样一来,最后,单元格(50,90)的投票数将最高。因此,如果在累加器中搜索最大票数,则将获得(50,90)值,该值表示该图像中的一条线与原点的距离为50,角度为90度。
使用标准Hough变换在二值图像中查找线条
HoughLines(image, rho, theta, threshold[, lines[, srn[, stn[, min_theta[, max_theta]]]]]) -> lines
def line_detection(image):
"""霍夫变换直线检测"""
gray = cv.cvtColor(image, cv.COLOR_BGR2GRAY)
edges = cv.Canny(gray, 50, 150, apertureSize=3)
# cv2.HoughLines()返回值就是(ρ,θ)。ρ 的单位是像素,θ 的单位是弧度。
# 这个函数的第一个参数是一个二值化图像,所以在进行霍夫变换之前要首先进行二值化,或者进行 Canny 边缘检测。
# 第二和第三个值分别代表 ρ 和 θ 的精确度。第四个参数是阈值,只有累加其中的值高于阈值时才被认为是一条直线,
# 也可以把它看成能 检测到的直线的最短长度(以像素点为单位)。
lines = cv.HoughLines(edges, 1, np.pi / 180, 200)
# print(lines):
for line in lines:
# print(type(lines))
rho, theta = line[0]
a = np.cos(theta)
b = np.sin(theta)
x0 = a * rho
y0 = b * rho
x1 = int(x0 + 1000 * (-b))
y1 = int(y0 + 1000 * (a))
x2 = int(x0 - 1000 * (-b))
y2 = int(y0 - 1000 * (a))
cv.line(image, (x1, y1), (x2, y2), (0, 0, 255), 2)
cv.imshow("line_detection", image)
结果:
注意: 当检测不到图片时,lines为None,这时候程序会报错。建议加上if判断。
概率霍夫变换对基本霍夫变换算法进行了一些修正,是霍夫变换算法的优化。它没有考虑所有的点。相反,它只需要一个足以进行线检测的随机点子集即可。
为了更好地判断直线(线段),概率霍夫变换算法还对选取直线的方法作了两点改进:
所接受直线的最小长度。如果有超过阈值个数的像素点构成了一条直线,但是这条直线很短,那么就不会接受该直线作为判断结果,而认为这条直线仅仅是图像中的若干个像素点恰好随机构成了一种算法上的直线关系而已,实际上原图中并不存在这条直线。
接受直线时允许的最大像素点间距。如果有超过阈值个数的像素点构成了一条直线,但是这组像素点之间的距离都很远,就不会接受该直线作为判断结果,而认为这条直线仅仅是图像中的若干个像素点恰好随机构成了一种算法上的直线关系而已,实际上原始图像中并不存在这条直线。
用概率Hough变换在二值图像中查找线段
HoughLinesP(image, rho, theta, threshold[, lines[, minLineLength[, maxLineGap]]]) -> lines
def line_detection_possible(image):
"""概率霍夫变换直线检测"""
gray = cv.cvtColor(image, cv.COLOR_BGR2GRAY)
edges = cv.Canny(gray, 50, 150, apertureSize=3)
lines = cv.HoughLinesP(edges, 1, np.pi / 180, 100, minLineLength=100, maxLineGap=10)
for line in lines:
x1, y1, x2, y2 = line[0]
cv.line(image, (x1, y1), (x2, y2), (0, 255, 0), 2)
cv.imshow('line_detection_possible', image)
参考链接:
使用Hough变换在灰度图像中查找圆
HoughCircles(image, method, dp, minDist[, circles[, param1[, param2[, minRadius[, maxRadius]]]]]) -> circles
def circle_detection(image):
"""霍夫圆检测"""
# 均值迁移滤波,因为霍夫圆检测对噪声敏感,sp,sr为空间域核与像素范围域核半径
dst = cv.pyrMeanShiftFiltering(image, 10, 100)
gray = cv.cvtColor(dst, cv.COLOR_BGR2GRAY)
circles = cv.HoughCircles(gray, cv.HOUGH_GRADIENT, 1, 20, param1=50, param2=30, minRadius=0, maxRadius=0)
circles = np.uint16(np.around(circles))
# print(circles.shape)
for i in circles[0, :]: # draw the outer circle
cv.circle(image, (i[0], i[1]), i[2], (0, 255, 0), 2) # 画圆
cv.circle(image, (i[0], i[1]), 2, (0, 0, 255), 3) # 画圆心
cv.imshow('detected circles', image)
结果:
注意: 霍夫圆检测对噪声敏感,使用霍夫圆检测之前,需要先对图片进行滤波降噪处理。
参考链接:
在二值图像中查找轮廓
findContours(image, mode, method[, contours[, hierarchy[, offset]]]) -> contours, hierarchy
绘制轮廓轮廓或填充轮廓
drawContours(image, contours, contourIdx, color[, thickness[, lineType[, hierarchy[, maxLevel[, offset]]]]]) -> image
def canny(image):
"""canny边缘提取"""
blurred = cv.GaussianBlur(image, (3, 3), 0)
gray = cv.cvtColor(blurred, cv.COLOR_BGR2GRAY)
grad_x = cv.Sobel(gray, cv.CV_16SC1, 1, 0)
grad_y = cv.Sobel(gray, cv.CV_16SC1, 0, 1)
# image:要检测的图像,threshold1:阈值1(最小值),threshold2:阈值2(最大值),使用此参数进行明显的边缘检测,
# canny_output2 = cv.Canny(grad_x, grad_y, 30, 150)
canny_output1 = cv.Canny(gray, 50, 150) # 也可以直接传入gray
return canny_output1
def contours(image):
"""轮廓查找"""
binary = canny(image)
contours, hierarchy = cv.findContours(binary, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
for i, contour in enumerate(contours):
# 函数 cv2.drawContours() 可以被用来绘制轮廓。它可以根据你提供的边界点绘制任何形状。
# 它的第一个参数是原始图像,第二个参数是轮廓,一个 Python 列表。
# 第三个参数是轮廓的索引(在绘制独立轮廓是很有用,当设 置为 -1 时绘制所有轮廓)。
# 接下来的参数是轮廓的颜色和厚度等。
cv.drawContours(image, contours, i, (0, 0, 255), 2) # 2为像素大小,-1时填充轮廓
print(i)
cv.imshow("detect contours", image)
def image_contour(image):
"""轮廓查找并描点"""
gray = cv.cvtColor(image, cv.COLOR_BGR2GRAY)
ret, binary = cv.threshold(gray, 0, 255, cv.THRESH_BINARY_INV | cv.THRESH_OTSU) # 图像二值化
print("threshold value: %s" % ret) # 输出阈值
# cv.imshow("binary image", binary)
contours, hierarchy = cv.findContours(binary, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
for i, contour in enumerate(contours):
cv.drawContours(image, contours, i, (0, 0, 255), 2) # 用红色线条画出轮廓
epsilon = 0.01 * cv.arcLength(contour, True)
approx = cv.approxPolyDP(contour, epsilon, True)
cv.drawContours(image, approx, -1, (255, 0, 0), 10)
cv.imshow("contour_approx", image)
结果:
注意:
参考链接:
此函数利用格林公式计算轮廓的面积。对于具有自交点的轮廓,该函数几乎肯定会给出错误的结果。
contourArea(contour[, oriented]) -> retval
计算轮廓周长或曲线长度
arcLength(curve, closed) -> retval
用一个最小的矩形,把找到的形状包起来
boundingRect(array) -> retval
返回四个值,分别是x,y,w,h;
计算多边形或栅格化形状的所有三阶矩
moments(array[, binaryImage]) -> retval
def image_measure(image):
"""对象测量"""
gray = cv.cvtColor(image, cv.COLOR_BGR2GRAY)
ret, binary = cv.threshold(gray, 0, 255, cv.THRESH_BINARY_INV | cv.THRESH_OTSU) # 图像二值化
# print("threshold value: %s" % ret)
# cv.imshow("binary image", binary)
contours, hierarchy = cv.findContours(binary, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
for i, contour in enumerate(contours):
cv.drawContours(image, contours, i, (0, 255, 255), 1) # 用黄色线条画出轮廓
area = cv.contourArea(contour) # 计算轮廓面积
print("contour area:", area)
# 轮廓周长,第二参数可以用来指定对象的形状是闭合的(True),还是打开的(一条曲线)。
perimeter = cv.arcLength(contour, True)
print("contour perimeter:", perimeter)
x, y, w, h = cv.boundingRect(contour) # 用矩形框出轮廓
cv.rectangle(image, (x, y), (x + w, y + h), (0, 0, 255), 2) # 画出矩形
rate = min(w, h) / max(w, h) # 计算矩阵宽高比
print("rectangle rate", rate)
mm = cv.moments(contour) # 函数 cv2.moments() 会将计算得到的矩以一个字典的形式返回
print(mm)
# 计算出对象的重心
cx = mm['m10'] / mm['m00']
cy = mm['m01'] / mm['m00']
cv.circle(image, (np.int(cx), np.int(cy)), 2, (0, 255, 255), -1) # 用实心圆画出重心
cv.imshow("measure_object", image)
结果:
参考链接:
与卷积核对应的原图像的像素值中只要有一个是1,中心元素的像素值就是1。会增加图像中的白色区域(前景)。一般在去噪声时先用腐蚀再用膨胀。因为腐蚀在去掉白噪声的同时,也会使前景对象变小。所以我们再对他进行膨胀。这时噪声已经被去除了,不会再回来了,但是前景还在并会增加。
作用:
使用特定的结构元素膨胀图像
dilate(src, kernel[, dst[, anchor[, iterations[, borderType[, borderValue]]]]]) -> dst
对图像二值化并膨胀
def dilate(image):
"""膨胀"""
gray = cv.cvtColor(image, cv.COLOR_BGR2GRAY)
ret, binary = cv.threshold(gray, 0, 255, cv.THRESH_BINARY | cv.THRESH_OTSU) # 二值化图像
cv.imshow("binary", binary)
# 构造5×5的结构元素,可选十字形、菱形、方形和X型等
kernel = cv.getStructuringElement(cv.MORPH_RECT, (5, 5)) # 构造5×5的方形结构元素
dst = cv.dilate(binary, kernel=kernel) # 膨胀
cv.imshow("dilate", dst)
结果
注意: 二值化图像的膨胀和腐蚀要注意是白色还是黑色作为前景
对彩图进行膨胀
def dilate_color(image):
"""彩图膨胀"""
kernel = cv.getStructuringElement(cv.MORPH_RECT, (5, 5)) # 构造5×5的方形结构元素
dst = cv.dilate(image, kernel=kernel) # 膨胀
cv.imshow("dilate", dst)
卷积核沿着图像滑动,如果与卷积核对应的原图像的所有像素值都是1,那么中心元素就保持原来的像素值,否则就变为零。 根据卷积核的大小靠近前景的所有像素都会被腐蚀掉(变为0),所以前景物体会变小,整幅图像的白色区域会减少。
作用:
使用特定的结构元素腐蚀图像
erode(src, kernel[, dst[, anchor[, iterations[, borderType[, borderValue]]]]]) -> dst
对图像二值化并腐蚀
def erode(image):
"""腐蚀"""
gray = cv.cvtColor(image, cv.COLOR_BGR2GRAY)
ret, binary = cv.threshold(gray, 0, 255, cv.THRESH_BINARY | cv.THRESH_OTSU) # 二值化图像
cv.imshow("binary", binary)
# 构造5×5的结构元素,可选十字形、菱形、方形和X型等
kernel = cv.getStructuringElement(cv.MORPH_RECT, (5, 5)) # 构造5×5的方形结构元素
dst = cv.erode(binary, kernel=kernel) # 腐蚀
cv.imshow("erode", dst)
结果
对彩图进行腐蚀
def erode_color(image):
"""彩图腐蚀"""
kernel = cv.getStructuringElement(cv.MORPH_RECT, (5, 5)) # 构造5×5的方形结构元素
dst = cv.erode(image, kernel=kernel) # 腐蚀
cv.imshow("erode", dst)
参考链接:
执行形态学操作
morphologyEx(src, op, kernel[, dst[, anchor[, iterations[, borderType[, borderValue]]]]]) -> dst
先进行腐蚀再进行膨胀就叫做开运算,它被用来去除噪声。
开操作
def open_image(image):
"""开操作"""
# print(image.shape)
gray = cv.cvtColor(image, cv.COLOR_BGR2GRAY)
ret, binary = cv.threshold(gray, 0, 255, cv.THRESH_BINARY_INV | cv.THRESH_OTSU) # 二值化图像
cv.imshow("binary", binary)
kernel = cv.getStructuringElement(cv.MORPH_RECT, (5, 5)) # 构造5×5的方形结构元素
dst = cv.morphologyEx(binary, cv.MORPH_OPEN, kernel=kernel) # 执行形态学操作(此时为开操作)
cv.imshow("open_image", dst)
先膨胀再腐蚀。它经常被用来填充前景物体中的小洞,或者前景物体上的小黑点。
闭操作
def close_image(image):
"""闭操作"""
# print(image.shape)
gray = cv.cvtColor(image, cv.COLOR_BGR2GRAY)
ret, binary = cv.threshold(gray, 0, 255, cv.THRESH_BINARY | cv.THRESH_OTSU) # 二值化图像
cv.imshow("binary", binary)
kernel = cv.getStructuringElement(cv.MORPH_RECT, (5, 5)) # 构造5×5的方形结构元素
dst = cv.morphologyEx(binary, cv.MORPH_CLOSE, kernel=kernel) # 执行形态学操作(此时为闭操作)
cv.imshow("close_image", dst)
原图像与开操作之间的差值图像
d s t = t o p h a t ( s r c , r l r m e n t ) = s r c − o p e n ( s r c , r l r m e n t ) dst = tophat(src,rlrment) = src - open(src,rlrment) dst=tophat(src,rlrment)=src−open(src,rlrment)
因为开运算带来的结果是放大了裂缝或者局部低亮度的区域,因此,从原图中减去开运算后的图,得到的效果图突出了比原图轮廓周围的区域更明亮的区域,且这一操作和选择的核的大小相关。
顶帽运算往往用来分离比邻近点亮一些的斑块。当一幅图像具有大幅的背景的时候,而微小物品比较有规律的情况下,可以使用顶帽运算进行背景提取。
def tophat_image(image):
"""顶帽操作"""
gray = cv.cvtColor(image, cv.COLOR_BGR2GRAY)
kernel = cv.getStructuringElement(cv.MORPH_RECT, (5, 5)) # 构造5×5的方形结构元素
dst = cv.morphologyEx(gray, cv.MORPH_TOPHAT, kernel=kernel) # 执行形态学操作(此时为顶帽操作)
cv.imshow("tophat_image", dst)
发现原图右下角的字体与背景的亮度都有些高,所以会出现这种情况,现在想让字体与背景分离出来,我们可以用顶帽操作对前景进行明亮化。
因此先进行顶帽再二值化
def comprehensive_example(image):
gray = cv.cvtColor(image, cv.COLOR_BGR2GRAY)
cv.imshow("gray", gray)
kernel = cv.getStructuringElement(cv.MORPH_RECT, (30, 30)) # 构造30×30的方形结构元素
dst = cv.morphologyEx(gray, cv.MORPH_TOPHAT, kernel=kernel) # 执行形态学操作(此时为顶帽操作)
cv.imshow("tophat_image", dst)
ret, binary = cv.threshold(dst, 0, 255, cv.THRESH_BINARY_INV | cv.THRESH_OTSU) # 二值化图像
cv.imshow("binary", binary)
闭操作与原图像之间的差值图像
d s t = b l a c k h a t ( s r c , r l r m e n t ) = c l o s e ( s r c , r l r m e n t ) − s r c dst = blackhat(src,rlrment) = close(src,rlrment) - src dst=blackhat(src,rlrment)=close(src,rlrment)−src
黑帽运算后的效果图突出了比原图轮廓周围的区域更暗的区域,且这一操作和选择的核的大小相关。
所以,黑帽运算用来分离比邻近点暗一些的斑块。可以得到轮廓效果图。
def blackhat_image(image):
"""黑帽操作"""
gray = cv.cvtColor(image, cv.COLOR_BGR2GRAY)
kernel = cv.getStructuringElement(cv.MORPH_RECT, (5, 5)) # 构造5×5的方形结构元素
dst = cv.morphologyEx(gray, cv.MORPH_BLACKHAT, kernel=kernel) # 执行形态学操作(此时为黑帽操作)
cv.imshow("blackhat_image", dst)
本质上就是对图像的边缘提取,也可以说是膨胀的结果减去腐蚀的结果
def gardient_image(image):
"""形态学梯度操作"""
gray = cv.cvtColor(image, cv.COLOR_BGR2GRAY)
kernel = cv.getStructuringElement(cv.MORPH_RECT, (5, 5)) # 构造5×5的方形结构元素
dst = cv.morphologyEx(gray, cv.MORPH_GRADIENT, kernel=kernel) # 执行形态学操作(此时为形态学梯度操作)
cv.imshow("blackhat_image", dst)
参考链接:
任何灰度图像都可以看作是一个地形表面,其中高强度表示山峰,低强度表示山谷。你开始用不同颜色的水(标签)填充每个孤立的山谷(局部最小值)。随着水位的上升,根据附近的山峰(坡度),来自不同山谷的水明显会开始合并,颜色也不同。为了避免这种情况,你要在水融合的地方建造屏障。你继续填满水,建造障碍,直到所有的山峰都在水下。然后你创建的屏障将返回你的分割结果。
但是这种方法会由于图像中的噪声或其他不规则性而产生过度分割的结果。因此OpenCV实现了一个基于标记的分水岭算法,你可以指定哪些是要合并的山谷点,哪些不是。这是一个交互式的图像分割。我们所做的是给我们知道的对象赋予不同的标签。用一种颜色(或强度)标记我们确定为前景或对象的区域,用另一种颜色标记我们确定为背景或非对象的区域,最后用0标记我们不确定的区域。这是我们的标记。然后应用分水岭算法。然后我们的标记将使用我们给出的标签进行更新,对象的边界值将为-1。
使用分水岭算法实现基于标记的图像分割
watershed(image, markers) -> markers
def watershed_image(image):
"""分水岭算法"""
# 图像二值化
blurred = cv.pyrMeanShiftFiltering(image, 10, 50) # 均值迁移滤波
gray = cv.cvtColor(blurred, cv.COLOR_BGR2GRAY) # 转换成灰度图
# cv.imshow("gray", gray)
ret, binary = cv.threshold(gray, 0, 255, cv.THRESH_BINARY_INV | cv.THRESH_OTSU) # 图像二值化
# cv.imshow("binary", binary)
# 去除噪声
kernel = cv.getStructuringElement(cv.MORPH_RECT, (3, 3)) # 构造25×25的方形结构元素
opening = cv.morphologyEx(binary, cv.MORPH_OPEN, kernel=kernel, iterations=2) # 开操作(需要去除图像中的任何白点噪声),迭代次数2
# cv.imshow("noise removal", opening)
# 确定背景区域sure_bg
sure_bg = cv.dilate(opening, kernel, iterations=3) # 腐蚀,迭代次数3,会去除边界像素
cv.imshow("sure_bg", sure_bg)
# 寻找前景区域sure_fg
""" 距离变换的基本含义是计算一个图像中非零像素点到最近的零像素点的距离,也就是到零像素点的最短距离
一个最常见的距离变换算法就是通过连续的腐蚀操作来实现,腐蚀操作的停止条件是所有前景像素都被完全腐蚀。
这样根据腐蚀的先后顺序,我们就得到各个前景像素点到前景中心像素点的距离。根据各个像素点的距离值,设置
为不同的灰度值。这样就完成了二值图像的距离变换。
cv2.distanceTransform(src, distanceType, maskSize)
distanceType为距离类型CV_DIST_L1, CV_DIST_L2 , CV_DIST_C;maskSize为距离转换掩码的大小
"""
dist_transform = cv.distanceTransform(opening, cv.DIST_L2, 5) # 距离变换
dist_output = cv.normalize(dist_transform, 0, 1.0, cv.NORM_MINMAX) # 矩阵归一化,主要是为了显示出dist_output
cv.imshow("dist_transform", dist_output*50) # dist_output不乘50看不出来
ret, sure_fg = cv.threshold(dist_transform, 0.7 * dist_transform.max(), 255, 0) # 图像二值化
cv.imshow("sure_fg", sure_fg)
# 找到未知的区域unknown
sure_fg = np.uint8(sure_fg)
unknown = cv.subtract(sure_bg, sure_fg) # 从sure_bg区域中减去sure_fg区域来获得unknown
cv.imshow("unknown", unknown)
# 类别标记
ret, markers1 = cv.connectedComponents(sure_fg)
print(ret) # 计算数量,但此时会把图像边框也算进去,因此ret会多1
# print(markers1)
# 为所有的标记加1,保证背景是0而不是1
markers = markers1 + 1
# print(markers)
# 现在让所有的未知区域为0
markers[unknown == 255] = 0
# 使用分水岭算法
markers3 = cv.watershed(image, markers=markers) # 边界区域将被修改标记为-1
image[markers3 == -1] = [0, 0, 255] # 边界区域画红色
# print(markers3)
cv.imshow("result", image)
参考链接:
使用Haar分类器进行面部检测
Haar特征分类器对象检测技术
它是基于机器学习的,通过使用大量的正负样本图像训练得到一个cascade_function,最后再用它来做对象检测
如果你想实现自己的面部检测分类器,需要大量的正样本图像(面部图像)和负样本图像(不含面部的图像)来训练分类器。可参考https://docs.opencv.org/2.4/doc/user_guide/ug_traincascade.html,这里不做介绍,现在我们利用OpenCV已经训练好的分类器,直接利用它来实现面部和眼部检测。
主要步骤:
重要方法分析:def detectMultiScale(self, image, scaleFactor=None, minNeighbors=None, minSize=None, maxSize=None)
原理:检测输入图像在不同尺寸下可能含有的目标对象
#minSize – Minimum possible object size. Objects smaller than that are ignored.
#maxSize – Maximum possible object size. Objects larger than that are ignored.
入参:
1)image:输入的图像
2)scaleFactor:比例因子,图像尺寸每次减少的比例,要大于1,这个需要自己手动调参以便获得想要的结果
3)minNeighbors:最小附近像素值,在每个候选框边缘最小应该保留多少个附近像素
4)minSize,maxSize:最小可能对象尺寸,所检测的结果小于该值会被忽略。最大可能对象尺寸,所检测的结果大于该值会被忽略
返回:若干个包含对象的矩形区域
def face_detection(image):
"""人脸识别和人眼识别"""
gray = cv.cvtColor(image, cv.COLOR_BGR2GRAY) # 在灰度图上进行识别
# 人脸xml分类器
face_detector = cv.CascadeClassifier(r"C:\Users\22164\AppData\Local\Programs\Python\Python39\Lib\site-packages"
r"\cv2\data\haarcascade_frontalface_alt_tree.xml")
# 人眼xml分类器
eyes_detector = cv.CascadeClassifier(r"C:\Users\22164\AppData\Local\Programs\Python\Python39\Lib\site-packages"
r"\cv2\data\haarcascade_eye.xml")
faces = face_detector.detectMultiScale(gray, 1.01, 5) # 检测目标对象(人脸)
for x, y, w, h in faces:
img = cv.rectangle(image, (x, y), (x + w, y + h), (0, 0, 255), 2) # 红色矩形框框出人脸
roi_gray = gray[y:y + h, x:x + w]
roi_color = img[y:y + h, x:x + w]
# 在人脸识别基础上进行人眼识别
eyes = eyes_detector.detectMultiScale(roi_gray, 1.3, 5) # 检测目标对象(人眼)
for ex, ey, ew, eh in eyes:
cv.rectangle(roi_color, (ex, ey), (ex + ew, ey + ey), (0, 255, 0), 2) # 绿色矩形框框出人眼
cv.imshow("result", image)
def video_detection():
"""打开摄像头进行人脸和人眼检测"""
capture = cv.VideoCapture(0)
cv.namedWindow("result", cv.WINDOW_AUTOSIZE)
while True:
ret, frame = capture.read()
frame = cv.flip(frame, 1)
face_detection(frame)
c = cv.waitKey(10)
if c == 27:
break
步骤:
def verification_code_recognition(image):
"""验证码识别"""
# 二值化图像
gray = cv.cvtColor(image, cv.COLOR_BGR2GRAY) # 转灰度图
ret, binary = cv.threshold(gray, 0, 255, cv.THRESH_BINARY_INV | cv.THRESH_OTSU) # 二值化
cv.imshow("binary", binary)
# 形态学操作
kernel = cv.getStructuringElement(cv.MORPH_RECT, (4, 4))
bin_af = cv.morphologyEx(binary, cv.MORPH_OPEN, kernel=kernel)
cv.imshow("bin_af", bin_af)
textImage = Image.fromarray(bin_af)
text = tess.image_to_string(textImage)
print("The result:", text)
报错:raise TesseractNotFoundError() pytesseract.pytesseract.TesseractNotFoundError: tesseract is not installed or it’s not in your path
不同系统采用不同策略:
参考链接:
参考链接:
Python使用的是“/”,而windows使用的是“\”。
解决方案:可以使用在路径前使用“r”完成转义
例如:
face_detector = cv.CascadeClassifier(r"C:\Users\22164\AppData\Local\Programs\Python\Python39\Lib\site-packages"
r"\cv2\data\haarcascade_frontalface_alt_tree.xml")
eyes_detector = cv.CascadeClassifier(r"C:\Users\22164\AppData\Local\Programs\Python\Python39\Lib\site-packages"
r"\cv2\data\haarcascade_eye.xml")
原因:未知
解决方案:未知
数学符号输入