OpenCV-笔记

文章目录

教程

python+opencv3.3视频教学基础入门

求知讲堂2020python+人工智能 99天完整版

OpenCV贾志刚学习笔记

文档

Python标准库

Python标准库

OpenCV官方文档

OpenCV官方文档

OpenCV中文文档

OpenCV中文文档

博客

matplotlib官方文档

matplotlib官方文档

环境配置

Python

下载

python下载链接

安装

注意:勾选add to path,这样可以添加Python到环境变量里
最终要添加的路径:

  • Python\Python39
  • Python\Python39\Lib
  • Python\Python39\Scripts

Pycharm

下载

Pycharm下载链接

安装

注意:全部勾选这样可以添加环境变量

激活

使用教育邮箱可以获得专业版

汉化

汉化教程链接

步骤如下图:
请添加图片描述

文件模板

pycharm使用笔记3-自动生成文件注释和函数注释

OpenCV

下载

命令行里执行
pip install opencv-python

错误记录

pip指令如果提示需要更新pip包,如下图所示:
请添加图片描述

可以使用指令
python3 -m pip install --user --upgrade pip

检查是否安装成功

  1. 在命令行里执行Python指令,到python的环境下
  2. 执行import cv2,如果不报错就没问题

可以参考链接:
python实现opencv学习一:安装、环境配置、工具

matplotlib

下载

命令行里执行
pip install matplotlib

Pycharm安装第三方包

  1. 直接cmd用命令行输入:pip install +要安装的第三方库,
    比如pip install requests
    安装完库之后,可能会存在找不到包路径的情况,可以让Pycharm直接使用Python安装路径下的site-packages
    在这里插入图片描述

  2. 直接用pycharm安装
    File-Settings,点击Project: 在Project Interpreter里点击右上角的+来安装
    在这里插入图片描述


图像处理

图像的加载与保存

文件路径

参考链接:

OpenCV的坐标方向

  • 坐标原点在图像左上角
  • 行数rows其实对应于坐标轴上的y,即表示的是图像的高度竖直方向
  • 列数cols对应于坐标轴上的x,即表示的是图像的宽度水平方向

参考链接:

cv2.imread

读取一张图片

def imread(filename: Any,flags: Any = None) -> retval
  • flag = -1,8位深度,原通道
  • flag = 0,8位深度,1通道
  • flag = 1,8位深度,3通道
  • flag = 2,原深度, 1通道
  • flag = 3,原深度, 3通道
  • flag = 4,8位深度,3通道

cv2.imwrite

保存一张图片

imwrite(filename, img[, params]) -> retval

cv2.namedWindow

创建GUI

def namedWindow(winname: Any,flags: Any = None) -> None  

cv2.imshow

显示图片

def imshow(winname: Any,mat: Any) -> None 

cv2.waitkey

在一个给定的时间内(单位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()

copy

复制一份图片

注意: 使用图像前可以复制一份

shape

返回图片的高,宽,通道数

  • image.shape[0], 图片垂直尺寸
  • image.shape[1], 图片水平尺寸
  • image.shape[2], 图片通道数

视频的读取与保存

cv2.VideoCapture

视频文件的读取

参数是0,表示打开笔记本的内置摄像头,参数是视频文件路径则打开视频

cv2.flip

图像翻转

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

计算程序运行时间

cv2.getTickCount()

cv.getTickFrequency()

示例

    # 计算gaussian_noise的运行时间
    t1 = cv.getTickCount()
    gaussian_noise(src)
    t2 = cv.getTickCount()
    time = (t2 - t1)/cv.getTickFrequency()
    print("time consume: %s"%(time * 1000))  # 单位ms

Numpy数组操作

numpy.zeros

返回一个全为0的数组

def zeros(shape: Union[int, Iterable, tuple[int]],
          dtype: Optional[object] = None,
          order: Optional[str] = 'C',
          *args: Any,
          **kwargs: Any) -> ndarray

numpy.ones

同numpy.zeros

numpy.eyse

返回一个对角线全为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)

色彩空间

Gray灰度图

GARY色彩空间(灰度图像)通常指8位灰度图,具有256个灰度级,像素值的范围是[0,255]。不同数值表示不同程度的灰色。像素值越低,灰色越深。0表示纯黑色,255表示纯白色。注意这个值不是RGB里的任何一个元素。
GARY色彩空间为单通道,所以通常用二维数组表示一幅灰度图像。
其中二值图像:只有0和255两种像素值的灰度图像

RBG

OpenCV中通道排序为BGR

  • B(Blue) 蓝色 取值范围:[0,255]
  • G(Green) 绿色 取值范围:[0,255]
  • R(Red) 红色 取值范围:[0,255]

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更容易跟踪某种颜色的物体,常用于分割指定颜色的物体。

HSV

OpenCV中通道排序为HSV

  • H(Hue) 色调,色相 取值范围:[0,179](归一化处理了,为了能用一个字节存下)
  • S(Saturation) 饱和度,色彩纯净度 取值范围:[0,255]
  • V(Value) 明度 取值范围:[0,255]

HSV是一种将RGB色彩空间中的点在倒圆锥体中的表示方法。色相是色彩的基本属性,就是平常说的颜色的名称,如红色、黄色等。饱和度(S)是指色彩的纯度,越高色彩越纯,低则逐渐变灰,取0-100%的数值。明度(V),取0-max(计算机中HSV取值范围和存储的长度有关)。HSV颜色空间可以用一个圆锥空间模型来描述。圆锥的顶点处,V=0,H和S无定义,代表黑色。圆锥的顶面中心处V=max,S=0,H无定义,代表白色。

在这里插入图片描述

在图像处理中使用较多的是HSV颜色空间,它比RGB更接近人们对彩色的感知经验。非常直观地表达颜色的色调、鲜艳程度和明暗程度,方便进行颜色的对比。

在HSV 颜色空间下,比BGR更容易跟踪某种颜色的物体,常用于分割指定颜色的物体。

HLS

  • H(Hue) 色相
  • L(Lightness) 亮度
  • S(Saturation) 饱和度

HSL和HSV稍有区别,一般我们常用的是HSV模型。

HLS中的L分量为亮度,亮度为100,表示白色,亮度为0,表示黑色;HSV 中的 V 分量为明度,明度为100,表示光谱色,明度为0,表示黑色。
在这里插入图片描述
为什么HSV有圆锥和圆柱两种定义,参考链接如下:

参考链接:

色彩空间的转换

BGR到HSV色彩转换表
在这里插入图片描述

cv2.cvtColor

  • src:原图像
  • code:color转化代码
  • dst:输出图像
  • dstCn:输出通道
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模块,需要导入这个包

色彩阀值化处理

cv2.inRange

将在两个阈值内的像素值设置为白色(255),而不在阈值区间内的像素值设置为黑色(0)

inRange(src, lowerb, upperb[, dst]) -> dst
  • hsv指的是原图
  • lower_red指的是图像中低于这个lower_red的值,图像值变为0
  • upper_red指的是图像中高于这个upper_red的值,图像值变为0

注意:
在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

结果:
在这里插入图片描述

通道的分离与合并

cv2.split

分离通道

split(m[, mv]) -> mv

cv2.merge

合并通道

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)

像素运算

注意:
需要两张图片大小格式完全一样

cv2.add

两张图片相加

add(src1, src2[, dst[, mask[, dtype]]]) -> dst

注意:
大于255的使用255计数

cv2.subtract

两张图片相减

subtract(src1, src2[, dst[, mask[, dtype]]]) -> dst

cv2.multiply

两张图片相乘(点乘)

multiply(src1, src2[, dst[, scale[, dtype]]]) -> dst

图1:
在这里插入图片描述

图2:
在这里插入图片描述
相乘结果:
在这里插入图片描述
注意:
因为Linux这张图是抗锯齿的,边缘经过光滑处理。黑白图像边缘并不完全是黑白的。

cv2.divide

两张图片相除

divide(src1, src2[, dst[, scale[, dtype]]]) -> dst

cv2.bitwise_and

对像素的二进制数据进行“与”操作

