OpenCV-Python系列 - OpenCV中的图像处理 三

系列文章目录

一 OpenCV中的GUI特性
二 核心操作
三 OpenCV中的图像处理
四 特征检测与描述
五 视频分析
六 相机校准和3D重建
七 机器学习
八 计算摄影学
九 目标检测
十 OpenCV-Python Bindings



前言

本文主要介绍OpenCV图像相关的处理。


一、改变颜色空间

1.改变颜色空间

    OpenCV中有超过150种颜色空间转换方法。但是我们将研究只有两个最广泛使用的,BGR↔灰色和BGR↔HSV。

    对于颜色转换,我们使用cv函数。cvtColor(input_image, flag),其中flag决定转换的类型。

    对于BGR→灰度转换,我们使用标志cv.COLOR_BGR2GRAY。类似地,对于BGR→HSV,我们使用标志cv.COLOR_BGR2HSV。要获取其他标记,只需在Python终端中运行以下命令:

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    flags = [i for i in dir(cv) if i.startswith('COLOR_')]
    print( flags )

    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

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

2.对象追踪

    在HSV中比在BGR颜色空间中更容易表示颜色。在我们的应用程序中,我们将尝试提取一个蓝色的对象。
    方法如下:

  • 取视频的每一帧 - 转换从BGR到HSV颜色空间
  • 我们对HSV图像设置蓝色范围的阈值
  • 现在单独提取蓝色对象,我们可以对图像做任何我们想做的事情。
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    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()
    
    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

    下图显示了对蓝色对象的跟踪:
在这里插入图片描述
    注意:图像中有一些噪点。我们将在后面的章节中看到如何删除它们。 这是对象跟踪中最简单的方法。一旦学习了轮廓的功能,你就可以做很多事情,例如找到该对象的质心并使用它来跟踪对象,仅通过将手移到相机前面以及其他许多有趣的东西就可以绘制图表。

3.如何找到要追踪的HSV值?

    这是在stackoverflow.com上发现的一个常见问题。它非常简单,你可以使用相同的函数cv.cvtColor()。你只需传递你想要的BGR值,而不是传递图像。例如,要查找绿色的HSV值,请在Python终端中尝试以下命令:

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    green = np.uint8([[[0,255,0 ]]])
    hsv_green = cv.cvtColor(green,cv.COLOR_BGR2HSV)
    print( hsv_green )
   
    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

在这里插入图片描述
    现在把[H- 10,100,100]和[H+ 10,255, 255]分别作为下界和上界。除了这个方法之外,你可以使用任何图像编辑工具(如GIMP或任何在线转换器)来查找这些值,但是不要忘记调整HSV范围。

二、图像的几何变换

1.变换

    OpenCV提供了两个转换函数cv.warpAffinecv.warpPerspective,您可以使用它们进行各种转换。cv.warpAffine采用2x3转换矩阵,而cv.warpPerspective采用3x3转换矩阵作为输入。

2.缩放

    缩放只是调整图像的大小。为此,OpenCV带有一个函数cv.resize()。图像的大小可以手动指定,也可以指定缩放比例。也可使用不同的插值方法。首选的插值方法是cv.INTER_AREA用于缩小,cv.INTER_CUBIC(慢)和cv.INTER_LINEAR用于缩放。默认情况下,出于所有调整大小的目的,使用的插值方法为cv.INTER_LINEAR。您可以使用以下方法调整输入图像的大小:

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    #1.加载彩色图像
    image1 = cv.imread(r'C:\Users\zhutianpeng\Desktop\panda.jpg',cv.IMREAD_COLOR)
    cv.imshow('ori',image1)
    res = cv.resize(image1,None,fx=2, fy=2, interpolation = cv.INTER_CUBIC)
    cv.imshow('res1',res)
    #或者
    height, width = image1.shape[:2]
    res2 = cv.resize(image1,(2*width, 2*height), interpolation = cv.INTER_CUBIC)
    cv.imshow('res2',res2)
    
    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

在这里插入图片描述

3.平移

    平移是物体位置的移动。如果您知道在(x,y)方向上的位移,则将其设为(tx,ty),你可以创建转换矩阵M,如下所示:
在这里插入图片描述
    您可以将其放入np.float32类型的Numpy数组中,并将其传递给cv.warpAffine函数。参见下面偏移为(100, 50)的示例:

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    #1.加载彩色图像
    image1 = cv.imread(r'C:\Users\zhutianpeng\Desktop\panda.jpg',cv.IMREAD_COLOR)
    cv.imshow('ori',image1)
    rows,cols,channel = image1.shape
    M = np.float32([[1,0,100],[0,1,50]])
    dst = cv.warpAffine(image1,M,(cols,rows))
    cv.imshow('img',dst)
    
    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

在这里插入图片描述
    警告cv.warpAffine函数的第三个参数是输出图像的大小,其形式应为(width,height)。记住width =列数,height =行数。

4.旋转

    图像旋转角度为θ是通过以下形式的变换矩阵实现的:
在这里插入图片描述
    但是OpenCV提供了可缩放的旋转以及可调整的旋转中心,因此您可以在自己喜欢的任何位置旋转。修改后的变换矩阵为
在这里插入图片描述
    为了找到此转换矩阵,OpenCV提供了一个函数cv.getRotationMatrix2D。请检查以下示例,该示例将图像相对于中心旋转90度而没有任何缩放比例。

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    #1.加载彩色图像
    image1 = cv.imread(r'C:\Users\zhutianpeng\Desktop\panda.jpg',cv.IMREAD_COLOR)
    cv.imshow('ori',image1)
    rows,cols,channel = image1.shape
    # cols-1 和 rows-1 是坐标限制
    M = cv.getRotationMatrix2D(((cols-1)/2.0,(rows-1)/2.0),90,1)
    dst = cv.warpAffine(image1,M,(cols,rows))
    cv.imshow('img',dst)
    
    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

在这里插入图片描述

5.仿射变换

    在仿射变换中,原始图像中的所有平行线在输出图像中仍将平行。为了找到变换矩阵,我们需要输入图像中的三个点及其在输出图像中的对应位置。然后cv.getAffineTransform将创建一个2x3矩阵,该矩阵将传递给cv.warpAffine
    查看以下示例,并查看我选择的点(以绿色标记):

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    #1.加载彩色图像
    image1 = cv.imread(r'C:\Users\zhutianpeng\Desktop\panda.jpg',cv.IMREAD_COLOR)
    cv.imshow('ori',image1)
    rows,cols,channel = image1.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(image1,M,(cols,rows))
    cv.imshow('dst',dst)
    
    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

在这里插入图片描述

6.透视变换

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

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    #1.加载彩色图像
    image1 = cv.imread(r'C:\Users\zhutianpeng\Desktop\panda.jpg',cv.IMREAD_COLOR)
    #cv.imshow('ori',image1)
    rows,cols,channel = image1.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(image1,M,(300,300))
    plt.subplot(121),plt.imshow(image1),plt.title('Input')
    plt.subplot(122),plt.imshow(dst),plt.title('Output')
    plt.show()
    
    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

在这里插入图片描述

三、图像阈值

