Z jedné kontury na druhou
Dnes si ukážeme takovou legrácku. Vezmeme konturu, a přetranformujeme ji na jinou. Třeba takhle:
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()
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)
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:
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.
Na závěr jeden efekt
Komentáře byly zrušeny
V EU teď máme složitou situaci s Cookies. Na komentáře jsem používal jistou službu třetí strany. Ta však používá Cookies poměrně, ehm, benevolentně. Tak jsem se rozhodl komentáře zrušit. Pokud chcete, můžete mi napsat přímo