bitwise_and(src1, src2[, dst[, mask]]) -> dst

cv2.bitwise_or

对像素的二进制数据进行“或”操作

bitwise_or(src1, src2[, dst[, mask]]) -> dst

cv2.bitwise_not

对像素的二进制数据进行“非”操作

bitwise_not(src[, dst[, mask]]) -> dst

注意:
bitwise_not()只需要三个参数

cv2.bitwise_xor

对像素的二进制数据进行“异或”操作

bitwise_xor(src1, src2[, dst[, mask]]) -> dst

掩膜

图像掩膜,是用选定的图像、图形或物体,对处理的图像(全部或局部)进行遮挡,来控制图像处理的区域或处理过程。

数字图像处理中,掩模为二维矩阵数组,有时也用多值图像,图像掩模主要用于:

  1. 提取感兴趣区,用预先制作的感兴趣区掩模与待处理图像相乘,得到感兴趣区图像,感兴趣区内图像值保持不变,而区外图像值都为0。
  2. 屏蔽作用,用掩模对图像上某些区域作屏蔽,使其不参加处理或不参加处理参数的计算,或仅对屏蔽区作处理或统计。
  3. 结构特征提取,用相似性变量或图像匹配方法检测和提取图像中与掩模相似的结构特征。

cv2.addWeighted

计算两个数组的加权和,将两个图片进行重叠操作

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=src1alpha+src2beta+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)

参考链接:

ROI与泛洪填充

ROI

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()复制一份

cv2.floodFill

泛洪填充算法,也称漫水填充算法。填充一个对象内部区域。

floodFill(image, mask, seedPoint, newVal[, loDiff[, upDiff[, flags]]]) -> retval, image, mask, rect
  • image参数表示输入/输出1或3通道,8位或浮点图像。
  • mask参数表示掩码,该掩码是单通道8位图像,比image的高度多2个像素,宽度多2个像素。填充时不能穿过输入掩码中的非零像素。
  • seedPoint参数表示泛洪算法(漫水填充算法)的起始点。
  • newVal参数表示在重绘区域像素的新值。
  • loDiff参数表示当前观察像素值与其部件邻域像素值或待加入该组件的种子像素之间的亮度或颜色之负差的最大值。
  • upDiff参数表示当前观察像素值与其部件邻域像素值或待加入该组件的种子像素之间的亮度或颜色之正差的最大值。
  • flags参数:操作标志符,包含三部分:(参考:https://www.cnblogs.com/little-monkey/p/7598529.html)
    • 低八位(0~7位):用于控制算法的连通性,可取4(默认)或8。
    • 中间八位(8~15位):用于指定掩码图像的值,但是如果中间八位为0则掩码用1来填充.
    • 高八位(16~32位):可以为0或者如下两种标志符的组合:
      • FLOODFILL_FIXED_RANGE:表示此标志会考虑当前像素与种子像素之间的差,否则就考虑当前像素与相邻像素的差。
      • FLOODFILL_MASK_ONLY:表示函数不会去填充改变原始图像,而是去填充掩码图像mask,mask指定的位置为零时才填充,不为零不填充。
        • 设置FLOODFILL_FIXED_RANGE – 改变图像,泛洪填充
        • 设置FLOODFILL_MASK_ONLY –不改变图像,只填充遮罩层本身,忽略新的颜色值参数

填充区域:

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

注意:

  • OpenCV里的mask都是为uin8类型的单通道阵列
  • mask的高和宽都需要比image多2个像素
  • FLOODFILL_MASK_ONLY不是不改变image本身,它也是改变了的。只是说这个“不改变”指的是不改变图像本身的范围,而是只与遮罩层有关

示例

对图片进行泛洪填充,标记为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)

结果:
在这里插入图片描述

滤波

图像处理中,常用的滤波算法有均值滤波、中值滤波以及高斯滤波等

滤波器种类基本原理特点
均值滤波使用模板内所有像素的平均值代替模板中心像素灰度值易收到噪声的干扰,不能完全消除噪声,只能相对减弱噪声
中值滤波计算模板内所有像素中的中值,并用所计算出来的中值体改模板中心像素的灰度值对噪声不是那么敏感,能够较好的消除椒盐噪声,但是容易导致图像的不连续性
高斯滤波对图像邻域内像素进行平滑时,邻域内不同位置的像素被赋予不同的权值对图像进行平滑的同时,同时能够更多的保留图像的总体灰度分布特征
  1. 噪声:主要有三种:
  • 椒盐噪声(Salt & Pepper):含有随机出现的黑白亮度值。
  • 脉冲噪声:只含有随机的正脉冲和负脉冲噪声。
  • 高斯噪声:含有亮度服从高斯或正态分布的噪声。高斯噪声是很多传感器噪声的模型,如摄像机的电子干扰噪声。
  1. 滤波器:主要两类:线性和非线性
  • 线性滤波器:使用连续窗函数内像素加权和来实现滤波,同一模式的权重因子可以作用在每一个窗口内,即线性滤波器是空间不变的。如果图像的不同部分使用不同的滤波权重因子,线性滤波器是空间可变的。因此可以使用卷积模板来实现滤波。线性滤波器对去除高斯噪声有很好的效果。常用的线性滤波器有均值滤波器和高斯平滑滤波器。
    • (1) 均值滤波器:最简单均值滤波器是局部均值运算,即每一个像素只用其局部邻域内所有值的平均值来置换.
    • (2) 高斯平滑滤波器是一类根据高斯函数的形状来选择权值的线性滤波器。 高斯平滑滤波器对去除服从正态分布的噪声是很有效的。
  • 非线性滤波器:
    • (1) 中值滤波器:均值滤波和高斯滤波运算主要问题是有可能模糊图像中尖锐不连续的部分。中值滤波器的基本思想使用像素点邻域灰度值的中值来代替该像素点的灰度值,它可以去除脉冲噪声、椒盐噪声同时保留图像边缘细节。中值滤波不依赖于邻域内与典型值差别很大的值,处理过程不进行加权运算。中值滤波在一定条件下可以克服线性滤波器所造成的图像细节模糊,而对滤除脉冲干扰很有效。
    • (2) 边缘保持滤波器:由于均值滤波:平滑图像外还可能导致图像边缘模糊和中值滤波:去除脉冲噪声的同时可能将图像中的线条细节滤除。 边缘保持滤波器是在综合考虑了均值滤波器和中值滤波器的优缺点后发展起来的,它的特点是:滤波器在除噪声脉冲的同时,又不至于使图像边缘十分模糊。过程:分别计算[i,j]的左上角子邻域、左下角子邻域、右上角子邻域、右下角子邻域的灰度分布均匀度V;然后取最小均匀度对应区域的均值作为该像素点的新灰度值。分布越均匀,均匀度V值越小。v=<(f(x, y) - f_(x, y))^2

均值滤波

cv2.blur
  • 原理:只取内核区域下所有像素的平均值并替换中心元素
  • 特征:核中区域贡献率相同
  • 作用:对随机噪声去噪较好
blur(src, ksize[, dst[, anchor[, borderType]]]) -> dst
  • src:输入图像
  • ksize:卷积核大小
示例

均值模糊

def blurry(image):
    """均值模糊,对随机噪声去噪较好"""
    # 均值模糊,卷积核为大小为5*5(第1位为x方向,第2位为y方向)
    image_blurry = cv.blur(image, (5, 5))
    cv.imshow("image_blurry", image_blurry)

结果:
在这里插入图片描述

中值滤波

cv2.medianBlur
  • 原理:imgs为原图像,k为方框的尺寸,相当于将方框内的个值进行排序,取中值作为当前值
  • 特征:中心点的像素被核中中位数的像素值代替
  • 作用:对椒盐噪声去较好
medianBlur(src, ksize[, dst]) -> dst
示例

中值模糊

def blurry_medium(image):
    """中值模糊,对椒盐噪声去噪较好"""
    image_median = cv.medianBlur(image, 5)
    cv.imshow("image_median", image_median)

结果:
在这里插入图片描述

自定义滤波器

cv2.filter2D