1.简单阈值

    对于每个像素,应用相同的阈值。如果像素值小于阈值,则将其设置为0,否则将其设置为最大值。函数cv.threshold用于应用阈值。第一个参数是源图像,它应该是灰度图像。第二个参数是阈值,用于对像素值进行分类。第三个参数是分配给超过阈值的像素值的最大值。OpenCV提供了不同类型的阈值,这由函数的第四个参数给出。
    通过使用cv.THRESH_BINARY类型。所有简单的阈值类型为:

  • cv.THRESH_BINARY
  • cv.THRESH_BINARY_INV
  • cv.THRESH_TRUNC
  • cv.THRESH_TOZERO
  • cv.THRESH_TOZERO_INV

    该方法返回两个输出。第一个是使用的阈值,第二个输出是阈值后的图像

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    #1.加载彩色图像
    image1 = cv.imread(r'C:\Users\zhutianpeng\Desktop\panda.jpg',cv.IMREAD_COLOR)
    ret,thresh1 = cv.threshold(image1,127,255,cv.THRESH_BINARY)
    ret,thresh2 = cv.threshold(image1,127,255,cv.THRESH_BINARY_INV)
    ret,thresh3 = cv.threshold(image1,127,255,cv.THRESH_TRUNC)
    ret,thresh4 = cv.threshold(image1,127,255,cv.THRESH_TOZERO)
    ret,thresh5 = cv.threshold(image1,127,255,cv.THRESH_TOZERO_INV)
    titles = ['Original Image','BINARY','BINARY_INV','TRUNC','TOZERO','TOZERO_INV']
    images = [image1, 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()
    
    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

在这里插入图片描述
    注意:为了绘制多个图像,我们使用plt.subplot()函数。请查看matplotlib文档以获取更多详细信息。

2.自适应阈值

    我们使用一个全局值作为阈值。但这可能并非在所有情况下都很好,例如,如果图像在不同区域具有不同的光照条件。在这种情况下,自适应阈值阈值化可以提供帮助。在此,算法基于像素周围的小区域确定像素的阈值。因此,对于同一图像的不同区域,我们获得了不同的阈值,这为光照度变化的图像提供了更好的结果。
    除上述参数外,方法cv.adaptiveThreshold还包含三个输入参数:
    该adaptiveMethod决定阈值是如何计算的:

  • cv.ADAPTIVE_THRESH_MEAN_C::阈值是邻近区域的平均值减去常数C
  • cv.ADAPTIVE_THRESH_GAUSSIAN_C:阈值是邻域值的高斯加权总和减去常数C

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

    下面的代码比较了光照变化的图像的全局阈值和自适应阈值:

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    #1.加载彩色图像
    image1 = cv.imread(r'C:\Users\zhutianpeng\Desktop\panda.jpg',cv.IMREAD_COLOR)
    ret,thresh1 = cv.threshold(image1,127,255,cv.ADAPTIVE_THRESH_MEAN_C)
    ret,thresh2 = cv.threshold(image1,127,255,cv.ADAPTIVE_THRESH_GAUSSIAN_C)
    titles = ['Original Image','ADAPTIVE_THRESH_MEAN_C','ADAPTIVE_THRESH_GAUSSIAN_C']
    images = [image1, thresh1, thresh2]
    for i in range(3):
        plt.subplot(2,3,i+1),plt.imshow(images[i],'gray')
        plt.title(titles[i])
        plt.xticks([]),plt.yticks([])
    plt.show()
    
    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

在这里插入图片描述

3.Otsu的二值化

    在全局阈值化中,我们使用任意选择的值作为阈值。相反,Otsu的方法避免了必须选择一个值并自动确定它的情况。
    考虑仅具有两个不同图像值的图像(双峰图像),其中直方图将仅包含两个峰。一个好的阈值应该在这两个值的中间。类似地,Otsu的方法从图像直方图中确定最佳全局阈值。
    为此,使用了cv.threshold作为附加标志传递。阈值可以任意选择。然后,算法找到最佳阈值,该阈值作为第一输出返回。
    查看以下示例。输入图像为噪点图像。在第一种情况下,采用值为127的全局阈值。在第二种情况下,直接采用Otsu阈值法。在第三种情况下,首先使用5x5高斯核对图像进行滤波以去除噪声,然后应用Otsu阈值处理。了解噪声滤波如何改善结果。

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    #1.加载彩色图像
    image1 = cv.imread(r'C:\Users\zhutianpeng\Desktop\panda.jpg',cv.IMREAD_COLOR)
    image1 = cv.resize(image1, None, fx=0.3, fy=0.3)
    image1 = cv.cvtColor(image1, cv.COLOR_BGR2GRAY)
    # 全局阈值
    ret1,th1 = cv.threshold(image1,127,255,cv.THRESH_BINARY)
    # Otsu阈值
    ret2,th2 = cv.threshold(image1,0,255,cv.THRESH_BINARY+cv.THRESH_OTSU)
    # 高斯滤波后再采用Otsu阈值
    blur = cv.GaussianBlur(image1,(5,5),0)
    ret3,th3 = cv.threshold(blur,0,255,cv.THRESH_BINARY+cv.THRESH_OTSU)
    # 绘制所有图像及其直方图
    images = [image1, 0, th1,
              image1, 0, th2,
              blur, 0, th3]
    titles = ['Original Noisy Image','Histogram','Global Thresholding (v=127)',
              'Original Noisy Image','Histogram',"Otsu's Thresholding",
              'Gaussian filtered Image','Histogram',"Otsu's Thresholding"]
    for i in range(3):
        plt.subplot(3,3,i*3+1),plt.imshow(images[i*3],'gray')
        plt.title(titles[i*3]), plt.xticks([]), plt.yticks([])
        plt.subplot(3,3,i*3+2),plt.hist(images[i*3].ravel(),256)
        plt.title(titles[i*3+1]), plt.xticks([]), plt.yticks([])
        plt.subplot(3,3,i*3+3),plt.imshow(images[i*3+2],'gray')
        plt.title(titles[i*3+2]), plt.xticks([]), plt.yticks([])
    plt.show()
    
    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

在这里插入图片描述
Otsu的二值化如何实现?
    本节演示了Otsu二值化的Python实现,以展示其实际工作方式。如果您不感兴趣,可以跳过此步骤。

    由于我们正在处理双峰图像,因此Otsu的算法尝试找到一个阈值(t),该阈值将由关系式给出的加权类内方差最小化:
在这里插入图片描述
    实际上,它找到位于两个峰值之间的t值,以使两个类别的差异最小。它可以简单地在Python中实现,如下所示:

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    #1.加载彩色图像
    image1 = cv.imread(r'C:\Users\zhutianpeng\Desktop\panda.jpg',cv.IMREAD_COLOR)
    image1 = cv.resize(image1, None, fx=0.3, fy=0.3)
    image1 = cv.cvtColor(image1, cv.COLOR_BGR2GRAY)
    blur = cv.GaussianBlur(image1,(5,5),0)
    # 寻找归一化直方图和对应的累积分布函数
    hist = cv.calcHist([blur],[0],None,[256],[0,256])
    hist_norm = hist.ravel()/hist.max()
    Q = hist_norm.cumsum()
    bins = np.arange(256)
    fn_min = np.inf
    thresh = -1
    for i in range(1,256):
        p1,p2 = np.hsplit(hist_norm,[i]) # 概率
        q1,q2 = Q[i],Q[255]-Q[i] # 对类求和
        b1,b2 = np.hsplit(bins,[i]) # 权重
        # 寻找均值和方差
        m1,m2 = np.sum(p1*b1)/q1, np.sum(p2*b2)/q2
        v1,v2 = np.sum(((b1-m1)**2)*p1)/q1,np.sum(((b2-m2)**2)*p2)/q2
        # 计算最小化函数
        fn = v1*q1 + v2*q2
        if fn < fn_min:
            fn_min = fn
            thresh = i
    # 使用OpenCV函数找到otsu的阈值
    ret, otsu = cv.threshold(blur,0,255,cv.THRESH_BINARY+cv.THRESH_OTSU)
    print( "{} {}".format(thresh,ret) )
    
    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

在这里插入图片描述

四、图像平滑

1.2D卷积(图像过滤)

    与一维信号一样,还可以使用各种低通滤波器(LPF),高通滤波器(HPF)等对图像进行滤波。LPF有助于消除噪声,使图像模糊等。HPF滤波器有助于在图像中找到边缘。
    OpenCV提供了一个函数cv.filter2D来将内核与图像进行卷积。例如,我们将尝试对图像进行平均滤波。5x5平均滤波器内核如下所示:
在这里插入图片描述
    操作如下:保持这个内核在一个像素上,将所有低于这个内核的25个像素相加,取其平均值,然后用新的平均值替换中心像素。它将对图像中的所有像素继续此操作。试试这个代码,并检查结果:

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    #1.加载彩色图像
    image1 = cv.imread(r'C:\Users\zhutianpeng\Desktop\panda.jpg',cv.IMREAD_COLOR)
    kernel = np.ones((5,5),np.float32)/25
    dst = cv.filter2D(image1,-1,kernel)
    plt.subplot(121),plt.imshow(image1),plt.title('Original')
    plt.xticks([]), plt.yticks([])
    plt.subplot(122),plt.imshow(dst),plt.title('Averaging')
    plt.xticks([]), plt.yticks([])
    plt.show()
    
    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

在这里插入图片描述

2.图像模糊(图像平滑)

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

1.平均
    这是通过将图像与归一化框滤镜进行卷积来完成的。它仅获取内核区域下所有像素的平均值,并替换中心元素。这是通过功能cv.blur()或cv.boxFilter()完成的。检查文档以获取有关内核的更多详细信息。我们应该指定内核的宽度和高度。3x3归一化框式过滤器如下所示:
在这里插入图片描述

    注意:如果您不想使用标准化的框式过滤器,请使用cv.boxFilter()。将参数normalize = False传递给函数。
    查看下面的示例演示,其内核大小为5x5:

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    #1.加载彩色图像
    image1 = cv.imread(r'C:\Users\zhutianpeng\Desktop\panda.jpg',cv.IMREAD_COLOR)
    blur = cv.blur(image1,(5,5))
    plt.subplot(121),plt.imshow(image1),plt.title('Original')
    plt.xticks([]), plt.yticks([])
    plt.subplot(122),plt.imshow(blur),plt.title('Blurred')
    plt.xticks([]), plt.yticks([])
    plt.show()
    
    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

在这里插入图片描述
2.高斯模糊
    在这种情况下,代替盒式滤波器,使用了高斯核。这是通过功能cv.GaussianBlur() 完成的。我们应指定内核的宽度和高度,该宽度和高度应为正数和奇数。我们还应指定X和Y方向的标准偏差,分别为sigmaX和sigmaY。如果仅指定sigmaX,则将sigmaY与sigmaX相同。如果两个都为零,则根据内核大小进行计算。高斯模糊对于从图像中去除高斯噪声非常有效。
    如果需要,可以使用函数cv.getGaussianKernel() 创建高斯内核。
    可以修改以上代码以实现高斯模糊:

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    #1.加载彩色图像
    image1 = cv.imread(r'C:\Users\zhutianpeng\Desktop\panda.jpg',cv.IMREAD_COLOR)
    blur = cv.GaussianBlur(image1,(5,5),0)
    plt.subplot(121),plt.imshow(image1),plt.title('Original')
    plt.xticks([]), plt.yticks([])
    plt.subplot(122),plt.imshow(blur),plt.title('Blurred')
    plt.xticks([]), plt.yticks([])
    plt.show()
    
    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

在这里插入图片描述
3.中位模糊

    在这里,函数cv.medianBlur() 提取内核区域下所有像素的中值,并将中心元素替换为该中值。这对于消除图像中的椒盐噪声非常有效。有趣的是,在上述过滤器中,中心元素是新计算的值,该值可以是图像中的像素值或新值。但是在中值模糊中,中心元素总是被图像中的某些像素值代替。有效降低噪音。其内核大小应为正奇数整数。

    在此演示中,我向原始图像添加了50%的噪声并应用了中值模糊。检查结果:

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    #1.加载彩色图像
    image1 = cv.imread(r'C:\Users\zhutianpeng\Desktop\panda.jpg',cv.IMREAD_COLOR)
    blur = cv.medianBlur(image1,5)
    plt.subplot(121),plt.imshow(image1),plt.title('Original')
    plt.xticks([]), plt.yticks([])
    plt.subplot(122),plt.imshow(blur),plt.title('Blurred')
    plt.xticks([]), plt.yticks([])
    plt.show()
    
    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

在这里插入图片描述
4.双边滤波

    cv.bilateralFilter() 在去除噪声的同时保持边缘清晰锐利非常有效。但是,与其他过滤器相比,该操作速度较慢。我们已经看到,高斯滤波器采用像素周围的邻域并找到其高斯加权平均值。高斯滤波器仅是空间的函数,也就是说,滤波时会考虑附近的像素。它不考虑像素是否具有几乎相同的强度。它不考虑像素是否是边缘像素。因此它也模糊了边缘,这是我们不想做的。
    双边滤波器在空间中也采用高斯滤波器,但是又有一个高斯滤波器,它是像素差的函数。空间的高斯函数确保仅考虑附近像素的模糊,而强度差的高斯函数确保仅考虑强度与中心像素相似的那些像素的模糊。由于边缘的像素强度变化较大,因此可以保留边缘。

    以下示例显示了使用双边过滤器

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    #1.加载彩色图像
    image1 = cv.imread(r'C:\Users\zhutianpeng\Desktop\panda.jpg',cv.IMREAD_COLOR)
    blur = cv.bilateralFilter(image1,9,75,75)
    plt.subplot(121),plt.imshow(image1),plt.title('Original')
    plt.xticks([]), plt.yticks([])
    plt.subplot(122),plt.imshow(blur),plt.title('Blurred')
    plt.xticks([]), plt.yticks([])
    plt.show()
    
    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

在这里插入图片描述

五、形态学转换

    形态变换是一些基于图像形状的简单操作。通常在二进制图像上执行。它需要两个输入,一个是我们的原始图像,第二个是决定操作性质的结构元素内核。两种基本的形态学算子是侵蚀和膨胀。然后,它的变体形式(如“打开”,“关闭”,“渐变”等)也开始起作用。

1.侵蚀

    侵蚀的基本思想就像土壤侵蚀一样,它侵蚀前景物体的边界(尽量使前景保持白色)。它是做什么的呢?内核滑动通过图像(在2D卷积中)。原始图像中的一个像素(无论是1还是0)只有当内核下的所有像素都是1时才被认为是1,否则它就会被侵蚀(变成0)。
    结果是,根据内核的大小,边界附近的所有像素都会被丢弃。因此,前景物体的厚度或大小减小,或只是图像中的白色区域减小。它有助于去除小的白色噪声(正如我们在颜色空间章节中看到的),分离两个连接的对象等。
    在这里,作为一个例子,我将使用一个5x5内核,它包含了所有的1。让我们看看它是如何工作的:

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    #1.加载彩色图像
    image1 = cv.imread(r'C:\Users\zhutianpeng\Desktop\panda.jpg',cv.IMREAD_COLOR)
    kernel = np.ones((5,5),np.uint8)
    erosion = cv.erode(image1,kernel,iterations = 1)
    cv.imshow('ori',image1)
    cv.imshow('erosion',erosion)
    
    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

在这里插入图片描述

2.扩张

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

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    #1.加载彩色图像
    image1 = cv.imread(r'C:\Users\zhutianpeng\Desktop\panda.jpg',cv.IMREAD_COLOR)
    kernel = np.ones((5,5),np.uint8)
    dilation = cv.dilate(image1,kernel,iterations = 1) 
    cv.imshow('ori',image1)
    cv.imshow('dilation',dilation)
    
    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

在这里插入图片描述

3.开运算

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

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    #1.加载彩色图像
    image1 = cv.imread(r'C:\Users\zhutianpeng\Desktop\panda.jpg',cv.IMREAD_COLOR)
    kernel = np.ones((5,5),np.uint8)
    opening = cv.morphologyEx(image1, cv.MORPH_OPEN, kernel) 
    cv.imshow('ori',image1)
    cv.imshow('opening',opening)
    
    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

在这里插入图片描述

4. 闭运算

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

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    #1.加载彩色图像
    image1 = cv.imread(r'C:\Users\zhutianpeng\Desktop\panda.jpg',cv.IMREAD_COLOR)
    kernel = np.ones((5,5),np.uint8)
    closing = cv.morphologyEx(image1 , cv.MORPH_CLOSE, kernel) 
    cv.imshow('ori',image1)
    cv.imshow('closing',closing)
    
    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

在这里插入图片描述

5.形态学梯度

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

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    #1.加载彩色图像
    image1 = cv.imread(r'C:\Users\zhutianpeng\Desktop\panda.jpg',cv.IMREAD_COLOR)
    kernel = np.ones((5,5),np.uint8)
    gradient = cv.morphologyEx(image1, cv.MORPH_GRADIENT, kernel) 
    cv.imshow('ori',image1)
    cv.imshow('gradient',gradient)
    
    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

在这里插入图片描述

6.顶帽

    它是输入图像和图像开运算之差。下面的示例针对9x9内核完成。

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    #1.加载彩色图像
    image1 = cv.imread(r'C:\Users\zhutianpeng\Desktop\panda.jpg',cv.IMREAD_COLOR)
    kernel = np.ones((9,9),np.uint8)
    tophat = cv.morphologyEx(image1, cv.MORPH_TOPHAT, kernel) 
    cv.imshow('ori',image1)
    cv.imshow('tophat',tophat)
    
    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

在这里插入图片描述

7.黑帽

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

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    #1.加载彩色图像
    image1 = cv.imread(r'C:\Users\zhutianpeng\Desktop\panda.jpg',cv.IMREAD_COLOR)
    kernel = np.ones((9,9),np.uint8)
    blackhat = cv.morphologyEx(image1, cv.MORPH_BLACKHAT, kernel) 
    cv.imshow('ori',image1)
    cv.imshow('blackhat',blackhat)
    
    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

在这里插入图片描述

六、图像梯度

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

1.Sobel 和 Scharr 算子

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

2.Laplacian 算子

在这里插入图片描述
    给出的图像的拉普拉斯图,它是每一阶导数通过Sobel算子计算。如果ksize = 1,然后使用以下内核用于过滤:
在这里插入图片描述
    下面的代码显示了单个图表中的所有算子。所有内核都是5x5大小。输出图像的深度通过-1得到结果的np.uint8型。

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    #1.加载彩色图像
    image1 = cv.imread(r'C:\Users\zhutianpeng\Desktop\panda.jpg',cv.IMREAD_COLOR)
    laplacian = cv.Laplacian(image1,cv.CV_64F)
    sobelx = cv.Sobel(image1,cv.CV_64F,1,0,ksize=5)
    sobely = cv.Sobel(image1,cv.CV_64F,0,1,ksize=5)
    plt.subplot(2,2,1),plt.imshow(image1,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()
    
    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

在这里插入图片描述
    在我们的最后一个示例中,输出数据类型为cv.CV_8U或np.uint8。但这有一个小问题。黑色到白色的过渡被视为正斜率(具有正值),而白色到黑色的过渡被视为负斜率(具有负值)。因此,当您将数据转换为np.uint8时,所有负斜率均​​设为零。简而言之,您会错过这一边缘信息。

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

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    #1.加载彩色图像
    image1 = cv.imread(r'C:\Users\zhutianpeng\Desktop\panda.jpg',cv.IMREAD_COLOR)
    # Output dtype = cv.CV_8U
    sobelx8u = cv.Sobel(image1,cv.CV_8U,1,0,ksize=5)
    # Output dtype = cv.CV_64F. Then take its absolute and convert to cv.CV_8U
    sobelx64f = cv.Sobel(image1,cv.CV_64F,1,0,ksize=5)
    abs_sobel64f = np.absolute(sobelx64f)
    sobel_8u = np.uint8(abs_sobel64f)
    plt.subplot(1,3,1),plt.imshow(image1,cmap = 'gray')
    plt.title('Original'), plt.xticks([]), plt.yticks([])
    plt.subplot(1,3,2),plt.imshow(sobelx8u,cmap = 'gray')
    plt.title('Sobel CV_8U'), plt.xticks([]), plt.yticks([])
    plt.subplot(1,3,3),plt.imshow(sobel_8u,cmap = 'gray')
    plt.title('Sobel abs(CV_64F)'), plt.xticks([]), plt.yticks([])
    plt.show()
    
    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

在这里插入图片描述

七、Canny边缘检测

    Canny Edge Detection是一种流行的边缘检测算法。它由John F. Canny发明

  • 这是一个多阶段算法,我们将经历每个阶段。
  • 降噪
    由于边缘检测容易受到图像中噪声的影响,因此第一步是使用5x5高斯滤波器消除图像中的噪声。我们已经在前面的章节中看到了这一点。

1.查找图像的强度梯度

    然后使用Sobel核在水平和垂直方向上对平滑的图像进行滤波,以在水平方向(Gx)和垂直方向(Gy)上获得一阶导数。渐变方向始终垂直于边缘。将其舍入为代表垂直,水平和两个对角线方向的四个角度之一。

  1. 非极大值抑制 在获得梯度大小和方向后,将对图像进行全面扫描,以去除可能不构成边缘的所有不需要的像素。为此,在每个像素处,检查像素是否是其在梯度方向上附近的局部最大值。点A在边缘(垂直方向)上。渐变方向垂直于边缘。点B和C在梯度方向上。因此,将A点与B点和C点进行检查,看是否形成局部最大值。如果是这样,则考虑将其用于下一阶段,否则将其抑制(置为零)。 简而言之,你得到的结果是带有“细边”的二进制图像。
  2. 磁滞阈值 该阶段确定哪些边缘全部是真正的边缘,哪些不是。为此,我们需要两个阈值minVal和maxVal。强度梯度大于maxVal的任何边缘必定是边缘,而小于minVal的那些边缘必定是非边缘,因此将其丢弃。介于这两个阈值之间的对象根据其连通性被分类为边缘或非边缘。如果将它们连接到“边缘”像素,则将它们视为边缘的一部分。边缘A在maxVal之上,因此被视为“确定边缘”。尽管边C低于maxVal,但它连接到边A,因此也被视为有效边,我们得到了完整的曲线。但是边缘B尽管在minVal之上并且与边缘C处于同一区域,但是它没有连接到任何“确保边缘”,因此被丢弃。因此,非常重要的一点是我们必须相应地选择minVal和maxVal以获得正确的结果。

2.OpenCV中的Canny Edge检测

    penCV将以上所有内容放在单个函数cv.Canny()中。我们将看到如何使用它。第一个参数是我们的输入图像。第二个和第三个参数分别是我们的minVal和maxVal。第三个参数是perture_size。它是用于查找图像渐变的Sobel内核的大小。默认情况下为3。最后一个参数是L2gradient,它指定用于查找梯度幅度的方程式。如果为True,则使用上面提到的更精确的公式,否则使用以下函数:
在这里插入图片描述
默认情况下,它为False。

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    #1.加载彩色图像
    image1 = cv.imread(r'C:\Users\zhutianpeng\Desktop\panda.jpg',cv.IMREAD_COLOR)
    edges = cv.Canny(image1,100,200)
    plt.subplot(121),plt.imshow(image1,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()
    
    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

在这里插入图片描述

八、图像金字塔

    通常,我们过去使用的是恒定大小的图像。但是在某些情况下,我们需要使用不同分辨率的(相同)图像。例如,当在图像中搜索某些东西(例如人脸)时,我们不确定对象将以多大的尺寸显示在图像中。在这种情况下,我们将需要创建一组具有不同分辨率的相同图像,并在所有图像中搜索对象。这些具有不同分辨率的图像集称为“图像金字塔”(因为当它们堆叠在底部时,最高分辨率的图像位于顶部,最低分辨率的图像位于顶部时,看起来像金字塔)。
    有两种图像金字塔。
        1)高斯金字塔和
        2)拉普拉斯金字塔
    高斯金字塔中的较高级别(低分辨率)是通过删除较低级别(较高分辨率)图像中的连续行和列而形成的。然后,较高级别的每个像素由基础级别的5个像素的贡献与高斯权重形成。通过这样做,
M×N图像变成M/2×N/2图像。因此面积减少到原始面积的四分之一。它称为Octave。当我们在金字塔中越靠上时(即分辨率下降),这种模式就会继续。同样,在扩展时,每个级别的面积变为4倍。我们可以使用cv.pyrDown()和cv.pyrUp()函数找到高斯金字塔。

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    #1.加载彩色图像
    image1 = cv.imread(r'C:\Users\zhutianpeng\Desktop\panda.jpg',cv.IMREAD_COLOR)
    lower_reso = cv.pyrDown(image1)
    higher_reso2 = cv.pyrUp(lower_reso) 
    cv.imshow('ori',image1)
    cv.imshow('lower_reso',lower_reso)
    cv.imshow('higher_reso',higher_reso2)
    
    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

在这里插入图片描述
    记住,higher_reso2不等于higher_reso,因为一旦降低了分辨率,就会丢失信息。下面的图像是3层的金字塔从最小的图像在前面的情况下创建。
    拉普拉斯金字塔由高斯金字塔形成。没有专用功能。拉普拉斯金字塔图像仅像边缘图像。它的大多数元素为零。它们用于图像压缩。拉普拉斯金字塔的层由高斯金字塔的层与高斯金字塔的高层的扩展版本之间的差形成。拉普拉斯等级的三个等级如下所示(调整对比度以增强内容):

使用金字塔进行图像融合

    金字塔的一种应用是图像融合。例如,在图像拼接中,您需要将两个图像堆叠在一起,但是由于图像之间的不连续性,可能看起来不太好。在这种情况下,使用金字塔混合图像可以无缝混合,而不会在图像中保留大量数据。
    请检查其他资源中的第一个参考,它具有图像混合,拉普拉斯金字塔等的完整图解详细信息。只需完成以下步骤即可:

  1. 加载苹果和橙子的两个图像
  2. 查找苹果和橙子的高斯金字塔
  3. 在高斯金字塔中,找到其拉普拉斯金字塔
  4. 然后在每个拉普拉斯金字塔级别中加入苹果的左半部分和橙子的右半部分
  5. 最后从此联合图像金字塔中重建原始图像。
import cv2 as cv
import numpy as np,sys
A = cv.imread('apple.jpg')
B = cv.imread('orange.jpg')
# 生成A的高斯金字塔
G = A.copy()
gpA = [G]
for i in xrange(6):
    G = cv.pyrDown(G)
    gpA.append(G)
# 生成B的高斯金字塔
G = B.copy()
gpB = [G]
for i in xrange(6):
    G = cv.pyrDown(G)
    gpB.append(G)
# 生成A的拉普拉斯金字塔
lpA = [gpA[5]]
for i in xrange(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 xrange(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 xrange(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)
## 

九、OpenCV中的轮廓

    轮廓可以简单地解释为连接具有相同颜色或强度的所有连续点(沿边界)的曲线。轮廓是用于形状分析以及对象检测和识别的有用工具。

  1. 为了获得更高的准确性,请使用二进制图像。因此,在找到轮廓之前,请应用阈值或canny边缘检测。
  2. 从OpenCV 3.2开始,findContours()不再修改源图像。
  3. 在OpenCV中,找到轮廓就像从黑色背景中找到白色物体。因此请记住,要找到的对象应该是白色,背景应该是黑色。

让我们看看如何找到二进制图像的轮廓:

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    #1.加载彩色图像
    image1 = cv.imread(r'C:\Users\zhutianpeng\Desktop\panda.jpg',cv.IMREAD_COLOR)
    imgray = cv.cvtColor(image1, cv.COLOR_BGR2GRAY)
    ret, thresh = cv.threshold(imgray, 127, 255, 0)
    contours, hierarchy = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)
    
    print(contours)

    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

在这里插入图片描述
    findcontour()函数中有三个参数,第一个是源图像,第二个是轮廓检索模式,第三个是轮廓逼近方法。输出等高线和层次结构。轮廓是图像中所有轮廓的Python列表。每个单独的轮廓是一个(x,y)坐标的Numpy数组的边界点的对象。
    注意 稍后我们将详细讨论第二和第三个参数以及有关层次结构。在此之前,代码示例中赋予它们的值将适用于所有图像。

1.绘制轮廓

    要绘制轮廓,请使用cv.drawContours函数。只要有边界点,它也可以用来绘制任何形状。它的第一个参数是源图像,第二个参数是应该作为Python列表传递的轮廓,第三个参数是轮廓的索引(在绘制单个轮廓时有用。要绘制所有轮廓,请传递-1),其余参数是颜色,厚度等等
在图像中绘制所有轮廓:

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    #1.加载彩色图像
    image1 = cv.imread(r'C:\Users\zhutianpeng\Desktop\panda.jpg',cv.IMREAD_COLOR)
    imgray = cv.cvtColor(image1, cv.COLOR_BGR2GRAY)
    ret, thresh = cv.threshold(imgray, 127, 255, 0)
    contours, hierarchy = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)
    newImage = cv.drawContours(image1, contours, -1, (0,255,0), 3)
    
    cv.imshow('new',newImage)

    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

在这里插入图片描述

绘制单个轮廓,如第四个轮廓:

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    #1.加载彩色图像
    image1 = cv.imread(r'C:\Users\zhutianpeng\Desktop\panda.jpg',cv.IMREAD_COLOR)
    imgray = cv.cvtColor(image1, cv.COLOR_BGR2GRAY)
    ret, thresh = cv.threshold(imgray, 127, 255, 0)
    contours, hierarchy = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)
    newImage = cv.drawContours(image1, contours, 3, (0,255,0), 3) 
    
    cv.imshow('new',newImage)

    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

