小白的opencv学习笔记v2.0

OpenCV

序章

介绍

OpenCV是计算机视觉中经典的专用库,其支持多语言、跨平台,功能强大。 OpenCV-Python为OpenCV提供了Python接口,使得使用者在Python中能够调用C/C++,在保证易读性和运行效率的前提下,实现所需的功能。 OpenCV-Python Tutorials是官方提供的文档,其内容全面、简单易懂,使得初学者能够快速上手使用。

  • OpenCV-Python
    • OpenCV-Python是旨在解决计算机视觉问题的Python专用库。
    • 与C/C++之类的语言相比,Python速度较慢。也就是说,可以使用C/C++轻松扩展Python,这使我们能够用C/C++编写计算密集型代码并创建可用作Python模块的Python包装器。这给我们带来了两个好处:首先,代码与原始C/C++代码一样快(因为它是在后台运行的实际C++代码),其次,在Python中比C/C++编写代码更容易。OpenCV-Python是原始OpenCV C++实现的Python包装器。
    • OpenCV-Py利用了Numpy。所有OpenCV数组结构都与Numpy数组相互转换。这也使与使用Numpy的其他库(例如SciPy和Matplotlib)的集成变得更加容易。

安装

anaconda新建环境,pip install numpy,openCV-python,matploylib

然后在pycharm中为该解释器添加cv2路径,方便代码补全

图像入门

OpenCV中的坐标系是一个左上角为原点的平面直角坐标系,x轴向右延伸,y轴向下延伸

imread

  • 功能

    读取图像

  • 参数

    • 图像的完整路径或者相对路径
    • 读取图像的方式
      • cv.IMREAD_COLOR:加载彩色图像。任何图像的透明度都会被忽视。它是默认标志。
      • cv.IMREAD_GRAYSCALE:以灰度模式加载图像
      • cv.IMREAD_UNCHANGED:加载图像,包括alpha通道

注意:除了这三个标志,你可以分别简单地传递整数1、0或-1。

  • example
import numpy as np
import cv2
img = cv2.imread('路径',参数)
  • 注意

    即使图像路径错误,它也不会引发任何错误,但是 print img 会给出 None

imshow

  • 功能

    显示图像,窗口自动适合图像尺寸

  • 参数

    第一个参数是窗口名称,它是一个字符串。第二个参数是我们的对象。你可以根据需要创建任意多个窗口,但可以使用不同的窗口名称。

  • example

    cv.imshow('image',img)
    cv.waitKey(0)
    cv.destroyAllWindows()
    
  • waitkey

    一个键盘绑定函数。其参数是以毫秒为单位的时间。该函数等待任何键盘事件指定的毫秒。如果在这段时间内按下任何键,程序将继续运行。如果0被传递,它将无限期地等待一次敲击键。

  • destroyAllWindows

    只会破坏我们创建的所有窗口。如果要销毁任何特定的窗口,请使用函数cv.destroyWindow()在其中传递确切的窗口名称作为参数。

注意 在特殊情况下,你可以创建一个空窗口,然后再将图像加载到该窗口。在这种情况下,你可以指定窗口是否可调整大小。这是通过功能cv.namedWindow()完成的。默认情况下,该标志为cv.WINDOW_AUTOSIZE。但是,如果将标志指定为cv.WINDOW_NORMAL,则可以调整窗口大小。当图像尺寸过大以及向窗口添加跟踪栏时,这将很有帮助。

cv.namedWindow('image',cv.WINDOW_NORMAL)
cv.imshow('image',img)
cv.waitKey(0)
cv.destroyAllWindows()

imwrite

  • 功能

    保存图像

  • 参数

    第一个参数是文件名,第二个参数是要保存的图像。 cv.imwrite(‘messigray.png’,img)

    保存格式:PNG

  • example

    import numpy as np
    import cv2 as cv
    img = cv.imread('messi5.jpg',0)
    cv.imshow('image',img)
    k = cv.waitKey(0)
    if k == 27: # 等待ESC退出
     cv.destroyAllWindows()
    elif k == ord('s'): # 等待关键字,保存和退出
     cv.imwrite('messigray.png',img)
     cv.destroyAllWindows()
    
  • 警告

    如果使用的是64位计算机,则必须 k = cv.waitKey(0) 按如下所示修改行: k = cv.waitKey(0) & 0xFF

使用matplotlib

  • 介绍

    绘图库,有多种绘图方法

  • example

    import numpy as np
    import cv2 as cv
    from matplotlib import pyplot as plt
    img = cv.imread('messi5.jpg',0)
    plt.imshow(img, cmap = 'gray', interpolation = 'bicubic')
    plt.xticks([]), plt.yticks([]) # 隐藏 x 轴和 y 轴上的刻度值
    plt.show()
    
  • 警告

    OpenCV加载的彩色图像处于BGR模式。但是Matplotlib以RGB模式显示。因此,如果使用OpenCV读取彩色图像,则Matplotlib中将无法正确显示彩色图像。

  • 参考链接

    [Matplotlib绘图样式及其功能](pyplot — Matplotlib 2.0.2 documentation)

    Matplotlib使用教程(保姆级说明教程) - 知乎 (zhihu.com)

VideoCapture

  • 功能1

    从相机中读取视频

  • 步骤

    创建一个 VideoCapture 对象。它的参数可以是设备索引或视频文件的名称。设备索引就是指定哪个摄像头的数字。正常情况下,一个摄像头会被连接。所以我简单地传0(或-1)。你可以通过传递1来选择第二个相机,以此类推。

  • example

    import numpy as np
    import cv2 as cv
    cap = cv.VideoCapture(0)
    if not cap.isOpened():
     print("Cannot open camera")
     exit()
    while True:
     # 逐帧捕获
     ret, frame = cap.read()
     # 如果正确读取帧,ret为True
     if not ret:
     print("Can't receive frame (stream end?). Exiting ...")
     break
        # 我们在框架上的操作到这里
     gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY)
     # 显示结果帧e
     cv.imshow('frame', gray)
     if cv.waitKey(1) == ord('q'):
     break
    # 完成所有操作后,释放捕获器
    cap.release()
    cv.destroyAllWindows()
    
  • cap.read() 返回布尔值( True / False )。如果正确读取了帧,它将为 True 。因此,你可以通过检查此返回值来检查视频的结尾。

  • 通过cap.isOpened()方法检查cap是否已初始化。如果是 True ,那么确定。否则,使用cap.open()打开它

  • 还可以使用 cap.get(propId) 方法访问该视频的某些功能,其中propId是0到18之间的一个数字。每个数字表示视频的属性(如果适用于该视频),并且可以显示完整的详细信息在这里看到:cv::VideoCapture::get()。其中一些值可以使用 cap.set(propId,value) 进行修改。value是你想要的新值

  • 例如,我可以通过 cap.get(cv.CAP_PROP_FRAME_WIDTH) 和 cap.get(cv.CAP_PROP_FRAME_HEIGHT)检查框架的宽度和高度。默认情况下,它的分辨率为640x480。但我想将其修改为320x240。只需使用和即可。 ret = cap.set(cv.CAP_PROP_FRAME_WIDTH,320) and ret =cap.set(cv.CAP_PROP_FRAME_HEIGHT,240) .

  • 功能2

    播放视频。与从相机捕获相同,只是用视频文件名更改摄像机索引。另外,在显示框架时,请使用适当的时间 cv.waitKey() 。如果太小,则视频将非常快,而如果太大,则视频将变得很慢(这就是显示慢动作的方式)。正常情况下25毫秒就可以了。

  • example

    import numpy as np
    import cv2 as cv
    cap = cv.VideoCapture('vtest.avi')
    while cap.isOpened():
        ret, frame = cap.read()
     # 如果正确读取帧,ret为True
     	if not ret:
     	print("Can't receive frame (stream end?). Exiting ...")
     	break
     	gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY)
     	cv.imshow('frame', gray)
     	if cv.waitKey(1) == ord('q'):
     	break
    cap.release()
    cv.destroyAllWindows()
    

注意 确保安装了正确的 ffmpeg 或 gstreamer 版本。有时,使用视频捕获(Video Capture)是一件令人头疼的事情,主要原因是错误地安装了 ffmpeg / gstreamer。

VideoWriter

  • 功能

    保存视频。

    该类四个参数:

    • 指定输出文件名(例如: output.avi)。
    • 指定 FourCC 代码(详见下一段)。
    • 传递帧率的数量和帧大小
    • 颜色标志。如果为True ,编码器期望颜色帧,否则它与灰度帧一起工作。
  • 使用

    FourCC代码作为MJPG的cv.VideoWriter_fourcc(‘M’,‘J’,‘P’,‘G’) or cv.VideoWriter_fourcc(*‘MJPG’)

    类似的可以解码XVID等

  • example

    cap = cv.VideoCapture(0)
    # 定义编解码器并创建VideoWriter对象
    fourcc = cv.VideoWriter_fourcc(*'XVID')
    out = cv.VideoWriter('output.avi', fourcc, 20.0, (640, 480))
    while cap.isOpened():
     ret, frame = cap.read()
     if not ret:
     print("Can't receive frame (stream end?). Exiting ...")
     break
     frame = cv.flip(frame, 0)
     # 写翻转的框架
     out.write(frame)
     cv.imshow('frame', frame)
     if cv.waitKey(1) == ord('q'):
     break
    # 完成工作后释放所有内容
    cap.release()
    out.release()
    cv.destroyAllWindows()
    
  • 参考资料

    FourCC:FourCC - 维基百科 (wikipedia.org)用于指定视频编解码器的4字节代码

    可用代码列表可在fourcc.org中:http://www.fourcc.org/codecs.php 找到

    Windows:DIVX

    在OSX中:MJPG(.mp4),DIVX(.avi),X264(.mkv)

OpenCV中的绘图功能

常见参数

  • img:要绘制形状的图像

  • color:形状的颜色。对于BGR,将其作为元组传递,例如:(255,0,0)对于蓝色。对于灰度,只需传递标量值即可。

  • 厚度:线或圆等的粗细。如果对闭合图形(如圆)传递 -1 ,它将填充形状。默认厚度= 1

  • lineType:线的类型,是否为8连接线,抗锯齿线等。默认情况下,为8连接线。cv.LINE_AA给出了抗锯齿的线条,看起来非常适合曲线。

line

  • 参数

    传递线的开始和结束坐标

  • example

    # 创建黑色的图像
    img = np.zeros((512,512,3), np.uint8)
    # 绘制一条厚度为5的蓝色对角线
    cv.line(img,(0,0),(511,511),(255,0,0),5)
    

rectangle

  • 参数

    矩形的左上角和右下角

  • cv.rectangle(img,(384,0),(510,128),(0,255,0),3)

circle

  • 参数

    中心坐标和半径

  • cv.circle(img,(447,63), 63, (0,0,255), -1)

ellipse

  • 参数

    • 中心位置(x,y)
    • 轴长度(长轴,短轴)
    • angle是椭圆逆时针方向旋转的角度
    • startAngle和endAngle表示从主轴沿顺时针方向测量的椭圆弧的开始和结束。即给出0和360给出完整的椭圆
  • 使用

    def ellipse(img: Any,
    center: Any,
    axes: Any,
    angle: Any,
    startAngle: Any,
    endAngle: Any,
    color: Any,
    thickness: Any = None,
    lineType: Any = None,
    shift: Any = None) -> None

  • cv.ellipse(img,(256,256),(100,50),0,0,180,255,-1)

polylines

  • 参数

    首先需要顶点的坐标。将这些点组成形状为 ROWSx1x2 的数组,其中 ROWS 是顶点

    数,并且其类型应为int32。

  • example

    pts = np.array([[10,5],[20,30],[70,20],[50,10]], np.int32)
    pts = pts.reshape((-1,1,2))
    cv.polylines(img,[pts],True,(0,255,255))
    

注意 如果第三个参数为False,将获得一条连接所有点的折线,而不是闭合形状。

cv.polylines()可用于绘制多条线。只需创建要绘制的所有线条的列表,然后将其传递给函数即可。所有线条将单独绘制。与为每条线调用cv.line相比,绘制一组线是一种更好,更快的方法

