Posunout, otočit

... Petr Blahoš, 21. 9. 2017 ComputerVision Python

Kamarád má takový sen, že vyrobí obecný obráběcí stroj, do kterého najede kus, stroj si detekuje, v jaké pozici ten kus je oproti referenční pozici, kus posune a otočí, a pak na něm provede práci. Případně nehýbe s kusem, ale upraví posun a otočení v programu. Tak se na to podíváme.

Jak na to

OpenCV disponuje silným aparátem pro práci s konturami. Předpokládejme tedy, že jsme schopni tento tvar nějak rozpoznat a uložit. Pro porovnávané objekty pak také rozpoznáme tvar. Jím budeme posouvat a otáčet, dokud se tvary nebudou shodovat. Otáčet ovšem nebudeme úplně tupě. Začneme tím, že si zjistíme minimální obdélník, do kterého se objekt vleze. Totéž uděláme i s porovnávaným objektem. Tím zjistíme střed, a jsme schopni posunout objekty na sebe, neboli zjistit, o kolik musíme posunout porovnávaný objekt, aby jeho střed ležel na středu referenčního objektu. Zároveň zjistíme, o kolik jsou oproti sobě ty dva obdélníky otočené. Ovšem, jak známo, obdélník je obdélník. Má většinou 2 strany stejně dlouhé, a ty druhé 2 strany taky. No a když je ten obdélník náhodou čtverec, tak je to ještě trochu horší. Takže víme, o kolik otočit, ale možná je to o 90°, 180° nebo 270° jinak. Zde opět nastoupí původní kontury, které si budeme muset ve všech těch otočeních porovnat, a vybrat nejlepší.

Nechci Vám to naservírovat celé. Jestli máte zájem něco se naučit, musíte na tom hlavně pracovat sami. Předpokládám tedy, že si sami zjistíte, jak nainstalovat OpenCV, jak nahrát obrázek, případně jak pracovat s videem, jak zobrazit obrázek, vykreslit do něj konturu. Docela dobrý zdroj je blog pyimagesearch.com.

Detekce kontury

Začneme trochu oklikou - černobílým obrázkem, na kterém je černé pozadí, a bílý objekt. Jak dostat z barevného obrázku ten správný černobílý není úplná banalita, a ještě se k tomu vrátíme.

def get_contour_info(thr):
    # thr je náš černo-bílý obrázek
    (_im2, cnts, hierarchy) = cv2.findContours(thr, cv2.RETR_EXTERNAL,
                                              cv2.CHAIN_APPROX_SIMPLE)
    # v cnts jsou pouze externí kontury, ideálně pouze jedna
    # vezmeme tu největší
    outmost = sorted(cnts, key=cv2.contourArea, reverse=True)[0]

    # zjednodušíme konturu
    gamma_0 = 0.005*thr.shape[0]
    counter = 100
    shape_len = 10
    while outmost.shape[0] > shape_len and counter > 0:
        outmost = cv2.approxPolyDP(outmost, gamma_0, True)
        gamma_0 *= 2.0
        counter -= 1

Zjednodušení kontury spočívá v tom, že zjednodušujeme, dokud má kontura více, než 10 bodů. Algoritmus, jakým pracuje funkce approxPolyDP je pěkně popsán v tutoriálu OpenCV. My zde provedeme max. 100 iterací, a doufáme, že to vyjde dobře. Znám i lepší způsob, pravděpodobně se k němu vrátím v nějakém budoucím příspěvku. Na příklady kontur a zjednodušených kontur se podívejte zde. Čísla na obrázku jsou počty bodů kontur. Když se podíváte zblízka, uvidíte, že ta původní, modrá, kontura je dost hrbolatá.

Zjednodušení kontur

Ke zjednodušené kontuře si ještě přidáme minimální obdélník:

    return (outmost, cv2.minAreaRect(outmost))

A pro náš referenční obrázek provedeme:

    calibration_data = get_contour_info(thresholded_image)

Pro obrázky, na nichž detekujeme otočení, uděláme totéž:

    compared_image = get_contour_info(thresholded_image)