在这里插入图片描述
但是在大多数情况下,以下方法会很有用:

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    #1.加载彩色图像
    image1 = cv.imread(r'C:\Users\zhutianpeng\Desktop\panda.jpg',cv.IMREAD_COLOR)
    imgray = cv.cvtColor(image1, cv.COLOR_BGR2GRAY)
    ret, thresh = cv.threshold(imgray, 127, 255, 0)
    contours, hierarchy = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)
    cnt = contours[4]
    newImage = cv.drawContours(image1, [cnt], 0, (0,255,0), 3)
    
    cv.imshow('new',newImage)

    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

在这里插入图片描述

注意 最后两种方法相似,但是前进时,您会发现最后一种更有用。

轮廓近似方法

    这是cv.findContours函数中的第三个参数。它实际上表示什么?

    上面我们告诉我们轮廓是强度相同的形状的边界。它存储形状边界的(x,y)坐标。但是它存储所有坐标吗?这是通过这种轮廓近似方法指定的。

    如果传递cv.CHAIN_APPROX_NONE,则将存储所有边界点。但是实际上我们需要所有这些要点吗?例如,您找到了一条直线的轮廓。您是否需要线上的所有点来代表该线?不,我们只需要该线的两个端点即可。这就是cv.CHAIN_APPROX_SIMPLE所做的。它删除所有冗余点并压缩轮廓,从而节省内存。

    下面的矩形图像演示了此技术。只需在轮廓数组中的所有坐标上绘制一个圆(以蓝色绘制)。第一幅图像显示了我用cv.CHAIN_APPROX_NONE获得的积分(734个点),第二幅图像显示了我用cv.CHAIN_APPROX_SIMPLE获得的效果(只有4个点)。看,它可以节省多少内存!!!

