Praktikum Medizinische Bildverarbeitung in C#: „Volume

Transcription

Praktikum Medizinische Bildverarbeitung in C#: „Volume
Praktikum Medizinische Bildverarbeitung in C#:
„Volume-Rendering“
Technische Universität München / Lehrstuhl Informatik IX
Dr. Heiko Gottschling Prof. Dr. Bernd Radig
SS 2007
Blatt 5
Aufgabe 24
In den nächsten Aufgaben wird Ihr Programm um eine OpenGL-basierte 3D-Ansicht
erweitert. Diese soll alternativ zum SliceView dargestellt werden, d.h. der Benutzer soll
zwischen der Anzeige der einzelnen Schichten und der 3D-Ansicht frei wählen können.
Windows bietet hierfür so genannte Tabs an, wie Sie sie sicher schon viele Male in
diversen anderen Programmen gesehen haben.
Fügen Sie eine Instanz der Klasse TabControl aus dem Namensraum
System.Windows.Forms Ihrer Anwendung hinzu. Erzeugen Sie eine
TabPage, die einen SliceView enthält, und fügen Sie diese zur
TabControl hinzu.
Wie funktioniert das TabControl-Fenster?
Ein TabControl-Fenster besteht aus mehreren TabPage-Objekten. Diese sind Fenster, die
mit einem Tab der TabControl verbunden sind. Die Kindfenster einer TabPage legen fest,
was angezeigt werden soll, wenn der zugehörige Tab aktiviert ist, z.B:
// Erzeugen einer TabPage
TabPage t = new TabPage( “Erster Tab“ );
// Einhaengen in die TabControl (=this)
this.TabPages.Add( t );
// Inhalt von t:
// c ist ein Control oder eine davon abgeleitete Klasse
c.Dock = DockStyle.Fill;
t.Controls.Add( c );
Aufgabe 25
Auf diesem Blatt wird die Applikation um eine OpenGL-Ansicht erweitert, d.h. um ein
Fenster, in dem eine 3D-Szene dargestellt werden kann.
OpenGL ist eine Bibliothek für 3D-Grafik mit der auch eine eventuell vorhandene
Hardwarebeschleunigung durch die Grafikkarte ausgenutzt werden kann. Die
Programmierschnittstelle der OpenGL-Bibliothek ist in erster Linie für die
Programmiersprache „C“ definiert worden, es gibt aber auch Varianten für viele
andere Sprachen.
Seite 1 von 7
NeHe OpenGL-Tutorials
Für die Bearbeitung unserer Praktikumsaufgaben zwar nicht zwingend notwendig, aber mitunter
trotzdem sehr hilfreich sind die unter http://nehe.gamedev.net/ zu findenden NeHe OpenGLTutorials, die Schritt für Schritt die wichtigesten OpenGL-Features ausführlich behandeln und
Beispielprogramme für nahezu alle üblichen Programmiersprachen bereitstellen.
Die in diesem Praktikum verwendete sprachspezifische Variante für C# ist die
Bibliothek Tao (http://www.mono-project.com/Tao), die aber für das
Praktikum weiter in einer Klasse PGL gekapselt ist. Den Quellcode für PGL können
Sie sich von den WWW-Seiten des Praktikums herunter laden und Ihrem Projekt
hinzufügen. Auf den Seiten finden sie außerdem auch die Dokumentation für PGL.
Die Klasse PGL führt einen neuen Namespace Rendering ein. Erzeugen Sie in
Ihrem Projekt hierfür analog zum Namespace Volume ein eigenes Verzeichnis.
Die OpenGL-Funktionalität wird durch statische Methoden der Klasse PGL
bereitgestellt, d.h. sie werden durch das Voranstellen des Klassennamens PGL
aufgerufen, z.B.
PGL.glClearColor(0.0f,0.0f,0.0f,1.0f);
Die Kommandos gelten jeweils für das momentan aktive Fenster mit OpenGLAnbindung.
Erzeugen Sie nun eine Klasse namens OpenGLView, die ein Fenster mit OpenGLAnbindung darstellt und für erste Testzwecke den gesamten Fensterinhalt in einer
Farbe löscht. Die Farbe soll dabei aus dem Document über eine Property Color
Background ermittelt werden, die initial den Wert Color.Black besitzt.
Die neue Klasse sollte von SimpleOpenGlControl aus dem Namensraum
Tao.Platform.Windows ableiten. Den Code zum Zeichnen des Fensterinhalts,
also den eigentliche OpenGL-Code, können Sie in der Methode protected
override void OnPaint (PaintEventArgs args) in Ihrem
OpenGLView platzieren, die automatisch vom .NET-Framework aufgerufen wird.
Dabei wird auch das Fenster für die OpenGL-Ausgabe aktiviert.
Seite 2 von 7
Wie löscht man ein Fenster in OpenGL?
Hierzu setzen Sie einfach mittels der Methode PGL.glClearColor() die entsprechende
Hintergrundfarbe im RGBA-Format (die Komponenten sind im Intervall [0,1]) und löschen den
Ausgabebereich mittels PGL.glClear(PGL.GL_COLOR_BUFFER_BIT); Der Parameter von
glClear legt dabei fest, was gelöscht werden soll. GL_COLOR_BUFFER_BIT steht dabei für den
so genannten Framebuffer, d.h. das per OpenGL dargestellte Bild.
Weitere Initialisierungen
Damit das OpenGL-Fenster richtig initialisiert wird, sind noch folgende Schritte zu unternehmen:
●
Als erster Aufruf im Konstruktor der Klasse OpenGLView ist die Methode
InitializeContexts() zu wählen. Davor sind die Einstellungen für die generellen
Datenformate in OpenGL zu wählen. Verwenden sie dafür folgenden Code:
ColorBits
= 32;
DepthBits
= 16;
StencilBits
= 8;
InitializeContexts();
●
Nehmen Sie im Konstruktor die weiteren Initialisierungen für ein UserControl vor, wie
sie es in früheren Arbeitsblättern gelernt haben.
●
Ergänzen Sie wie folgt den Rumpf der überschriebenen OnPaint-Methode:
protected override void OnPaint( PaintEventArgs args )
{
base.OnPaint(args);
// Aktivierung des OpenGL Contexts von diesem Fenster
MakeCurrent();
// Setze Zeichenbereich
Gl.glViewport( 0, 0, ClientSize.Width,
ClientSize.Height );
.... // Aufbau der Szene
}
// Szene anzeigen (Double Buffering!)
SwapBuffers();
Seite 3 von 7
Aufgabe 26
Natürlich ist ein einfarbiges Fenster auf die Dauer zu langweilig. Zeichnen Sie
deshalb ein Dreieck, das durch die Mittelpunkte der oberen, unteren und linken
Fensterkante definiert ist. Dabei soll jeder Eckpunkt des Dreiecks eine andere Farbe
(Ihrer Wahl) besitzen.
Der OpenGL-Zeichenbereich geht, wenn nicht anders eingestellt, von (-1,1) bis (1,-1).
Zeichnen Sie Ihr Dreieck in der xy-Ebene.
Wie zeichnet man etwas mit OpenGL?
Das Zeichnen erfolgt in OpenGL nach folgendem Schema:
PGL.glBegin( <Interpretation der Punktdaten> );
// Daten erster Punkt
PGL.glVertex3d( <x>, <y>, <z> );
// Daten zweiter Punkt
PGL.glVertex3d( <x>, <y>, <z> );
…
PGL.glEnd() ;
D.h. es wird eine Menge von Punkten angegeben, die zu grösseren Konstrukten zusammengefasst
werden. Wie das geschieht entscheidet der Parameter von PGL.glBegin().
Typische Werte sind hier z.B. PGL.GL_TRIANGLE_STRIP oder PGL.GL_TRIANGLES.
Punkt 4
Punkt 2
Punkt 4
Punkt 2
Punkt 6
Punkt 6
Punkt 3
Punkt 3
Punkt 5
Punkt 5
Punkt 1
GL_TRIANGLE_STRIP
Punkt 1
GL_TRIANGLES
Wie setzt man die Farbe eines Vertex?
Mittels PGL.glColor3d(<r>, <g>, <b>); kann die Farbe für die Vertices gesetzt werden,
die in den nachfolgenden glVertex3d() Aufrufen erzeugt werden.
Hier sieht man auch, dass in OpenGL mit der Endung „3d“ nicht „dreidimensional“ gemeint ist.
OpenGL kodiert stattdessen Anzahl und Typ der Parameter der Funktion: 3d steht für „Parameterliste
besteht aus drei double-Werten“. Es gibt z.B. auch die Endungen 3f, 2i ... wobei f für float und
i für int steht.
Seite 4 von 7
Aufgabe 27
Natürlich können in OpenGL Objekte auch gedreht und verschoben werden. Die
hierfür nötigen Funktionen lauten:
•
•
PGL.glRotated(<Winkel in Grad>, <x>, <y>, <z>) für die
Drehung um die Achse (x,y,z). Die Achse geht dabei immer durch den
Ursprung (0,0,0).
PGL.glTranslated(<x>, <y>, <z>) für eine Verschiebung. Der
Vektor (x,y,z) wird auf die Position der Vertices aufaddiert.
Diese Befehle erzeugen jeweils eine entsprechende Transformationsmatrix, deren
genauer Aufbau für dieses Praktikum aber nicht wichtig ist, und multiplizieren diese
auf eine globale Transformationsmatrix auf, die für jeden nachfolgenden Vertex
angewandt wird. Dadurch kann man die Transformationen beliebige kombinieren.
Die Reihenfolge ist dabei rückwärts im Vergleich zu der Reihenfolge der Befehle, d.h.
die zuletzt erzeugte Transformation wird zuerst angewandt.
Um die globale Transformationsmatrix zu löschen, d.h. auf eine Matrix
zurückzusetzen, welche die Positionen nicht verändert, wird der Befehl
PGL.glLoadIdentity() verwendet. Das Zurücksetzen der globalen Matrix
empfiehlt sich auch zu Beginn des gesamten Zeichenvorgangs, damit immer mit einer
neuen, frischen Matrix gerechnet wird.
Modifizieren Sie Ihr Programm nun so, dass die folgenden Transformationen auf das
Dreieck angewendet werden:
Drehung um rotX-Grad um die X-Achse
Drehung um rotY-Grad um die Y-Achse
Die Winkel rotX und rotY sollen auf intuitive Weise mit der Maus bei gedrückter
linken Maustaste veränderbar sein.
Aufgabe 28
Die Drehung des Dreiecks in Aufgabe 27 sieht momentan noch etwas unnatürlich aus, da
OpenGL standardmäßig ein Kameramodell verwendet, das auf der Parallelprojektion basiert.
Dabei werden Objekte unabhängig von der Entfernung zum Beobachter immer gleich groß
dargestellt. Ein realistischeres Ergebnis lässt sich durch Verwendung der Zentralprojektion
erreichen, bei der sich alle Projektionsstrahlen in einem Brennpunkt (dem Auge des
Beobachters treffen).
Schalten Sie für Ihren OpenGLView die Zentralprojektion ein.
Die verschiedenen Projektionsarten sind wie die Rotation oder die Translation
spezielle
Transformationsmatrizen,
die
auf
die
aktuelle,
globale
Transformationsmatrix aufgerechnet werden können.
Seite 5 von 7
Damit die Projektion von den übrigen Transformationen getrennt ist, kann eine andere
globale Transformationsmatrix verwendet werden. Diese wird mittels
PGL.glMatrixMode(PGL.GL_PROJECTION); ausgewählt. Alle MatrixBefehle beziehen sich dann auf diese Matrix.
Mit PGL.glMatrixMode(PGL.GL_MODELVIEW); kann ggf. wieder auf die
normale Matrix zurückgeschaltet werden.
Wie verwendet man in OpenGL eine Zentralprojektion?
Zum Setzen der Zentralprojektion wird der Befehl
PGL.gluPerspective(<fieldOfView>, <aspectRatio>, <near>, <far>);
verwendet (nach PGL.glmatrixMode(...) ).
fieldOfView: “Sehwinkel” siehe Bild, z.B. 60 Grad
aspectRatio: Verhältnis von Bildbreite zu Bildhöhe, dadurch wird auch das Problem
der Bildverzerrung behoben, wenn das Fenster in seiner Größe verändert wird.
near clipping-plane, far clipping-plane: Abstand des Beobachters zu den
Beschneidungsebenen (Clipping-Planes) des visualisierten Volumens. Nur der Ausschnitt
zwischen den beiden Ebenen wird dargestellt. Sinnvolle Werte sind z.B. near = 1.0 und
far = 1000.0.
fieldOfView
Nur Objekte innerhalb
dieses Bereichs (ein
Pyramidenstumpf in
3D!) sind sichtbar
near clipping-plane
far clipping-plane
Warum wird das Dreieck nicht oder nicht ganz dargestellt?
Es werden nur die Anteile des Dreiecks dargestellt, die sich zwischen den beiden Clipping-Planes
befinden. Eventuell müssen Sie hier ihr Dreieck noch mittels PGL.glTranslated(0,0,-d);
geeignet in den visualisierten Bereich hineinschieben. Achten Sie dabei unbedingt auf die richtige
Reihenfolge von Rotation und Translation (das Dreieck soll sich weiterhin um die „eigenen“ Achsen
drehen wie zuvor)!
Achten Sie auch darauf, dass Sie die Projektionsmatrix mittels PGL.glLoadIdentity() vor
jedem neuen Aufbau der Projektion zurücksetzen.
Aufgabe 32
Seite 6 von 7
Mit PGL.glTranslated(0,0,-d); aus der letzten Hilfebox können Sie das
Objekt von der Kamera/von dem Beobachter wegschieben und dadurch auch eine Art
Zoom-Effekt erzielen.
Erweitern Sie Ihr Programm nun derart, dass der Wert von d mittels des
MouseWheels eingestellt werden kann.
Wie kann man das MouseWheel abfragen?
Die Verwendung des MouseWheels funktioniert ganz analog zu den übrigen Mouse-Events, durch
Überladen einer entsprechenden virtuellen Methode aus Control:
protected override void OnMouseWheel(MouseEventArgs args)
In args steht dann in der Property Delta um wieviele MouseWheel-Einheiten das Rad gedreht
wurde.
Ein kleines Problem gibt es aber noch: die MouseWheel-Events werden nur an das Fenster geschickt,
das momentan den so genannten Focus besitzt. Es ist deshalb sinnvoll, den Focus auf den
OpenGLView zu setzen, sobald die Maus in das Fenster geschoben wird. Letzteres Ereignis kann
mittels OnMouseEnter(EventArgs args) abgefangen werden. Das Setzen des Focus
geschieht einfach durch den Aufruf der Methode Focus() von Control.
Seite 7 von 7