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



opencv-python เบื้องต้น บทที่ ๑๒: การกร่อน พองตัว และแปลงสัณฐาน
เขียนเมื่อ 2020/06/28 19:03

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

ในบทนี้จะเป็นเรื่องของการปรับรูปของภาพที่มีแค่ขาวดำ ๒ ค่า ได้แก่ การกร่อน พองตัว และแปลงสัณฐานด้วยวิธีการต่างๆ




การกร่อน

การกร่อน หรือพองตัว หรือแปลงสันฐานที่จะกล่าวถึงในบทนี้เป็นเทคนิคที่ใช้ตัวกรอง (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




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



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

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

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

หมวดหมู่

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

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

สารบัญ

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

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

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



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

  ค้นหาบทความ

  บทความแนะนำ

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

ไทย

日本語

中文