Visualisierung von Punktwolken mit Three.js
Seit einiger Zeit arbeite ich an einem internen Projekt, dessen Output unter anderem auch Punktwolken sind. Als ich über ein geeignetes Visualisierungstool für diese Wolken nachdachte, habe ich eine Liste mit Dingen erstellt, die es können sollte:
- Anpassung
- Möglichkeit, benutzerdefinierte Vertex-/Fragment-Shader zu übergeben
- Einfach zu teilen
Wie immer endete dieses Nebenprojekt nicht nur mit der Visualisierung. Da ich das auch für andere interessant machen wollte, erzähle ich euch in den nächsten 15 Minuten etwas über Tiefenkarten / MiDaS, Gesichtserkennung / TensorFlow, Beier-Neely-Morphing-Algorithmus und Parallax-Effekt. Keine Sorge, es wird auch viele Bilder und Videos geben. Dieser Beitrag ist kein detailliertes Tutorial, sondern eher ein Schaufenster einiger Konzepte, sollte jedoch zusammen mit dem Github-Repository als Studienmaterial gut genug sein.
Über Punktwolken und Three.js
Im Allgemeinen ist eine Punktwolke nur eine Menge von Punkten im Raum. Solche Punktwolken könnten zB über Lidar oder sogar über ein modernes iPhone erhalten werden. Top-Lidars können mehrere Millionen Punkte pro Sekunde generieren, daher ist die Optimierung und Nutzung der GPU im Grunde ein Muss für eine ordnungsgemäße Visualisierung. Ich könnte einen weiteren Beitrag über Visualisierungsansätze in Bezug auf die Datensatzgröße schreiben, aber im Moment sind Three.js und WebGL gut genug. Hoffentlich sollten wir bald auch WebGPU verwenden können – es ist bereits verfügbar, aber bisher nichts Offizielles in Bezug auf Three.js.
Three.js gibt uns Points 3D-Objekt, das intern mit dem Flag gl.POINTS gerendert wird . Die Points-Klasse erwartet zwei Parameter, Material und Geometry. Schauen wir uns beide genauer an.
Geometrie
Es gibt viele Hilfsgeometrien wie Plane of Sphere, mit denen Sie schneller Prototypen erstellen können. Ich habe mich jedoch für BufferGeometry entschieden . Obwohl Sie mehr Zeit brauchen, um etwas Sinnvolles zu erstellen, haben Sie die volle Kontrolle über alles, was mit Geometrie zu tun hat. Schauen wir uns den folgenden Ausschnitt an:
if (this.guiWrapper.textureOptions.textureUsage) {
geometry.setAttribute('uv', new THREE.BufferAttribute(pointUv, this.UV_NUM))
} else {
geometry.setAttribute('color', new THREE.BufferAttribute(pointsColors, this.COL_NUM))
}
geometry.setAttribute('position', new THREE.BufferAttribute(pointsToRender, this.POS_NUM))
geometry.morphAttributes.position = []
geometry.morphAttributes.color = []
// Add flat mapping
geometry.morphAttributes.position[0] = new THREE.BufferAttribute(pointsToRenderFlat, this.POS_NUM)
geometry.morphAttributes.color[0] = new THREE.BufferAttribute(pointsColors, this.COL_NUM)
// Add "natural" mapping
geometry.morphAttributes.position[1] = new THREE.BufferAttribute(pointsToRenderMapped, this.POS_NUM)
geometry.morphAttributes.color[1] = new THREE.BufferAttribute(pointsColors, this.COL_NUM)
In einfachen Fällen wie 2D-Bildern könnten Sie eine Tiefenkarte als Verschiebungskarte verwenden, aber in meinem Anwendungsfall benötige ich ein komplexeres Verhalten von Punktwolken und 3D-Modellen.
Material
Auch hier gibt es viele vordefinierte Materialien in Three.js, aber in unserem Fall habe ich ShaderMaterial verwendet, da wir benutzerdefinierte Shader bereitstellen können und mehr Flexibilität / bessere Leistung haben. Das nächste Snippet zeigt sowohl den grundlegenden Vertex- als auch den Fragment-Shader. Einige der Includes sind Three.js-spezifisch, sodass Sie die API nutzen können, auf die auf der TypeScript-Seite zugegriffen werden kann. Da ihr Code auf Github verfügbar ist , können Sie jederzeit überprüfen, was benötigt wird.
export const VERTEX_SHADER = `
#ifdef GL_ES
precision highp float;
#endif
uniform float size;
uniform float scale;
#define USE_COLOR
#define USE_MORPHCOLORS
#include <common>
#include <color_pars_vertex>
#include <morphtarget_pars_vertex>
attribute vec3 color;
varying vec2 vUv;
uniform float u_time;
uniform float u_minZ;
uniform float u_maxZ;
uniform float u_scale;
uniform vec3 u_camera_angle;
uniform int u_parallax_type;
void main()
{
#include <color_vertex>
#include <begin_vertex>
#include <morphtarget_vertex>
vColor = color;
vUv = uv;
gl_PointSize = 2.0;
#if defined( MORPHTARGETS_COUNT )
vColor = morphTargetBaseInfluence;
for ( int i = 0; i < MORPHTARGETS_COUNT; i ++ ) {
if ( morphTargetInfluences[ i ] != 0.0 ) vColor += getMorph( gl_VertexID, i, 2 ).rgb morphTargetInfluences[ i ];
}
#endif
if (u_parallax_type == 1) {
transformed.x += u_scale*u_camera_angle.x*transformed.z*0.05;
transformed.y += u_scale*u_camera_angle.y*transformed.z*0.02;
} else if (u_parallax_type == 2) {
transformed.x += transformed.z*cos(0.5*u_time)*0.01;
transformed.y += transformed.z*sin(0.5*u_time)*0.005;
}
vec4 mvPosition = modelViewMatrix * vec4( transformed, 1.0 );
gl_Position = projectionMatrix * mvPosition;
}
`;
export const FRAGMENT_SHADER = `
#ifdef GL_ES
precision highp float;
#endif
varying vec3 vColor;
varying vec2 vUv;
uniform sampler2D u_texture;
uniform bool u_use_texture;
void main()
{
if (u_use_texture) {
gl_FragColor = texture2D(u_texture, vUv);
} else {
gl_FragColor = vec4( vColor, 1.0 );
}
}
`;
Wichtiger Hinweis zur Live-Demo-Website. Gifs und Videos, die in diesem Beitrag zu sehen sind, wurden erstellt, während der Visualizer Roh-/HQ-Assets verwendet hat. Die Live-Demo verwendet um den Faktor 10 verkleinerte Assets, insgesamt etwa 15 MB. Während ich es mir auf lokaler Umgebung leisten kann, Rohdaten zu laden und ein paar Sekunden zu warten, verwendet die Live-Demo einige interne Verknüpfungen, um das Erlebnis zu verbessern. Das heißt, die Geometriekonstruktion wird teilweise an den Webworker ausgelagert oder Assets als Texturen geladen und in die Offscreen-Leinwand geladen, damit ich ImageData lesen und grundlegende und Morphing-Puffer füllen kann. — Verzweifelte Zeiten erfordern verzweifelte Maßnahmen. :]
Lasst uns die Wolken zum Tanzen bringen!
In den nächsten Abschnitten werde ich Ihnen mehr über verwendete Konzepte erzählen und Demo-Szenarien beschreiben, die Sie später ausprobieren können. Ich werde Pipelines wie [Bildsatz -> Gesichtserkennung -> Datenexport -> Three.js-Integration] nicht im Detail beschreiben, aber Sie sollten in der Lage sein, dies selbst zu tun, indem Sie den bereitgestellten Links folgen.
MiDaS und Parallax-Effekt
MiDaS ist ein erstaunliches maschinelles Lernmodell, das eine Tiefenkarte aus einem einzigen Bild berechnen kann (monokulare Tiefenschätzung). Auf mehreren Datensätzen erlernt, liefert es wirklich schöne Ergebnisse und Sie können es auf Ihrem Gerät verwenden. Der größte Teil der Einrichtung erfolgt über Conda, Sie müssen nur das gewünschte Modell (bis zu 1,3 GB) herunterladen. Was für eine Zeit, um am Leben zu sein, Sie können das Wissen einer Gruppe kluger Leute zu Hause leicht nutzen. Sobald Sie ein Bild und die entsprechende Tiefenkarte haben, hindert Sie nichts daran, diesen Punktwolken-Renderer zu verwenden, wie Sie im nächsten Video sehen können.
Aber warten Sie, es gibt noch mehr! Text2Image, Image2Image, Text2Mesh usw. Modelle sind im Trend und wir können nicht zurückbleiben. Das nächste Video zeigt einige KI-generierte Bilder (Midjourney und Stable Diffusion), die durch MiDaS verarbeitet und visualisiert wurden (Wenn sich Bilder ändern, tauschen wir nur die zuvor erwähnten Morphing-Puffer aus):
Sie haben wahrscheinlich bemerkt, dass sich Bilder bewegen. Das ist die Arbeit unseres Vertex-Shaders. Es gibt zwei Arten von Parallaxeneffekten. Zuerst verfolgt man die Kameraposition und versucht, die Wolke so zu morphen, dass die Bildmitte direkt auf die Kamera starrt. Dieser Effekt funktioniert ziemlich gut, sollte aber ausgefeilter sein, da die Schwerkraft nicht immer in der Mitte positioniert ist. Ich plane, bald einen besseren Ansatz zu schreiben. Das folgende Code-Snippet zeigt die Übergabe von Parallaxenparametern an Uniformen, sodass sich jeder Scheitelpunkt entsprechend anpassen kann. Letztendlich wird dies auch auf Shader verschoben.
//Will be simplified once testing / prototyping is finished
const imgCenterPoint = this.geometryHelper.imgCenterPoints[this.guiWrapper.renderingType.getValue()][this.interpolationOptions.frame]
this.lastCameraPosition = this.camera.position.clone()
let angleX = this.camera.position.clone().setY(0).angleTo(imgCenterPoint)
let angleY = this.camera.position.clone().setX(0).angleTo(imgCenterPoint)
let normalX = new THREE.Plane().setFromCoplanarPoints(new THREE.Vector3(), this.camera.position.clone().setY(0), imgCenterPoint).normal
let normalY = new THREE.Plane().setFromCoplanarPoints(new THREE.Vector3(), this.camera.position.clone().setX(0), imgCenterPoint).normal
this.parallaxUniforms.u_scale.value = 1 + this.currentSceneObject.scale.z
this.parallaxUniforms.u_camera_angle.value = new THREE.Vector3(-angleX*normalX.y, angleY*normalY.x, 0)
Bildmorphing und Koalas
Tiefenkarten und KI-generierte Bilder sind cool und alles, aber wir müssen auch der Natur etwas Respekt zollen. Es gibt ein Szenario namens Koala mit zwei Features. Bildmorphing zwischen zwei Bildern und „Natur“-Bilddarstellung. Beginnen wir mit letzterem. In diesem Szenario werden Farben als 24-Bit-Zahlen in die Z-Ebene abgebildet und dann normalisiert. Die räumliche Bilddarstellung hat etwas Magisches, vor allem, weil sie uns die Möglichkeit gibt, Objekte zu fühlen, und uns auch näher bringt, warum neuronale Netze zur Objekterkennung funktionieren. Sie können die Charakterisierung von Objekten im Raum fast fühlen:
In Bezug auf das Morphing haben wir in diesem Szenario zwei verschiedene Koalas und 10 Morphing-Zwischenbilder – Sie können sie per Schieberegler durchblättern oder von WebGL hüpfend animieren lassen. Bilder zwischen Originalen wurden über den Beier-Neely- Morphing-Algorithmus generiert. Für diesen Algorithmus müssen Sie 1–1 Liniensätze abbilden, wobei jede Linie ein Merkmal wie Augen oder Kinn darstellt. Auf den Algorithmus werde ich im nächsten Abschnitt ausführlicher eingehen. Im Video unten können Sie sehen, dass es einige seltsame Fragmente gibt, weil ich nur 10 Linien-Features verwendet habe und die Ergebnisse sehen wollte. Aber auch wenn sie nicht perfekt ist, ist die animierte Version ziemlich nett, besonders der Morphing-Schal, sehr psychedelisch:
Image Morphing, Portrait Depth API und Waifus
Es ist an der Zeit, alles oben Genannte zu kombinieren und ein komplexeres Beispiel zu betrachten. In diesem Abschnitt werden wir zwischen zwei Bildern mit Tiefenkarten interpolieren. Wie ich bereits erwähnt habe, ist MiDaS im Allgemeinen erstaunlich, aber wenn Sie eine Tiefenkarte des menschlichen Gesichts erhalten möchten, benötigen Sie ein detaillierteres Modell, das speziell auf menschliche Gesichter trainiert ist (das bedeutet nicht, dass Sie keine ziemlich gute Tiefenkarte erhalten können). Midas). Anfang dieses Jahres präsentierte TensorFlow die Portrait Depth API und ähnlich wie MiDaS kann auch dieses Projekt auf Ihrem Gerät ausgeführt werden. Für diesen Abschnitt wähle ich Anne Hathaway und Olivia Munn als Morphing-Ziele aus. In der Dropdown-Liste finden Sie diesen Abschnitt mit dem Namen Waifus. Frag mich nicht warum. Schauen wir uns vor dem Morphen Annes Tiefenkarte als Punktwolke an:
Wir können sehen, dass das Modell uns ziemlich gute Ergebnisse lieferte. Wir wären nicht in der Lage, ein wirklichkeitsgetreues Netz des menschlichen Gesichts zu erstellen, aber ansonsten gibt es schöne Details wie Zähne mit Volumen, Kopf deutlich auf einer anderen Höhe als der Oberkörper und das Gesichtsrelief ist auch genau. Man könnte sagen, dass es einen Bildfehler gibt, weil es so aussieht, als hätte Anne ein zweites Kinn, aber wir wissen, dass es nicht stimmt. Wie Nietzsche sagte: „Nur Menschen mit Doppelkinn sind diejenigen, die in den Abgrund ihres gescheiterten Lebens blicken.“ Vertrauen Sie auch keinem Angebot im Internet. :]
Die Portrait Depth API funktioniert in zwei Schritten. Zuerst erkennt es Gesicht/Haare/Hals und andere gesichtsnahe Teile (ich bin mir nicht sicher, wie es gewichtet wird, dieser Satz stammt nur aus meiner Erfahrung beim Herumspielen mit dem Modell), dann macht es eine schwarze Maske auf das Rest des Bildes und erzeugt schließlich eine Tiefenkarte. Der Maskierungsschritt ist nicht immer genau, sodass die endgültige Tiefenkarte scharfes Rauschen am Rand aufweist. Glücklicherweise ist es ein gutes Rauschen – es kann in derselben exemplarischen Vorgehensweise entfernt werden, während in Puffer geschrieben wird. Ich habe eine Heuristik geschrieben, um das Rauschen zu entfernen, und es hat beim ersten Versuch funktioniert.
removeNoise(depthMapColor: number, depthmap: Array<Array<number>>, i: number, j: number, width: number, height: number, pointPointer: number, pointsToRender: Float32Array) {
if (depthMapColor != 0) {
const percentage = depthMapColor/100
let left = depthMapColor
let right = depthMapColor
let top = depthMapColor
let down = depthMapColor
const dropThreshold = 5*percentage
if (j > 0) left = depthmap[i][j-1]
if (j < width-1) right = depthmap[i][j+1]
if (i > 0) top = depthmap[i-1][j]
if (i < height-1) down = depthmap[i+1][j]
if(Math.abs(left - depthMapColor) > dropThreshold || Math.abs(right - depthMapColor) > dropThreshold) {
pointsToRender[pointPointer*3 + 2] = 0
}
else if(Math.abs(top - depthMapColor) > dropThreshold || Math.abs(down - depthMapColor) > dropThreshold) {
pointsToRender[pointPointer*3 + 2] = 0
} else {
// extra scale
pointsToRender[pointPointer*3 + 2] = 3*(1 - depthMapColor)
}
}
}
Da das Morphing von Koalas Fragmente produzierte, entschied ich mich für das Morphing von Gesichtern, TensorFlow und ihr Gesichts-/Feature-Erkennungsmodell zu verwenden. Ich habe ungefähr 80 Feature-Linien verwendet, 8-mal mehr als für Koalas:
Das Ergebnis ist viel besser und es sieht wirklich cool aus, sogar bei Verwendung von Tiefenkarten! Es gibt einen Fehler bei Olivias Haaren, aber nichts, was nicht später behoben werden könnte. Wenn Sie dieses Morphing animieren, schneidet jeder Frame ein Stück Pixel weg. Dies liegt daran, dass der Hintergrund als flach betrachtet wird, aber auch in der Nachbearbeitung behoben werden könnte, indem dieser Teil einfach einer glatten Funktion und einer beschleunigten Animation in Bezug auf den Abstand vom Kopf zugeordnet wird.
Image Morphing im Allgemeinen
In Bezug auf den Beier-Neely-Morphing-Algorithmus konnte ich keine parallelisierte oder GPU-beschleunigte Implementierung finden, sodass die Berechnung von Zwischenframes auf großen Bildern wie 2k * 2k-Pixeln mit Dutzenden von Feature-Zeilen viel Zeit in Anspruch nimmt. In Zukunft plane ich, diese Implementierungen zu schreiben. Die erste Implementierung wäre für die lokale/Server-Nutzung über CUDA und die zweite würde GPGPU und Shader verwenden. GPGPU ist besonders interessant, da Sie kreativer sein können.
Es gibt mehrere Projekte ( z. B. DiffMorph ), die neuronale Netze beim Morphing-Prozess begleiten – als Beschleunigung des Prozesses oder als vollständiger Prozess. Der Grund für die Verwendung und das Experimentieren mit dem guten alten deterministischen Algorithmus ist, dass ich mir ein paar Artikel ansehen und eine GPU-bezogene Implementierung durchführen wollte.
Nebenprojekte sind toll
Es begann als schneller Prototyp, nur um Punktwolken aus meinem anderen Projekt zu visualisieren, aber am Ende verbrachte ich etwa eine Woche damit, nur zum Spaß neue Funktionen zu testen und hinzuzufügen, und ich lernte dabei auch ein paar Dinge. Unten finden Sie einen Link zu meinem Github mit vollständigem Code und zur Website für die Live-Demo. Fühlen Sie sich frei, mit neuen Ideen zu forken oder sogar eine PR zu erstellen. Ich plane, weitere Funktionen hinzuzufügen, aber ich möchte nicht mehr spoilern. Nur ein Haftungsausschluss, ich bin kein Webentwickler, also verwende ich vielleicht einige seltsame Konstruktionen, aber ich bin offen für Kritik!
Github-Repository
Live-Webdemo
Project ist eine statische Website ohne die Möglichkeit, benutzerdefinierte Bilder hochzuladen. Für die benutzerdefinierte Verwendung müssen Sie das Repository klonen und Bilder / Tiefenkarten selbst bereitstellen. Wenn Sie jemals conda und npm verwendet haben, sollten Sie keine Probleme mit MiDaS und anderen Tools haben. Ich könnte das Projekt dynamisch gestalten und serverseitig Tiefenkarten / Morphing / 3D-Fotos generieren, aber es hängt von meinen zeitlichen Möglichkeiten ab. Aber ich plane auf jeden Fall, Funktionen hinzuzufügen und zu überarbeiten. Einige Szenarien führen dazu, dass die Seite auf dem iPhone neu geladen wird, während Safari oder Chrome verwendet wird. Das wird bald behoben.

![Was ist überhaupt eine verknüpfte Liste? [Teil 1]](https://post.nghiatu.com/assets/images/m/max/724/1*Xokk6XOjWyIGCBujkJsCzQ.jpeg)



































