φυβλαςのβλογ
บล็อกของ phyblas



opencv-python เบื้องต้น บทที่ ๘: ตัวกรองคอนโวลูชันและการทำภาพเบลอ
เขียนเมื่อ 2020/06/28 18:54
แก้ไขล่าสุด 2024/02/22 10:25

ต่อจาก บทที่ ๗

ในบทนี้เป็นเรื่องของการใช้ตัวกรองเพื่อทำคอนโวลูชัน ซึ่งอาจใช้ในการทำเบลอ หรือเน้นขอบ หรือลดจุดปนเปื้อนหรือคลื่นรบกวนได้

เรื่องของคอนโวลูชันนั้นมีรายละเอียดมาก ถูกใช้อย่างกว้างขวางในด้านต่างๆ รวมถึงใช้ในโครงข่ายประสาทเทียม ในบทนี้จะแค่อธิบายคร่าวๆ โดยเน้นไปที่การใช้จัดการรูปภาพ

สำหรับคำอธิบายละเอียดเกี่ยวกับคอนโวลูชัน ขอแนะนำบทความนี้ https://phyblas.hinaboshi.com/20180609




คอนโวลูชัน

อีกเทคนิคหนึ่งที่นิยมนำมาใช้ในการตัดแต่งภาพก็คือการใช้ตัวกรอง (filter) มาทำคอนโวลูชัน (convolution)

คำว่า "คอนโวลูชัน" มีคำแปลไทยว่า "สังวัตนาการ" ด้วย แต่ไม่นิยมใช้ แต่ถ้าจะให้แปลแบบให้เห็นภาพก็คือเป็น "การม้วนทบ"

หลักการทำงานของคอนโวลูชันก็คือเอาสิ่งที่เรียกว่า "ตัวกรอง" มาวิ่งบนภาพแล้วคำนวณค่าที่ได้จากผลบวกของผลคูณระหว่างค่าบนตัวกรองนั้นกับค่าบนภาพ

ตัวกรองนี้บางทีก็เรียกว่า เคอร์เนล (kernel) ในบทความนี้ก็จะใช้ทั้ง ๒ คำ เพราะในชื่อฟังก์ชันต่างๆก็มีการใช้ทั้ง kernel และ filter ๒ คำนี้ปนกันไป ขอให้เข้าใจว่าในเรื่องของคอนโวลูชันแล้วเคอร์เนลก็คือตัวกรอง อาจมองว่ามีความหมายเดียวกันได้

เพื่ออธิบายหลักการของของคอนโวลูชันให้เห็นภาพ อาจลองยกตัวอย่างภาพนี้



ในภาพนี้คือสมมุติว่ามีตัวกรองขนาด 3×3 (สีม่วง) มาวิ่งลงบนภาพขนาด 4×5 (สีฟ้า) จะเกิดการคูณระหว่างค่าบนตัวกรองกับค่าบนภาพแล้วเอาค่าทั้งหมดมาบวกกัน แล้วเอาผลที่ได้มา จากนั้นก็เลื่อนตัวกรองแล้วคำนวณเหมือนเดิม ผลที่ได้เอามาเรียงกันตามตำแหน่งที่ตัวกรองวิ่งไป ในที่นี้แสดงเป็นภาพสีแดงด้านล่าง

แต่ถ้าหากคูณไปเรื่อยๆโดยไม่ได้มีการเติมขอบแล้ว แบบนั้นผลที่ได้จะมีขนาดเล็กลง ดังนั้นโดยทั่วไปแล้วเวลาทำคอนโวลูชันกับรูปภาพมักจะมีการเติมขอบเข้าไปเพื่อรักษาขนาดภาพให้คงเดิม ใน opencv เองก็เช่นกัน

ขนาดขอบที่ต้องเติมมีค่าเป็นขนาดตัวกรองลบด้วย 1 เช่นในตัวอย่างนี้ตัวกรองเป็น 3×3 จึงต้องมีการเติมขอบ 2 ช่องทั้งแนวตั้งและแนวนอน จึงเติมขอบทั้งบนล่างซ้ายขวาอีก 1 ช่อง