向图像添加文本

  • 参数

    • 要写入的文字数据
    • 放置它的位置坐标(数据开始的左下角)
    • 字体类型(检查cv.putText文档以获取受支持的字体)
    • 字体比例(指定字体大小)
    • 常规的内容(如颜色、厚度、线条类型)为了获得更好的外观,建议使用lineType = cv.LINE_AA
  • example

    font = cv.FONT_HERSHEY_SIMPLEX
    cv.putText(img,'OpenCV',(10,500), font, 4,(255,255,255),2,cv.LINE_AA)
    
  • 参考资料

    椭圆函数中使用的角度不是我们的圆角。有关更多详细信息,请访问此讨论:http://answers.opencv.org/question/14541/angles-in-ellipse-function/

鼠标作为画笔

setMouseCallback

  • 参数

    窗口名字,鼠标回调函数名称

  • 简单演示

    功能:双击画圆

    思路:创建一个鼠标回调函数,该函数在发生鼠标事件时执行。鼠标事件可以是与鼠标相关的任何事物,例如左键按下,左键按下,左键双击等。它为我们提供了每个鼠标事件的坐标(x,y)。通过此活动和地点,我们可以做任何我们喜欢的事情。

    鼠标回调函数格式:def mouse_callback(event, x, y, flags, param)

    • 查看所有可用事件:
    import cv2 as cv
    events = [i for i in dir(cv) if 'EVENT' in i]
    print( events )
    
    • 创建鼠标回调函数具有特定的格式,该格式在所有地方都相同。它仅在功能上有所不同。按ESC键关闭窗口。

      import cv2 as cv
      # 鼠标回调函数
      def draw_circle(event,x,y,flags,param):
       if event == cv.EVENT_LBUTTONDBLCLK:
       cv.circle(img,(x,y),100,(255,0,0),-1)
      # 创建一个黑色的图像,一个窗口,并绑定到窗口的功能
      img = np.zeros((512,512,3), np.uint8)
      cv.namedWindow('image')
      cv.setMouseCallback('image',draw_circle)
      while(1):
       cv.imshow('image',img)
       if cv.waitKey(20) & 0xFF == 27:
       break
      cv.destroyAllWindows()
      

      结果:

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZLHA7tHH-1685890469424)(assets/image-20230527154658974.png)]

  • 更高级的演示

    功能:拖动鼠标来绘制矩形或圆形。我们的鼠标回调函数有两部分,一部分用于绘制矩形,另一部分用于绘制圆形。这个具体的例子对于创建和理解一些交互式应用程序非常有帮助,比如目标跟踪,图像分割地图等等。

    import numpy as np
    import cv2 as cv
    drawing = False # 如果按下鼠标,则为真
    mode = True # 如果为真,绘制矩形。按 m 键可以切换到曲线
    ix,iy = -1,-1
    # 鼠标回调函数
    def draw_circle(event,x,y,flags,param):
     global ix,iy,drawing,mode
     if event == cv.EVENT_LBUTTONDOWN:
     drawing = True
     ix,iy = x,y
     elif event == cv.EVENT_MOUSEMOVE:
     if drawing == True:
     if mode == True:
     cv.rectangle(img,(ix,iy),(x,y),(0,255,0),-1)
     else:
     cv.circle(img,(x,y),5,(0,0,255),-1)
     elif event == cv.EVENT_LBUTTONUP:
     drawing = False
     if mode == True:
     cv.rectangle(img,(ix,iy),(x,y),(0,255,0),-1)
     else:
     cv.circle(img,(x,y),5,(0,0,255),-1)
    
  • 练习:修改代码以绘制一个未填充的矩形

    • 代码:
img = np.zeros((512, 512, 3), np.uint8)

is_mouse = False  # 判断是否按下鼠标
mode = True  # 真则绘制矩形
ix, iy = -1, -1

# 鼠标回调函数
def draw_circle(event, x, y, flags, param):
    global ix, iy, is_mouse, mode
    if event == cv.EVENT_LBUTTONDOWN:
        # 按下左键
        is_mouse = True
        ix, iy = x, y
    elif event == cv.EVENT_MOUSEMOVE:
        if is_mouse:
            # 如果已经按定鼠标了
            if mode:
                pass
            else:
                cv.circle(img, (x, y), 5, (0, 0, 255), -1)
    elif event == cv.EVENT_LBUTTONUP:
        # 松开鼠标左键
        is_mouse = False
        if mode:
            cv.rectangle(img, (ix, iy), (x, y), (0, 255, 0), 4)
        else:
            cv.circle(img,(x,y), 5,(0,0,255), -1)

cv.namedWindow('image')
cv.setMouseCallback('image', draw_circle)
while 1:
    cv.imshow('image', img)
    smile = cv.waitKey(20) & 0xff
    if smile == 27:
        # esc
        break
    elif smile == 109:
        # 'm'切换模式
        mode = (mode + 1) % 2
cv.destroyAllWindows()
  • 结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dPCLAVtB-1685890469425)(assets/image-20230527161519774.png)]

轨迹栏作为调色板

getTrackbarPos,createTrackbar

  • cv.getTrackbarPos

    • 参数
      • 轨迹栏名称
      • 附加到的窗口名称
      • 默认值
      • 最大值
      • 执行的回调函数,每次跟踪栏值更改
    • def createTrackbar(trackbarName: Any,
      windowName: Any,
      value: Any,
      count: Any,
      onChange: Any) -> None
  • 代码演示

    创建一个简单的应用程序,以显示指定的颜色。

    有一个显示颜色的窗口,以及三个用于指定BGR颜色的跟踪栏。滑动轨迹栏,并相应地更改窗口颜色。默认情况下,初始颜色设置为黑色

    轨迹栏的以重要作用是将其作为按钮或开关。默认情况下opencv没有按钮功能。在我们的应用中,创建一个开关,只有当开关为ON时,该程序才能在其中运行,否则屏幕始终为黑色

    import cv2 as cv
    import numpy as np
    
    def nothing(x):
        pass
    
    img = np.zeros((300,512,3), np.uint8)
    cv.namedWindow('image')
    # 创建颜色变化的轨迹栏
    cv.createTrackbar('R', 'image', 0, 255, nothing)
    cv.createTrackbar('G','image',0,255,nothing)
    cv.createTrackbar('B','image',0,255,nothing)
    # 为 ON/OFF 功能创建开关
    switch = '0 : OFF \n1 : ON'
    cv.createTrackbar(switch, 'image',0 ,1 ,nothing)
    while 1:
        cv.imshow('image', img)
        k = cv.waitKey(1) & 0xff
        if k == 27:
            break
        # 得到四条轨迹的当前位置
        r = cv.getTrackbarPos('R', 'image')
        g = cv.getTrackbarPos('G', 'image')
        b = cv.getTrackbarPos('B', 'image')
        s = cv.getTrackbarPos(switch, 'image')
        if s == 0:
            img[:] = 0
        else:
            img[:]= [np.uint8(b),np.uint8(g),np.uint8(r)]
    cv.destroyAllWindows()
    

图像的基本操作

访问和修改像素值

可以通过行和列坐标来访问像素值。对于 BGR 图像,它返回一个由蓝色、绿色和红色值组成的数组。

  • warning

    Numpy是用于快速数组计算的优化库。因此,简单地访问每个像素值并对其进行修改将非常缓慢,因此不建议使用。

    注意 上面的方法通常用于选择数组的区域,例如前5行和后3列。对于单个像素访问,Numpy数组方法array.item()和array.itemset())被认为更好,但是它们始终返回标量。如果要访问所有B,G,R值,则需要分别调用所有的array.item()。

更好的修改方法

# 访问 RED 值
>>> img.item(10,10,2)
# 修改 RED 值
>>> img.itemset((10,10,2),100)
>>> img.item(10,10,2)

访问图像属性

  • 图像属性包括行数,列数和通道数,图像数据类型,像素数等。-

  • 图像的形状可通过 img.shape 访问。它返回行,列和通道数的元组(如果图像是彩色的)

注意 如果图像是灰度的,则返回的元组仅包含行数和列数,因此这是检查加载的图像是灰度还是彩色的好方法。

  • 像素总数

    通过img.size访问

  • 图像数据类型

    通过img.dtype过的

    注意 dtype在调试时非常重要,因为OpenCV-Python代码中的大量错误是由无效的数据类型引起的。

图像感兴趣区域ROI(重点)

对于图像中的眼睛检测,首先对整个图像进行人脸检测。在获取人脸图像时,我们只选择人脸区域,搜索其中的眼睛,而不是搜索整个图像。它提高了准确性(因为眼睛总是在面部上 )和性能(因为我们搜索的区域很小)。

ball = img[280:340, 330:390]
img[273:333, 100:160] = ball

拆分和合并图像通道

有时候需要分别处理图像的BGR通道,所以需要将图像拆分为单个通道。其他情况下,可能需要将这些单独的频道加入BGR图片

  • 方式

    b,g,r = cv.split(img)
    img = cv.merge((b,g,r))
    # 获取B
    b = img [:, :, 0]
    #将红色像素置0
    img [:, :, 2] = 0
    
  • 警告

    cv.split() 是一项耗时的操作(就时间而言)。因此,仅在必要时才这样做。否则请进行Numpy索引。

为图像设置边框(填充)

  • cv.copyMakeBorder

    在图像周围创建边框,在卷积运算、零填充等方面有更多应用

    参数

    • src - 输入图像

    • topbottomleftright 边界宽度(以相应方向上的像素数为单位)

    • borderType - 定义要添加哪种边框的标志。它可以是以下类型:

      cv.BORDER_CONSTANT - 添加恒定的彩色边框。该值应作为下一个参数给出。

      cv.BORDER_REFLECT - 边框将是边框元素的镜像,如下所示: *fedcba | abcdefgh |hgfedcb

      cv.BORDER_REFLECT_101cv.BORDER_DEFAULT与上述相同,但略有变化,例如: gfedcb | abcdefgh | gfedcba

      cv.BORDER_REPLICATE最后一个元素被复制,像这样: aaaaaa | abcdefgh | hhhhhhh

      cv.BORDER_WRAP用于指定在对图像进行某些操作时如何处理超出图像边界的像素。该常量表示当访问超出图像边界的像素时,应该从相邻的另一侧开始读取图像数据。这种边界填充模式适用于图像拼接和纹理合成等需要保持图像连续性的场景。它看起来像这样: cdefgh | abcdefgh | abcdefg

    • value -边框的颜色,如果边框类型为cv.BORDER_CONSTANT

如果matplotlib不显示图像,则加上matplotlib.use(‘TkAgg’)

图像上的算术运算

图像加法

  • cv.add

    • 功能

      添加两个图像,两个图像应具有相同的深度和类型,或者第二个图像可以只是一个标量值。

      注意 OpenCV加法和Numpy加法之间有区别。OpenCV加法是饱和运算,而Numpy加法是模运算。

      example

      >>> x = np.uint8([250])
      >>> y = np.uint8([10])
      >>> print( cv.add(x,y) ) # 250+10 = 260 => 255
      [[255]]
      >>> print( x+y ) # 250+10 = 260 % 256 = 4
      [4]
      

      当添加两个图像时,它将更加可见。OpenCV功能将提供更好的结果。因此,始终最好坚持使用OpenCV功能

图像融合

这也是图像加法,但是对图像赋予不同的权重,以使其具有融合或透明的感觉

  • cv.addWeighted

    • 功能

      d s t = α ⋅ i m g 1 + β ⋅ i m g 2 + γ dst=\alpha \cdot img1+\beta \cdot img2 + \gamma dst=αimg1+βimg2+γ

      在这里gamma被视为0

    • example

      img1 = cv.imread('yuan.png')
      img2 = cv.imread('qiong.png')
      dst = cv.addWeighted(img1,0.7,img2,0.3,0)
      cv.imshow('dst',dst)
      cv.waitKey(0)
      cv.destroyAllWindows()
      

      result

      image-20230528110344309

按位运算

包括AND、OR、NOT和XOR操作。