Posunout a otočit

Centrum těch našich kontur známe, takže posun bude jednoduchý. Ta základní rotace je taky jednoduchá:

    rect = compared_image[1]
    calib_rect = calibration_data[1]
    # Translation matrix
    trans = np.float32([[1, 0, -(rect[0][0] - calib_rect[0][0])],
                        [0, 1, -(rect[0][1] - calib_rect[0][1])]])

    angle_0 = rect[2] - calib_rect[2]

Pomocí otočení o úhel angle_0 a posunutí podle matice trans tedy položíme objekty na sebe. Pokud jsou ty minimální obdélníky opravdu obdélníky, máme 50% šanci, že je to dobře. Pokud jsou to čtverce, máme pouze 25% šanci. Takže jako poslední krok:

def find_best_rotation(calibration_data, compared_image, trans, basic_angle):
    (calib_shape, calib_rect) = calibration_data
    (shape, rect) = compared_image

    dists = []
    for delta in (0, 90, 180, 270):
        rot = cv2.getRotationMatrix2D(rect[0], basic_angle + delta, 1.0)
        tr = cv2.transform(shape, rot)
        tr = cv2.transform(tr, trans)
        sum_dist = 0
        for a1 in tr.reshape(tr.shape[0], 2):
            row = []
            for a2 in calib_shape.reshape(calib_shape.shape[0], 2):
                row.append(numpy.linalg.norm(a1 - a2))
            sum_dist += min(row)
        dists.append((sum_dist, delta))
    return sorted(dists)[0][1]

Ve kterém postupně zkusíme otočit o rozdílový úhel angle_0 + 0°, 90°, 180° a 270° a posunout. Pak najdeme vzdálenosti nejbližších bodů. To otočení, pro které jsou vzdálenosti nejkratší bude to naše. Není to dokonalé, ale zatím jsem nepřišel na případ, pro který to nefunguje.

Doplníme volání find_best_rotation a máme hotovo:

    rect = compared_image[1]
    calib_rect = calibration_data[1]
    # Translation matrix
    trans = np.float32([[1, 0, -(rect[0][0] - calib_rect[0][0])],
                        [0, 1, -(rect[0][1] - calib_rect[0][1])]])

    angle_0 = rect[2] - calib_rect[2]
    angle_delta = find_best_rotation(calibration_data, compared_image,
                                     trans, angle_0)
    rot = cv2.getRotationMatrix2D(rect[0], angle_0 + angle_delta, 1.0)

    rotated_shape = cv2.transform(compared_image[0], rot)
    final_shape = cv2.transform(rotated_shape, trans)

Druhé video na ukázku:

Potřebujeme světlo

Jak si můžete všimnout na videích, okraje objektů jsou neostré, a objekty vrhají stín. Při oddělení objektu od pozadí se obvykle používá převod do odstínů šedi, a thresholding, česky prahování, převede obrázek na černo-bílý (tedy pouze černá a bílá, žádné odstíny) pomocí hodnot jednotlivých pixelů. Stín se nám při této technice snadno zamíchá do vlastního objektu. Podívejte se:

    img = cv2.imread("thr1.jpg")
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    counter = 0
    for i in range(0, 256, 8):
        thr = cv2.threshold(gray, i, 255, cv2.THRESH_BINARY_INV)[1]
        cv2.imwrite("thr_%03d.jpg" % i, thr)

Takže pokud to myslíte vážně, nepotřebujete jenom kvalitní kameru s dobrým stabilním uchycením a stabilním stolem, ale taky dokonalé osvětlení.

Závěr

V tomto příkladu jsme si ukázali techniku, jak dostat objekt do správné pozice. Neřešíme situaci, kdy se nám pod kameru přimíchá úplně jiný objekt. Algoritmus se jej prostě pokusí umístit, a něco z toho vyjde. Další věc, kterou by bylo potřeba implementovat, je kontrola naváděcích značek. Pokud je objekt obdélníkový nebo čtvercový, tak nelze poznat správné otočení jen podle kontury, a musí nastoupit ještě další techniky.