Z jedné kontury na druhou

... Petr Blahoš, 20. 12. 2017 ComputerVision Python

Dnes si ukážeme takovou legrácku. Vezmeme konturu, a přetranformujeme ji na jinou. Třeba takhle:

Animace čísel

Python a OpenCV už máme, tak směle do toho.

Jak na to

cv2.findContours vrátí pole kontur nalezených v obrázku, každá kontura je numpy array s polem bodů. Takže když budeme mít dvě takové kontury, tak jen namapujeme body první kontury na druhou, a budeme mezi nimi interpolovat. Takže je to jasné, a můžeme se dát do práce. Nejprve si to zkusíme s ručně vyrobenými konturami. Skutečné obrázky přijdou na řadu potom.

Jednoduchý případ

V první řadě si vyrobíme dvě kontury, a zkusíme interpolovat mezi nimi. Začneme tím, že si nadefinujeme čáry, a nakreslíme si je do obrázku. Jen tak abychom viděli, co děláme.

import cv2
import numpy as np

def main():
    c1 = np.array([[[100, 0]], [[100, 100]], [[100, 199]]], dtype=np.int32)
    c2 = np.array([[[0, 100]], [[100, 100]], [[199, 100]]], dtype=np.int32)

    img = np.zeros((200, 200, 3), dtype=np.uint8)

    cv2.drawContours(img, [c1], -1, (255, 0, 0), 2)
    cv2.drawContours(img, [c2], -1, (0, 255, 0), 2)

    cv2.imshow("A", img)
    cv2.waitKey(0)


if "__main__" == __name__:
    main()

Čáry připravené k interpolaci

Začínáme jednoduše, takže máme dvě kontury se stejným počtem bodů, vezmeme je v tom pořadí, v jakém jsou, a budeme mezi nimi interpolovat:

def get_interpolated_points(c1, c2, step, scale):
    dif = c2 - c1
    return np.array(c1 + dif*step/scale, dtype=np.int32)


def main():
    c1 = np.array([[[100, 0]], [[100, 100]], [[100, 199]]], dtype=np.int32)
    c2 = np.array([[[0, 100]], [[100, 100]], [[199, 100]]], dtype=np.int32)

    for i in range(101):
        moving = get_interpolated_points(c1, c2, i, 100)
        img = np.zeros((200, 200, 3), dtype=np.uint8)

        cv2.drawContours(img, [c1], -1, (255, 0, 0), 2)
        cv2.drawContours(img, [c2], -1, (0, 255, 0), 2)
        cv2.drawContours(img, [moving], -1, (0, 0, 255), 5)

        cv2.imshow("A", img)
        cv2.waitKey(5)

    cv2.waitKey(0)

Jednoduchá interpolace

Všimněte si funkce get_interpolated_points. V ní se chceme dostat na čáře mezi prvním a druhým bodem na nějaké místo (krok step ze scale kroků). Paramerty pro tuto funkci jsou numpy arrays, a operace na array (v tomto případě sčítání, odčítání a násobení) se provedou na každém prvku pole. Takže neinterpolujeme jeden bod po druhém, to za nás udělá numpy.

Jen tak mimochodem, co se stane, když si ty body namapujeme jinak? Třeba zkuste:

    c1 = np.array([[[100, 0]], [[100, 100]], [[100, 199]]], dtype=np.int32)
    c2 = np.array([[[100, 100]], [[0, 100]], [[199, 100]]], dtype=np.int32)

Nebo tohle:

    c1 = np.array([ [[random.randint(0, 199), random.randint(0, 199)]] for i in range(5)], dtype=np.int32)
    c2 = np.array([ [[random.randint(0, 199), random.randint(0, 199)]] for i in range(5)], dtype=np.int32)

Nebo si zkuste interpolovat mezi čtvercem a otočeným čtvercem.

Různě dlouhé kontury

Určitě jste si všimli, že obě kontury máme stejně dlouhé. Co když nejsou? Vždycky musíme mít pokrytou celou tu finální konturu. Jinak to bude vypadat divně. Když je ta první kratší, můžeme buď z té druhé ubrat, nebo do té první přidat.

Navíc, jak asi tušíte, křivky se nám rozpadnou do docela velkého množství bodů (proto používáme cv2.approxPolyDP na zjednodušení kontur).

Já jsem se rozhodl jít tou cestou, že vždycky tu kratší konturu prodloužím (kratší ve smyslu složenou z menšího počtu bodů). Tím pádem bude celá ta transformace plynulejší a přesnější. Prodlužuju jednoduchým způsobem, tak, že si seřadím vzdálenosti mezi jednotlivými body sestupně, a mezi ty nejvzdálenější vkládám nové body.

import scipy.spatial.distance as distance