它们在提取图像的任何部分,定义和处理非矩形 ROI 等方面非常有用。

  • 函数

    cv.bitwise_and(src1, src2[, dst[, mask]] → dst # 位操作: 与
    cv.bitwise_or(src1, src2[, dst[, mask]] → dst # 位操作: 或
    cv.bitwise_xor(src1, src2[, dst[, mask]] → dst # 位操作: 与或
    cv.bitwise_not(src1, src2[, dst[, mask]] → dst # 位操作: 非(取反)

  • example:bitwise_and

    • 当src1和src2代表的两个图像矩阵数组的大小相同时,结果矩阵元素的值为:
      dst(I)=src1(I)∧src2(I) if mask(I)≠0
    • 当src1为矩阵数组而src2为标量时,结果矩阵元素的值为:
      dst(I)=src1(I)∧src2 if mask(I)≠0
    • 如果是常数,OpenCV会转成一个代表BGRA的4元组参与运算,该四元组的第一个元素就是该常数,其他元素为0。
      src1为标量而src2为矩阵数组时,结果矩阵元素的值为:
      dst(I)=src1∧src2(I) if mask(I)≠0
      标量取值与上面场景相同。
  • mask

    mask的最大作用:让我们只关注我们感兴趣的图像部分。

    Masking(掩膜运算) 即图与掩膜的“按位与”运算:

    原图中的每个像素和掩膜(Mask)中的每个对应像素进行按位与运算,

    如果为真,结果是原图的值【这是重点】

    如果为假,结果就是零

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TmXmFsqd-1685890469425)(C:\Users\86189\AppData\Roaming\Typora\typora-user-images\image-20230528113335963.png)]

  • example

    如何改变一个图像的特定区域。 我想把穹的标志放在一个图像上面。如果我添加两个图像,它会改变颜色。如果我混合它,我得到一个透明的效果。但我希望它是不透明的。如果是一个矩形区域,我可以使用 ROI,就像我们在上一章中所做的那样。但是 OpenCV 的 logo 不是长方形的。所以你可以使用如下的按位操作来实现:

    # 加载两张图片
    img1 = cv.imread('yuan.jpg')
    img2 = cv.imread('qiong.png')
    img2 = cv.resize(img2, (200,200))
    # 我想把logo放在左上角,所以我创建了ROI
    rows,cols,channels = img2.shape
    roi = img1[0:200, 0:200]
    # 现在创建logo的掩码,并同时创建其相反掩码
    img2gray = cv.cvtColor(img2,cv.COLOR_BGR2GRAY)
    ret, mask = cv.threshold(img2gray, 150, 255, cv.THRESH_BINARY)
    mask_inv = cv.bitwise_not(mask)
    cv.imshow('', mask_inv)
    cv.waitKey(0)
    # 现在将ROI中logo的区域涂黑
    img1_bg = cv.bitwise_and(roi,roi,mask = mask_inv)
    cv.imshow('', img1_bg)
    cv.waitKey(0)
    # 仅从logo图像中提取logo区域
    img2_fg = cv.bitwise_and(img2,img2,mask = mask)
    cv.imshow('', img2_fg)
    cv.waitKey(0)
    # 将logo放入ROI并修改主图像
    dst = cv.add(img1_bg,img2_fg) # 不太想要
    img1[0:200, 0:200] = img1_bg
    img1[200:400, 200:400] = img2_fg
    cv.imshow('res',img1)
    cv.waitKey(0)
    cv.destroyAllWindows()
    

    res

    image-20230528112254609

性能衡量和提升技术

使用openCV衡量性能

  • 目标

    在图像处理中,由于每秒要处理大量操作,因此必须使代码不仅提供正确的解决方案,而且还必须以最快的方式提供。

  • getTickCount

    • 功能

      返回从参考事件(如打开机器的那一刻)到调用此函数那一刻之间的时钟周期数。因此,如果在函数执行之前和之后调用它,则会获得用于执行函数的时钟周期数。

  • getTickFrequency

    • 功能

      返回时钟周期的频率或每秒的时钟周期数。

  • example

    e1 = cv.getTickCount()
    # 你的执行代码
    e2 = cv.getTickCount()
    time = (e2 - e1)/ cv.getTickFrequency()
    

    注意 可以使用时间模块执行相同的操作:time.time()

OpenCV中的默认优化

许多 OpenCV 函数都是使用 SSE2、 AVX 等进行优化的。 它还包含未优化的代码。因此,如果我们的系统支持这些特性,我们就应该利用它们(几乎所有现代的处理器都支持它们)。在编译时默认启用它。因此,如果启用了 OpenCV,它将运行优化的代码,否则它将运行未优化的代码。

拓展:SSE2代表流式SIMD扩展2,它是Intel CPU中的指令集扩展。它支持一种称为单指令多数据(SIMD)的并行处理方式,这意味着它可以同时对多个数据执行相同的操作。 SSE2通常用于图像处理、视频编解码以及3D图形运算等方面。

而AVX(高级向量扩展)是Intel CPU中更高级别的指令集扩展。它进一步提高了SIMD指令的效率,并且可以同时处理更多的数据。 AVX比SSE2更适合大规模并行计算任务,如科学计算、机器学习和神经网络训练等。

总之,SSE2和AVX都是一些CPU指令集的扩展,它们可以加快一些特定类型的计算。

  • cvUseoptimized 检查是否启用 / 禁用和 cvSetuseoptimized 以启用 / 禁用它

  • example

    cv.setUseOptimized(False)
    cv.useOptimized()
    

    对上文的按位运算优化快了0.3s多

在IPython中衡量性能

Ipython介绍 :IPython是一个交互式的Python shell,它提供了比标准Python shell更多的功能和工具。IPython允许在命令行界面中执行Python代码,并提供了一些方便的功能,例如自动完成功能、历史记录、代码调试、内置帮助等等。此外,IPython还支持对话式编程,可以将Python代码与文本、图片、视频等内容结合起来,形成可交互的笔记本文件(Jupyter Notebook),这使得IPython成为数据分析、科学计算及机器学习领域中非常流行的工具之一。

例如,你知道以下哪个加法运算更好, x = 5 ; y = x ∗ ∗ 2 , x = 5 ; y = x ∗ x , x = n p . u i n t 8 ( [ 5 ] ) ; y = x ∗ x 或 y = n p . s q u a r e ( x ) x = 5; y = x**2, x = 5; y = x*x, x = np.uint8([5]); y = x*x 或 y = np.square(x) x=5;y=x2,x=5;y=xx,x=np.uint8([5]);y=xxy=np.square(x)?我们将在IPython shell中使用timeit得到答案。

In [10]: x = 5
In [11]: %测时 y=x**2
10000000 loops, best of 3: 73 ns per loop
In [12]: %测时 y=x*x
10000000 loops, best of 3: 58.3 ns per loop
In [15]: z = np.uint8([5])
In [17]: %测时 y=z*z
1000000 loops, best of 3: 1.25 us per loop
In [19]: %测时 y=np.square(z)
1000000 loops, best of 3: 1.16 us per loop

注意:Python标量操作比Numpy标量操作快。因此,对于包含一两个元素的运算,Python标量比Numpy数组好。当数组大小稍大时,Numpy会占优势。

通常,OpenCV函数比Numpy函数要快。因此,对于相同的操作,首选OpenCV功能。但是,可能会有例外,尤其是当Numpy处理视图而不是副本时

性能优化技术

有几种技术和编码方法可以充分利用 Python 和 Numpy 的最大性能。这里只注明相关信息,并提供重要信息来源的链接。这里要注意的主要事情是,首先尝试以一种简单的方式实现算法。一旦它运行起来,分析它,找到瓶颈并优化它们。

  1. 尽量避免在Python中使用循环,尤其是双/三重循环等。它们本来就很慢。

  2. 由于Numpy和OpenCV已针对向量运算进行了优化,因此将算法/代码向量化到最大程度。

  3. 利用缓存一致性。(多个处理器或核心的缓存副本之间数据的一致性)

  4. 除非需要,否则切勿创建数组的副本。尝试改用视图。数组复制是一项昂贵的操作。

即使执行了所有这些操作后,如果你的代码仍然很慢,或者不可避免地需要使用大循环,请使用Cython等其他库来使其更快。

其他资源

  1. Python优化技术:http://wiki.python.org/moin/PythonSpeed/PerformanceTips

  2. Scipy讲义- 高级Numpy:http://scipy-lectures.github.io/advanced/advanced_numpy/index.html#advanced-numpy

  3. IPython中的时序和性能分析:http://pynash.org/2013/03/06/timing-and-profiling/

改变颜色空间(重点)

  • cvtColor

    • 参数

      cvtColor(input_image, flag),其中flag决定转换的类型

      要获取更多flag,只需运行以下命令:

      import cv2 as cv
      flags = [i for i in dir(cv) if i.startswith('COLOR_')]
      print(flags)
      

      注意 HSV的色相范围为[0,179],饱和度范围为[0,255],值范围为[0,255]。不同的软件使用不同的规模。因此,如果你要将OpenCV值和它们比较,你需要将这些范围标准化。

对象追踪

在HSV中比在BGR颜色空间中更容易表示颜色。

提取蓝色 example:

cap = cv.VideoCapture(0)
while(1):
 # 读取帧
 _, frame = cap.read()
 # 转换颜色空间 BGR 到 HSV
 hsv = cv.cvtColor(frame, cv.COLOR_BGR2HSV)
 # 定义HSV中蓝色的范围
 lower_blue = np.array([110,50,50])
 upper_blue = np.array([130,255,255])
 # 设置HSV的阈值使得只取蓝色
 mask = cv.inRange(hsv, lower_blue, upper_blue)
 # 将掩膜和图像逐像素相加
 res = cv.bitwise_and(frame,frame, mask= mask)
 cv.imshow('frame',frame)
 cv.imshow('mask',mask)
 cv.imshow('res',res)
 k = cv.waitKey(5) & 0xFF
 if k == 27:
 break
cv.destroyAllWindows()

如何找到想要追踪的HSV值?

它非常简单,你可以使用相同的函数cv.cvtColor()。你只需传递你想要的BGR值,而不是传递图像。例如,要查找绿色的HSV值,可在 py终端中尝试:

>>> green = np.uint8([[[0,255,0 ]]])
>>> hsv_green = cv.cvtColor(green,cv.COLOR_BGR2HSV)
>>> print( hsv_green )
[[[ 60 255 255]]]

提取多个彩色对象:

  1. 将图像转换为HSV颜色空间。
  2. 确定每个对象的颜色范围。这可以通过手动调整阈值进行完成,或者使用自适应阈值算法,例如Otsu阈值化。
  3. 对于每个颜色范围,使用inRange函数生成一个布尔掩码。
  4. 将所有的颜色掩码组合成一个最终掩码,通过将所有掩码相加或合并来完成此操作。这可以使用位运算符(例如AND,OR)来完成。
import cv2

# 读取图像
img = cv2.imread('image.jpg')

# 转换颜色空间为HSV
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

# 定义颜色范围
lower_blue = np.array([100,50,50])
upper_blue = np.array([130,255,255])
lower_green = np.array([50,50,120])
upper_green = np.array([70,255,255])

# 创建掩码
blue_mask = cv2.inRange(hsv, lower_blue, upper_blue)
green_mask = cv2.inRange(hsv, lower_green, upper_green)

# 组合掩码
final_mask = blue_mask + green_mask

# 应用掩码到原始图像
result = cv2.bitwise_and(img, img, mask=final_mask)

# 显示最终结果
cv2.imshow('Result', result)
cv2.waitKey(0)
cv2.destroyAllWindows()

图像的几何变换

变换

OpenCV提供了两个转换函数cv.warpAffinecv.warpPerspectivecv.warpAffine采用2x3转换矩阵,而cv.warpPerspective采用3x3转换矩阵作为输入。

缩放

  • cv.resize()

    • 功能

      只是调整图像的大小。图像的大小可以手动指定,也可以指定缩放比例。也可使用不同的插值方法。

    • 插值方法

      首选的插值方法是cv.INTER_AREA用于缩小,*cv.INTER_CUBIC(慢)和cv.INTER_LINEAR用于缩放。默认情况下,出于所有调整大小的目的,使用的插值方法为cv.INTER_LINEAR*。

    • example

      import numpy as np
      import cv2 as cv
      img = cv.imread('yuan.jpg')
      res = cv.resize(img,None,fx=2, fy=2, interpolation = cv.INTER_CUBIC)
      #或者
      height, width = img.shape[:2]
      res = cv.resize(img,(2*width, 2*height), interpolation = cv.INTER_CUBIC)
      

平移

平移是物体位置的移动。如果知道在(x,y)方向上的位移,则将其设为(t_x,t_y),你可以创建转换矩阵 M \mathbf{M} M,如下所示:

M = [ 1 0 t x 0 1 t y ] M = \begin{bmatrix} 1 & 0 & t_x \\ 0 & 1 & t_y \end{bmatrix} M=[1001txty]

可以将其放入np.float32类型的Numpy数组中,并将其传递给cv.warpAffine函数。参见下面偏移为(100, 50)的示例

import numpy as np
import cv2 as cv
img = cv.imread('yuan.jpg',0)
rows,cols = img.shape
M = np.float32([[1,0,100],[0,1,50]])
dst = cv.warpAffine(img,M,(cols,rows))
cv.imshow('img',dst)
cv.waitKey(0)
cv.destroyAllWindows()
  • warnning

    cv.warpAffine函数的第三个参数是输出图像的大小,其形式应为 (width,height) 。记住width =列数, height =行数。

旋转

图像旋转角度为θ是通过以下形式的变换矩阵实现的:

M = [ c o s θ − s i n θ s i n θ c o s θ ] M = \begin{bmatrix} cos\theta & -sin\theta \\ sin\theta & cos\theta \end{bmatrix} M=[cosθsinθsinθcosθ]

OpenCV提供了可缩放的旋转以及可调整的旋转中心,因此可以在自己喜欢的任何位置旋转。

[ α β ( 1 − α ) ⋅ c e n t e r . x − β ⋅ c e n t e r . y − β α β ⋅ c e n t e r . x + ( 1 − α ) ⋅ c e n t e r . y ] \begin{bmatrix} \alpha & \beta & (1- \alpha ) \cdot center.x - \beta \cdot center.y \\ - \beta &\alpha & \beta \cdot center.x + (1- \alpha ) \cdot center.y \end{bmatrix} [αββα(1α)center.xβcenter.yβcenter.x+(1α)center.y]

α = s c a l e ⋅ cos ⁡ θ , β = s c a l e ⋅ sin ⁡ θ \begin{array}{l} \alpha = scale \cdot \cos \theta , \\ \beta = scale \cdot \sin \theta \end{array} α=scalecosθ,β=scalesinθ

为了找到此转换矩阵,OpenCV提供了一个函数cv.getRotationMatrix2D。请检查以下示例,该示例将图像相对于中心旋转90度而没有任何缩放比例。

img = cv.imread('yuan.jpg',0)
rows,cols = img.shape
# cols-1 和 rows-1 是坐标限制
M = cv.getRotationMatrix2D(((cols-1)/2.0,(rows-1)/2.0),90,1)
dst = cv.warpAffine(img,M,(cols,rows))

def getRotationMatrix2D(center: Any,
angle: Any,
scale: Any) -> None

仿射变换

在仿射变换中,原始图像中的所有平行线在输出图像中仍将平行。为了找到变换矩阵,我们需要输入图像中的三个点及其在输出图像中的对应位置。然后cv.getAffineTransform将创建一个2x3矩阵,该矩阵将传递给cv.warpAffine

  • example

    img = cv.imread('yuan.jpg')
    rows,cols,ch = img.shape
    pts1 = np.float32([[50,50],[200,50],[50,200]])
    pts2 = np.float32([[10,100],[200,50],[100,250]])
    M = cv.getAffineTransform(pts1,pts2)
    dst = cv.warpAffine(img,M,(cols,rows))
    plt.subplot(121),plt.imshow(img),plt.title('Input')
    plt.subplot(122),plt.imshow(dst),plt.title('Output')
    plt.show()
    
    6c1e5311477bbe04145d736a4e4d229

透视变换

对于透视变换,需要3x3变换矩阵。即使在转换后,直线也将保持直线。要找到此变换矩阵,需要在输入图像上有4个点,在输出图像上需要相应的点。在这四个点中,其中三个不应共线。然后可以通过函数cv.getPerspectiveTransform找到变换矩阵。然后将cv.warpPerspective应用于此3x3转换矩阵。

  • example
img = cv.imread('yuan.jpg')
rows,cols,ch = img.shape
pts1 = np.float32([[56,65],[368,52],[28,387],[389,390]])
pts2 = np.float32([[0,0],[300,0],[0,300],[300,300]])
M = cv.getPerspectiveTransform(pts1,pts2)
dst = cv.warpPerspective(img,M,(300,300))
plt.subplot(121),plt.imshow(img),plt.title('Input')
plt.subplot(122),plt.imshow(dst),plt.title('Output')
plt.show()
image-20230531212245706

图像阈值

对于每个像素,应用相同的阈值。如果像素值小于阈值,则将其设置为0,否则将其设置为最大值

  • cv.threshold

    • 参数(从前往后)

      • 源图像,它应该是灰度图像

      • 阈值,用于对像素值进行分类

      • 分配给超过阈值的像素值的最大值

      • 不同类型的阈值

        • cv.THRESH_BINARY

          cv.THRESH_BINARY_INV

          cv.THRESH_TRUNC

          cv.THRESH_TOZERO

          cv.THRESH_TOZERO_INV

    • 返回值

      第一个是使用的阈值,第二个输出是阈值后的图像

  • example

    img = cv.imread('qiong.png',0)
    ret,thresh1 = cv.threshold(img,127,255,cv.THRESH_BINARY)
    ret,thresh2 = cv.threshold(img,127,255,cv.THRESH_BINARY_INV)
    ret,thresh3 = cv.threshold(img,127,255,cv.THRESH_TRUNC)
    ret,thresh4 = cv.threshold(img,127,255,cv.THRESH_TOZERO)
    ret,thresh5 = cv.threshold(img,127,255,cv.THRESH_TOZERO_INV)
    titles = ['Original Image','BINARY','BINARY_INV','TRUNC','TOZERO','TOZERO_INV']
    images = [img, thresh1, thresh2, thresh3, thresh4, thresh5]
    for i in range(6):
    plt.subplot(2,3,i+1),plt.imshow(images[i],'gray')
     plt.title(titles[i])
     plt.xticks([]),plt.yticks([])
    plt.show()
    
    image-20230531213513557
  • 自适应阈值

    我们可以使用一个全局值作为阈值,但这可能并非在所有情况下都很好,例如,如果图像在不同区域具有不同的光照条件。在这种情况下,自适应阈值阈值化可以提供帮助。在此,算法基于像素周围的小区域确定像素的阈值。因此,对于同一图像的不同区域,我们获得了不同的阈值,这为光照度变化的图像提供了更好的结果。

    • 方法cv.adaptiveThreshold还包含三个输入参数

      • adaptiveMethod决定阈值是如何计算的:

        cv.ADAPTIVE_THRESH_MEAN_C::阈值是邻近区域的平均值减去常数C

        cv.ADAPTIVE_THRESH_GAUSSIAN_C:阈值是邻域值的高斯加权总和减去常数C

      • BLOCKSIZE确定附近区域的大小,C是从邻域像素的平均或加权总和中减去的一个常数。

    网图:

    image-20230531213942500
  • Otsu的二值化

    Otsu避免了全局阈值化的死板。

    考虑仅具有两个不同图像值的图像(双峰图像),其中直方图将仅包含两个峰。一个好的阈值应该在这两个值的中间。类似地,Otsu的方法从图像直方图中确定最佳全局阈值。

    为此,使用了cv.threshold作为附加标志传递。阈值可以任意选择。然后,算法找到最佳阈值,该阈值作为第一输出返回。

    # 全局阈值
    ret1,th1 = cv.threshold(img,127,255,cv.THRESH_BINARY)
    # Otsu阈值
    ret2,th2 = cv.threshold(img,0,255,cv.THRESH_BINARY+cv.THRESH_OTSU)
    # 高斯滤波后再采用Otsu阈值
    blur = cv.GaussianBlur(img,(5,5),0)
    ret3,th3=cv.threshold(blur,0,255,cv.THRESH_BINARY+cv.THRESH_OTSU)
    
    image-20230531214547853

图像平滑

  • 2D卷积(图像过滤)

    • 目标

      与一维信号一样,还可以使用各种低通滤波器(LPF),高通滤波器(HPF)等对图像进行滤波。LPF有助于消除噪声,使图像模糊等。HPF滤波器有助于在图像中找到边缘。

    • 函数cv.filter2D

      • 功能

        将内核与图像进行卷积

        5x5平均滤波器内核 K = 1 25 [ 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 ] K = \frac{1}{25} \begin{bmatrix} 1 & 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 & 1 \\ 1 & 1 &1 & 1 & 1 \\ 1 & 1 & 1 & 1 & 1 \end{bmatrix} K=251 1111111111111111111111111

        操作如下:保持这个内核在一个像素上,将所有低于这个内核的25个像素相加,取其平均值,然后用新的平均值替换中心像素。它将对图像中的所有像素继续此操作

      • example

        img = cv.imread('opencv_logo.png')
        kernel = np.ones((5,5),np.float32)/25
        dst = cv.filter2D(img,-1,kernel)
        plt.subplot(121),plt.imshow(img),plt.title('Original')
        plt.xticks([]), plt.yticks([])
        plt.subplot(122),plt.imshow(dst),plt.title('Averaging')
        plt.xticks([]), plt.yticks([])
        plt.show()
        
  • 图像模糊

    通过将图像与低通滤波器内核进行卷积来实现图像模糊。这对于消除噪音很有用。它实际上从图像中消除了高频部分(例如噪声,边缘)。因此,在此操作中边缘有些模糊。(有一些模糊技术也可以不模糊边缘)。

    1. 平均

      是通过将图像与归一化框滤镜进行卷积来完成的。它仅获取内核区域下所有像素的平均值,并替换中心元素。这是通过功能**cv.blur()或cv.boxFilter()**完成的。

      如果不想使用标准的框式过滤器,请使用cv.boxFilter。将参数normalize = False传递给函数

      example

      img = cv.imread('opencv-logo-white.png')
      blur = cv.blur(img,(5,5))
      plt.subplot(121),plt.imshow(img),plt.title('Original')
      plt.xticks([]), plt.yticks([])
      plt.subplot(122),plt.imshow(blur),plt.title('Blurred')
      plt.xticks([]), plt.yticks([])
      plt.show()
      
    2. 高斯模糊

      通过功能cv.GaussianBlur() 完成的,使用高斯核。

      相较于平均模糊的优点:

      1. 高斯模糊能够更好地保留图像的边缘信息。
      2. 高斯模糊能够更好地处理大尺度的噪声(分配权值)

      们应指定内核的宽度和高度,该宽度和高度应为正数和奇数。我们还应指定X和Y方向的标准偏差,分别为sigmaX和sigmaY。如果仅指定sigmaX,则将sigmaY与sigmaX相同。如果两个都为零,则根据内核大小进行计算。高斯模糊对于从图像中去除高斯噪声非常有效。

      如果需要,可以使用函数cv.getGaussianKernel() 创建高斯内核

      其内核大小应为正奇数整数

      example

      blur = cv.GaussianBlur(img,(5,5),0)
      
    3. 中位模糊

      函数cv.medianBlur() 提取内核区域下所有像素的中值,并将中心元素替换为该中值。这对于消除图像中的椒盐噪声非常有效。

      其内核大小应为正奇数整数

      median = cv.medianBlur(img,5)
      
    4. 双边滤波

      **cv.bilateralFilter()**在去除噪声的同时保持边缘清晰锐利非常有效。但是,与其他过滤器相比,该操作速度较慢。

      高斯滤波一些漏洞:高斯滤波器采用像素周围的邻域并找到其高斯加权平均值。高斯滤波器仅是空间的函数,也就是说,滤波时会考虑附近的像素。它不考虑像素是否具有几乎相同的强度。它不考虑像素是否是边缘像素。因此它也模糊了边缘,这是我们不想做的。

      相比于高斯的一些优点:双边滤波器在空间中也采用高斯滤波器,但是又有一个高斯滤波器,它是像素差的函数。空间的高斯函数确保仅考虑附近像素的模糊,而强度差的高斯函数确保仅考虑强度与中心像素相似的那些像素的模糊。由于边缘的像素强度变化较大,因此可以保留边缘。

      • 参数
        • 输入图像
        • 半径范围(考虑多远的像素点)
        • 灰度值范围(考虑多少像素间灰度差异性值。值小则非常接近时候才认为相似,否则反之)
        • 空间范围(指定了在计算加权平均值时要考虑的像素空间距离,表示了像素之间的空间距离差异性)
      blur = cv.bilateralFilter(img,9,75,75)
      

    形态学转换

    1. 侵蚀

      • 内容

        侵蚀的基本思想就像土壤侵蚀一样,它侵蚀前景物体的边界(尽量使前景保持白色)。

        内核滑动通过图像。原始图像中的一个像素(无论是1还是0)只有当内核下的所有像素都是1时才被认为是1,否则它就会被侵蚀(变成0)。

        结果,根据内核的大小,边界附近的所有像素都会被丢弃。因此,前景物体的厚度或大小减小,或只是图像中的白色区域减小。它有助于去除小的白色噪声(正如我们在颜色空间章节中看到的),分离两个连接的对象等。

      • example

        kernel = np.ones((5,5),np.uint8)
        erosion = cv.erode(img,kernel,iterations = 1)
        
      image-20230603162224985
    2. 扩张

      • 作用

        它与侵蚀正好相反。如果内核下的至少一个像素为“ 1”,则像素元素为“ 1”。因此,它会增加图像中的白色区域或增加前景对象的大小。通常,在消除噪音的情况下,腐蚀后会膨胀。因为腐蚀会消除白噪声,但也会缩小物体。因此,我们对其进行了扩展。由于噪音消失了,它们不会回来,但是我们的目标区域增加了。在连接对象的损坏部分时也很有用。

      • example

        dilation =cv.dilate(img,kernel,iterations = 1)
        
    3. 开运算

      开放只是侵蚀然后扩张的另一个名称。如上文所述,它对于消除噪音很有用。在这里,我们使用函数cv.morphologyEx()

      • example

        opening = cv.morphologyEx(img, cv.MORPH_OPEN, kernel)
        
    4. 闭运算

      闭运算与开运算相反,先扩张然后再侵蚀。在关闭前景对象内部的小孔或对象上的小黑点时很有用。

      • example

        closing = cv.morphologyEx(img, cv.MORPH_CLOSE, kernel)
        
    5. 形态学梯度

      这是图像扩张和侵蚀之间的区别。结果将看起来像对象的轮廓。

      gradient = cv.morphologyEx(img, cv.MORPH_GRADIENT, kernel)
      
    6. 顶帽

      它是输入图像和图像开运算之差

      tophat = cv.morphologyEx(img, cv.MORPH_TOPHAT, kernel)
      
    7. 黑帽

      这是输入图像和图像闭运算之差

      blackhat = cv.morphologyEx(img, cv.MORPH_BLACKHAT, kernel)
      

结构元素

  • cv.getStructuringElement()

    • 参数

      矩形内核、椭圆内核、十字内核

      cv.getStructuringElement(cv.MORPH_RECT,(5,5))
      cv.getStructuringElement(cv.MORPH_ELLIPSE,(5,5))
      cv.getStructuringElement(cv.MORPH_CROSS,(5,5))
      

图像梯度

OpenCV提供三种类型的梯度滤波器或高通滤波器,即Sobel,Scharr和Laplacian。

  1. Sobel和Scharr算子

    Sobel算子是高斯平滑加微分运算的联合运算,因此它更抗噪声。逆可以指定要采用的导数方向,垂直或水平(分别通过参数yorder和xorder)。逆还可以通过参数ksize指定内核的大小。如果ksize = -1 ,则使用3x3 Scharr滤波器,比3x3 Sobel滤波器具有更好的结果。

  2. Laplacian算子

    它计算了由关系 Δ s r c = ∂ 2 s r c ∂ x 2 + ∂ 2 s r c ∂ y 2 \Delta src = \frac{\partial ^2{src}}{\partial x^2} + \frac{\partial ^2{src}}{\partial y^2} Δsrc=x22src+y22src给出的图像的拉普拉斯图,它是每一阶导数通过Sobel算子计算。如果 ksize = 1 ,然后使用以下内核用于过滤: k e r n e l = [ 0 1 0 1 − 4 1 0 1 0 ] kernel = \begin{bmatrix} 0 & 1 & 0 \\ 1 & -4 & 1 \\ 0 & 1 & 0 \end{bmatrix} kernel= 010141010

  • example

    以下输出图像的深度为-1

    laplacian = cv.Laplacian(img,cv.CV_64F)
    sobelx = cv.Sobel(img,cv.CV_64F,1,0,ksize=5)
    sobely = cv.Sobel(img,cv.CV_64F,0,1,ksize=5)
    plt.subplot(2,2,1),plt.imshow(img,cmap = 'gray')
    plt.title('Original'), plt.xticks([]), plt.yticks([])
    plt.subplot(2,2,2),plt.imshow(laplacian,cmap = 'gray')
    plt.title('Laplacian'), plt.xticks([]), plt.yticks([])
    plt.subplot(2,2,3),plt.imshow(sobelx,cmap = 'gray')
    plt.title('Sobel X'), plt.xticks([]), plt.yticks([])
    plt.subplot(2,2,4),plt.imshow(sobely,cmap = 'gray')
    plt.title('Sobel Y'), plt.xticks([]), plt.yticks([])
    plt.show()
    

    CV_64F是一个计算机图像处理中常用的数据类型,也叫做64位浮点型。在图像处理中,通常会对图像进行数学运算和变换,因此需要使用高精度的数据类型来保存图像数据,以避免精度损失

warning:输出数据类型为 cv.CV_8U 或 np.uint8会有一些小问题。黑色到白色的过渡被视为正斜率(具有正值),而白色到黑色的过渡被视为负斜率(具有负值)。因此,将数据转换为np.uint8时,所有负斜率均设为零。简而言之,会错过这一边缘信息。

所以,如果要检测两个边缘,更好的选择是将输出数据类型保留为更高的形式,例如 cv.CV_16S ,cv.CV_64F 等,取其绝对值,然后转换回 cv.CV_8U 。 下面的代码演示了用于水平Sobel滤波器和结果差异的此过程。

  • 转换 example

    abs_sobel64f = np.absolute(sobelx64f)
    sobel_8u = np.uint8(abs_sobel64f)
    

Canny边缘检测(重点)

由于边缘检测容易受到图像中噪声的影响,因此第一步是使用5x5高斯滤波器消除图像中的噪声。

分多步,步骤如下:

  1. 查找图像的强度梯度

    使用Sobel核在水平和垂直方向上对平滑的图像进行滤波,以在水平方向(Gx)和垂直方向(Gy)上获得一阶导数。从这两张图片中,我们可以找到每个像素的边缘渐变和方向。

    渐变方向始终垂直于边缘。将其舍入为代表垂直,水平和两个对角线方向的四个角度之一。

  2. 非极大值抑制

    在获得梯度大小和方向后,将对图像进行全面扫描,以去除可能不构成边缘的所有不需要的像素。为此,在每个像素处,检查像素是否是其在梯度方向上附近的局部最大值。

image-20230603165255222

点A在边缘(垂直方向)上。渐变方向垂直于边缘。点B和C在梯度方向上。因此,将A点与B点和C点进行检查,看是否形成局部最大值。如果是这样,则考虑将其用于下一阶段,否则将其抑制(置为零)。 简而言之,你得到的结果是带有“细边”的二进制图像。

  1. 磁滞阈值

    该阶段确定哪些边缘全部是真正的边缘,哪些不是。

    强度梯度大于 maxVal 的任何边缘必定是边缘,而小于 minVal 的那些边缘必定是非边缘,因此将其丢弃。介于这两个阈值之间的对象根据其连通性被分类为边缘或非边缘。如果将它们连接到“边缘”像素,则将它们视为边缘的一部分。否则,它们也将被丢弃。

    如果边缘为长线,那么我们还成功消除了小像素噪声

  • cv.Canny()

    • 参数

      • 输入图像
      • 阈值:minVal,maxVal
      • perture_size。
      • 它是用于查找图像渐变的Sobel内核的大小。默认情况下为3。
      • L2gradient,它指定用于查找梯度幅度的方程式。如果为 True ,则使用上面提到的更精确的公式(二阶),否则使用以下函数: E d g e _ G r a d i e n t    ( G ) = ∣ G x ∣ + ∣ G y ∣ 。 Edge\_Gradient \; (G) = |G_x| + |G_y|。 Edge_Gradient(G)=Gx+Gy默认情况下,它为 False 。
    • example

      edges = cv.Canny(img,100,200)
      plt.subplot(121),plt.imshow(img,cmap = 'gray')
      plt.title('Original Image'), plt.xticks([]), plt.yticks([])
      plt.subplot(122),plt.imshow(edges,cmap = 'gray')
      plt.title('Edge Image'), plt.xticks([]), plt.yticks([])
      plt.show()
      
  • 参考资料

    Canny edge detector at Wikipedia:http://en.wikipedia.org/wiki/Canny_edge_detector

图像金字塔

  • 简介

    当我们想要在图像中搜索某样东西时,不知道他会以多大的尺寸显示在图像中。这种情况下,将需要创建一组具有不同分辨率的相同图像,并在所有图像中搜索。

    这些具有不同分辨率的图像集称为图像金字塔(因为当它们堆叠在底部时,最高分辨率的图像位于顶部,最低分辨率的图像位于顶部时,看起来像金字塔)。

    两种:1)高斯金字塔和2)拉普拉斯金字塔

    • 高斯金字塔

      高斯金字塔中的较高级别(低分辨率)是通过删除较低级别(较高分辨率)图像中的连续行和列而形成的。然后,较高级别的每个像素由基础级别的5个像素的贡献与高斯权重形成。通过这样做,M×N图像变成M/2 × N/2图像。因此面积减少到原始面积的四分之一。它称为Octave。当我们在金字塔中越靠上时(即分辨率下降),这种模式就会继续。同样,在扩展时,每个级别的面积变为4倍。我们可以使用cv.pyrDown()和cv.pyrUp()函数找到高斯金字塔。

      lower_reso = cv.pyrDown(higher_reso)
      

      可以使用cv.pyrUp()函数查看图像金字塔。

      higher_reso2 = cv.pyrUp(lower_reso)
      

      higher_reso2不等于higher_reso,因为一旦降低了分辨率,就会丢失信息

    • 拉普拉斯金字塔

      拉普拉斯金字塔由高斯金字塔形成。没有专用功能。拉普拉斯金字塔图像仅像边缘图像。它的大多数元素为零。它们用于图像压缩。拉普拉斯金字塔的层由高斯金字塔的层与高斯金字塔的高层的扩展版本之间的差形成

  • 使用金字塔进行图像融合

    金字塔的一种应用是图像融合。例如,在图像拼接中,需要将两个图像堆叠在一起,但是由于图像之间的不连续性,可能看起来不太好。在这种情况下,使用金字塔混合图像可以无缝混合,而不会在图像中保留大量数据。一个经典的例子是将两种水果,橙和苹果混合在一起

    image-20230603171636554
    • 步骤

      1. 加载两个图像
      2. 查看两者的高斯金字塔
      3. 找到其拉普拉斯金字塔
      4. 在每个拉普拉斯金字塔中加入苹果的左半部分和右半部分
      5. 联合金字塔重建图像
    • example

      A = cv.imread('apple.jpg')
      B = cv.imread('orange.jpg')
      # 生成A的高斯金字塔
      G = A.copy()
      gpA = [G]
      for i in range(6):
       G = cv.pyrDown(G)
       gpA.append(G)
      # 生成B的高斯金字塔
      G = B.copy()
      gpB = [G]
      for i in range(6):
       G = cv.pyrDown(G)
       gpB.append(G)
      # 生成A的拉普拉斯金字塔
      lpA = [gpA[5]]
      for i in range(5,0,-1):
       GE = cv.pyrUp(gpA[i])
       L = cv.subtract(gpA[i-1],GE)
       lpA.append(L)
      # 生成B的拉普拉斯金字塔
      lpB = [gpB[5]]
      for i in range(5,0,-1):
       GE = cv.pyrUp(gpB[i])
       L = cv.subtract(gpB[i-1],GE)
       lpB.append(L)
      # 现在在每个级别中添加左右两半图像
      LS = []
      for la,lb in zip(lpA,lpB):
       rows,cols,dpt = la.shape
       ls = np.hstack((la[:,0:cols/2], lb[:,cols/2:]))
       LS.append(ls)
      # 现在重建
      ls_ = LS[0]
      for i in range(1,6):
       ls_ = cv.pyrUp(ls_)
       ls_ = cv.add(ls_, LS[i])
      # 图像与直接连接的每一半
      real = np.hstack((A[:,:cols/2],B[:,cols/2:]))
      cv.imwrite('Pyramid_blending2.jpg',ls_)
      cv.imwrite('Direct_blending.jpg',real)
      

