Skip navigation

Leíró hasonlóságon alapuló párosítás

Előismeret

Előzőleg láttuk, hogy önmagában a jellemzőpontok elhelyezkedése kevés információt hordoz a hatékony párosításhoz. Jelentősen javul a hatékonyság, amennyiben az alábbi lépéseket hajtjuk végre.

  • Detektáljunk kulcspontokat és hozzájuk tartozó leírókat mindkét képen.
  • Menjünk végig az egyik kép pontlistáján, és válasszuk azt a pontot potenciális párjának, amelynek a leírója a leghasonlóbb a sajátjához.
  • Az így előálló pontpár listából válasszunk modell konfigurációt és a kimaradókkal teszteljük a modellt. Célszerű RANSAC algoritmust használni.

A potenciális párokon további szűréseket végezhetünk az alábbiak szerint.

  • Végezzünk keresztvalidációt. Vagyis fordított irányba is hajtsuk végre a párosítást, és csak azokat a párokat hagyjuk meg, amelyekre kölcsönösen egymás legközelebbi párjai.
  • Csak azokat a párokat hagyjuk meg, amelyekre a leírók távolsága egy küszöbérték alatt van. Ezt rádiusz szűrések is nevezik.
  • Ne 1, hanem több, általában 2 legközelebbi pontot is rendeljünk a pontokhoz. Ebben az esetben végezhetünk távolság-arány (más néven Lowe) szűrést. Csak azokat a párokat hagyjuk meg, ahol a legközelebbi leíró távolsága jóval közelebb van a másodiknál. Vagyis a kulcspont képi környezete egyedi. Nagyon hasonló leíró távolságok fordulnak elő például a képen található ismétlődő képrészleteknél. (Például épület homlokzatokon ablakok, stb.)

Az így szűrt adatokra hajtsuk végre a RANSAC szűrést.

Térben elhelyezkedő síkokról készült fotók illesztése

Hasznos feladat 2D sík mintázatok térbeli vetítéseinek torzulását korrigálni automatikus pontpárosító módszerrel. Az alap algoritmust az ocv_pair_homography_crosscheck.py példaprogram mutatja be.

A szükséges importálások.

#
# https://docs.opencv.org/4.0.1/db/d70/tutorial_akaze_matching.html
#

import numpy as np
import cv2
from matplotlib import pyplot as plt

Az algoritmus működését globális változókkal szabályozzuk. A METHOD 'AKAZE' vagy 'ORB' lehet. A MIN_MATCH_COUNT azt mondja meg, hogy a RANSAC hívás előtt minimálisan hány pontpárra van szükség. Ennél kevesebb esetén feladjuk a párosítást. A CROSS_CHECK a kereszt-validáció, a RADIUS_MATCH a távolság alapú szűrés kapcsolója, bool típusú értékkel.

METHOD: str = 'AKAZE'
MIN_MATCH_COUNT: int = 10
EDGE_OVERLAY: bool = False
CROSS_CHECK: bool = True
RADIUS_MATCH: bool = True

Ha van, a rádiusz szűrés értéke 50.

radius_thresh: int = 50

Be- és kimeneti fájlnevek definiálása, képek beolvasása

img1Name = 'clock2_0367_rs.jpg'
img2Name = 'clock2_0363_rs.jpg'
resultName = 'clock2_0367_0363_reg.jpg'

out_base_name = 'clocks_1nn_{0}'.format(METHOD)
if CROSS_CHECK:
    out_base_name = out_base_name + '_crosscheck'
if RADIUS_MATCH:
    out_base_name = out_base_name + '_radius{:03d}'.format(radius_thresh)

img1 = cv2.imread(img1Name, 0) # baseImage
img2 = cv2.imread(img2Name, 0) # queryImage
img2_gr_bgr = cv2.merge((img2, img2, img2))

Kulcspont detektor kinyerő objektum létrehozása, kulcspont és leíró számítása mindkét képre egymástól függetlenül.

# Initiate detector
if METHOD == 'AKAZE':
    kpextract = cv2.AKAZE_create()
else:
    kpextract = cv2.ORB_create()

# Find the keypoints and descriptors
kp1, des1 = kpextract.detectAndCompute(img1, None)
kp2, des2 = kpextract.detectAndCompute(img2, None)