def enlarge_contour(c1, point_count):
    to_add = point_count - c1.shape[0]
    if to_add <= 0:
        return c1

    # Kontury vrácené cv.findContours jsou [ [[x,y]], [[x,y]], [[x,y]], ... ]
    # ale my chceme [ [x,y], [x,y], [x,y], ... ]
    c1 = c1.reshape((c1.shape[0], c1.shape[-1]))

    # Spočítáme vzdálenosti mezi jednotlivými body tak,
    # jak jdou po sobě, očíslujeme a seřadíme
    dists = []
    for (idx, i) in enumerate(c1[1:]):
        dists.append((distance.euclidean(c1[idx], i), idx))
    dists.sort(reverse=1)

    # Připravíme si body, které potom přidáme do kontury.
    # Nejvýše ale do první poloviny pole vzdáleností.
    add_idxs = []
    add_items = []
    for i in range(max(1, min(len(dists)//2, to_add))):
        i0 = dists[i][1]
        new_pt = c1[i0] + (c1[i0+1] - c1[i0])/2
        add_idxs.append(i0+1)
        add_items.append(new_pt)

    # Vložíme spočítané body (díky numpy, vymysleli Tě dobře)
    c1 = np.insert(c1, add_idxs, add_items, 0)

    # Možná ještě nemáme dost bodů, tak to zkusíme ještě jednou.
    return enlarge_contour(c1, point_count)

Když to teď zkusíme použít, mohlo by to vypadat třeba takto:

Přidání bodů do jedné z kontur

def main():
    c1 = np.array([[[0, 90]], [[0, 110]]], dtype=np.int32)
    c2 = np.array([[[100, 0]], [[150, 90]], [[199, 0]], [[199, 199]],
                   [[150, 110]], [[100, 199]]], dtype=np.int32)

    if c1.shape[0] < c2.shape[0]:
        c1 = enlarge_contour(c1, c2.shape[0])
    else:
        c2 = enlarge_contour(c2, c1.shape[0])

    for i in range(101):
        moving = get_interpolated_points(c1, c2, i, 100)
        img = np.zeros((200, 200, 3), dtype=np.uint8)

        cv2.drawContours(img, [c1], -1, (255, 0, 0), 2)
        cv2.drawContours(img, [c2], -1, (0, 255, 0), 2)
        cv2.drawContours(img, [moving], -1, (0, 0, 255), 5)

        cv2.imshow("A", img)
        cv2.waitKey(5)

Čísla a písmenka

Už víme, jak animovat jednu konturu na druhou. Kde ale vzít kontury? Prostě si je nakreslíme.

def determine_font_scale(img_size):
    scale = 1.0
    for i in range(10):
        (sz, baseline) = cv2.getTextSize("M", cv2.FONT_HERSHEY_SIMPLEX, scale, 4)
        print(scale, sz, baseline)
        if sz[1] + baseline < img_size[0]*0.9:
            scale *= (img_size[0]*0.9)/(sz[1]+baseline)
        elif sz[1] + baseline > img_size[0]*0.95:
            scale *= (img_size[0]*0.9)/(sz[1]+baseline)
        else:
            break
    return (scale, baseline)


def gen_char(img_size, c, scale, baseline):
    img = np.zeros(img_size, dtype=np.uint8)
    (sz, _baseline) = cv2.getTextSize(c, cv2.FONT_HERSHEY_SIMPLEX,
                                      scale, 4)

    cv2.putText(img, c, ((img_size[1]-sz[0])//2, img_size[0] - baseline),
                cv2.FONT_HERSHEY_SIMPLEX, scale, 40, cv2.LINE_AA)
    return img


img_size = (300, 300)
(scale, baseline) = determine_font_scale(img_size)
img = gen_char(img_size, "a", scale, baseline)

Nepodařilo se mi zjistit, jaký má vztah parametr fontScale pro putText, proto si musíme nejprve změřit, jak vysoké je písmenko, a podle toho spočítat hodnotu pro parametr fontScale. Pak při kreslení znaku musíme zjistit, jak je široký, abychom ho dokázali dát doprostřed.

Z takového obrázku pak získáme konturu, a můžeme vesele interpolovat.

def get_contour(img, height):
    # Aby nám to fungovalo s barevnými i s odstíny šedi.
    if 3 == len(img.shape):
        img = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
    thr = cv2.threshold(img, 0, 255,
                        cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
    cnts = cv2.findContours(thr, cv2.RETR_TREE,
                            cv2.CHAIN_APPROX_SIMPLE)[1]

    # Vezmeme největší konturu
    for (idx, i) in enumerate(sorted(cnts, key=cv2.contourArea, reverse=1)):
    # Zjednodušíme
        return cv2.approxPolyDP(i, height/200, True)
    return None

Na ukázku to dáme celé dohromady:

def interpolate_shapes(img_size, i0, i1):
    out_shape = img_size

    c1 = get_contour(i0, out_shape[0])
    c2 = get_contour(i1, out_shape[0])

    c1 = c1.reshape(c1.shape[0], c1.shape[-1])
    c2 = c2.reshape(c2.shape[0], c2.shape[-1])

    if c1.shape[0] < c2.shape[0]:
        c1 = enlarge_contour(c1, c2.shape[0])
    else:
        c2 = enlarge_contour(c2, c1.shape[0])

    c_fin = c2

    SCALE = 50
    for i in range(SCALE + 1):
        c2 = get_interpolated_points(c1, c_fin, i, SCALE)
        img = np.zeros((out_shape[0], out_shape[1]), dtype=np.uint8)

        cv2.drawContours(img, [c2], -1, 255, 50)

        cv2.imshow("A", img)
        cv2.waitKey(1)


if "__main__" == __name__:
    img_size = (600, 600)
    (scale, baseline) = determine_font_scale(img_size)
    i0 = gen_char(img_size, 'A', scale, baseline)
    i1 = gen_char(img_size, 'O', scale, baseline)
    interpolate_shapes(img_size, i0, i1)

Nebo pozdravte kamaráda gista.

Z jednoho písmena na druhé

Na závěr jeden efekt

petr.blahos.com animace