2.轮廓特征

1. 特征矩
    特征矩可以帮助您计算一些特征,例如物体的质心,物体的面积等。请查看特征矩上的维基百科页面。函数cv.moments()提供了所有计算出的矩值的字典。

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    #1.加载彩色图像
    image1 = cv.imread(r'C:\Users\zhutianpeng\Desktop\orange.jpg',0)
    ret,thresh = cv.threshold(image1,127,255,0)
    contours,hierarchy = cv.findContours(thresh, 1, 2)
    cnt = contours[0]
    M = cv.moments(cnt)
    print( M )

    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

从这一刻起,您可以提取有用的数据,例如面积,质心等。质心由关系给出,cx = int(M[‘m10’]/M[‘m00’])
cy = int(M[‘m01’]/M[‘m00’])

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

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    #1.加载彩色图像
    image1 = cv.imread(r'C:\Users\zhutianpeng\Desktop\orange.jpg',0)
    ret,thresh = cv.threshold(image1,127,255,0)
    contours,hierarchy = cv.findContours(thresh, 1, 2)
    cnt = contours[0]
    area = cv.contourArea(cnt) 
    print( area )

    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

在这里插入图片描述
3. 轮廓周长
    也称为弧长。可以使用cv.arcLength()函数找到它。第二个参数指定形状是闭合轮廓(True)还是曲线。

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    #1.加载彩色图像
    image1 = cv.imread(r'C:\Users\zhutianpeng\Desktop\orange.jpg',0)
    ret,thresh = cv.threshold(image1,127,255,0)
    contours,hierarchy = cv.findContours(thresh, 1, 2)
    cnt = contours[0]
    perimeter = cv.arcLength(cnt,True)
    print( perimeter )

    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