print('Keypoints in img1:', len(kp1))
print('Keypoints in img2:', len(kp2))
img1kp1 = cv2.drawKeypoints(img1, kp1, None, (255, 0, 0), 4)
img2kp2 = cv2.drawKeypoints(img2, kp2, None, (255, 0, 0), 4)
cv2.imshow('img1kp1', img1kp1)
cv2.imshow('img2kp2', img2kp2)
cv2.imwrite('{0}_img1kp1.png'.format(out_base_name), img1kp1)
cv2.imwrite('{0}_img2kp2.png'.format(out_base_name), img2kp2)
cv2.waitKey(0)

Leíró hasonlóságon alapuló párosítás. A cv2.BFMatcher() függvény egy összes esetet kiértékelő (brute force) objektumot hoz létre. Meg kell adni a leírók összehasonlításának módszerét. Ez Euklidészi vagy Hamming-jellegű távolság lehet, attól függően, hogy a leíró vektorok milyen jellegű információt hordoznak. Klasszikus, számértékeket tartalmazó leírók például a SIFT és a SURF. Az AKAZE és ORB detektorok bináris leíróvektorokat használnak, ebben az esetben az eltérő bitpozíciók száma adja a távolságot. Második paraméterként megadhatjuk, végezzen-e a párosító kereszt-validációt.

# Matching based on descriptor similarity
# HAMMING should be used for binary descriptors (ORB, AKAZE, ...)
# NORM_L2 (Euclidean distance) can be used for non-binary descriptors (SIFT, SURF, ...)
matcher = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=CROSS_CHECK)

Ha rádiusz szűrés kezelését megvalósíthatjuk az alábbi módon. A good_match lista fogja a szűrt eredményt tárolni. A példában megmutatjuk azt is, hogyan lehet a Match objektum adataihoz hozzáférni. A queryIdx és trainIdx a két kulcspontlista indexét adja a párra, a distance a két leíró távolságát.

if RADIUS_MATCH:
    radius_match = matcher.radiusMatch(des1, des2, radius_thresh)
    good_match = []
    for m in radius_match:
        if m:
            print(m[0].queryIdx, m[0].trainIdx, m[0].distance)
            good_match.append(m[0])
        else:
            good_match = matcher.match(des1, des2)

Fontos megérteni, hogy ezek előzetes, potenciális párok csak! A kulcspontok egyméshoz képesti geometriai elhelyezkedése nem került még figyelembe vételre! Megjelenítjük a potenciális párokat. A cv2.drawMatches() végzi a vizualizációt. A működését számos paraméterrel szabályozhatjuk, amit célszerű egy Python dict objektumba kigyűjteni. A függvény egymás mellett jeleníti meg a két képet, és összeköti a potenciális párokat. Megadhatjuk, hogy a párosítatlan pontok kirajzolásra kerüljenek-e (SINGLE_POINT)?

# Visualize matches
print("Potential good matches found:", len(good_match))
draw_params = dict(matchColor=(255, 0, 0), # draw matches in blue color
    singlePointColor=(0, 0, 255), # draw non-matched points in red
    matchesMask=None, # draw everything
    flags=cv2.DrawMatchesFlags_DEFAULT)
img5 = cv2.drawMatches(img1, kp1, img2, kp2, good_match, None, **draw_params)
cv2.imshow('Crosscheck: Potential good matches', img5)
cv2.imwrite('{0}_pgm.png'.format(out_base_name), img5)
cv2.waitKey(0)

Amennyiben van elegendő számú potenciális pontpárunk, RANSAC segítségével kiszűrjük a geometriailag nem korrekt párokat. A pár listából az OpenCV-nek megfelelő src_pts és dst_pts struktúrákat állítjuk elő.

# Filter outlier matches using RANSAC assuming homography between objects
if len(good_match) > MIN_MATCH_COUNT:
    src_pts = np.float32([kp1[m.queryIdx].pt for m in good_match]).reshape(-1, 1, 2)
    dst_pts = np.float32([kp2[m.trainIdx].pt for m in good_match]).reshape(-1, 1, 2)

A pontpár halmazokra végrehajtuk a RANSAC algoritmust, közöttük síkhomográfiát feltételezve. Fontos megérteni, hogy ez a modell arra alkalmas, hogy térben elhelyezett sík területek közötti viszonyt írja le, tetszőleges térbeli színtér kulcspontjaira nem fog jól működni! Ha ilyen jellegű geometriai szűrést szeretnénk végezni, akkor az epipoláris geometriai kényszert kell alkalmaznuk. Ezzel az utolsó témakörben fogunk foglalkozni. Eredményként megkapjuk a a szűrt ponthalmazok között ható síkhomográfia transzformációt, és egy maszk tömböt, amely 0 és 1 értékekkel jelzi, hogy az adott pontpár-index megtartásra vagy kiszűrésre került.

    # Find homography with outlier filtering
    M, mask = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0)
    print('Matching keypoints after RANSAC:', np.count_nonzero(mask))
    outliersMask = 1 - mask
    matchesMask = mask.ravel().tolist()
    matchesOutliersMask = outliersMask.ravel().tolist()

