ต่อจาก
บทที่ ๗
ในบทนี้เป็นเรื่องของการใช้ตัวกรองเพื่อทำคอนโวลูชัน ซึ่งอาจใช้ในการทำเบลอ หรือเน้นขอบ
หรือลดจุดปนเปื้อนหรือคลื่นรบกวนได้
เรื่องของคอนโวลูชันนั้นมีรายละเอียดมาก ถูกใช้อย่างกว้างขวางในด้านต่างๆ รวมถึงใช้ในโครงข่ายประสาทเทียม
ในบทนี้จะแค่อธิบายคร่าวๆ โดยเน้นไปที่การใช้จัดการรูปภาพ
สำหรับคำอธิบายละเอียดเกี่ยวกับคอนโวลูชัน ขอแนะนำบทความนี้
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
ยิ่งใช้ขนาดตัวกรองใหญ่มากก็จะยิ่งเบลอมาก
ตัวกรองเกาส์
นอกจากใช้ตัวกรองที่มีค่าเท่ากันเป็นค่าเฉลี่ยหมดแล้ว
อีกวิธีที่มักใช้ในการทำภาพเบลอก็คือใช้ตัวกรองเป็นฟังก์ชันเกาส์
ฟังก์ชันเกาส์จะมีค่าเป็นขนาดใหญ่ที่สุดตรงกลาง และเล็กลงเรื่อยๆเมื่อห่างจากใจกลางไป
การคำนวณในฟังก์ชันเกาส์เป็นแบบนี้
โดย x
c, y
c เป็นตำแหน่งใจกลาง ส่วนตัวแปร σ (ซิกมา) เป็นตัวกำหนดสัดส่วนของฟังก์ชันเกาส์
คือค่าที่กำหนดว่าค่าจะลดลงเร็วแค่ไหนเมื่อออกห่างจากใจกลางไป ยิ่งค่า σ น้อยก็จะยิ่งลดลงอย่างรวดเร็ว
เมื่อใช้ค่า σ ต่างกันในการสร้างตัวกรองเกาส์ผลที่ได้ก็จะต่างออกไปด้วย หาก σ มีค่าสูงมากผลที่ได้ก็จะใกล้เคียงกับการใช้
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
ผลลัพธ์ได้ภาพที่ดูคมขึ้นมา
อ่านบทถัดไป >>
บทที่ ๙