Kalit nuqtalar: burilgan va o'lchami o'zgargan obyektni topish
Muammo: 12-bob qayerda to'xtadi
O'tgan bobda shablon qidirish 20° burilishda 0.683 ballga tushdi va ishonchsiz bo'lib qoldi.
Sabab tushunarli: matchTemplate shablonni piksel-ba-piksel solishtiradi, burilgan obyektning
piksellari esa boshqa joyda turadi.
Yechim — butun bo'lakni emas, o'ziga xos nuqtalarni solishtirish. Burchak, dog', chorraha kabi joylar obyekt bilan birga buriladi va kichrayadi, lekin "o'ziga xosligi" saqlanadi.
Bu uch qadam:
- Detektor — o'ziga xos nuqtalarni topadi (keypoints).
- Deskriptor — har nuqta atrofini raqamlar vektoriga aylantiradi.
- Matcher — ikki rasmning vektorlarini juftlaydi.
ORB: nuqtalarni topish
rasm = cv2.imread(str(RASM / "qurilish_kichik.jpg")) gray = cv2.cvtColor(rasm, cv2.COLOR_BGR2GRAY) orb = cv2.ORB_create(nfeatures=1000) nuqtalar, tavsif = orb.detectAndCompute(gray, None) print(f"topilgan kalit nuqtalar: {len(nuqtalar)}") print(f"deskriptor matritsasi: {tavsif.shape} | turi: {tavsif.dtype}") print(f"birinchi nuqta — joyi: ({nuqtalar[0].pt[0]:.0f}, {nuqtalar[0].pt[1]:.0f}), " f"o'lchami: {nuqtalar[0].size:.1f}, burchagi: {nuqtalar[0].angle:.0f}°") belgi = cv2.drawKeypoints(rasm, nuqtalar[:300], None, color=(0, 255, 0), flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS) ko_rsat(belgi, nom="orb_nuqtalar.png", kenglik=10, izoh="Har doira — bitta kalit nuqta. Doira o'lchami masshtabni, chiziq esa burchakni ko'rsatadi.")
topilgan kalit nuqtalar: 1000 deskriptor matritsasi: (1000, 32) | turi: uint8 birinchi nuqta — joyi: (96, 386), o'lchami: 31.0, burchagi: 251°

Har doira — bitta kalit nuqta. Doira o'lchami masshtabni, chiziq esa burchakni ko'rsatadi.
Deskriptor — har nuqta uchun 32 bayt (256 bit). ORB uni atrofdagi piksel juftlarini solishtirib yasaydi: "bu piksel anavidan yorug'mi?" — 256 marta. Natija ikkilik satr, shuning uchun uni solishtirish juda tez (XOR va bit sanash).
Kalit nuqtalar qayerda paydo bo'ladi? Faqat burchak va o'ziga xos naqsh bor joyda:
sohalar = {"osmon (tekis)": gray[0:120, 300:600], "bino (naqshli)": gray[150:400, 120:420], "yer (o'rtacha)": gray[420:540, 100:500]} show(pd.DataFrame([{"soha": n, "o'lcham": f"{s.shape[1]}x{s.shape[0]}", "kalit nuqtalar": len(orb.detect(s, None)), "1000 pikselga": round(1000 * len(orb.detect(s, None)) / s.size, 2)} for n, s in sohalar.items()]))
| soha | o'lcham | kalit nuqtalar | 1000 pikselga |
|---|---|---|---|
| osmon (tekis) | 300x120 | 18 | 0.5 |
| bino (naqshli) | 300x250 | 824 | 10.99 |
| yer (o'rtacha) | 400x120 | 357 | 7.44 |
Tekis sohada nuqta deyarli yo'q — u yerda "o'ziga xos" narsa yo'q. Bu 12-bobdagi bir tekis shablon muammosining javobi: kalit nuqtalar usuli bunday joyni o'zi chetlab o'tadi.
Ikki rasmni juftlash
Endi asosiy sinov. Rasmni buramiz va kichraytiramiz, so'ng ikkalasining nuqtalarini juftlaymiz:
M = cv2.getRotationMatrix2D((gray.shape[1] / 2, gray.shape[0] / 2), 30, 0.8) ozgargan = cv2.warpAffine(gray, M, (gray.shape[1], gray.shape[0]), borderValue=128) n1, d1 = orb.detectAndCompute(gray, None) n2, d2 = orb.detectAndCompute(ozgargan, None) bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True) juftlar = sorted(bf.match(d1, d2), key=lambda m: m.distance) print(f"nuqtalar: {len(n1)} va {len(n2)} | juftlar: {len(juftlar)}") print(f"eng yaqin juft masofasi: {juftlar[0].distance:.0f} | eng uzog'i: {juftlar[-1].distance:.0f}") chizma = cv2.drawMatches(gray, n1, ozgargan, n2, juftlar[:40], None, flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS) ko_rsat(chizma, nom="juftlar.png", kenglik=14, izoh="30° burilgan va 0.8 barobar kichraytirilgan rasm bilan juftlash. Chiziqlar parallel bo'lsa — to'g'ri juftlar.")
nuqtalar: 1000 va 1000 | juftlar: 468 eng yaqin juft masofasi: 8 | eng uzog'i: 84

