momente şi schiţe de informatică şi matematică
To attain knowledge, write. To attain wisdom, rewrite.

Creaţie grafică (croşetare) folosind R

limbajul R
2016 dec

Pachetul de "creaţie grafică" (sau "computer art") pe care îl propunem aici este format din două funcţii de "croşetare" (prin puncte şi respectiv, prin segmente) şi una care defineşte coordonatele punctelor, selectează unul sau altul dintre sistemele grafice şi apoi apelează (pentru acele puncte şi sistem grafic) una sau alta dintre funcţiile de croşetare; unele rezultate sunt redate în [2] (dar în dezvoltarea "de la capăt" întreprinsă aici, avem unele modificări ale parametrilor acestora).

1. Croşetare cu puncte (şi linie directoare)

Primind vectorii absciselor şi ordonatelor, funcţia următoare "croşetează" punctele respective şi segmentele care le unesc consecutiv - apelând plot() şi lines(), cu anumiţi parametri de mărime ("cex", "lwd"), de formă ("pch", "lty") şi culoare ("col"):

crochet <- function(x, y, 
                    col1 = "gray7", 
                    pch = 21, cex = 6.5,
                    col2 = "gray70", 
                    lty = "dotted", lwd = 1.5
                   ) {
    opar <- par(mar=c(0, 0, 0, 0), bty="n", 
                xaxt="n", yaxt="n")
    plot(x, y, cex=cex, pch=pch, col=col1, asp=1)
    lines(x, y, col=col2, lty=lty, lwd=lwd)
    par(opar)
}

Imaginea redată mai sus este generată (pe ecran) prin:

angle <- seq(0, 2*pi, pi/72)  # 145 de "unghiuri" α echidistante, din [0, 2π]
crochet(sin(2*angle)/2, sin(angle)/2)  # 145 de "puncte" (sin(α)*cos(α), 0.5*sin(α))

Dispozitivul grafic implicit este ecranul (sau "memoria ecran"), însă R asigură şi alte sisteme grafice ("graphical devices") - astfel că imaginea poate fi creată şi într-un fişier PNG, sau într-un fişier SVG, etc. Dar fiecare dintre acestea are propriul set de parametri grafici, iar funcţia par() vizează sistemul grafic activ - încât anularea marginilor lăsate în mod implicit în jurul imaginii propriu-zise (prin parametrul "mar"), a borderului (prin "bty") şi a axelor (prin "xaxt" şi "yaxt") se cuvine să fie făcută - invocând par() - imediat înaintea comenzilor de plotare (iar la sfârşit avem de reconstituit parametrii iniţiali ai sistemului grafic respectiv).

Puteam evita explicitarea parametrilor în definiţia funcţiei de mai sus, folosind operatorul '...' - lăsând liniei de apel a funcţiei, sarcina de a preciza parametrii de execuţie a plotării:

macrame <- function(x, y, ...) {
    plot(x, y, asp=1, ...)
    lines(x, y, ...)
}
# pentru prima dintre figurile redate dedesubt:
macrame(sin(2*angle)/2, sin(angle), pch='~', 
        col=c("dimgray","gray60"), cex=12.5, lty="dotted") 
# pentru a doua figură (în sistemul grafic "PNG"):
png(filename="CA081032.png", bg="transparent", units="px", width=672, height=672)
opar <- par(mar=c(0, 0, 0, 0), bty="n", xaxt="n", yaxt="n")
macrame(sin(4*angle)/3, sin(angle), pch='~', 
        col=c("dimgray","gray60"), cex=12.5, lty="dotted")
par(opar)
dev.off()  # închide fişierul şi comută sistemul grafic

Drept formă pentru "puncte" am ales 'pch="~"' (asemănând prima imagine cu două caractere "tilda" rotite cu ±90° şi alipite). În prima figură se pot vedea marginile, borderul şi axele specifice graficii statistice (fiindcă am omis anularea acestora, înaintea plotării); a doua figură a fost creată direct într-un fişier PNG, prin funcţia png() (şi are "background" transparent, ceea ce nu prea se poate obţine în sistemul grafic obişnuit, pentru ecran).

De observat că acum - indicând în parametrul "col" un vector de culori - am putut alterna culorile "punctelor" (dar la fel va rezulta şi din crochet(), setând ca vector parametrul "col1").