使用自定义内核对图像进行卷积。该功能将任意线性滤波器应用于图像。支持就地操作。当光圈部分位于图像外部时,该功能会根据指定的边框模式插入异常像素值。

filter2D(src, ddepth, kernel[, dst[, anchor[, delta[, borderType]]]]) -> dst
  • src:原图像
  • dst:目标图像,与原图像尺寸和通过数相同
  • ddepth:目标图像的所需深度
  • kerne:卷积核(或相当于相关核),单通道浮点矩阵;如果要将不同的内核应用于不同的通道,请使用拆分将图像拆分为单独的颜色平面,然后单独处理它们。
  • anchor:内核的锚点,指示内核中过滤点的相对位置;锚应位于内核中;默认值(-1,-1)表示锚位于内核中心。
  • detal:在将它们存储在dst中之前,将可选值添加到已过滤的像素中。类似于偏置。
  • borderTyp:像素外推法,参见BorderTypes
示例

自定义滤波器,可产生锐化等多种效果

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中找到的效果,例如模糊,锐化,轮廓或浮雕。它们还用于机器学习中的“特征提取”,这是一种用于确定图像最重要部分的技术。在这种情况下,该过程更普遍地称为“卷积”。

常用的几种内核:

  1. 模糊(blur)
    模糊内核消除了相邻像素值之间的差异。
  2. 索贝尔(sobel)
    sobel内核用于仅显示特定方向上相邻像素值的差异,分为left sobel、right sobel(检测梯度的水平变化)、top sobel、buttom sobel(检测梯度的垂直变化)。
  3. 浮雕(emboss)
    通过强调像素的差在给定方向的Givens深度的错觉。在这种情况下,沿着从左上到右下的直线的方向。
  4. 大纲(outline)
    一个轮廓内核(也称为“边缘”的内核)用于突出显示的像素值大的差异。具有接近相同强度的相邻像素旁边的像素在新图像中将显示为黑色,而与强烈不同的相邻像素相邻的像素将显示为白色。
  5. 锐化(sharpen)
    该锐化内核强调在相邻的像素值的差异。这使图像看起来更生动。
  6. 拉普拉斯算子(laplacian operator)
    拉普拉斯算子可以用于边缘检测,对于检测图像中的模糊也非常有用。
  7. 分身(identity)
    这个非常简单,就是原图(不考虑边界时)。

上述内核定义及效果参考链接:

高斯滤波

高斯分布=正态分布

cv2.GaussianBlur
  • 说明:sigmaX,sigmaY分别表示X,Y方向的标准偏差。如果仅指定了sigmaX,则sigmaY与sigmaX相同.如果两者都为零,则根据内核大小计算它们。
  • 特征:核中区域贡献率与距离区域中心成正比,权重与高斯分布相关。
  • 作用:高斯模糊在从图像中去除高斯噪声方面非常有用。(高斯噪声是指它的概率密度函数服从高斯分布(即正态分布)的一类噪声。常见的高斯噪声包括起伏噪声、宇宙噪声、热噪声和散粒噪声等等。)
cv2.GaussianBlur( SRC,ksize,sigmaX [,DST [,sigmaY [,borderType ] ] ] ) →DST
  • SRC:输入图像;图像可以具有任何数量的信道,其独立地处理的,但深度应CV_8U,CV_16U,CV_16S,CV_32F或CV_64F。
  • dst:输出与图像大小和类型相同的图像src
  • ksize :高斯核大小。 ksize.width 并且 ksize.height 可以有所不同,但它们都必须是正数和奇数。或者,它们可以为零,然后从计算 sigma*
  • sigmaX:X方向上的高斯核标准偏差
  • sigmaY – Y方向上的高斯核标准差;如果 sigmaY 为零,则将其设置为等于 sigmaX;如果两个sigmas均为零,则分别根据ksize.width 和ksize.height进行计算 link);为了完全控制结果,而不管将来可能对所有这些语义进行的修改,建议指定所有ksize,sigmaX和sigmaY
  • borderType –像素外推方法,一般为None

注意:
卷积核和标准差sigma只需要填一个,另一个为0

高斯模糊中卷积核大小与X,Y方向的标准差δ之间的关系

高斯核可以看成是与中心距离负相关的权重。平滑时,调整σ实际是在调整周围像素对当前像素的影响程度,调大σ即提高了远处像素对中心像素的影响程度,滤波结果也就越平滑。高斯曲线随σ变化的曲线如下:
在这里插入图片描述

参考链接:

示例

给图片加上高斯噪声

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)  

边缘保留滤波EPF

双边滤波

双边滤波(Bilateral filter)是一种非线性的滤波方法,是结合图像的空间邻近度和像素值相似度的一种折衷处理,同时考虑空域信息和灰度相似性,达到保边去噪的目的。具有简单、非迭代、局部的特点。

双边滤波器的好处是可以做边缘保存(edge preserving),一般用高斯滤波去降噪,会较明显地模糊边缘,对于高频细节的保护效果并不明显。双边滤波器顾名思义比高斯滤波多了一个高斯方差sigma-d,它是基于空间分布的高斯滤波函数,所以在边缘附近,离的较远的像素不会太多影响到边缘上的像素值,这样就保证了边缘附近像素值的保存。但是由于保存了过多的高频信息,对于彩色图像里的高频噪声,双边滤波器不能够干净的滤掉,只能够对于低频信息进行较好的滤波。

双边滤波的原理如下图所示:
在这里插入图片描述

cv2.bilateralFilter

高斯双边滤波

bilateralFilter(src, d, sigmaColor, sigmaSpace[, dst[, borderType]]) -> dst
  • 第一个参数,InputArray类型的src,输入图像,即源图像,需要为8位或者浮点型单通道、三通道的图像。
  • 第二个参数,OutputArray类型的dst,即目标图像,需要和源图片有一样的尺寸和类型。
  • 第三个参数,int类型的d,表示在过滤过程中每个像素邻域的直径。如果这个值我们设其为非正数,那么OpenCV会从第五个参数sigmaSpace来计算出它来。
  • 第四个参数,double类型的sigmaColor,颜色空间滤波器的sigma值。这个参数的值越大,就表明该像素邻域内有更宽广的颜色会被混合到一起,产生较大的半相等颜色区域。
  • 第五个参数,double类型的sigmaSpace坐标空间中滤波器的sigma值,坐标空间的标注方差。他的数值越大,意味着越远的像素会相互影响,从而使更大的区域足够相似的颜色获取相同的颜色。当d>0,d指定了邻域大小且与sigmaSpace无关。否则,d正比于sigmaSpace。
  • 第六个参数,int类型的borderType,用于推断图像外部像素的某种边界模式。注意它有默认值BORDER_DEFAULT。

注意:

  • d和sigmaSpace只需要填一个,另一个为0
  • 将sigma_space设置小一点(保留主要差异),sigma_color设置大一点
示例
def bilateral(image):
    """
    同时考虑空间与信息和灰度相似性,达到保边去噪的目的
    双边滤波的核函数是空间域核与像素范围域核的综合结果:
    在图像的平坦区域,像素值变化很小,对应的像素范围域权重接近于1,此时空间域权重起主要作用,相当于进行高斯模糊;
    在图像的边缘区域,像素值变化很大,像素范围域权重变大,从而保持了边缘的信息。
    """
    # bilateralFilter(src, d, sigmaColor, sigmaSpace, dst=None, borderType=None)
    dst = cv.bilateralFilter(image, 0, 100, 15)  # 高斯双边滤波
    cv.imshow("bilateral ", dst)

结果:
在这里插入图片描述
参考链接:

均值迁移滤波
cv2.pyrMeanShiftFiltering

均值漂移算法是一种通用的聚类算法,它的基本原理是:对于给定的一定数量样本,任选其中一个样本,以该样本为中心点划定一个圆形区域,求取该圆形区域内样本的质心,即密度最大处的点,再以该点为中心继续执行上述迭代过程,直至最终收敛。

这个函数严格来说并不是图像的分割,而是图像在色彩层面的平滑滤波,它可以中和色彩分布相近的颜色,平滑色彩细节,侵蚀掉面积较小的颜色区域。