A síkhomográfiát végrehajtuk az img1 képre és a négy sarokpontjára. Ez utóbbi segítségével tudjuk az img1 kép transzformált körvonalát az img2 képbe rajzolni majd.

    # Perspective transformation of img1 and its corner points
    h, w = img1.shape[:2]
    h2, w2 = img2.shape[:2]
    img1_persp = cv2.warpPerspective(img1, M, (w2, h2))
    cv2.imwrite(resultName, img1_persp)
    # 4 corner point coordinates of img1
    pts = np.float32([[0, 0], [0, h - 1], [w - 1, h - 1], [w - 1, 0]]).reshape(-1, 1, 2)
    # Transformed to img2
    pts_persp = cv2.perspectiveTransform(pts, M)

Előállítjuk az illesztett képek vörös-zöld fúzióját.

    # Blend registered images
    if EDGE_OVERLAY:
        img1_persp = cv2.Canny(img1_persp, 100, 300)

    image_blend = cv2.merge((np.zeros((h2, w2), np.uint8), img2, img1_persp))

Berajzoljuk a transzformált  img1 határvonalat sárga színnel az img2 képbe.

    # Draw bounding rectangle of img1 over img2
    img2 = cv2.polylines(img2_gr_bgr, [np.int32(pts_persp)], True, (0, 255, 255), 3, cv2.LINE_AA)

Inlier és outlier párosítások, és a fúzionált eredmény megjelenítése.

    # Draw keypoint matches
    draw_params = dict(matchColor=(0, 255, 255), # draw outlier matches in yellow color
        singlePointColor=(0, 0, 255),
        matchesMask=matchesOutliersMask, # draw only outliers
        flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS)
     img3 = cv2.drawMatches(img1, kp1, img2, kp2, good_match, None, **draw_params)
    cv2.imshow('Outlier matches', img3)
    cv2.imwrite('{0}_outm.png'.format(out_base_name), img3)
    cv2.waitKey(0)

    draw_params = dict(matchColor=(0, 255, 0), # draw matches in green color
    singlePointColor=(0, 0, 255),
    matchesMask=matchesMask, # draw only inliers
    flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS)
    img4 = cv2.drawMatches(img1, kp1, img2, kp2, good_match, None, **draw_params)
    cv2.imshow('Inlier matches', img4)
    cv2.imwrite('{0}_inm.png'.format(out_base_name), img4)
    cv2.waitKey(0)

    cv2.imshow('Blended result', image_blend)
    cv2.imwrite('{0}_blend.png'.format(out_base_name), image_blend)

Ha nincs elegendő pont a RANSAC-hoz, akkor üzenetet írunk a konzolra.

else:
    print("Not enough matches are found - %d/%d" % (len(good_match), MIN_MATCH_COUNT))
    matchesMask = None

Program lezárása.

cv2.waitKey(0)
cv2.destroyAllWindows()

Eredmények

Óra számlapok esetén az alábbi eredményeket kapjuk.

AKAZE kulcspontok a két képen:

 

Potenciális párok a kereszt-validáció és az 50 értékű rádiusz szűrés után. A pirosak a kiszűrt és párosítatlan pontok. Jól látható, hogy a potenciális párok között még több hibás is előfordul.

A RANSAC által kiszűrt, a síkhomográfiás geometriai kényszert nem teljesítő párok:

Az inlier párok. Sárga színnel látjuk a bal oldali kép transzformált határát berajzolva a jobb oldali képre.

Az inlier párok alapján számított síkhomográfia alkalmazásával előállítot vörös-zöld fúzionált kép jó illeszkedést mutat. Az eltérő mutató állások nem zavarják meg a módszert.

Feladat

  • Teszteljük a példaprogramot többféle paraméterezéssel!
  • Figyeljük meg, hogyan alakulnak a pontpár számok az egyes lépések után!

Két legközelebbi pár választása