轮廓(重点)

  • findContour()找到轮廓

    • 参数

      1. 源图像

      2. 轮廓检索模式

        1. RETR_EXTERNAL: 只检索最外层轮廓

        2. RETR_LIST: 检索所有的轮廓,并将其保存到列表中

        3. RETR_CCOMP: 检索所有轮廓并将它们组织为两级层次结构。具体来说,每个轮廓都属于其中一个父轮廓,而这些父轮廓本身则属于一个更高级的父轮廓。

        4. RETR_TREE: 检索所有轮廓并重建完整的层次结构。

      3. 轮廓近似方法

    • 输出

      轮廓和层次结构。

      轮廓是图像中所有轮廓的Python列表。每个单独的轮廓是一个(x,y)坐标的Numpy数组的边界点的对象。

  • cv.drawContours绘制轮廓

    • 内容

      只要有边界点,它也可以用来绘制任何形状

    • 参数

      1. 源图像
      2. 作为Python列表传递的轮廓
      3. 轮廓的索引(在绘制单个轮廓时有用。要绘制所有轮廓,请传递-1),其余参数是颜色,厚度等等
    • example

      cv.drawContours(img, contours, -1, (0,255,0), 3)
      
      cv.drawContours(img, contours, 3, (0,255,0), 3)
      

