Image Processing on GPU
Transcription
Image Processing on GPU
STUDENTEN Clemens Blumer Dominik Oriet BETREUER Marcus Hudritsch AUFTRAGGEBER Prof. Dr. Thomas Vetter Departement Informatik Universität Basel Experte Pascal Paysan Diplomarbeit DA_11_0506 Basel, Januar 2006 Diplomarbeit Image Processing on GPU Vorwort Im Rahmen unserer Diplomarbeit beschäftigten wir uns während 10 Wochen intensiv mit der GPUProgrammierung. Dabei durchlebten wir verschiedenste Höhen und Tiefen. Erfreulicherweise überwiegten rückblickend gesehen die Höhen klar. Ein grosser Dank geht an unseren Betreuer Marcus Hudrtisch, der uns viele Freiheiten liess, aber uns trotzdem dabei half, das Ziel der Arbeit nicht zu vergessen. Auch versuchte er uns jeweils in die richtige Bahn zu lenken und schenkte unserer Diplomarbeit grosses Interesse und viel Zeit, obwohl er noch viele andere Mitstudenten betreuen musste. Auch unserem Auftraggeber, Thomas Vetter und seiner Gruppe sind wir dankbar. Sie ermöglichten uns mit Ihrem Auftrag eine interessante Diplomarbeit und boten uns nebst einer guten Arbeitsumgebung und technischer Ausrüstung auch stets fachliche Unterstützung. Last but not least möchten wir auch all denjenigen Personen danken, die uns in sonst irgendeiner Form unterstützten und hier nicht aufgeführt sind. Basel, Januar 2006 Seite 2/42 Diplomarbeit Image Processing on GPU Abstract Wir wollen aufzeigen, dass es grundsätzlich möglich ist, Bildverarbeitung auf der GPU zu betreiben. Die Stärken und Schwächen dieser Methodik machen wir ersichtlich, indem wir einen aufwändigen Algorithmus auf der Grafikhardware implementieren. Dieser Algorithmus, der den Optischen Fluss zweier Bilder berechnen kann, wird an der Uni Basel für gewisse Projekte gebraucht und wird durch unsere Implementierung drastisch beschleunigt. Damit unser erworbenes Wissen nicht verloren geht, beschreiben wir detailliert, wie auf der Grafikkarte Bildverarbeitung betrieben werden kann. Ausserdem zeigen wir alle herausgefundenen Tricks auf, um Fragmentprogramme möglichst optimal zu schreiben. Basel, Januar 2006 Seite 3/42 Diplomarbeit Image Processing on GPU Inhaltsverzeichnis Inhaltsverzeichnis.................................................................................................................................... 4 1 Ausgangslage und Ziele .................................................................................................................. 5 2 Konzept ........................................................................................................................................... 7 3 Bildverarbeitung: Optischer Fluss................................................................................................... 8 3.1 Optischer Fluss und Umkehrung............................................................................................. 8 3.2 Warp ........................................................................................................................................ 9 3.3 Gradienten, Eigenwerte und Eigenvektoren.......................................................................... 10 3.4 Farbbilder .............................................................................................................................. 13 4 Grundlagen (GP)GPU-Programmierung ....................................................................................... 14 4.1 Programmfluss mit der CPU ................................................................................................. 14 4.1.1 Laden der Textur ........................................................................................................... 14 4.1.2 Fragment Programm laden ............................................................................................ 15 4.1.3 Setzen des Viewports und der orthogonalen Projektion................................................ 15 4.1.4 Übergeben der Uniform Variablen................................................................................ 16 4.1.5 Quad zeichnen ............................................................................................................... 16 4.1.6 Abspeichern der Textur ................................................................................................. 16 4.2 Fragmentprogramm ............................................................................................................... 17 4.2.1 Simpler Vergleich von GPU und CPU Programmen .................................................... 17 4.2.2 Binomial-Filter Beispiel ................................................................................................ 17 4.2.2.1 Binomialfilter auf der CPU ....................................................................................... 18 4.2.2.2 Binomialfilter auf der GPU ....................................................................................... 18 4.2.2.3 Die Unterschiede ....................................................................................................... 18 4.2.3 Tipps & Tricks............................................................................................................... 19 4.2.3.1 Möglichst einfacher Programmfluss.......................................................................... 19 4.2.3.2 Möglichst viel vorberechnen ..................................................................................... 19 4.2.3.3 Framebuffer Objekte ................................................................................................. 19 4.2.3.4 Textur-Zugriffe.......................................................................................................... 19 4.2.4 Readback von der Textur oder des Framebuffers.......................................................... 20 4.2.4.1 Zeitmessungen am CannyEdge ................................................................................. 20 4.2.5 Render-to-Texture ......................................................................................................... 21 4.2.5.1 Beispiel mit zwei Texturen und einem Framebuffer Object ..................................... 22 5 Canny Edge auf der GPU .............................................................................................................. 24 6 Optischer Fluss auf der GPU......................................................................................................... 27 6.1 Ablauf des Optischen Flusses auf der GPU .......................................................................... 27 6.1.1 Die Klasse OpticalFlowGPU......................................................................................... 27 6.2 Benchmark ............................................................................................................................ 29 6.2.1 Flussfeld ........................................................................................................................ 29 6.2.2 Bildinitialisierung.......................................................................................................... 29 6.2.3 Morphing....................................................................................................................... 29 7 Demo-Applikation......................................................................................................................... 30 7.1 Technischer Aufbau .............................................................................................................. 31 7.1.1 Kurze Beschreibung des Aufbaus ................................................................................. 31 7.2 Funktionsumfang................................................................................................................... 32 8 Ausblick ........................................................................................................................................ 35 8.1 Einsatz bei Projekten von Thomas Vetter ............................................................................. 35 8.2 Image Processing on GPU..................................................................................................... 35 9 Fazit............................................................................................................................................... 37 A Screenshots.................................................................................................................................... 38 Quellenverzeichnis ................................................................................................................................ 41 Anhang: ................................................................................................................................................. 42 Basel, Januar 2006 Seite 4/42 Diplomarbeit Image Processing on GPU 1 Ausgangslage und Ziele Thomas Vetter hat einige Projekte am laufen, welche uns als Ausgangslage dienen. Es soll nicht etwas Neues für diese entwickelt werden. Vielmehr wollen wir herausfinden, ob es Sinn macht, für einzelne Teilbereiche (Bildverarbeitung) die GPU als Recheneinheit zu verwenden. Ein spezieller Aspekt unserer Arbeit gilt dabei dem Optischen Fluss. Dieser ist relativ interessant für Optimierungen, da er ziemlich rechenintensiv ist. Grundsätzlich ist es aber von Interesse zu wissen, wie die aktuelle Situation bezüglich der Bildverarbeitung auf der GPU aussieht. Das Ziel der Arbeit ist es, die Möglichkeiten der Bildverarbeitung auf der GPU darzulegen und im Speziellen eine Implementierung des Optischen Flusses auf der GPU umzusetzen. Von zentralem Interesse ist dabei jeweils der Performance-Gewinn. Zum besseren Verständnis dient ein kleiner Überblick, der aufzeigt, in welchem Bereich der Optische Fluss angewendet werden soll: Anhand einer Fotoaufnahme eines Gesichtes soll ein 3D-Modell des Kopfes entstehen. Dazu wird ein iterativer Prozess durchlaufen, der sich wiederholt. 1. Anhand von vorhandenen Parametern wird ein 3D-Modell des Kopfes erstellt. 2. Danach soll der Unterschied zwischen dem generierten und dem realen, fotografierten Gesicht durch den Optischen Fluss dargestellt werden. 3. Anhand des Flussfeldes kann eine Verbesserung der angewendeten Parameter vorgenommen werden. 4. Danach wird wiederum ein 3D-Modell des Kopfes mit den neuen Parametern erstellt und der Prozess wiederholt sich von vorne. Start Parameter-Set Start 3D-Modell rendern Optischer Fluss zw. Foto und 3D-Modell Ende Foto und Modell „identisch“? Parameter anhand vom Flussfeld optimieren Bild 1: Skizze Prozessablauf Basel, Januar 2006 Seite 5/42 Diplomarbeit Image Processing on GPU Nebst dem Verständnis der Bildverarbeitung auf der GPU ist auch das detaillierte Verständnis über den Optischen Fluss ein Ziel der Arbeit. Als letztes sollen dann die auf der GPU geschriebenen Algorithmen mit den Implementierungen auf der CPU verglichen werden. Basel, Januar 2006 Seite 6/42 Diplomarbeit Image Processing on GPU 2 Konzept Da der Ausgang der Arbeit offen und unsicher war, wurde das Konzept von Beginn an relativ flexibel gestaltet. Über das Thema Bildverarbeitung auf der GPU waren anfangs gar keine Kenntnisse vorhanden. Des Weiteren hatten wir noch sehr wenig Erfahrung in der GPU-Programmierung, weshalb wir uns zuerst darin einarbeiten wollten. Trotz des offenen Ausgangs und den wenigen Vorkenntnissen wurde ein Konzept mit einigen wenigen, aber wichtigen Eckpunkten erstellt. a) b) c) d) e) f) g) Einarbeitung in GPU-Programmierung und Tutorials Aktuellen Stand der Entwicklung anhand von bestehenden OpenSource-Projekten abschätzen Knowhow vertiefen anhand von kleinen Beispielen Optischen Fluss verstehen Optischen Fluss auf GPU implementieren Demo-Applikation mit GPU- und CPU-Version Dokumentation Tätigkeit/ Woche 1 2 3 4 5 6 7 8 9 10 a) b) c) d) e) f) g) Bild 2: Konzept Diplomarbeit Basel, Januar 2006 Seite 7/42 Diplomarbeit Image Processing on GPU 3 Bildverarbeitung: Optischer Fluss Laut Definition ist der Optische Fluss ein Vektorfeld zur Bewegungsangabe. Etwas detaillierter gemäss Quelle[13]: Mit dem Optischen Fluss wird ein Vektorfeld bezeichnet, das die 2D-Bewegungsrichtung und Geschwindigkeit für jeden Bildpunkt bzw. jedes Pixel einer Bildsequenz angibt. Das Konzept des Optischen Flusses wird zur Bewegungskontrolle in der visuellen Repräsentation verwendet und erfährt durch den automatisierten Datenfluss von digitalen Bildern einen starken Entwicklungsschub. Unbewusst setzen auch Menschen und Tiere dieses Prinzip ein. Quelle[13] Abschnitt „Anwendung bei der Biene und beim Menschen“: Bienen und andere Tierarten nutzen den Optischen Fluss, um Hindernissen auszuweichen und Abstände einfach schätzen zu können. Sie werten dazu offenbar die Bilder beider Facettenaugen aus und fliegen dann in die Richtung des Auges mit geringerem Optischen Fluss. Das führt auf einfachste Weise zu einer Flugbahn, die den größten Freiraum und die wenigsten Hindernisse vor sich hat. Ein ähnlicher Vorgang ist die Basis unserer alltäglichen Erfahrung im Fussgänger- und im Strassenverkehr: wir nehmen die Bewegung der anderen Verkehrsteilnehmer aus den Augenwinkeln wahr und berücksichtigen sie "unbewusst" bei der eigenen Fortbewegung. Schert hingegen ein Verkehrsteilnehmer aus diesem Fluss aus, ist unser Gesichtssinn sofort alarmiert und es wird teilweise sogar ein schützender Reflex ausgelöst, wie ein Sprung oder Kopf einziehen. Der Optische Fluss kommt somit ursprünglich aus der Natur und das Prinzip wurde für die Bildverarbeitung übernommen. Grundsätzlich wurde zum Verständnis des Optischen Flusses die Quelle [2] verwendet. In diesem Kapitel soll der Optische Fluss aber nicht in allen Details erklärt werden. Es werden nur die Punkte angesprochen, welche für das Verständnis wichtig und unserer Meinung nach oft schlecht erklärt sind. Für das allgemeine Verständnis wird jedoch die Quelle als bekannt vorausgesetzt. 3.1 Optischer Fluss und Umkehrung Das Flussfeld stellt für jeden Pixel in Bild 1 dar, wo sich dessen passender Pixel im Bild 2 befindet. Man erhält also für jeden Pixel in Bild 1 ein Vektor. Bild 3: Umkehrung Flussfeld Basel, Januar 2006 Seite 8/42 Diplomarbeit Image Processing on GPU Es wird angenommen, dass alle Pixel weiss sind, bis auf einer, welcher Rot ist. Dieser wird nun verschoben. Der Verschiebungsvektor für den roten Pixel ist der blaue Pfeil. Dieser zeigt auf die Position, wo im Bild 2 der rote Pixel neu zu finden ist. Der Vektor ist also korrekt. Geht man nun aber hin und kehrt diesen Vektor bzw. das Flussfeld, erhält man den grünen Pfeil. Nun wird ja oft angenommen, dass das umgekehrte Flussfeld die Veränderung von Bild 2 zu Bild 1 darstellt. Dies ist aber falsch. Es ist klar erkennbar, dass man nun an der Stelle des roten Pixels in Bild 2 nicht den grünen Pfeil hat. Dieser befindet sich an einem völlig falschen Ort. Für kleine Bewegungen mag dieser Fehler nicht bemerkt werden, jedoch darf das Flussfeld nicht einfach invertiert werden, um korrekte Ergebnisse zu erhalten. Will man den Fluss von Bild 2 zu Bild 1, muss eine neue Berechnung getätigt werden. 3.2 Warp Hier behandeln wir nur den Backward-Warp. Dieser ist deutlich einfacher und wird am häufigsten eingesetzt. Für den Forward-Warp müsste man auch wissen, wohin jeder Pixel wandert und nicht wo dessen passendes Gegenstück im 2. Bild ist. Beim Warp wird ein Bild durch ein Verschiebungsvektorfeld in ein anderes übergeführt. Es sei ein Verschiebungsvektorfeld (Dx,Dy) gegeben. Für jeden Pixel wird folgender neue Wert O(x,y) aus dem alten Wert I(x,y) berechnet: x' = x + Dx( x, y ) y ' = y + Dy ( x, y ) O( x, y ) = bilinear ( I ( x, y ), x' , y ' ) Dabei muss, da für x’ und y’ kaum Integer-Werte herauskommen, bilinear interpoliert werden: δx = Dx − floor ( Dx) δy = DY − floor ( Dy ) O ( x, y ) = (1 − δx)(1 − δy ) * I ( floor ( x' ), floor ( y ' )) + δx(1 − δy ) * I ( floor ( x' ) + 1, floor ( y ' )) + (1 − δx)δy * I ( floor ( x' ), floor ( y ' ) + 1) + δx + δy * I ( floor ( x' ) + 1, floor ( y ' ) + 1) Bsp.: (x,y) sei (0,0) Dx = 1.3 Dy = 1.3 δ x’ = 1 y’ = 1 δ x = 0.3 y = 0.3 Bild 4: Bilineare Interpolation Basel, Januar 2006 Seite 9/42 Diplomarbeit Image Processing on GPU An der Position (0,0) findet eine bilineare Interpolation aus den markierten Bereichen statt, wobei folgende Gewichtung gilt: (1,1): 0.49 (2,1): 0.21 (2,1): 0.21 (2,2): 0.09 1.00 Hinweis: Ein Warp darf nicht mit Morphing verwechselt werden. Beim Warpen wird nur die Geometrie verändert. Beim Morphen hingegen verändert sich sowohl die Geometrie als auch die Textur, wobei die Warp Funktion verwendet wird. 3.3 Gradienten, Eigenwerte und Eigenvektoren Zur Bestimmung des Flusses werden die Gradienten gebildet. Dies geschieht bezüglich x- und yRichtung und dem zeitlichen Gradienten. Die Gradienten entsprechen den Ableitungen: F2 ( x + 1, y ) − F2 ( x − 1, y ) 2 .0 F ( x, y + 1) − F2 ( x, y − 1) F y ( x, y ) = 2 2 .0 Ft ( x, y ) = F2 ( x, y ) − F1 ( x, y ) Fx ( x, y ) = Die so genannte Optical Flow Constraint wird durch folgende Gleichung dargestellt und stellt die Bedingungen für den Fluss dar: v x Fx + v y Fy − Ft = 0 Um die Bewegung v = (vx, vy)T am Punkt (x,y) zu erhalten wird der Fehler minimiert: E = ∑ (v x Fx + v y Fy − Ft ) 2 (Die Summe wird über ein Fenster R=wh gebildet (typ. 5x5).) Dies geschieht durch Ableitung nach vx bzw. vy, welche Null sein sollen: E = ∑ (2 * (v x Fx + v y Fy − Ft ) * Fx ) = 0 δv x → v x ∑ ( Fx Fx ) + v y ∑ ( Fx Fy ) = ∑ ( Ft Fx ) E = ∑ (2 * (v x Fx + v y Fy − Ft ) * Fy ) = 0 δv y → v x ∑ ( Fx Fy ) + v y ∑ ( Fy Fy ) = ∑ ( Ft Fx ) Oder in Matrix Schreibweise: Basel, Januar 2006 Seite 10/42 Diplomarbeit Image Processing on GPU ∑ ( Fx Fx ) ∑ ( Fx Fy ) ∑ ( F F ) ∑ ( F F ) x y y y ∑ ( Fy Ft ) v x * = ∑ ( Fy Ft ) v y W *v = γ → Der Fluss kann durch Errechnen der Eigenwerte und Eigenvektoren ermittelt werden. Gesucht sei der Eigenwert von: a b A= b c a − λ A − λE = b b c − λ Eigenwert: det( A − λE ) = 0 det( A − λE ) = (a − λ )(c − λ ) − b 2 = λ2 − λ (a + c) + ac − b 2 = 0 2 Quad. Gleichung: x + px + q = 0 2 λ1, 2 = → a+c a 2 + 2ac + c 2 − 4ac + 4b 2 ± 2 4 x1, 2 p p = ± −q 2 2 = (a − c) 2 + 4b 2 a+c ± 2 4 Da die Matrix symmetrisch ist, reduziert sich das Gleichungssystem für die Eigenvektoren auf eine Gleichung. Eine Unbekannte kann frei gewählt werden (z.B. x2=1) ( A − λE ) x = 0 b x1 a − λ =0 b c − λ x 22 Daraus folgen die 2 Gleichungen: (a − λ ) x1 + bx 2 = 0 (1) bx1 + (c − λ ) x 2 = 0 (2) Behauptung: Gleichung (1) sei ein Vielfaches von Gleichung (2) und deshalb reduziert sich das Gleichungssystem auf eine Gleichung mit 2 Unbekannten, wobei eine davon frei gewählt werden kann: Damit (1) ein Vielfaches von (2) ist, gilt: a−λ b = b c−λ (a − λ )(c − λ ) = b 2 → ac − λ (a + c) + λ = b 2 2 → λ − λ (a + c) + ac − b = 0 2 Basel, Januar 2006 2 Seite 11/42 Diplomarbeit Image Processing on GPU Aufgelöst folgt für : λ λ1, 2 a+c a 2 + 2ac + c 2 − 4ac + 4b 2 ± 2 4 = = a+c (a − c) 2 + 4b 2 ± 2 4 q.e.d . Dies entspricht den Eigenwerten. Somit ist die Bedingung erfüllt. Eine Gleichung kann also fallen gelassen werden: (a − λ ) x1 + bx 2 = 0 x1 = − wobei gilt (frei gewählt): x2 = 1 b b = a−λ λ −a b Φ 1 = λ − a 1 normieren!! Des Weiteren gilt, dass die Eigenvektoren einer symmetrischen Matrix orthogonal aufeinander stehen. Deshalb reicht die Berechnung eines Vektors und für den zweiten folgt: 1 b Φ2 = − λ − a normieren!! Die Berechnung der Eigenwerte erfolgt mit der Matrix W. Des Weiteren gilt: v = α max Φ max + α min Φ min wobei: α max = α min = Φ Tmax γ λmax Φ Tmin γ λmin Für v gilt: v=0 wenn λmax < 1 v = α max Φ max wenn λmax >> λmin v = α max Φ max + α min Φ min sonst Für das grundsätzliche Verständnis über Eigenvektoren und Eigenwerte sei auf die allgemein bekannten Mathematik Bücher hingewiesen. Basel, Januar 2006 Seite 12/42 Diplomarbeit Image Processing on GPU 3.4 Farbbilder Die Optical Flow Constraint (siehe oben) kann auch aufgelöst und die Summen über Kanäle und Regionen gebildet werden: ∑ ( Fx Fx ) ∑ ( Fx Fy ) ∑ ( F F ) ∑ ( F F ) x y y y ∑ ( Fy Ft ) v x * = ∑ ( Fy Ft ) v y → W *v = γ Daraus folgt: v = W −1 * γ a b A= b c 1 d − b ad − bc − c a A −1 = → Woraus folgt: det(W ) = ∑ Fx Fx * ∑ Fy Fy − ∑ Fx Fy * ∑ Fx Fy v= ∑ Fy Fy 1 * det( w) − ∑ Fx Fy − ∑ Fx Fy ∑ Fx Ft * ∑ Fx Fx ∑ Fy Ft Wobei nun auch noch die Kanäle aufsummiert werden. entspricht Kanäle Nachbarn ∑ ∑ ∑ Summen über Nachbarschaft (z.B. 5x5) und Kanäle (rgb). K = Kanäle; N = Nachbarn vx = vy = ∑ ∑ (F F )* ∑ ∑ (F F ) − ∑ ∑ (F F ∑ ∑ (F F ) * ∑ ∑ (F F ) − ∑ ∑ (F F K N y y K N x t K N x y K N x x K N y y K N x y )* ∑ ∑ (F F ) )* ∑ ∑ (F F ) K N y t K N x y ∑ ∑ (F F ) * ∑ ∑ (F F ) − ∑ ∑ (F F )* ∑ ∑ (F F ) ∑ ∑ (F F ) * ∑ ∑ (F F ) − ∑ ∑ (F F )* ∑ ∑ (F F ) K N x x K N y t K N x y K N x t K N x x K N y y K N x y K N x y Sollte die Determinante Null sein, sind vx und vy auch 0. Der Ansatz, die Summen über die Kanäle zu bilden, kommt daher, dass jeder Kanal für sich eine korrekte Repräsentation der Szene darstellt. Es gibt auch Versuche, bei denen anstelle von Farbbildern, welche nur wenig mehr Information als Graustufen-Bilder bieten, andere Aufnahmemethoden verwendet werden. Unter anderem wurde die Kombination der Aufnahme von klassischen Graustufen-Bildern und Infrarot-Bildern erwähnt. Je mehr Kanäle bzw. Aufnahmemedien vorhanden sind, umso grösser ist dadurch auch der Informationsgehalt. Basel, Januar 2006 Seite 13/42 Diplomarbeit Image Processing on GPU 4 Grundlagen (GP)GPU-Programmierung Normalerweise wird die GPU dazu gebraucht, 3D Spiele beschleunigen zu können. Somit wird die CPU entlastet und kann sich auf andere Aufgaben konzentrieren. In unserem Fall möchten wir die GPU dazu benützen, rechenintensive Bildverarbeitung auf einer handelsüblichen Consumer-Grafikkarte betreiben zu können. Für unsere Zwecke benützen wir lediglich Fragmentprogramme. Vertexprogramme werden in unserem Fall nicht benötigt, da wir nur Bilddaten von Texturen manipulieren wollen. Heutige Grafikprozessoren besitzen je nach Modell bis zu 24 Pixelpipelines (z.B. nVidia GeForce 7800 GTX). Dies bedeutet, dass ein Programm parallelisierbar sein muss, um die Leistung des Prozessors vollständig ausnutzen zu können. Filteroperationen, wie sie bei der Bildverarbeitung zahlreich vorkommen, sind dazu sehr häufig geeignet. Ein Beispiel des Canny Edge Filters wird im zweiten Unterkapitel gezeigt. Zu beachten ist, dass nicht alles von der GPU berechnet wird. Es werden lediglich die rechenintensiven Filteroperationen darauf gerechnet. Die CPU dient dazu, den Programmfluss zu steuern. Wie kann man nun ein einfaches Bildverarbeitungsprogramm von der GPU berechnen lassen? Das wollen wir hier in den nächsten beiden Unterkapiteln zeigen. Voraussetzung für das Verständnis ist allerdings eine gewisse Vorkenntnis über die Grafikkartenprogrammierung. Es wird hier keine Einführung in die Shaderprogrammierung gemacht, da dieses Thema in der Literatur[12] bestens erklärt wird. 4.1 Programmfluss mit der CPU Folgende Vorgehensweise wird empfohlen, um ein Bildverarbeitungsprogramm laufen zu lassen. 1. 2. 3. 4. 5. 6. 4.1.1 Laden der Textur Fragment Programm laden Setzen des Viewports und der orthogonalen Projektion Übergeben der Uniform Variablen Quad zeichnen Abspeichern der Textur Laden der Textur Es gibt eine Bibliothek namens DevIL, mit welcher Bilder ziemlich einfach in OpenGL eingebunden werden können. Dies kann mit folgenden Befehlen erreicht werden: ilInit(); ilLoadImage("BildnameIN.bmp"); if (ilGetInteger(IL_IMAGE_FORMAT)!=IL_BGRA) ilConvertImage(IL_BGRA, IL_UNSIGNED_BYTE); Danach folgen die gewohnten OpenGL Befehle, um das Bild als Textur anzulegen: glGenTextures(1, texName); glBindTexture(GL_TEXTURE_2D, texName); Basel, Januar 2006 Seite 14/42 Diplomarbeit Image Processing on GPU glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_BGRA, GL_UNSIGNED_BYTE, ilGetData()); Wichtig in diesem Zusammenhang ist, dass beim Parameter GL_TEXTURE_WRAP_S/T der Wert GL_CLAMP zugewiesen ist. Dies hat zur Folge, dass die Textur an den Kanten wiederholt wird. Wenn zum Beispiel ausserhalb der Textur gelesen wird, bekommt man die entsprechende Farbe des wiederholten Randes zurück. Das kann nützlich sein, wenn das Randproblem bei einer Filterung durch Wiederholen des Randes gelöst werden soll. Mit ilGetData() werden die Bilddaten des zuvor geladenen Bildes gelesen. Die Wahl des internen Farbformates hat einen direkten Zusammenhang mit der Geschwindigkeit. Von nVidia[8] wird empfohlen, das GL_BGRA Format zu verwenden, um eine möglichst hohe Datentransferrate zu erreichen. 4.1.2 Fragment Programm laden Mit den folgenden Codezeilen lässt sich ein Fragmentprogramm laden. progObj = glCreateProgramObjectARB(); fragObj = glCreateShaderObjectARB(GL_FRAGMENT_SHADER_ARB); glShaderSourceARB(fragObj, 1, (const GLcharARB **)&fragSrc, NULL); glCompileShaderARB(fragObj); glAttachObjectARB(progObj, fragObj); glLinkProgramARB(progObj); In der Methode glShaderSourceARB enthält fragSrc die Daten der Fragment Datei. Um Fehler beim Linken sichtbar zu machen, genügen folgende Zeilen. int len = 0; glGetObjectParameterivARB(progObj, GL_OBJECT_INFO_LOG_LENGTH_ARB, & len); if (len >1) { GLcharARB *log = new GLcharARB[len]; glGetInfoLogARB(progObj, len, 0, log); cerr << progName << " could not be linked:\n" << log << endl; exit(1); } 4.1.3 Setzen des Viewports und der orthogonalen Projektion Als erstes muss die Grösse des Bildschirmausschnitts festgelegt werden. glViewport(0, 0, width, height); Als nächstes muss die orthografische Projektion eingestellt werden. Diese bewirkt, dass alle Objekte in der Szene durch parallele Strahlen auf der Projektionsfläche abgebildet werden. glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluOrtho2D(-1, 1, -1, 1); Basel, Januar 2006 Seite 15/42 Diplomarbeit Image Processing on GPU 4.1.4 Übergeben der Uniform Variablen Dem Shaderprogramm können verschiedene Werte übergeben werden, so zum Beispiel neben Integers oder Floats auch Texturen, Arrays und Vektoren. glUseProgramObjectARB(progObj); glUniform1iARB(glGetUniformLocationARB(progObj, "imageName"), 0); glUniform1ivARB(glGetUniformLocationARB(progObj, "intArrName"), 5, x); glUniform1fARB(glGetUniformLocationARB(progObj, "floatValName"), y); 4.1.5 Quad zeichnen Bis hierhin sind fast alle notwendigen Vorbereitungen getroffen. Um das Fragmentprogramm zu starten, muss ein Quad gezeichnet werden. glBegin(GL_QUADS); { glTexCoord2f(0, 0); glTexCoord2f(1, 0); glTexCoord2f(1, 1); glTexCoord2f(0, 1); } glEnd(); glVertex3f(-1, -1, -0.5f); glVertex3f( 1, -1, -0.5f); glVertex3f( 1, 1, -0.5f); glVertex3f(-1, 1, -0.5f); Mit diesem Vorgehen wird erreicht, dass die Textur in einem Bereich von 0 bis 1 angesprochen werden kann. Der Punkt (0/0) ist dabei in der linken unteren Ecke. 4.1.6 Abspeichern der Textur Das gerenderte Bild liegt nun im Framebuffer vor, sofern kein Framebuffer Objekt (FBO) verwendet wurde (siehe Kapitel 4.2.4). Um die Textur aus dem Buffer zu lesen, genügen folgende Zeilen. glBindTexture(GL_TEXTURE_2D, outTex); glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, width, height); Hiermit wird der Inhalt des Framebuffers in die Textur outTex geschrieben. Um die Textur abzuspeichern, können die folgenden DevIL Funktionen verwendet werden. ilEnable(IL_FILE_OVERWRITE); ilSaveImage("BildnameOUT.bmp"); Hinweis: Das Rendern in den Framebuffer funktioniert zwar gut, bringt aber einige Nachteile mit sich. Zum einen muss darauf geachtet werden, dass die Farbqualität der Bildschirmeinstellung auf 32bit gesetzt ist. Zum anderen können keine negativen Farbwerte ausgegeben werden. Ausserdem beträgt die maximale Grösse der Ausgabe soviel wie im System eingestellt ist. Und zu guter Letzt muss nach jeder Berechnung der Framebuffer in eine Textur zurückkopiert werden. Aus all diesen Gründen wird empfohlen, direkt in ein FBO zu rendern anstatt in den Framebuffer. Basel, Januar 2006 Seite 16/42 Diplomarbeit Image Processing on GPU 4.2 Fragmentprogramm Mit Fragmentprogrammen kann die eigentliche Bildverarbeitungslogik implementiert werden. Damit diese effizient ausgeführt werden können, sind einige Dinge zu beachten, die nachfolgend aufgelistet werden. 4.2.1 Simpler Vergleich von GPU und CPU Programmen CPU GPU for(int y=0; y<maxY ; ++y){ for(int x=0 ; x<maxX ; ++x){ // Berechnung pro Pixel } } /* Erhöhung von jedem Kanal um 30. Angenommen, das Bild liegt wie in einem Array vor (rgb rgb rgb rgb) */ Der Fragment-Shader beinhaltet bereits die 2 forSchleifen und macht diese dadurch überflüssig. glFragColor=texture2D(texName, gl_TexCoord[0].st)+30; outImg[(x+y*maxX)*3]=inImg[(x+y*maxX)*3]+30; outImg[(x+y*maxX)*3+1] =inImg[(x+y*maxX)*3+1]+30; outImg[(x+y*maxX)*3+2] =inImg[(x+y*maxX)*3+2]+30; // Zugriff auf Nachbarpixel (rgb-Kanäle) // z.b. x-1, y+1 nachbar[r]=inImg[(x-1+(y+1)*maxX)*3]; nachbar[g]=inImg[(x-1+(y+1)*maxX)*3+1]; nachbar[b]=inImg[(x-1+(y+1)*maxX)*3+2]; vec2 v=vec2(-1,1); //Verschiebung vec3 nachbar= texture2D(texName, gl_TexCoord[0].st+v).rgb; Es zeigt sich, dass der Shader-Code oft deutlich kürzer ist. Auch das Denken fällt einfacher, da man nicht auf jeden Kanal einzeln zugreifen muss, sondern wirklich das ganze Texel bzw. Pixel zur Verfügung hat. Jedoch muss man, sollte man die Kanäle „missbrauchen“ und z.B. 4 s/w Bilder in eine rgba-Textur füllen, berücksichtigen, dass man die richtigen Kanäle nimmt und kein Durcheinander veranstaltet. Es ist klar erkennbar, dass Pixeloperationen sehr einfach möglich sind. Die Schwäche kommt allerdings bei Operationen, wie z.B. dem durchschnittlichen Helligkeitswert eines s/w Bildes, zum Vorschein. Das ist normalerweise eine sehr einfache Funktion, welche alles aufsummiert und durch die Anzahl Pixel dividiert. Jedoch werden genau solche Operationen auf der GPU sehr komplex und dadurch auch langsam. Man arbeitet pro Pixel und hat keine Ausgabevariabel zu Verfügung. Die einzige Ausgabe erfolgt über den Framebuffer bzw. Texturen. Auch eine scheinbar einfache Aufgabe wie z.B. ein Bild auf der GPU horizontal zu spiegeln, ist ohne weiteres machbar, die Performance dürfte aber durch die sehr weiten Texturzugriffe deutlich sinken. 4.2.2 Binomial-Filter Beispiel Im Gegensatz zur klassischen Bildverarbeitung auf der CPU ist die Denkweise eines Fragmentprogramms ganz anders. Dies soll hier anhand eines einfachen Beispiels gezeigt werden. Basel, Januar 2006 Seite 17/42 Diplomarbeit Image Processing on GPU Zu beachten ist, dass das Randproblem bei diesem Beispiel vernachlässigt wurde und es alles andere als optimiert ist. Des Weiteren funktioniert das CPU Beispiel nur für Graustufenbilder, während dessen das GPU Beispiel auch für Farbbilder funktioniert. 4.2.2.1 Binomialfilter auf der CPU for (y=0; y<height; ++y) { for (x=0; x<width; ++x) { sum sum sum sum sum sum sum sum sum = += += += += += += += += inImg[POS(x-1, inImg[POS(x , inImg[POS(x+1, inImg[POS(x-1, inImg[POS(x , inImg[POS(x+1, inImg[POS(x-1, inImg[POS(x , inImg[POS(x+1, y-1)]; y-1)] * y-1)]; y )] * y )] * y )] * y+1)]; y+1)] * y+1)]; 2; 2; 4; 2; 2; outImg[POS(x,y)] = sum/16; } } Dieser Programmausschnitt sollte soweit selbsterklärend sein. 4.2.2.2 Binomialfilter auf der GPU uniform sampler2D inImg; void main(void) { vec4 sum; sum = texture2D(inImg, sum += texture2D(inImg, sum += texture2D(inImg, sum += texture2D(inImg, sum += texture2D(inImg, sum += texture2D(inImg, sum += texture2D(inImg, sum += texture2D(inImg, sum += texture2D(inImg, gl_TexCoord[0].xy gl_TexCoord[0].xy gl_TexCoord[0].xy gl_TexCoord[0].xy gl_TexCoord[0].xy gl_TexCoord[0].xy gl_TexCoord[0].xy gl_TexCoord[0].xy gl_TexCoord[0].xy + + + + + + + + + vec2(-step, vec2(0 , vec2(step , vec2(-step, vec2(0 , vec2(step , vec2(-step, vec2(0 , vec2(step , -step)); -step)) * -step)); 0 )) * 0 )) * 0 )) * step )); step )) * step )); 2.0; 2.0; 4.0; 2.0; 2.0; gl_FragColor = sum/16.0; } Mit uniform sampler2D inImg wird das Bild in Form einer Textur dem Fragment Programm übergeben. Mit texture2D wird auf die Textur zugegriffen. Gl_Coord[0].xy stellt die aktuelle Position dar, in der sich das Programm im Moment befindet. step ist die Distanz von Pixel zu Nachbarpixel. Gl_FragColor repräsentiert den Ausgabepixel. 4.2.2.3 Die Unterschiede Das Auffälligste ist, dass im Shaderprogramm keine for-Anweisung zu finden ist. Der Grund dafür ist, dass beim CPU-Programm Zeile für Zeile abgearbeitet wird. Beim GPU-Programm ist das anders. Dort bestimmt die GPU, welcher Ausgabepixel gerade berechnet wird. Aus diesem Grund wird Basel, Januar 2006 Seite 18/42 Diplomarbeit Image Processing on GPU Gl_Coord[0].xy benötigt, damit das Fragmentprogramm weiss, an welcher Position es sich gerade befindet. Ein weiterer Unterschied ist der, dass das Shaderprogramm für jeden Pixelwert einen 4 dimensionalen Vektor zur Verfügung hat, welcher die RGBA Werte enthält. Aus diesem Grund lassen sich Farbbilder auf der GPU mit wesentlich weniger Aufwand berechnen als auf der CPU, da mit einer Vektormultiplikation die 4 Werte auf einmal ausgerechnet werden. 4.2.3 Tipps & Tricks Während unserer Diplomarbeit sind wir auf einige wichtige Optimierungsmöglichkeiten gestossen, die wir hier auflisten möchten. 4.2.3.1 Möglichst einfacher Programmfluss Shaderprogramme sollten möglichst ohne if, else Anweisungen auskommen. Da die Grafikkarte diese Programme parallel abarbeiten lässt, könnten sie sich sonst gegenseitig ausbremsen. 4.2.3.2 Möglichst viel vorberechnen Muss im Fragmentprogramm zum Beispiel der Wert width*height bekannt sein, könnte man diesen Ausdruck so im Programm hinschreiben. Das bewirkt allerdings, dass diese Multiplikation für jeden Pixel ausgeführt wird, da das Fragmentprogramm für jeden Bildpunkt aufgerufen wird. Bei einem 512x512 Pixel Bild wären das bereits 262’144 Multiplikationen! Besser ist es, wenn dieser Wert einmal von der CPU berechnet und dann per Uniform Variable dem Shaderprogramm übergeben wird. 4.2.3.3 Framebuffer Objekte Normalerweise wird direkt in den Framebuffer gerendert. Dies bringt allerdings den Nachteil, dass wenn das Ausgabebild für einen weiteren Schritt benötigt wird, dieses vom Framebuffer zurück in eine Textur geschrieben werden muss. Mit Framebuffer Objekten kann dieser Nachteil behoben werden. Detaillierte Informationen dazu können dem Kapitel 4.2.5 entnommen werden. 4.2.3.4 Textur-Zugriffe Texturzugriffe können die Performance von Fragmentshader-Programmen stark beeinflussen. Man sollte möglichst wenig Texturzugriffe in einem Programm haben. Allfällige Offsets sollten wenn möglich als Vektor oder Array dem Programm übergeben und nicht jeweils berechnet werden (siehe 4.2.3.2). Lässt man z.B. die Offsets für einen Filter in einer For-Schleife berechnen, wird die Geschwindigkeit deutlich schlechter sein, als wenn man die Offsets als Uniform-Array bzw. Vektor übergibt. Basel, Januar 2006 Seite 19/42 Diplomarbeit Image Processing on GPU 4.2.4 Readback von der Textur oder des Framebuffers Für den Bereich Bildverarbeitung ist oft ein Zurücklesen von Informationen bzw. des Bildes nötig, da dieses meist nicht nur angezeigt, sondern auch abgespeichert oder auf der CPU weiter verarbeitet werden soll. Verschiedenste Möglichkeiten bieten sich dafür an. Die einfachste ist, wenn sich das Endergebnis auf dem Framebuffer befindet. Ein einfaches Auslesen des Framebuffers ist möglich, was auch relativ schnell ist. Jedoch muss dafür bei render-to-texture die Ausgabe am Schluss auf den Framebuffer erfolgen. Ausserdem ist man in der Grösse eingeschränkt. void glReadPixels(Glint x, Glint y, Glsizei width, GLsizei format, Glenum type, GLvoid *pixels); height, GLenum Direktes Auslesen einer Textur. Dabei ist man deutlich langsamer, jedoch nicht mehr abhängig vom Framebuffer und kann die vollen Texturgrössen lesen. Auch kann das via render-to-texture in die Textur geschriebene Ergebnis direkt gelesen werden: void glGetTexImage(Glenum target, Glint level, Glenum format, Glenum type, GLvoid *pixels); Auslesen des Framebuffer Object. Wird ein render-to-texture mit Framebuffer Object durchgeführt, kann anstatt direkt die Textur auszulesen auch der Buffer ausgelesen werden. Dafür muss der Buffer aktiviert werden und die richtige Textur zugeordnet sein. Danach einfach den Buffer auslesen. Diese Methode erwies sich als die schnellste. Bsp: GLubyte * data = new GLubyte[_iWidth*_iHeight*4]; glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fbo); glDrawBuffer(GL_COLOR_ATTACHMENT0_EXT); glReadPixels(0 ,0 , _iWidth, _iHeight, GL_RGBA, GL_UNSIGNED_BYTE, data); glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0); Methoden: void GenFramebuffersEXT(sizei n, uint *framebuffers); void DeleteFramebuffersEXT(sizei n, const uint *framebuffers); void glBindFramebufferEXT(int target, int framebuffer); void glDrawBuffer(Glenum mode); void glReadPixels(Glint x, GLint y, Glsizei width, Glsizei height, Glenum format, Glenum type, GLvoid *pixels); Referenz zu Framebuffer Object Extension, siehe [5]. 4.2.4.1 Zeitmessungen am CannyEdge Der mit FBO implementierte CannyEdge-Filter wurde gemessen. Folgende Schritte wurden 100-mal durchlaufen: - Die Daten des Inputbildes werden auf die Textur geschrieben Basel, Januar 2006 Seite 20/42 Diplomarbeit Image Processing on GPU - Der CannyEdge-Filter läuft mit aktiviertem FBO darüber Der Readback findet statt (2 Arten) Anzahl Wiederholungen 100 100 100 100 100 100 100 100 100 100 100 100 100 100 100 100 Readback-Art Bildgrösse Kein Texture FBO openCV Kein Texture FBO openCV Kein Texture FBO openCV Kein Texture FBO openCV 512*512 512*512 512*512 512*512 1024*1024 1024*1024 1024*1024 1024*1024 2048*2048 2048*2048 2048*2048 2048*2048 4096*4096 4096*4096 4096*4096 4096*4096 Dauer Total Readback (sec) (sec) 0.53 3.52 0.85 2.06 1.30 14.50 2.36 7.40 4.24 59.00 8.25 26.50 16.15 xxxx 32.35 96.8 Durchschn. Readback 0.000 3.000 0.350 0.000 0.030 0.004 0.000 13.300 1.370 0.000 0.133 0.014 0.000 56.000 5.310 0.000 0.560 0.053 0.000 xxxx 21.700 0.000 xxxx 0.022 Durch einzelne Optimierungen wurden die Messwerte der GPU-Version noch deutlich verbessert. Unter anderem wurde der Zugriff auf die Texturen verbessert. Siehe dazu auch Kapitel 4.2.3. 4.2.5 Render-to-Texture Render-to-Texture soll die Möglichkeit bringen, direkt in eine Textur zu rendern. Man erspart sich so das Rendern in den Framebuffer, um die Daten von dort wieder auszulesen und diese dann für die Weiterverarbeitung in eine Textur zu schreiben. Für den Einsatz von Framebuffer-Objekten ist die Extension „EXT_framebuffer_object“ (String: GL_EXT_framebuffer_object) nötig. Basel, Januar 2006 Seite 21/42 Diplomarbeit Image Processing on GPU Textur 1 Textur Fragmentshader Textur 1 Framebuffer Object Framebuffer Input- und OutputTextur wählen Textur rendern Fragmentshader Displayanzeige Framebuffer für Ausgabe aktivieren Bild 6: Rendern in Textur ohne FramebufferObjekt Textur rendern Display Bild 5: Rendern in Textur mit Framebuffer-Objekt 4.2.5.1 Beispiel mit zwei Texturen und einem Framebuffer Object Benötigte Attribute (Header-File): GLuint fbo, color[2]; Anlegen des Framebuffer Object (Konstruktor): glGenFramebuffersEXT(1, &fbo); Basel, Januar 2006 Seite 22/42 Diplomarbeit Image Processing on GPU Anlegen von zwei leeren Texturen (können auch mit Daten gefüllt werden): glGenTextures(2, color); glBindTexture(GL_TEXTURE_2D, color[0]); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, _iWidth, _iHeight, 0, GL_BGRA, GL_UNSIGNED_BYTE, NULL); glBindTexture(GL_TEXTURE_2D, color[1]); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, _iWidth, _iHeight, 0, GL_BGRA, GL_UNSIGNED_BYTE, NULL); Framebuffer Object aktivieren und Texturen an Color-Attachment anhängen, danach zurück zum Framebuffer als Ausgabe wechseln: glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fbo); glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_TEXTURE_2D, color[0], 0); glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_TEXTURE_2D, color[1], 0); glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0); GL_COLOR_ATTACHMENT0_EXT, GL_COLOR_ATTACHMENT1_EXT, Render-to-Texture-Bereich: Framebuffer Object aktivieren: glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fbo); Ziel wird Color-Attachment1 -> Ergebnis befindet sich in Textur color[1]. Quell-Textur ist color[0]: glDrawBuffer(GL_COLOR_ATTACHMENT1_EXT); glBindTexture(GL_TEXTURE_2D, color[0]); Zum Wechseln bzw. Ändern, einfach Argumente austauschen: glDrawBuffer(GL_COLOR_ATTACHMENT0_EXT); glBindTexture(GL_TEXTURE_2D, color[1]); Ausgabe in Textur bzw. Framebuffer Object deaktivieren: glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0); Danach kann ganz normal wieder auf den Framebuffer gerendert werden. Anstelle einer festen Zahl kann man auch zuerst den aktuellen Framebuffer auslesen und später wieder aktivieren. So wird im Falle mehrerer Werte eine Verwechslung verhindert: GLint _currentDrawBuf glGetIntegerv(GL_DRAW_BUFFER, &_currentDrawbuf); Anmerkungen: Die benötigten Texturen, welche an das Framebuffer Object angehängt werden, müssen bezüglich Format und Grösse identisch sein!!! Vorteile: Man wird unabhängig von der Display-Grösse und kann auch problemlos grössere Texturen rendern. Zusätzlich entfällt das Kopieren vom Framebuffer in Texturen. Basel, Januar 2006 Seite 23/42 Diplomarbeit Image Processing on GPU 5 Canny Edge auf der GPU Anhand vom Beispiel des CannyEdge-Filters soll etwas auf die Implementierung von Bildverarbeitung auf der GPU eingegangen werden. Es wird jedoch keine vollständige Implementation dargelegt, sondern nur auf die wichtigsten Punkte hingewiesen. Für den Einsatz der Bildverarbeitung wird der Ansatz genommen, dass Bilder durch 2D-Texturen dargestellt werden. Filter werden entweder durch Matrizen oder durch kleine 2D-Texturen repräsentiert, wobei Matrizen zu empfehlen sind. Das Ausgabe-Bild wird durch den Framebuffer oder durch eine an ein Framebuffer Object angehängte Textur dargestellt. Für jeden Pixel wird ein neuer Wert errechnet, in dem, wie auf der CPU, die gewünschten Pixel mit dem Filter verrechnet werden. Auch hier können, wie auf der CPU, separierbare Filter realisiert werden. Empfindlicher als auf die Anzahl Multiplikationen ist die GPU auf die Anzahl beanspruchter Nachbarschaftstexel bzw. wie weit diese voneinander entfernt sind. Je grösser ein Filter ist, umso langsamer wird die Berechnung. Dies nicht nur durch die erhöhte Anzahl Berechnungen, sondern durch die Texturzugriffe. Grundsätzlich gilt es, die Anzahl der Texturzugriffe zu minimieren. Für die Implementation des CannyEdge-Filters wird ein standardmässiges Gerüst für GPUProgramme, wie in Kapitel 4.1 erklärt wird, verwendet. Hier sollen nur die verwendeten Fragmentprogramme dargestellt werden. Gauss-Filterung in X-Richtung uniform vec4 offset; uniform sampler2D texUnit0; // (1.0/_iWidth, 2.0/_iWidth, 0.0, 0.0) // Inputbild void main(void){ vec2 coord=gl_TexCoord[0].st; // horizontale Filterung vec4 oneL, twoL, oneR, twoR, current; // Auslesen der Nachbarn und des aktuellen Pixels oneL = texture2D(texUnit0, coord - offset.xz); twoL = texture2D(texUnit0, coord - offset.yz); oneR = texture2D(texUnit0, coord + offset.xz); twoR = texture2D(texUnit0, coord + offset.yz); current = texture2D(texUnit0, coord); vec4 tmp = vec4(0.0625*twoL+0.25*oneL+0.375*current+0.25*oneR+0.0625*twoR); gl_FragColor=tmp; } Gauss-Filterung in Y-Richtung uniform vec4 offset; uniform sampler2D texUnit0; // (0.0, 1.0/_iHeight, 2.0/_iHeight, 0.0) // Inputbild void main(void){ vec2 coord = gl_TexCoord[0].st; // vertikale Filterung vec4 oneU, twoU, oneD, twoD, current; // Auslesen der Nachbarn und des aktuellen Pixels oneU = texture2D(texUnit0, coord + offset.xy); twoU = texture2D(texUnit0, coord + offset.xz); Basel, Januar 2006 Seite 24/42 Diplomarbeit Image Processing on GPU oneD = texture2D(texUnit0, coord - offset.xy); twoD = texture2D(texUnit0, coord - offset.xz); current = texture2D(texUnit0, coord); vec4 tmp = vec4(0.0625*twoU+0.25*twoU+0.375*current+0.25*oneD+0.0625*twoD); gl_FragColor = tmp; } Gradienten uniform sampler2D texGrad; // Inputbild uniform vec4 offset; // (1.0/_iWidth, 1.0/_iHeight, 0.0, -1.0/_iHeight) void main(void){ vec4 t,b,l,r,tl,tr,bl,br; vec2 coord = gl_TexCoord[0].st; // auslesen der 8 Nachbarn t = texture2D(texGrad, coord + offset.zy); b = texture2D(texGrad, coord - offset.zy); l = texture2D(texGrad, coord - offset.xz); r = texture2D(texGrad, coord + offset.xz); tl = texture2D(texGrad, coord - offset.xw); tr = texture2D(texGrad, coord + offset.xy); bl = texture2D(texGrad, coord - offset.xy); br = texture2D(texGrad, coord + offset.xw); // berechnen von der Ableitung nach x bzw. y vec3 gray = vec3(0.299, 0.587, 0.114); float tmp_gx = dot(((tr+br+2.0*r)-(tl+bl+2.0*l)).xyz, gray); float tmp_gy = dot(((tl+tr+2.0*t)-(bl+br+2.0*b)).xyz, gray); // berechnen der Länge des Gradienten float mag=sqrt(tmp_gx*tmp_gx+tmp_gy*tmp_gy); // gx und gy Normieren und zwischen 0 und 1 bringen tmp_gx = (((tmp_gx)/mag)+1.0)*0.5; tmp_gy = (((tmp_gy)/mag)+1.0)*0.5; gl_FragColor = vec4(tmp_gx,tmp_gy,mag, 0.0); } Non-Maximum-Suppression uniform sampler2D texUnit0; uniform float offsetX; uniform float offsetY; // Inputbild void main(void){ vec2 coord = gl_TexCoord[0].st; vec4 tmp = texture2D(texUnit0, coord); vec4 n1, n2; vec2 offset2; // tmp.x und tmp.y sind die Gradienten gx, gy zw. -1 und 1, // resized zu 0-1 offset2.x = offsetX*floor((2.0*tmp.x)-0.5); offset2.y = offsetY*floor((2.0*tmp.y)-0.5); // beide Nachbarn n1 =texture2D(texUnit0, coord + offset2); n2 = texture2D(texUnit0, coord - offset2); // belasse es wenn Maximum, ansonsten schwarz Basel, Januar 2006 Seite 25/42 Diplomarbeit Image Processing on GPU If (tmp.z>=n1.z && tmp.z>=n2.z) gl_FragColor=vec4(tmp.zzz, 1.0); else gl_FragColor=vec4(0.0); } Basel, Januar 2006 Seite 26/42 Diplomarbeit Image Processing on GPU 6 Optischer Fluss auf der GPU 6.1 Ablauf des Optischen Flusses auf der GPU In Kapitel 3 wurde beschrieben, wie der Optische Fluss in der Theorie funktioniert. In diesem Kapitel wird schematisch erklärt, wie die Implementation aussieht. Es wird nicht detailliert auf den Code eingegangen, da dieser in der entsprechenden Dokumentation genauer beschrieben wird. 6.1.1 Die Klasse OpticalFlowGPU Die wichtigste, öffentliche Methode ist displayMorphedImg(float dt, ...). Sie bewirkt, dass die vorgängig gesetzten Bilder zusammengemorpht werden. Der Parameter dt, welcher zwischen 0 und 1 liegen muss, bestimmt dabei das Mischverhältnis der beiden Bilder. Nach dem Aufruf dieser Methode wird die ganze Rechenmaschinerie angeworfen: Bild 7: Erstes Aktivitätsdiagramm Bei diesem Diagram ist ersichtlich, dass das Flussfeld für beide Eingabebilder nur einmal berechnet wird. Werden diese durch setImgs() ausgetauscht, wird die Variable flowComputed wieder auf false gesetzt, damit das Flussfeld neu berechnet wird. Da das Warpen der beiden Inputbilder jeweils rückwärts geschieht, müssen auch zwei Flussfelder berechnet werden, denn das Flussfeld, welches aus dem Bild 1 zum Bild 2 berechnet wurde, ist nicht gleich dem umgekehrten Flussfeld aus dem Bild 2 zum Bild 1! Aus Zeit und Performancegründen haben wir auf einen „richtigen“ Vorwärtswarp verzichtet und stattdessen zweimal den Rückwärtswarp angewendet. Basel, Januar 2006 Seite 27/42 Diplomarbeit Image Processing on GPU Bild 8: Zweites Aktivitätsdiagramm Normalerweise wird mit den Eingabebildern jeweils vorgängig eine Gausspyramide mit der Tiefe depth erstellt. Da wir allerdings nur 16 Texturen zur Verfügung haben, müssen wir die Gaussbilder der aktuellen Tiefe bei jedem Schleifendurchgang neu berechnen. Basel, Januar 2006 Seite 28/42 Diplomarbeit Image Processing on GPU 6.2 Benchmark Innerhalb unserer Demo-Applikation haben wir verschiedene Zeitmessungen durchgeführt. Die Berechnung des Flussfeldes stellt dabei die anspruchvollste Anwendung dar. Das reine Berechnen des Flusses auf der GPU ist extrem schnell. Jedoch wird für den ersten Aufruf sehr viel Zeit für den Aufbau der Shader und ähnlicher Dinge verbraucht. Dieses Phänomen ist aber bekannt und eine einmalige Berechnung auf der GPU deshalb nicht sinnvoll, sondern mehr sich wiederholende Algorithmen. Darum dürfte die Verbreitung der GPU für Video-Anwendung, wo jeder Frame berechnet wird, so breit sein. Für alle Zeitmessungen haben wir ein System mit folgender Ausstattung verwendet: - Pentium 4, 3.2 GHz - GeForce 7800 GTX, 256MB RAM, PCIe - 2 GB RAM Die benutzten Farbbilder hatten eine Kantenlänge von 512x512 Pixel. 6.2.1 Flussfeld Die Berechnung des Optischen Flusses (Methode calcFlow()) dauert auf der GPU rund 29ms. Der allererste Aufruf braucht ein Vielfaches davon. Die Berechnung auf der CPU ist um ein Vielfaches langsamer. Sie dauert rund 1 Sekunde. Bei dieser Betrachtung wurde, wie es üblich ist, nur die Berechnungszeit berücksichtigt. Der Anteil, welcher für den Textur-Download gebraucht wird, fehlt. 6.2.2 Bildinitialisierung Zum Vergleich wird hier deshalb noch die benötigte Zeit der Methode setImgs() berechnet. Diese setzt zwei neue Bilder, lädt diese in den Speicher und führt einige Kontrollberechnungen durch, wie z.B. die maximale Tiefe. Auf der GPU dauert diese Methode rund 170ms. Die CPU ist hier deutlich schneller mit rund 35ms. Hier spielt die CPU ihren Vorteil aus, dass der Bildtransfer in eine Textur fehlt. Des Weiteren finden auch alle Kontrollberechnungen auf der CPU statt. Deshalb hat die GPU-Version keine Möglichkeit, ihre Stärken auszuspielen. 6.2.3 Morphing Beim reinen Morphen treten auch deutliche Geschwindigkeitsunterschiede auf. Die CPU braucht für das reine Morphen 150ms. Die GPU glänzt hier mit Werten von unter 1ms! Basel, Januar 2006 Seite 29/42 Diplomarbeit Image Processing on GPU 7 Demo-Applikation Die Implementation des Optischen Flusses wurde in eine Demo-Applikation integriert. Diese bietet die Möglichkeit, zwei Bilder zu laden und diese zusammen zu morphen. Bild 9: Screenshot Demo-Applikation Basel, Januar 2006 Seite 30/42 Diplomarbeit Image Processing on GPU 7.1 Technischer Aufbau Die Demoapplikation besteht aus zwei Teilen. Einerseits aus der Recheneinheit, andererseits dem GUI. Als GUI Framework haben wir Qt 4 verwendet, da es sehr gut dokumentiert ist und in der Entwicklungsumgebung integriert werden kann. Zusätzlich wird bei aktuellen Entwicklungen der Uni Basel auch Qt 4 eingesetzt. Bild 10: Klassendiagramm der Demo-Applikation 7.1.1 Kurze Beschreibung des Aufbaus Auf eine detaillierte Beschreibung der Applikation wird hier verzichtet. Mehr Informationen dazu kann der Dokumentation des Quellcodes entnommen werden. Im Zentrum der Demo-Applikation steht die Klasse OpticalFlowDemo. Diese verwaltet alle nötigen Widgets. Eines davon ist GLWidget, welches dazu dient, den Framebuffer auszugeben. Diese Klasse besitzt ein OpticalFlowFactory Objekt. Dieses ist in der Lage, selbständig zu entscheiden, ob eine OpticalFlowGPU oder OpticalFlowCPU Instanz angelegt werden soll, je nachdem, ob und welche Grafikkarte im System vorhanden ist. OpticalFlowGPU und OpticalFlowCPU enthalten die eigentliche Logik. Beide implementieren die Methoden von OpticalFlow und können daher dasselbe tun. Die Klasse FlowWindow ist in der Lage, das berechnete Flussfeld auszugeben. Dazu wird das erste Eingabebild genommen und alle 10 Pixel eine Linie in die entsprechende Richtung mit korrekter Länge darüber gezeichnet. Mit dem ImageLoader können die zwei Eingabebilder ausgewählt werden. Unterstützt werden die Bildformate JPEG, PNG, BMP und GIF. Damit die berechneten Bilder zu einem Filmchen gerendert werden können, kann mittels BatchSave eine einstellbare Anzahl gemorphter Zwischenbilder abgespeichert werden. Wie funktioniert nun das Morphen? In der Mitte der Applikation befindet sich ein Slider. Dieser dient dazu, das Mischverhältnis der beiden Eingabebilder zu bestimmen. Wird dieser Slider bewegt, fordert das GLWidget-Objekt von OpticalFlow ein neues, gemorphtes Bild an und gibt dieses aus. Basel, Januar 2006 Seite 31/42 Diplomarbeit Image Processing on GPU Das rechenintensive Flussfeld wird im Übrigen nur dann berechnet, wenn neue Bilder gesetzt wurden. Danach wird für ein angefordertes, gemorphtes Bild mit dem berechneten Flussfeld weiter gearbeitet. 7.2 Funktionsumfang Unsere Demo-Applikation bietet verschiedenste Funktionalitäten, die es auch ermöglichen, die Details des Optischen Flusses besser zu verstehen. Bild 11: Menü File Obenstehendes Bild zeigt den Menüpunkt File, welcher eigentlich selbsterklärend ist. Load Images… Laden von neuen InputBildern. Save as… Speichern des aktuellen Ausgabebildes. Save Flowfield… Export… Quit Speichern des Flussfeld-Bildes. Exportiert eine Reihe von Ausgabebildern. Programm beenden. Der Menüpunkt Edit beinhaltet die wichtigen Punkte, um die Eigenschaften des Optischen Flusses zu erkunden bzw. zu ändern. Bild 12: Menü Edit Menü Automatic CPU GPU CannyEdgeDetection Amount Depth Decrease Depth Basel, Januar 2006 Beschreibung Wenn möglich wird alles auf der GPU gerechnet, sonst auf der CPU. Die Berechnung des Flusses und alles andere geschieht auf der CPU. Die Bildoperationen geschehen auf der GPU. Der Kantenfilter wird aktiviert bzw. deaktiviert. Die Berechnung findet abhängig von der Wahl auf der CPU bzw. GPU statt. Erhöht die aktuelle Zahl der verwendeten Auflösungspyramide um 1. Standardmässig ist der Wert zu Beginn auf 8 bzw. dem maximal möglichen Wert. Senkt die aktuelle Zahl der verwendeten Auflösungspyramide um 1. Seite 32/42 Diplomarbeit Image Processing on GPU Amount/Decrease Depth: Durch Veränderung der Tiefe der Auflösungspyramide lassen sich völlig unterschiedliche Resultate erzielen. Ein höherer Wert ermöglicht grössere Bewegungen, bietet aber auch Nachteile. So wird die Berechnungszeit deutlich höher und es können auch Fragmente entstehen. Ab einer gewissen Tiefe wird ausserdem oft keine grosse Verbesserung mehr festgestellt. Einsatz CannyEdge-Detection: Bild 13: Gemorphtes Bild ohne CannyEdge-Detection Bild 14: Gemorphtes Bild mit CannyEdge-Detection Bild 15: Menü View Der Menüpunkt View beinhaltet nur den Punkt „Show Flowfield“. Dieser ist aber sehr interessant, da er das aktuelle Flussfeld zwischen den beiden Inputbildern anzeigt. Dabei wird eine Auswahl von jedem 10. Verschiebungsvektor dargestellt. Der rote Punkt markiert den Beginn der Vektoren. Das Feld wird bei Änderungen der Eingabe-Bilder oder des Depth aktualisiert. Bild 16: Ausschnitt aus einem Flussfeld der Tramszene Basel, Januar 2006 Seite 33/42 Diplomarbeit Image Processing on GPU Bild 17: Menü Help Unter „Help“ befinden sich die verschiedenen „About’s“. Erwähnenswert ist hier nur das „About OpenGL“. Es bietet verschiedene Informationen über die verwendete Grafikkarte bzw. OpenGL Version. Basel, Januar 2006 Seite 34/42 Diplomarbeit Image Processing on GPU 8 8.1 Ausblick Einsatz bei Projekten von Thomas Vetter Unsere Diplomarbeit stellt für das Projekt von Thomas Vetter grundsätzlich nichts Neues dar. Es waren auch schon Ansätze vorhanden, die Performance des Algorithmus mittels GPU zu verbessern. Jedoch bieten die neuen Sprachen HLSL, Cg und GLSL deutliche Vereinfachungen gegenüber früher, als GPU-Programme in Assembler geschrieben wurden. Auch die unterstützten Instruktionen auf der GPU sind in den letzten Jahren vielfältiger geworden. Ein Grossteil von Berechnungen könnte deshalb auf die GPU ausgelagert werden, speziell natürlich alle Berechnungen, welche mit Bildern bzw. Texturen zu tun haben. Zu den Projekten von Thomas Vetter lässt sich sagen, dass die Machbarkeit wohl klar ist. Jedoch ist der Zeitpunkt für einen vollständigen Einsatz noch etwas zu früh. Das grösste Problem ist, dass momentan nur 16 Texturen in den Grafikkarten-Speicher geladen werden können und es auch nicht möglich ist, direkt in 3D-Texturen zu rendern (FBO). Jedoch sollte man die Entwicklung im Auge behalten und versuchen, den Anschluss nicht zu verpassen, um im richtigen Augenblick den Einsatz nochmals intensiv zu überprüfen. Für einen richtigen Einsatz muss aber die Kartenunabhängigkeit noch breiter werden. nVidia hat momentan wohl die Nase etwas vorne, da die Einsetzbarkeit höher ist. Allerdings liegen die Preise für die Top-Karten auch deutlich über denen der Konkurrenz. Dadurch wird der breite Einsatz etwas unwahrscheinlicher, da die Karten sehr teuer und zu schnell veraltet sind. 8.2 Image Processing on GPU Während unserer Diplomarbeit konnten wir auf diesem Gebiet viele Erfahrungen sammeln. Wie aus diesem Dokument ersichtlich ist, konnten wir gegenüber herkömmlichen CPU Implementationen erhebliche Geschwindigkeitssteigerungen realisieren. Auch die Wiederverwendbarkeit der geschriebenen Programme ist deutlich gestiegen. Dank GLSL ist sichergestellt, dass die nächsten Versionen von Grafik-Karten den Code unterstützen werden. Ältere Karten hingegen wohl kaum. Unser Code enthält keine herstellerspezifischen Anweisungen, jedoch müssen einige Extensions verfügbar sein. Diese sind scheinbar auf den ATI-Karten noch nicht verfügbar (zumindest waren sie dies nicht auf der getesteten ATI-Karte). Den Aspekt der Wiederverwendbarkeit kann man jedoch auch wieder anders drehen. Der Code wird zwar auf neuen Karten laufen, jedoch werden sich in nächster Zeit GLSL und die Extensions wohl noch deutlich weiterentwickeln. Deshalb ist es sehr wahrscheinlich, dass in kürzester Zeit unsere Implementierung nicht mehr dem aktuellen Stand der Technik entspricht und eine weitere Steigerung des Leistungsgewinns möglich wäre. Der Ansatz, durch die GPU Performance-Gewinne zu erhalten, ist sicher auf viele rechenintensive Projekte anwendbar und somit auch auf den Bereich der verschiedenen Projekte von Thomas Vetter. Momentan stellt die GPU eine deutlich höhere Performance zur Verfügung als die CPU. Jedoch wird ein Grossteil bei falschem Einsatz durch Texture-Down- und –Upload zu Nichte gemacht. Der Einsatz macht nur Sinn, wenn eine möglichst lange Kette von Berechnungen auf der GPU durchgeführt werden kann und höchstens einzelne Argumente bzw. Vektoren geändert werden müssen, nicht jedoch ganze Texturen. Bei komplizierteren Anwendungen kann man da auch unerwartet schnell an die Grenzen der Anzahl Texturen stossen, obwohl diese nicht so klein erscheint. Voraussichtlich sollte aber Besserung eintreten, sobald die Framebuffer-Objekte 3D-Texturen unterstützen. Basel, Januar 2006 Seite 35/42 Diplomarbeit Image Processing on GPU Grundsätzlich kann man die Ergebnisse der Arbeit sehr zwiespältig sehen. Einerseits ist die Umsetzung geglückt und auch ein Performance-Gewinn klar erkennbar. Anderseits herrschen immer noch deutliche Einschränkungen bei der GPU-Programmierung. Die Entwicklung ist momentan noch sehr stark in Bewegung und die Folgen nicht genau abschätzbar. Auch kann man den Ansatz der Parallelisierung auf der CPU langsam erkennen, da Dual-Core-Prozessoren höhere Leistungen versprechen und trotzdem viele Instruktionen besitzen. Dürfte der Ansatz der GPU auf die CPU übertragen werden, könnten die Vorteile der GPU sehr schnell verschwinden und Nachteile wie Download/Upload und Einschränkungen bezüglich Programmierung überwiegen. Schaffen es die GPU-Hersteller aber, eine stabile Situation in ihrem Markt durch klarere Standards und bessere Dokumentationen zu erreichen, ist den Grafikkarten noch ein grosses Potential zuzuschreiben. Da man meistens nicht das volle Risiko eingehen will und die Entwicklungen allfälliger Projekte parallel auf GPU und CPU macht, entsteht auch ein deutlich höherer Aufwand. Das Vertrauen in die CPU dürfte wohl momentan noch deutlich überwiegen. Basel, Januar 2006 Seite 36/42 Diplomarbeit Image Processing on GPU 9 Fazit Der Spassfaktor ist bei der Arbeit mit Bildverarbeitung auf der GPU relativ hoch, da man nebst technischen Erfolgen wie Performance-Gewinnen auch diverse optisch interessante und spassige Eindrücke gewinnen kann. Die Arbeit bot jedoch auch Schattenseiten. Die Einarbeitung und auch das Mithalten mit der technischen Entwicklung bezüglich der GPU ist sehr zeitintensiv und mühsam, da die Quellen oft schlecht bzw. rar sind. Auch das Einarbeiten für das Verständnis des Optischen Flusses ist aufwändig und die Komplexität des Algorithmus darf nicht unterschätzt werden. Die Umsetzung des Optischen Flusses bzw. allgemein von bildverarbeitenden Algorithmen auf der GPU ist zum heutigen Zeitpunkt grundsätzlich möglich. Einmal eingearbeitet, ist die Umsetzung von bekannten BV-Algorithmen auf die GPU schnell gemacht. Jedoch muss immer noch mit einigen Einschränkungen umgegangen werden. Diese sind im Vergleich zu früheren Arbeiten aber deutlich geringer. Das Potential der GPU für den allgemeinen Einsatz ist sicher weiterhin vorhanden und sogar gestiegen. Die schnelle Entwicklung lässt zudem auf viele weitere Möglichkeiten hoffen. Basel, Januar 2006 Seite 37/42 Diplomarbeit Image Processing on GPU A Screenshots Die nachfolgenden Screenshots zeigen 2 Originalbilder und ein berechnetes Bild. Zwei ähnliche Gesichter werden zusammen gemorpht. Bild 18: Gesicht 1 Bild 19: Zusammen gemorpht Bild 20: Gesicht 2 zu je 50% Interpolation einer Kopfdrehung zwischen zwei Kopfpositionen. Bild 21: Originalbild 1 Bild 22: Interpolation 50% Bild 23: Originalbild 2 Interpolation zwischen zwei Mimiken. Bild 24: Mimik „böse“ Bild 25: Interpolation 50% Bild 26: Mimik „freundlich“ entspricht „neutral“ Basel, Januar 2006 Seite 38/42 Diplomarbeit Image Processing on GPU Eine Landschaftsszene, in welche hinein gezoomt wurde. Bild 27: Landschaft ohne Zoom Bild 28: Landschaft mit Zoom Bild 29: Flussfeld vom ersten zum zweiten Bild Basel, Januar 2006 Seite 39/42 Diplomarbeit Image Processing on GPU Morphing zwischen realen, relativ unterschiedlichen Gesichtern. Bild 30: Clemens Bild 31: ☺ Cleminik ☺ Bild 32: Dominik Bei zwei so unterschiedlichen Gesichtern zeigen sich deutliche Schwächen des Algorithmus. Die Distanzen sind teilweise zu gross, so dass Artefakte entstehen können – siehe Mundbereich. Gleichwohl ist es erstaunlich, dass ein solch gutes Ergebnis, ohne Hilfe von manuell gesetzten Referenzpunkten, erzielt werden kann. Basel, Januar 2006 Seite 40/42 Diplomarbeit Image Processing on GPU Quellenverzeichnis Literatur [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] [12] [13] J.L. Barron and R. Klette. Quantitative Colour Optical Flow, Int. Conf. on Pattern Recognition (ICPR2002), Vol. 4, pp251-255, August 2002 J.R. Bergen and R. Hingorani. Hierachical Motion-Based Frame Rate Conversion, April 1990 M. Christen. http://www.clockworkcoders.com GPGPU http://www.gpgpu.org S. Green. The OpenGL Framebuffer Object Extension, NVIDIA Corporation, 2005 B. Jähne. Digitale Bildverarbeitung, Springer, S. 405-415, 2002 A. Nischwitz und P. Haberäckler, Masterkurs Computergrafik und Bildverarbeitung, Vieweg, 2004 NVidia Corportaion, Fast Texture Downloads and Readbacks using Pixel Buffer Objects in OpenGL, August 2005 NVidia Corporation, NVIDIA GPU Programming Guide Version 2.4.0, 2005 M. Phar and F. Randima, GPU Gems 2, Addison Wesley, 2005 D. Schreiner, M. Woo, J. Neider and T. Davis. OpenGL Programming Guide, 5th Edition, Addison Wesley, 2005 R. J. Rost. OpenGL Shading Language, Addison Wesley, 2004 Wikipedia. http://de.wikipedia.org/wiki/Optischer_Fluss Abbildungen Bilder 18 und 20 stammen von der Uni Basel. Bilder 21 und 23 sind Screenshots aus Porenut, Uni Basel (http://porenut.cs.unibas.ch). Alle anderen Bilder wurden von uns selbst erstellt bzw. fotografiert. Basel, Januar 2006 Seite 41/42 Diplomarbeit Image Processing on GPU Anhang: Ehrlichkeitserklärung Hiermit bestätigen die unterzeichnenden Autoren dieses Berichts, dass alle nicht klar gekennzeichneten Stellen von ihnen selbst erarbeitet und verfasst wurden. Ort und Datum Unterschrift Basel, 06.01.2006 Clemens Blumer Dominik Oriet Kontakte Clemens Blumer [email protected] Dominik Oriet [email protected] Website zum Projekt: http://www.fhbb.ch/informatik/bvmm/index_projekte.html Uni Basel Prof. Dr. Thomas Vetter Bernoullistrasse 16 4056 Basel http://www.cs.unibas.ch Basel, Januar 2006 FHBB, Abteilung Informatik Hofackerstr. 73 4132 Muttenz [email protected] http://www.fhbb.ch/informatik Seite 42/42