เช่นเดียวกันถ้าหากตัวกรองมีขนาด 5×5 ก็จะต้องเติมขอบบนล่างซ้ายขวาอย่างละ 2

วิธีการเติมขอบมีอยู่หลายวิธี โดยพื้นฐานที่สุดก็คือถมด้วยค่า 0 หรือค่าคงตัวอะไรก็ได้ไปดังในตัวอย่างข้างต้น

แต่ว่าวิธีนี้มีปัญหาอยู่ตรงที่ว่าจะทำให้ค่าที่ขอบน้อยลงไปเมื่อเทียบกับส่วนอื่น จึงอาจไม่เหมาะนัก

ใน opencv เวลาทำคอนโวลูชันสามารถเลือกวิธีการเติมขอบได้ แต่ถ้าไม่ได้กำหนดอะไรไป วิธีมาตรฐานที่ใช้ใน opencv คือการเติมขอบโดยสะท้อนกระจกเอาเช่นเดียวกับเมื่อใช้ cv2.BORDER_REFLECT_101 ในฟังก์ชัน cv2.copyMakeBorder() (ดูในบทที่ ๖)

ดังนั้นขอบจะถูกเติมแบบนี้



กรณีที่ตัวกรองขนาด 5×5 ใช้กับภาพขนาด 4×4 จะคำนวณแบบนี้



ด้วยวิธีนี้จะทำให้ค่าที่ขอบไม่น้อยไปกว่าปกติ ดังนั้นในการคอนโวลูชันกับรูปภาพจึงมักจะมีการเติมขอบด้วยวิธีนี้ก่อนที่จะเอาตัวกรองมาดำเนินการ




การใช้ฟังก์ชัน filter2D เพื่อทำคอนโวลูชันภาพ

cv2.filter2D() เป็นฟังก์ชันที่ใช้ทำคอนโวลูชันในอาเรย์รูปภาพ

ค่าที่ต้องใส่มีดังนี้

ลำดับ ชื่อ สิ่งที่ต้องใส่ ชนิดข้อมูล
1 img อาเรย์รูปภาพ np.array
2 ddepth ชนิดตัวแปรในอาเรย์ flag (int)
2 kernel อาเรย์ตัวกรอง np.array

ตัวที่ ๒ ddepth ในที่นี้เป็นตัวกำหนดชนิดตัวแปรในอาเรย์ของผลลัพธ์ที่จะได้ออกมาจากตัวกรอง โดยทั่วไปจะใส่เป็น -1 ซึ่งหมายถึงให้ออกมาเป็นชนิดเดียวกับอาเรย์รูปภาพที่ป้อนเข้าไป ยกเว้นบางกรณีที่ต้องการเปลี่ยนชนิดข้อมูลไปในระหว่างคำนวณด้วย ในที่นี้จะขอใส่เป็น -1 ตลอด

สำหรับการกำหนดรูปแบบการเติมขอบทำโดยใส่คีย์เวิร์ด borderType ซึ่งค่าที่ใส่ก็จะเหมือนกับที่ใช้ในฟังก์ชัน cv2.copyMakeBorder() (อ้างอิงบทที่ ๖)

ขอยกตัวอย่างการใช้โดยสร้างอาเรย์ภาพและตัวกรองแบบสุ่มขึ้นมา แล้วเทียบการใช้ borderType แบบต่างๆ

สำหรับภาพขาวดำ
import cv2
import numpy as np
import matplotlib.pyplot as plt

# สร้างตัวอาเรย์รูปภาพ
arr = (np.random.permutation(64)).astype(np.float32).reshape(8,8)
arr *= 255/arr.max()
# สร้างตัวกรอง
kernel = np.random.randint(0,10,[5,5]).astype(np.float32)
kernel /= kernel.sum()

# ภาพเดิม
plt.figure(figsize=[6,9])
plt.subplot(321)
plt.title('ภาพเดิม',family='Tahoma')
plt.imshow(arr,cmap='gray')

# วาดตัวกรอง
plt.subplot(322,title='kernel')
plt.imshow(kernel,cmap='gray')

# ชนิดของขอบ ๔ แบบ
border = [cv2.BORDER_REPLICATE,cv2.BORDER_REFLECT,cv2.BORDER_REFLECT_101,cv2.BORDER_CONSTANT]
title = ['replicate','reflect','reflect101','constant']

