使用Python實現數字圖像處理中如下功能:
- 彩色圖像轉成灰度圖像
- 實現圖像的相關&卷積操作
- 實現圖像的高斯核卷積
使用的庫和python版本如下:
- imageio:2.9.0 用於讀取磁盤中的圖片文件
- numpy:1.20.3 用於矩陣等操作
- matplotlib:3.4.2 用於畫圖
- python:3.8.11
項目地址:xiaohuiduan/image_process (github.com)
讀取圖像
在進行圖像處理操作前,首先需要對圖像進行讀取。這里使用imageio庫對圖片進行讀取,並將其轉成numpy數組。
下面定義一個covert_img_to_array
函數,用於讀取圖片。
def covert_img_to_array(self, path:str) -> np.array:
"""[將圖片轉成Array便於處理]
Args:
path (str): [圖片保存位置]
Returns:
np.array: [返回numpy數組,數組元素uint8]
"""
return np.array(imageio.imread(path))
展示圖片
使用matplotlib庫用於展示圖片,為了更高的展示如片,定義下show_img
函數,當不指定col或者row時盡量以方正的形式去展示圖片。
def show_img(self,title:str, imgs:list, cmaps:list,row:int = 0,col:int = 0):
"""展示圖片 len(imgs) must equal to the len of cmaps
Args:
title (str): [圖像標題]
imgs (list): [圖片元組]
cmaps (list): [mask,plt以何種形式展示圖片,可參考官方文檔使用:'gray'表示灰度圖,None表示彩色圖]
row (int, optional): [指令row]. Defaults to 0.
col (int, optional): [指令col]. Defaults to 0.
"""
if len(imgs) != len(cmaps):
print("圖片和mask的len必須相同")
else:
if row == 0 and col !=0:
row = np.ceil(len(imgs)/col).astype("uint8")
elif row!=0 and col == 0:
col = np.ceil(len(imgs)/row).astype("uint8")
elif row*col < len(imgs):
# 盡量以方正的形式去展示圖片
row = np.ceil(np.sqrt(len(imgs))).astype("uint8")
col = np.ceil(len(imgs)/row).astype("uint8")
for index,img in enumerate(imgs):
plt.subplot(row,col,index+1)
plt.imshow(img,cmap=cmaps[index])
plt.suptitle(title)
plt.show()
彩色圖像轉成灰度圖像
彩色圖像一般來說RGB表示的。也就是說,如果有一張64*64大小的圖片,那么它在numpy中便是以64*64*3的shape進行保存的。將RGB圖片轉成灰度圖有兩種方式:
- \(gray=\frac{R+G+B}{3}\)
- \(gray=R*0.2989 + G*0.5870 + B*0.1140\) 這種灰度轉換稱之為NTSC標准,考慮了人類的彩色感知體驗。
下面定義covert_rgb_to_gray
函數,其中method如果為average,則使用第一種方式灰度轉換方式;默認為NTSC,使用第二種方式轉換。
def covert_rgb_to_gray(self, image:np.array, method:str = 'NTSC') -> np.array:
"""將RGB圖像轉成gray圖像
Args:
image (np.array): [rgb圖像]
method (str, optional): [轉換模式]. Defaults to 'NTSC'.
Returns:
Array: [返回的灰度圖像]
"""
if method == 'average':
gray_img = image[:,:,0]/3+image[:,:,1]/3+image[:,:,2]/3
else:
gray_img = image[:,:,0]*0.2989 + image[:,:,1]*0.5870 + image[:,:,2]*0.1140
return gray_img
圖像卷積
圖像卷積的公式如下所示,\(g\)代表輸入的像素矩陣,\(w\)代表的是權重系數矩陣也就是所謂的卷積核kernel。
這里有一個很需要值得注意的點,那就是相關操作。相關操作和卷積很類似,相關操作的公式如下:
在網絡有一些博客文章,在解釋卷積的時候,使用的是第一個公式,但是在做計算或者實現代碼的時候卻用的是第二個公式,這樣做是不對的。因為卷積的kernel與相關的kernel相差了\(180^{\circ}\)。
但是值得注意的是,在卷積神經網絡中,實際上使用的數學公式是相關相關運算,如下圖所示。因為在CNN中,kernel的參數是學習過來的,kernel是否翻轉並不會影響結果。
理解卷積
前置知識:
卷積定理指出,函數卷積的傅里葉變換是函數傅里葉變換的乘積。至於推導,可以查一下資料。
\[\mathcal{F}\{f * g\}=\mathcal{F}\{f\} \cdot \mathcal{F}\{g\} \]
提一下圖像卷積的含義。如果一個如下的均值濾波器對圖像進行卷積,從人類的直覺進行出發,可以去除噪聲和平滑圖像。(在圖像中,一般圖像噪聲的頻率比較大,圖像邊緣部分的頻率也比較大。 因此使用均值濾波器可以去除噪聲和平滑圖像。)
那么為什么會造成這種現象呢?如何從數學的角度來解釋均值濾波器的作用呢?
如下所示,圖左邊是一個一維均值濾波器的函數圖像,圖右邊是均值函數在頻域上面的圖像。在右邊圖像上,可以發現一個很明顯的特點:頻率越高,\(F(\mu)\)越小。
那么如果將\(F(\mu)\)與某另外一個頻域上面的函數(比如圖像)相乘,顯而易見,如果圖像的頻率越高,則\(F(\mu)\)與之相乘被拖下水的的程度就越大。也就是說,相乘之后,頻率低的就被抬上去了,頻率高的被拉下去了。
說的細一點,其實從上圖可以看到,隨着頻率的增大,\(F(\mu)\)並不是嚴格的下降,中間有一個波浪的起伏,這樣會在邊緣造成一些不好的現象。但是高斯濾波不會有這種情況。后面會介紹高斯濾波。
均值濾波器的二維頻域圖如下所示:
矩陣點積
下面定義矩陣點積函數。
def __matrix_dot_product(self,matrix,kernel):
"""矩陣點乘 [1,2,3]*[4,5,6] = 1*4 + 2*5 + 3*6 = 32
Args:
matrix ([type]): [部分圖像]
kernel ([type]): [kernel]
Returns:
[type]: [點乘結果]
"""
if len(matrix) != len(kernel):
print("點積失敗,大小不一致")
else:
# 速度快
return (np.multiply(matrix,kernel)).sum()
# result = 0
# for i, row_nums in enumerate(matrix):
# for j,num in enumerate(row_nums):
# result += num * kernel[i][j]
# return result
圖像padding
如果不對圖像進行padding的話,會造成一個現象,圖像越卷越小。在卷積的時候,我們希望卷積后的圖像大小與原圖像保持一致(CNN網絡可能會越卷越小),因此需要對圖像進行padding。padding有兩種方式,一種在填充0,一種是填充與其距離最近的元素。下圖中圖像周圍虛線部分就是padding的元素。
下面是實現padding操作的具體函數。實際上,可以直接使用np.pad操作實現。(但是我的作業要求不能使用pad操作,只能自己實現)
def __padding(self, padding_type:str, image:np.array, padding_w:int, padding_h:int):
"""對圖片進行padding
Args:
padding_type (str): [padding方式]
image (np.array): [圖片]
padding_w (int): [寬度pdding]
padding_h (int): [高度padding,一般來說padding_w = padding_h]
Returns:
[type]: [返回padding之后的結果]
"""
image_w = image.shape[0]
image_h = image.shape[1]
padding_image = np.zeros((image_w+padding_w*2,image_h+padding_h*2))
padding_image[padding_w:padding_w+image_w,padding_h:padding_h+image_h] = image
if padding_type == 'zero':
return padding_image
if padding_type == "replicate":
# 補充四個角
padding_image[0:padding_w+1,0:padding_h+1] = image[0,0]
padding_image[image_w+padding_w-1:,0:padding_h+1] = image[image_w-1,0]
padding_image[0:padding_w+1,image_h+padding_h-1:] = image[0,image_h-1]
padding_image[image_w+padding_w-1:,image_h+padding_h-1:] = image[image_w-1,image_h-1]
# 補充旁邊的元素
for i in range(padding_w+1,image_w+padding_w-1):
padding_image[i,0:padding_h] = image[i-padding_w,0]
padding_image[i,image_h+padding_h:] = image[i-padding_w,image_h-1]
for i in range(padding_h+1,image_h+padding_h-1):
padding_image[0:padding_w,i] = image[0,i-padding_h]
padding_image[image_w+padding_w:,i] = image[image_w-1,i-padding_h]
return padding_image
如果想使得卷積之后的結果與原圖像一致,padding_w,padding_h
為卷積核大小的一半(向下取整,卷積核大小一般是奇數)。比如核的大小是\(5 \times 5\),那么padding的長寬便是\(2\)。
圖像相關操作
前面說過圖像的卷積實際上就是將kernel進行翻轉\(180^{\circ}\),然后進行相關運算,因此可以先定義相關操作函數:
def corr2D(self, image:np.array, kernel:np.array, padding:str = 'zero') -> np.array:
"""對圖片進行相關運算。
Args:
image (np.array): [(*,*)shape的圖片]
kernel (np.array): [kernel,kernel為奇數]
padding (str, optional): [zero以零填充,replicate以鄰近的填充]. Defaults to 'zero'.
Returns:
[type]: [description]
"""
kernel_size_w = kernel.shape[0]
kernel_size_h = kernel.shape[1]
image_w,image_h = image.shape
padding_w = kernel_size_w // 2
padding_h = kernel_size_h // 2
# 將圖片padding起來
padding_image = self.__padding(padding,image,padding_w,padding_h)
new_image = np.zeros((image_w,image_h))
for i in range(image_w):
for j in range(image_h):
new_image[i][j] = self.__matrix_dot_product(padding_image[i:i+kernel_size_w,j:j+kernel_size_h],kernel)
return new_image.clip(0,255).astype("uint8")
卷積操作
旋轉kernel
旋轉kernel的代碼很簡單,如下所示,通過以下操作可以將行和列翻轉(相當於反轉了\(180^{\circ}\))。
def flip_180(self, arr: np.array) -> np.array:
return arr[::-1,::-1]
卷積
將kernel繼續寧翻轉,然后進行相關運算便是卷積了。
def conv2D(self, image:np.array, kernel:np.array, padding:str = 'zero') -> np.array:
"""二維卷積
Args:
image (np.array): [(*,*)shape的圖片]
kernel (np.array): [kernel,kernel為奇數]
padding (str, optional): [zero以零填充,replicate以鄰近的填充]. Defaults to 'zero'.
Returns:
[type]: [卷積好的結果]
"""
return self.corr2D(image,self.flip_180(kernel),padding)
高斯核
二維高斯核的公式如下所示:
二維高斯核的頻域圖如下所示。
下面是二維高斯濾波函數的定義,其中\(\sigma_x=\sigma_y=sig\)。並對卷積核進行歸一化,使得所有元素加起來和為1。
def gauss_2d_kernel(self,sig,m=0):
"""產生高斯核
Args:
sig ([type]): [高斯核參數 sigx = sigy]
m (int, optional): [高斯kernel的大小]. Defaults to 0. if m=0,then m = ceil(3*sig)*2 +1
Returns:
[type]: [m*m大小的高斯核]
"""
fit_m = math.ceil(3 * sig)*2+1
if m == 0:
m = fit_m
if m < fit_m:
print("你的核的size應該大一點")
# 中心點
center = m //2
kernel = np.zeros(shape=(m,m))
for i in range(m):
for j in range(m):
kernel[i][j] = (1/(2*math.pi*sig**2))*math.e**(-((i-center)**2+(j-center)**2)/(2*sig**2))
# 歸一化
return kernel/(kernel.sum())
結果
灰度轉換結果
高斯核卷積
參考
- 數字圖像處理(第三版)