pyrMeanShiftFiltering(src, sp, sr[, dst[, maxLevel[, termcrit]]]) -> dst
  • 第一个参数src,输入图像,8位,三通道的彩色图像,并不要求必须是RGB格式,HSV、YUV等Opencv中的彩色图像格式均可;
  • 第二个参数sp,定义的漂移物理空间半径大小;
  • 第三个参数sr,定义的漂移色彩空间半径大小;
  • 第四个参数dst,输出图像,跟输入src有同样的大小和数据格式;
  • 第五个参数maxLevel,定义金字塔的最大层数;
  • 第六个参数termcrit,定义的漂移迭代终止条件,可以设置为迭代次数满足终止,迭代目标与中心点偏差满足终止,或者两者的结合。

pyrMeanShiftFiltering函数的执行过程:

  1. 迭代空间构建:
    以输入图像上src上任一点P0为圆心,建立物理空间上半径为sp,色彩空间上半径为sr的球形空间,物理空间上坐标2个—x、y,色彩空间上坐标3个—R、G、B(或HSV),构成一个5维的空间球体。
    其中物理空间的范围x和y是图像的长和宽,色彩空间的范围R、G、B分别是0~255。

  2. 求取迭代空间的向量并移动迭代空间球体后重新计算向量,直至收敛:
    在1中构建的球形空间中,求得所有点相对于中心点的色彩向量之和后,移动迭代空间的中心点到该向量的终点,并再次计算该球形空间中所有点的向量之和,如此迭代,直到在最后一个空间球体中所求得的向量和的终点就是该空间球体的中心点Pn,迭代结束。

  3. 更新输出图像dst上对应的初始原点P0的色彩值为本轮迭代的终点Pn的色彩值,如此完成一个点的色彩均值漂移。

  4. 对输入图像src上其他点,依次执行步骤1,、2、3,遍历完所有点位后,整个均值偏移色彩滤波完成,这里忽略对金字塔的讨论。

注意:
关键参数是sp和sr的设置,二者设置的值越大,对图像色彩的平滑效果越明显,同时函数耗时也越多。

示例

均值迁移

def shift(image):
    """均值迁移"""
    dst = cv.pyrMeanShiftFiltering(image, 10, 50)
    cv.imshow("shift", dst)

结果:
在这里插入图片描述
均值迁移滤波原理参考链接:

参考链接:

直方图(histogram)

直方图是对图像像素的统计分布,它统计了每个像素(0到L-1)的数量。

注意:
pycharm从2017.3版之后,将matplotlib的绘图的结果默认显示在SciView窗口中,而不是弹出独立的窗口。

修改方式见链接:
新版Pycharm中Matplotlib图像不在弹出独立的显示窗口

numpy.ravel

将多维数组降为一维
可以传入索引:‘C’, ‘F’, ‘A’, ‘K’

参考链接:
numpy 辨异 (五)—— numpy.ravel() vs numpy.flatten()

enumerate

枚举

参考链接:

绘制直方图

matplotlib.pyplot.hist

绘制直方图,一般用来绘制灰度直方图

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

计算图像直方图

cv2.calcHist(images, channels, mask, histSize, ranges[, hist[, accumulate ]]) ->hist

  • images:输入图像,参数必须用方括号括起来。
  • channels:计算直方图的通道。
  • Mask:掩膜,一般用None,表示处理整幅图像。
  • histSize:表示这个直方图分成多少份(即多少个直方柱)。
  • range:直方图中各个像素的值,[0.0,256.0]表示直方图能表示像素值从0.0到256的像素。
  • hist:是一个256x1阵列,每个值对应于该图像中的像素值及其对应的像素值
  • accumulate:是一个布尔值,用来表示直方图是否叠加。

注意:

  • 最后是两个可选参数,由于直方图作为函数结果返回了。
  • 除了mask,其他四个参数都要带[]号
示例

绘制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点击查看的操作,让图表单独显示后,产生了图框出现而内容不出现的问题,如下图所示:
在这里插入图片描述
原因:未知,个人猜测是交互模式和阻碍模式的问题(可能性高),或是线程阻碍产生的结果
解决方案:
第一种:

  1. 注释掉主函数里的cv.imshow()
    # cv.imshow("example", src1)
    plot(src1)

第二种:

  1. 给plt.show传入False参数
plt.show(False)

第三种(推荐):

  1. 在plt.show之前添加plt.ioff函数,关闭交互模式。

第四种(推荐):

  1. 命令行里运行(命令行默认是阻塞模式)(按道理交互模式可以显示多张图,阻塞模式不行。但是现在情况与文档说明完全相反,不一致),或者直接在设置里改成终端运行

参考链接:

问题二:
在出现问题一并解决后,想要同时显示两张图表,却发现两张图只显示前一张
原因: 显示第一张的时候就用了plt.show
解决方案:
第一种:

  1. 使用pyplot.savefig保存图片和pyplot.clf清除图表内容,防止两张图合为一张

第二种:

  1. 使用别的绘图指令,产生不同的图表对象再一起绘图

直方图应用

直方图均衡化

直方图均衡化就是将原始的直方图拉伸,使之均匀分布在全部灰度范围内,从而增强图像的对比度。

直方图均衡化的中心思想是把原始图像的的灰度直方图从比较集中的某个区域变成在全部灰度范围内的均匀分布。

如果一幅图像的灰度直方图几乎覆盖了整个灰度的取值范围,并且除了个别灰度值的个数较为突出,整个灰度值分布近似于均匀分布,那么这幅图像就具有较大的灰度动态范围和较高的对比度,同时图像的细节更为丰富。

均衡化步骤:

  • 统计直方图中每个灰度级出现的次数;
  • 计算累计归一化直方图;
  • 重新计算像素点的像素值

参考链接:

注意:
OpenCV里的直方图均衡化都是基于灰度图

cv2.equalizeHist

直方图均衡化

equalizeHist(src[, dst]) -> dst
  • src:8位单通道图像
示例

直方图均衡化

def equal_hist(image):
    """全局直方图均衡化,用于增强图像对比度,即黑的更黑,白的更白"""
    gray = cv.cvtColor(image, cv.COLOR_BGR2GRAY)
    dst = cv.equalizeHist(gray)
    cv.imshow("equalHist", dst)

结果:
在这里插入图片描述

局部直方图均衡化

直方图均衡化可以可能到是一种全局意义上的均衡化,但是有的时候这种操作并不是很好,会把某些不该调整的部分给调整了。OpenCV中还有一种直方图均衡化,它是一种局部局部来的均衡化,也就是是说把整个图像分成许多小块(比如按10*10作为一个小块),那么对每个小块进行均衡化。这种方法主要对于图像直方图不是那么单一的(比如存在多峰情况)图像比较实用。

cv2.createCLAHE

自适应均衡化图像

createCLAHE([, clipLimit[, tileGridSize]]) -> retval
  • clipLimit颜色对比度的阈值
  • titleGridSize进行像素均衡化的网格大小,即在多少网格下进行直方图的均衡化操作

注意:

  • 用法与equalizeHist不一样。createCLAHE返回的是一个实例化的对象。需要调用apply函数才能完成均衡化操作。
示例

局部直方图均衡化

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)

结果:
在这里插入图片描述
参考链接:

参考链接:

直方图比较

如果我们有两张图像,并且这两张图像的直方图一样,或者有极高的相似度,那么在一定程度上,我们可以认为这两幅图是一样的,这就是直方图比较的应用之一。

cv2.compareHist

直方图比较

compareHist(H1, H2, method) -> retval
  • H1,H2 :分别为要比较图像的直方图
  • method :比较方式

比较方式有:

  • 相关性(method=cv.HISTCMP_CORREL) 值越大,相关度越高,最大值为1,最小值为0
  • 卡方(method=cv.HISTCMP_CHISQR)值越小,相关度越高,最大值无上界,最小值0
  • 巴氏距离(method=cv.HISTCMP_BHATTACHARYYA)值越小,相关度越高,最大值为1,最小值为0
  • 相交性(method=HISTCMP_INTERSECT)取两个直方图每个相同位置的值的最小值,然后求和,这个比较方式不是很好,不建议使用

