Asosiy qismga o'tish

Kalit nuqtalar: burilgan va o'lchami o'zgargan obyektni topish

13-bob 8 kod bloki 3216 so'z

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:

  1. Detektor — o'ziga xos nuqtalarni topadi (keypoints).
  2. Deskriptor — har nuqta atrofini raqamlar vektoriga aylantiradi.
  3. Matcher — ikki rasmning vektorlarini juftlaydi.

ORB: nuqtalarni topish

python13 qator
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.")
Chiqish
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.

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:

python6 qator
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()]))
sohao'lchamkalit nuqtalar1000 pikselga
osmon (tekis)300x120180.5
bino (naqshli)300x25082410.99
yer (o'rtacha)400x1203577.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:

python12 qator
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.")
Chiqish
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.

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:

python9 qator
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]
ratioqolgan juftlarulush %
0.624824.8
0.731131.1
0.7535835.8
0.840540.5
0.955255.2
194294.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:

python15 qator
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))]))
Chiqish
juftlar: 358 | RANSAC ichkilari (inliers): 342 (96%)
topilgan homografiya:
 [[  0.692   0.399  39.93 ]
 [ -0.4     0.694 274.085]
 [ -0.     -0.      1.   ]]
burchaktopilganhaqiqiyxato, 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:

python26 qator
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))
burilishORB ichkilarimatchTemplate balli
0°1 0001
5°5880.536
10°4890.43
20°3740.303
45°2270.232
90°1170.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:

python7 qator
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'lchamORB ichkilarimatchTemplate balli
x0.52560.166
x0.74870.373
x1.01 0001
x1.43910.309
x2.02090.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

python24 qator
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))
usulnuqtalardeskriptoryaxshi juftlarichkilarichki ulushi %vaqt, ms
ORB1 00032 x uint835834295.59
SIFT1 001128 x float325375219780
AKAZE1 96761 x uint865759089.837
BRISK6 77064 x uint81 6241 51393.292

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

  1. ratio ni 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?
  2. hujjat.jpg va hujjat_toza.png ni juftlang va homografiyani toping. U 6-bobda qo'lda yozgan getPerspectiveTransform matritsasiga qanchalik yaqin?
  3. 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?
  4. shakllar.png ni 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.

Telegramda yozish Narxlar