在这里插入图片描述

4. 轮廓近似

    根据我们指定的精度,它可以将轮廓形状近似为顶点数量较少的其他形状。它是Douglas-Peucker算法的实现。检查维基百科页面上的算法和演示。

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

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    #1.加载彩色图像
    image1 = cv.imread(r'C:\Users\zhutianpeng\Desktop\orange.jpg',0)
    ret,thresh = cv.threshold(image1,127,255,0)
    contours,hierarchy = cv.findContours(thresh, 1, 2)
    cnt = contours[0]
    epsilon = 0.1*cv.arcLength(cnt,True) 
    approx = cv.approxPolyDP(cnt,epsilon,True)
    print( approx )

    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

在这里插入图片描述
5. 轮廓凸包
    凸包外观看起来与轮廓逼近相似,但不相似(在某些情况下两者可能提供相同的结果)。在这里,cv.convexHull()函数检查曲线是否存在凸凹缺陷并对其进行校正。一般而言,凸曲线是始终凸出或至少平坦的曲线。如果在内部凸出,则称为凸度缺陷。例如,检查下面的手的图像。红线显示手的凸包。双向箭头标记显示凸度缺陷,这是凸包与轮廓线之间的局部最大偏差。

hull = cv.convexHull(points[, hull[, clockwise[, returnPoints]] 

    参数详细信息: - 点**是我们传递到的轮廓。 - 凸包是输出,通常我们忽略它。 - **顺时针方向:方向标记。如果为True,则输出凸包为顺时针方向。否则,其方向为逆时针方向。 - returnPoints:默认情况下为True。然后返回凸包的坐标。如果为False,则返回与凸包点相对应的轮廓点的索引。

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    #1.加载彩色图像
    image1 = cv.imread(r'C:\Users\zhutianpeng\Desktop\orange.jpg',0)
    ret,thresh = cv.threshold(image1,127,255,0)
    contours,hierarchy = cv.findContours(thresh, 1, 2)
    cnt = contours[0]
    hull = cv.convexHull(cnt) 
    print( hull )

    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

在这里插入图片描述
    但是,如果要查找凸度缺陷,则需要传递returnPoints = False。为了理解它,我们将拍摄上面的矩形图像。首先,我发现它的轮廓为cnt。

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

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    #1.加载彩色图像
    image1 = cv.imread(r'C:\Users\zhutianpeng\Desktop\orange.jpg',0)
    ret,thresh = cv.threshold(image1,127,255,0)
    contours,hierarchy = cv.findContours(thresh, 1, 2)
    cnt = contours[0]
    k = cv.isContourConvex(cnt) 
    print( k )

    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

在这里插入图片描述
7. 边界矩形

7.a.直角矩形

    它是一个矩形,不考虑物体的旋转。所以边界矩形的面积不是最小的。它是由函数cv.boundingRect()找到的。

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

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    #1.加载彩色图像
    image1 = cv.imread(r'C:\Users\zhutianpeng\Downloads\8c2dd09c3460edf0fec9d3ae08cd159d.jpg',0)
    ret,thresh = cv.threshold(image1,127,255,0)
    contours,hierarchy = cv.findContours(thresh, 1, 2)
    cnt = contours[0]
    x,y,w,h = cv.boundingRect(cnt)
    cv.rectangle(image1,(x,y),(x+w,y+h),(0,255,0),2)
    cv.imshow('s',image1)

    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

在这里插入图片描述
7.b. 旋转矩形
    这里,边界矩形是用最小面积绘制的,所以它也考虑了旋转。使用的函数是cv.minAreaRect()。它返回一个Box2D结构,其中包含以下细节 -(中心(x,y),(宽度,高度),旋转角度)。但要画出这个矩形,我们需要矩形的四个角。它由函数cv.boxPoints()获得

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    #1.加载彩色图像
    image1 = cv.imread(r'C:\Users\zhutianpeng\Downloads\8c2dd09c3460edf0fec9d3ae08cd159d.jpg',0)
    ret,thresh = cv.threshold(image1,127,255,0)
    contours,hierarchy = cv.findContours(thresh, 1, 2)
    cnt = contours[0]
    rect = cv.minAreaRect(cnt)
    box = cv.boxPoints(rect)
    box = np.intc(box)
    cv.drawContours(image1,[box],0,(0,0,255),2)
    cv.imshow('s',image1)

    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

在这里插入图片描述
8. 最小闭合圈
    使用函数**cv.minEnclosingCircle(*()查找对象的圆周。它是一个以最小面积完全覆盖物体的圆。

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    #1.加载彩色图像
    image1 = cv.imread(r'C:\Users\zhutianpeng\Desktop\orange.jpg',0)
    ret,thresh = cv.threshold(image1,127,255,0)
    contours,hierarchy = cv.findContours(thresh, 1, 2)
    cnt = contours[0]
    (x,y),radius = cv.minEnclosingCircle(cnt)
    center = (int(x),int(y))
    radius = int(radius)
    cv.circle(image1,center,radius,(0,255,0),2)
    cv.imshow('s',image1)

    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

在这里插入图片描述
9. 拟合一个椭圆
    把一个椭圆拟合到一个物体上。它返回内接椭圆的旋转矩形。

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    #1.加载彩色图像
    image1 = cv.imread(r'C:\Users\zhutianpeng\Desktop\orange.jpg',0)
    ret,thresh = cv.threshold(image1,127,255,0)
    contours,hierarchy = cv.findContours(thresh, 1, 2)
    cnt = contours[0]
    ellipse = cv.fitEllipse(cnt)
    cv.ellipse(image1,ellipse,(0,255,0),2)
    cv.imshow('s',image1)

    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

10. 拟合直线
    我们可以将一条直线拟合到一组点。下图包含一组白点。我们可以近似一条直线。

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    #1.加载彩色图像
    image1 = cv.imread(r'C:\Users\zhutianpeng\Desktop\orange.jpg',0)
    ret,thresh = cv.threshold(image1,127,255,0)
    contours,hierarchy = cv.findContours(thresh, 1, 2)
    cnt = contours[0]
    rows,cols = image1.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(image1,(cols-1,righty),(0,lefty),(0,255,0),2)
    cv.imshow('s',image1)

    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

在这里插入图片描述

3.轮廓属性

1. 长宽比
它是对象边界矩形的宽度与高度的比值。
在这里插入图片描述

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    #1.加载彩色图像
    image1 = cv.imread(r'C:\Users\zhutianpeng\Desktop\orange.jpg',0)
    ret,thresh = cv.threshold(image1,127,255,0)
    contours,hierarchy = cv.findContours(thresh, 1, 2)
    cnt = contours[0]
    x,y,w,h = cv.boundingRect(cnt)
    aspect_ratio = float(w)/h
    print(aspect_ratio)

    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

2. 范围
范围是轮廓区域与边界矩形区域的比值。
在这里插入图片描述

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    #1.加载彩色图像
    image1 = cv.imread(r'C:\Users\zhutianpeng\Desktop\orange.jpg',0)
    ret,thresh = cv.threshold(image1,127,255,0)
    contours,hierarchy = cv.findContours(thresh, 1, 2)
    cnt = contours[0]
    area = cv.contourArea(cnt)
    x,y,w,h = cv.boundingRect(cnt)
    rect_area = w*h
    extent = float(area)/rect_area
    print(extent)

    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

3. 坚实度
坚实度是等高线面积与其凸包面积之比。
在这里插入图片描述

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    #1.加载彩色图像
    image1 = cv.imread(r'C:\Users\zhutianpeng\Desktop\orange.jpg',0)
    ret,thresh = cv.threshold(image1,127,255,0)
    contours,hierarchy = cv.findContours(thresh, 1, 2)
    cnt = contours[0]
    area = cv.contourArea(cnt)
    hull = cv.convexHull(cnt)
    hull_area = cv.contourArea(hull)
    solidity = float(area)/hull_area
    print(solidity)

    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

4. 等效直径
等效直径是面积与轮廓面积相同的圆的直径。
在这里插入图片描述

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    #1.加载彩色图像
    image1 = cv.imread(r'C:\Users\zhutianpeng\Desktop\orange.jpg',0)
    ret,thresh = cv.threshold(image1,127,255,0)
    contours,hierarchy = cv.findContours(thresh, 1, 2)
    cnt = contours[0]
    area = cv.contourArea(cnt)
    equi_diameter = np.sqrt(4*area/np.pi)
    print(equi_diameter)

    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

5. 取向
取向是物体指向的角度。以下方法还给出了主轴和副轴的长度。

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    #1.加载彩色图像
    image1 = cv.imread(r'C:\Users\zhutianpeng\Desktop\orange.jpg',0)
    ret,thresh = cv.threshold(image1,127,255,0)
    contours,hierarchy = cv.findContours(thresh, 1, 2)
    cnt = contours[0]
    (x,y),(MA,ma),angle = cv.fitEllipse(cnt)
    print(angle)

    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

6. 掩码和像素点
在某些情况下,我们可能需要构成该对象的所有点。可以按照以下步骤完成:

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    #1.加载彩色图像
    image1 = cv.imread(r'C:\Users\zhutianpeng\Desktop\orange.jpg',0)
    imgray = cv.cvtColor(image1, cv.COLOR_BGR2GRAY)
    ret,thresh = cv.threshold(image1,127,255,0)
    contours,hierarchy = cv.findContours(thresh, 1, 2)
    cnt = contours[0]
    mask = np.zeros(imgray.shape,np.uint8)
    cv.drawContours(mask,[cnt],0,255,-1)
    pixelpoints = np.transpose(np.nonzero(mask))
    #pixelpoints = cv.findNonZero(mask)
    print(pixelpoints)

    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

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

7. 最大值,最小值和它们的位置
我们可以使用掩码图像找到这些参数。

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    #1.加载彩色图像
    image1 = cv.imread(r'C:\Users\zhutianpeng\Desktop\orange.jpg',0)
    imgray = cv.cvtColor(image1, cv.COLOR_BGR2GRAY)
    ret,thresh = cv.threshold(image1,127,255,0)
    contours,hierarchy = cv.findContours(thresh, 1, 2)
    cnt = contours[0]
    mask = np.zeros(imgray.shape,np.uint8)
    min_val, max_val, min_loc, max_loc = cv.minMaxLoc(imgray,mask = mask)


    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

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

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    #1.加载彩色图像
    image1 = cv.imread(r'C:\Users\zhutianpeng\Desktop\orange.jpg',0)
    imgray = cv.cvtColor(image1, cv.COLOR_BGR2GRAY)
    ret,thresh = cv.threshold(image1,127,255,0)
    contours,hierarchy = cv.findContours(thresh, 1, 2)
    cnt = contours[0]
    mask = np.zeros(imgray.shape,np.uint8)
    mean_val = cv.mean(imgray,mask = mask)


    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

9. 极端点

极点是指对象的最顶部,最底部,最右侧和最左侧的点。

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    #1.加载彩色图像
    image1 = cv.imread(r'C:\Users\zhutianpeng\Desktop\orange.jpg',0)
    imgray = cv.cvtColor(image1, cv.COLOR_BGR2GRAY)
    ret,thresh = cv.threshold(image1,127,255,0)
    contours,hierarchy = cv.findContours(thresh, 1, 2)
    cnt = contours[0]
    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])


    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

4.轮廓:更多属性

1. 凸性缺陷
    我们看到了关于轮廓的第二章的凸包。从这个凸包上的任何偏差都可以被认为是凸性缺陷。 OpenCV有一个函数来找到这个,cv.convexityDefects()。一个基本的函数调用如下:

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

    注意 记住,我们必须在发现凸包时,传递returnPoints= False,以找到凸性缺陷。

    它返回一个数组,其中每行包含这些值—[起点、终点、最远点、到最远点的近似距离]。我们可以用图像把它形象化。我们画一条连接起点和终点的线,然后在最远处画一个圆。记住,返回的前三个值是cnt的索引。所以我们必须从cnt中获取这些值。

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

def main():
    #1.加载彩色图像
    image1 = cv.imread(r'C:\Users\zhutianpeng\Desktop\panda.jpg',cv.IMREAD_COLOR)
    img_gray = cv.cvtColor(image1, cv.COLOR_BGR2GRAY)
    ret,thresh = cv.threshold(img_gray, 127, 255,0)
    contours,hierarchy = cv.findContours(thresh,2,1)
    cnt = contours[0]
    hull = cv.convexHull(cnt,returnPoints = True)
    defects = cv.convexityDefects(cnt,hull)
    for i in range(defects.shape[0]):
        s,e,f,d = defects[i,0]
        start = tuple(cnt[s][0])
        end = tuple(cnt[e][0])
        far = tuple(cnt[f][0])
        cv.line(image1,start,end,[0,255,0],2)
        cv.circle(image1,far,5,[0,0,255],-1)
    cv.imshow('img',image1)


    k = cv.waitKey(0) & 0xFF
    
if __name__ == "__main__":
    main()

2. 点多边形测试

这个函数找出图像中一点到轮廓线的最短距离。它返回的距离,点在轮廓线外时为负,点在轮廓线内时为正,点在轮廓线上时为零。
例如,我们可以检查点(50,50)如下:

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

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

    注意:如果您不想找到距离,请确保第三个参数为False,因为这是一个耗时的过程。因此,将其设置为False可使速度提高2-3倍。

3. 形状匹配
    OpenCV附带一个函数cv.matchShapes(),该函数使我们能够比较两个形状或两个轮廓,并返回一个显示相似性的度量。结果越低,匹配越好。它是根据矩值计算出来的。不同的测量方法在文档中有解释。

import cv2 as cv
import numpy as np
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)
print( ret )