Rescrierea funcţiei crochet() sub forma macrame() - folosind argumentul "..." - are un dezavantaj, care se trage de la faptul că mai toate funcţiile grafice de bază recunosc cam aceiaşi parametri (de exemplu, "col"); ca urmare - neputând transmite doi parametri cu acelaşi nume - nu putem preciza într-un apel macrame(...) o culoare pentru "puncte" (de transmis la plot()) şi o alta pentru "linii" (de adăugat în lines()). Deşi macrame() este mai simplă şi poate mai elegantă - vom prefera mai departe formularea crochet() (cu setări implicite convenabile, pentru parametri).

2. Croşetare cu segmente (şi pete de culoare)

Să plotăm segmentele de capete (cos(a), sin(a)) şi (0.5*cos(a), 0.5*sin(a)) - aflate respectiv pe cercurile cu centrul în origine, de raze 1 şi 0.5 - unde a este un vector de valori din [0, 2π]:

a <- seq(0, 2*pi, pi/50)  # cercurile vor avea câte 100 de puncte
plot.new()  # activează fereastra grafică
plot.window(xlim=c(-1, 1), ylim=c(-1, 1), asp=1)
segments(cos(a), sin(a), 0.5*cos(a), 0.5*sin(a))

Spre deosebire de plot() (care şi iniţializează fereastra grafică necesară plotării), segments() acţionează numai într-o fereastră grafică existentă - aşa că am folosit în prealabil plot.new() şi - pentru a preciza limitele axelor şi "aspect-ratio" - plot.window().

Putem obţine ceva mai interesant dacă implicăm funcţia smoothScatter(); aceasta produce o "estimare de densitate" a punctelor şi o reprezintă prin acoperirea punctelor şi a vecinătăţilor aferente cu o gamă de culori (eventual transparente) gradată după densitatea respectivă:

smoothScatter(cos(5*a), sin(5*a), asp=1)  # asigură şi reiniţializarea ferestrei grafice
segments(cos(a), sin(a), 0.5*cos(a), 0.5*sin(a))

Multiplicând argumentul (5*a, în loc de a - însemnând rărirea punctelor de pe cerc, de la 100 la numai 20), am obţinut o imagine relevantă pentru efectul pe care îl poate adăuga folosirea funcţiei smoothScatter(); cele 20 de puncte sunt reprezentate prin câte o pată distinctă de culoare închisă, care se degradează pe măsura depărtării de punct (v. imaginea alăturată; mărind-o, se va putea observa că "petele" au formă dreptunghiulară).

Pentru a marca densităţile, smoothScatter() foloseşte în mod implicit, paleta de culori "blues9":

args(smoothScatter)  # ne arată "prototipul" funcţiei
function (x, y=NULL, nbin=128, bandwidth, nrpoints=100,
    colramp = colorRampPalette(c("white", blues9)),  
    # o funcţie de transformare, o funcţie 'postPlotHook', etc.
    , ...) 
# vizualizăm culorile din vectorul 'blues9':
barplot(rep(1, 9), col = blues9, names.arg = blues9, 
        horiz=TRUE, las=2)

colorRampPalette() primeşte ca argument o secvenţă de culori şi returnează o funcţie care primind ca argument numărul dorit de culori, va returna un vector de culori care interpolează secvenţa respectivă:

tone <- colorRampPalette(c("white", blues9))
tone(5)  # interpolează 5 culori, între "white" şi blues9[9]
[1] "#FFFFFF" "#D8E6F5" "#84BCDB" "#2979B9" "#08306B"

Reluând exemplul cercurilor concentrice, apare acum o idee interesantă, cu un rezultat de neanticipat: să repetăm - prin încercări, am optat pentru 25 de ori - o secvenţă de culori de marcare a densităţii (în loc de a "rări" punctele cercurilor, cum am făcut mai sus):

grad <- colorRampPalette(rep(c("white", blues9), 25))
smoothScatter(cos(a), sin(a), asp=1, colramp = grad)
# adăugăm segmentele (de exemplu, pentru cercuri concentrice)
segments(cos(a), sin(a), 0.5*cos(a), 0.5*sin(a))

În final - dar în realitate lucrurile nu au decurs după cronologia îmbibată didactic pe care am adoptat-o aici - împachetăm smoothScatter() şi segments() în următoarea funcţie generală:

weave <- function(x0, y0, x1, y1, 
                  col = "black", 
                  grd = c(rgb(0, 0.5, 0.6, 0), rgb(0.2, 0.6, 0.4, 0)),
                  lwd = 0.6,
                  trim = FALSE, trim_at=2,  # va permite rărirea segmentelor 
                  sct_x = y0, sct_y = x1,   # sau cos(ang) şi sin(ang), etc.
                  ... # pentru alţi parametri de transmis lui smoothScatter()
                 ) {
    opar <- par(mar=c(0, 0, 0, 0), bty="n", xaxt="n", yaxt="n")
    smoothScatter(sct_x, sct_y, col=NA, 
                  colramp=colorRampPalette(grd, alpha=TRUE), ...)
    if(trim) {  # răreşte segmentele
        i2 <- seq(1, length(x0), trim_at)  # selectează indicii din 2 în 2, etc.
        x0 <- x0[i2]; y0 <- y0[i2]
        x1 <- x1[i2]; y1 <- y1[i2]
    }
    segments(x0, y0, x1, y1, col=col, lwd=lwd, 
             xlim=range(x0, x1), ylim=range(y0, y1))
    par(opar)
}

Revenim la exemplul cercurilor concentrice, dar acum reducem raza cercului interior, îndesim punctele cercurilor şi tratăm pe rând două variante: cu repetare de 12 ori şi respectiv de 24 de ori, a culorilor necesare în argumentul "grd"; segmentele sunt trasate alternând două culori apropiate gamei blues9 (am ales "navy" şi "snow"):

ang <- seq(0, 2*pi, pi/600)
ca <- cos(ang); sa <- sin(ang)
nrep <- 12  # pentru a doua imagine: nrep <- 24
grd <- rep(c("lightblue", blues9), nrep)
weave(ca, sa, 0.1*ca, 0.1*sa, col=c("navy", "snow"), sct_x=ca, sct_y=sa, grd=grd)

Parametrii sct_x şi sct_y din weave() permit definirea independentă a punctelor pentru care apelul smoothScatter() să determine densitatea şi să o reprezinte pe baza setului de culori furnizat în parametrul grd; în exemplul produs mai sus am ales însă chiar punctele unuia dintre cele două cercuri şi am multiplicat de un anumit număr de ori un set de culori pentru reprezentarea densităţii (obţinând imaginile cam spectaculoase, redate mai sus) - dar în principiu, parametrii respectivi ar trebui aleşi astfel încât desenul final să nu fie supraîncărcat "artificial" (iniţial m-am gândit că ar putea contribui la iluminarea unor zone prea dense de segmente).

3. Sintetizarea croşetărilor

În exemplele considerate mai sus, am constituit vectorii de coordonate aşteptaţi de crochet() şi de weave() prin cele mai simple expresii bazate pe funcţiile periodice fundamentale cosinus şi sinus; dar putem avea în vedere şi expresii care adună sau înmulţesc sin() şi cos() şi folosesc diverşi factori multiplicativi (după cum am putea viza şi alte funcţii de bază).

Fiind posibile mai multe sisteme grafice şi diverse modalităţi de constituire a expresiilor care dau coordonatele iniţiale, ambalăm cele două funcţii stabilite mai sus într-o funcţie "generală" (care totuşi - în mod implicit - pare orientată pe expresii de sin() şi cos()):

plotart <- function(ang = seq(0, 2*pi, pi/2000), 
                    amp = c(6, 13),
                    to_png = FALSE, dev = "png", 
                    FUN = 1, way = 1, ...) {
    if(to_png) {  # denumeşte fişierul şi activează sistemul grafic indicat
        filename <- paste("/images/CA", format(Sys.time(), "%d%H%M"), 
                          ".", dev, sep="") 
        switch(dev,
            png = png(filename = filename, units="px", 
                      width=672, height=672, bg="transparent"),
            svg = svglite::svglite(file = filename, 
                                   width=7, height=7, bg="transparent")
        ) 
    }
    switch(FUN,
        { v1 <- sin(amp[1]*ang)
          v2 <- cos(amp[2]*ang)
          v3 <- cos(amp[1]*ang) 
        },
        { v1 <- cos(amp[1]*ang)
          v2 <- sin(amp[2]*ang)
          v3 <- sin(amp[1]*ang)
        }  # , { o a treia funcţie }, etc.
    )    
    switch(way,
        { x = v1*v2
          y = v2*v3
          crochet(x, y, ...)
        },
        { x = v1*v2
          y = (v2 + v3)/2
          crochet(x, y, ...)
        },
        { v4 = sin(amp[2]*ang)
          weave(v1, v2, v4, v3, ...)
        },
        { weave(...) }  
    )
    if(to_png) {
        dev.off()
    }
}

Funcţiile de bază implicite sunt tot sin() şi cos(), cum se vede din setarea implicită a parametrului 'ang' şi din definiţiile existente pentru 'FUN' (unde putem totuşi adăuga o a treia definiţie, apelând apoi plotart() cu 'FUN = 3', etc.).

