Python OpenCV 365 天學(xué)習(xí)計(jì)劃逛犹,與橡皮擦一起進(jìn)入圖像領(lǐng)域吧端辱。
@[toc](Python OpenCV)
基礎(chǔ)知識鋪墊
圖像梯度是計(jì)算圖像變化速度的方法,對于圖像邊緣部分虽画,灰度值如果變化幅度較大舞蔽,則其對應(yīng)梯度值也較大,反之码撰,圖像中比較平滑的部分渗柿,灰度值變化較小,相應(yīng)的梯度值變化也小脖岛。
有以上內(nèi)容就可以學(xué)習(xí)圖像梯度相關(guān)計(jì)算了朵栖,該知識后面會用到獲取圖像邊緣信息相關(guān)技術(shù)中。
OpenCV 提供三種不同的梯度濾波器柴梆,或者說高通濾波器:<kbd>Sobel</kbd>陨溅,<kbd>Scharr</kbd> 和 <kbd>Laplacian</kbd>。
在數(shù)學(xué)上绍在,<kbd>Sobel</kbd>门扇,<kbd>Scharr</kbd> 是求一階或二階導(dǎo)數(shù),<kbd>Scharr</kbd> 是對 <kbd>Sobel</kbd>(使用小的卷積核求解求解梯度角度時)的優(yōu)化偿渡,<kbd>Laplacian</kbd>是求二階導(dǎo)數(shù)臼寄。
數(shù)學(xué)部分我們依舊不展開,在第一遍通識學(xué)習(xí)之后卸察,從上帝視角去補(bǔ)充脯厨。
Sobel 算子和 Scharr 算子
Sobel 算子說明與使用
<kbd>Sobel</kbd>算子是高斯平滑與微分操作的結(jié)合體铅祸,所以它的抗噪聲能力很好(具體橡皮擦沒有學(xué)到精髓坑质,先用起來)。
在使用過程中可以設(shè)定求導(dǎo)的方向(xorder
或 yorder
)临梗,還可以設(shè)定使用的卷積核的大形卸蟆(ksize
)。
函數(shù)原型如下:
dst = cv2.Sobel(src, ddepth, dx, dy[, dst[, ksize[, scale[, delta[, borderType]]]]])
參數(shù)說明如下:
- <kbd>src</kbd>:輸入圖像盟庞;
- <kbd>ddepth</kbd>:圖像顏色深度吃沪,針對不同的輸入圖像,輸出圖像有不同的深度什猖,具體參加后文票彪,
-1
表示圖像深度一致红淡; - <kbd>dx</kbd>:x 方向求導(dǎo)階數(shù);
- <kbd>dy</kbd>:y 方向求導(dǎo)階數(shù)降铸;
- <kbd>ksize</kbd>:內(nèi)核大小在旱,一般取奇數(shù),1,3,5,7等值推掸;
- <kbd>scale</kbd>:縮放大小桶蝎,默認(rèn)值為 1;
- <kbd>delta</kbd>:增量數(shù)值谅畅,默認(rèn)值為 0登渣;
- <kbd>borderType</kbd>:邊界類型,默認(rèn)值為 <kbd>BORDER_DEFAULT</kbd>毡泻。
備注:如果 ksize=-1
胜茧,會使用 3x3
的 <kbd>Scharr</kbd> 濾波器,該效果要比 3x3
的 <kbd>Sobel</kbd> 濾波器好(而且速度相同仇味,所以在使用 3x3
濾波器時應(yīng)該盡量使用 <kbd>Scharr</kbd> 濾波器)竹揍。
關(guān)于該函數(shù)的測試代碼如下(注意選擇自己的圖片,并放置到同一文件夾):
import cv2
import numpy as np
img = cv2.imread('./test.jpg')
dst = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=3)
dst = cv2.convertScaleAbs(dst)
cv2.imshow("dst", dst)
cv2.waitKey()
cv2.destroyAllWindows()
以上代碼中有很多要說明和學(xué)習(xí)的邪铲,接下來我們逐步分析芬位。
<kbd>Sobel</kbd> 濾波器中的第二個參數(shù),設(shè)置為 <kbd>cv2.CV_64F</kbd>带到,在互聯(lián)網(wǎng)找了一下通用解釋
我們一般處理的都是 8 位圖像昧碉,當(dāng)計(jì)算的梯度小于零,會自動變成 0揽惹,造成邊界信息丟失被饿,關(guān)于為什么小于零,是由于從黑到白的邊界導(dǎo)數(shù)是整數(shù)搪搏,而從白到黑的邊界點(diǎn)導(dǎo)數(shù)是負(fù)數(shù)狭握。當(dāng)圖像深度是 np.uint8
的時候,負(fù)值就會變成 0疯溺,基于這個原因论颅,需要把輸出圖像的數(shù)據(jù)類型設(shè)置高一些,例如 <kbd>cv2.CV_16S</kbd>囱嫩,<kbd>cv2.CV_64F</kbd>等值恃疯。
當(dāng)然簡單記憶的方式,就是設(shè)置為 -1
即可墨闲。
處理完畢之后今妄,在通過 <kbd>cv2.convertScaleAbs</kbd>函數(shù)將其轉(zhuǎn)回原來的 uint8
格式,如果不轉(zhuǎn)換,獲取的圖像不正確盾鳞。
對比一下犬性,轉(zhuǎn)換與沒有轉(zhuǎn)換兩次運(yùn)行的效果,右側(cè)明顯失去邊界值腾仅。
注意上文代碼中仔夺,還有一個細(xì)節(jié)需要說明一下
dst = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=3)
- <kbd>dx</kbd>:x 方向求導(dǎo)階數(shù);
- <kbd>dy</kbd>:y 方向求導(dǎo)階數(shù)攒砖。
這其中 dx=1,dy=0
表示計(jì)算水平方向的缸兔,不計(jì)算垂直方向,說白了就是哪個方向等于 1吹艇,計(jì)算哪個方向惰蜜。
還可以對函數(shù)中的 <kbd>ddepth</kbd>參數(shù)進(jìn)行不同取值的比對,查看差異化內(nèi)容受神。下述代碼用到了圖像融合函數(shù) <kbd>cv.addWeighted</kbd>
import numpy as np
import cv2 as cv
def sobel_demo1(image):
grad_x = cv.Sobel(image, cv.CV_16S, 1, 0)
grad_y = cv.Sobel(image, cv.CV_16S, 0, 1)
gradx = cv.convertScaleAbs(grad_x)
grady = cv.convertScaleAbs(grad_y)
cv.imshow('sobel_demo1_gradient_x', gradx)
cv.imshow('sobel_demo1_gradient_y', grady)
# 合并x, y兩個梯度
add_image = cv.addWeighted(gradx, 0.5, grady, 0.5, 0)
cv.imshow('sobel_demo1_addWeighted', add_image)
def sobel_demo2(image):
grad_x = cv.Sobel(image, cv.CV_32F, 1, 0)
grad_y = cv.Sobel(image, cv.CV_32F, 0, 1)
gradx = cv.convertScaleAbs(grad_x)
grady = cv.convertScaleAbs(grad_y)
cv.imshow('sobel_demo2_gradient_x', gradx)
cv.imshow('sobel_demo2_gradient_y', grady)
# 合并x, y兩個梯度
add_image = cv.addWeighted(gradx, 0.5, grady, 0.5, 0)
cv.imshow('sobel_demo2_addWeighted', add_image)
src = cv.imread('./test.jpg', 1)
sobel_demo1(src)
sobel_demo2(src)
cv.waitKey(0)
cv.destroyAllWindows()
你可以不使用圖像融合函數(shù)抛猖,直接通過 <kbd>Sobel</kbd> 函數(shù)計(jì)算 x
方向和 y
方向的導(dǎo)數(shù),代碼如下:
def sobel_demo3(image):
grad = cv.Sobel(image, cv.CV_16S, 1, 1)
grad = cv.convertScaleAbs(grad)
cv.imshow('sobel_demo1_gradient', grad)
得到的結(jié)果比圖像融合效果要差很多鼻听。
這樣對比查閱不是很容易看清楚财著,可以結(jié)合下述代碼進(jìn)行改造,將圖片整合到一起展示撑碴。
def sobel_demo1(image):
grad_x = cv.Sobel(image, cv.CV_16S, 1, 0)
grad_y = cv.Sobel(image, cv.CV_16S, 0, 1)
gradx = cv.convertScaleAbs(grad_x)
grady = cv.convertScaleAbs(grad_y)
# 合并x, y兩個梯度
dst = cv.addWeighted(gradx, 0.5, grady, 0.5, 0)
imgs1 = np.hstack([cv.cvtColor(image, cv.COLOR_BGR2RGB), gradx])
imgs2 = np.hstack([grady, dst])
img_all = np.vstack([imgs1, imgs2])
plt.figure(figsize=(20, 10))
plt.imshow(img_all, cmap=plt.cm.gray)
plt.show()
上述代碼運(yùn)行效果如下撑教,非常容易比對效果。
<kbd>Sobel</kbd>算子算法的優(yōu)點(diǎn)是計(jì)算簡單醉拓,速度快伟姐。
但由于只采用了 2 個方向的模板,只能檢測水平和垂直方向的邊緣亿卤,因此這種算法對于紋理較為復(fù)雜的圖像愤兵,其邊緣檢測效果就不是很理想,該算法認(rèn)為:凡灰度新值大于或等于閾值的像素點(diǎn)時都是邊緣點(diǎn)排吴。這種判斷不是很合理秆乳,會造成邊緣點(diǎn)的誤判,因?yàn)樵S多噪聲點(diǎn)的灰度值也很大钻哩。
Scharr 算子說明與使用
在 <kbd>Sobel</kbd>算子算法函數(shù)中屹堰,如果設(shè)置 <kbd>ksize=-1</kbd> 就會使用 3x3
的 <kbd>Scharr</kbd>濾波器。
它的原理和<kbd>sobel</kbd>算子原理一樣憋槐,只是卷積核不一樣双藕,所以精度會更高一點(diǎn)。
該函數(shù)的原型如下:
# Sobel 算子算法
dst = cv2.Sobel(src, ddepth, dx, dy[, dst[, ksize[, scale[, delta[, borderType]]]]])
# Scharr 算子算法
dst = cv2.Scharr(src, ddepth, dx, dy[, dst[, scale[, delta[, borderType]]]])
它的參數(shù)與 <kbd>Sobel</kbd> 基本一致阳仔。
測試代碼如下,可以比較兩種算法的差異:
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
def scharr_demo(image):
grad_x = cv.Scharr(image, cv.CV_16S, 1, 0)
grad_y = cv.Scharr(image, cv.CV_16S, 0, 1)
gradx = cv.convertScaleAbs(grad_x)
grady = cv.convertScaleAbs(grad_y)
# 合并x, y兩個梯度
dst = cv.addWeighted(gradx, 0.5, grady, 0.5, 0)
return dst
def sobel_demo1(image):
dst1 = scharr_demo(image)
grad_x = cv.Sobel(image, cv.CV_16S, 1, 0)
grad_y = cv.Sobel(image, cv.CV_16S, 0, 1)
gradx = cv.convertScaleAbs(grad_x)
grady = cv.convertScaleAbs(grad_y)
# 合并x, y兩個梯度
dst = cv.addWeighted(gradx, 0.5, grady, 0.5, 0)
imgs = np.hstack([dst, dst1])
plt.figure(figsize=(20, 10))
plt.imshow(imgs, cmap=plt.cm.gray)
plt.show()
def sobel_demo3(image):
grad = cv.Sobel(image, cv.CV_16S, 1, 1)
grad = cv.convertScaleAbs(grad)
cv.imshow('sobel_demo1_gradient', grad)
def sobel_demo2(image):
grad_x = cv.Sobel(image, cv.CV_32F, 1, 0)
grad_y = cv.Sobel(image, cv.CV_32F, 0, 1)
gradx = cv.convertScaleAbs(grad_x)
grady = cv.convertScaleAbs(grad_y)
cv.imshow('sobel_demo2_gradient_x', gradx)
cv.imshow('sobel_demo2_gradient_y', grady)
# 合并x, y兩個梯度
add_image = cv.addWeighted(gradx, 0.5, grady, 0.5, 0)
cv.imshow('sobel_demo2_addWeighted', add_image)
src = cv.imread('./test.jpg', 1)
sobel_demo1(src)
# sobel_demo2(src)
# sobel_demo3(src)
cv.waitKey(0)
cv.destroyAllWindows()
<kbd>sobel</kbd>算子和<kbd>scharr</kbd>算子差異
- <kbd>sobel</kbd>算子系數(shù):[1 2 1] ; <kbd>scharr</kbd>算子[3 10 3] ;
- <kbd>scharr</kbd>算子要比<kbd>sobel</kbd>算子擁有更高的精確度;
- <kbd>scharr</kbd>算子可以把比較細(xì)小的邊界也檢測出來近范。
laplacian 算子
<kbd>Laplace</kbd>函數(shù)實(shí)現(xiàn)的方法:先用<kbd>Sobel</kbd> 算子計(jì)算二階 x 和 y 導(dǎo)數(shù)嘶摊,再求和。
應(yīng)用層面评矩,我們先看與一下該函數(shù)的原型:
dst = cv2.Laplacian(src, ddepth[, dst[, ksize[, scale[, delta[, borderType]]]]])
無特殊參數(shù)叶堆,可以直接進(jìn)入測試代碼的學(xué)習(xí)。
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
image = cv.imread('./test1.png',cv.IMREAD_GRAYSCALE)
laplacian = cv.Laplacian(image, cv.CV_64F)
laplacian = cv.convertScaleAbs(laplacian)
imgs = np.hstack([image, laplacian])
plt.figure(figsize=(20, 10))
plt.imshow(imgs, cmap=plt.cm.gray)
plt.show()
為了測試方便斥杜,我將圖片提前轉(zhuǎn)換為了灰度圖虱颗。
拉普拉斯卷積核,也可以自行進(jìn)行定義蔗喂。默認(rèn)使用的是四鄰域 [[0, 1, 0], [1, -4, 1], [0, 1, 0]]
忘渔,修改為八鄰域 [[1, 1, 1], [1, -8, 1], [1, 1, 1]]
。
自定義卷積核代碼如下缰儿,這里用到了我們之前學(xué)習(xí)的 2D 卷積操作:
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
image = cv.imread('./test1.png',cv.IMREAD_GRAYSCALE)
# 定義卷積和
kernel = np.array([[1, 1, 1], [1, -8, 1], [1, 1, 1]])
dst = cv.filter2D(image, cv.CV_32F, kernel=kernel)
lapalian = cv.convertScaleAbs(dst)
imgs = np.hstack([image, lapalian])
plt.figure(figsize=(20, 10))
plt.imshow(imgs, cmap=plt.cm.gray)
plt.show()
這里還學(xué)習(xí)到一句話畦粮,拉普拉斯對噪聲敏感,會產(chǎn)生雙邊效果乖阵,但是不能檢測出邊的方向宣赔。并且它不直接用于邊的檢測,只起輔助的角色瞪浸,檢測一個像素是在邊的亮的一邊還是暗的一邊利用零跨越儒将,確定邊的位置
梯度簡單來說就是求導(dǎo),就是這關(guān)鍵的求導(dǎo)对蒲,要去學(xué)習(xí)數(shù)學(xué)知識了椅棺,本部分在第一遍學(xué)習(xí)完畢,將會展開學(xué)習(xí)齐蔽。梯度在圖像上表現(xiàn)出來的就是提取圖像的邊緣(無論是橫向的两疚、縱向的、斜方向的等等)含滴,所需要的是一個核模板诱渤,模板的不同結(jié)果也不同√缚觯基于此勺美,上文涉及的算子函數(shù),都能夠用函數(shù)<kbd>cv2.filter2D()</kbd>來表示碑韵,不同的方法給予不同的核模板赡茸,然后演化為不同的算子。
橡皮擦的小節(jié)
希望今天的 1 個小時(今天內(nèi)容有點(diǎn)多祝闻,不一定可以看完)占卧,你有所收獲,我們下篇博客見~
相關(guān)閱讀
技術(shù)專欄
逗趣程序員
- 曝光,程序員的 10 個摸魚神器
- 5 個無聊 Python 程序叭喜,用 Python 整蠱你的朋友們吧
- 10 年老程序員教你甩鍋必殺技贺拣,論【如何優(yōu)雅的甩鍋】
- 你想被開除嗎?來看看程序員【離職小技巧】吧
今天是持續(xù)寫作的第 <font color="red">72</font> / 100 天捂蕴。
如果你有想要交流的想法譬涡、技術(shù),歡迎在評論區(qū)留言啥辨。