# วาดผลการคอนโวลูชันโดยกำหนดขอบแบบต่างๆ
for i in range(4):
    plt.subplot(323+i,title=title[i])
    arrconvo = cv2.filter2D(arr,-1,kernel,borderType=border[i])
    plt.imshow(arrconvo,cmap='gray')

plt.tight_layout(0)
plt.show()


กรณีที่ตั้งขอบเป็นแบบ cv2.BORDER_CONSTANT จะทำให้มีการเติมขอบด้วย 0 จึงทำให้ส่วนขอบดูมืดลงกว่าแบบอื่น

กรณีที่เป็นภาพสีก็สามารถใช้ได้เช่นกัน แต่ตัวกรองก็ยังคงเป็นอาเรย์ ๒ มิติ ซึ่งไม่มีสี
arr = (np.random.permutation(64*3)).astype(np.float32).reshape(8,8,3)
arr *= 255/arr.max()
kernel = np.random.randint(0,10,[5,5]).astype(np.float32)
kernel /= kernel.sum()

plt.figure(figsize=[6,9])
plt.subplot(321)
plt.title('ภาพเดิม',family='Tahoma')
plt.imshow(arr[:,:,::-1].astype(np.uint8))

plt.subplot(322,title='kernel')
plt.imshow(kernel,cmap='gray')

border = [cv2.BORDER_REPLICATE,cv2.BORDER_REFLECT,cv2.BORDER_REFLECT_101,cv2.BORDER_CONSTANT]
title = ['replicate','reflect','reflect101','constant']

for i in range(4):
    plt.subplot(323+i,title=title[i])
    arrconvo = cv2.filter2D(arr,-1,kernel,borderType=border[i])
    plt.imshow(arrconvo[:,:,::-1].astype(np.uint8))

plt.tight_layout(0)
plt.show()


ลองนำมาใช้กับรูปภาพที่เตรียมไว้ โดยลองใช้กับตัวกรองขนาด 5×5 ซึ่งมีค่าเท่ากันหมดเป็น 1/25


teto08c01.jpg

teto = cv2.imread('teto08c01.jpg')
kernel = np.ones([5,5],dtype=np.float32)/25
kernel /= kernel.sum()
teto = cv2.filter2D(teto,-1,kernel)
cv2.imwrite('teto08c02.jpg',teto)

teto08c02.jpg


ผลที่ได้จะออกมาเป็นภาพเบลอๆ เนื่องจากแต่ละจุดได้ค่าจากบริเวณรอบๆมาเฉลี่ย และนี่ก็คือหลักการทำภาพเบลอ




ตัวกรองเฉลี่ยเพื่อทำภาพเบลอ

จากตัวอย่างที่แล้วจะเห็นว่าเมื่อใช้ cv2.filter2D() ทำคอนโวลูชันโดยใส่ค่าในตัวกรองทุกช่องเป็นค่าเท่ากันซึ่งรวมกันเป็น 1 แล้วจะสามารถทำภาพเบลอได้

ในกรณีนี้แทนที่จะใช้ cv2.filter2D() โดยตรง อาจเลือกใช้ฟังก์ชัน cv2.blur() แทน โดยเพียงแค่แค่กำหนดขนาดของตัวกรองลงไปก็จะถูกสร้างตัวกรองที่มีค่าเท่ากันเป็น 1 หารด้วยจำนวนช่อง ให้โดยอัตโนมัติ

ค่าที่ต้องใส่

ลำดับ ชื่อ สิ่งที่ต้องใส่ ชนิดข้อมูล
1 src อาเรย์รูปภาพ np.array
2 ksize ขนาดตัวกรอง (กว้าง,สูง) tuple ของ int

ลองทำเบลอแบบเดียวกับตัวอย่างแต่ใช้ cv2.blur()


miku08c01.jpg
miku = cv2.imread('miku08c01.jpg')
miku = cv2.blur(miku,(5,5))
cv2.imwrite('miku08c02.jpg',miku)

miku08c02.jpg

ยิ่งใช้ขนาดตัวกรองใหญ่มากก็จะยิ่งเบลอมาก




