POLITECNICO DI MILANO
Corso di Laurea Specialistica in Ingegneria Matematica
Orientamento Calcolo Scientifico
PROGRAMMAZIONE AVANZATA
per il
CALCOLO SCIENTIFICO
PROGETTO: Proiezione di punti su una superficie triangolata.
progetto di:
Elisa Maculan
Matr. 679979
Anno Accademico 2006-2007
Indice
1 Introduzione
2
2 Classi e strutture
2.1 Struct Point . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.2 Class Triangle . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.3 Class Mesh . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3
3
5
8
3 Funzioni e metodi
10
3.1 Proiezione di un punto su un piano . . . . . . . . . . . . . . . 10
3.2 Appartenenza di un punto ad un triangolo . . . . . . . . . . . 14
4 Ricerca del triangolo stl di appartenenza della proiezione di
ogni nodo della mesh netgen
4.1 Metodo brute force . . . . . . . . . . . . . . . . . . . . . . . .
4.2 Metodo con ricerca del nodo stl più vicino al nodo netgen . .
4.3 Metodo con algoritmo di ricerca del nearest neighbor . . . . .
21
21
23
25
5 Applicazioni e problemi riscontrati
28
6 Algoritmi alternativi
31
1
1
Introduzione
Date una mesh netgen ed una superficie triangolata stl, l’obiettivo di questo
lavoro è quello di trovare per ogni nodo della mesh netgen i tre nodi più
vicini, nello spazio 3D, della superficie stl. Le due mesh sono fornite a priori
e l’idea che si desidera sviluppare è quella di cercare per ogni nodo netgen il
triangolo stl a cui appartiene la sua proiezione, dato che il nodo netgen può
non giacere sul piano definito da un triangolo stl.
La ricerca dei tre nodi stl più vicini ad ogni nodo netgen è utile in quanto
per ogni nodo stl sono noti i suoi spostamenti in dieci istanti temporali diversi. Perciò, una volta individuati i tre nodi stl più vicini ad un nodo netgen
ed interpolati i loro spostamenti, si riesce a calcolare anche lo spostamento
del nodo netgen e quindi a ricostruire la mesh netgen ad ogni istante temporale. Questa mesh verrà usata per il calcolo fluidodinamico d’interazione
fluido-struttura, però con struttura nota.
Per spiegare il procedimento seguito per il raggiungimento dell’obiettivo,
questo lavoro si articola in paragrafi nel seguente modo:
• Paragrafo 2: CLASSI E STRUTTURE
Breve descrizione della struttura Point e delle classi Triangle e Mesh,
necessarie per la lettura della mesh netgen e della superficie triangolata
stl di partenza.
• Paragrafo 3: FUNZIONI E METODI
Descrizione delle idee teoriche alla base della funzione esterna di proiezione
di un punto sul piano individuato da un triangolo e del metodo della
classe Triangle che verifica l’appartenenza di un punto ad un triangolo,
quando giacciono sullo stesso piano.
• Paragrafo 4: RICERCA DEL TRIANGOLO STL DI APPARTENENZA DELLA PROIEZIONE DI OGNI NODO NETGEN
Descrizione di tre metodi differenti per la ricerca del triangolo stl di
appartenenza. Il primo consiste in un metodo brute force che cicla su
tutti i triangoli della superficie stl, calcolando la proiezione di ogni nodo netgen sul piano individuato da ciascun triangolo e l’appartenenza
della proiezione stessa ad ogni triangolo stl. Il secondo, invece, cicla
su tutti i nodi stl per cercare quello più vicino ad ogni nodo netgen;
in seguito calcola proiezione e sua appartenenza ad ogni triangolo nel
2
patch del nodo stl più vicino. Il terzo esegue le stesse operazioni del
secondo, ma rende più veloce ed efficiente la ricerca del nodo stl più
vicino sfruttando un algoritmo di ricerca del nearest neighbor di una
libreria esterna, la libreria ANN.
• Paragrafo 5: APPLICAZIONI E PROBLEMI RISCONTRATI
Applicazione dei tre metodi ad un caso test semplice con mesh costituite da pochissimi nodi (cilindro) e ad un caso reale più complesso
(arco aortico). Analisi dei problemi riscontrati eseguendo il codice
implementato nel caso reale con geometria poco regolare.
• Paragrafo 6: ALGORITMI ALTERNATIVI
Descrizione di due algoritmi alternativi, implementati per risolvere i
problemi descritti nel paragrafo precedente. Il primo algoritmo sfrutta
la libreria ANN per la ricerca diretta dei tre nearest neighbor di ogni
nodo netgen; il secondo calcola il baricentro di ogni triangolo stl, ricerca il baricentro più vicino ad ogni nodo netgen e considera i vertici del
triangolo con questo baricentro come i tre punti stl più vicini al nodo
netgen.
2
Classi e strutture
In questo paragrafo introduciamo brevemente attributi e metodi delle strutture di base implementate, necessarie per la lettura della mesh netgen e della
superficie stl note e per i calcoli successivi.
2.1
Struct Point
Per leggere i nodi di una mesh e i punti 3D si è deciso di usare una struttura in modo tale da avere degli attributi pubblici a cui poter accedere
direttamente, senza bisogno di metodi appositi. Questo perchè si è scelto
di richiamare gli attributi in modo diretto anche nella funzione esterna di
proiezione, ma naturalmente è possibile effettuare altre scelte.
La struttura Point è dotata di quattro attributi:
double x;
double y;
double z;
che rappresentano le tre coordinate spaziali
3
int ID;
che rappresenta un indicatore del nodo nella mesh ed è posto = −1 di
default per punti 3D che non appartengono ad alcuna mesh.
Per questa struttura vengono forniti diversi costruttori:
//costruttore di default
Point():x(0.0),y(0.0),z(0.0),ID(-1) {};
//costruttore che legge da file
Point(char* fileName,int num):ID(-1) {};
//costruttore a cui passo tre numeri in ingresso
Point(const double a, const double b, const double c):
x(a),y(b),z(c),ID(-1) {};
//costruttore di copia
Point(const Point& P): x(P.x),y(P.y),z(P.z),ID(P.ID) {}; .
Il distruttore non viene sovrascritto, dato che risulta essere già adatto quello
di default. Questo, infatti, distrugge i membri della classe, che nel nostro
caso sono di tipo nativo, e quindi vengono distrutti in modo adeguato dal
compilatore.
Infine si ha l’overloading degli operatori =, <<, ==, ! =
Point& operator=(const Point& P)
{x=P.x;
y=P.y;
z=P.z;
ID=P.ID;}
friend ostream& operator<<(ostream& s, const Point& P)
{s<<P.x<<" "<<P.y<<" "<<P.z<<" "<<P.ID;
return s;}
friend bool const operator==(const Point& P1, const Point& P2)
{
if(P1.x==P2.x && P1.y==P2.y && P1.z==P2.z)
return true;
4
else
return false;
}
friend bool const operator!=(const Point& P1, const Point& P2)
{
if(P1.x!=P2.x && P1.y!=P2.y && P1.z!=P2.z)
return true;
else
return false;
} .
2.2
Class Triangle
Questa classe può rappresentare un triangolo di una mesh oppure un qualsiasi triangolo posizionato in uno spazio 3D.
I suoi attributi sono i seguenti:
Vertices vertici;
un vettore di tre punti che rappresenta i tre vertici del triangolo
Point normale;
che rappresenta la normale al triangolo (in realtà questa sarebbe un
vettore, ma in uno spazio 3D risulta comunque individuata da tre
coordinate spaziali, l’ID è posto = −1 di default)
int ID;
che rappresenta un indicatore del triangolo nella mesh ed è posto = −1
di default per triangoli 3D che non appartengono ad alcuna mesh.
Per quanto riguarda i metodi, ci sono diversi costruttori:
//costruttore che legge da file
Triangle(char* fileName,int num):ID(-1) {};
//costruttore che riceve tre punti in ingresso + la normale
Triangle(const Point& N, const Point& P1, const Point& P2, const
Point& P3):normale(N),ID(-1)
{
vertici.push_back(P1);
5
vertici.push_back(P2);
vertici.push_back(P3);
}
//costruttore che riceve tre punti in ingresso
Triangle(const Point& P1, const Point& P2, const Point& P3):ID(-1)
{
vertici.push_back(P1);
vertici.push_back(P2);
vertici.push_back(P3);
normale = this->calcolanormale();
}
//costruttore che riceve in ingresso un vettore di vertici+la normale
Triangle(const Point& N,const Vertices& v):normale(N),ID(-1)
{
if(v.size()==3)
{ vertici.push_back(v.at(0));
vertici.push_back(v.at(1));
vertici.push_back(v.at(2));}
else {cout<<"dimensione del vettore di vertici passato deve essere = 3";}
}
//costruttore di copia
Triangle(const Triangle& T): normale(T.normale),ID(T.ID)
{
vertici.push_back(T.vertici.at(0));
vertici.push_back(T.vertici.at(1));
vertici.push_back(T.vertici.at(2));}
e per il distruttore viene usato ancora quello di default.
Vi sono metodi per accedere agli attributi privati
//metodo che ritorna il vettore di vertici
vector<Point> getvertici() const {return vertici;}
//metodo che ritorna un vertice specifico
Point getvertice(int i) const {return vertici.at(i);}
6
//metodo che ritorna la normale
Point getnormale() const {return normale;}
//metodo che ritorna l’ID
int getID() const {return ID;}
metodi per assegnare il valore agli attributi privati
//metodo che setta l’ID
void setID(int i) {ID=i;}
//metodo che setta la normale
void setnormale(Point N) {normale=N;}
metodi per calcolare attributi o grandezze caratteristiche del triangolo
//metodo che calcola il versore normale dati i vertici del triangolo
Point calcolanormale()
{
double a,b,c;
a=(vertici.at(1).y-vertici.at(0).y)*(vertici.at(2).z-vertici.at(0).z)
-(vertici.at(1).z-vertici.at(0).z)*(vertici.at(2).y-vertici.at(0).y);
b=(vertici.at(1).z-vertici.at(0).z)*(vertici.at(2).x-vertici.at(0).x)
-(vertici.at(1).x-vertici.at(0).x)*(vertici.at(2).z-vertici.at(0).z);
c=(vertici.at(1).x-vertici.at(0).x)*(vertici.at(2).y-vertici.at(0).y)
-(vertici.at(1).y-vertici.at(0).y)*(vertici.at(2).x-vertici.at(0).x);
//calcolo il versore normale
Point N;
N.x=a/sqrt(a*a+b*b+c*c);
N.y=b/sqrt(a*a+b*b+c*c);
N.z=c/sqrt(a*a+b*b+c*c);
return N;
}
//metodo che calcola il baricentro del triangolo dati i vertici
Point baricentro()
{
double xb,yb,zb;
xb=(vertici.at(0).x+vertici.at(1).x+vertici.at(2).x)/3;
yb=(vertici.at(0).y+vertici.at(1).y+vertici.at(2).y)/3;
7
zb=(vertici.at(0).z+vertici.at(1).z+vertici.at(2).z)/3;
Point B(xb,yb,zb);
return B;
}
ed infine sono implementati i metodi
bool appartTriangle(const Point & P) {};
bool proiezandappart(const Point & P) {};
bool proiezandappart(const Point & P,double & dist) {};
bool proiezandappart(const Point & P,double & dist,
const Point & N){};
che saranno descritti più precisamente nel paragrafo successivo.
Anche per questa classe vengono sovrascritti gli operatori = e <<
friend ostream& operator<<(ostream& s, const Triangle& T)
{s<<"triangolo: "<<endl<<" vertice 1: "<<T.vertici[0]<<"\n vertice 2:
<<T.vertici[1]<<"\n vertice 3: "<<T.vertici[2]<<"\n normale: "<<
T.normale<<"\n ID: "<<T.ID<<endl;
return s;}
Triangle& operator=(const Triangle& T)
{vertici.push_back(T.vertici.at(0));
vertici.push_back(T.vertici.at(1));
vertici.push_back(T.vertici.at(2));
normale=T.normale;
ID=T.ID;} .
2.3
Class Mesh
Questa classe rappresenta naturalmente una mesh ed è dotata di due soli
attributi privati:
Nodes Mnodi;
vettore di punti che contiene tutti i nodi della mesh
8
"
Elements Mtriang;
vettore dei triangoli superficiali della mesh (per la mesh netgen non è
definito perchè questa mesh non contiene informazioni sui triangoli).
Gli unici costruttori forniti leggono dal file.neu per costruire la mesh netgen
e dal file.stl per costruire la mesh stl:
//costruttore che legge da file.neu
Mesh(char* fileName, int ns, int es) {};
//costruttore che legge da file.stl
Mesh(char* fileName) {}; .
Questo secondo costruttore sfrutta anche le informazioni provenienti dal
file.dat, se questo è disponibile, risultando però molto lento; per questo
scrive la mesh in un file.txt più strutturato e veloce da leggere che viene
usato tutte le volte successive in cui viene eseguito il codice.
Ci sono poi metodi che ritornano il numero di nodi o di triangoli ed un nodo
o un triangolo specifico:
//numero di punti
inline int nP() const {return Mnodi.size();}
//numero di triangoli (elementi di superficie)
inline int nT() const {return Mtriang.size();}
//referenza al nodo i-esimo (solo lettura)
inline const Point& nodo(int i) const {return Mnodi.at(i);}
//referenza al triangolo i-esimo (solo lettura)
inline Triangle& triangolo(int i) {return Mtriang.at(i);}
//vettore di nodi
inline Nodes nodi() {return Mnodi;} .
Il metodo
vector<list<int> > patch;
vector<list<int> > intornopunto()
{ patch.resize(Mnodi.size());
9
for(int j=0;j<Mtriang.size();++j)
{
int idt=this->Mtriang.at(j).getID();
for(int k=0;k<=2;++k)
{
int i=this->Mtriang.at(j).getvertice(k).ID;
patch.at(i).push_back(idt);
}
}
return patch;
}
serve per costruire il patch di ogni nodo della mesh, contenuto poi nella lista
situata nel vettore alla posizione corrispondente all’ID del nodo.
Per questa classe si ha solo l’overloading dell’operatore <<
friend ostream& operator<<(ostream& s, const Mesh& M)
{int mn=M.Mnodi.size();
int mt=M.Mtriang.size();
s<<"vettore di "<<mn<<" punti:"<<endl;
for(int h=0;h<mn;++h){cout<<M.Mnodi.at(h)<<endl;}
s<<"vettore di "<<mt<<" triangoli:"<<endl;
for(int k=0;k<mt;++k){cout<<M.Mtriang.at(k)<<endl;}
return s;} .
3
Funzioni e metodi
In questo paragrafo introduciamo in modo più accurato la funzione che calcola la proiezione di un punto sul piano individuato da un triangolo e il
metodo della classe Triangle che verifica l’appartenenza di un punto ad un
triangolo. In particolar modo descriviamo dettagliatamente le idee che sono
alla base di queste due funzioni, affrontate in entrambi i casi sfruttando la
geometria analitica.
3.1
Proiezione di un punto su un piano
Noti un punto P di coordinate (xp , yp , zp ) ed un triangolo con vertici
P1 = (x1 , y1 , z1 ) P2 = (x2 , y2 , z2 ) P3 = (x3 , y3 , z3 ), calcoliamo la proiezione
di P , Pp , sul piano individuato dal triangolo: Ax + By + Cz + D = 0.
10
Figura 1: Proiezione del punto P sul piano individuato dal triangolo di
vertici P1 P2 P3 .
Dalla geometria analitica sappiamo che il piano passante per tre punti è
dato da:


