Asosiy qismga o'tish

Konturlar: obyektni topish, o'lchash va tanib olish

10-bob 10 kod bloki 3479 so'z

Kontur nima

Kontur — bir xil rangdagi sohaning chegara nuqtalari ro'yxati. cv2.findContours binar rasmni oladi va har obyekt uchun nuqtalar massivini qaytaradi. Shundan keyin obyektning maydoni, perimetri, markazi, shakli — hammasi hisoblab olinadi.

Muhim shart: obyekt oq, fon qora bo'lishi kerak. Shuning uchun 6-bobdagi THRESH_BINARY_INV kerak bo'ladi.

python9 qator
img = cv2.imread(str(RASM / "shakllar.png"))
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, binar = cv2.threshold(gray, 200, 255, cv2.THRESH_BINARY_INV)
konturlar, ierarxiya = cv2.findContours(binar, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
print(f"topilgan konturlar: {len(konturlar)} ta (rasmda 6 ta shakl bor)")
chizilgan = img.copy()
cv2.drawContours(chizilgan, konturlar, -1, (0, 0, 255), 3)
ko_rsat(binar, chizilgan, nom="konturlar.png", sarlavhalar=["binar (obyekt oq)", "topilgan konturlar"],
        izoh="findContours binar rasmni oladi, drawContours esa natijani rangli rasm ustiga chizadi.")
Chiqish
topilgan konturlar: 6 ta (rasmda 6 ta shakl bor)

findContours binar rasmni oladi, drawContours esa natijani rangli rasm ustiga chizadi.

findContours binar rasmni oladi, drawContours esa natijani rangli rasm ustiga chizadi.

O'lchash: maydon va perimetr

Har konturning maydonini hisoblangan haqiqat bilan solishtiramiz. Shakllar bizning kodimiz bilan chizilgani uchun aniq o'lchamlarini bilamiz:

python14 qator
HAQIQAT = {                                      # datasets/generate.py dagi chizish buyruqlaridan
    "to'rtburchak katta": 161 * 141,
    "to'rtburchak kichik": 91 * 91,
    "doira r=70": np.pi * 70 ** 2,
    "doira r=45": np.pi * 45 ** 2,
    "uchburchak": 8100.0,                        # uchlari: (300,400), (240,280), (380,290)
    "ellips 90x50": np.pi * 90 * 50,
}
olchangan = sorted((cv2.contourArea(k) for k in konturlar), reverse=True)
qatorlar = []
for (nom, haq), olch in zip(sorted(HAQIQAT.items(), key=lambda x: -x[1]), olchangan):
    qatorlar.append({"shakl": nom, "haqiqiy maydon": round(haq), "o'lchangan": round(olch),
                     "xato %": round(100 * abs(olch - haq) / haq, 2)})
show(pd.DataFrame(qatorlar))
shaklhaqiqiy maydono'lchanganxato %
to'rtburchak katta22 70122 4001.33
doira r=7015 39415 1761.41
ellips 90x5014 13714 1300.05
to'rtburchak kichik8 2818 1351.76
uchburchak8 1008 1000
doira r=456 3626 2342.01

Xato ustuni 1% dan kichik: kontur maydoni haqiqatga juda yaqin. Qolgan farq diskretlashtirishdan — doira piksellardan yasalgani uchun uning chegarasi zinapoya shaklida bo'ladi, matematik doira emas.

python9 qator
qatorlar = []
for k in sorted(konturlar, key=cv2.contourArea, reverse=True):
    M = cv2.moments(k)
    cx, cy = int(M["m10"] / M["m00"]), int(M["m01"] / M["m00"])
    perim = cv2.arcLength(k, True)
    qatorlar.append({"maydon": round(cv2.contourArea(k)), "perimetr": round(perim),
                     "markaz (x, y)": f"({cx}, {cy})",
                     "yumaloqlik": round(4 * np.pi * cv2.contourArea(k) / perim ** 2, 3)})
show(pd.DataFrame(qatorlar))
maydonperimetrmarkaz (x, y)yumaloqlik
22 400600(120, 130)0.782
15 176462(470, 130)0.895
14 130473(500, 350)0.794
8 135432(306, 323)0.547
8 100360(285, 105)0.785
6 234297(120, 340)0.89

Yumaloqlik (circularity) = 4πA / P². Nazariy qiymatlar: ideal doira 1.0, kvadrat 0.785, uchburchak 0.60. O'lchanganlari: kvadrat 0.785 (aniq mos), uchburchak 0.547, doiralar esa 0.895 va 0.890 — 1.0 emas. Sababi yana diskretlashtirish: piksel doirasining chegarasi zinapoyali, shuning uchun perimetri matematik doiranikidan uzunroq. Bu raqamni eslab qoling — keyingi bo'limda unga urilamiz.

cv2.moments — sohaning "og'irlik markazi"ni beradi: m10/m00 va m01/m00. Obyekt qayerda ekanini aytish kerak bo'lganda shu ishlatiladi (masalan, kuzatuvda — 15-bob).

Shaklni tanib olish: approxPolyDP

approxPolyDP konturni kamroq nuqta bilan yaqinlashtiradi. Natijadagi burchaklar soni shaklni aytadi:

python16 qator
def shakl_nomi(k):
    perim = cv2.arcLength(k, True)
    yaqin = cv2.approxPolyDP(k, 0.02 * perim, True)
    n = len(yaqin)
    if n == 3:
        return "uchburchak", n
    if n == 4:
        x, y, w, h = cv2.boundingRect(yaqin)
        return ("kvadrat" if 0.95 <= w / h <= 1.05 else "to'rtburchak"), n
    yumaloqlik = 4 * np.pi * cv2.contourArea(k) / perim ** 2
    return ("doira" if yumaloqlik > 0.9 else "ellips" if yumaloqlik > 0.7 else "ko'pburchak"), n

tartiblangan = sorted(konturlar, key=cv2.contourArea, reverse=True)
show(pd.DataFrame([{"topilgan shakl": shakl_nomi(k)[0], "burchaklar soni": shakl_nomi(k)[1],
                    "yumaloqlik": round(4 * np.pi * cv2.contourArea(k) / cv2.arcLength(k, True) ** 2, 3)}
                   for k in tartiblangan]))
topilgan shaklburchaklar soniyumaloqlik
to'rtburchak40.782
ellips80.895
ellips80.794
uchburchak30.547
kvadrat40.785
ellips80.89

Uchburchak va ikkala to'rtburchak to'g'ri tanildi. Lekin ikkala doira ham "ellips" deb belgilandi — chunki ularning o'lchangan yumaloqligi 0.895 va 0.890, bizning > 0.9 shartimizdan past. Chegarani haqiqiy o'lchovga qaramasdan tanlagan edik.

Buni tuzatishning ikki yo'li bor. Birinchisi — chegarani 0.88 ga tushirish, lekin u holda haqiqiy ellips (0.794) bilan orasi tor qoladi. Ikkinchisi va to'g'rirog'i — yumaloqlik o'rniga o'qlar nisbatini o'lchash: cv2.fitEllipse konturga ellips moslaydi va uning ikki o'qini beradi.

python25 qator
def shakl_nomi_v2(k):
    perim = cv2.arcLength(k, True)
    n = len(cv2.approxPolyDP(k, 0.02 * perim, True))
    if n == 3:
        return "uchburchak", 1.0
    if n == 4:
        x, y, w, h = cv2.boundingRect(k)
        return ("kvadrat" if 0.95 <= w / h <= 1.05 else "to'rtburchak"), round(w / h, 2)
    (_, _), (a, b), _ = cv2.fitEllipse(k)
    nisbat = max(a, b) / min(a, b)
    return ("doira" if nisbat < 1.15 else "ellips"), round(nisbat, 2)

natija = img.copy()
qatorlar = []
for k in tartiblangan:
    nom, nisbat = shakl_nomi_v2(k)
    M = cv2.moments(k)
    cx, cy = int(M["m10"] / M["m00"]), int(M["m01"] / M["m00"])
    cv2.drawContours(natija, [k], -1, (0, 0, 255), 2)
    cv2.putText(natija, nom, (cx - 55, cy), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 0), 2, cv2.LINE_AA)
    qatorlar.append({"topilgan shakl": nom, "nisbat (w/h yoki o'qlar)": nisbat,
                     "eski usul": shakl_nomi(k)[0]})