A párosító algoritmust paraméterezhetjük úgy, hogy az első kép minden kulcspontjához a hozzá legközelebbi kettőt adja meg a másik képről. Így lehetőségünk van annak vizsgálatára, hogy az adott pontpár mennyire egyedi. Ha ugyanis a második legközelebbi leíró távolsága jóval nagyobb az elsőnél, akkor ez teljesül. Közel egyforma távolságú leírók esetén egy minta többszöri előfordulása lehetséges, ezért célszerű figyelmen kívül hagyni.

A programunkban pár apróbb módosítást kell csak eszközölnünk. A végeredményt az ocv_pair_homography_knn.py példaprogramban látjuk.

Több legközelebbi pár keresése a matcher.knnMatch() függvénnyel lehetséges. Az utolsó paraméter a kért párok számát adja.

matcher = cv2.BFMatcher(cv2.NORM_HAMMING)
matches = matcher.knnMatch(des1, des2, 2)

Távolság-arány alapú szűrés lehetséges megvalósítása:

good_match = []
match_ratio = 0.7
for m, n in matches:
if m.distance < match_ratio * n.distance:
    good_match.append(m)

A többi rész érdemben változatlan.

FLANN k-nn párosító

Az előző módszereink az összes lehetséges kulcspont párosítást vizsgálták. Bár ez pár ezer pontig is hatékony maradhat, különösen valós idejű alkalmazások esetén célszerű "okosabb" keresőt használni. Erre alkalmas az OpenCV-ben elérhető FLANN (Fast Library for Approximate Nearest Neighbors) módszer. Az ocv_pair_homography_flann.py ezt használja. A főbb eltérések az alábbiak a korábbi kódhoz képest.

Indexelő és kereső paramétereket kell átadnunk. Figyeljünk arra, hogy az indexelő paraméterezés eltér a bináris és nem bináris leírók esetén! Az AKAZE és az ORB bináris leírók, ezért a kód a hozzájuk tartozót mutatja. Kommentár blokkban láthatjuk a SIFT vagy SURF esetén használandót. A flann.knnMatch() hívás eredményét ugyanúgy felhasználhatjuk, mint a BF párosító knn eredményét.

# FLANN parameters for non-binary descriptors (SIFT, SURF, ...)
# FLANN_INDEX_KDTREE = 1
# index_params = dict(algorithm = FLANN_INDEX_KDTREE, trees = 5)

# FLANN parameters for binary descriptors (ORB, AKAZE, ...)
FLANN_INDEX_LSH = 6
index_params= dict(algorithm=FLANN_INDEX_LSH,
    table_number=6, # 12
    key_size=12, # 20
    multi_probe_level=1) #2
search_params = dict(checks=50) # or pass empty dictionary

flann = cv2.FlannBasedMatcher(index_params, search_params)
matches = flann.knnMatch(des1, des2, k=2)

Sík objektum előfordulásának keresése 3D színtérről készült fotón

Az előző példaprogramok arra is használhatók, hogy megkeressünk egy 2D sík objektumot egy 3D színtérben.

Első képként tekintsük az alábbi könyvborító 2D szkennelt képét (cv_book_cover.png).

Keressük ennek előfordulását az alábbi fotókon (cv_book_scene1.jpg és cv_book_scene2.jpg).

Az AKAZE 2-nn arányellenőrzéses módszer eredménye az alábbi.

Potenciális pontpárok.

RANSAC által kiszűrt hibás párok. Sárga körvonal jelzi a transzformált könyvborító határát.

Inlier párok.

Vörös körvonal rávetítéses eredmény:

Sík objektum keresése videófolyamon

Az objektumkeresés működésést, stabilitását jól szemlélteti, ha videófolyam képkockáin hajtjuk végre. Az ocv_pair_homography_crosscheck_video.py és ocv_pair_homography_flann_video.py példaprogramok a cv_book_scene.mp4 videófolyam képkockáin mennek végig, és mindegyiken egy könyv borítóját keresik leíró alapú hasonlóság kereséssel.

Megjegyezzük, hogy minden képkockára az előzőektől teljesen függetlenül történik a keresés! A gyakorlatban ilyen feladat esetén célszerű úgynevezett követést (tracking) alkalmazni, ami az előző képkockán detektált pontokat próbálja keresni a következőn, azok közelében. Feltételezve, hogy a videó képkockái között kicsi a geometriai eltérés.

Futtassuk többféle paraméterezéssel a programokat! Néhány eredmény videó elérhető az alábbi linkeken.