ตัวกรองเกาส์

นอกจากใช้ตัวกรองที่มีค่าเท่ากันเป็นค่าเฉลี่ยหมดแล้ว อีกวิธีที่มักใช้ในการทำภาพเบลอก็คือใช้ตัวกรองเป็นฟังก์ชันเกาส์

ฟังก์ชันเกาส์จะมีค่าเป็นขนาดใหญ่ที่สุดตรงกลาง และเล็กลงเรื่อยๆเมื่อห่างจากใจกลางไป

การคำนวณในฟังก์ชันเกาส์เป็นแบบนี้



โดย xc, yc เป็นตำแหน่งใจกลาง ส่วนตัวแปร σ (ซิกมา) เป็นตัวกำหนดสัดส่วนของฟังก์ชันเกาส์ คือค่าที่กำหนดว่าค่าจะลดลงเร็วแค่ไหนเมื่อออกห่างจากใจกลางไป ยิ่งค่า σ น้อยก็จะยิ่งลดลงอย่างรวดเร็ว

เมื่อใช้ค่า σ ต่างกันในการสร้างตัวกรองเกาส์ผลที่ได้ก็จะต่างออกไปด้วย หาก σ มีค่าสูงมากผลที่ได้ก็จะใกล้เคียงกับการใช้ cv2.blur() ที่แค่เฉลี่ยให้ค่าทุกตัวบนตัวกรองเท่ากันหมด

ค่าบนตัวกรองเกาส์ขนาด 5×5 เทียบระหว่าง σ ต่างๆกัน



ใน opencv สามาถใช้ตัวกรองเกาส์ได้โดยใช้ฟังก์ชัน cv2.GaussianBlur() โดยไม่จำเป็นต้องเตรียมอาเรย์ตัวกรองมาใช้ cv2.filter2D() เองโดยตรง

ลำดับ ชื่อ สิ่งที่ต้องใส่ ชนิดข้อมูล
1 src อาเรย์รูปภาพ np.array
2 ksize ขนาดตัวกรอง (กว้าง,สูง) tuple ของ int
2 sigmaX ค่าซิกมา float

เมื่อใช้ฟังก์ชันนี้ ตัวกรองจะถูกสร้างขึ้นตามค่าที่กำหนด แล้วนำไปคอนโวลูชันกับภาพ

ตัวอย่าง ลองใช้กับภาพเดิมที่ใช้ในตัวอย่างที่แล้ว เทียบผลที่ได้ดู
miku = cv2.imread('miku08c01.jpg')
miku = cv2.GaussianBlur(miku,(5,5),1)
cv2.imwrite('miku08c03.jpg',miku)

miku08c03.jpg

จะเห็นว่าเมื่อใช้ใช้ขนาดตัวกรองเท่ากับ cv2.blur() แล้ว cv2.GaussianBlur() จะเบลอน้อยกว่าเพราะยังเน้นค่าตรงใจกลางซึ่งเป็นตำแหน่งเดิมมากกว่าที่จะไปเน้นบริเวณรอบๆ




ตัวกรองทวิภาคี

เมื่อใช้ตัวกรองเกาส์ในการทำเบลอ จะพบว่าที่บริเวณขอบแบ่ง (ตำแหน่งที่สีมีการตัดเปลี่ยนไปค่าอย่างกะทันหัน) จะเบลอมากกว่า เพราะเกิดการผสมของสีบริเวณที่ต่างกันมาก

แต่ในบางกรณีเราอาจต้องการทำเบลอในขณะที่แยกเอาส่วนขอบออกจากกัน กรณีแบบนี้อาจใช้ตัวกรองอีกชนิดหนึ่งที่เรียกว่า ตัวกรองทวิภาคี (bilateral filter) ซึ่งเป็นวิธีที่ถูกคิดค้นมาในปี 1998

หลักการคำนวณของตัวกรองชนิดนี้โดยพื้นฐานแล้วก็คือใช้ฟังก์ชันเกาส์เหมือนตัวกรองเกาส์ แต่จะพิจารณาเฉพาะบริเวณที่สีใกล้เคียงกันเท่านั้น ใช้เวลาคำนวณค่อนข้างนานกว่าหากเทียบกับตัวกรองเกาส์ธรรมดา