30° burilgan va 0.8 barobar kichraytirilgan rasm bilan juftlash. Chiziqlar parallel bo'lsa — to'g'ri juftlar.
NORM_HAMMING — ikkilik deskriptorlar uchun masofa: nechta bit farq qiladi. crossCheck=True esa
ikki tomonlama tekshiruv: A ning eng yaqini B bo'lsa va B ning eng yaqini A bo'lsa, faqat shunda juft
deb hisoblanadi. Bu soxta juftlarning katta qismini o'zi olib tashlaydi.
Lowe ratio test: soxta juftlarni tashlash
crossCheck o'rniga ko'proq ishlatiladigan usul — har nuqta uchun ikkita eng yaqinini olib, ularning
masofasini solishtirish. Agar ikkalasi ham bir xil darajada yaqin bo'lsa, demak nuqta o'ziga xos emas:
bf2 = cv2.BFMatcher(cv2.NORM_HAMMING) xom = bf2.knnMatch(d1, d2, k=2) qatorlar = [] for nisbat in (0.6, 0.7, 0.75, 0.8, 0.9, 1.0): yaxshi = [a for a, b in xom if a.distance < nisbat * b.distance] qatorlar.append({"ratio": nisbat, "qolgan juftlar": len(yaxshi), "ulush %": round(100 * len(yaxshi) / len(xom), 1)}) show(pd.DataFrame(qatorlar)) yaxshi = [a for a, b in xom if a.distance < 0.75 * b.distance]
| ratio | qolgan juftlar | ulush % |
|---|---|---|
| 0.6 | 248 | 24.8 |
| 0.7 | 311 | 31.1 |
| 0.75 | 358 | 35.8 |
| 0.8 | 405 | 40.5 |
| 0.9 | 552 | 55.2 |
| 1 | 942 | 94.2 |
Lowe tavsiya qilgan qiymat — 0.75. Kichikroq qilsangiz juftlar kam, lekin toza; kattaroq qilsangiz ko'p, lekin soxta juftlar aralashadi.
Homografiya: "obyekt qayerda?" degan savolga javob
Juftlar bor, lekin ular hali obyektning joyini bermaydi. Homografiya — bir tekislikdan ikkinchisiga
o'tkazadigan 3x3 matritsa. findHomography uni juftlardan hisoblaydi va RANSAC yordamida soxta
juftlarni o'zi chetlab o'tadi:
src = np.float32([n1[m.queryIdx].pt for m in yaxshi]).reshape(-1, 1, 2) dst = np.float32([n2[m.trainIdx].pt for m in yaxshi]).reshape(-1, 1, 2) H, maska = cv2.findHomography(src, dst, cv2.RANSAC, 5.0) ichkilar = int(maska.sum()) print(f"juftlar: {len(yaxshi)} | RANSAC ichkilari (inliers): {ichkilar} ({100 * ichkilar / len(yaxshi):.0f}%)") print("topilgan homografiya:\n", np.round(H, 3)) HAQIQAT = np.vstack([M, [0, 0, 1]]) # biz qo'llagan burilish+masshtab matritsasi burchaklar = np.float32([[0, 0], [gray.shape[1], 0], [gray.shape[1], gray.shape[0]], [0, gray.shape[0]]]).reshape(-1, 1, 2) topilgan_b = cv2.perspectiveTransform(burchaklar, H).reshape(4, 2) haqiqiy_b = cv2.perspectiveTransform(burchaklar, HAQIQAT).reshape(4, 2) xato = np.linalg.norm(topilgan_b - haqiqiy_b, axis=1) show(pd.DataFrame([{"burchak": i, "topilgan": f"({t[0]:.0f}, {t[1]:.0f})", "haqiqiy": f"({q[0]:.0f}, {q[1]:.0f})", "xato, piksel": round(float(e), 1)} for i, (t, q, e) in enumerate(zip(topilgan_b, haqiqiy_b, xato))]))
juftlar: 358 | RANSAC ichkilari (inliers): 342 (96%) topilgan homografiya: [[ 0.692 0.399 39.93 ] [ -0.4 0.694 274.085] [ -0. -0. 1. ]]
| burchak | topilgan | haqiqiy | xato, piksel |
|---|---|---|---|
| 0 | (40, 274) | (39, 275) | 1 |
| 1 | (705, -110) | (705, -109) | 0.6 |
| 2 | (921, 265) | (921, 265) | 0.7 |
| 3 | (256, 649) | (255, 649) | 0.5 |
Biz burilish matritsasini o'zimiz yozganmiz, demak haqiqiy javob qo'limizda. Jadval algoritm topgan burchaklarni haqiqiy burchaklar bilan solishtiradi — xato piksel bilan o'lchanadi.
RANSAC qanday ishlaydi: tasodifiy 4 juftni olib homografiya hisoblaydi, qolgan juftlarning qanchasi unga mos kelishini sanaydi, va buni yuzlab marta takrorlab eng ko'p tarafdor to'plagan variantni tanlaydi. Shuning uchun juftlarning yarmi soxta bo'lsa ham to'g'ri javob chiqadi.
Asosiy sinov: 12-bob bilan yonma-yon
Endi o'sha uchta sinovni takrorlaymiz va matchTemplate bilan solishtiramiz:
def orb_ichkilar(o1, o2): """Ikki rasm orasidagi RANSAC ichkilari soni.""" a1, t1 = orb.detectAndCompute(o1, None) a2, t2 = orb.detectAndCompute(o2, None) if t1 is None or t2 is None or len(t1) < 4 or len(t2) < 4: return 0 jufts = bf2.knnMatch(t1, t2, k=2) yax = [p[0] for p in jufts if len(p) == 2 and p[0].distance < 0.75 * p[1].distance] if len(yax) < 4: return 0 s = np.float32([a1[m.queryIdx].pt for m in yax]).reshape(-1, 1, 2) d = np.float32([a2[m.trainIdx].pt for m in yax]).reshape(-1, 1, 2) Hm, mk = cv2.findHomography(s, d, cv2.RANSAC, 5.0) return int(mk.sum()) if mk is not None else 0 def shablon_ball(katta, shab): return round(float(cv2.minMaxLoc(cv2.matchTemplate(katta, shab, cv2.TM_CCOEFF_NORMED))[1]), 3) shab = gray[150:400, 120:420] # binoning bir bo'lagi — shablon qatorlar = [] for burchak in (0, 5, 10, 20, 45, 90): Mr = cv2.getRotationMatrix2D((gray.shape[1] / 2, gray.shape[0] / 2), burchak, 1.0) o = cv2.warpAffine(gray, Mr, (gray.shape[1], gray.shape[0]), borderValue=128) qatorlar.append({"burilish": f"{burchak}°", "ORB ichkilari": orb_ichkilar(gray, o), "matchTemplate balli": shablon_ball(o, shab)}) show(pd.DataFrame(qatorlar))
| burilish | ORB ichkilari | matchTemplate balli |
|---|---|---|
| 0° | 1 000 | 1 |
| 5° | 588 | 0.536 |
| 10° | 489 | 0.43 |
| 20° | 374 | 0.303 |
| 45° | 227 | 0.232 |
| 90° | 117 | 0.169 |
Bu jadval bobning butun mazmuni. Ikki ustunni yonma-yon o'qing:
| Burilish | matchTemplate | ORB ichkilari | Xulosa |
|---|---|---|---|
| 0° | 1.000 | 1000 | Ikkalasi ham mukammal |
| 10° | 0.430 | 489 | Shablon ishonchsiz, ORB bemalol |
| 20° | 0.303 | 374 | Shablon sinди, ORB hali kuchli |
| 90° | 0.169 | 117 | Shablon butunlay yo'q, ORB hali ishlaydi |
90° burilishda ham ORB 117 ta ichki juft topdi — bu homografiya hisoblash uchun (4 ta kerak) o'ta yetarli. Shablon balli esa 0.169 ga tushdi, ya'ni tasodifiy shovqindan farqi yo'q.
Diqqat qiling: bu yerda shablon balli 12-bobdagidan ham tezroq tushdi (5° da 0.536, u yerda 0.897 edi). Sababi — bu haqiqiy fotosurat, sun'iy shakllar emas: unda mayda tekstura ko'p, va burilganda o'sha tekstura butunlay boshqa piksellarga aylanadi.
O'lcham sinovi ham xuddi shunday:
qatorlar = [] for koef in (0.5, 0.7, 1.0, 1.4, 2.0): o = cv2.resize(gray, None, fx=koef, fy=koef, interpolation=cv2.INTER_AREA) sigadi = o.shape[0] >= shab.shape[0] and o.shape[1] >= shab.shape[1] qatorlar.append({"o'lcham": f"x{koef}", "ORB ichkilari": orb_ichkilar(gray, o), "matchTemplate balli": shablon_ball(o, shab) if sigadi else None}) show(pd.DataFrame(qatorlar))
| o'lcham | ORB ichkilari | matchTemplate balli |
|---|---|---|
| x0.5 | 256 | 0.166 |
| x0.7 | 487 | 0.373 |
| x1.0 | 1 000 | 1 |
| x1.4 | 391 | 0.309 |
| x2.0 | 209 | 0.298 |
Rasm ikki barobar kattalashtirilganda (x2.0) shablon balli 0.298 ga tushdi, ORB esa 209 ichki
juft topdi. Yarim o'lchamda (x0.5) — 0.166 ga qarshi 256.
Ya'ni kalit nuqtalar usuli 12-bobning ikkala muammosini ham yechadi. Buning evaziga u ancha murakkabroq va sekinroq — quyida o'lchaymiz.
ORB, SIFT va AKAZE
import time usullar = {"ORB": cv2.ORB_create(nfeatures=1000), "SIFT": cv2.SIFT_create(nfeatures=1000), "AKAZE": cv2.AKAZE_create(), "BRISK": cv2.BRISK_create()} Mr = cv2.getRotationMatrix2D((gray.shape[1] / 2, gray.shape[0] / 2), 30, 0.8) burilgan = cv2.warpAffine(gray, Mr, (gray.shape[1], gray.shape[0]), borderValue=128) qatorlar = [] for nom, det in usullar.items(): t0 = time.perf_counter() k1, v1 = det.detectAndCompute(gray, None) k2, v2 = det.detectAndCompute(burilgan, None) ms = (time.perf_counter() - t0) * 1000 norma = cv2.NORM_L2 if nom == "SIFT" else cv2.NORM_HAMMING m = cv2.BFMatcher(norma).knnMatch(v1, v2, k=2) yax = [a for a, b in m if a.distance < 0.75 * b.distance] ichki = 0 if len(yax) >= 4: s = np.float32([k1[x.queryIdx].pt for x in yax]).reshape(-1, 1, 2) d = np.float32([k2[x.trainIdx].pt for x in yax]).reshape(-1, 1, 2) _, mk = cv2.findHomography(s, d, cv2.RANSAC, 5.0) ichki = int(mk.sum()) if mk is not None else 0 qatorlar.append({"usul": nom, "nuqtalar": len(k1), "deskriptor": f"{v1.shape[1]} x {v1.dtype}", "yaxshi juftlar": len(yax), "ichkilar": ichki, "ichki ulushi %": round(100 * ichki / max(len(yax), 1), 1), "vaqt, ms": round(ms)}) show(pd.DataFrame(qatorlar))
| usul | nuqtalar | deskriptor | yaxshi juftlar | ichkilar | ichki ulushi % | vaqt, ms |
|---|---|---|---|---|---|---|
| ORB | 1 000 | 32 x uint8 | 358 | 342 | 95.5 | 9 |
| SIFT | 1 001 | 128 x float32 | 537 | 521 | 97 | 80 |
| AKAZE | 1 967 | 61 x uint8 | 657 | 590 | 89.8 | 37 |
| BRISK | 6 770 | 64 x uint8 | 1 624 | 1 513 | 93.2 | 92 |
Jadvalni ehtiyot bo'lib o'qing. BRISK eng ko'p ichki juft topdi (1513), lekin u 6770 ta nuqta ham topgan — biz unga chegara qo'ymaganmiz. Ko'p nuqta qidirsangiz ko'p juft topasiz, bu sifat emas.
Solishtirish uchun ichki ulushi ustuni to'g'riroq: u topilgan yaxshi juftlarning qanchasi homografiyaga mos kelganini ko'rsatadi. Bu yerda SIFT ustun — 97%, ORB 95.5%, BRISK 93.2%, AKAZE 89.8%.
Tezlikda esa tartib teskari: ORB 9 ms, AKAZE 36 ms, SIFT 79 ms, BRISK 93 ms. ORB SIFT dan to'qqiz barobar tez.
| Usul | Litsenziya | Bu o'lchovda | Kuchli tomoni |
|---|---|---|---|
| ORB | Bepul (BSD) | 9 ms, ichki ulushi 95.5% | Tez; real vaqt va video uchun |
| SIFT | Bepul (2020 dan patenti tugagan) | 79 ms, ichki ulushi 97% | Eng aniq; float deskriptor |
| AKAZE | Bepul | 36 ms, ichki ulushi 89.8% | O'rtacha yo'l |
| BRISK | Bepul | 93 ms, 6770 nuqta | Nuqta ko'p, lekin sekin |
Boshlang'ich tanlov: real vaqt kerak bo'lsa ORB, aniqlik kerak bo'lsa SIFT, o'rtasi — AKAZE. Video uchun 9 ms bilan 79 ms orasidagi farq hal qiluvchi: 30 FPS da bitta kadrga atigi 33 ms bor.
:::gotcha SIFT uchun boshqa masofa
SIFT deskriptori — float (128 o'lchov), shuning uchun NORM_L2 ishlatiladi. ORB, AKAZE, BRISK esa
ikkilik, ularga NORM_HAMMING kerak. Noto'g'ri normani bersangiz natija ma'nosiz bo'ladi, lekin xato
ham bermaydi — shuning uchun buni sezish qiyin.
:::
Kalit nuqtalar qachon ishlamaydi
Usul kuchli, lekin sehrli emas:
- Tekis yoki bir xil sohalar — devor, osmon, bir rangli detal. Nuqta topilmaydi.
- Takrorlanuvchi naqsh — g'isht devor, panjara, shaxmat taxtasi. Nuqtalar bor, lekin hammasi bir-biriga o'xshaydi; ratio test ularni tashlab yuboradi.
- Kuchli xiralik — 7-bobdagi blur. Burchaklar yo'qoladi.
- Egiluvchan obyekt — mato, odam yuzi mimikasi. Homografiya faqat tekis obyekt uchun to'g'ri.
Shuning uchun kalit nuqtalar eng yaxshi ishlaydigan joylar: kitob muqovasi, plakat, logotip, hujjat, qurilma paneli — ya'ni tekis va naqshli yuzalar.
Xulosa
| Tushuncha | Asosiy fikr |
|---|---|
| Kalit nuqta | Burchak yoki o'ziga xos joy; osmonda 1000 pikselga 0.5, binoda 11 ta |
| Deskriptor | Nuqta atrofining raqamli "barmoq izi" (ORB: 32 bayt) |
NORM_HAMMING |
Ikkilik deskriptorlar uchun; SIFT ga NORM_L2 |
crossCheck |
Ikki tomonlama tekshiruv, soxta juftlarni kamaytiradi |
| Ratio test | a.distance < 0.75 * b.distance — Lowe ning standart qoidasi |
findHomography + RANSAC |
Juftlardan obyekt joyini topadi; bu bobda xato 0.5–1 piksel |
| Ichkilar (inliers) | Ishonch o'lchovi: 10 tadan kam bo'lsa natijaga ishonmang |
| Burilishga chidam | 90° da ham 117 ichki juft (matchTemplate balli 0.169) |
| Tezlik | ORB 9 ms, SIFT 79 ms — videoda farq hal qiluvchi |
| Cheklov | Tekis obyekt uchun; takrorlanuvchi naqshda ishlamaydi |
Talaba ishi
rationi 0.6 dan 0.9 gacha o'zgartirib, ichkilar sonini (juftlar sonini emas) grafikda chizing. Eng ko'p ichki qaysi qiymatda? Lowe ning 0.75 i shu rasm uchun ham optimalmi?hujjat.jpgvahujjat_toza.pngni juftlang va homografiyani toping. U 6-bobda qo'lda yozgangetPerspectiveTransformmatritsasiga qanchalik yaqin?- Rasmga turli darajada Gauss blur qo'llab (yadro 3, 7, 15, 31), har safar ORB nuqtalari sonini va ichkilarni o'lchang. Qaysi xiralikdan keyin usul buziladi?
shakllar.pngni o'zi bilan juftlang, lekin biriga tuz-qalampir shovqinini qo'shing. Ichkilar soni qancha tushdi? Median filtr yordam beradimi?
Bu bobni bepul o'qidingiz.
Kitob o'rgatadi, lekin sizning kodingiz nega ishlamaganini ayta olmaydi. Kursda uy vazifangiz tekshiriladi.