show(pd.DataFrame(qatorlar))
ko_rsat(natija, nom="shakl_tanish.png", kenglik=9,
        izoh="Yumaloqlik o'rniga o'qlar nisbati: doira ham, ellips ham to'g'ri tanildi.")
topilgan shaklnisbat (w/h yoki o'qlar)eski usul
to'rtburchak1.14to'rtburchak
doira1ellips
ellips1.8ellips
uchburchak1uchburchak
kvadrat1kvadrat
doira1ellips

Yumaloqlik o'rniga o'qlar nisbati: doira ham, ellips ham to'g'ri tanildi.

Yumaloqlik o'rniga o'qlar nisbati: doira ham, ellips ham to'g'ri tanildi.

Endi oltitasi ham to'g'ri. Saboq — chegarani "chiroyli son" bo'lgani uchun emas, o'lchangan taqsimotga qarab tanlang; va agar ikki sinf orasidagi masofa tor bo'lsa, boshqa belgi qidiring.

0.02 * perim — epsilon, ya'ni "nuqtani tashlab yuborishga ruxsat etilgan xato". Kichik epsilon deyarli hamma nuqtani saqlaydi, katta epsilon hamma narsani uchburchakka aylantiradi:

python6 qator
uchburchak_k = min(konturlar, key=lambda k: 4 * np.pi * cv2.contourArea(k) / cv2.arcLength(k, True) ** 2)
doira_k = max(konturlar, key=lambda k: 4 * np.pi * cv2.contourArea(k) / cv2.arcLength(k, True) ** 2)
show(pd.DataFrame([{"epsilon": e,
                    "uchburchak: nuqtalar": len(cv2.approxPolyDP(uchburchak_k, e * cv2.arcLength(uchburchak_k, True), True)),
                    "doira: nuqtalar": len(cv2.approxPolyDP(doira_k, e * cv2.arcLength(doira_k, True), True))}
                   for e in (0.001, 0.005, 0.01, 0.02, 0.05, 0.1)]))
epsilonuchburchak: nuqtalardoira: nuqtalar
0.001108100
0.005320
0.01312
0.0238
0.0534
0.134

Amalda 0.01–0.04 oralig'i ishlaydi. Agar shakl tanilmasa, birinchi tekshiradigan narsa — epsilon.

O'rab olish: to'rtta usul

python22 qator
k = max(konturlar, key=cv2.contourArea)          # eng katta shakl
urab = img.copy()
x, y, w, h = cv2.boundingRect(k)
cv2.rectangle(urab, (x, y), (x + w, y + h), (0, 255, 0), 2)                  # yashil: to'g'ri to'rtburchak
qutib = cv2.boxPoints(cv2.minAreaRect(k)).astype(int)
cv2.drawContours(urab, [qutib], 0, (255, 0, 0), 2)                           # ko'k: burilgan to'rtburchak
(mx, my), r = cv2.minEnclosingCircle(k)
cv2.circle(urab, (int(mx), int(my)), int(r), (0, 0, 255), 2)                 # qizil: doira
qavariq = cv2.convexHull(k)
cv2.drawContours(urab, [qavariq], -1, (255, 0, 255), 2)                      # siyohrang: qavariq qobiq

qatorlar = []
for nom, c in [("eng katta shakl", k), ("uchburchak", uchburchak_k)]:
    maydon = cv2.contourArea(c)
    xx, yy, ww, hh = cv2.boundingRect(c)
    qatorlar.append({"shakl": nom, "kontur maydoni": round(maydon),
                     "boundingRect maydoni": ww * hh,
                     "to'ldirish (extent) %": round(100 * maydon / (ww * hh), 1),
                     "solidity %": round(100 * maydon / cv2.contourArea(cv2.convexHull(c)), 1)})
show(pd.DataFrame(qatorlar))
ko_rsat(urab, nom="urab_olish.png", kenglik=9,
        izoh="Yashil — boundingRect, ko'k — minAreaRect, qizil — minEnclosingCircle, siyohrang — convexHull.")
shaklkontur maydoniboundingRect maydonito'ldirish (extent) %solidity %
eng katta shakl22 40022 70198.7100
uchburchak8 13517 06147.798.3

Yashil — boundingRect, ko'k — minAreaRect, qizil — minEnclosingCircle, siyohrang — convexHull.

Yashil — boundingRect, ko'k — minAreaRect, qizil — minEnclosingCircle, siyohrang — convexHull.

  • extent = kontur maydoni / bounding box maydoni. To'rtburchak uchun ~100%, uchburchak uchun ~50%.
  • solidity = kontur maydoni / qavariq qobiq maydoni. Botiq (yulduzsimon, singan) shakllarda pasayadi — detalda siniq bor-yo'qligini aniqlashning oddiy usuli.

Qaysi biri qachon: boundingRect — kesib olish va detektor natijasi uchun; minAreaRect — burilgan matn yoki detal uchun (u burchakni ham beradi); minEnclosingCircle — dumaloq obyektlar uchun; convexHull — shaklning "soyasi" kerak bo'lganda.

Ierarxiya: teshikli obyektlar

RETR_EXTERNAL faqat tashqi konturni beradi. RETR_TREE esa ichkilarini ham — va kim kimning ichida ekanini aytadi:

python15 qator
teshikli = np.full((300, 400, 3), 245, np.uint8)
cv2.rectangle(teshikli, (40, 40), (180, 260), (80, 80, 80), -1)
cv2.circle(teshikli, (110, 110), 30, (245, 245, 245), -1)                    # ichida teshik
cv2.circle(teshikli, (110, 200), 25, (245, 245, 245), -1)                    # yana bitta
cv2.circle(teshikli, (300, 150), 80, (80, 80, 80), -1)
g = cv2.cvtColor(teshikli, cv2.COLOR_BGR2GRAY)
_, b = cv2.threshold(g, 200, 255, cv2.THRESH_BINARY_INV)
tashqi, _ = cv2.findContours(b, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
hamma, ier = cv2.findContours(b, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
print(f"RETR_EXTERNAL: {len(tashqi)} kontur | RETR_TREE: {len(hamma)} kontur")
show(pd.DataFrame([{"kontur": i, "maydon": round(cv2.contourArea(c)),
                    "ota-kontur": int(ier[0][i][3]), "daraja": "tashqi" if ier[0][i][3] == -1 else "teshik"}
                   for i, c in enumerate(hamma)]))
ko_rsat(teshikli, b, nom="ierarxiya.png", sarlavhalar=["ikkita obyekt, ikkita teshik", "binar"],
        izoh="RETR_TREE teshiklarni ham topadi; ierarxiyaning 4-ustuni ota-konturni ko'rsatadi.")
Chiqish
RETR_EXTERNAL: 2 kontur | RETR_TREE: 4 kontur
konturmaydonota-konturdaraja
019 854-1tashqi
130 800-1tashqi
22 0321teshik
32 9061teshik

RETR_TREE teshiklarni ham topadi; ierarxiyaning 4-ustuni ota-konturni ko'rsatadi.

RETR_TREE teshiklarni ham topadi; ierarxiyaning 4-ustuni ota-konturni ko'rsatadi.

ierarxiya[0][i] — to'rt son: [keyingi, oldingi, birinchi bola, ota]. ota == -1 bo'lsa bu tashqi kontur. Detalda teshik bo'lishi kerakmi yoki yo'qmi degan tekshiruv shu bilan qilinadi.

Rejim Nima qaytaradi
RETR_EXTERNAL Faqat eng tashqi konturlar — odatdagi tanlov
RETR_LIST Hammasi, lekin ierarxiyasiz
RETR_CCOMP Ikki daraja: tashqi va teshik
RETR_TREE To'liq daraxt

Filtrlash: shovqindan keraklisini ajratish

Haqiqiy rasmda findContours yuzlab mayda bo'lak topadi. Ularni tashlash kerak:

python12 qator
shovqinli = cv2.imread(str(RASM / "shovqinli.png"), cv2.IMREAD_GRAYSCALE)
_, sb = cv2.threshold(shovqinli, 200, 255, cv2.THRESH_BINARY_INV)
xom, _ = cv2.findContours(sb, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
yadro = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
tozalangan = cv2.morphologyEx(cv2.morphologyEx(sb, cv2.MORPH_OPEN, yadro), cv2.MORPH_CLOSE, yadro)
morf, _ = cv2.findContours(tozalangan, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
show(pd.DataFrame([
    {"usul": "xom binar", "konturlar": len(xom)},
    {"usul": "xom + maydon > 500", "konturlar": sum(1 for c in xom if cv2.contourArea(c) > 500)},
    {"usul": "morfologiyadan keyin", "konturlar": len(morf)},
    {"usul": "morfologiya + maydon > 500", "konturlar": sum(1 for c in morf if cv2.contourArea(c) > 500)},
]))
usulkonturlar
xom binar4 235
xom + maydon > 5006
morfologiyadan keyin6
morfologiya + maydon > 5006

Rasmda 6 ta shakl bor. Xom binar rasmda findContours minglab kontur topadi — deyarli hammasi tuz-qalampir shovqinining yakka piksellari. Ikki yo'l bilan tozalash mumkin va ikkalasi ham 6 ga olib keladi: maydon bo'yicha filtr yoki 9-bobdagi morfologiya. Amalda ikkalasi birga ishlatiladi — morfologiya shaklni ham tuzatadi, maydon filtri esa qolgan-qutganini oladi.

:::tip Filtrlash mezonlari Maydon (contourArea), o'lcham (boundingRect ning w, h), nisbat (w/h), to'ldirish (extent), solidity, yumaloqlik, joylashuv (rasmning qaysi qismida). Vazifangizga mos ikki-uchtasini tanlang — bittasi kamlik qiladi, oltitasi esa mo'rt bo'lib qoladi. :::

Skaner uchun: varaqning to'rt burchagi

8-bobda varaqning chekkasini topdik. Endi undan to'rtburchak yasaymiz — 17-bobdagi loyihaning ikkinchi qadami:

python29 qator
hujjat = cv2.imread(str(RASM / "hujjat.jpg"))
h_gray = cv2.cvtColor(hujjat, cv2.COLOR_BGR2GRAY)
chekka = cv2.Canny(cv2.GaussianBlur(h_gray, (5, 5), 0), 50, 150)
chekka = cv2.morphologyEx(chekka, cv2.MORPH_CLOSE, np.ones((5, 5), np.uint8))
h_kont, _ = cv2.findContours(chekka, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
h_kont = sorted(h_kont, key=cv2.contourArea, reverse=True)[:5]

varaq = None
for c in h_kont:
    yaqin = cv2.approxPolyDP(c, 0.02 * cv2.arcLength(c, True), True)
    if len(yaqin) == 4:
        varaq = yaqin
        break

KUTILGAN = np.array([[150, 90], [720, 170], [650, 940], [90, 830]])
topilgan = varaq.reshape(4, 2)
tartib = sorted(topilgan.tolist(), key=lambda p: (p[1], p[0]))    # burchaklarni bir xil tartibga solamiz
tartib = sorted(tartib[:2], key=lambda p: p[0]) + sorted(tartib[2:], key=lambda p: -p[0])
xato = np.linalg.norm(np.array(tartib) - KUTILGAN, axis=1)
show(pd.DataFrame([{"burchak": n, "topilgan": f"({t[0]}, {t[1]})", "haqiqiy": f"({k[0]}, {k[1]})",
                    "xato, piksel": round(float(e), 1)}
                   for n, t, k, e in zip(["yuqori-chap", "yuqori-o'ng", "past-o'ng", "past-chap"],
                                         tartib, KUTILGAN.tolist(), xato)]))
belgi = hujjat.copy()
cv2.drawContours(belgi, [varaq], -1, (0, 255, 0), 4)
for p in tartib:
    cv2.circle(belgi, tuple(p), 10, (0, 0, 255), -1)
ko_rsat(chekka, belgi, nom="varaq_burchak.png", sarlavhalar=["Canny + MORPH_CLOSE", "topilgan to'rtburchak"],
        izoh="Eng katta 4 burchakli kontur — varaq. Burchaklar 6-bobda qo'lda yozgan koordinatalarimizga mos.")
burchaktopilganhaqiqiyxato, piksel
yuqori-chap(150, 90)(150, 90)0
yuqori-o'ng(719, 170)(720, 170)1
past-o'ng(650, 939)(650, 940)1
past-chap(91, 830)(90, 830)1

Eng katta 4 burchakli kontur — varaq. Burchaklar 6-bobda qo'lda yozgan koordinatalarimizga mos.

Eng katta 4 burchakli kontur — varaq. Burchaklar 6-bobda qo'lda yozgan koordinatalarimizga mos.

Diqqat qiling: 6-bobda varaq burchaklarini qo'lda yozgan edik. Endi ularni algoritm topdi va xato ustuni ikkalasi qanchalik mos kelishini ko'rsatadi. Aynan shu — "qo'lda yozilgan qiymatni avtomatik topishga almashtirish" — loyihani demo'dan mahsulotga aylantiradigan qadam.

Xulosa

Funksiya Nima beradi
findContours Binar rasmdan kontur nuqtalari (obyekt oq bo'lishi kerak)
contourArea, arcLength Maydon va perimetr
moments Og'irlik markazi: m10/m00, m01/m00
approxPolyDP Burchaklar soni — shaklni tanish; epsilon ≈ 0.02 × perimetr
boundingRect To'g'ri to'rtburchak (kesib olish uchun)
minAreaRect Burilgan to'rtburchak + burchak
minEnclosingCircle Eng kichik o'rab oluvchi doira
convexHull Qavariq qobiq; solidity botiqlikni ko'rsatadi
RETR_TREE Teshiklarni va ierarxiyani beradi
Filtrlash Maydon + morfologiya: shovqinli rasmda minglab konturdan 6 taga

Talaba ishi

  1. qurilish_kichik.jpg dagi sariq kaskalar maskasidan (5-bob) konturlar toping va har birining maydoni, markazi va yumaloqligini jadvalda chiqaring. Qaysi mezon kaskani soyadan ajratadi?
  2. shakl_nomi_v2 dagi nisbat < 1.15 chegarasini asoslang: doirani asta-sekin cho'zib (cv2.ellipse bilan o'qlarni 1.0 dan 1.5 gacha) nisbat qanday o'sishini o'lchang. 1.15 to'g'ri joydami?
  3. Uchburchakni burab (4-bob) qayta tanitib ko'ring. boundingRect va minAreaRect maydonlari burilish burchagi bilan qanday o'zgaradi? Grafikda chizing.
  4. Varaq topish kodini epsilon va Canny chegaralariga nisbatan sinang: qaysi qiymatlarda 4 burchakli kontur topilmay qoladi? Bu qanday himoya kerakligini ko'rsatadi (17-bobda shuni yozamiz).

Bu bobni bepul o'qidingiz.

Kitob o'rgatadi, lekin sizning kodingiz nega ishlamaganini ayta olmaydi. Kursda uy vazifangiz tekshiriladi.

Telegramda yozish Narxlar