5.轮廓分层

层次结构是什么?
    通常我们使用cv.findcontour()函数来检测图像中的对象,对吧?有时对象在不同的位置。但在某些情况下,某些形状在其他形状中。就像嵌套的图形一样。在这种情况下,我们把外部的称为父类,把内部的称为子类。这样,图像中的轮廓就有了一定的相互关系。我们可以指定一个轮廓是如何相互连接的,比如,它是另一个轮廓的子轮廓,还是父轮廓等等。这种关系的表示称为层次结构

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

        “Next表示同一层次的下一个轮廓。”

    例如,在我们的图片中取contour-0。谁是下一个同级别的等高线?这是contour-1。简单地令Next = 1。类似地,Contour-1也是contour-2。所以Next = 2。 contour-2呢?同一水平线上没有下一条等高线。简单地,让Next = -1。contour-4呢?它与contour-5处于同一级别。它的下一条等高线是contour-5,所以next = 5。

    “Previous表示同一层次上的先前轮廓。”

    和上面一样。contour-1之前的等值线为同级别的contour-0。类似地,contour-2也是contour-1。对于contour-0,没有前项,所以设为-1。

        “First_Child表示它的第一个子轮廓。”

    没有必要作任何解释。对于contour-2, child是contour-2a。从而得到contour-2a对应的指标值。contour-3a呢?它有两个孩子。但我们只关注第一个孩子。它是contour-4。那么First_Child = 4 对contour-3a而言。

        “Parent表示其父轮廓的索引。”

    它与First_Child相反。对于轮廓线-4和轮廓线-5,父轮廓线都是轮廓线-3a。对于轮廓3a,它是轮廓-3,以此类推。

    注意 如果没有子元素或父元素,则该字段被视为-1

    现在我们已经了解了OpenCV中使用的层次样式,我们可以借助上面给出的相同图像来检查OpenCV中的轮廓检索模式。一些标志如 cv.RETR_LIST, cv.RETR_TREE,cv.RETR_CCOMP, cv.RETR_EXTERNAL等等的含义。