==注意: ==
用巴氏距离和相关性比较好

参考链接:

示例

利用直方图比较图像相似性

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
在这里插入图片描述
计算结果:
在这里插入图片描述
注意:
上述比较的代码只能用于两张大小一样的图片,如果需要比较两张不一样
大小的图片,需要进行归一化操作

直方图比较中的bins如何理解

把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’如果显示分辨率与图像分辨率不同(通常是这种情况),则只显示图像而不尝试在像素之间进行插值。它将生成一个像素显示为多个像素的正方形的图像。

参考链接:

归一化与标准化

cv2.normalize

归一化(矩阵的值通过某种方式变到某一个区间内)

normalize(src, dst[, alpha[, beta[, norm_type[, dtype[, mask]]]]]) -> dst

src-输入数组。
dst-与SRC大小相同的输出数组。
α-范数值在范围归一化的情况下归一化到较低的范围边界。
β-上限范围在范围归一化的情况下;它不用于范数归一化。
norm_type-规范化类型,见下面参考链接。
dType——当输出为负时,输出数组具有与SRC相同的类型;否则,它具有与SRC相同的信道数和深度=CVH-MatthAsHead(DyType)。
mask-可选的操作掩膜。

参考链接:

直方图反向投影

cv2.calcBackProject

直方图反向投影

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。

工作方法:在带检测图像上,从左到右,从上向下计算模板图像与重叠子图像的匹配度,匹配程度越大,两者相同的可能性越大。

cv2.matchTemplate

模板匹配

matchTemplate(image, templ, method[, result[, mask]]) -> result
  • iamge:待搜索图像(大图)
  • templ:搜素模板,需和原图一样的数据类型且尺寸不能大于原图像
  • 比较结果的映射图像,如果输入图像的大小为(WxH),而模板图像的大小为(wxh),则输出图像的大小将为(W-w+1,H-h+ 1)
  • method:
    • cv.TM_SQDIFF------平方差匹配法(最好匹配0)
    • cv.TM_SQDIFF_NORMED------归一化平方差匹配法(最好匹配0)
    • cv.TM_CCORR------相关匹配法(最坏匹配0)
    • cv.TM_CCORR_NORMED------归一化相关匹配法(最坏匹配0)
    • cv.TM_CCOEFF------.系数匹配法(最好匹配1)
    • cv.TM_CCOEFF_NORMED------化相关系数匹配法(最好匹配1)

注意: 如果使用cv.TM_SQDIFF作为比较方法,则最小值提供最佳匹配

在这里插入图片描述

cv2.minMaxLoc

寻找最值

minMaxLoc(src[, mask]) -> minVal, maxVal, minLoc, maxLoc
  • src:输入单通道图像。
  • mask:用于选择子数组的可选掩码。
  • minVal:返回的最小值,如果不需要,则使用NULL。
  • maxVal:返回的最大值,如果不需要,则使用NULL。
  • minLoc:返回的最小位置的指针(在2D情况下); 如果不需要,则使用NULL。
  • maxLoc:返回的最大位置的指针(在2D情况下); 如果不需要,则使用NULL。

示例

模板匹配

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()

结果:

sample和target
在这里插入图片描述

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)

参考链接:

图像二值化

简单阈值(OTSU和Triangle)

cv2.threshold

简单阈值

threshold(src, thresh, maxval, type[, dst]) -> retval, dst
  • src:表示的是图片源
  • thresh:表示的是阈值(起始值)
  • maxval:表示的是最大值
  • type:表示的是这里划分的时候使用的是什么类型的算法

注意:

  • 当选择计算方式(如:cv.THRESH_OTSU)之后,前面所定义的thresh会不起作用
  • type参数:图像处理方式 | 阈值计算方法
    • 图像处理方式:cv.THRESH_BINARY、cv.THRESH_BINARY_INV、cv.THRESH_TOZERO等
    • 阈值计算方法:cv.THRESH_OTSU、cv.THRESH_TRIANGLE

在这里插入图片描述

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’。
参考链接:

自适应阈值

在前面的部分我们使用是全局阈值,整幅图像采用同一个数作为阈值。

当时这种方法并不适应与所有情况,尤其是当同一幅图像上的不同部分的具有不同亮度时。这种情况下我们需要采用自适应阈值。此时的阈值是根据图像上的每一个小区域计算与其对应的阈值。

因此在同一幅图像上的不同区域采用的是不同的阈值,从而使我们能在亮度不同的情况下得到更好的结果。

cv2.adaptiveThreshold

自适应阈值

 adaptiveThreshold(src, maxValue, adaptiveMethod, thresholdType, blockSize, C[, dst]) -> dst
  • adaptiveMethod:Int类型的,这里有两种选择,不过这两种方法最后得到的结果要减掉参数里面的C值。

    • ADAPTIVE_THRESH_MEAN_C(通过平均的方法取得平均值)———阈值取自相邻区域的平均值)
    • ADAPTIVE_THRESH_GAUSSIAN_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)  # 保存图像

个人优化后结果(与优化前做对比):
在这里插入图片描述

图像金字塔(Image Pyramid)

图像金字塔最初用于机器视觉和图像压缩,一幅图像的金字塔是一系列以金字塔形状排列的分辨率逐步降低,且来源于同一张原始图的图像集合。其通过梯次向下采样获得,直到达到某个终止条件才停止采样。

金字塔的底部是待处理图像的高分辨率表示,而顶部是低分辨率的近似。我们将一层一层的图像比喻成金字塔,层级越高,则图像越小,分辨率越低。

  • 高斯金字塔 ( Gaussian pyramid): 用来向下/降采样,主要的图像金字塔
  • 拉普拉斯金字塔(Laplacian pyramid): 用来从金字塔低层图像重建上层未采样图像,在数字图像处理中也即是预测残差,可以对图像进行最大程度的还原,配合高斯金字塔一起使用。

两者的区别:高斯金字塔用来向下降采样图像,注意降采样其实是由金字塔底部向上采样,分辨率降低,它和我们理解的金字塔概念相反;而拉普拉斯金字塔则用来从金字塔底层图像中向上采样重建一个图像。

在这里插入图片描述

  • 对图像向上采样:pyrUp函数
  • 对图像向下采样:pyrDown函数

注意:

  • 向下与向上采样,是对图像的尺寸而言的(和金字塔的方向相反),向上就是图像尺寸加倍,向下就是图像尺寸减半。而如果我们按上图中演示的金字塔方向来理解,金字塔向上图像其实在缩小,这样刚好是反过来了。
  • PryUp和PryDown不是互逆的,即PryUp不是降采样的逆操作。这种情况下,图像首先在每个维度上扩大为原来的两倍,新增的行(偶数行)以0填充。然后给指定的滤波器进行卷积(实际上是一个在每个维度都扩大为原来两倍的过滤器)去估计“丢失”像素的近似值。PryDown( )是一个会丢失信息的函数。

参考链接:

高斯金字塔

高斯金字塔是通过高斯平滑和亚采样获得一系列下采样图像,也就是说第K层高斯金字塔通过平滑、亚采样就可以获得K+1层高斯图像,高斯金字塔包含了一系列低通滤波器,其截至频率从上一层到下一层是以因子2逐渐增加,所以高斯金字塔可以跨越很大的频率范围。
在这里插入图片描述

  1. 对图像的向下取样操作,即缩小图像。
    为了获取层级为 G_i+1 的金字塔图像,方法步骤如下:

    • 对图像G_i进行高斯内核卷积,进行高斯模糊;
    • 将所有偶数行和列去除。

    得到的图像即为G_i+1的图像,显而易见,结果图像只有原图的四分之一。通过对输入图像G_i(原始图像)不停迭代以上步骤就会得到整个金字塔。同时我们也可以看到,向下取样会逐渐丢失图像的信息。以上就是对图像的向下取样操作,即缩小图像。

  2. 对图像的向上取样,即放大图像
    方法步骤如下:

    • 将图像在每个方向扩大为原来的两倍,新增的行和列以0填充
    • 使用先前同样的内核(乘以4)与放大后的图像卷积,获得 “新增像素”的近似值

    得到的图像即为放大后的图像,但是与原来的图像相比会发觉比较模糊,因为在缩放的过程中已经丢失了一些信息,如果想在缩小和放大整个过程中减少信息的丢失,这些数据形成了拉普拉斯金字塔。