轮廓近似方法

  • 如果传递cv.CHAIN_APPROX_NONE,则将存储所有边界点
  • cv.CHAIN_APPROX_SIMPLE删除所有冗余点并压缩轮廓,比如一条直线只需要存储该线的两个端点即可

轮廓特征

  1. 特征矩

    函数cv.moments()提供了所有计算出的矩值的字典

    ret,thresh = cv.threshold(img,127,255,0)
    contours,hierarchy = cv.findContours(thresh, 1, 2)
    cnt = contours[0]
    M = cv.moments(cnt)
    print( M )
    
    # m00表示零阶矩,也就是所有像素点的加权和;m10和m01分别表示一阶矩,即所有像素点的x和y坐标的加权和。通过除以m00,我们得到了物体的中心坐标(cx, cy)
    
    cx = int(M['m10']/M['m00'])
    cy = int(M['m01']/M['m00'])
    
  2. 轮廓面积

    轮廓区域由函数cv.contourArea()或从矩 M[‘m00’] 中给出。

    area = cv.contourArea(cnt)
    
  3. 轮廓周长

    使用cv.arcLength()函数找到它。第二个参数指定形状是闭合轮廓( True )还是曲线。

  4. 轮廓近似

    假设试图在图像中找到一个正方形,但是由于图像中的某些问题,没有得到一个完美的正方形,而是一个“坏形状”(如下图所示)。现在,可以使用此功能来近似形状。在这种情况下,第二个参数称为epsilon,它是从轮廓到近似轮廓的最大距离。它是一个精度参数。需要正确选择epsilon才能获得正确的输出

    epsilon = 0.1*cv.arcLength(cnt,True)
    approx = cv.approxPolyDP(cnt,epsilon,True)
    # 10%
    
  5. 轮廓凸包

    cv.convexHull()函数检查曲线是否存在凸凹缺陷并对其进行校正

    • cv.convexHull
      1. 点是我们传递的轮廓
      2. 凸包是输出,通常忽略
      3. clockwise,True顺时针输出凸包,否则逆时针
      4. returnPoints ,True返回凸包坐标,False返回与凸包点对应的索引
    • 返回凸包坐标
  6. 检查凸度

    cv.isContourConvex()具有检查曲线是否凸出的功能。它只是返回True还是False

  7. 边界矩形

    1. 直角矩形

      它是一个矩形,不考虑物体的旋转。所以边界矩形的面积不是最小的。

      • cv.boundingRect()

        (x,y) 为矩形的左上角坐标,而 (w,h) 为矩形的宽度和高度。

        x,y,w,h = cv.boundingRect(cnt)
        cv.rectangle(img,(x,y),(x+w,y+h),(0,255,0),2)
        
    2. 旋转矩形

      这里,边界矩形是用最小面积绘制的,所以它也考虑了旋转。

      • cv.minAreaRect()

        参数:(中心(x,y),(宽度,高度),旋转角度)

        返回一个Box2D结构

        但要画出这个矩形,我们需要矩形的四个角。它由函数cv.boxPoints()获得

        rect = cv.minAreaRect(cnt)
        box = cv.boxPoints(rect)
        box = np.intp(box)  # 64位有符号
        cv.drawContours(img,[box],0,(0,0,255),2)
        
  8. 最小闭合圈

    函数cv.minEnclosingCircle()查找对象的圆周。

    它是一个以最小面积完全覆盖物体的圆。

    (x,y),radius = cv.minEnclosingCircle(cnt)
    center = (int(x),int(y))
    radius = int(radius)
    cv.circle(img,center,radius,(0,255,0),2)
    
  9. 拟合一个椭圆

    把一个椭圆拟合到一个物体上。它返回内接椭圆的旋转矩形。

    ellipse = cv.fitEllipse(cnt)
    cv.ellipse(img,ellipse,(0,255,0),2)
    
  10. 拟合直线

    rows,cols = img.shape[:2]
    [vx,vy,x,y] = cv.fitLine(cnt, cv.DIST_L2,0,0.01,0.01)
    lefty = int((-x*vy/vx) + y)
    righty = int(((cols-x)*vy/vx)+y)
    cv.line(img,(cols-1,righty),(0,lefty),(0,255,0),2)
    

