Im Teil 1 dieser Reihe führte ich in die grundlegenden Werkzeuge für unsere Aufgabenstellung ein und zeigte das nun entstehende Anwendungsprogramm.
Teil 2 widmete sich den optischen Grundlagen unserer Modellbildung.
Im Teil 3 betrachteten wir die gesamte Transformations- und Projektionslinie von der Aufnahme bis zur Darstellung des entzerrten Bildausschnittes.
Im Teil 4 betrachten wir die Einbindung der Shader in unsere Anwendung.
Dieser Teil widmet sich der Umsetzung der Numerik in den Shadern.
Zunächst noch einmal unsere Grafik aus Teil 3 mit unserer Kette von der Aufnahme bis zur entzerrten Teilansicht:
Wir gehen nun diese Kette Stück für Stück durch.
Wir sind hier direkt an der Schnittstelle zwischen dem Qt-Scenengraphen und dem Shader. Der Scenengraph bindet einen rechteckigen View ein und bindet diesen an unseren Qt-Shader.
Hier startet zunächst unser Vertex-Shader.
#version 440
layout(location = 0) in vec4 qt_Vertex;
layout(location = 1) in vec2 qt_MultiTexCoord0;
layout(location = 0) out vec2 qt_TexCoord0;
layout(std140, binding = 0) uniform buf
{
mat4 qt_Matrix;
float qt_Opacity;
// Entzerrtes Zielraster und Fassadenausschnitt.
bool facadeMappingEnabled;
vec2 targetToRectifiedScale;
vec2 targetToRectifiedOffset;
vec2 allowedRectifiedMinimum;
vec2 allowedRectifiedMaximum;
// Rückprojektion auf die unverzeichnete Kameraebene.
mat3 inverseRectification;
// Linsenverzeichnung und Abbildung auf den Bildsensor.
float k1;
float k2;
float k3;
float p1;
float p2;
vec2 distortedToSourceUvScale;
// Definierte Ausgabe für nicht abbildbare Zielpixel.
vec4 backgroundColor;
} ubuf;
void main()
{
qt_TexCoord0 = qt_MultiTexCoord0;
gl_Position = ubuf.qt_Matrix * qt_Vertex;
}
In diesem erfolgen im Wesentlichen nur zwei Dinge:
1. Transformation der Vertexposition in den Clip-Raum:
gl_Position = ubuf.qt_Matrix * qt_Vertex;
Hier wird die vordefinierte Ausgabevariable gl_Position berechnet. Sie ist ein Pflichtfeld. Jeder Vertex-Shader muss ihr einen Wert zuweisen, damit die Grafikpipeline weiß, wo der Vertex im Clip-Raum liegt. Die Spezifikation legt fest, dass gl_Position die homogene Vertexposition enthält, also durch die Perspektivdivision (Teilung durch w) in normalisierte Gerätekoordinaten umgewandelt wird. gl_Position wird anschließend für Primitive Assembly, Clipping, Culling und Rasterisierung verwendet.
Qt liefert hier über qt_Vertex die Eckpunkte des QML-Rechtecks (4 Rufe). Normalerweise ist und . und sind lokale Koordinaten innerhalb des QML-Items. Da Qt die Berechnungen für Pan / Zoom in der UI-Logik vornimmt, stecken die Größe des gerenderten Bildes schon in diesen Vertices.
2. die Durchreichung der Texturkoordinaten an den Rasterizer
Diese gehen 1:1 weiter:
qt_TexCoord0 = qt_MultiTexCoord0;
qt_TexCoord0 ist die Ausgabe des Vertexshaders an den Rasterizer und anschließend an den Fragmentshader.
Zwischen den vier Eckpunkten interpoliert der Rasterizer den Wert. Streng genommen erfolgt die Interpolation baryzentrisch innerhalb der beiden Dreiecke des Rechtecks. Beim ebenen Qt-Rechteck ergibt sich daraus die erwartete lineare Abbildung über das gesamte Rechteck.
Und damit kommen wir auch schon zum Fragment-Shader, in welchem unsere Entzerrungs-Algorithmik liegt: Hier wird nun für jedes Pixel im QML-Ausgabefenster die entsprechende Farbe des entzerrten Bildausschnittes berechnet.
#version 440
layout(location = 0) in vec2 qt_TexCoord0;
layout(location = 0) out vec4 fragColor;
layout(std140, binding = 0) uniform buf
{
mat4 qt_Matrix;
float qt_Opacity;
// Entzerrtes Zielraster und Fassadenausschnitt.
bool facadeMappingEnabled;
vec2 targetToRectifiedScale;
vec2 targetToRectifiedOffset;
vec2 allowedRectifiedMinimum;
vec2 allowedRectifiedMaximum;
// Rückprojektion auf die unverzeichnete Kameraebene.
mat3 inverseRectification;
// Linsenverzeichnung und Abbildung auf den Bildsensor.
float k1;
float k2;
float k3;
float p1;
float p2;
vec2 distortedToSourceUvScale;
// Definierte Ausgabe für nicht abbildbare Zielpixel.
vec4 backgroundColor;
} ubuf;
layout(binding = 1) uniform sampler2D source;
fragColor ist der Ausgang unserer berechneten Farbe als RGBA.
Über qt_TexCoord0 erhalten wir die vom Rasterizer interpolierte Position im entzerrten Zielbild. Der Bereich ist normalerweise , . Durch Zoom und Pan ist nicht unbedingt das ganze Rechteck sichtbar. Für ein sichtbares Fragment enthält qt_TexCoord0 die normierte Koordinate im ShaderEffect-/Zielrechteck.
ubuf liefert uns alle sonstigen relevanten Daten, wie die von der CPU vorher berechneten Entzerrungs-Parameter, aber auch der durch Pan und Zoom bestimmte sichtbare Bereich.
Der Hintergrund ist einfach zu erklären: Die Entzerrung arbeitet natürlich nur erwartungstreu für Originalpunkte, welche auf der zu entzerrenden Fassade liegen. Übrige Bildpunkte können dagegen bei der Entzerrung extreme Werte erhalten, die den realen Wertebereich weit verlassen. Das Rechteck wird in unserem Fall im Vorfeld auf der CPU aus Grenzlinien abgeleitet, welche den auszuwertenden Bereich der Fassade umschließen. Mit einem festlegbaren Faktor wird um diesen Bereich noch ein Rand hinzugefügt:


