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



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

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

บทที่แล้วได้แนะนำการสร้างเส้นเค้าโครงในเบื้องต้นไปแล้ว สำหรับบทนี้จะเขียนถึงการนำเค้าโครงที่ได้มาลดรูปลงโดยยุบย่อหรือนำกรอบสี่เหลี่ยมมาคลุม




การประมาณเพื่อยุบย่อเค้าโครง

เค้าโครงที่สร้างขึ้นมาได้จาก cv2.findContours() นั้นจะแสดงตำแหน่งจุดที่จะวางเพื่อใช้เส้นตรงคั่น ถ้าตรงไหนเป็นเส้นโค้งก็จะต้องใช้หลายจุดเพื่อกำหนดจุดของเส้นเค้าโครง

ในบางครั้งเราอาจไม่จำเป็นต้องใช้จุดอย่างละเอียดมากถึงขนาดนั้น แต่ต้องการลดความซับซ้อนลงสักหน่อย ให้จุดของเค้าโครงแค่พอคลุมคร่าวๆ แต่ช่วยให้เรียบง่ายขึ้น ประหยัดที่เก็บข้อมูลขึ้น

ใน opencv มีฟังก์ชัน cv2.approxPolyDP() ซึ่งใช้อัลกอริธึมของดักลาส-พอยเกอร์ (Douglas–Peucker algorithm) เพื่อทำการประมาณเพื่อยุบย่อจุดบนเส้นให้เรียบง่ายขึ้น วิธีการนี้สามารถนำมาใช้กับเส้นเค้าโครงได้

ค่าที่ต้องใส่ในฟังก์ชัน
ลำดับที่ ชื่อ ค่าที่ต้องใส่ ชนิดข้อมูล
1 curve ตำแหน่งจุดบนเส้น np.array
2 epsilon ค่าที่กำหนดความละเอียดของการยุบย่อ float
3 closed เป็นเส้นปิดหรือไม่ True/False

ตัวแรกคืออาเรย์ที่แสดงตำแหน่งจุดบนเส้น ซึ่งสามารถใช้ผลที่ได้จาก cv2.findContours() แต่ว่าต้องแปลงทีละเค้าโครง

ตัวที่ ๒ คือ epsilon คือค่าความละเอียด ยิ่งใส่ค่ามากก็ยิ่งยุบจนลดรายละเอียดไปมาก

ตัวที่ ๓ นั้นใส่ True หรือ False เพื่อบอกว่าเส้นเค้าโครงเป็นเส้นปิดหรือเปิด ปกติเส้นเค้าโครงในที่นี้เป็นเส้นปิด จึงให้ใส่ True

ตัวอย่าง ลองเอาภาพนี้มาหาเค้าโครง เสร็จแล้วก็ลองวาดเส้นเค้าโครง เทียบดูระหว่างย่อกับไม่ย่อ โดยเทียบระหว่างการย่อด้วยค่า epsilon ต่างกัน


teto14c01.jpg
import cv2
import numpy as np
import matplotlib.pyplot as plt

teto = cv2.imread('teto14c01.jpg') # อ่านภาพสี
teto_gr = cv2.cvtColor(teto,cv2.COLOR_BGR2GRAY) # แปลงเป็นขาวดำ
_,teto_thr = cv2.threshold(teto_gr,10,255,0)
# แปลงสัณฐานเพื่อลดจุดปนเปื้อน
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(5,5))
teto_thr = cv2.morphologyEx(teto_thr,cv2.MORPH_CLOSE,kernel)
# หาเค้าโครง
contour,_ = cv2.findContours(teto_thr,cv2.RETR_LIST,cv2.CHAIN_APPROX_SIMPLE)
n_contour = len(contour)
# สีของเส้นเค้าโครงแต่ละเส้น
si = plt.get_cmap('rainbow')(np.arange(n_contour)/(n_contour-1))[:,[2,1,0]]*255
# สร้างเค้าโครงใหม่ที่ย่อลงจากเดิมเล็กน้อย
contour_dp1 = [cv2.approxPolyDP(cnt,4.5,True) for cnt in contour]
# สร้างเค้าโครงใหม่ที่ย่อลงจากเดิมไปค่อนข้างมาก
contour_dp2 = [cv2.approxPolyDP(cnt,11,True) for cnt in contour]
teto_cnt = teto.copy()
teto_cnt_dp1 = teto.copy()
teto_cnt_dp2 = teto.copy()
for i in range(n_contour):
    teto_cnt = cv2.drawContours(teto_cnt,contour,i,si[i],2)
    teto_cnt_dp1 = cv2.drawContours(teto_cnt_dp1,contour_dp1,i,si[i],2)
    teto_cnt_dp2 = cv2.drawContours(teto_cnt_dp2,contour_dp2,i,si[i],2)

cv2.imwrite('teto14c02.jpg',teto_cnt)
cv2.imwrite('teto14c03.jpg',teto_cnt_dp1)
cv2.imwrite('teto14c04.jpg',teto_cnt_dp2)