十、傅里叶变换(待更新)

1.变换

    

1.缩放

    

十一、模板匹配(待更新)

1.变换

    

1.缩放

    

十二、霍夫线变换(待更新)

1.变换

    

1.缩放

    

十三、霍夫圈变换(待更新)

1.变换

    

1.缩放

    

十四、图像分割与Watershed算法(待更新)

1.变换

    

1.缩放

    

十五、交互式前景提取使用GrabCut算法(待更新)

1.变换

    

1.缩放

    


“笑对人生,智慧同行!博客新文出炉,微信订阅号更新更实时,等你笑纳~”
在这里插入图片描述

相关推荐

  1. Python图像处理篇】opencv去畸变

    2024-07-11 18:24:01       32 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-11 18:24:01       66 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-11 18:24:01       70 阅读
  3. 在Django里面运行非项目文件

    2024-07-11 18:24:01       57 阅读
  4. Python语言-面向对象

    2024-07-11 18:24:01       68 阅读

热门阅读

  1. git revert怎么使用?

    2024-07-11 18:24:01       24 阅读
  2. Webpack配置及工作流程

    2024-07-11 18:24:01       21 阅读
  3. 如何理解李彦宏说的“不要卷模型,要卷应用”

    2024-07-11 18:24:01       22 阅读
  4. 谷歌广告投放策略 -- 业务&成本

    2024-07-11 18:24:01       18 阅读
  5. 表单代码示例

    2024-07-11 18:24:01       22 阅读
  6. Unity中短路法在背包系统的应用

    2024-07-11 18:24:01       17 阅读
  7. 3133. 数组最后一个元素的最小值

    2024-07-11 18:24:01       22 阅读
  8. windows脚本获取 svn版本号

    2024-07-11 18:24:01       19 阅读
  9. 力扣题解(摆动序列)

    2024-07-11 18:24:01       21 阅读
  10. 搭建discuz论坛(lvs+nginx+http+mysql+nfs)8台服务器

    2024-07-11 18:24:01       18 阅读
  11. 【AI大模型】如何在企业环境中部署GPT-3/GPT-4模型

    2024-07-11 18:24:01       22 阅读
  12. 图论学习 c++Ford-Fulkerson 方法

    2024-07-11 18:24:01       19 阅读