ฟังก์ชันสำหรับทำการกรองแบบทวิภาคีก็คือ cv2.bilateralFilter()

ตัวอย่างการใช้
miku = cv2.imread('miku08c01.jpg')
miku = cv2.bilateralFilter(miku,5,50,55)
cv2.imwrite('miku08c04.jpg',miku)

miku08c04.jpg

หากเทียบกับการใช้ตัวกรองเกาส์แล้ว ผลที่ได้จะเห็นว่าส่วนที่อยู่ใกล้ขอบแบ่งจะยังคงรักษาความคมอยู่ ไม่ได้เบลอมาก ในขณะที่ส่วนที่อยู่ไกลจากขอบจะเบลอพอๆกัน




ตัวกรองมัธยฐาน

การทำเบลอนั้นยังอาจนำมาใช้เป็นตัวกรองเพื่อกำจัดส่วนปนเปื้อนในภาพ ทำให้ภาพดูเรียบขึ้น

ในการทำเบลอเพื่อกำจัดส่วนปนเปื้อนนั้นมักจะใช้ตัวกรองมัธยฐาน (median filter) โดยเป็นตัวกรองที่ทำการหาค่ามัธยฐานจากจุดทั้งหมดภายในบริเวณตัวกรอง

* มัธยธาน (median) คือค่าของตัวที่มีค่าอยู่ในลำดับกึ่งกลางเมื่อนำข้อมูลทั้งหมดที่พิจารณามาเรียงต่อกัน

วิธีนี้จะใช้ในการกำจัดการเบลอได้ดีกว่าใช้ตัวกรองเกาส์หรือใช้ค่าเฉลี่ย

ฟังก์ชันสำหรับทำภาพเบลอด้วยมัธยฐานคือ cv2.medianBlur()

ค่าที่ต้องใส่ในฟังก์ชัน

ลำดับ ชื่อ สิ่งที่ต้องใส่ ชนิดข้อมูล
1 src อาเรย์รูปภาพ np.array
2 ksize ขนาดตัวกรอง (กว้าง,สูง) tuple ของ int

ขอยกตัวอย่างโดยเอาภาพที่มีอยู่มาใส่จุดปนเปื้อนลงไป


gumi08c01.jpg
gumi = cv2.imread('gumi08c01.jpg')
noi = np.random.randint(0,10,gumi.shape)==0
gumi[noi] = np.random.random(noi.sum())*255
cv2.imwrite('gumi08c02.jpg',gumi)

gumi08c02.jpg

เอาภาพที่มีจุดปนเปื้อนที่ได้นั้นมาลองใช้ตัวกรองมัธยฐานเพื่อลดความเบลอดู ลองเทียบระหว่างตัวกรองขนาดต่างกัน
gumk = cv2.imread('gumi08c02.jpg')
cv2.imwrite('gumi08c03.jpg',cv2.medianBlur(gumi,3))
cv2.imwrite('gumi08c04.jpg',cv2.medianBlur(gumi,5))

gumi08c03.jpg


gumi08c04.jpg

ภาพบนตัวกรองขนาด 3 ส่วนภาพล่างขนาด 5 จะเห็นว่าใช้ตัวกรองขนาดใหญ่อาจทำให้ลบจุดปนเปื้อนได้มากกว่า แต่ก็ทำให้ภาพเบลอขึ้นมากไปด้วย




ตัวกรองปรับภาพให้คม

สิ่งที่ตรงกันข้ามกับการทำให้ภาพเบลอก็คือทำให้ภาพคมขึ้น ใน opencv ไม่ได้มีฟังก์ชันที่เตรียมตัวกรองสำหรับทำคมไว้โดยเฉพาะ แต่ก็สามารถทำเองขึ้นมาได้ แล้วใช้ cv2.filter2D() ทำคอนโวลูชันเอา

ตัวกรองทำภาพคมอาจมีหน้าตาแบบนี้



ลองสร้างแล้วใช้ดู


rin08c01.jpg
kernel = np.array([[0,-1,0],
                   [-1,5,-1],
                   [0,-1,0]],dtype=np.float32)
