如果需要處理的原圖及程式碼,請移步小編的GitHub地址
傳送門:請點選我
如果點選有誤:https://github.com/LeBron-Jian/ComputerVisionPractice
形態學操作簡單來說,就是改變物體的形狀,下面學習一下,首先本文的目錄如下:
- 1,定義結構元素
- 2,腐蝕和膨脹
- 3,開運算和閉運算
- 4,禮帽/頂帽,黑帽演算法
- 5,梯度運算
- 6,形態學運算 檢測邊和角點(1,檢測邊緣 ; 2,檢測拐角)
1,定義結構元素
形態學操作的原理:在特殊領域運算形式——結構元素(Structure Element),在每個畫素位置上與二值影像對應的區域進行特定的邏輯運算。運算結構是輸出影像的相應畫素。運算效果取決於結構元素大小內容以及邏輯運算性質。
結構元素:膨脹和腐蝕操作的最基本組成部分,用於測試輸出影像,通常要比待處理的影像小很多,二維平面結構元素由一個數值為0或1的矩陣組成。結構元素的原點指定了影像中需要處理的畫素範圍,結構元素中數值為1的點決定結構元素的領域畫素進行膨脹或腐蝕操作時是否需要參與計算。
形態學處理的核心就是定義結構元素,在OpenCV-Python中,可以使用其自帶的 getStructuringElement 函式,也可以直接使用 Numpy 的 ndarray 來定義一個結構元素,形象圖如下:
下面程式碼為上圖的十字形,程式碼如下:
#_*_coding:utf-8_*_ import cv2 import numpy as np def show_element(): element_cross = cv2.getStructuringElement(cv2.MORPH_CROSS, (5, 5)) print(element_cross) element_ellipse = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) print(element_ellipse) element_rect = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) print(element_rect) ''' [[0 0 1 0 0] [0 0 1 0 0] [1 1 1 1 1] [0 0 1 0 0] [0 0 1 0 0]] [[0 0 1 0 0] [1 1 1 1 1] [1 1 1 1 1] [1 1 1 1 1] [0 0 1 0 0]] [[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]]''' def define_cross_structure(): NpKernel = np.uint8(np.zeros((5, 5))) for i in range(5): NpKernel[2, i] = 1 NpKernel[i, 2] = 1 print("NpKernel", NpKernel) ''' NpKernel [[0 0 1 0 0] [0 0 1 0 0] [1 1 1 1 1] [0 0 1 0 0] [0 0 1 0 0]] '''
上面我們自定義了一個結構元素 kernel,先宣告一個矩陣,然後對其進行賦值,這種方法靈活但是略顯複雜。OpenCV提供了一個函式 也就是上面展示的,可以獲取常用結構元素的性質:矩形(包括線形),橢圓(包括圓形)以及十字形。下面具體學習一下此方法
1.1 定義一些基本符號和關係
1,元素
設有一幅影像X,若點 a 在 X 的區域以內,則稱 a 為 X 的元素,記做 a 屬於 X,如圖 6.1所示。
2,B包含於X
設有兩幅影像 B, X。對於 B中所有的元素 ai, 都有 ai 屬於 X,則稱B包含於 (included in)X ,記做 B 屬於 X,如圖6.2所示。
3,B擊中 X
設有兩幅影像B, X。若存在這一一個點,它即是B的元素,又是 X 的元素,則稱 B 擊中(hit)X,記做 B ↑ X,如圖6.3所示。
4,B不擊中 X
設有兩幅影像B, X。若不存在任何一個點,它既是B的元素,又是 X的元素,即 B和 X的交集是空,則稱 B 不擊中(miss)X,記做 B ∩ X = Φ;其中 ∩ 是集合運算相交的符號,Φ 表示空集,如圖6.4所示。
5,補集
設有一幅影像 X,所有 X 區域以外的點構成的集合稱為 X 的補集,記做 Xc,如下圖所示。顯然,如果B ∩ X = Φ,則 B 在 X的補集內,即 B 屬於 Xc。
6,結構元素
設有兩幅影像B,X。若X是被處理的物件,而B是用來處理X的,則稱B為結構元素(structure element),又被形象的稱作刷子。結構元素通常都是一些比較小的影像。
7,對稱集
設有一幅影像B,將B中所有元素的座標取反,即令(x, y)變為(-x, -y),所有這些點構成的新的集合稱為B的對稱集,記做 Bv,如下圖6.6所示。
8,平移
設有一幅影像B,有一個點a(x0, y0),將B平移a後的結果是,把B中所有元素的橫座標加 x0,縱座標加 y0,即令(x, y)變成(x + x0, y+y0),所有這些點構成新的集合稱為B的平移,記做 Ba,如圖6.7所示。
1.2 getStructuringElement 方法
getStructuringElement 是OpenCV提供的一個函式,getStructuringElement 的內部並沒有什麼優化實現,只是封裝了一些功能,其原理同樣是宣告瞭一個矩陣,然後求形狀,指定矩陣的值。而我們只需要直接呼叫即可。
函式原型如下:
def getStructuringElement(shape, ksize, anchor=None):
引數的意思:
- shape 表示核心的形狀,有三種形狀可以選擇:
——十字形:cv2.getStructuringElement(cv2.MORPH_CROSS,(5,5))
——橢圓:cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(5,5))
——矩形:cv2.getStructuringElement(cv2.MORPH_RECT,(5,5))
- ksize 表示核心的尺寸(n, n)
- anchor 錨點的位置
此函式最終會返回指定形狀和尺寸的結構元素。
下面程式碼實現一下,這裡同時展示一下自己寫的:
kernel1 = np.ones((3, 3), np.uint8) kernel2 = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) print(kernel1) print(kernel2) print(kernel1 == kernel2) ''' [[1 1 1] [1 1 1] [1 1 1]] [[1 1 1] [1 1 1] [1 1 1]] [[ True True True] [ True True True] [ True True True]] '''
這裡其實再證明一次。
2,腐蝕和膨脹
影像的膨脹(Dilation)和腐蝕(Erosion)是兩種基本的形態學運算,主要用來尋找影像中的極大區域和極小區域。其中膨脹類似於“領域擴張”,將影像中的高亮區域或白色部分進行擴張,其執行結果圖比原圖的高亮區域更大;腐蝕類似於“領域被蠶食”,將影像中的高亮區域或白色部分進行縮減細化,其執行結果圖比原圖的高亮區域更小。
形態學各種功能實現,都歸結為腐蝕 erode 和 膨脹 dilate 的組合,形象理解一下就是腐蝕等於變瘦,膨脹等於變胖,所以下面學習一下腐蝕和膨脹。
注意:腐蝕和膨脹主要針對二值化影像的白色部分。
2.1 腐蝕
腐蝕就是把結構元素B平移a後得到Ba,若Ba包含於X,我們記下這個a點,所有滿足上述條件的 a點組成的集合稱為X被B腐蝕(Erosion)的結果。
上圖 X 是被處理的物件,B是結構元素,不難知道,對於任意一個在陰影部分的點 a,Ba包含於X,所以 X被B腐蝕的結果就是那個陰影部分,陰影部分在 X的範圍之內,且比 X小,就像 X 被剝掉了一層似的,這0就是為什麼叫腐蝕的原因。
腐蝕的運算子為 “ - ”,其定義如下:
該公式表示影像A用卷積模板B來進行腐蝕處理,通過模板B與影像A進行卷積計算,得到B覆蓋區域的畫素點最小值,並用這個最小值來替代參考點的畫素值。如圖所示,將左邊的原始影像A腐蝕處理為右邊的效果圖A-B。
腐蝕:腐蝕會把物體的邊界腐蝕掉,卷積核沿著影像滑動,如果卷積核對應的原圖的所有畫素值為1,那麼中心元素就保持原來的值,否則變為零。主要應用在去除白噪聲,也可以斷開連在一起的物體。
在原圖的每一個區域中取最小值,由於是二值化影像,只要有一個點為0,則為0,來達到瘦身的目的。
腐蝕的作用:
- 1,物件大小減少1個畫素(3*3)
- 2,平滑物件邊緣
- 3,弱化或者分割影像之間的半島型連線
2.2 膨脹(Dilate)
膨脹可以看做是腐蝕的對偶運算,其定義是:把結構元素B平移 a 後得到 Ba,若Ba擊中X,我們記下這個 a 點。所有滿足上述條件的 a點組成的集合稱為 X被B膨脹的結果。
膨脹的方法是:拿B的中心點和X上的點及X周圍的點一個一個的對,如果B上有一個點落在X的範圍內,則該點為黑,可以看出X的範圍就像X膨脹一圈似的。
影像膨脹的運算子是“⊕”,其定義如下:
這個公式表示用B來對影像A進行膨脹處理,其中B是一個卷積模板或卷積核,其形狀可以為正方形或圓形,通過模板B與影像A進行卷及計算,掃描影像中的每一個畫素點,用模板元素與二值影像元素做“與”運算,如果都為0,那麼目標畫素點為0,否則為1。從而計算B覆蓋區域的畫素點最大值,並用該值替換參考點的畫素值實現膨脹。下圖是將左邊的原始影像A膨脹處理為右邊的效果圖 A⊕B。
膨脹:卷積核所對應的原影像的畫素值只要有一個是1,中心畫素值就是1.一般在除噪聲,先腐蝕再膨脹,因為腐蝕在去除白噪聲的時候也會使影像縮小,所以我們之後要進行膨脹。當然也可以用來將兩者物體分開。
膨脹的作用:
- 1,物件大小增加一個畫素 (3*3)
- 2,平滑物件邊緣
- 3,減少或者填充物件之間的距離
2.3 程式碼展示
程式碼如下:
import cv2 import numpy as np def erode_image(img_path): origin_img = cv2.imread(img_path) gray_img = cv2.cvtColor(origin_img, cv2.COLOR_BGR2GRAY) # OpenCV定義的結構元素 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) # 腐蝕影像 eroded = cv2.erode(gray_img, kernel) # 顯示腐蝕後的影像 cv2.imshow('Origin', origin_img) cv2.imshow('Erode', eroded) cv2.waitKey(0) cv2.destroyAllWindows() def dilate_image(img_path): origin_img = cv2.imread(img_path) gray_img = cv2.cvtColor(origin_img, cv2.COLOR_BGR2GRAY) # OpenCV定義的結構元素 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) # 膨脹影像 dilated = cv2.dilate(gray_img, kernel) # 顯示腐蝕後的影像 cv2.imshow('Dilate', dilated) cv2.waitKey(0) cv2.destroyAllWindows() if __name__ == '__main__': img_path = 'origin.jpg' erode_image(img_path) dilate_image(img_path)
如上所示,腐蝕和膨脹的處理很簡單,只需設定好結構元素,然後分別呼叫 cv2.erode() 和 cv2.dilate()函式,其中第一個引數為需要處理的影像,第二個是結構元素,返回處理好的影像。
下圖從左到右依次是 原圖,腐蝕,膨脹(我們可以看出腐蝕是將線條變瘦,膨脹是變胖):
2.4 腐蝕和膨脹的知識點補充
1,可以看做膨脹是將白色區域擴大,腐蝕是將黑色區域擴大。
2,可以不進行灰度處理,對彩色影像進行處理
腐蝕 cv2.erode(src, # 輸入影像 kernel, # 卷積核 dst=None, anchor=None, iterations=None, # 迭代次數,預設1 borderType=None, borderValue=None) 膨脹 cv2.dilate(src, # 輸入影像 kernel, # 卷積核 dst=None, anchor=None, iterations=None, # 迭代次數,預設1 borderType=None, borderValue=None)
3,開運算和閉運算
開運算和閉運算就是將腐蝕和膨脹按照一定的次序進行處理。但是這兩者並不是可逆的,即先開後閉並不能得到原來的影像。
為了獲取影像中的主要物件:對一幅二值圖連續使用閉運算和開運算,或者消除影像中的噪聲,也可以對影像先用開運算後用閉運算,不過這樣也會消除一些破碎的物件。
- 開運算:先腐蝕後膨脹,用於移除由影像噪聲形成的斑點
- 閉運算:先膨脹後腐蝕,用來連線被誤分為許多小塊的物件
3.1 開運算
開運算 = 先腐蝕運算,再膨脹運算(看上去把細微連在一起的兩塊目標分開了)
開運算的效果圖如下圖所示:
開運算總結:
- (1)開運算能夠除去孤立的小點,毛刺和小橋,而總的位置和形狀不變。
- (2)開運算是一個基於幾何運算的濾波器
- (3)結構元素大小的不同將導致濾波效果的不同
- (4)不同的結構元素的選擇導致了不同的分割,即提取出不同的特徵。
3.2 閉運算
閉運算=先膨脹運算,再腐蝕運算(看上去將兩個細微連線的圖封閉在一起)
閉運算的效果如下圖所示:
閉運算總結:
- (1)閉運算能夠填平小湖(即小孔),彌合小裂縫,而總的位置和形狀不變。
- (2)閉運算是通過填充影像的凹角來濾波影像的。
- (3)結構元素大小的不同將導致濾波效果的不同。
- (4)不同結構元素的選擇導致了不同的分割。
程式碼如下:
import cv2 import numpy as np def Open_operation(img_path): origin_img = cv2.imread(img_path) gray_img = cv2.cvtColor(origin_img, cv2.COLOR_BGR2GRAY) # OpenCV定義的結構元素 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) # 開運算 open = cv2.morphologyEx(gray_img, cv2.MORPH_OPEN, kernel) # 顯示腐蝕後的影像 cv2.imshow('Open', open) cv2.waitKey(0) cv2.destroyAllWindows() def Closed_operation(img_path): origin_img = cv2.imread(img_path) gray_img = cv2.cvtColor(origin_img, cv2.COLOR_BGR2GRAY) # OpenCV定義的結構元素 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) # 閉運算 closed = cv2.morphologyEx(gray_img, cv2.MORPH_CLOSE, kernel) # 顯示腐蝕後的影像 cv2.imshow('Closed', closed) cv2.waitKey(0) cv2.destroyAllWindows() def show_origin(origin_path): # img = cv2.imread(origin_path, ) # 灰度化 img = cv2.imread(origin_path, 0) cv2.imshow('origin', img) cv2.waitKey(0) cv2.destroyAllWindows() if __name__ == '__main__': # 此圖為加了高斯噪聲的圖片 img_path = 'butterfly_Gaussian.jpg' show_origin(img_path) Closed_operation(img_path) Open_operation(img_path)
效果如下:(分佈是原圖,開運算的圖,閉運算的圖)
閉運算用來連線被誤分為許多小塊的物件,而開運算用於移除由影像噪聲形成的斑點。因此,某些情況下可以連續運用這兩種運算。如對一幅二值圖連續使用閉運算和開運算,將獲得影像中的主要讀寫。同樣,如果想消除影像中噪聲(即影像中的“小點”),也可以對影像先用開運算後用閉運算,不過這樣也會消除一些破碎的物件。
3.3 開運算和閉運算的知識點補充
這裡主要補充函式原型
開運算 影像開運算主要使用的函式morphologyEx,它是形態學擴充套件的一組函式, 其引數cv2.MORPH_OPEN對應開運算。其原型如下: dst = cv2.morphologyEx(src, cv2.MORPH_OPEN, kernel) 引數dst表示處理的結果,src表示原影像,cv2.MORPH_OPEN表示開運算,kernel表示卷積核 閉運算 影像閉運算主要使用的函式morphologyEx,其原型如下: dst = cv2.morphologyEx(src, cv2.MORPH_CLOSE, kernel) 引數dst表示處理的結果,src表示原影像, cv2.MORPH_CLOSE表示閉運算,kernel表示卷積核
4,禮帽/頂帽,黑帽演算法
禮帽 :原始影像與其進行開運算後的影像進行一個差
黑帽:原始影像與其閉運算後的影像進行一個差
禮帽運算 = 原始影像 - 開運算
黑帽運算 = 閉運算 - 原始影像
程式碼如下:
import cv2 def hat_algorithm(img_path): original_img0 = cv2.imread(img_path) original_img = cv2.imread(img_path, 0) kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) # 定義矩形結構元素 TOPHAT_img = cv2.morphologyEx(original_img, cv2.MORPH_TOPHAT, kernel) # 頂帽運算 BLACKHAT_img = cv2.morphologyEx(original_img, cv2.MORPH_BLACKHAT, kernel) # 黒帽運算 # 顯示影像 cv2.imshow("original_img0", original_img0) cv2.imshow("original_img", original_img) cv2.imshow("TOPHAT_img", TOPHAT_img) cv2.imshow("BLACKHAT_img", BLACKHAT_img) cv2.waitKey(0) cv2.destroyAllWindows() if __name__ == '__main__': img_path = 'butterfly_Gaussian.jpg' hat_algorithm(img_path)
效果如下:(依次是原圖,灰度圖圖片(兩個類似,是因為我將原圖做了灰度化高斯處理,所以灰度化之後和原圖類似),頂帽圖片,黑帽圖片)
該演算法可以用於影像識別的預處理,用於影像二值化後取出孤立點,程式碼如下:
import cv2 def deal_isolated(img_path): original_img = cv2.imread(img_path, 0) gray_img = cv2.resize(original_img, None, fx=0.8, fy=0.8, interpolation=cv2.INTER_CUBIC) # 圖形太大了縮小一點 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) # 定義矩形結構元素(核大小為3效果好) TOPHAT_img = cv2.morphologyEx(gray_img, cv2.MORPH_TOPHAT, kernel) # 頂帽運算 BLACKHAT_img = cv2.morphologyEx(gray_img, cv2.MORPH_BLACKHAT, kernel) # 黒帽運算 bitwiseXor_gray = cv2.bitwise_xor(gray_img, TOPHAT_img) # 顯示如下腐蝕後的影像 cv2.imshow("gray_img", gray_img) cv2.imshow("TOPHAT_img", TOPHAT_img) cv2.imshow("BLACKHAT_img", BLACKHAT_img) cv2.imshow("bitwiseXor_gray", bitwiseXor_gray) cv2.waitKey(0) cv2.destroyAllWindows() if __name__ == '__main__': img_path = 'lena.jpg' deal_isolated(img_path)
可以看出,最後效果更加明顯了一些:
5,梯度運算
梯度 = 膨脹 - 腐蝕
下面看一個示例:
import cv2 import numpy as np import matplotlib.pyplot as plt img = cv2.imread('circle.jpg') kernel = np.ones((7, 7), np.uint8) # kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (7, 7)) erosion = cv2.erode(img, kernel, iterations = 5) dilation = cv2.dilate(img, kernel, iterations = 3) gradient = cv2.morphologyEx(img, cv2.MORPH_GRADIENT, kernel) result = [img, erosion, dilation, gradient] titles = ['origin img', 'erosion img', 'dilate img', 'gradient img'] for i in range(4): plt.subplot(2, 2, i+1), plt.imshow(result[i]) plt.title(titles[i]) plt.xticks([]), plt.yticks([]) plt.show()
效果如下:
6,用形態學運算檢測邊和角點
這裡通過一個較複雜的例子學習如何用形態學運算元檢測影像中的邊緣和拐角(這裡只做形態學處理例子,實際使用請參考Canny和Harris等演算法:請參考博文:深入學習OpenCV中幾種影像邊緣檢測運算元)
6.1 檢測邊緣
形態學檢測邊緣的原理很簡單,在膨脹時,影像中的物體會向周圍“擴張”;腐蝕時,影像的額物體會“收縮”。比較兩幅影像,由於其變化的區域只發生在邊緣。所以這時將這兩幅影像相減,得到的就是影像中的邊緣。這裡用的依然是參考資料《Opencv2 Computer Vision Application Programming Cookbook》中相關章節的圖片:
程式碼如下:
# coding=utf-8 import cv2 import numpy def detection_edge(img_path): image = cv2.imread(img_path, 0) # 構造一個3×3的結構元素 element = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) dilate = cv2.dilate(image, element) erode = cv2.erode(image, element) # 將兩幅影像相減獲得邊,第一個引數是膨脹後的影像,第二個引數是腐蝕後的影像 # cv2.absdiff引數:(膨脹後的影像,腐蝕後的影像) result = cv2.absdiff(dilate, erode) # 上面得到的結果是灰度圖,將其二值化以便更清楚的觀察結果 retval, result = cv2.threshold(result, 40, 255, cv2.THRESH_BINARY) # 反色,即對二值圖每個畫素取反 result = cv2.bitwise_not(result) # 顯示影像 cv2.imshow("result", result) cv2.waitKey(0) cv2.destroyAllWindows() if __name__ == '__main__': img_path = "building.jpg" detection_edge(img_path)
結果如下:
6.2 檢測角點(拐角)
與邊緣檢測不同,拐角的檢測過程稍稍有些複雜。但是原理相同,所不同的是先用十字形的結構元素膨脹畫素,這種情況下只會在邊緣處“擴張”,角點不發生變化。接著用菱形的結構元素腐蝕原影像,導致只有在拐角處才會“收縮”,而直線邊緣都未發生變化。
第二步是用X行膨脹原影像,焦點膨脹的比邊要多。這樣第二次用方塊腐蝕時,角點恢復原狀,而邊要腐蝕的更多。所以當兩幅影像相減時,只保留了拐角處,示意圖如下(示意圖來自參考資料《Opencv2 Computer Vision Application Programming Cookbook》):
程式碼如下:
# coding=utf-8 import cv2 import numpy def detection_inflexion(img_path): image = cv2.imread(img_path, 0) origin = cv2.imread(img_path) # 構造5×5的結構元素,分別為十字形、菱形、方形和X型 cross = cv2.getStructuringElement(cv2.MORPH_CROSS, (5, 5)) # 菱形結構元素的定義稍麻煩一些 diamond = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) diamond[0, 0] = 0 diamond[0, 1] = 0 diamond[1, 0] = 0 diamond[4, 4] = 0 diamond[4, 3] = 0 diamond[3, 4] = 0 diamond[4, 0] = 0 diamond[4, 1] = 0 diamond[3, 0] = 0 diamond[0, 3] = 0 diamond[0, 4] = 0 diamond[1, 4] = 0 square = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) x = cv2.getStructuringElement(cv2.MORPH_CROSS, (5, 5)) # 使用cross膨脹影像 dilate_cross_img = cv2.dilate(image, cross) # 使用菱形腐蝕影像 erode_diamond_img = cv2.erode(dilate_cross_img, diamond) # 使用X膨脹原影像 dilate_x_img = cv2.dilate(image, x) # 使用方形腐蝕影像 erode_square_img = cv2.erode(dilate_x_img, square) # result = result1.copy() # 將兩幅閉運算的影像相減獲得角 result = cv2.absdiff(erode_square_img, erode_diamond_img) # 使用閾值獲得二值圖 retval, result = cv2.threshold(result, 40, 255, cv2.THRESH_BINARY) # 在原圖上用半徑為5的圓圈將點標出。 for j in range(result.size): y = int(j / result.shape[0]) x = int(j % result.shape[0]) if result[x, y] == 255: cv2.circle(image, (y, x), 5, (255, 0, 0)) cv2.imshow("Result", image) cv2.waitKey(0) cv2.destroyAllWindows() if __name__ == '__main__': img_path = "building.jpg" detection_inflexion(img_path)
通過上面的程式碼就能檢測出影像的拐角並標出來,效果如下:
當然這個只是一個示例,效果不是很好。
參考文獻:https://www.cnblogs.com/ssyfj/p/9276999.html
https://blog.csdn.net/wsp_1138886114/article/details/82917661
https://blog.csdn.net/JohinieLi/article/details/81041276
https://blog.csdn.net/hanshanbuleng/article/details/80657148
1、《Opencv2 Computer Vision Application Programming Cookbook》
2、《OpenCV References Manule》
最初的來源: https://blog.csdn.net/sunny2038/article/details/9137759
https://blog.csdn.net/gbxvip/article/details/50844007