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