เค้าโครงที่ยังไม่ได้ยุบ

teto14c02.jpg

เค้าโครงที่ยุบไปเล็กน้อย

teto14c03.jpg

เค้าโครงที่ยุบไปมาก

teto14c04.jpg

จะเห็นว่ายิ่งค่า epsilon สูงก็ยิ่งย่อจนเหลือจุดน้อยลงและดูหลวมลงไปมากจนยิ่งไม่ค่อยพอดีกับวัตถุที่ถูกล้อมอยู่




การขึงเส้นเค้าโครง

อีกวิธีที่อาจใช้ในการลดจุดของเส้นเค้าโครงก็คือ จับส่วนที่เว้าแหว่งมาขึงให้เรียบให้หมด

ดูเผินๆอาจคล้ายกับ cv2.approxPolyDP() แต่ว่าวิธีการง่ายกว่า คือดูว่าตรงไหนเว้าลงไปก็ขึงตรงนั้นซะ และข้อดีคือกรอบเค้าโครงที่ย่อแล้วจะยังคงคลุมวัตถุอยู่

ฟังก์ชันสำหรับทำการขึงเส้นเค้าโครงคือ cv2.convexHull()

ค่าที่ต้องใส่มีแค่อาเรย์ของตำแหน่งจุดเค้าโครงเดิม และก็จะได้เค้าโครงใหม่ที่โดนขึงย่อ

เพื่อให้เห็นภาพชัด สร้างอาเรย์เค้าโครงที่มี ๔ จุด แล้วลองใช้ cv2.convexHull() ขึงดู คราวนี้ใช้ matplotlib วาด
cnt = np.array([[[0,0.5]],
                [[1,1]],
                [[0.5,0.5]],
                [[0.8,0]]],dtype=np.float32)
ax = plt.subplot(121,aspect=1)
ax.add_patch(plt.Polygon(cnt[:,0],fc='w',ec='g'))
cnt_ch = cv2.convexHull(cnt)
ax = plt.subplot(122,aspect=1)
ax.add_patch(plt.Polygon(cnt_ch[:,0],fc='w',ec='g'))
plt.show()


คราวนี้ลองเอาภาพเดิมต่อจากตัวอย่างก่อนหน้านี้มาใช้ cv2.convexHull()
teto_cnt = teto.copy()
for i in range(n_contour):
    cnt = cv2.convexHull(contour[i])
    teto_cnt = cv2.drawContours(teto_cnt,[cnt],0,si[i],2)
cv2.imwrite('teto14c05.jpg',teto_cnt)

teto14c05.jpg




การตรวจดูว่าจุดบนเค้าโครงถูกขึงตึงหมดหรือยัง

หากมีเส้นเค้าโครงแล้วต้องการดูว่าถูกขึงตึงหมดหรือยังอาจใช้ฟังก์ชัน cv2.isContourConvex() จะคืนค่าเป็น True ถ้าขึงตึงแล้ว และ False ถ้ายังไม่ขึงตึง

ตัวอย่าง ลองสร้างอาเรย์ของจุดเส้นเค้าโครงแบบง่ายๆขึ้นมา วาดดูแล้วดูว่าแบบไหนขึงตึงแล้วหรือไม่
cnt1 = np.array([[[0,0]],
                 [[1,2]],
                 [[0,3]],
                 [[-1,2]]])

cnt2 = np.array([[[0,0]],
                 [[1,2]],
                 [[0,1]],
                 [[-1,3]]])

cnt2_ch = cv2.convexHull(cnt2)

ax = plt.subplot(131,aspect=1,xlim=[-2,2],ylim=[0,4])
ax.add_patch(plt.Polygon(cnt1[:,0],fc='w',ec='m'))
ax = plt.subplot(132,aspect=1,xlim=[-2,2],ylim=[0,4])
ax.add_patch(plt.Polygon(cnt2[:,0],fc='w',ec='m'))
ax = plt.subplot(133,aspect=1,xlim=[-2,2],ylim=[0,4])
ax.add_patch(plt.Polygon(cnt2_ch[:,0],fc='w',ec='m'))
plt.show()

print(cv2.isContourConvex(cnt1)) # ได้ True
print(cv2.isContourConvex(cnt2)) # ได้ False
print(cv2.isContourConvex(cnt2_ch)) # ได้ True


ในที่นี้ cnt1 ขึงตึงอยู่แล้ว cn2 ไม่ได้ขึงตึง แต่เอามาขึงด้วย cv2.convexHull() ก็จะได้เส้นที่ขึงตึง




การสร้างกรอบสี่เหลี่ยมผืนผ้าล้อมเค้าโครง

เค้าโครงที่มีโครงสร้างอย่างง่ายที่อาจสามารถสร้างได้ก็คือเป็นเส้นกรอบสี่เหลี่ยมล้อม