rin = cv2.imread('rin08c01.jpg')
cv2.imwrite('rin08c02.jpg',cv2.filter2D(rin,-1,kernel))

rin08c02.jpg

ผลลัพธ์ได้ภาพที่ดูคมขึ้นมา




อ่านบทถัดไป >> บทที่ ๙



-----------------------------------------

囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧

ดูสถิติของหน้านี้

หมวดหมู่

-- คอมพิวเตอร์ >> เขียนโปรแกรม >> opencv
-- คอมพิวเตอร์ >> เขียนโปรแกรม >> python >> numpy
-- คอมพิวเตอร์ >> เขียนโปรแกรม >> python >> matplotlib

ไม่อนุญาตให้นำเนื้อหาของบทความไปลงที่อื่นโดยไม่ได้ขออนุญาตโดยเด็ดขาด หากต้องการนำบางส่วนไปลงสามารถทำได้โดยต้องไม่ใช่การก๊อปแปะแต่ให้เปลี่ยนคำพูดเป็นของตัวเอง หรือไม่ก็เขียนในลักษณะการยกข้อความอ้างอิง และไม่ว่ากรณีไหนก็ตาม ต้องให้เครดิตพร้อมใส่ลิงก์ของทุกบทความที่มีการใช้เนื้อหาเสมอ

สารบัญ

รวมคำแปลวลีเด็ดจากญี่ปุ่น
มอดูลต่างๆ
-- numpy
-- matplotlib

-- pandas
-- manim
-- opencv
-- pyqt
-- pytorch
การเรียนรู้ของเครื่อง
-- โครงข่าย
     ประสาทเทียม
ภาษา javascript
ภาษา mongol
ภาษาศาสตร์
maya
ความน่าจะเป็น
บันทึกในญี่ปุ่น
บันทึกในจีน
-- บันทึกในปักกิ่ง
-- บันทึกในฮ่องกง
-- บันทึกในมาเก๊า
บันทึกในไต้หวัน
บันทึกในยุโรปเหนือ
บันทึกในประเทศอื่นๆ
qiita
บทความอื่นๆ

บทความแบ่งตามหมวด



ติดตามอัปเดตของบล็อกได้ที่แฟนเพจ

  ค้นหาบทความ

  บทความแนะนำ

ตัวอักษรกรีกและเปรียบเทียบการใช้งานในภาษากรีกโบราณและกรีกสมัยใหม่
ที่มาของอักษรไทยและความเกี่ยวพันกับอักษรอื่นๆในตระกูลอักษรพราหมี
การสร้างแบบจำลองสามมิติเป็นไฟล์ .obj วิธีการอย่างง่ายที่ไม่ว่าใครก็ลองทำได้ทันที
รวมรายชื่อนักร้องเพลงกวางตุ้ง
ภาษาจีนแบ่งเป็นสำเนียงอะไรบ้าง มีความแตกต่างกันมากแค่ไหน
ทำความเข้าใจระบอบประชาธิปไตยจากประวัติศาสตร์ความเป็นมา
เรียนรู้วิธีการใช้ regular expression (regex)
การใช้ unix shell เบื้องต้น ใน linux และ mac
g ในภาษาญี่ปุ่นออกเสียง "ก" หรือ "ง" กันแน่
ทำความรู้จักกับปัญญาประดิษฐ์และการเรียนรู้ของเครื่อง
ค้นพบระบบดาวเคราะห์ ๘ ดวง เบื้องหลังความสำเร็จคือปัญญาประดิษฐ์ (AI)
หอดูดาวโบราณปักกิ่ง ตอนที่ ๑: แท่นสังเกตการณ์และสวนดอกไม้
พิพิธภัณฑ์สถาปัตยกรรมโบราณปักกิ่ง
เที่ยวเมืองตานตง ล่องเรือในน่านน้ำเกาหลีเหนือ
ตระเวนเที่ยวตามรอยฉากของอนิเมะในญี่ปุ่น
เที่ยวชมหอดูดาวที่ฐานสังเกตการณ์ซิงหลง
ทำไมจึงไม่ควรเขียนวรรณยุกต์เวลาทับศัพท์ภาษาต่างประเทศ

ไทย

日本語

中文