Există numeroase lucrări de "computer art" - dar nu şi generate folosind R! - şi am încercat fireşte să obţin (cât mai simplu) imagini cu o anumită pretenţie sau aparenţă de originalitate - ajungând la expresiile date la cazul 'way = 1' (în care abscisele şi ordonatele sunt produse cu câte un factor funcţional comun, de exemplu sin(6t)*cos(13t) şi cos(13t)*cos(6t)) şi la 'way = 2' (unde iarăşi avem un termen funcţional comun). Bineînţeles că şi pentru parametrul 'way' se pot adăuga încă alte definiţii pe lângă cele patru specificate mai sus.

Pentru a obţine şi imagini în format SVG, am folosit pachetul svglite (fişierele SVG rezultate astfel fiind uneori mult mai "scurte" decât s-ar obţine prin funcţia svg() a pachetului de bază graphics). În apelurile indicate prin parametrul 'dev' (la funcţiile png() şi svglite::svglite()) am fixat aparent convenabil, dimensiunea imaginii; dar am întâlnit cazuri în care imaginea obţinută astfel mi s-a părut mai "slabă" decât cea care se obţine în mod implicit pe ecran (păstrând setarea to_png = FALSE). Am constatat pentru asemenea cazuri, că putem obţine o copie fidelă a imaginii de pe ecranul grafic (într-un fişier PNG, sau PDF) folosind comanda savePlot().

Imaginile care urmează mai jos au fost obţinute respectiv prin următoarele apeluri:

plotart(ang = seq(0, 2*pi, pi/800), amp = c(10, 2), way = 3, 
        col = "gray40", grd = rep(c("snow", gray.colors(5)), 24), lwd = 0.6)
savePlot("/images/mostra3.png", type = "png")

grd = c("#ffffff", "#cccccc")
plotart(ang = seq(0, 2*pi, pi/640), amp = c(10, 2), way = 3, 
        col = c("cyan4", "sienna3", "sienna"), grd = grd, lwd = 0.5)  # mostra.png
plotart(ang = seq(0, 2*pi, pi/3000), amp = c(10, 2), way = 3, 
        col = gray.colors(12), grd = grd, lwd = 10)  # mostra2.png
plotart(ang = seq(0, 2*pi, pi/3000), amp = c(10, 2), way = 3, 
        col = gray.colors(24), grd = grd, lwd = 10)  # mostra1.png

În toate cele patru apeluri menţionate mai sus, am folosit amp = c(10, 2) şi way = 3 - rezultând prin weave() segmente care unesc punctele de coordonate (sin(10*a), cos(2*a)) şi respectiv (sin(2*a), cos(10*a)), unde a este vectorul transmis în parametrul "ang" (valori echidistante din intervalul [0, 2π], în număr de 2*800 pentru prima imagine, 2*640 pentru a doua şi 2*3000 pentru celelalte).

Am ales în mod intenţionat aceiaşi parametri de bază "amp" şi "way" (şi "FUN", care este în mod implicit 1) - arătând cum putem obţine imagini care uneori sunt chiar greu de apropiat între ele, modificând numărul de puncte, culorile implicate (pentru trasarea segmentelor, dar şi pentru smoothScatter()) şi grosimea liniilor.

Pentru "mostra3.png" (prima imagine) am folosit aceeaşi idee ca pentru ultimele două imagini de la §2 - constituind vectorul de culori aşteptat de argumentul "grd" prin repetarea de 24 de ori a unei anumite game de culori; dar am ales acum o gamă de nuanţe gri - în loc de nuanţele blues9 implicate la §2 - obţinută prin funcţia gray.colors(). Completarea gamei de 5 nuanţe de gri cu o culoare apropiată (am ales "snow") s-a dovedit importantă - fără aceasta, regiunea centrală a imaginii ar fi fost mult prea închisă la culoare.

