φυβλαςのβλογ
phyblas的博客



opencv-python เบื้องต้น บทที่ ๑๓: การหาเส้นเค้าโครง
เขียนเมื่อ 2020/06/28 19:05
แก้ไขล่าสุด 2024/02/22 10:24

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

บทนี้ว่าด้วยเรื่องของการหาเส้นเค้าโครงซึ่งกั้นล้อมแบ่งอาณาเขตภายในภาพ




เส้นเค้าโครงคืออะไร

เส้นเค้าโครง (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]]]

คือจะได้มาเฉพาะตัวที่อยู่นอกสุดเท่านั้น ซึ่งมีตัวเดียว เหมาะจะใช้เวลาที่สนใจเฉพาะโครงด้านนอกสุดจริงๆ



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



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

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

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

หมวดหมู่

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

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

目录

从日本来的名言
模块
-- numpy
-- matplotlib

-- pandas
-- manim
-- opencv
-- pyqt
-- pytorch
机器学习
-- 神经网络
javascript
蒙古语
语言学
maya
概率论
与日本相关的日记
与中国相关的日记
-- 与北京相关的日记
-- 与香港相关的日记
-- 与澳门相关的日记
与台湾相关的日记
与北欧相关的日记
与其他国家相关的日记
qiita
其他日志

按类别分日志



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

  查看日志

  推荐日志

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