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



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

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

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




การกร่อน

การกร่อน หรือพองตัว หรือแปลงสันฐานที่จะกล่าวถึงในบทนี้เป็นเทคนิคที่ใช้ตัวกรอง (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
-- manim
-- opencv
-- pyqt
-- pytorch
การเรียนรู้ของเครื่อง
-- โครงข่าย
     ประสาทเทียม
maya
javascript
ความน่าจะเป็น
บันทึกในญี่ปุ่น
บันทึกในจีน
-- บันทึกในปักกิ่ง
-- บันทึกในฮ่องกง
-- บันทึกในมาเก๊า
บันทึกในไต้หวัน
บันทึกในยุโรปเหนือ
บันทึกในประเทศอื่นๆ
เรียนภาษาจีน
qiita
บทความอื่นๆ

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



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

  ค้นหาบทความ

  บทความแนะนำ

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

บทความแต่ละเดือน

2022年

1月 2月 3月 4月
5月 6月 7月 8月
9月 10月 11月 12月

2021年

1月 2月 3月 4月
5月 6月 7月 8月
9月 10月 11月 12月

2020年

1月 2月 3月 4月
5月 6月 7月 8月
9月 10月 11月 12月

2019年

1月 2月 3月 4月
5月 6月 7月 8月
9月 10月 11月 12月

2018年

1月 2月 3月 4月
5月 6月 7月 8月
9月 10月 11月 12月

ค้นบทความเก่ากว่านั้น

ไทย

日本語

中文