Pentru celelalte trei imagini am schimbat gama de culori transmisă în "grd" (şi am renunţat la factori de repetare), optând pentru culori interpolate între alb şi "Gray80" (cum este denumită culoarea de cod #CCCCCC indicată în definiţia variabilei "grd" care precede ultimele trei apeluri).

Pentru "mostra.png" (a doua dintre imaginile de pe primul rând) cele 2*640 de segmente au fost trasate subţire (cu "lwd = 0.5"), folosind alternativ cele trei culori transmise în parametrul "col" prin cel de-al doilea apel plotart() dintre cele redate mai sus.

În schimb, la ultimele două imagini am crescut numărul de segmente la 6000, am mărit grosimea segmentelor (cu "lwd = 10") şi pentru colorarea alternativă a acestora am indicat 12 şi respectiv 24 de nuanţe gri (vezi mai sus ultimele două comenzi plotart()).

4. Acknowledgments

Arătând prin [2] imagini dintre cele produse astfel, unii au sesizat că "seamănă" cu imagini redate la www.ams.org/mathimagery/ (sursă pe care şi eu am menţionat-o în [1]). Se pune într-adevăr, problema originalităţii; cum "seamănă" - sunt produse printr-un acelaşi program, având aceiaşi parametri? Eu am prezentat complet programul prin care le-am obţinut, vizând nu atât "computer art", cât deprinderea limbajului R şi arătând cum pot fi speculate pentru "creaţie grafică" funcţii care în mod obişnuit deservesc necesităţile statistice specifice acestui limbaj (apropo: pe Internet eu nu am găsit programe în R pentru "computer art").

Faptul că am folosit pentru coordonatele punctelor expresii bazate pe sinus şi cosinus îl datorez ca toată lumea lui Lissajous (v. curbele lui Lissajous). Dar am evitat să folosesc expresii pe care le-am întâlnit şi eu, prin alte locuri - asigurându-mi cred o anumită doză de originalitate prin acel "factor funcţional comun" (pentru abscise şi ordonate) pe care l-am evidenţiat mai sus.

Dar să vedem acum şi invers: pot genera prin funcţia proprie plotart() redată mai sus, o imagine asemănătoare cu - de exemplu - imaginea semnată 'Hamid Naderi Yeganeh, "A Bird in Flight" (November 2014)', din albumul "mathimagery" citat mai sus? Nu reproduc şi aici, imaginea respectivă - nu am cerut permisiunea pentru aceasta şi bănuiesc că este necesară - dar cred că pot reda indicaţia care o însoţeşte:

Desigur, obişnuindu-ne cu R (care este orientat pe operaţii vectoriale) nu vom adopta maniera "for each"; dar precizăm (mai sus - ne-a scăpat această idee) că plotând segment după segment avem avantajul de a vedea pe ecran cum se "naşte" imaginea, pas cu pas (şi dezavantajul - cel puţin, în R - că execuţia ar putea dura neconvenabil de mult).

Subliniez că nu mi-am propus să imit exact imaginea originală - pentru aceasta, oricine poate folosi "screenshot" - ci să obţin o imagine foarte asemănătoare ei. Bineînţeles că am folosit "way = 4" (pentru a transmite funcţiei weave() coordonatele capetelor segmentelor) şi după câteva încercări - am optat pentru un număr de 2*4000 de segmente de grosime "lwd = 0.1" şi colorate alternativ cu "black" şi "gray60", iar pentru parametrul (specific programului meu) "grd" am ales nişte nuanţe caracteristice văzduhului limpede al iernii (desigur, a trebuit să adaptez şi parametrii "sct_x" şi "sct_y", odată cu schimbarea necesară a raportului "asp"):

a = seq(0, 2*pi, pi/4000)
grd = c("snow", "lightblue")
plotart(way = 4, x0 = 3*sin(a)^3, y0 = -cos(4*a), 
        x1 = 1.5*sin(a)^3, y1 = -0.5*cos(3*a),
        grd = grd, sct_x = x0, sct_y = 5*x1, 
        asp = 1.2, lwd = 0.1, col = c("black", "gray60"))

Rezultatul reprodus mai sus mi se pare mulţumitor ("seamănă" imaginii originale)…

Nu cred că greşesc: prin funcţia aparent foarte simplă plotart() descrisă complet mai sus, putem şi "imita" imaginile de "computer art" promovate în diverse locuri (cel puţin, pe acelea în care coordonatele sunt expresii de sin() şi cos(), mai ales dacă expresiile respective nu sunt secrete); în schimb, pentru ca un alt program de "creaţie grafică" - în alt limbaj decât R - să "imite" imaginile posibil de obţinut prin plotart(), el ar trebui probabil să implementeze "suplimentar" şi o funcţie analogă cu smoothScatter().

Dar şi "pachetul" plotart() s-ar putea formula mai bine decât mi-a reuşit mie aici şi s-ar putea completa; de exemplu, aş fi vrut în unele cazuri să dispun de o funcţie prin care să trunchiez segmentele dintr-o anumită zonă a imaginii (sau, să pot defini o regiune în care segmentele respective să fie trasate parţial şi nu pe toată lungimea lor).

vezi Cărţile mele (de programare)

docerpro | Prev | Next