轮廓属性

  1. 长宽比

    对象边界矩形的宽度与高度的比值。

  2. 范围

    范围是轮廓区域与边界矩形区域的比值。

  3. 坚实度

    坚实度是等高线面积与其凸包面积之比。

    area = cv.contourArea(cnt)
    hull = cv.convexHull(cnt)
    hull_area = cv.contourArea(hull)
    solidity = float(area)/hull_area
    
  4. 等效直径

    等效直径是面积与轮廓面积相同的圆的直径。

    area = cv.contourArea(cnt)
    equi_diameter = np.sqrt(4*area/np.pi)
    
  5. 取向

    取向是物体指向的角度。

  6. 掩码和像素点

    我们可能需要构成该对象的所有点

    mask = np.zeros(imgray.shape,np.uint8)
    cv.drawContours(mask,[cnt],0,255,-1)
    pixelpoints = np.transpose(np.nonzero(mask))
    #pixelpoints = cv.findNonZero(mask)
    

    这里提供了两个方法,一个使用Numpy函数,另一个使用OpenCV函数(最后的注释行)。结果也是一样的,只是略有不同。Numpy给出的坐标是 (行、列) 格式,而OpenCV给出的坐标是 (x,y) 格式。所以基本上答案是可以互换的。注意, row = x, column = y 。

  7. 最大值,最小值和它们的位置

    可以使用掩码图像找到这些参数

    min_val, max_val, min_loc, max_loc = cv.minMaxLoc(imgray,mask = mask)
    
  8. 平均颜色或平均强度

    可以找到对象的平均颜色。或者可以是灰度模式下物体的平均强度。我们再次使用相同的掩码进行此操作。

    mean_val = cv.mean(im,mask = mask)
    
  9. 极端点

    对象的最顶部,最底部,最右侧和最左侧的点

    leftmost = tuple(cnt[cnt[:,:,0].argmin()][0])
    rightmost = tuple(cnt[cnt[:,:,0].argmax()][0])
    topmost = tuple(cnt[cnt[:,:,1].argmin()][0])
    bottommost = tuple(cnt[cnt[:,:,1].argmax()][0])
    

轮廓:更多属性

  1. 凸性缺陷

    • cv.convexityDefects()

      hull = cv.convexHull(cnt,returnPoints = False)
      defects = cv.convexityDefects(cnt,hull)
      

      注意传递returnPoints = False

      • 返回值
        • 一个数组:其中每行包含这些值—[起点、终点、最远点、到最远点的近似距离]。我们可以用图像把它形象化。
  2. 点多边形

    这个函数找出图像中一点到轮廓线的最短距离。它返回的距离,点在轮廓线外时为负,点在轮廓线内时为正,点在轮廓线上时为零。

    dist = cv.pointPolygonTest(cnt,(50,50),True)
    

    在函数中,第三个参数是measureDist。如果它是真的,它会找到有符号的距离。如果为假,则查找该点是在轮廓线内部还是外部(分别返回+1、-1和0)

    注意 如果不想找到距离,请确保第三个参数为False,因为这是一个耗时的过程

  3. 形状匹配

    • cv.matchShapes()

      该函数使我们能够比较两个形状或两个轮廓,并返回一个显示相似性的度量。结果越低,匹配越好。它是根据矩值计算出来的

      example

      img1 = cv.imread('star.jpg',0)
      img2 = cv.imread('star2.jpg',0)
      ret, thresh = cv.threshold(img1, 127, 255,0)
      ret, thresh2 = cv.threshold(img2, 127, 255,0)
      contours,hierarchy = cv.findContours(thresh,2,1)
      cnt1 = contours[0]
      contours,hierarchy = cv.findContours(thresh2,2,1)
      cnt2 = contours[0]
      ret = cv.matchShapes(cnt1,cnt2,1,0.0)
      

轮廓分层

有时对象在不同的位置。但在某些情况下,某些形状在其他形状中。就像嵌套的图形一样。在这种情况下,我们把外部的称为父类,把内部的称为子类。这样,图像中的轮廓就有了一定的相互关系。我们可以指定一个轮廓是如何相互连接的,比如,它是另一个轮廓的子轮廓,还是父轮廓等等。这种关系的表示称为层次结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P5LM9FEZ-1685890469426)(C:\Users\86189\AppData\Roaming\Typora\typora-user-images\image-20230603200342147.png)]

每个轮廓都有它自己的信息关于它是什么层次,谁是它的孩子,谁是它的父母等等。OpenCV将它表示为一个包含四个值的数组: [Next, Previous, First_Child, Parent](注意:Next表示同一层次的下一个轮廓)

  • Next表示同一层次的下一个轮廓
  • Previous表示同一层次上的先前轮廓
  • First_Child表示它的第一个子轮廓
  • Parent表示其父轮廓的索引

轮廓检索模式:

1. RETR_LIST

它只是检索所有的轮廓,但不创建任何亲子关系。在这个规则下,父轮廓和子轮廓是平等的,他们只是轮廓。他们都属于同一层级。

2. RETR_EXTERNAL

如果使用此标志,它只返回极端外部标志。所有孩子的轮廓都被留下了。我们可以说,根据这项规则,每个家庭只有长子得到关注。它不关心家庭的其他成员**😃**

图中只有0,1,2满足条件

3. RETR_CCOMP

此标志检索所有轮廓并将其排列为2级层次结构。物体的外部轮廓(即物体的边界)放在层次结构-1中。对象内部孔洞的轮廓(如果有)放在层次结构-2中。如果其中有任何对象,则其轮廓仅在层次结构1中重新放置。以及它在层级2中的漏洞等等。

4. RETR_TREE

它检索所有的轮廓并创建一个完整的家族层次结构列表。它甚至告诉,谁是爷爷,父亲,儿子,孙子,甚至更多**…😃**。

直方图

1 查找、绘制和分析

直方图可以了解总体图像的强度分布

BINS:上面的直方图显示每个像素值的像素数,即从0到255。即,需要256个值来显示上面的直方图。但是考虑一下,如果不需要分别找到所有像素值的像素数,而是找到像素值间隔中的像素数怎么办? 例如,需要找到介于0到15之间的像素数,然后找到16到31之间,…,240到255之间的像素数。只需要16个值即可表示直方图