In diesem Bereich können wir nun Pan und Zoom ausführen. Die Steuerung übernimmt QML. Am Ende lässt sich die Berechnung jedes Pixels eines Ausschnitts auf zwei Parameter targetToRectifiedScale und targetToRectifiedOffset reduzieren nach der Berechnung:
Wichtig ist dabei, wie oben erläutert, die Prüfung der Bereichsgrenzen. Liegt das Fragment nicht im definierten Darstellungsbereich der Fassade, so wird es auf eine Default-Farbe backgroundColor gesetzt. Der Darstellungsbereich wird durch ein Rechteck definiert, welches durch die beiden 2D-Punkte allowedRectifiedMinimum und allowedRectifiedMaximum aufgespannt wird.
Mit diesen Werten rechnen wir nun zunächst qt_TexCoord0 als normierte Koordinate im ShaderEffect-/Zielrechteck in rectifiedPoint als entsprechende Koordinate im vollständigen entzerrten Koordinatenraum um und prüfen, ob diese im Zielrechteck liegt:
vec2 rectifiedPoint = qt_TexCoord0 * ubuf.targetToRectifiedScale + ubuf.targetToRectifiedOffset;
if (any(lessThan(rectifiedPoint, ubuf.allowedRectifiedMinimum)) || any(greaterThan(rectifiedPoint, ubuf.allowedRectifiedMaximum)))
{
fragColor = ubuf.backgroundColor * ubuf.qt_Opacity;
return;
}
Anmerkung: die Kombination der GLSL-Befehle any, lessThan und greaterThan formulieren die Vergleiche vektoriell und kompakt, dies ist bei Shaderprogrammierung eine übliche Technik.
Wir haben nun die relative Punktkoordinate rectifiedPoint im 2D-Koordinatensystem der Fassade. rectifiedPoint.x wächst weiter nach rechts, rectifiedPoint.y weiter nach unten, jedoch mit einer Verschiebung des Koordinatenursprungs in die Mitte des die Fassade festlegenden Rechtecks.
Nun folgt die perspektivische Verzerrung durch die Aufnahme der Kamera, da Fassaden- und Bild-Ebene nicht parallel zueinander verlaufen müssen.
Zur Erinnerung: wir kehren bei der Entzerrung im Unterschied zu anderen photogrammetrischen Verfahren den Aufnahmeprozess NICHT numerisch um, sondern folgen ihm in seiner Reihenfolge (siehe Teil 3).
Nun führen wir mit der Transformationsmatrix (3×3) inverseRectification unseren Punkt rectifiedPoint zurück auf die IDEALE, noch unverzeichnete Kameraebene:
Danach erfolgt die homogene Division:
Noch einmal im Code:
vec3 homogeneousUndistortedPoint = ubuf.inverseRectification * vec3(rectifiedPoint, 1.0);
if (abs(homogeneousUndistortedPoint.z) < 0.000001)
{
fragColor = ubuf.backgroundColor * ubuf.qt_Opacity;
return;
}
vec2 undistortedPoint = homogeneousUndistortedPoint.xy / homogeneousUndistortedPoint.z;
if (abs(homogeneousUndistortedPoint.z) < 0.000001) ist dabei lediglich unser Backup, um eine Division durch 0 abzufangen.
Wir haben nun mit undistortedPoint unseren Punkt bereits in der Ebene des Bildsensors. Die Ausrichtung der Achsen bleibt, undistortedPoint.x wächst nach rechts, undistortedPoint.y nach unten. Der Ursprung unseres 2D-Koordinatensystems liegt nun aber im angenommenen Bildhauptpunkt der noch unverzeichneten Kameraebene.
Darauf aufbauend modellieren wir nun die Verzeichnung. Wieder als Erinnerung: wir gehen den Abbildungsprozess vorwärts, wir modellieren die Verzeichnung, nicht ihre Umkehrung!
In unserer Genauigkeitsklasse modellieren wir die radialsymmetrische und tangentiale Verzeichnung.
Zunächst die radialsymmetrische Verzeichnung, hier wird ein Punkt nach seiner Entfernung vom Bildhauptpunkt verschoben nach:
Die zweite Form, das Horner-Schema, ist hier von Vorteil, da nur berechnet werden muss, bei ansonsten gleicher Anzahl von Multiplikationen und Additionen. Bei der Berechnung von kommt uns GLSL mit der Berechnung des inneren Produktes über den Befehl dot entgegen, gerade solche Befehle werden häufig benötigt und entsprechend effizient in Hardware abgebildet:
Hinzu kommt noch die tangentiale Verzeichnung:
und
oder zusammen formalisiert:
Daraus folgt wieder unser Shader-Code:
float radiusSquared = dot(undistortedPoint, undistortedPoint);
float radialFactor = 1.0 + radiusSquared * (ubuf.k1 + radiusSquared * (ubuf.k2 + ubuf.k3 * radiusSquared));
vec2 tangentialOffset;
tangentialOffset.x = 2.0 * ubuf.p1 * undistortedPoint.x * undistortedPoint.y + ubuf.p2 * (radiusSquared + 2.0 * undistortedPoint.x * undistortedPoint.x);
tangentialOffset.y = ubuf.p1 * (radiusSquared + 2.0 * undistortedPoint.y * undistortedPoint.y) + 2.0 * ubuf.p2 * undistortedPoint.x * undistortedPoint.y;
vec2 distortedPoint = undistortedPoint * radialFactor + tangentialOffset;
Mit distortedPoint schwenken wir nun gewissermaßen in die Zielgerade ein. Wir haben nun die Position unseres Fragments auf dem Bildsensor und müssen diese nur noch umrechnen in das Koordinatensystem der Abbildung. Diese ist in unserem Fall von Qt als Textur eingehangen und hat daher den Koordinatenursprung links OBEN, also y nach unten wachsend.
Für unsere normierten Texturkoordinaten gilt , . Wir skalieren distortedPoint daher mit distortedToSourceUvScale und verschieben ihn in normierte Texturkoordinaten:
Auch diesen Teil noch einmal als Shader-Code:
vec2 sourceUv = distortedPoint * ubuf.distortedToSourceUvScale + vec2(0.5);
if (any(lessThan(sourceUv, vec2(0.0))) || any(greaterThan(sourceUv, vec2(1.0))))
{
fragColor = ubuf.backgroundColor * ubuf.qt_Opacity;
return;
}
fragColor = texture(source, sourceUv) * ubuf.qt_Opacity;
Und damit haben wir unsere Pipe erfolgreich geschlossen.