ถ้ามีเส้นเค้าโครงอยู่ แล้วอยากหากรอบสี่เหลี่ยมผืนผ้าขนาดเล็กที่สุดที่จะคลุมเส้นเค้าโครงนั้นได้อาจใช้ฟังก์ชัน cv2.minAreaRect()

ฟังก์ชันนี้จะให้ข้อมูลของสี่เหลี่ยมผืนผ้าออกมา โดยให้เป็นทูเพิลตัวเลข ๕ ตัวที่บอกถึง ((x,y),(กว้าง,สูง),มุมเอียง) ออกมา

จากนั้นถ้าใช้ฟังก์ชัน cv2.boxPoints() ก็จะแปลงเป็นตำแหน่งจุดพิกัดมุมของเค้าโครงรูปกรอบสี่เหลี่ยมผืนผ้า เอาค่าที่ได้มาใช้วาดได้

ตัวอย่าง เอาภาพนี้มาหาเค้าโครง แล้วก็หาสี่เหลี่ยมที่ล้อมเค้าโครงนี้


rin14c01.jpg
rin = cv2.imread('rin14c01.jpg')
rin_gr = cv2.cvtColor(rin,cv2.COLOR_BGR2GRAY)
_,rin_thr = cv2.threshold(rin_gr,10,255,0)
# แปลงสัณฐานเพื่อลดจุดปนเปื้อน
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(5,5))
rin_thr = cv2.morphologyEx(rin_thr,cv2.MORPH_CLOSE,kernel)
# หาเค้าโครง
contour,_ = cv2.findContours(rin_thr,cv2.RETR_LIST,cv2.CHAIN_APPROX_SIMPLE)
n_contour = len(contour)

# สีของเส้นเค้าโครงแต่ละเส้น
si = plt.get_cmap('rainbow')(np.arange(n_contour)/(n_contour-1))[:,[2,1,0]]*255

rin_cnt = rin.copy()
siliam = []
for i,cnt in enumerate(contour):
    # วาดเส้นเค้าโครงเดิม
    rin_cnt = cv2.drawContours(rin_cnt,[cnt],0,si[i],2)
    rect = cv2.minAreaRect(cnt) # หาสี่เหลี่ยมที่ล้อม
    siliam.append(rect) # เก็บข้อมูลกรอบสี่เหลี่ยมไว้

for i in range(n_contour):
    # แปลงข้อมูลกรอบสี่เหลี่ยมเป็นเค้าโครง ข้อมูลต้องเปลี่ยนเป็น int จึงจะใช้ใน cv2.drawContours() ได้
    box = cv2.boxPoints(siliam[i]).astype(int)
    # วาดเส้นเค้าโครงสี่เหลี่ยม
    rin_cnt = cv2.drawContours(rin_cnt,[box],0,si[i],2)
cv2.imwrite('rin14c02.jpg',rin_cnt)

rin14c02.jpg

จะได้เค้าโครงที่เป็นสี่เหลี่ยมผืนผ้าที่บรรจุเค้าโครงเดิมของภาพ

เค้าโครงสี่เหลี่ยมผืนผ้าที่ได้มานี้อาจมีบางส่วนซ้อนกัน หากอยากรู้ว่าพื้นที่ส่วนที่ซ้อนกันอยู่ตรงไหนบ้างอาจใช้ฟังก์ชัน cv2.rotatedRectangleIntersection() โดยใช้ข้อมูลที่ได้จาก cv2.minAreaRect() ใส่ลงไป

ฟังก์ชันนี้จะได้ค่าคืนกลับมา ๒ ตัว ตัวแรกคือเลข 0 หรือ 1 ซึ่งบอกว่าสี่เหลี่ยมทั้งสองมีส่วนที่ซ้อนทับกันหรือไม่ ส่วนอีกตัวคือจุดมุมของบริเวณที่ซ้อนทับกัน

ลองหาส่วนซ้อนทับจากภาพที่แล้วแล้วมาวาดทับลงไปดูได้
# ไล่เทียบดูทีละคู่
for i in range(len(siliam)):
    for j in range(i+1,len(siliam)):
        mi,tat = cv2.rotatedRectangleIntersection(siliam[i],siliam[j])
        # ถ้า mi เป็น 1 แสดงว่ามีส่วนซ้อนทับ ก็เอามาวาด
        if(mi):
            tat = tat.astype(int) # ต้องเปลี่ยนชนิดข้อมูลเป็น int จึงจะใช้ใน cv2.drawContours() ได้
            rin_cnt = cv2.drawContours(rin_cnt,[tat],0,(si[i]+si[j])/2,5)
cv2.imwrite('rin14c03.jpg',rin_cnt)

rin14c03.jpg



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



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

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

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

หมวดหมู่

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

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

สารบัญ

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

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

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



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

  ค้นหาบทความ

  บทความแนะนำ

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

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

2024年

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

2023年

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

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月

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

ไทย

日本語

中文