Vylepšená animace kontur

... Petr Blahoš, 5. 1. 2018 ComputerVision Python

V příspěvku Z jedné kontury na druhou jsme si ukázali, jak snadno udělat plynulý přechod z jedné kontury na druhou pomocí lineární interpolace. Asi jste si všimli, že jsme pracovali pouze s vnější obálkou animovaného obrazce, takže třeba osmička vypadala divně. Dnes to zkusíme tak, aby se nám přelévaly celé obrazce, včetně vnitřních kontur.

Tak tedy: minule jsme vzali vnější konturu zdrojového obrazce, přetransformovali jsme ji do vnější kontury cílového obrazce. Dnes rozhodíme kontury na jednotlivé úsečky, už je nebudeme držet pohromadě, a prostě uděláme tu interpolaci úsečku po úsečce.

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

Jak na to

K nalezení kontur nám opět bude sloužit cv2.findContours. Každá kontura je seznam bodů. My vezmeme jednu konturu po druhé, a přetvoříme je na seznam úseček. Funkce get_line_list nejprve detekuje kontury, a poté vezme jednu po druhé, a vrátí její úsečky. Pozor, nesmíme zapomenout spojit první bod s tím posledním. Kromě toho funkce volitelně nalezené kontury zjednoduší. Kolo pak není úplně kulaté, ale zase nebudeme mít 1000 úseček, ale třeba jenom 200.

def get_line_list(img, height, simplified):
    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_LIST,
                            cv2.CHAIN_APPROX_SIMPLE)[1]
    ret = []
    for cnt in cnts:
        if simplified:
            cnt = cv2.approxPolyDP(cnt, height/200, True)
        for i in range(len(cnt)-1):
            ret.append((cnt[i][0], cnt[i+1][0]))
        ret.append((cnt[len(cnt)-1][0], cnt[0][0]))
    return np.array(ret)

Druhá úprava pak bude srovnání velikostí těch seznamů úseček. Když jsme jeli na konturách, tak jsme úsečky v konturách dělili napůl. Teď to uděláme úplně jednoduše - když nám bude úsečka chybět, tak nějakou zduplikujeme. Tahle funkce není úplně jednoduchá, komentáře snad pomůžou:

def enlarge_line_list(c1, point_count):
    """
    :param c1: Seznam úseček, který potřebujeme rozšířit.
    :param point_count: Cílový počet úseček.
    """
    # Počet úseček v c1:
    src_points_count = c1.shape[0]
    # Počet, kolikrát musíme každou úsečku opakovat:
    smaller = point_count // src_points_count
    # Naplníme pole číslem smaller:
    rep_schema = np.full([src_points_count], smaller)
    # Jenže to musíme ještě přidat. úsečky.
    rep_schema[0:src_points_count-(smaller+1)*src_points_count+point_count] = smaller+1
    # Vygenerujeme nové pole, ve kterém jsou prvky opakovány podle
    # schématu rep_schema.
    return np.repeat(c1, rep_schema, axis=0)

Nakonec to dáme všechno dohromady. Funkce get_interpolated_points, determine_font_scale a gen_char známe z minula.

def interpolate_shapes(img_size, i0, i1):
    SIMPLIFIED = True
    SHUFFLED = False

    c1 = get_line_list(i0, img_size[1], simplified=SIMPLIFIED)
    c2 = get_line_list(i1, img_size[1], simplified=SIMPLIFIED)

    if c1.shape[0] < c2.shape[0]:
        c1 = enlarge_line_list(c1, c2.shape[0])
    elif c2.shape[0] < c1.shape[0]:
        c2 = enlarge_line_list(c2, c1.shape[0])

    if SHUFFLED:
        np.random.shuffle(c2)

    SCALE = 100
    for j in range(SCALE + 1):
        img = np.zeros((img_size[0], img_size[1], 3), dtype=np.uint8)
        cc = get_interpolated_points(c1, c2, j, SCALE)
        for i in cc:
            cv2.line(img, tuple(i[0]), tuple(i[1]), (255, 255, 0), 2)

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


if "__main__" == __name__:
    img_size = (400, 400)
    (scale, baseline) = determine_font_scale(img_size)
    i0 = gen_char(img_size, '-', scale, baseline)
    for i in "abcdefghijklmnopqrstuvwxyz":
        i = ord(i)
        img = gen_char(img_size, chr(i), scale, baseline)
        out_img = interpolate_shapes(img_size, i0, img)
        i0 = img
        cv2.waitKey(300)

Všimněte si proměnných SIMPLIFIED a SHUFFLED ve funkci interpolate_shapes, a zkuste si, co to udělá, když změníte jejich hodnotu.

A pro ty, komu se nechce psát tu mám jeden gist.

Závěr

Příště se podíváme na ještě trošku jiný způsob, jak na sebe kontury namapovat. Dnes jsme viděli na jedné straně zajímavý efekt, když jsme si výslednou konturu promíchali, a na druhé straně nám celkem pěkně přecházeli mezi sebou jednotlivé křivky, když jsme je nepromíchali. Přesto se tam občas objevila trhlina, a té se příště pokusíme zbavit.

Chcete Javascriptovou verzi pro HTML Canvas? Napište do komentářů.