ต่อจาก
บทที่ ๑๑
ในบทนี้จะเป็นเรื่องของการปรับรูปของภาพที่มีแค่ขาวดำ ๒ ค่า ได้แก่ การกร่อน พองตัว และแปลงสัณฐานด้วยวิธีการต่างๆ
การกร่อน
การกร่อน หรือพองตัว หรือแปลงสันฐานที่จะกล่าวถึงในบทนี้เป็นเทคนิคที่ใช้ตัวกรอง (filter)
เช่นเดียวกับที่ได้เขียนถึงไปแล้วใน
บทที่ ๘
"การกร่อน" (erode) ในที่นี้คือการนำเอาตัวกรองมาไล่กวาดบนภาพเพื่อลบบางส่วนทิ้งออกไป
เช่นเดียวกับการที่ชายฝั่งหรือโขดหินเมื่อโดนน้ำกัดเซาะก็จะเกิดการกร่อนสูญเสียส่วนขอบไปแล้วยุบตัวลงได้
เทคนิคนี้ใช้กับภาพที่มีแค่ ๒ สีขาวดำ ซึ่งอาจได้มาจากการแบ่งตามความสว่างโดยใช้ cv2.threshold() (ดู
บทที่ ๑๐) เป็นต้น
ใน opencv มีฟังก์ชัน cv2.erode() ใช้สำหรับทำการกร่อนภาพ
ค่าที่ต้องใส่ลงในฟังก์ชันนี้คือ
ลำดับที่ |
ชื่อ |
ค่าที่ต้องใส่ |
ชนิดข้อมูล |
1 |
src |
อาเรย์รูปภาพ |
np.array |
2 |
kernel |
ตัวกรอง |
np.array |
เมื่อใส่รูปภาพตั้งต้นและตัวกรองที่จะใช้ในการกร่อนลงไปในฟังก์ชัน
ก็จะได้ผลลัพธ์ออกมาเป็นภาพที่ผ่านการกร่อนเรียบร้อย
ก่อนที่จะอธิบายรายละเอียดวิธีการคำนวณ ขอเริ่มจากแสดงการใช้งานฟังก์ชันนี้เพื่อให้เห็นภาพ
ขอยกภาพนี้ขึ้นมาเป็นตัวอย่าง
โดยเริ่มจากเอามาแปลงเป็นภาพขาวดำโดยพยายามให้ส่วนตัวคนเป็นสีขาวและฉากหลังที่เหลือเป็นสีดำ
miku12c01.jpg
import cv2
import numpy as np
import matplotlib.pyplot as plt
miku = cv2.imread('miku12c01.jpg',0)
_,miku = cv2.threshold(miku,52,255,cv2.THRESH_BINARY)
cv2.imwrite('miku12c02.png',miku)
miku12c02.png
ต่อมานำภาพนี้มาลองกร่อนดูด้วยฟังก์ชัน cv2.erode() โดยใช้ตัวกรองแบบง่ายๆคือเป็นอาเรย์ขนาด 5×5 ที่มีค่าเป็น 1
ทั้งหมด
kernel = np.ones((5,5),dtype=np.float32)
miku_ero = cv2.erode(miku,kernel)
cv2.imwrite('miku12c03.png',miku_ero)
miku12c03.png
จะได้ภาพที่มีอาณาเขตสีขาวลดลง เปลี่ยนกลายเป็นสีดำ เหมือนกับว่าโดนกัดเซาะให้กร่อนหายไป นี่คือผลของการกร่อน
สิ่งที่ตัวกรองทำก็คือ กวาดลงไปในภาพทีละจุดแล้วคูณเข้ากับจัดนั้นและจุดรอบๆ
เมื่อลากผ่านจุดใดแล้วหากมีช่องใดช่องหนึ่งภายในบริเวณของตัวกรองเป็นสีดำ ผลของช่องนั้นก็จะได้เป็นสีดำ
ดังนั้นส่วนที่อยู่ใกล้ขอบดำจึงกลายเป็นสีดำไป ในที่นี้ตัวกรองขนาด 5×5 จึงทำให้ผิวถูกกร่อนไปประมาณ 2 ช่อง
ฟังก์ชันนี้สามารถใส่คีย์เวิร์ด iterations เข้าไปเพื่อให้ทำการกร่อนแบบเดิมซ้ำๆตามจำนวนครั้งที่กำหนดได้
คราวนี้ลองเอามากร่อนสัก ๕ ครั้ง
cv2.imread('miku12c02.png',0)
miku_ero = cv2.erode(miku,kernel,iterations=5)
cv2.imwrite('miku12c04.png',miku_ero)
miku12c04.png
ยิ่งถูกกร่อนก็ยิ่งร่อยหรอ ในที่สุดก็จะหายไปหมด
การพองตัว
สิ่งที่ตรงกันข้ามกับการกร่อนก็คือการพองตัว (dilate) ซึ่งทำได้โดยใช้ฟังก์ชัน cv2.dilate()
วิธีการใช้ก็เหมือนกับ cv2.erode() สิ่งที่ทำก็เหมือนกันคือจะเป็นการนำตัวกรองไปไล่กวาดดูบนภาพทีละช่อง
ข้อแตกต่างก็คือ เมื่อใช้ cv2.dilate() นั้นหากมีช่องใดช่องหนึ่งเป็นสีขาวก็จะให้ผลเป็นสีขาวทันที
ดังนั้นบริเวณที่อยู่ใกล้สีขาวก็จะเป็นสีขาวไปด้วย ทำให้บริเวณสีขาวขยายตัวขึ้น
ตัวอย่าง ใช้กับภาพเดิมด้วยตัวกรองตัวเดิม แต่คราวนี้เปลี่ยนมาเป็นทำให้พองตัว
miku_dil = cv2.dilate(miku,kernel)
cv2.imwrite('miku12c05.png',miku_dil)
miku12c05.png
จะได้ว่าบริเวณสีขาวขยายตัวมากกว่าเดิม
ตัวกรองชนิดต่างๆ
ในตัวอย่างที่ผ่านมาใช้ตัวกรองเป็นแบบง่ายๆคือสีขาวระบายเต็มช่อง 5×5 (คือเป็นเลข 1 ทั้งหมด)
แต่ถ้าเปลี่ยนตัวกรองเป็นแบบอื่นก็จะได้ผลที่ต่างกันออกไป การเลือกตัวกรองให้เหมาะกับงานจึงมีความสำคัญ
ตัวกรองที่ใช้สำหรับเรื่องนี้จะมีแค่เลข 1 หรือ 0 เท่านั้น โดยเลข 1 ในตัวกรองคือบริเวณที่พิจารณา ส่วนเลข 0
จะมองข้ามไป
นอกคัวกรองสี่เหลี่ยมง่ายๆแล้ว รูปแบบของตัวกรองที่เป็นวงรี ㅇ กับรูปตัว 十 ก็อาจถูกเลือกใช้
ใน opencv ได้เตรียมฟังก์ชันสำหรับสร้างตัวกรองในลักษณะแบบนั้นเอาไว้ให้ คือฟังก์ชัน cv2.getStructuringElement()
ค่าที่ต้องใส่คือ
ลำดับที่ |
ชื่อ |
ค่าที่ต้องใส่ |
ชนิดข้อมูล |
1 |
shape |
ชนิดรูปแบบ |
flag (int) |
2 |
ksize |
ขนาดตัวกรอง |
tuple ของ int |
ในที่นี้ชนิดตัวกรองใส่เป็นแฟล็ก มีอยู่ ๓ แบบ
แฟล็ก |
ค่า |
เป็นรูป |
cv2.MORPH_RECT |
0 |
สี่เหลี่ยม |
cv2.MORPH_CROSS |
1 |
ตัว 十 |
cv2.MORPH_ELLIPSE |
2 |
วงรี |
ลองนำมาใช้ดูเป็นตัวอย่าง
arr = np.zeros([9,9])
plt.figure(figsize=[6,2])
plt.subplot(131,xticks=[],yticks=[])
plt.imshow(cv2.getStructuringElement(cv2.MORPH_RECT,(7,7)),cmap='gray',vmin=0,vmax=1)
plt.subplot(132,xticks=[],yticks=[])
plt.imshow(cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(7,7)),cmap='gray',vmin=0,vmax=1)
plt.subplot(133,xticks=[],yticks=[])
plt.imshow(cv2.getStructuringElement(cv2.MORPH_CROSS,(7,7)),cmap='gray',vmin=0,vmax=1)
plt.tight_layout()
plt.show()
ลองใช้กับภาพเดิม เปรียบเทียบผลการใช้ตัวกรองที่ต่างๆกัน
ตัวกรองรูปวงรี
miku = cv2.imread('miku12c02.png',0)
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(5,5))
miku_ero = cv2.erode(miku,kernel)
cv2.imwrite('miku12c06.png',miku_ero)
miku_dil = cv2.dilate(miku,kernel)
cv2.imwrite('miku12c07.png',miku_dil)
miku12c06.png
miku12c07.png
ตัวกรองรูป 十
miku = cv2.imread('miku12c02.png',0)
kernel = cv2.getStructuringElement(cv2.MORPH_CROSS,(5,5))
miku_ero = cv2.erode(miku,kernel)
cv2.imwrite('miku12c08.png',miku_ero)
miku_dil = cv2.dilate(miku,kernel)
cv2.imwrite('miku12c09.png',miku_dil)
miku12c08.png
miku12c09.png
ผลที่ได้จะต่างจากตัวกรองรูปสี่เหลี่ยมดังที่ใช้มาในตัวอย่างก่อนหน้านี้
การแปลงสัณฐาน
หลักการกร่อนและการพองตัวนี้นำไปสู่เทคนิคที่ใช้ในการแปลงสัณฐาน (morphology) ของรูป
โดยมีรูปแบบการแปลงต่างๆที่มีพื้นบานมาจากการกร่อนและการพอง
ฟังก์ชันที่ใช้ทำการแปลงสัณฐานคือ cv2.morphologyEx()
ค่าที่ต้องใส่ในฟังก์ชัน
ลำดับที่ |
ชื่อ |
ค่าที่ต้องใส่ |
ชนิดข้อมูล |
1 |
src |
อาเรย์รูปภาพ |
np.array |
2 |
op |
รูปแบบการแปลง |
flag (int) |
3 |
kernel |
ตัวกรอง |
np.array |
รูปแบบการแปลงใส่เป็นแฟล็ก มีตัวเลือกดังนี้
แฟล็ก |
ค่า |
ความหมาย |
cv2.MORPH_ERODE |
0 |
กร่อน erode(x) |
cv2.MORPH_DILATE |
1 |
พองตัว dilate(x) |
cv2.MORPH_OPEN |
2 |
กร่อนแล้วพอง dilate(erode(x)) |
cv2.MORPH_CLOSE |
3 |
พองแล้วกร่อน erode(dilate(x)) |
cv2.MORPH_GRADIENT |
4 |
ผลการพองลบผลการกร่อน dilate(x) - erode(x) |
cv2.MORPH_TOPHAT |
5 |
ภาพเดิมลบผลจากการพองแล้วกร่อน x - erode(dilate(x)) |
cv2.MORPH_BLACKHAT |
6 |
ผลจากการกร่อนแล้วพองลบภาพเดิม dilate(erode(x)) - x |
cv2.MORPH_ERODE กับ cv2.MORPH_DILATE นั้นจะเหมือนการใช้ cv2.erode() หรือ cv2.dilate()
ส่วนอีก ๕ ตัวที่เหลือก็เป็นการผสมผสานระหว่างผลการกร่อนและการพองอีกที
ตัวอย่างเปรียบเทียบแบบต่างๆ
miku = cv2.imread('miku12c02.png',0)
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(5,5))
miku_open = cv2.morphologyEx(miku,cv2.MORPH_OPEN,kernel)
cv2.imwrite('miku12c10.png',miku_open)
miku_close = cv2.morphologyEx(miku,cv2.MORPH_CLOSE,kernel)
cv2.imwrite('miku12c11.png',miku_close)
miku_gradient = cv2.morphologyEx(miku,cv2.MORPH_GRADIENT,kernel)
cv2.imwrite('miku12c12.png',miku_gradient)
miku_tophat = cv2.morphologyEx(miku,cv2.MORPH_TOPHAT,kernel)
cv2.imwrite('miku12c13.png',miku_tophat)
miku_blackhat = cv2.morphologyEx(miku,cv2.MORPH_BLACKHAT,kernel)
cv2.imwrite('miku12c14.png',miku_blackhat)
cv2.MORPH_OPEN
miku12c10.png
cv2.MORPH_CLOSE
miku12c11.png
cv2.MORPH_GRADIENT
miku12c12.png
cv2.MORPH_TOPHAT
miku12c13.png
cv2.MORPH_BLACKHAT
miku12c14.png
ที่น่าจะพบว่าใช้บ่อยก็คือ ๒ ตัวแรก
cv2.MORPH_OPEN คือการกร่อนเสร็จแล้วค่อยพองใหม่จะทำให้จุดขาวเล็กๆถูกลบทิ้ง
ในทางตรงกันข้าม cv2.MORPH_CLOSE คือการพองเสร็จแล้วค่อยกร่อน จะทำให้จุดสีดำเล็กๆหายไป
ส่วน cv2.MORPH_GRADIENT นั้นจะเอาผลการกร่อนกับผลการพองมาหักลบกัน ผลที่ได้จะเป็นลักษณะเค้าโครงของภาพเดิม
cv2.morphologyEx() ก็สามารถใส่ iterations เพื่อทำซ้ำหลังครั้ง สำหรับ MORPH_OPEN
จะหมายถึงกร่อนตามจำนวนครั้งเท่านั้นก่อนแล้วจึงพองตัวกลับตามจำนวนครั้งเท่าเดิม ส่วน MORPH_CLOSE ก็ตรงกันข้าม
ตัวอย่าง
miku = cv2.imread('miku12c02.png',0)
kernel = cv2.getStructuringElement(cv2.MORPH_CROSS,(5,5))
cv2.imwrite('miku12c15.png',cv2.morphologyEx(miku,cv2.MORPH_OPEN,kernel,iterations=8))
cv2.imwrite('miku12c16.png',cv2.morphologyEx(miku,cv2.MORPH_CLOSE,kernel,iterations=8))
miku12c15.png
miku12c16.png
การใช้การแปลงสัณฐานเพื่อคัดเอาบางส่วนของภาพ
ใน
บทที่ ๑๐ ตอนท้ายสุดได้แนะนำวิธีการใช้
cv2.threshold() เพื่อคัดเอาเฉพาะส่วนสีขาวแล้วทำส่วนที่เหลือให้โปร่งใสไป
แต่ว่าแค่กรองขาวดำแบบนั้นจะทำให้เหลือจุดขาวๆเล็กๆที่อยู่ในฉากหลังปนมามากมายได้ เช่นลองดูภาพนี้
teto12c01.jpg
teto = cv2.imread('teto12c01.jpg')
teto_gr = cv2.cvtColor(teto,cv2.COLOR_BGR2GRAY)
_,teto_thr = cv2.threshold(teto_gr,60,255,cv2.THRESH_BINARY)
teto = cv2.cvtColor(teto,cv2.COLOR_BGR2BGRA)
teto[:,:,3] = teto_thr
cv2.imwrite('teto12c02.png',teto)
teto12c02.png
หากเปลี่ยนเป็นเอาภาพมากร่อนก่อนจะได้แบบนี้
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(5,5))
teto[:,:,3] = cv2.erode(teto_thr,kernel)
cv2.imwrite('teto12c03.png',teto)
teto12c03.png
แต่ถ้าแค่กร่อนภาพก็จะแหว่งหายไป จึงใช้วิธีการกร่อนแล้วพองกลับคืน (MORPH_OPEN) ผลที่ได้ก็จะออกมาสวยงามแบบนี้
teto[:,:,3] = cv2.morphologyEx(teto_thr,cv2.MORPH_OPEN,kernel)
cv2.imwrite('teto12c04.png',teto)
teto12c04.png
หรือถ้าอยากลองทำเป็นเค้าโครงดูสวยๆก็อาจใช้ MORPH_GRADIENT ก็จะได้ภาพแบบนี้ออกมา
teto[:,:,3] = cv2.morphologyEx(teto_thr,cv2.MORPH_GRADIENT,kernel)
cv2.imwrite('teto12c05.png',teto)
teto12c05.png
อ่านบทถัดไป >>
บทที่ ๑๓