Konturlar: obyektni topish, o'lchash va tanib olish
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.
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.")
topilgan konturlar: 6 ta (rasmda 6 ta shakl bor)

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:
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))
| shakl | haqiqiy maydon | o'lchangan | xato % |
|---|---|---|---|
| to'rtburchak katta | 22 701 | 22 400 | 1.33 |
| doira r=70 | 15 394 | 15 176 | 1.41 |
| ellips 90x50 | 14 137 | 14 130 | 0.05 |
| to'rtburchak kichik | 8 281 | 8 135 | 1.76 |
| uchburchak | 8 100 | 8 100 | 0 |
| doira r=45 | 6 362 | 6 234 | 2.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.
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))
| maydon | perimetr | markaz (x, y) | yumaloqlik |
|---|---|---|---|
| 22 400 | 600 | (120, 130) | 0.782 |
| 15 176 | 462 | (470, 130) | 0.895 |
| 14 130 | 473 | (500, 350) | 0.794 |
| 8 135 | 432 | (306, 323) | 0.547 |
| 8 100 | 360 | (285, 105) | 0.785 |
| 6 234 | 297 | (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:
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 shakl | burchaklar soni | yumaloqlik |
|---|---|---|
| to'rtburchak | 4 | 0.782 |
| ellips | 8 | 0.895 |
| ellips | 8 | 0.794 |
| uchburchak | 3 | 0.547 |
| kvadrat | 4 | 0.785 |
| ellips | 8 | 0.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.
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 shakl | nisbat (w/h yoki o'qlar) | eski usul |
|---|---|---|
| to'rtburchak | 1.14 | to'rtburchak |
| doira | 1 | ellips |
| ellips | 1.8 | ellips |
| uchburchak | 1 | uchburchak |
| kvadrat | 1 | kvadrat |
| doira | 1 | ellips |

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:
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)]))
| epsilon | uchburchak: nuqtalar | doira: nuqtalar |
|---|---|---|
| 0.001 | 108 | 100 |
| 0.005 | 3 | 20 |
| 0.01 | 3 | 12 |
| 0.02 | 3 | 8 |
| 0.05 | 3 | 4 |
| 0.1 | 3 | 4 |
Amalda 0.01–0.04 oralig'i ishlaydi. Agar shakl tanilmasa, birinchi tekshiradigan narsa — epsilon.
O'rab olish: to'rtta usul
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.")
| shakl | kontur maydoni | boundingRect maydoni | to'ldirish (extent) % | solidity % |
|---|---|---|---|---|
| eng katta shakl | 22 400 | 22 701 | 98.7 | 100 |
| uchburchak | 8 135 | 17 061 | 47.7 | 98.3 |

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:
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.")
RETR_EXTERNAL: 2 kontur | RETR_TREE: 4 kontur
| kontur | maydon | ota-kontur | daraja |
|---|---|---|---|
| 0 | 19 854 | -1 | tashqi |
| 1 | 30 800 | -1 | tashqi |
| 2 | 2 032 | 1 | teshik |
| 3 | 2 906 | 1 | teshik |

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:
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)}, ]))
| usul | konturlar |
|---|---|
| xom binar | 4 235 |
| xom + maydon > 500 | 6 |
| morfologiyadan keyin | 6 |
| morfologiya + maydon > 500 | 6 |
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:
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.")
| burchak | topilgan | haqiqiy | xato, 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.
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
qurilish_kichik.jpgdagi sariq kaskalar maskasidan (5-bob) konturlar toping va har birining maydoni, markazi va yumaloqligini jadvalda chiqaring. Qaysi mezon kaskani soyadan ajratadi?shakl_nomi_v2daginisbat < 1.15chegarasini asoslang: doirani asta-sekin cho'zib (cv2.ellipsebilan o'qlarni 1.0 dan 1.5 gacha) nisbat qanday o'sishini o'lchang. 1.15 to'g'ri joydami?- Uchburchakni burab (4-bob) qayta tanitib ko'ring.
boundingRectvaminAreaRectmaydonlari burilish burchagi bilan qanday o'zgaradi? Grafikda chizing. - Varaq topish kodini
epsilonva 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.