DIMS:这是我们为其收集数据的参数的数量。在这种情况下,我们仅收集关于强度值的一件事的数据。所以这里是1

RANGE:这是要测量的强度值的范围。通常,它是 [0,256] ,即所有强度值。

1. Opencv中的直方图计算

  • cv.calcHist()

    • 参数

      • images:uint8或float32类型的源图像
      • channels:方括号给出。对于灰度图像,灰度值为0;对于彩色图像,可以传递0,1,2分别计算蓝色绿色或红色通道
      • mask:图像掩码。
      • histSize:BIN计数
      • ranges:通常为[0,256]
    • example

      img = cv.imread('home.jpg',0)
      hist = cv.calcHist([img],[0],None,[256],[0,256])
      # hist是256*1的数组,每个值对应于该图像中具有相应像素值的像素数
      

2. numpy的直方图计算

  • np.histogram()

    hist,bins = np.histogram(img.ravel(),256,[0,256])
    

    注意 Opencv函数更快

绘制直方图

1.matploylib

  • matplotlib.pyplot.hist()

    plt.hist(img.ravel(),256,[0,256]); plt.show()
    
  • 法线图,对于BGR图很好

    img = cv.imread('home.jpg')
    color = ('b','g','r')
    for i,col in enumerate(color):
     histr = cv.calcHist([img],[i],None,[256],[0,256])
     plt.plot(histr,color = col)
     plt.xlim([0,256])
    plt.show()
    

2. Opencv

如果你想找到图像某些区域的直方图,只需创建一个掩码图像,在你要找到直方图为白色,否则黑色。然后把这个作为掩码传递。

2 直方图均衡

考虑这样一个图像,它的像素值仅局限于某个特定的值范围。例如,较亮的图像将把所有像素限制在高值上。但是一幅好的图像会有来自图像所有区域的像素。因此,需要将这个直方图拉伸到两端,这就是直方图均衡化的作用(简单来说)。这通常会提高图像的对比度。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D3nXU79R-1685890469426)(C:\Users\86189\AppData\Roaming\Typora\typora-user-images\image-20230604155555810.png)]

  • cv.equalizeHist()

    • 输入是灰度图像,输出是直方图均衡图像

    • img = cv.imread('wiki.jpg',0)
      equ = cv.equalizeHist(img)
      res = np.hstack((img,equ)) #stacking images side-by-side
      cv.imwrite('res.png',res)
      
  • CLAHE(对比度受限的自适应直方图均衡)

    • 思想

      在每一块小区域进行直方图均衡,还应用了对比度限制。如果任何直方图bin超出指定的对比度限制(在OpenCV中默认为40),则在应用直方图均衡之前,将这些像素裁剪并均匀地分布到其他bin。均衡后,要消除图块边界中的伪影,请应用双线性插值。

    • example

      clahe = cv.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
      cl1 = clahe.apply(img)
      

3 二维直方图

在二维直方图中,要考虑两个特征。 通常,它用于查找颜色直方图,其中两个特征是每个像素的色相和饱和度值。

  • OpenCV中的二维直方图

    • 内容

      对于颜色直方图,我们需要将图像从BGR转换为HSV。(请记住,对于一维直方图,我们从BGR转换为灰度)。对于二维则需要更改

    • 参数

      channel = [0,1],因为我们需要同时处理HS平面。

      bins = [180,256] 对于H平面为180,对于S平面为256

      range = [0,180,0,256] 色相值介于0180之间,饱和度介于0256之间

  • 绘制二维直方图

    • cv.imshow

      得到的是80x256的二维数组。可以使用cv.imshow()函数像平常一样显示它们。它将是一幅灰度图像,除非知道不同颜色的色相值,否则不会对其中的颜色有太多了解。

    • plt.imshow()

      绘制具有不同颜色图的2D直方图。它使我们对不同的像素密度有了更好的了解。但是,除非知道不同颜色的色相值,否则乍一看并不能使我们知道到底是什么颜色。我还是更喜欢这种方法。它简单而更好

      使用此功能时,请记住,插值法应采用最近邻以获得更好的结果。

    • example

      hsv = cv.cvtColor(img,cv.COLOR_BGR2HSV)
      hist = cv.calcHist( [hsv], [0, 1], None, [180, 256], [0, 180, 0, 256] )
      plt.imshow(hist,interpolation = 'nearest')
      plt.show()
      

4 直方图反投影

OpenCV的反投影

  • cv.calcBackProject()

    • 内容

      它的参数几乎与cv.calchist()函数相同。它的一个参数是直方图,也就是物体的直方图,我们必须找到它。另外,在传递给backproject函数之前,应该对对象直方图进行归一化。它返回概率图像。然后我们用圆盘内核对图像进行卷积并应用阈值

    • example

      roi = cv.imread('rose_red.png')
      hsv = cv.cvtColor(roi,cv.COLOR_BGR2HSV)
      target = cv.imread('rose.png')
      hsvt = cv.cvtColor(target,cv.COLOR_BGR2HSV)
      # 计算对象的直方图
      roihist = cv.calcHist([hsv],[0, 1], None, [180, 256], [0, 180, 0, 256] )
      # 直方图归一化并利用反传算法
      cv.normalize(roihist,roihist,0,255,cv.NORM_MINMAX)
      dst = cv.calcBackProject([hsvt],[0,1],roihist,[0,180,0,256],1)
      # 用圆盘进行卷积
      disc = cv.getStructuringElement(cv.MORPH_ELLIPSE,(5,5))
      cv.filter2D(dst,-1,disc,dst)
      # 应用阈值作与操作
      ret,thresh = cv.threshold(dst,50,255,0)
      thresh = cv.merge((thresh,thresh,thresh))
      res = cv.bitwise_and(target,thresh)
      res = np.vstack((target,thresh,res))
      cv.imwrite('res.jpg',res)
      

傅里叶变换

  • Numpy中的傅里叶变换

    • np.fft.fft2()

      • 参数

        • 输入图像,灰度
        • 决定输出数组的大小。如果大于输入则用0填充,如果小于则裁切图像
      • 输出

        一旦获得结果,零频率分量(DC分量)将位于左上角。如果要使其居中,则需要在两个方向上将结果都移动 N 2 \frac{N}{2} 2N。只需通过函数np.fft.fftshift()即可完成。(它更容易分析)。找到频率变换后,就可以找到幅度谱。

      • example

        fshift = np.fft.fftshift(f)
        magnitude_spectrum = 20*np.log(np.abs(fshift))
        plt.subplot(121),plt.imshow(img, cmap = 'gray')
        plt.title('Input Image'), plt.xticks([]), plt.yticks([])
        plt.subplot(122),plt.imshow(magnitude_spectrum, cmap = 'gray')
        plt.title('Magnitude Spectrum'), plt.xticks([]), plt.yticks([])
        plt.show()
        
    • 如果在中心看到很多白色区域,表明低频内容更多

      因此,发现了频率变换现在,可以在频域中进行一些操作,例如高通滤波和重建图像,即找到逆DFT。为此,只需用尺寸为60x60的矩形窗口遮罩即可消除低频。然后,使用np.fft.ifftshift()应用反向移位,以使DC分量再次出现在左上角。然后使用np.ifft2()函数找到逆FFT。同样,结果将是一个复数。可以采用其绝对值。

      rows, cols = img.shape
      crow,ccol = rows//2 , cols//2
      fshift[crow-30:crow+31, ccol-30:ccol+31] = 0
      f_ishift = np.fft.ifftshift(fshift)
      img_back = np.fft.ifft2(f_ishift)
      img_back = np.real(img_back)
      plt.subplot(131),plt.imshow(img, cmap = 'gray')
      plt.title('Input Image'), plt.xticks([]), plt.yticks([])
      plt.subplot(132),plt.imshow(img_back, cmap = 'gray')
      plt.title('Image after HPF'), plt.xticks([]), plt.yticks([])
      plt.subplot(133),plt.imshow(img_back)
      plt.title('Result in JET'), plt.xticks([]), plt.yticks([])
      plt.show()
      
  • OpenCV中的傅里叶变换

    • cv.dft()和cv.idft()

      • 输出

        两通道,一个是实部,另外一个是虚部

        输入图像首先应转换为 np.float32

      • example

        dft = cv.dft(np.float32(img),flags = cv.DFT_COMPLEX_OUTPUT)
        dft_shift = np.fft.fftshift(dft)
        magnitude_spectrum = 20*np.log(cv.magnitude(dft_shift[:,:,0],dft_shift[:,:,1]))
        plt.subplot(121),plt.imshow(img, cmap = 'gray')
        plt.title('Input Image'), plt.xticks([]), plt.yticks([])
        plt.subplot(122),plt.imshow(magnitude_spectrum, cmap = 'gray')
        plt.title('Magnitude Spectrum'), plt.xticks([]), plt.yticks([])
        plt.show()
        

        cv.magnitude(dft_shift[:,:,0],dft_shift[:,:,1])是OpenCV中用于计算复数数组的幅度的函数。在这里,dft_shift是一个二维复数数组,其中第一个索引表示行,第二个索引表示列,第三个索引表示实部或虚部。返回一个与输入数组大小相同的浮点类型数组,其中每个元素的值等于对应复数的模长。

        还可以使用cv.cartToPolar(),它在单个镜头中同时返回幅值和相位

      • 逆变换,删除高频

        rows, cols = img.shape
        crow,ccol = rows/2 , cols/2
        # 首先创建一个掩码,中心正方形为1,其余全为零
        mask = np.zeros((rows,cols,2),np.uint8)
        mask[crow-30:crow+30, ccol-30:ccol+30] = 1
        # 应用掩码和逆DFT
        fshift = dft_shift*mask
        f_ishift = np.fft.ifftshift(fshift)
        img_back = cv.idft(f_ishift)
        img_back = cv.magnitude(img_back[:,:,0],img_back[:,:,1])
        plt.subplot(121),plt.imshow(img, cmap = 'gray')
        plt.title('Input Image'), plt.xticks([]), plt.yticks([])
        plt.subplot(122),plt.imshow(img_back, cmap = 'gray')
        plt.title('Magnitude Spectrum'), plt.xticks([]), plt.yticks([])
        plt.show()
        
  • DFT的性能优化

    • 对于某些数组尺寸,DFT的计算性能较好。当数组大小为2的幂时,速度最快。对于大小为2、3和5的乘积的数组,也可以非常有效地进行处理

    • cv.getOptimalDFTSize()

      • 功能

        找到最优的大小

      • example

        In [16]: img = cv.imread('messi5.jpg',0)
        In [17]: rows,cols = img.shape
        In [18]: print("{} {}".format(rows,cols))
        342 548
        In [19]: nrows = cv.getOptimalDFTSize(rows)
        In [20]: ncols = cv.getOptimalDFTSize(cols)
        In [21]: print("{} {}".format(nrows,ncols))
        360 576
        

        对于opencv,不想会自动进行零填充,需要创建一个新的零数组并将数据赋值过来

        nimg = np.zeros((nrows,ncols))
        nimg[:rows,:cols] = img
        
    • 可以看到OpenCV函数比Numpy函数快3倍左右

  • 拉普拉斯算子是高通滤波器等问题

    # 没有缩放参数的简单均值滤波器
    mean_filter = np.ones((3,3))
    # 创建高斯滤波器
    x = cv.getGaussianKernel(5,10)
    gaussian = x*x.T
    # 不同的边缘检测滤波器
    # x方向上的scharr
    scharr = np.array([[-3, 0, 3],
     [-10,0,10],
     [-3, 0, 3]])
    # x方向上的sobel
    sobel_x= np.array([[-1, 0, 1],
     [-2, 0, 2],
     [-1, 0, 1]])
    # y方向上的sobel
    sobel_y= np.array([[-1,-2,-1],
     [0, 0, 0],
     [1, 2, 1]])
    # 拉普拉斯变换
    laplacian=np.array([[0, 1, 0],
     [1,-4, 1],
     [0, 1, 0]])
    filters = [mean_filter, gaussian, laplacian, sobel_x, sobel_y, scharr]
    filter_name = ['mean_filter', 'gaussian','laplacian', 'sobel_x', \
    'sobel_y', 'scharr_x']
    fft_filters = [np.fft.fft2(x) for x in filters]
    fft_shift = [np.fft.fftshift(y) for y in fft_filters]
    mag_spectrum = [np.log(np.abs(z)+1) for z in fft_shift]
    for i in range(6):
     		 plt.subplot(2,3,i+1),plt.imshow(mag_spectrum[i],cmap = 'gray')
    plt.title(filter_name[i]),     plt.xticks([]), plt.yticks([])
    plt.show()
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-12Gz25BE-1685890469426)(C:\Users\86189\AppData\Roaming\Typora\typora-user-images\image-20230604163210489.png)]

