ต่อจาก
บทที่ ๑๒
บทนี้ว่าด้วยเรื่องของการหาเส้นเค้าโครงซึ่งกั้นล้อมแบ่งอาณาเขตภายในภาพ
เส้นเค้าโครงคืออะไร
เส้นเค้าโครง (contour) คือเส้นที่กั้นระหว่าง ๒ บริเวณที่มีสีแตกต่างกันในภาพ
ยกตัวอย่างเช่นมีภาพง่ายๆที่แบ่งเป็นสีขาวดำแบบนี้อยู่
a13c01.png
เส้นเค้าโครงก็คือเส้นที่แบ่งเขตเชื่อมระหว่างสีขาวกับดำในภาพนี้ เส้นนี้มีความสำคัญ
ถ้าหาเส้นเค้าโครงได้ก็สามารถเอาไปใช้ทำอะไรได้มากมาย ดังที่จะเขียนถึงในหัวข้อต่อๆไป
ใน opencv มีฟังก์ชัน cv2.findContours() ไว้ใช้ค้นหาเส้นเค้าโครงทั้งหมดที่มีอยู่ในภาพให้โดยอัตโนมัติ
ค่าที่ต้องใส่ในฟังก์ชันนี้มีอยู่ ๓ ตัว คือ
ลำดับที่ |
ชื่อ |
ค่าที่ต้องใส่ |
ชนิดข้อมูล |
1 |
image |
อาเรย์รูปภาพ |
np.array |
2 |
mode |
วิธีการกำหนดชั้นของเส้นเค้าโครง |
flag (int) |
3 |
method |
วิธีการประมาณเส้นเค้าโครง |
flag (int) |
อาเรย์ภาพในที่นี้ต้องเป็นภาพขาวดำ ถ้าเป็นภาพสีก็ให้แปลงเป็นขาวดำก่อน
ค่าในอาเรย์อาจเป็นค่าอะไรก็ได้ตั้งแต่ 0 ถึง 255 แต่ cv2.findContours() จะพิจารณาแค่ว่าเป็น 0 (สีดำ) หรือไม่
ดังนั้นจุดแบ่งที่จะถูกตรวจจับก็คือบริเวณที่เปลี่ยนจากสีดำเป็นสีอื่น
ในส่วนของวิธีการกำหนดชั้นของเส้นเค้าโครง ใส่เป็นแฟล็กดังนี้
แฟล็ก |
ค่า |
ความหมาย |
cv2.RETR_EXTERNAL |
0 |
ค้นเอาเฉพาะวงนอกสุด |
cv2.RETR_LIST |
2 |
เอาทุกวงโดยไม่มีการสร้างเป็นลำดับชั้น |
cv2.RETR_CCOMP |
3 |
เอาทุกวงโดยมีการสร้างเป็นลำดับชั้นแต่ไม่ลึกลงไปกว่าสองชั้น |
cv2.RETR_TREE |
4 |
เอาทุกวงโดยพิจารณาลำดับชั้นให้ตัวข้างในเป็นเค้าโครงลูกของตัวนอกไล่ไปเรื่อยๆ |
๓ ตัวหลังต่างก็จะหาเส้นเค้าโครงทั้งหมดเท่าที่จะหาได้ แต่จะแตกต่างตรงลำดับชั้น
ซึ่งเกี่ยวกับเรื่องนี้จะเขียนถึงอีกทีภายหลัง
สำหรับกรณีทั่วไปที่ต้องการหาเส้นเค้าโครงทั้งหมดที่มีโดยไม่สนลำดับชั้นก็เลือกใช้ cv2.RETR_LIST
ส่วนตรงวิธีการประมาณเส้นเค้าโครงนั้นก็ใส่เป็นแฟล็ก ถ้าต้องการให้หาเค้าโครงแบบละเอียดเป็นจุดต่อจุดให้ใส่
cv2.CHAIN_APPROX_NONE แต่ในกรณีทั่วไปจะใช้ cv2.CHAIN_APPROX_SIMPLE ซึ่งหมายถึงประมาณเอาเท่าที่จำเป็น
ฟังก์ชันนี้จะคืนค่ามาให้ ๒ ตัว ตัวแรกคือข้อมูลของเส้นเค้าโครงที่หามาได้
ส่วนตัวหลังคือข้อมูลเรื่องลำดับชั้นของแต่ละเส้นเค้าโครง
ยกตัวอย่างการใช้โดยสร้างภาพแบบง่ายๆที่แค่มีสี่เหลี่ยมอยู่ตรงกลางภาพ
import cv2
import numpy as np
import matplotlib.pyplot as plt
rup = np.zeros([8,8],dtype=np.uint8) # สร้างภาพสีดำล้วน
rup[2:-2,2:-2] = 170 # ใส่สีเทาตรงกลาง
# เปลี่ยนเป็น RGB เพื่อแสดงผลใน matplotlib
plt.imshow(cv2.cvtColor(rup,cv2.COLOR_GRAY2RGB),cmap='gray')
plt.show()
แล้วลองใช้ฟังก์ชัน cv2.findContours() เพื่อหาเค้าโครงดู
contour,hierarchy = cv2.findContours(rup,cv2.RETR_LIST,cv2.CHAIN_APPROX_SIMPLE)
print(contour)
print(len(contour)) # จำนวนเส้นเค้าโครง
print(contour[0].shape) # อาเรย์แสดงข้อมูลของเส้นเค้าโครงตัวแรก
ผลลัพธ์ที่ได้ จะได้ข้อมูลของเส้นเค้าโครงมาดังนี้
[array([[[2, 2]],
[[2, 5]],
[[5, 5]],
[[5, 2]]], dtype=int32)]
1
(4, 1, 2)
ซึ่งจะได้เป็นลิสต์ของอาเรย์สามมิติซึ่งแสดงตำแหน่งจุดของเส้นเค้าโครง
ในที่นี้มีเส้นเค้าโครงเดียวเลยได้มาอาเรย์เดียว
มิติแรกของอาเรย์จะมีค่าเท่ากับจำนวนจุด ในที่นี้เป็นสี่เหลี่ยมจึงมี ๔ จุด ส่วนมิติที่ ๒ จะเป็น ๑ เสมอ และมิติที่ ๓
จะเป็น ๒ ค่าซึ่งแสดงถึง
ส่วนค่าคืนกลับตัวหลังคือ
ลำดับชั้น (hierarchy) ของเส้นเค้าโครง ตอนนี้จะขอละไว้ก่อนเพราะมีรายละเอียดมาก
จะเขียนถึงในหัวข้อถัดไป
ลองใส่สีที่ตำแหน่งแต่ละจุดของเส้นเค้าโครงเพื่อแสดงให้เห็นดูชัดว่าจุดมองของเค้าโครงคือตรงไหน
rup = cv2.cvtColor(rup,cv2.COLOR_GRAY2RGB)
for i in range(4):
x,y = contour[0][i,0]
rup[y,x] = (0,255,0)
plt.imshow(rup)
plt.show()
จะเห็นว่าจุดอยู่ที่ทั้ง ๔ มุมของของสี่เหลี่ยม
คราวนี้ลองหาเส้นเค้าโครงของภาพในตัวอย่างด้านบนสุดนั่น วาดจุดลงบนเส้น
rup = cv2.imread('a13c01.png',0)
contour,_ = cv2.findContours(rup,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
print(len(contour[0])) # ได้ 362
plt.imshow(cv2.cvtColor(rup,cv2.COLOR_GRAY2RGB))
x = contour[0][:,0,0]
y = contour[0][:,0,1]
plt.scatter(x,y,s=10,c='m')
plt.show()
เนื่องจากเต็มไปด้วยส่วนโค้งมาก จะได้เค้าโครงที่มีจุดอยู่มากมาย ในที่นี้มี ๓๖๒ จุด โดยล้อมอยู่รอบๆเค้าโครง
เว้นแค่ตรงที่เป็นเส้นตรงยาวๆ
ว่าด้วยเรื่องการประมาณเส้นเค้าโครงโดยเว้นส่วนเส้นตรง
ดังที่ได้กล่าวถึงไปในตัวอย่างที่แล้วว่าวิธีการประมาณเส้นเค้าโครงมี cv2.CHAIN_APPROX_NONE กับ
cv2.CHAIN_APPROX_SIMPLE
ในที่นี้จะแสดงตัวอย่างเพื่อให้เห็นความแตกต่างชัดเจน โดยลองสร้างอาเรย์ภาพสี่เหลี่ยมง่ายๆแบบตัวอย่างที่แล้วขึ้นแล้วใช้
cv2.findContours() โดยใช้ทั้ง ๒ วิธีนี้เทียบกัน
rup = np.zeros([20,20],dtype=np.uint8)
rup[4:-6,4:-6] = 120
contour,hierarchy = cv2.findContours(rup,cv2.RETR_LIST,cv2.CHAIN_APPROX_SIMPLE)
print(contour[0].shape) # ได้ (4, 1, 2)
contour,hierarchy = cv2.findContours(rup,cv2.RETR_LIST,cv2.CHAIN_APPROX_NONE)
print(contour[0].shape) # ได้ (36, 1, 2)
plt.imshow(cv2.cvtColor(rup,cv2.COLOR_GRAY2RGB),cmap='gray')
plt.show()
ผลที่ได้จะเห็นว่า cv2.CHAIN_APPROX_SIMPLE จะมีแค่ ๔ จุด ก็คือที่มุมของสี่เหลี่ยม ในขณะที่ cv2.CHAIN_APPROX_NONE
จะได้จุดทั้งหมดที่อยู่รอบเส้นเค้าโครง จึงเป็น ๓๖ จุด
ในกรณีส่วนใหญ่มักไม่ได้จำเป็นต้องใช้จุดทั้งหมด เพราะถ้าเจอเส้นตรงยาวก็สามารถละได้ ดังนั้นจึงเลือก
cv2.CHAIN_APPROX_SIMPLE ก็พอ
การวาดเส้นเค้าโครงลงบนภาพ
ใน opencv มีฟังก์ชัน cv2.drawContours() ซึ่งใช้สำหรับนำเอาข้อมูลเส้นเค้าโครงที่ได้มาจาก cv2.findContours()
มาวาดเป็นเส้นใส่ลงบนภาพ
ค่าที่ต้องใส่ในฟังก์ชันนี้
ลำดับที่ |
ชื่อ |
ค่าที่ต้องใส่ |
ชนิดข้อมูล |
1 |
image |
อาเรย์รูปภาพ |
np.array |
2 |
contours |
ลิสต์ของข้อมูลเส้นเค้าโครง |
list ของ np.array |
3 |
contourIdx |
ลำดับของเส้นเค้าโครงที่จะใช้ |
int |
4 |
color |
สี |
tuple ของ int |
5 |
thickness |
ความหนาของเส้นที่จะวาด (สามารถละได้ ถ้าละจะหนาเป็น 1 พิกเซล) |
int |
ขอใช้ภาพนี้เป็นตัวอย่าง
a13c06.png
นำภาพมาหาเส้นเค้าโครงแล้วนำมาวาดเส้นเค้าโครงทุกเส้นด้วยสีต่างกัน
rup = cv2.imread('a13c06.png',0) # อ่านภาพเป็นขาวดำ
# หาเส้นเค้าโครง
contour,_ = cv2.findContours(rup,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
n_contour = len(contour) # จำนวนเส้นเค้าโครง
print(n_contour) # ได้ 9
rup = cv2.cvtColor(rup,cv2.COLOR_GRAY2BGR) # แปลงเป็นภาพสี
for i in range(n_contour):
# สร้างสีต่างๆขึ้นจากฟังก์ชัน get_cmap
r,g,b,a = plt.get_cmap('rainbow')(i/(n_contour-1))
si = (b*255,g*255,r*255)
rup = cv2.drawContours(rup,contour,i,si,2)
cv2.imwrite('a13c07.png',rup) # เขียนลงไฟล์
a13c07.png
ในที่นี้ใช้ฟังก์ชัน plt.get_cmap() ของ matplotlib เพื่อสร้างสี รายละเอียดวิธีใช้อ่านได้ใน
numpy & matplotlib บทที่ ๒๔
ในตัวอย่างนี้ได้เส้นเค้าโครงออกมาทั้งหมด ๙ เส้น เนื่องจากด้านในมีส่วนสีดำเล็กๆน้อยแทรกอยู่
ทุกซอกหลืบเหล่านี้ก็นับด้วยเช่นกัน
การหาเค้าโครงของภาพสี
ถ้ามีภาพสีที่ดูแล้วมีขอบเขตที่แน่นอนก็สามารถนำมาหาเค้าโครงแบ่งภาพได้เช่นกัน โดยแปลงเป็นขาวดำ แล้วใช้ cv2.threshold()
เพื่อทำให้เหลือแต่ 0 กับ 255 นำภาพนี้มาหาเค้าโครง
จากนั้นเมื่อได้เค้าโครงแล้วก็นำไปใช้วาดลงบนภาพเก่าที่เป็นภาพสีได้
ตัวอย่าง
miku13c01.jpg
miku = cv2.imread('miku13c01.jpg') # อ่านภาพมาเป็นภาพสี
miku_gr = cv2.cvtColor(miku,cv2.COLOR_BGR2GRAY) # แปลงเป็นขาวดำ
_,miku_thr = cv2.threshold(miku_gr,10,255,0)
# หาเส้นเค้าโครง
contour,_ = cv2.findContours(miku_thr,cv2.RETR_LIST,cv2.CHAIN_APPROX_SIMPLE)
# วาดเส้นเค้าโครงลงบนภาพเดิม
for i in range(len(contour)):
miku = cv2.drawContours(miku,contour,i,(0,200,0),2)
cv2.imwrite('miku13c02.jpg',miku)
miku13c02.jpg
ผลที่ได้ก็จะได้เส้นเค้าโครงออกมา แต่ก็จะเห็นว่าประกอบด้วยจุดเล็กน้อยเส้นย่อยซอกหลืบด้านใน
ซึ่งบางครั้งเราอาจไม่ได้ต้องการด้วย
เพื่อที่จะเอามันออกไป วิธีหนึ่งที่นิยมใช้ก็คือแปลงสัณฐาน cv2.morphologyEx() ด้วยโหมด MORPH_CLOSE
วิธีนี้จะช่วยลบจุดดำเล็กๆด้านในไปได้
ลองแปลงสัณฐานแล้วหาเส้นเค้าโครงใหม่เหมือนเดิม
miku = cv2.imread('miku13c01.jpg')
miku_gr = cv2.cvtColor(miku,cv2.COLOR_BGR2GRAY)
_,miku_thr = cv2.threshold(miku_gr,10,255,0)
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(5,5))
miku_thr = cv2.morphologyEx(miku_thr,cv2.MORPH_CLOSE,kernel)
contour,_ = cv2.findContours(miku_thr,cv2.RETR_LIST,cv2.CHAIN_APPROX_SIMPLE)
for i in range(len(contour)):
miku = cv2.drawContours(miku,contour,i,(0,200,0),2)
cv2.imwrite('miku13c03.jpg',miku)
miku13c03.jpg
คราวนี้เส้นเค้าโครงตามซอกเล็กซอกน้อยหายไป ดูแล้วเรียบกว่าเดิม
จากนั้นอาจลองนำเค้าโครงมาวาดด้วย matplotlib โดยใช้ plt.Polygon()
ax = plt.axes(aspect=1,xlim=[0,600],ylim=[450,0])
for cnt in contour:
ax.add_patch(plt.Polygon(cnt[:,0],fc='c',ec='r'))
plt.show()
ลำดับชั้นของเส้นเค้าโครง
ดังที่ได้กล่าวไปข้างต้นแล้วว่าฟังก์ชัน cv2.findContours()
นอกจากจะคืนข้อมูลตำแหน่งจุดของเส้นเค้าโครงมาแล้วก็ยังให้ข้อมูลลำดับชั้นของแต่ละเค้าโครงมาด้วย
โดยจะให้มาเป็นค่าคืนกลับตัวที่ ๒ ของฟังก์ชัน
ข้อมูลลำดับชั้นจะเป็นตัวเลข ๔ ตัว ซึ่งเป็นดัชนีของเส้นเค้าโครงตัวอื่น ซึ่งบอกถึง [ตัวถัดไป, ตัวก่อนหน้า,
ตัวลูกตัวแรก, ตัวพ่อแม่]
เลขลำดับนั้นจะไล่ตั้งแต่ 0 ไป ถ้าส่วนประกอบไหนขาดไปจะเป็น -1
ลองสร้างรูปง่ายๆที่มีสี่เหลี่ยมซ้อนกันหลายชั้นขึ้นมา
rup = np.zeros([500,600],dtype=np.uint8)
rup[50:-50,50:-50] = 200
rup[100:-100,100:250] = 0
rup[100:-100,-250:-100] = 0
rup[150:200,150:200] = 240
rup[-250:-150,150:200] = 60
rup[150:-150,-200:-150] = 120
cv2.imwrite('a13c08.png',rup)
a13c08.png
ในภาพนี้ประกอบด้วยกรอบสี่เหลี่ยม ๖ อัน ถ้าหาเค้าโครงก็จะได้มา ๖ ตัว
ลองหาเค้าโครงดูโดยเลือกวิธีการกำหนดชั้นของเส้นเค้าโครงเป็น cv2.RETR_TREE แล้วดูข้อมูลลำดับชั้น
contour,hierar = cv2.findContours(rup,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
print(hierar)
[[[-1 -1 1 -1]
[ 3 -1 2 0]
[-1 -1 -1 1]
[-1 1 4 0]
[ 5 -1 -1 3]
[-1 4 -1 3]]]
จะได้อาเรย์ขนาด 6×4 เป็นข้อมูลลำดับชั้นของเส้นเค้าโครงทั้ง ๖ ตัว
แถวแรกคือตัวนอกสุด [-1 -1 1 -1] จะเห็นว่าไม่มีทั้งตัวลำดับก่อนหน้าและหลังและไม่มีตัวพ่อแม่ด้วยเพราะเป็นตัวนอกสุดแล้ว
จึงเป็น -1 ส่วนตัวลูกตัวแรกคือหมายเลข 1 นั่นหมายถึงตัวถัดไป
ตัวที่แถวถัดมา [ 3 -1 2 0] แสดงให้เห็นว่ามีตัวลูกอยู่ คือตัวหมายเลข 2 และตัวพ่อแม่คือตัวหมายเลข 0 คือตัวนอกสุด
ที่เหลือก็ไล่ไปเรื่อยๆก็จะเห็นภาพและเข้าใจความหมาย เห็นความสัมพันธ์ของแต่ละตัวได้
หากเปลี่ยนเป็น cv2.RETR_LIST ผลจะกลายเป็นแบบนี้
[[[ 1 -1 -1 -1]
[ 2 0 -1 -1]
[ 3 1 -1 -1]
[ 4 2 -1 -1]
[ 5 3 -1 -1]
[-1 4 -1 -1]]]
จะเห็นว่าเลข ๒ ตัวหลังเป็น -1 หมด เพราะไม่มีการสร้างความสัมพันธ์พ่อแม่ลูกขึ้น แต่ละตัวถือว่าแยกจากกันไม่ได้ซ้อนกัน
ถือว่าอยู่ชั้นที่ ๑ หมด
ถ้าใช้เป็น cv2.RETR_CCOMP
[[[ 1 -1 -1 -1]
[ 2 0 -1 -1]
[ 3 1 -1 -1]
[-1 2 4 -1]
[ 5 -1 -1 3]
[-1 4 -1 3]]]
ในที่นี้ตัวหมายเลข 3 คือตัวเค้าโครงนอกสุด ส่วนตัวหมายเลข 4 และ 5 คือช่องสีดำที่ซ้อนอยู่ในนั้น ส่วนอีก ๓ ตัวที่เหลือแม้จะอยู่ด้านในลงไปอีกแต่จะถือว่าไม่มีการสร้างลำดับชั้น
ถ้าเป็น cv2.RETR_EXTERNAL จะได้
[[[-1 -1 -1 -1]]]
คือจะได้มาเฉพาะตัวที่อยู่นอกสุดเท่านั้น ซึ่งมีตัวเดียว เหมาะจะใช้เวลาที่สนใจเฉพาะโครงด้านนอกสุดจริงๆ
อ่านบทถัดไป >>
บทที่ ๑๔