拉普拉斯金字塔

拉普拉斯金字塔第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=GiUP(Gi+1)G5×5GiiUP()(x,y)(2x+1,2y+1)G5×55×5
pryUp,就是在进行上面这个式子的运算。因此,可以直接用OpenCV进行拉普拉斯运算:
L i = G i − P y r U P ( G i + 1 ) L_i=G_i-PyrUP(G_{i+1}) Li=GiPyrUP(Gi+1)
将降采样之后的图像再进行上采样操作,然后与之前还没降采样的原图进行做差得到残差图!为还原图像做信息的准备。

也就是说,拉普拉斯金字塔是通过源图像减去先缩小后再放大的图像的一系列图像构成的。保留的是残差!

注意: 上采样和下采样是非线性处理,不可逆,有损的处理。

cv2.pyrDown

先对图像进行高斯平滑,然后再进行降采样

pyrDown(src[, dst[, dstsize[, borderType]]]) -> dst
  • src:输入的图像
  • dstsize:降采样之后的目标图像的大小,它是有默认值的,如果我们调用函数的时候不指定第三个参数,那么这个值是按照
    S i z e ( ( s r c . c o l s + 1 ) / 2 , ( s r c . r o w s + 1 ) / 2 ) Size((src.cols+1)/2, (src.rows+1)/2) Size((src.cols+1)/2,(src.rows+1)/2)
    计算的。不管你自己如何指定这个参数,一定必须保证满足以下关系式:

{ ∣ 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.width2src.cols2dstsize.height2src.rows2

cv2.pyrUp

先对图像进行升采样(将图像尺寸行和列方向增大一倍),然后再进行高斯平滑

pyrUp(src[, dst[, dstsize[, borderType]]]) -> dst
  • src:输入的图像
  • dstsize:升采样之后的目标图像的大小,在默认的情况下,这个尺寸大小是按照
    S i z e ( s r c . c o l s ∗ 2 , s r c . r o w s ∗ 2 ) Size(src.cols*2, src.rows*2) Size(src.cols2,src.rows2)
    来计算的。如果要指定大小,那么一定要满足下面的条件:

{ / / 如 果 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} //widthdstsize.widthsrc.cols2dstsize.widthsrc.cols2(dstsize.widthmod2);dstsize.heightsrc.rows2(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和Scharr算子

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=121000121//
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=101202101//
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=31030003103//
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=30310010303//

cv2.Sobel

Sobel滤波器

Sobel(src, ddepth, dx, dy[, dst[, ksize[, scale[, delta[, borderType]]]]]) -> dst
  • sec:是需要处理的图像;
  • ddepth:图像的深度,-1表示采用的是与原图像相同的深度。目标图像的深度必须大于等于原图像的深度
  • dx和dy表示的是求导的阶数,0表示这个方向上没有求导,一般为0、1、2。
  • dst:目标图像
  • ksize是Sobel算子的大小,必须为1、3、5、7。
  • scale是缩放导数的比例常数,默认情况下没有伸缩系数。
  • delta是一个可选的增量,将会加到最终的dst中,同样,默认情况下没有额外的值加到dst中。
  • borderType是判断图像边界的模式。这个参数默认值为cv2.BORDER_DEFAULT。

注意: Sobel函数求完导数后会有负值,还有会大于255的值。而原图像是uint8,即8位无符号数,所以Sobel建立的图像位数不够,会有截断。第二个参数可以传cv.CV_32F。在经过处理后,要用convertScaleAbs()函数将其转回原来的uint8形式。否则将无法显示图像,而只是一副灰色的窗口。

cv2.convertScaleAbs

在输入数组的每个元素上,函数convertScaleAbs依次执行三个操作:缩放,获取绝对值,转换为无符号的8位类型

convertScaleAbs(src[, dst[, alpha[, beta]]]) -> dst
  • src: 输入数组。
  • dst: 输出数组。
  • alpha: 可选比例因子。
  • beta: 可选增量添加到缩放值。
cv2.Scharr
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)

结果:

在这里插入图片描述

在这里插入图片描述

参考链接:

Laplacian算子

计算了由关系

Δ 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=010141010//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=111181111//8

cv2.Laplacian

Laplacian滤波器

Laplacian(src, ddepth[, dst[, ksize[, scale[, delta[, borderType]]]]]) -> dst
  • src:原图像
  • ddepth:图像的深度,-1表示采用的是与原图像相同的深度。目标图像的深度必须大于等于原图像的深度
  • dst:目标图像
  • ksize:算子的大小,必须为1、3、5、7。默认为1
  • scale:是缩放导数的比例常数,默认情况下没有伸缩系数
  • delta:是一个可选的增量,将会加到最终的dst中,同样,默认情况下没有额外的值加到dst中
  • borderType:是判断图像边界的模式。这个参数默认值为cv2.BORDER_DEFAULT
示例
def laplace_gradient(image):
    """Laplacian算子梯度滤波(二阶导数)"""
    dst = cv.Laplacian(image,cv.CV_32F)
    lpls = cv.convertScaleAbs(dst)
    cv.imshow("laplace_gradient", lpls)

结果:
在这里插入图片描述

canny边缘提取

  1. 高斯模糊——gaussian
  2. 灰度转换——cvtColor
  3. 计算梯度——Sobel/Scharr
  4. 非最大信号抑制
  5. 高低阈值输出二值图像
  • 第一步:使用高斯滤波器进行滤波,去除噪音点

    • 使用5x5高斯滤波器消除图像中的噪声
  • 第二步:使用sobel算子,计算出每个点的梯度大小和梯度方向

    • Sobel核在水平和垂直方向上对平滑的图像进行滤波,以在水平方向(Gx)和垂直方向(Gy)上获得一阶导数
      E d g e _ G r a d i e n t    ( G ) = G x 2 + G y 2 A n g l e    ( θ ) = tan ⁡ − 1 ( G y G x ) Edge\_Gradient \; (G) = \sqrt{G_x^2 + G_y^2} \\ Angle \; (\theta) = \tan^{-1} \bigg(\frac{G_y}{G_x}\bigg) Edge_Gradient(G)=Gx2+Gy2 Angle(θ)=tan1(GxGy)
  • 第三步:使用非极大值抑制(只有最大的保留),消除边缘检测带来的杂散效应

    • 在获得梯度大小和方向后,将对图像进行全面扫描,以去除可能不构成边缘的所有不需要的像素。为此,在每个像素处,检查像素是否是其在梯度方向上附近的局部最大值。
    • 在这里插入图片描述
      点A在边缘(垂直方向)上。渐变方向垂直于边缘。点B和C在梯度方向上。因此,将A点与B点和C点进行检查,看是否形成局部最大值。如果是这样,则考虑将其用于下一阶段,否则将其抑制(置为零)。 简而言之,得到的结果是带有“细边”的二进制图像。
  • 第四步:应用双阈值(磁滞阈值),来确定真实和潜在的边缘

    • 需要两个阈值minVal和maxVal。强度梯度大于maxVal的任何边缘必定是边缘,而小于minVal的那些边缘必定是非边缘,因此将其丢弃。介于这两个阈值之间的对象根据其连通性被分类为边缘或非边缘。如果将它们连接到“边缘”像素,则将它们视为边缘的一部分。否则,它们也将被丢弃。
    • 在这里插入图片描述
      边缘A在maxVal之上,因此被视为“确定边缘”。尽管边C低于maxVal,但它连接到边A,因此也被视为有效边,我们得到了完整的曲线。但是边缘B尽管在minVal之上并且与边缘C处于同一区域,但是它没有连接到任何“确保边缘”,因此被丢弃。因此,非常重要的一点是我们必须相应地选择minVal和maxVal以获得正确的结果。
  • 第五步:通过抑制弱边缘来完成最终的边缘检测