模板匹配

模板匹配是一种用于在较大图像中搜索和查找模板图像位置的方法。为此,OpenCV带有一个函数cv.matchTemplate()。 它只是将模板图像滑动到输入图像上(就像在2D卷积中一样),然后在模板图像下比较模板和输入图像的拼图。 OpenCV中实现了几种比较方法。它返回一个灰度图像,其中每个像素表示该像素的邻域与模板匹配的程度

如果输入图像的大小为 (WxH) ,而模板图像的大小为 (wxh) ,则输出图像的大小将为(W-w + 1,H-h + 1) 。得到结果后,可以使用cv.minMaxLoc()函数查找最大/最小值在哪。将其作为矩形的左上角,并以 (w,h) 作为矩形的宽度和高度。该矩形是模板的区域

比如在梅西图片里找他的头像

  • example

    img = cv.imread('messi5.jpg',0)
    img2 = img.copy()
    template = cv.imread('template.jpg',0)
    w, h = template.shape[::-1]
    # 列表中所有的6种比较方法
    methods = ['cv.TM_CCOEFF', 'cv.TM_CCOEFF_NORMED', 'cv.TM_CCORR',
     'cv.TM_CCORR_NORMED', 'cv.TM_SQDIFF', 'cv.TM_SQDIFF_NORMED']
    for meth in methods:
     img = img2.copy()
     method = eval(meth)
     # 应用模板匹配
     res = cv.matchTemplate(img,template,method)
     min_val, max_val, min_loc, max_loc = cv.minMaxLoc(res)
     # 如果方法是TM_SQDIFF或TM_SQDIFF_NORMED,则取最小值
     if method in [cv.TM_SQDIFF, cv.TM_SQDIFF_NORMED]:
     top_left = min_loc
     else:
     top_left = max_loc
     bottom_right = (top_left[0] + w, top_left[1] + h)
     cv.rectangle(img,top_left, bottom_right, 255, 2)
     plt.subplot(121),plt.imshow(res,cmap = 'gray')
     plt.title('Matching Result'), plt.xticks([]), plt.yticks([])
     plt.subplot(122),plt.imshow(img,cmap = 'gray')
     plt.title('Detected Point'), plt.xticks([]), plt.yticks([])
     plt.suptitle(meth)
     plt.show()
    

    使用cv.TM_CCORR的结果并不理想。

  • 多对象的模板匹配

    假设正在搜索具有多次出现的对象,则cv.minMaxLoc()不会提供所有位置。在这种情况下,我们将使用阈值化。

    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)
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KkzjEtJM-1685890469426)(C:\Users\86189\AppData\Roaming\Typora\typora-user-images\image-20230604225049380.png)]

霍夫线变换

原理

任何一条线都可以用(ρ,θ)这两个术语表示

  • cv.HoughLines

    • 参数

      • 输入二进制图像
      • rho精度
      • theta精度
      • 阈值(最低投票)
    • 返回

      一个math:(rho,theta) 值的数组。ρ以像素为单位,θ以弧度为单位

    • example

      gray = cv.cvtColor(img,cv.COLOR_BGR2GRAY)
      edges = cv.Canny(gray,50,150,apertureSize = 3)
      lines = cv.HoughLines(edges,1,np.pi/180,200)
      for line in 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(img,(x1,y1),(x2,y2),(0,0,255),2)
      cv.imwrite('houghlines3.jpg',img)
      
  • 概率霍夫变换

    • cv.HoughLinesP

      • 参数(相对于原来的多的)

        minLineLength - 最小行长。小于此长度的线段将被拒绝。 maxLineGap - 线段之间允许将它们视为一条线的最大间隙。

      • 返回

        它直接返回行的两个端点

      • example

      gray = cv.cvtColor(img,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(img,(x1,y1),(x2,y2),(0,255,0),2)
      

霍夫圈变换

  • cv.HoughCircles()

    • 参数

      1. image: 输入图像,必须为单通道灰度图像。

      2. method: 检测方法。目前只支持cv.HOUGH_GRADIENT。

      3. dp: 累加器分辨率与图像分辨率的比值。dp越小,检测到的圆形数量越多,计算时间也越长。

      4. minDist: 圆心之间的最小距离。如果两个圆的圆心距离小于minDist,则将其中一个圆剔除。

      5. param1: Canny 边缘检测的高阈值。

      6. param2: 累加器阈值。只有当累加器中的值大于param2时,才会被认为是一个圆。

      7. minRadius: 圆的最小半径。

      8. maxRadius: 圆的最大半径。

        返回值为一个包含检测到的圆心坐标和半径的 numpy 数组

    • example

      img = cv.imread('opencv-logo-white.png',0)
      img = cv.medianBlur(img,5)
      cimg = cv.cvtColor(img,cv.COLOR_GRAY2BGR)
      circles = cv.HoughCircles(img,cv.HOUGH_GRADIENT,1,20,
       param1=50,param2=30,minRadius=0,maxRadius=0)
      circles = np.uint16(np.around(circles))
      for i in circles[0,:]:
       # 绘制外圆
       cv.circle(cimg,(i[0],i[1]),i[2],(0,255,0),2)
       # 绘制圆心
       cv.circle(cimg,(i[0],i[1]),2,(0,0,255),3)
      cv.imshow('detected circles',cimg)
      cv.waitKey(0)
      cv.destroyAllWindows()
      

图像分割与Watershed算法

  • example(硬币估计)

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e9anpzMi-1685890469427)(C:\Users\86189\AppData\Roaming\Typora\typora-user-images\image-20230604170012852.png)]

    img = cv.imread('coins.png')
    gray = cv.cvtColor(img,cv.COLOR_BGR2GRAY)
    ret, thresh = cv.threshold(gray,0,255,cv.THRESH_BINARY_INV+cv.THRESH_OTSU)
    # 噪声去除
    kernel = np.ones((3,3),np.uint8)
    opening = cv.morphologyEx(thresh,cv.MORPH_OPEN,kernel, iterations = 2)
    # 确定背景区域
    sure_bg = cv.dilate(opening,kernel,iterations=3)
    # 寻找前景区域
    dist_transform = cv.distanceTransform(opening,cv.DIST_L2,5)
    ret, sure_fg = cv.threshold(dist_transform,0.7*dist_transform.max(),255,0)
    # 找到未知区域
    sure_fg = np.uint8(sure_fg)
    unknown = cv.subtract(sure_bg,sure_fg)
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fdaY3LhI-1685890469427)(C:\Users\86189\AppData\Roaming\Typora\typora-user-images\image-20230604170126454.png)]

    先侵蚀再扩张,侵蚀去除边界像素,但是无论剩余多少,我们都知道他肯定是硬币。现在要确定它们不是硬币的区域,所以扩张了结果,那么这些黑色区域就是背景了

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-awRu0jsA-1685890469427)(C:\Users\86189\AppData\Roaming\Typora\typora-user-images\image-20230604170144889.png)]

    剩下的区域是我们不知道的区域,无论是硬币还是背景。分水岭算法应该找到它。这些区域通常位于前景和背景相遇(甚至两个不同的硬币相遇)的硬币边界附近。我们称之为边界。可以通过从 sure_bg 区域中减去 sure_fg 区域来获得。

    # 噪声去除
    kernel = np.ones((3,3),np.uint8)
    opening = cv.morphologyEx(thresh,cv.MORPH_OPEN,kernel, iterations = 2)
    # 确定背景区域
    sure_bg = cv.dilate(opening,kernel,iterations=3)
    # 寻找前景区域
    dist_transform = cv.distanceTransform(opening,cv.DIST_L2,5)
    ret, sure_fg = cv.threshold(dist_transform,0.7*dist_transform.max(),255,0)
    # 找到未知区域
    sure_fg = np.uint8(sure_fg)
    unknown = cv.subtract(sure_bg,sure_fg)
    

    cv.distanceTransform是OpenCV中的一个函数,它用于计算二进制图像中每个像素与最近的零像素之间的距离

    - src: 输入的二进制图像,必须是单通道、8位或32位浮点型的。
    - distanceType: 距离类型,可以是cv2.DIST_L1cv2.DIST_L2cv2.DIST_Ccv2.DIST_LABEL_PIXEL等。
    - maskSize: 距离变换核的尺寸,它必须是1、3、5或7。

    返回一个与输入图像大小相同的32位浮点型的距离变换图像。在距离变换图像中,每个像素的值表示该像素与距离为0的像素之间的距离。如果该像素不在前景区域内,则其距离值为负数。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AxaqtJYG-1685890469427)(C:\Users\86189\AppData\Roaming\Typora\typora-user-images\image-20230604171447299.png)]

    现在我们可以确定哪些是硬币的区域,哪些是背景。因此,我们创建了标记(它的大小与原始图像的大小相同,但具有int32数据类型),并标记其中的区域。我们肯定知道的区域(无论是前景还是背景)都标有任何正整数,但是带有不同的整数,而我们不确定的区域则保留为零。为此,我们使用cv.connectedComponents()。它用0标记图像的背景,然后其他对象用从1开始的整数标记。

    # 类别标记
    ret, markers = cv.connectedComponents(sure_fg)
    # 为所有的标记加1,保证背景是0而不是1
    markers = markers+1
    # 现在让所有的未知区域为0
    markers[unknown==255] = 0
    

    标记准备就绪,启动分水岭算法

    边界区域标记为-1

    markers = cv.watershed(img,markers)
    img[markers == -1] = [255,0,0]
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-koEIBZVQ-1685890469428)(C:\Users\86189\AppData\Roaming\Typora\typora-user-images\image-20230604172047288.png)]

交互式前景提取使用GrabCut算法

  • cv.grabCut()

    • 参数

      1. img - 输入图像

      2. mask - 这是一个掩码图像,在其中我们指定哪些区域是背景,前景或可能的背景/前景等。这是通过以下标志完成的:

        cv.GC_BGD,cv.GC_FGD,

        cv.GC_PR_BGD,cv.GC_PR_FGD,或直接将 0,1,2,3 传递给图像。

      3. rect - 它是矩形的坐标,其中包括前景对象,格式为 (x,y,w,h)

      4. bdgModel, fgdModel - 这些是算法内部使用的数组。你只需创建两个大小为 (1,65) 的 np.float64 类型零数组。

      5. iterCount - 算法应运行的迭代次数

      6. model - 应该是cv.GC_INIT_WITH_RECTcv.GC_INIT_WITH_MASK或两者结合

    • example

      img = cv.imread('messi5.jpg')
      mask = np.zeros(img.shape[:2],np.uint8)
      bgdModel = np.zeros((1,65),np.float64)
      fgdModel = np.zeros((1,65),np.float64)
      rect = (50,50,450,290)
      cv.grabCut(img,mask,rect,bgdModel,fgdModel,5,cv.GC_INIT_WITH_RECT)
      mask2 = np.where((mask==2)|(mask==0),0,1).astype('uint8')
      img = img*mask2[:,:,np.newaxis]
      plt.imshow(img),plt.colorbar(),plt.show()
      

      如有缺漏,可以手动画白色或黑色告诉程序这是背景还是前景

      newmask = cv.imread('newmask.png',0)
      # 标记为白色(确保前景)的地方,更改mask = 1
      # 标记为黑色(确保背景)的地方,更改mask = 0
      mask[newmask == 0] = 0
      mask[newmask == 255] = 1
      mask, bgdModel, fgdModel = cv.grabCut(img,mask,None,bgdModel,fgdModel,
      5,cv.GC_INIT_WITH_MASK)
      mask = np.where((mask==2)|(mask==0),0,1).astype('uint8')
      img = img*mask[:,:,np.newaxis]
      plt.imshow(img),plt.colorbar(),plt.show()
      
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值