Matematika + Python + šiek tiek magijos arba meškiuko vertimas Voronoi diagrama

Voronoi diagramos yra gražios

Voronoi diagramos, kaip ir daugelis kitų matematinių dalykų mūsų smegenims yra gražu. Nė kiek neabejoju, kad bandant rasti priežastis yra prirašyta metrai knygų, bet aš čia ne apie tai, aš apie praktinius dalykus.

Kažkada pabandėm lazeriu išsipjauti vieną kitą voronoi diagrama dekoruotą daikčiuką ir šis stilius prilipo. Norėjosi jų daugiau, dar daugiau ir dar šiek tiek. Bet tai velnias, kiek prie jų dizaininimo reikėdavo rankinio darbo. To (foto aukščiau) kalėdinio elniuko nupaišymui užtrukau gal kokias dvi paras. Nebūčiau programuotojas jei tokiam nuobodžiam darbui nerasčiau automatizuoto sprendimo. Tad šiandien apie tai.

Problematika: dekoruojant kokią nors pasirinktą formą, tarkim, mešką norisi, kad meškoje būsianti Voronoi diagrama kiek įmanoma labiau atspindėtų meškos fyčerus – vidinius bei išorinius (ypač išorinius) kontūrus. Taip diagrama atrodo gerokai gyvesnė nei ant tvarkingai ar pusiau tvarkingai (iš rankos) išdėliotų taškų sugeneruota. Todėl ties kontūrais taškų (seeds) reikia daugiau, tose vietose, kuriose nėra fyčerų – mažiau.

Algoritmas: randami paveiksliuko fyčerų pakraščiai (edges), Poison Disc Sampling, algoritmu generuojami taškai (seeds), tose vietose, kuriose yra pakraščiai, taškai generuojami tankiau. Naudojant šiuos taškus suskaičiuojama Voronoi diagrama, iš jos paimamos sektorių viršūnės, naudojant jas suskaičiuojamas mažesni (vidiniai) sektoriai ir išsaugomi SVG faile. Tada šį SVG rankomis jungiu su norimu kontūru ir taip gaunu kontūrą, kurį galiu išpjauti lazeriu.

Didesnę dalį algoritmo žingsnių pavyko atvaizduoti tarpiniais paveiksliukais – procesas labai smagiai žiūrisi:

pirminis paveiksliukas, kurį versiu diagrama

Pirmas žingsnis yra pakraščių radimas, tam pirminį (source) paveiksliuką praleidžiu pro denoise filtrą ir paverčiu į du – greyscale ir Lab color space:







nimg = denoise_tv_bregman(img, weight=1, max_iter=100, eps=0.001, isotropic=True)
img_gray = rgb2gray(nimg)
img_lab = rgb2lab(nimg)
greyscale
lab color

Tada imant abu šiuos paveiksliukus ir naudojant sobel funkciją randami pakraščiai, vidurkinant, apvalinant ir panaudojus šiek tiek trigonometrijos bei dilation funkciją randama pakraščių svorių matrica, kuri ir yra kriterijus skaičiuojant norimą tankumą paveiksliuko vietose:

weighted edges

Turint svorių matricą Poison Disc Sampling algoritmu generuojami taškai:

tree = KDTree(sample_points)

Naudojant taškus sugeneruoju Voronoi diagramą:

vor = Voronoi(vPoints, furthest_site=False, ...)

Sugeneravus diagramą randu joje vidinius sektorius – parametruose turiu nustatymą kiek pixelių reikia “atitraukti” sektorių į sektoriaus centrą – taip skirtingo stiprumo medžiagoms galiu daryti skirtingo storio tinkliuką.

Kad būtų dar šiek tiek gražiau – kiekvieną vidinio sektoriaus viršūnę”randomizuoju”: sektoriaus centroido kryptimi (arba nuo jos) pastumiu nedideliu atsitiktiniu taškų skaičiumi. Šis efektas beveik nepastebimas žiūrint į kiekvieną viršūnę atskirai, bet labai gerai matosi visame paveiksliuke – jis nebėra toks matematiškai tikslkius, gaunu tokį “human touch” efektą, akiai jis malonesnis.

Tiesa, realiame algoritme turiu nustatymus kiek maksimaliai taškų noriu tam tikro dydžio paveiksliukams, todėl kai kurie taškai suvidurkinami. Čia matome taškus iki vidurkinimo ir diagramą sugeneruotą kai kuriuos iš kų suvidurkinus – sektorių mažiau ir jie tvarkingesni.

Belieka rezultatą išsaugoti į SVG failą:

Ir tada jau rankomis (gerai, gerai, iš tikro peliuku) apdoroti iki norimos formos:

(Čia ne meška, patys atspėkit kas)

Po galutinės formos suteikimo turiu dar porą Inkscape mėgiamų filtrų, kurie suteikia dar daugiau “human touch”, bet visų jų principas tas pats – šiek tiek atsitiktinumo a.k.a magijos.

Štai taip smagu kartais grįžti prie gryno matematinio programavimo, kasdienybė darbe būna kiek pilkesnė: just plain transition to event driven architectures and legacy fighting.

Kitą kartą parašysiu apie motociklus arba apie gyvenimą, nebus vien kodo blogas.