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