cv2.canny

canny边缘检测

Canny(image, threshold1, threshold2[, edges[, apertureSize[, L2gradient]]]) -> edges
  • image:要检测的图像
  • threshold1:阈值1(最小值)
  • threshold2:阈值2(最大值),使用此参数进行明显的边缘检测。
  • edges:图像边缘信息
  • apertureSize:sobel算子(卷积核)大小,默认情况下为3
  • L2gradient :布尔值。
    • True: 使用更精确的L2范数进行计算(即两个方向的导数的平方和再开方)
      E d g e _ G r a d i e n t    ( G ) = G x 2 + G y 2 Edge\_Gradient \; (G) = \sqrt{G_x^2 + G_y^2} \\ Edge_Gradient(G)=Gx2+Gy2
    • False:使用L1范数(直接将两个方向导数的绝对值相加)
      E d g e _ G r a d i e n t    ( G ) = ∣ G x ∣ + ∣ G y ∣ Edge\_Gradient \; (G) = \left| G_x\right| + \left| G_y\right| Edge_Gradient(G)=Gx+Gy

注意: 一般来说,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度。

标准直线检测
cv2.HoughLines

使用标准Hough变换在二值图像中查找线条

HoughLines(image, rho, theta, threshold[, lines[, srn[, stn[, min_theta[, max_theta]]]]]) -> lines
  • image 是输入图像,即源图像,必须是 8 位的单通道二值图像。如果是其他类型的图像,在进行霍夫变换之前,需要将其修改为指定格式。
  • rho 为以像素为单位的距离 r 的精度。一般情况下,使用的精度是 1。
  • theta 为角度 θ 的精度。一般情况下,使用的精度是 π/180,表示要搜索所有可能的角度。
  • threshold 是阈值。该值越小,判定出的直线就越多。通过上一节的分析可知,识别直线时,要判定有多少个点位于该直线上。在判定直线是否存在时,对直线所穿过的点的数量进行评估,如果直线所穿过的点的数量小于阈值,则认为这些点恰好(偶然)在算法上构成直线,但是在源图像中该直线并不存在;如果大于阈值,则认为直线存在。所以,如果阈值较小,就会得到较多的直线;阈值较大,就会得到较少的直线。
  • 返回值 lines 中的每个元素都是一对浮点数,表示检测到的直线的参数,即(r,θ),是 numpy.ndarray 类型
示例
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判断。

概率直线检测

概率霍夫变换对基本霍夫变换算法进行了一些修正,是霍夫变换算法的优化。它没有考虑所有的点。相反,它只需要一个足以进行线检测的随机点子集即可。

为了更好地判断直线(线段),概率霍夫变换算法还对选取直线的方法作了两点改进:

  • 所接受直线的最小长度。如果有超过阈值个数的像素点构成了一条直线,但是这条直线很短,那么就不会接受该直线作为判断结果,而认为这条直线仅仅是图像中的若干个像素点恰好随机构成了一种算法上的直线关系而已,实际上原图中并不存在这条直线。

  • 接受直线时允许的最大像素点间距。如果有超过阈值个数的像素点构成了一条直线,但是这组像素点之间的距离都很远,就不会接受该直线作为判断结果,而认为这条直线仅仅是图像中的若干个像素点恰好随机构成了一种算法上的直线关系而已,实际上原始图像中并不存在这条直线。

cv2.HoughLinesP

用概率Hough变换在二值图像中查找线段

HoughLinesP(image, rho, theta, threshold[, lines[, minLineLength[, maxLineGap]]]) -> lines
  • image 是输入图像,即源图像,必须为 8 位的单通道二值图像。对于其他类型的图像,在进行霍夫变换之前,需要将其修改为这个指定的格式。
  • rho 为以像素为单位的距离 r 的精度。一般情况下,使用的精度是 1。
  • theta 是角度 θ 的精度。一般情况下,使用的精度是 np.pi/180,表示要搜索可能的角度。
  • threshold 是阈值。该值越小,判定出的直线越多;值越大,判定出的直线就越少。
  • minLineLength 用来控制「接受直线的最小长度」的值,默认值为 0。
  • maxLineGap 用来控制接受共线线段之间的最小间隔,即在一条线中两点的最大间隔。
    如果两点间的间隔超过了参数 maxLineGap 的值,就认为这两点不在一条线上。默认值为 0。
  • 返回值 lines 是线的输出向量。每一行由一个4元素向量表示,x1,y1,x2,y2。
示例
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)

结果:
在这里插入图片描述

参考链接:

圆检测

cv2.HoughCircles

使用Hough变换在灰度图像中查找圆

HoughCircles(image, method, dp, minDist[, circles[, param1[, param2[, minRadius[, maxRadius]]]]]) -> circles
  • image:输入图像
  • method:cv2.HOUGH_GRADIENT 也就是霍夫圆检测,梯度法
  • dp:计数器的分辨率图像像素分辨率与参数空间分辨率的比值(官方文档上写的是图像分辨率与累加器分辨率的比值,它把参数空间认为是一个累加器,毕竟里面存储的都是经过的像素点的数量),dp=1,则参数空间与图像像素空间(分辨率)一样大,dp=2,参数空间的分辨率只有像素空间的一半大
  • minDist:圆心之间最小距离,如果距离太小,会产生很多相交的圆,如果距离太大,则会漏掉正确的圆
  • param1:canny检测的双阈值中的高阈值,低阈值是它的一半
  • param2:最小投票数(基于圆心的投票数)
  • minRadius:需要检测院的最小半径
  • maxRadius:需要检测院的最大半径
  • 返回值为圆心坐标与半径
示例
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)

结果:
在这里插入图片描述
注意: 霍夫圆检测对噪声敏感,使用霍夫圆检测之前,需要先对图片进行滤波降噪处理。

参考链接:

轮廓查找

cv2.findContours

在二值图像中查找轮廓

findContours(image, mode, method[, contours[, hierarchy[, offset]]]) -> contours, hierarchy
  • image:寻找轮廓的图像;
  • mode:表示轮廓的检索模式,有四种(本文介绍的都是新的cv2接口):
    • cv2.RETR_EXTERNAL:表示只检测外轮廓
    • cv2.RETR_LIST:检测的轮廓不建立等级关系
    • cv2.RETR_CCOMP :建立两个等级的轮廓,上面的一层为外边界,里面的一层为内孔的边界信息。如果内孔内还有一个连通物体,这个物体的边界也在顶层。
    • cv2.RETR_TREE:建立一个等级树结构的轮廓。
  • method:为轮廓的近似办法
    • cv2.CHAIN_APPROX_NONE 存储所有的轮廓点,相邻的两个点的像素位置差不超过1,即max(abs(x1-x2),abs(y2-y1))==1
    • cv2.CHAIN_APPROX_SIMPLE 压缩水平方向,垂直方向,对角线方向的元素,只保留该方向的终点坐标,例如一个矩形轮廓只需4个点来保存轮廓信息
    • cv2.CHAIN_APPROX_TC89_L1,CV_CHAIN_APPROX_TC89_KCOS 使用teh-Chinl chain 近似算法
    • CV_LINK_RUNS :通过连接为 1 的水平碎片使用完全不同的轮廓提取算法。仅有 CV_RETR_LIST 提取模式可以在本方法中应用
  • offset:每一个轮廓点的偏移量. 当轮廓是从图像 ROI 中提取出来的时候,使用偏移量有用,因为可以从整个图像上下文来对轮廓做分析
  • 返回值:
    • contours:一个列表,每一项都是一个轮廓, 不会存储轮廓所有的点,只存储能描述轮廓的点
    • hierarchy:一个ndarray, 元素数量和轮廓数量一样,每个轮廓contours[i]对应4个hierarchy元素hierarchy[i][0] ~hierarchy[i][3],分别表示后一个轮廓、前一个轮廓、父轮廓、内嵌轮廓的索引编号,如果没有对应项,则该值为负数