x − x1 y − y1 z − z1


det  x2 − x1 y2 − y1 z2 − z1  = 0
x3 − x1 y3 − y1 z3 − z1
risolvendo si ottiene che:
A = (y2 − y1 )(z3 − z1 ) − (z2 − z1 )(y3 − y1 )
B = (z2 − z1 )(x3 − x1 ) − (x2 − x1 )(z3 − z1 )
C = (x2 − x1 )(y3 − y1 ) − (y2 − y1 )(x3 − x1 )
D = −x1 A − y1 B − z1 C.
Calcolando i vettori
P2 P1 = (x2 −x1 , y2 −y1 , z2 −z1 ) e P3 P1 = (x3 −x1 , y3 −y1 , z3 −z1 ) e facendone
il prodotto vettoriale otteniamo il vettore normale al piano n = (A, B, C).
Dalla figura 1, il vettore u perpendicolare al piano e passante per P sarà
un multiplo di n (u = an) ed il suo modulo sarà dato esattamente dalla
distanza tra P e il piano che, sempre per la geometria analitica, vale:
Axp + Byp + Czp + D .
d= √
A2 + B 2 + C 2
11
Quindi a|n| = d
sarà data da:
⇒
a =
Axp +Byp +Czp +D
.
A2 +B 2 +C 2
Perciò la nostra proiezione
Pp = (xp − aA, yp − aB, zp − aC)
dato che Pp = P −u. Si può osservare che, calcolando la proiezione in questo
modo, non è necessario conoscere il verso in cui è diretta la normale; infatti
la costante moltiplicativa a può assumere segno qualsiasi, adattando cosı̀
automaticamente il calcolo delle coordinate della proiezione al verso della
normale stessa.
Alternativamente la proiezione può essere calcolata con la seguente formula:
PP = P − ((P − P1 ) · nn )nn
dove nn è il versore normale al triangolo.
La funzione
Point proiezione(const Point& P1, const Point& P2, const Point& P3,
const Point& P) {};
implementa esattamente entrambe queste strategie.
Point proiezione(const Point& P1, const Point& P2, const Point& P3,
const Point& P)
{//coefficienti del piano su cui giace il triangolo
double a,b,c,d;
a=(P2.y-P1.y)*(P3.z-P1.z)-(P2.z-P1.z)*(P3.y-P1.y); cout<<"a: "<<a<<endl;
b=(P2.z-P1.z)*(P3.x-P1.x)-(P2.x-P1.x)*(P3.z-P1.z); cout<<"b: "<<b<<endl;
c=(P2.x-P1.x)*(P3.y-P1.y)-(P2.y-P1.y)*(P3.x-P1.x); cout<<"c: "<<c<<endl;
d=-P1.x*a-P1.y*b-P1.z*c; cout<<"d: "<<d<<endl;
//versore normale
Point N;
N.x=a/sqrt(a*a+b*b+c*c);
N.y=b/sqrt(a*a+b*b+c*c);
N.z=c/sqrt(a*a+b*b+c*c); cout<<"normale: "<<N<<endl;
//introduco la costante di moltiplicazione
double cm;
//cm=(a*P.x+b*P.y+c*P.z+d)/(a*a+b*b+c*c); cout<<"cm: "<<cm<<endl;
cm=(P.x-P1.x)*N.x+(P.y-P1.y)*N.y+(P.z-P1.z)*N.z;
//calcolo le tre coordinate del punto di proiezione
double x1,y1,z1;
12
//x1=P.x-cm*a;
//y1=P.y-cm*b;
//z1=P.z-cm*c;
x1=P.x-cm*N.x;
y1=P.y-cm*N.y;
z1=P.z-cm*N.z;
//controllo che la proiezione appartenga al piano
cout<<"la proiezione appartiene al piano?"<<endl;
double temp=a*x1+b*y1+c*z1+d;
cout<<temp<<endl;
Point Pro(x1,y1,z1);
return Pro;
}
Per verificare la correttezza di queste strategie sono stati considerati due
esempi semplici, calcolabili anche a mano.
Nel primo caso si calcolano le proiezioni dei punti P = (2, 5, 3), P 0 = (3, 6, 5),
P 00 = (3, 6, 1) sul piano individuato dal triangolo di vertici P1 = (3, 6, 9),
P2 = (4, 2, 3) e P3 = (5, 7, 8). I risultati ottenuti, PP = (3.75497, 3.06954, 4.57947),
PP0 = (4.19205, 4.6887, 6.07285), PP00 = (5.38411, 3.37748, 3.1458), coincidono esattamente coi calcoli fatti a mano e sono visualizzati in figura 2.
Figura 2: Primo esempio semplice: visualizzazione dei punti, delle loro
proiezioni e del triangolo.
13
Nel secondo caso P = (5, 3, 2), P1 = (1, 4, 8), P2 = (2, 3, 6) e P3 = (9, 1, 2).
Anche per questo esempio il risultato, PP = (5, 1.4, 2.8), coincide perfettamente con il valore calcolato a mano ed è riportato in figura 3.
Figura 3: Secondo esempio semplice: visualizzazione del punto, della sua
proiezione e del triangolo.
3.2
Appartenenza di un punto ad un triangolo
Dati un punto P = (xp , yp , zp ) ed un triangolo di vertici
P1 = (x1 , y1 , z1 ) P2 = (x2 , y2 , z2 ) P3 = (x3 , y3 , z3 ) che giacciono sullo
stesso piano, vogliamo verificare se P appartiene al triangolo oppure no.
Come prima idea ci riconduciamo da un problema 3D ad uno 2D, riscrivendo
le coordinate dei punti rispetto ad un sistema di riferimento con origine in
P1 , asse x diretto come il vettore P2 P1 e asse y perpendicolare; i versori degli
assi del nuovo sistema di riferimento sono quindi:
ex =
P2 P1
|P2 P1 |
ez = nn
ey = nn × ex = (nny exz − nnz exy )i + (nnz exx − nnx exz )j + (nnx exy − nny exx )k.
14
Figura 4: Dati un punto P ed un triangolo di vertici P1 P2 P3 che giacciono
sullo stesso piano vogliamo verificare l’appartenenza del punto al triangolo.
Le nuove coordinate di P1 e P2 sono facili da determinare:
P˜1 = (x˜1 , y˜1 ) = (0, 0)
p
l = |P2 P1 | = (x2 − x1 )2 + (y2 − y1 )2 + (z2 − z1 )2
P˜2 = (x˜2 , y˜2 ) = (l, 0).
mentre le nuove coordinate di P3 e P sono date da:
P˜3 = (x˜3 , y˜3 )
con
x˜3 = P3 · ex
y˜3 = P3 · ey
P̃ = (x˜p , y˜p )
con
x˜p = P · ex
y˜p = P · ey
Riscrivendo P̃ come combinazione lineare dei vertici del triangolo otteniamo
x˜p = λ1 x˜1 + λ2 x˜2 + λ3 x˜3
y˜p = λ1 y˜1 + λ2 y˜2 + λ3 y˜3
λ 1 + λ 2 + λ3 = 1
15
λ3 =
dato che x˜1 = y˜1 = y˜2 = 0
⇒
y˜p
y˜3
x˜p −λ3 x˜3
x˜2
.
λ2 =
λ 1 = 1 − λ 2 − λ3
0 6 λ1 6 1
P̃ è combinazione convessa dei vertici del triangolo
⇒
Se 0 6 λ2 6 1
e appartiene al triangolo stesso.
0 6 λ3 6 1
Esiste un’altra possibile strategia per verificare l’appartenenza di un punto ad un triangolo. Una volta ricondotto il problema 3D ad un problema 2D
e riscritte le coordinate dei punti, è possibile implementare l’idea seguente.
Si scrive la retta passante per P1 e P2 , cioè per un lato del triangolo:
y 2 − y1
y 2 − y1
x − y − y1 −
x1 = 0.
x2 − x1
x2 − x1
Si vanno a sostituire nell’equazione della retta le coordinate del terzo vertice,
P3 , e del punto P e si osserva se si ottengono quantità di segno uguale, il
che implica che P3 e P giacciono dalla stessa parte rispetto alla retta, o
opposto. In seguito si eseguono gli stessi calcoli per gli altri due lati. Se il
terzo vertice e P giacciono sempre dalla stessa parte rispetto a tutti e tre i
lati, allora il punto P appartiene al triangolo.
Infine, una terza strategia plausibile analizza direttamente il problema
3D. L’idea è all’incirca la stessa della strategia precedente, ma, al posto di
prendere in considerazione le rette passanti per i lati del triangolo, si calcolano i piani individuati dai vettori direzionati come i lati e come la normale
al triangolo.
Cerchiamo di spiegare più dettagliatamente questo metodo.
Nello spazio 3D un piano può essere individuato da due vettori ortogonali
che giacciono su di esso, oppure da un terzo vettore ortogonale ai primi due,
le cui coordinate danno i coefficienti del piano stesso. Se infatti consideriamo
il piano generico Ãx + B̃y + C̃z + D̃ = 0, il vettore (Ã, B̃, C̃) è ortogonale
al piano. Perciò, conoscendo il vettore ortogonale al piano ed un punto che
appartiene ad esso è facile calcolare i coefficienti dell’equazione del piano
stesso.
Nel nostro caso, conoscendo i vertici del triangolo possiamo calcolare il versore normale al triangolo nn come spiegato nel paragrafo riguardante la
proiezione di un punto. Inoltre possiamo facilmente ottenere i vettori diretti come i lati P2 P1 , P3 P1 e P3 P2 . A questo punto possiamo calcolare i
coefficienti del piano definito, ad esempio, dai vettori P2 P1 e nn . Il vettore
16
v ortogonale al piano sarà dato da:
v = nn × P2 P1 = (nny (z2 − z1 ) − nnz (y2 − y1 ))i+
+(nnz (x2 − x1 ) − nnx (z2 − z1 ))j + (nnx (y2 − y1 ) − nny (x2 − x1 ))k.
I primi coefficienti del piano saranno quindi: Ã = vx , B̃ = vy , C̃ = vz . Il fatto che P1 appartenga al piano che si desidera ottenere permette di calcolare
anche l’ultimo coefficiente: D̃ = −x1 Ã−y1 B̃ −z1 C̃. A questo punto, avendo
a disposizione l’equazione del piano, vi sostituiamo le coordinate del terzo
vertice del triangolo, P3 , e del punto P e vediamo se otteniamo quantità di
ugual segno.
Ripetiamo lo stesso ragionamento per i piani individuati da P3 P1 e nn
à = nny (z3 − z1 ) − nnz (y3 − y1 )
B̃ = nnz (x3 − x1 ) − nnx (z3 − z1 )
C̃ = nnx (y3 − y1 ) − nny (x3 − x1 )
D̃ = −x1 Ã − y1 B̃ − z1 C̃
e da P3 P2 e nn
à = nny (z3 − z2 ) − nnz (y3 − y2 )
B̃ = nnz (x3 − x2 ) − nnx (z3 − z2 )
C̃ = nnx (y3 − y2 ) − nny (x3 − x2 )
D̃ = −x2 Ã − y2 B̃ − z2 C̃.
Come nella seconda strategia, se in tutti e tre i casi le quantità ottenute
hanno segno uguale il punto P appartiene al triangolo, altrimenti è esterno
ad esso.
Tutte queste strategie sono state implementate dal metodo della classe
Triangle
bool appartTriangle(const Point & P) {}; .
Per verificare il loro corretto funzionamento sono stati usati gli stessi esempi
semplici considerati nel paragrafo precedente. I risultati ottenuti sono riportati nelle figure 2 e 3 e sono facilmente verificabili a mano. Tutte e tre
i metodi sono teoricamente equivalenti ed in questi casi molto semplici con
coordinate intere restituiscono gli stessi risultati. Quando, però, vengono
applicate a casi più complessi, come quelli descritti nel paragrafo 5, le prime
due strategie, che si riconducono ad un problema 2D, non sono robuste e
17
restituiscono risultati errati (vedere figura 5), mentre la terza, che si basa
sul problema 3D, funziona correttamente (vedere figura 6).
Figura 5: Alcuni nodi netgen con le loro proiezioni ed i triangoli stl di
appartenenza per il caso test del cilindro. Come si può osservare alcune
proiezioni in realtà non appartengono al triangolo a cui, secondo il codice
implementato, dovrebbero appartenere. Si può quindi affermare che, per
casi abbastanza complessi, il codice con le prime due strategie non è robusto
e restituisce risultati scorretti.
Riportiamo il codice più robusto:
//metodo che sfrutta i piani passanti per i lati in 3D
bool appartTriangle(const Point & P)
{
//introduco delle variabili ausiliarie per facilità di scrittura
double x1=vertici.at(0).x; //cout<<"x1: "<<x1<<endl;
double y1=vertici.at(0).y; //cout<<"y1: "<<y1<<endl;
double z1=vertici.at(0).z; //cout<<"z1: "<<z1<<endl;
double x2=vertici.at(1).x; //cout<<"x2: "<<x2<<endl;
double y2=vertici.at(1).y; //cout<<"y2: "<<y2<<endl;
double z2=vertici.at(1).z; //cout<<"z2: "<<z2<<endl;
double x3=vertici.at(2).x; //cout<<"x3: "<<x3<<endl;
double y3=vertici.at(2).y; //cout<<"y3: "<<y3<<endl;
double z3=vertici.at(2).z; //cout<<"z3: "<<z3<<endl;
if((P.x==x1 && P.y==y1 && P.z==z1) || (P.x==x2 && P.y==y2 && P.z==z2)
18
Figura 6: Alcuni nodi netgen con le loro proiezioni ed i triangoli stl di
appartenenza ottenuti con la terza strategia per il caso test del cilindro
(prime 4 immagini) e per il caso reale dell’aorta (ultime 4 immagini). Si può
notare che tutte le proiezioni appartengono ai triangoli e quindi il codice
implementato fornisce risultati corretti.
|| (P.x==x3 && P.y==y3 && P.z==z3))
{return true;} //il punto è uguale ad un vertice del triangolo
else
{//versore normale
19
Point N=this->calcolanormale(); cout<<N<<endl;
//coefficienti dei piani passanti per i lati (ax+by+cz+d=0)
//piano per P1 e P2
double a,b,c,d;
a=N.y*(z2-z1)-N.z*(y2-y1);
b=N.z*(x2-x1)-N.x*(z2-z1);
c=N.x*(y2-y1)-N.y*(x2-x1);
d=-a*x1-b*y1-c*z1;
//piano per P1 e P3
double a1,b1,c1,d1;
a1=N.y*(z3-z1)-N.z*(y3-y1);
b1=N.z*(x3-x1)-N.x*(z3-z1);
c1=N.x*(y3-y1)-N.y*(x3-x1);
d1=-a1*x1-b1*y1-c1*z1;
//piano per P2 e P3
double a2,b2,c2,d2;
a2=N.y*(z3-z2)-N.z*(y3-y2);
b2=N.z*(x3-x2)-N.x*(z3-z2);
c2=N.x*(y3-y2)-N.y*(x3-x2);
d2=-a2*x2-b2*y2-c2*z2;
//introduciamo nelle equazioni dei piani le coordinate del terzo vertice
//e del punto P ed andiamo a vedere se otteniamo valori con lo stesso
//segno per ogni piano.
double r12p, r12p3, r23p, r23p1, r13p, r13p2;
r12p=a*P.x+b*P.y+c*P.z+d;
r12p3=a*x3+b*y3+c*z3+d;
r23p=a2*P.x+b2*P.y+c2*P.z+d2;
r23p1=a2*x1+b2*y1+c2*z1+d2;
r13p=a1*P.x+b1*P.y+c1*P.z+d1;
r13p2=a1*x2+b1*y2+c1*z2+d1;
if(((r12p>=0 && r12p3>=0) || (r12p<=0 && r12p3<=0)) && ((r23p>=0 && r23p1>=0)
|| (r23p<=0 && r23p1<=0)) && ((r13p>=0 && r13p2>=0) || (r13p<=0 && r13p2<=0)))
return true;
else
return false;
}
}
20
Il metodo
bool proiezandappart(const Point & P) {};
calcola direttamente la proiezione di un punto 3D sul piano individuato
da un triangolo e l’appartenenza della proiezione stessa a questo triangolo,
inglobando i codici descritti nel paragrafo precedente ed in questo.
Il metodo
bool proiezandappart(const Point & P,double & dist) {};
restituisce anche la distanza fra punto 3D di partenza e sua proiezione.
Infine il metodo
bool proiezandappart(const Point & P,double & dist,const Point & N)
{};
esegue gli stessi calcoli, però proiettando il punto P lungo la normale passata
come input.
4
Ricerca del triangolo stl di appartenenza della
proiezione di ogni nodo della mesh netgen
Sfruttando le funzioni descritte nel paragrafo precedente è possibile raggiungere l’obiettivo prefissato per questo lavoro: per ogni nodo della mesh netgen
si vuole calcolare la proiezione sulla superficie triangolata stl e trovare un
triangolo stl a cui questa appartenga. Dato che può succedere di trovare più
triangoli stl a cui appartiene la proiezione di un nodo netgen, si prenderà
in considerazione il triangolo di appartenenza a minima distanza dal nodo
netgen stesso.
Per effettuare questa ricerca sono stati implementati tre metodi diversi,
che verranno confrontati nel paragrafo successivo applicandoli ad un caso
test e ad un caso reale.
4.1
Metodo brute force
Questo metodo viene definito a forza bruta perchè usa due cicli for innestati,
uno sui nodi netgen ed uno sui triangoli stl, e viene implementato per primo
essendo il più immediato in quanto usa semplicemente il penultimo metodo introdotto nel paragrafo precedente, senza ulteriori calcoli. Il problema
21
collaterale è che la complessità computazionale di questo metodo crescerà
quadraticamente all’aumentare dei nodi e dei triangoli delle mesh.
Per ogni nodo netgen cicliamo, infatti, su tutti i triangoli stl, andando
a calcolare la proiezione del nodo netgen sul piano individuato da ciascun
triangolo e a verificare se essa appartiene al triangolo considerato di volta in
volta. Fra tutti i triangoli stl a cui appartiene la proiezione del nodo netgen
prendiamo quello a minima distanza e lo scriviamo in un file.txt e in un
vettore. Otteniamo quindi il triangolo stl di appartenenza per ogni nodo del
mesh netgen.
Il codice implementato nel main è il seguente:
vector<int> idtriangapp;
idtriangapp.reserve(npMn);
int idt(0);
double dist;
ofstream out("appBrutale.txt");
for(int pn=0;pn<npMn;++pn) //per ogni nodo della mesh netgen
{cout<<"\n\npunto "<<pn<<" della mesh netgen"<<endl;
idt=0;
dis=0;
dist=1000;
for(int ts=0;ts<ntMs2;++ts) // per ogni triangolo della mesh stl
{bool app=Ms2.triangolo(ts).proiezandappart(Mn.nodo(pn),dis);
cout<<"il punto appartiene al triangolo? "<<app<<endl;
cout<<"dis: "<<dis<<endl;
if(app==1)
{
if(dis<dist)
{
idt=Ms2.triangolo(ts).getID(); dist=dis;
}
cout<<"id:"<<idt<<endl<<"dist: "<<dist<<endl;
}
}
out<<"la proiezione a minima distanza del punto "<<Mn.nodo(pn)<<
" della mesh netgen appartiene al "<<Ms2.triangolo(idt)<<" della mesh stl"<<endl;
idtriangapp.push_back(idt);
}
22
4.2
Metodo con ricerca del nodo stl più vicino al nodo netgen
Questo metodo è meno immediato del primo perchè prevede il calcolo del
patch di ogni nodo stl, cioè la creazione di una lista contenente gli ID di
tutti i triangoli di cui il nodo stl è un vertice. Questi saranno i triangoli su
cui cicleremo per trovare il triangolo stl di appartenenza della proiezione di
ogni nodo netgen, velocizzando notevolmente la ricerca.
Per ogni nodo netgen cicliamo su tutti i nodi stl per cercare quello a distanza minima. Questo doppio ciclo è molto più veloce di quello considerato
nel metodo a forza bruta perchè serve per svolgere una semplice operazione
di calcolo della distanza e non per la ricerca del triangolo di appartenenza.
Questa viene eseguita solo sui triangoli appartenenti al patch del nodo stl
più vicino ad ogni nodo netgen. Anche in questo caso consideriamo solo il
triangolo stl di appartenenza a distanza minima.
Riportiamo di seguito il codice scritto nel main:
vector<list<int> > intorno;
intorno=Ms2.intornopunto();
vector<int> idtriangapp1;
idtriangapp1.reserve(npMn);
ofstream patch("apppatch.txt");
double dismin;
vector<double> distemp1;
distemp1.reserve(npMs2);
int idt1(0);
double dis1;
for(int pn=0; pn<npMn;++pn)
{cout<<"\n\npunto "<<pn<<" della mesh netgen"<<endl;
distemp1.clear();
idt1=0;
dismin=1000;
dis=0;
dis1=1000;
for(int ps=0; ps<npMs2; ++ps)
{
dis=distanza(Mn.nodo(pn),Ms2.nodo(ps)); cout<<"dis: "<<dis<<endl;
distemp1.push_back(dis);
}
cout<<"tot distanze: "<<distemp1.size()<<endl;
23
for(int ps=0; ps<distemp1.size(); ++ps)
{
dismin=min(dismin,distemp1.at(ps));
}
cout<<"distanza min: "<<dismin<<endl;
int idtemp(0),idtemp1(0);
for(int i=0; i<distemp1.size(); ++i)
{
if(distemp1.at(i)!=dismin)
{++idtemp1; cout<<"idtemp1: "<<idtemp1<<endl;}
else {idtemp=idtemp1; cout<<"idtemp: "<<idtemp<<endl;}
}
Point Pmindis=Ms2.nodo(idtemp);
cout<<"punto a minima distanza: "<<Pmindis<<endl;
list<int>::iterator p1=intorno.at(idtemp).begin();
dis=0;
for(int j=0; j<intorno.at(idtemp).size();++j)
{
bool appart=Ms2.triangolo(*p1).proiezandappart(Mn.nodo(pn),dis);
cout<<"il punto appartiene al triangolo? "<<appart<<endl;
cout<<"dis: "<<dis<<endl;
if(appart==1)
{
if(dis<dis1)
{
idt1=Ms2.triangolo(*p1).getID();
dis1=dis;
}
cout<<"id:"<<idt1<<endl<<"dis1: "<<dis1<<endl;
}
++p1;
}
patch<<"la proiezione del punto "<<Mn.nodo(pn)<<" della mesh netgen
appartiene al "<<Ms2.triangolo(idt1)<<" della mesh stl"<<endl;
patch<<"punto a minima distanza: "<<Pmindis<<endl;
idtriangapp1.push_back(idt1);
}
24
4.3
Metodo con algoritmo di ricerca del nearest neighbor
Questo metodo è ancora più avanzato e veloce del secondo perchè, al posto
di ciclare su tutti i nodi stl per cercare quello più vicino ad ogni nodo netgen,
usa un algoritmo di ricerca del nearest neighbor appartenente ad una libreria
esterna specializzata nell’implementazione di questo codice: la libreria ANN.
Questa libreria supporta la ricerca esatta e approssimata di k nearest
neighbor, con k definito dall’utente e nel nostro caso = 1, in spazi di qualsiasi dimensione. Un set di punti dati, nel nostro caso il vettore di nodi
della superficie stl, viene preprocessato per costruire una struttura dati, più
precisamente un kd-tree, in modo che la ricerca del nearest neighbor per ogni
nodo netgen sia più veloce ed efficiente.
I kd-tree sono data structure basate su suddivisioni ortogonali ricorsive
dello spazio in iperrettangoli disgiunti chiamati cells o boxes. Ogni nodo
dell’albero è associato ad una di queste regioni ed al sottoinsieme di punti che giacciono in essa. La radice dell’albero è legata ad un box limitato
che contiene tutti i punti dati, infatti è costituita da un puntatore al set di
punti dati. Inoltre fornisce due ulteriori informazioni: la splitting rule ed
una costante piccola chiamata bucket size. Queste informazioni sono necessarie per un nodo arbitrario dell’albero. Difatti, finchè il numero di punti
associati a questo nodo è maggiore del bucket size il box viene suddiviso
in due celle più piccole da un iperpiano con asse ortogonale che interseca il
box stesso; la selezione dell’iperpiano dipende dalla regola di suddivisione
considerata. I due box ottenuti vengono associati a due nodi figli del nodo in esame; i punti appartenenti al box originario sono suddivisi nelle due
celle derivate a seconda del lato dell’iperpiano in cui si trovano, quelli che
giacciono sull’iperpiano possono essere assegnati ad entrambi i box oppure
vengono ridistribuiti secondo la splitting rule. Quando il numero di punti
associati ad un nodo è minore del bucket size, questo nodo viene considerato una foglia e non viene più suddiviso. La struttura gerarchica dell’albero
viene sfruttata nell’algoritmo di ricerca chiamato standard search, che si
basa su un calcolo incrementale della distanza fra un nodo netgen e le celle
associate ai nodi dell’albero. Questo algoritmo opera ricorsivamente: quando si incontra un nodo del kd-tree, l’algoritmo prima visita il figlio che è
più vicino al punto netgen, in seguito, se l’altro figlio giace ad una distanza
compresa fra 1 e 1 + ε volte la distanza dal più vicino punto trovato fino a
quel momento, allora anche questo nodo viene visitato ricorsivamente.
Una volta trovato il nearest neighbor di ogni nodo netgen si procede
25
come nel secondo metodo: si cerca il triangolo stl di appartenenza a minima
distanza fra i triangoli appartenenti al patch del nearest neighbor calcolato.
Il codice implementato nel main è il seguente:
int k = 1;
// number of nearest neighbors
int dim = 3;
// dimension
double eps = 0;
// error bound
int m_pts = 1000000;
// maximum number of data points
int n_pts;
// actual number of data points
ANNpointArray data_pts;
// data points
ANNpoint query_pt;
// query point
ANNidxArray nn_idx;
// near neighbor indices
ANNdistArray dists;
// near neighbor distances
ANNkd_tree *the_tree;
// search structure
query_pt = annAllocPt(dim);
// allocate query point
data_pts = annAllocPts(m_pts, dim);
// allocate data points
nn_idx = new ANNidx[k];
// allocate near neigh indices
dists = new ANNdist[k];
// allocate near neighbor dists
n_pts = 0;
// read data points
for(int i=0; i<npMs2; ++i)
{
data_pts[i][0]=Ms2.nodo(i).x;
data_pts[i][1]=Ms2.nodo(i).y;
data_pts[i][2]=Ms2.nodo(i).z;
n_pts++;
}
cout<<"numero data points: "<<n_pts<<endl;
cout<<"Data Points:\n";
for(int j=0;j<n_pts;++j)
{
printPt(cout, data_pts[j]);
}
the_tree = new ANNkd_tree(
// build search structure
data_pts,
// the data points
n_pts, // number of points
dim);
// dimension of space
for(int i=0; i<npMn; ++i)
// read query points
{
26
query_pt[0]=Mn.nodo(i).x;
query_pt[1]=Mn.nodo(i).y;
query_pt[2]=Mn.nodo(i).z;
cout << "Query point: "; // echo query point
printPt(cout, query_pt);
the_tree->annkSearch(
// search
query_pt,
// query point
k,
// number of near neighbors
nn_idx, // nearest neighbors (returned)
dists,
// distance (returned)
eps);
// error bound
std::ofstream ofile;
ofile.open("nearestneig.txt",std::ios::app);
for (int i = 0; i < k; i++) { // print summary
printPt( ofile, data_pts[ nn_idx[i] ] );
};
ofile.close();
}
//leggo il file nearestneig.txt e creo un vettore di punti
//mettendo gli ID corretti
Nodes vicini=creanearestneighbor("nearestneig.txt", Ms2.nodi());
//metodo che usa i nearest neighbor per trovare il triangolo della mesh stl
//di appartenenza di ogni nodo della mesh netgen a minima distanza
vector<int> idtriangapp2;
idtriangapp2.reserve(npMn);
int idt2(0);
double dis2;
ofstream patch1("apppatchnn.txt");
for(int k=0; k<vicini.size(); ++k)
{idt2=-1;
dis2=1000;
dis=0;
int id=vicini.at(k).ID;
list<int>::iterator p2=intorno.at(id).begin();
for(int j=0; j<intorno.at(id).size();++j)
{
bool appart=Ms2.triangolo(*p2).proiezandappart(Mn.nodo(k),dis);
27
cout<<"il punto appartiene al triangolo? "<<appart<<endl;
cout<<"dis: "<<dis<<endl;
if(appart==1)
{
if(dis<dis2)
{
idt2=Ms2.triangolo(*p2).getID();
dis2=dis;
}
cout<<"id:"<<idt2<<endl<<"dis2: "<<dis2<<endl;
}
++p2;
}
if(idt2!=-1)
{
patch1<<"la proiezione del punto "<<Mn.nodo(k)<<" della mesh netgen
appartiene al "<<Ms2.triangolo(idt2)<<" della mesh stl"<<endl;
}
else
{ patch1<<"la proiezione del punto "<<Mn.nodo(k)<<" della mesh netgen
non appartiene a nessun triangolo stl del patch del nearest neighbor"<<endl;
}
patch1<<"punto a minima distanza: "<<vicini.at(k)<<endl;
idtriangapp2.push_back(idt2);
}
5
Applicazioni e problemi riscontrati
I tre metodi descritti nel paragrafo precedente sono stati applicati a due casi
differenti.
Il primo può essere considerato un caso test per verificare il corretto funzionamento del codice implementato, in quanto è costituito da una geometria molto semplice: un cilindro. Su questo cilindro sono state costruite due
mesh: una più lasca (formata da 146 nodi) che sarà la mesh netgen presa in
considerazione (vedere figura 7) ed una più fine (costituita da 846 nodi) che
sarà, invece, la superficie triangolata stl (vedere figura 8). Facendo girare i
tre codici su queste due mesh note si ottengono esattamente gli stessi risul-
28
Figura 7: Caso test del cilindro: mesh netgen.
Figura 8: Caso test del cilindro: superficie stl.
tati in tempi confrontabili e rapidi. Se si osservano bene i nearest neighbor
dei nodi netgen si vede che hanno coordinate molto vicine ai nodi netgen
stessi e si può notare che il triangolo stl di appartenenza correttamente fa
parte del patch del nearest neighbor (vedere figura 9). Inoltre, considerando
casualmente alcuni nodi netgen e visualizzandoli insieme alla loro proiezione
e al triangolo stl di appartenenza calcolato (vedere figura 6), si può osservare
che la proiezione correttamente appartiene al triangolo.
Questo esempio ha quindi permesso di ottenere risultati sensati in accordo
con l’implementazione del codice e di osservare che, per mesh molto piccole,
il costo computazionale ed il tempo di calcolo dei tre metodi è all’incirca
uguale.
29
Figura 9: Un nodo netgen con la sua proiezione, il suo nearest neighbor ed
il patch di quest’ultimo.
Il secondo caso, invece, è un caso reale: un arco aortico ricostruito a
partire da immagini TAC. Anche in questo caso si dispone di una superficie
triangolata stl, costituita però da tantissimi nodi (82674), e di una mesh
netgen formata da 3107 nodi (vedere figura 10). Applicando i tre metodi si
(a) mesh netgen
(b) superficie stl
Figura 10: Caso reale dell’aorta.
30
ottengono gli stessi risultati per il secondo ed il terzo, ma non per il primo,
perchè in questo caso reale l’irregolarità della superficie comporta il fatto
che alcuni nodi netgen abbiano una proiezione che non appartiene ad alcun
triangolo del patch del nearest neighbor. Quindi, con il secondo e il terzo
metodo non si riesce ad ottenere il triangolo stl di appartenenza per un numero elevato di nodi netgen; mentre con il metodo a forza bruta ciò succede
solo per pochi nodi, dato che in questo metodo si considera un ciclo su tutti i
triangoli stl. Perciò è necessario apportare dei cambiamenti che permettano
di trovare un triangolo stl di appartenenza anche per questi nodi netgen. La
prima idea è stata quella di calcolare una nuova normale, media delle normali ai triangoli appartenenti al patch dei nearest neighbor di questi nodi,
e di proiettare il nodo netgen lungo questa normale per vedere se la nuova
proiezione appartenga a uno dei triangoli del patch oppure no. Purtroppo
questo cambiamento apporta pochissimi miglioramenti e permette di trovare
il triangolo stl di appartenenza solo per un numero molto ridotto di nodi
netgen. Si è quindi deciso di implementare due algoritmi che abbandonano
l’idea della ricerca del triangolo di appartenenza e cercano i tre nodi stl più
vicini ad ogni nodo netgen in maniera alternativa. Questi due algoritmi sono
descritti nel paragrafo successivo.
In questo caso reale si può osservare che il costo computazionale, il tempo
di calcolo e l’efficienza variano notevolmente nei tre metodi per mesh con
numero di nodi molto elevato. Il primo metodo, infatti, impiega 30 ore per
il calcolo, il secondo 3 ore e 20 minuti, mentre il terzo pochi minuti.
Per il vaso aortico, inoltre, abbiamo a disposizione anche gli spostamenti dei
nodi stl in dieci tempi differenti. Perciò è stato possibile implementare il
codice che calcola, una volta noti i tre punti stl più vicini ad ogni nodo netgen, l’interpolazione degli spostamenti di questi tre nodi e fornisce lo spostamento di ogni nodo netgen per tutti i tempi, permettendo di ricostruire la
mesh netgen necessaria per i successivi calcoli fluidodinamici.
6
Algoritmi alternativi
Per risolvere il problema riscontrato nell’applicazione del codice implementato al caso reale dell’aorta, sono stati scritti due algoritmi alternativi che
calcolano i tre nodi stl più vicini ad ogni nodo netgen, non con la ricerca del
triangolo stl di appartenenza della proiezione, ma in modo differente.
Il primo algoritmo sfrutta la libreria ANN per la ricerca diretta dei tre
31
nearest neighbor. Come è già stato detto, infatti, questa libreria permette di
calcolare in modo esatto ed approssimato i primi k nearest neighbor. Finora
questa costante era stata posta = 1, per implementare il nuovo codice basta
settare k = 3 ed i tre punti stl più vicini ad ogni nodo netgen vengono
calcolati automaticamente.
Il codice implementato per questo algoritmo è il seguente:
int k = 3;
// number of nearest neighbors
int dim = 3;
// dimension
double eps = 0;
// error bound
int m_pts = 1000000;
// maximum number of data points
int n_pts;
// actual number of data points
ANNpointArray data_pts;
// data points
ANNpoint query_pt;
// query point
ANNidxArray nn_idx;
// near neighbor indices
ANNdistArray dists;
// near neighbor distances
ANNkd_tree *the_tree;
// search structure
query_pt = annAllocPt(dim);
// allocate query point
data_pts = annAllocPts(m_pts, dim);
// allocate data points
nn_idx = new ANNidx[k];
// allocate near neigh indices
dists = new ANNdist[k];
// allocate near neighbor dists
n_pts = 0;
// read data points
for(int i=0; i<noditriang.size(); ++i)
{
data_pts[i][0]=noditriang.at(i).x;
data_pts[i][1]=noditriang.at(i).y;
data_pts[i][2]=noditriang.at(i).z;
n_pts++;
}
cout<<"numero data points: "<<n_pts<<endl;
cout<<"Data Points:\n";
for(int j=0;j<n_pts;++j)
{
printPt(cout, data_pts[j]);
}
the_tree = new ANNkd_tree(
// build search structure
data_pts,
// the data points
n_pts, // number of points
32
dim);
// dimension of space
for(int i=0; i<npMn; ++i)
// read query points
{
query_pt[0]=Mn.nodo(i).x;
query_pt[1]=Mn.nodo(i).y;
query_pt[2]=Mn.nodo(i).z;
cout << "Query point: "; // echo query point
printPt(cout, query_pt);
the_tree->annkSearch(
// search
query_pt,
// query point
k,
// number of near neighbors
nn_idx, // nearest neighbors (returned)
dists,
// distance (returned)
eps);
// error bound
std::ofstream ofile;
ofile.open("nearestneig3.txt",std::ios::app);
for (int i = 0; i < k; i++) {
// print summary
printPt( ofile, data_pts[ nn_idx[i] ] );
};
ofile.close();
}
//leggo il file nearestneig3.txt e creo un vettore di punti
//mettendo gli ID corretti
Nodes vicini=creanearestneighbor("nearestneig3.txt", Ms2.nodi());
Il secondo algoritmo, invece, calcola il baricentro di ogni triangolo stl
sfruttando il metodo apposito della classe Triangle. Successivamente effettua
la ricerca del baricentro più vicino ad ogni nodo netgen, sempre usando la
ricerca del nearest neighbor della libreria ANN. Infine considera come tre
punti stl più vicini ad ogni nodo netgen i vertici del triangolo col baricentro
più vicino.
Riportiamo di seguito il codice che implementa questo algoritmo:
//creo il vettore dei baricentri per la ricerca di quello + vicino
//ad ogni nodo della mesh netgen
Nodes baricentritriang;
baricentritriang.reserve(ntMs2);
for(int i=0;i<ntMs2;++i)
{Point B;
33
B=Ms2.triangolo(i).baricentro();
B.ID=Ms2.triangolo(i).getID();//B.ID=i;
baricentritriang.push_back(B);
}
int k = 1;
// number of nearest neighbors
int dim = 3;
// dimension
double eps = 0;
// error bound
int m_pts = 1000000;
// maximum number of data points
int n_pts;
// actual number of data points
ANNpointArray data_pts;
// data points
ANNpoint query_pt;
// query point
ANNidxArray nn_idx;
// near neighbor indices
ANNdistArray dists;
// near neighbor distances
ANNkd_tree *the_tree;
// search structure
query_pt = annAllocPt(dim);
// allocate query point
data_pts = annAllocPts(m_pts, dim);
// allocate data points
nn_idx = new ANNidx[k];
// allocate near neigh indices
dists = new ANNdist[k];
// allocate near neighbor dists
n_pts = 0;
// read data points
for(int i=0; i<baricentritriang.size(); ++i)
{
data_pts[i][0]=baricentritriang.at(i).x;
data_pts[i][1]=baricentritriang.at(i).y;
data_pts[i][2]=baricentritriang.at(i).z;
n_pts++;
}
cout<<"numero data points: "<<n_pts<<endl;
cout<<"Data Points:\n";
for(int j=0;j<n_pts;++j)
{
printPt(cout, data_pts[j]);
}
the_tree = new ANNkd_tree(
// build search structure
data_pts,
// the data points
n_pts, // number of points
dim);
// dimension of space
for(int i=0; i<npMn; ++i)
// read query points
34
{
query_pt[0]=Mn.nodo(i).x;
query_pt[1]=Mn.nodo(i).y;
query_pt[2]=Mn.nodo(i).z;
cout << "Query point: "; // echo query point
printPt(cout, query_pt);
the_tree->annkSearch(
// search
query_pt,
// query point
k,
// number of near neighbors
nn_idx, // nearest neighbors (returned)
dists,
// distance (returned)
eps);
// error bound
std::ofstream ofile;
ofile.open("nearestbar.txt",std::ios::app);
for (int i = 0; i < k; i++) {
// print summary
printPt( ofile, data_pts[ nn_idx[i] ] );
};
ofile.close();
}
Questi due algoritmi permettono di trovare i tre nodi stl più vicini ad
ogni nodo netgen e quindi di calcolare il suo spostamento. Confrontando gli
spostamenti dei nodi netgen ottenuti a partire da questi due codici, si può
osservare che sono molto simili e per tantissimi nodi addirittura uguali. Perciò, a questo punto, è possibile ricostruire la mesh netgen superficiale ad ogni
istante temporale, estenderla con l’harmonic extension a mesh volumetrica
e procedere coi calcoli fluidodinamici.
Riferimenti bibliografici
[1] Daoqi Yang. “C++ AND OBJECT-ORIENTED NUMERIC COMPUTING for Scientists and Engineers”. Springer.
[2] David M. Mount. “ANN Programming Manual ”. University of
Maryland, College Park, Maryland 1998.
[3] Renato Betti. “Lezioni di Geometria”. Masson, Milano 1996.
[4] Wikipedia, the free encyclopedia: http://en.wikipedia.org
35