cv2.drawContours

绘制轮廓轮廓或填充轮廓

drawContours(image, contours, contourIdx, color[, thickness[, lineType[, hierarchy[, maxLevel[, offset]]]]]) -> image
  • image:指明在哪幅图像上绘制轮廓;image为三通道才能显示轮廓
  • contours:是轮廓本身,在Python中是一个list;
  • contourIdx:指定绘制轮廓list中的哪条轮廓,如果是-1,则绘制其中的所有轮廓。后面的参数很简单。
  • color:线的颜色
  • thickness表明轮廓线的宽度,如果是-1(cv2.FILLED),则为填充模式

示例

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)

结果:
在这里插入图片描述

注意:

  1. 为了更加准确,要使用二值化图像。在寻找轮廓之前,要进行阈值化处理或者 Canny 边界检测
  2. 在Opencv4.0中 cv.findContours()的返回值以从三个变为二个

参考链接:

对象测量

cv2.contourArea

此函数利用格林公式计算轮廓的面积。对于具有自交点的轮廓,该函数几乎肯定会给出错误的结果。

contourArea(contour[, oriented]) -> retval
  • contour:输入二维的向量。
  • oriented:有方向的区域标志。
    • true:此函数依赖轮廓的方向(顺时针或逆时针)返回一个已标记区域的值。
    • false:默认值。意味着返回不带方向的绝对值

cv2.arcLength

计算轮廓周长或曲线长度

arcLength(curve, closed) -> retval
  • curve:二维点的输入向量
  • closed:指示曲线是否闭合的标志(闭合的(True))

cv2.boundingRect

用一个最小的矩形,把找到的形状包起来

boundingRect(array) -> retval

返回四个值,分别是x,y,w,h;

  • x,y是矩阵左上点的坐标
  • w,h是矩阵的宽和高

cv2.moments

计算多边形或栅格化形状的所有三阶矩

moments(array[, binaryImage]) -> retval
  • array:输入数组,可以是光栅图像(单通道,8-bit或浮点型二维数组),或者是一个二维数组(1 X N或N X 1),二维数组类型为Point或Point2f
  • binaryImage:默认值是false,如果为true,则所有非零的像素都会按值1对待,也就是说相当于对图像进行了二值化处理,阈值为1,此参数仅对图像有效

示例

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。会增加图像中的白色区域(前景)。一般在去噪声时先用腐蚀再用膨胀。因为腐蚀在去掉白噪声的同时,也会使前景对象变小。所以我们再对他进行膨胀。这时噪声已经被去除了,不会再回来了,但是前景还在并会增加。

作用:

  1. 对象的大小增加一个像素(3×3)
  2. 平滑对象边缘
  3. 减少或填充对象之间的边缘
cv2.dilate

使用特定的结构元素膨胀图像

dilate(src, kernel[, dst[, anchor[, iterations[, borderType[, borderValue]]]]]) -> dst
  • src:表示输入的图片
  • kernel:表示膨胀所用的结构元素的大小
  • iteration:表示迭代的次数
示例

对图像二值化并膨胀

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),所以前景物体会变小,整幅图像的白色区域会减少。

作用:

  1. 对象的大小减少一个像素(3×3)
  2. 平滑对象边缘
  3. 弱化或者分割对象之间的半岛型连接
cv2.erode

使用特定的结构元素腐蚀图像

erode(src, kernel[, dst[, anchor[, iterations[, borderType[, borderValue]]]]]) -> dst
  • src:表示输入的图片
  • kernel:表示腐蚀所用的结构元素的大小
  • iteration:表示迭代的次数
示例

对图像二值化并腐蚀

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)

结果
在这里插入图片描述

参考链接:

形态学操作

cv2.morphologyEx

执行形态学操作

  • 开运算:先进行腐蚀再进行膨胀就叫做开运算,它被用来去除噪声。
  • 闭运算:先膨胀再腐蚀。它经常被用来填充前景物体中的小洞,或者前景物体上的小黑点。
  • 顶帽:原图像与开操作之间的差值图像
  • 黑帽:闭操作与原图像之间的差值图像
  • 形态学梯度:其实就是一幅图像膨胀与腐蚀的差别。 结果看上去就像前景物体的轮廓
  • 基本梯度:膨胀后图像减去腐蚀后图像得到的差值图像。
  • 内部梯度:用原图减去腐蚀图像得到的差值图像。
  • 外部梯度:膨胀后图像减去原图像得到的差值图像
morphologyEx(src, op, kernel[, dst[, anchor[, iterations[, borderType[, borderValue]]]]]) -> dst
  • src:输入图像
  • op:操作类型
    • MORTH_OPEN:函数做开运算
    • MORTH_CLOSE:函数做闭运算
    • MORTH_GRADIENT:函数做形态学梯度运算
    • MORTH_TOPHAT:函数做顶帽运算
    • MORTH_BLACKHAT:函数做黑帽运算
    • MORTH_DILATE :函数做膨胀运算
    • MORTH_ERODE:函数做腐蚀运算
  • kernel :内核类型,用getStructuringElement函数得到。
    • 例如:采用开操作,kernel为(1, 15),提取垂直线,kernel为(15, 1),提取水平线

开操作

先进行腐蚀再进行膨胀就叫做开运算,它被用来去除噪声。

示例

开操作

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)=srcopen(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。

在这里插入图片描述

cv2.watershed

使用分水岭算法实现基于标记的图像分割

watershed(image, markers) -> markers
  • image:8位3通道图像
  • markers:标记(输入/输出的32位单通道图像,大小与图像一致)

示例

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已经训练好的分类器,直接利用它来实现面部和眼部检测。

  • 主要步骤:

    1. 加载xml分类器,并将图像或者视频处理成灰度格式 cv.CascadeClassifier()
    2. 对灰度图像进行面部检测,返回若干个包含面部的矩形区域 Rect(x,y,w,h)face_detector.detectMultiScale()
    3. 创建一个包含面部的ROI,并在其中进行眼部检测
  • 重要方法分析: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:最小可能对象尺寸,所检测的结果小于该值会被忽略。最大可能对象尺寸,所检测的结果大于该值会被忽略
    返回:若干个包含对象的矩形区域

detectMultiScale

示例

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

验证码识别

步骤:

  1. 预处理-去除干扰线和点
  2. 不同的结构元素中选择
  3. Image和numpy array相互转换
  4. 识别和输出 tess.image_to_string

示例

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

  • 不同系统采用不同策略:

    • On Linux
      • sudo apt update
      • sudo apt install tesseract-ocr
      • sudo apt install libtesseract-dev
    • On Mac
      • brew install tesseract
    • On Windows
      • 先下载tesseract包:tesseract包
      • 然后修改pytesseract.py中tesseract_cmd指向的路径:tesseract_cmd = ‘C:\Program Files (x86)\Tesseract-OCR\tesseract.exe’
      • 路径也有可能是r’C:\Program Files\Tesseract-OCR\tesseract.exe’

参考链接:


错误与归纳

缺少对应的包

  • 运行代码报错显示:No module named ‘cv2’
    原因:缺少了OpenCV的cv2模块
    解决方案:需要导入这个包,命令行里执行:
    pip install opencv-python

Python * ** *args **kwargs用法与区别

参考链接:

反斜杠报错

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")

未解决的错误

  • 鼠标放到某些函数上虽然显示函数定义但是没有进一步显示细节,显示:您需要配置好的Python 2 SDK来渲染epydoc.sourceforge.net/"> Epydoc docstring
    You need configured Python 2 SDK to render Epydoc docstrings
    在这里插入图片描述

原因:未知
解决方案:未知

技巧

Pycharm

  • 鼠标放置在类、函数、方法上,Ctrl+鼠标左键可以进入函数的说明文档

  • 在定义的函数底下用三个引号写的注释,可以用鼠标放到引用处快速预览
    在这里插入图片描述

  • 代码补全:settings>keymap>参考链接
    在这里插入图片描述

markdown

数学符号输入

  • 9
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

咬着棒棒糖闯天下

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值