In diesem Tutorial zeige ich Ihnen, wie Sie eine SVG-Karte als Vektor auf einen Globus projizieren. Um die mathematischen Transformationen durchzuführen, die erforderlich sind, um die Karte auf eine Kugel zu projizieren, müssen wir die Kartendaten mithilfe von Python-Skript lesen und in ein Bild einer Kugel übersetzen. In diesem Lernprogramm wird davon ausgegangen, dass Sie Python 3.4, den neuesten verfügbaren Python, ausführen.
Inkscape verfügt über eine Art Python-API, mit der verschiedene Aufgaben erledigt werden können. Da wir jedoch nur an der Transformation von Formen interessiert sind, ist es einfacher, ein eigenständiges Programm zu schreiben, das SVG-Dateien selbst liest und druckt.
Die Art der Karte, die wir benötigen, wird als gleichwinklige Karte bezeichnet. In einer gleichwinkligen Karte entspricht der Längen- und Breitengrad eines Ortes seinem x und y Position auf der Karte. Eine gleichwinklige Weltkarte ist auf Wikimedia Commons zu finden (hier eine Version mit US-Bundesstaaten)..
SVG-Koordinaten können auf verschiedene Arten definiert werden. Zum Beispiel können sie relativ zu dem zuvor definierten Punkt sein oder absolut vom Ursprung aus definiert werden. Um unser Leben zu vereinfachen, möchten wir die Koordinaten in der Karte in die absolute Form konvertieren. Inkscape kann das. Rufen Sie die Inkscape-Einstellungen auf Bearbeiten Menü) und unter Input-Output > SVG-Ausgabe, einstellen Pfadzeichenfolgenformat zu Absolut.
Inkscape konvertiert die Koordinaten nicht automatisch. Sie müssen eine Art Transformation auf den Pfaden durchführen, damit dies geschieht. Am einfachsten ist es, einfach alles auszuwählen und mit einem Druck auf die Auf- und Abwärtspfeile auf und ab zu bewegen. Speichern Sie dann die Datei erneut.
Erstellen Sie eine neue Python-Datei. Importieren Sie die folgenden Module:
import sys import re import math import time import datetime import numpy als np import xml.etree.ElementTree als ET
Sie müssen NumPy installieren, eine Bibliothek, mit der Sie bestimmte Vektoroperationen wie Dot-Produkte und Cross-Produkte ausführen können.
Um einen Punkt im dreidimensionalen Raum in ein 2D-Bild zu projizieren, müssen Sie einen Vektor von der Kamera zum Punkt finden und diesen Vektor dann in drei rechtwinklige Vektoren aufteilen.
Die zwei Teilvektoren senkrecht zum Kameravektor (die Richtung, in die die Kamera zeigt) werden x und y Koordinaten eines orthogonal projizierten Bildes. Der Teilvektor parallel zum Kameravektor wird so genannt z Entfernung des Punktes. Um ein orthogonales Bild in ein perspektivisches Bild umzuwandeln, teilen Sie es bitte x und y koordinieren durch die z Entfernung.
An dieser Stelle ist es sinnvoll, bestimmte Kameraparameter zu definieren. Zunächst müssen wir wissen, wo sich die Kamera im 3D-Raum befindet. Speichern Sie seine x, y, und z Koordinaten in einem Wörterbuch.
Kamera = 'x': -15, 'y': 15, 'z': 30
Der Globus befindet sich am Ursprung, daher ist es sinnvoll, die Kamera darauf auszurichten. Das heißt, der Richtungsvektor der Kamera ist der Kameraposition entgegengesetzt.
cameraForward = 'x': -1 * Kamera ['x'], 'y': -1 * Kamera ['y'], 'z': -1 * Kamera ['z']
Es reicht nicht aus zu bestimmen, in welche Richtung die Kamera gerichtet ist - Sie müssen auch eine Drehung für die Kamera festlegen. Definieren Sie dazu einen Vektor senkrecht zur cameraForward
Vektor.
cameraPerwelle = 'x': cameraForward ['y'], 'y': -1 * cameraForward ['x'], 'z': 0
Es ist sehr hilfreich, wenn bestimmte Vektorfunktionen in unserem Programm definiert sind. Definieren Sie eine Vektorgrößenfunktion:
#magnitude eines 3D-Vektors def sumOfSquares (Vektor): Rückgabevektor ['x'] ** 2 + Vektor ['y'] ** 2 + Vektor ['z'] ** 2 Def-Größe (Vektor): Rückgabe mathematisch .sqrt (sumOfSquares (Vektor))
Wir müssen einen Vektor auf einen anderen projizieren können. Da dieser Vorgang ein Punktprodukt umfasst, ist die Verwendung der NumPy-Bibliothek viel einfacher. NumPy verwendet jedoch Vektoren in Listenform ohne die expliziten Bezeichner 'x', 'y', 'z'. Daher benötigen wir eine Funktion, um unsere Vektoren in NumPy-Vektoren zu konvertieren.
# konvertiert den Wörterbuchvektor in die Liste, um den Vektor zu definieren. vectorToList (Vektor): return [Vektor ['x'], Vektor ['y'], Vektor ['z']]
#Projekte u auf v def vectorProjekt (u, v): np.dot zurückgeben (vectorToList (v), vectorToList (u)) / Betrag (v)
Es ist schön, eine Funktion zu haben, die uns einen Einheitsvektor in Richtung eines gegebenen Vektors gibt:
#get unit vector def unitVector (Vektor): magVector = Betrag (Vektor) return 'x': Vektor ['x'] / magVector, 'y': Vektor ['y'] / magVector, 'z': Vektor [ 'z'] / magVector
Schließlich müssen wir zwei Punkte nehmen und einen Vektor zwischen ihnen finden können:
# Berechnet den Vektor aus zwei Punkten, Wörterbuchform def findVector (Ursprung, Punkt): return 'x': Punkt ['x'] - Ursprung ['x'], 'y': Punkt ['y'] - Ursprung [ 'y'], 'z': Punkt ['z'] - Ursprung ['z']
Jetzt müssen wir nur noch die Kameraachsen definieren. Wir haben bereits zwei dieser Achsen-cameraForward
und Kamera senkrecht
, Entsprechend der z Entfernung und x Koordinate des Kamerabildes.
Jetzt brauchen wir nur noch die dritte Achse, die durch einen Vektor definiert wird, der die darstellt y Koordinate des Kamerabildes. Wir können diese dritte Achse finden, indem wir das Kreuzprodukt dieser beiden Vektoren mit NumPy verwenden-np.cross (vectorToList (cameraForward), vectorToList (cameraPerrecht))
.
Das erste Element im Ergebnis entspricht dem x Komponente; der zweite zum y Komponente, und die dritte an die z Komponente, so ist der erzeugte Vektor gegeben durch:
# Berechnet den Vektor der Horizontebene (nach oben gerichtet) cameraHorizon = 'x': np.cross (vectorToList (cameraForward)), vectorToList (cameraPer lot) [0], 'y': np.cross (vectorToList (cameraForward)), vectorToList (cameraPerrect) )) [1], 'z': np.cross (vectorToList (cameraForward), vectorToList (cameraPerrect)) [2]
Um das Orthogonal zu finden x, y, und z Abstand finden wir zuerst den Vektor, der die Kamera mit dem betreffenden Punkt verbindet, und projizieren ihn dann auf jede der drei zuvor definierten Kameraachsen:
def PhysicalProjection (Punkt): pointVector = findVector (Kamera, Punkt) #pointVector ist ein Vektor, der von der Kamera aus beginnt und an einem fraglichen Punkt endet. 'x': vectorProject (pointVector, cameraPer lot), 'y': vectorProject (pointVector , cameraHorizon), 'z': vectorProject (pointVector, cameraForward)
Ein Punkt (dunkelgrau), der auf die drei Kameraachsen (grau) projiziert wird. x ist rot, y ist grün und z ist blau.
Perspektivische Projektion nimmt einfach die x und y der orthogonalen Projektion und teilt jede Koordinate durch die z Entfernung. Dies macht es so, dass Sachen, die weiter entfernt sind, kleiner aussehen als Sachen, die näher an der Kamera sind.
Weil durch teilen z Wenn Sie sehr kleine Koordinaten erhalten, multiplizieren wir jede Koordinate mit einem Wert, der der Brennweite der Kamera entspricht.
Brennweite = 1000
# zeichnet mit xDistance, yDistance und zDistance def perspektive Punkte auf den KamerasensorProjektion (pCoords): scaleFactor = focalLength / pCoords ['z'] return 'x': pCoords ['x'] * scaleFactor, 'y': pCoords [ 'y'] * scaleFactor
Die Erde ist eine Kugel. Unsere Koordinaten (Breitengrad und Längengrad) sind also sphärische Koordinaten. Wir müssen also eine Funktion schreiben, die sphärische Koordinaten in rechtwinklige Koordinaten umwandelt (sowie einen Radius der Erde definiert und den π Konstante):
Radius = 10 pi = 3,14159
# konvertiert sphärische Koordinaten in rechtwinklige Koordinaten def sphereToRect (r, a, b): return 'x': r * math.sin (b * pi / 180) * math.cos (a * pi / 180), 'y' : r * math.sin (b * pi / 180) * math.sin (a * pi / 180), 'z': r * math.cos (b * pi / 180)
Wir können eine bessere Leistung erzielen, indem wir einige Berechnungen speichern, die mehr als einmal verwendet werden:
# konvertiert sphärische Koordinaten in Rechteckkoordinaten def sphereToRect (r, a, b): aRad = math.radians (a) bRad = math.radians (b) r_sin_b = r * math.sin (bRad) Rückgabe 'x': r_sin_b * math.cos (aRad), 'y': r_sin_b * math.sin (aRad), 'z': r * math.cos (bRad)
Wir können einige zusammengesetzte Funktionen schreiben, die alle vorherigen Schritte in einer Funktion kombinieren, die direkt von sphärischen oder rechteckigen Koordinaten zu perspektivischen Bildern führt:
#Funktionen zum Zeichnen von Punkten def rectPlot (Koordinate): RückkehrperspektiveProjektion (physikalischeProjektion (Koordinate)) def KugelPlot (Koordinate, sRadius): Rückgabe rectPlot (SphäreToRect (sRadius, Koordinate ['long']))
Unser Skript muss in eine SVG-Datei schreiben können. So sollte es beginnen mit:
f = offen ('globe.svg', 'w') f.write ('\ n
Und enden mit:
f.write ('')
Eine leere, aber gültige SVG-Datei erstellen. Innerhalb dieser Datei muss das Skript SVG-Objekte erstellen können. Daher definieren wir zwei Funktionen, mit denen SVG-Punkte und -Polygone gezeichnet werden können:
#Zeichnet das SVG-Kreisobjekt def svgCircle (Koordinate, circleRadius, color): f.write ('\ n ') #Zeichnet einen SVG-Polygonknoten def polyNode (Koordinate): f.write (str (Koordinate [' x '] + 400) +', '+ str (Koordinate [' y '] + 400) + ")
Wir können dies testen, indem wir ein sphärisches Punktegitter rendern:
#DRAW GRID für x im Bereich (72): für y im Bereich (36): svgCircle (spherePlot ('long': 5 * x, 'lat': 5 * y, Radius), 1, '#ccc' )
Wenn dieses Skript gespeichert und ausgeführt wird, sollte dies folgendermaßen aussehen:
Um eine SVG-Datei lesen zu können, muss ein Skript eine XML-Datei lesen können, da es sich bei SVG um einen XML-Typ handelt. Deshalb haben wir importiert xml.etree.ElementTree
. Mit diesem Modul können Sie XML / SVG als verschachtelte Liste in ein Skript laden:
tree = ET.parse ('BlankMap Equirectangular states.svg') root = tree.getroot ()
Sie können durch die Listenindizes zu einem Objekt in der SVG navigieren (normalerweise müssen Sie den Quellcode der Map-Datei betrachten, um die Struktur zu verstehen). In unserem Fall befindet sich jedes Land in Wurzel [4] [0] [x] [n]
, woher x ist die Nummer des Landes, beginnend mit 1, und n steht für die verschiedenen Unterwege, die das Land umreißen. Die tatsächlichen Konturen des Landes werden in gespeichert d Attribut, zugänglich durch Wurzel [4] [0] [x] [n] .attrib ['d']
.
Wir können diese Map nicht einfach durchlaufen, da sie am Anfang ein "Dummy" -Element enthält, das übersprungen werden muss. Wir müssen also die Anzahl der „Country“ -Objekte zählen und eins abziehen, um den Dummy zu entfernen. Dann durchlaufen wir die restlichen Objekte.
Länder = len (Wurzel [4] [0]) - 1 für x in Reichweite (Länder): Wurzel [4] [0] [x + 1]
Einige Länderobjekte enthalten mehrere Pfade. Deshalb durchlaufen wir jeden Pfad in jedem Land:
Länder = len (Wurzel [4] [0]) - 1 für x in Reichweite (Länder): für Pfad in Wurzel [4] [0] [x + 1]:
In jedem Pfad gibt es getrennte Konturen, die durch die Zeichen 'Z M' im Zeichen getrennt sind d String, so teilen wir die d String entlang dieses Trennzeichens und durchlaufen jene.
Länder = len (Wurzel [4] [0]) - 1 für x in Reichweite (Länder): für Pfad in Wurzel [4] [0] [x + 1]: für k in re.split ('Z M', path.attrib ['d']):
Wir teilen dann jede Kontur durch die Begrenzungszeichen 'Z', 'L' oder 'M' auf, um die Koordinaten jedes Punktes im Pfad zu erhalten:
für x im Bereich (Länder): für Pfad in Wurzel [4] [0] [x + 1]: für k in re.split ('Z M', Pfad.attrib ['d']): für i in re .split ('Z | M | L', k):
Dann entfernen wir alle nicht numerischen Zeichen aus den Koordinaten und teilen sie entlang der Kommas in zwei Hälften auf, wobei wir die Breiten- und Längengrade angeben. Wenn beides vorhanden ist, speichern wir sie in einem SphäreKoordinaten
Wörterbuch (in der Karte gehen die Breitengradkoordinaten von 0 bis 180 °, aber wir möchten, dass sie von -90 ° bis 90 ° -North und Süd gehen - also subtrahieren wir 90 °).
für x im Bereich (Länder): für Pfad in Wurzel [4] [0] [x + 1]: für k in re.split ('Z M', Pfad.attrib ['d']): für i in re .split ('Z | M | L', k): breakup = re.split (',', re ("[^ - 0123456789.,]", "", i)), wenn breakup [0] und breakup [1]: sphereCoordinates = sphereCoordinates ['long'] = float (breakup [0]) sphereCoordinates ['lat'] = float (breakup [1]) - 90
Wenn wir es dann testen, indem wir einige Punkte zeichnen (svgCircle (spherePlot (sphereCoordinates, radius), 1, '# 333')
) bekommen wir so etwas:
Dies unterscheidet nicht zwischen Punkten auf der nahen Seite des Globus und Punkten auf der anderen Seite des Globus. Wenn wir nur Punkte auf der sichtbaren Seite des Planeten drucken möchten, müssen wir in der Lage sein, herauszufinden, auf welcher Seite des Planeten sich ein bestimmter Punkt befindet.
Wir können dies tun, indem wir die zwei Punkte auf der Kugel berechnen, an denen sich ein Strahl von der Kamera zu dem Punkt mit der Kugel schneiden würde. Diese Funktion implementiert die Formel zum Lösen der Abstände zu diesen beiden Punkten-dNear und dFar:
cameraDistanceSquare = sumOfSquares (Kamera) #Entfernung vom Globuszentrum zur Kamera def distanceToPoint (spherePoint): point = sphereToRect (radius, spherePoint ['long'], spherePoint ['lat']) ray, cameraForward)
def okclude (spherePoint): point = sphereToRect (Radius, spherePoint ['long'], spherePoint ['lat']) ray = findVector (Kamera, Punkt) d1 = Magnitude (ray) #Entfernung von der Kamera zum Punkt dot_l = np. dot ([ray ['x'] / d1, ray ['y'] / d1, ray ['z'] / d1], vectorToList (Kamera)) #dot Produkt des Einheitsvektors von Kamera zu Punkt und Kameravektordeterminante = math.sqrt (abs ((dot_l) ** 2 - cameraDistanceSquare + radius ** 2)) dNear = - (dot_l) + Determinante dFar = - (dot_l) - Determinante
Wenn die tatsächliche Entfernung zum Punkt, d1, ist kleiner oder gleich beide von diesen Entfernungen befindet sich der Punkt auf der nahen Seite der Kugel. Aufgrund von Rundungsfehlern ist in dieser Operation ein kleiner Spielraum eingebaut:
wenn d1 - 0,0000000001 <= dNear and d1 - 0.0000000001 <= dFar : return True else: return False
Wenn Sie diese Funktion als Bedingung verwenden, sollte das Rendern auf nahegelegene Punkte beschränkt sein:
if occlude (sphereCoordinates): svgCircle (spherePlot (sphereCoordinates, radius), 1, '# 333')
Natürlich sind die Punkte keine wirklich geschlossenen, gefüllten Formen - sie vermitteln nur die Illusion von geschlossenen Formen. Um tatsächlich gefüllte Länder zu zeichnen, bedarf es etwas mehr Raffinesse. Zunächst müssen wir die Gesamtheit aller sichtbaren Länder drucken.
Wir können dies tun, indem wir einen Schalter erstellen, der jedes Mal aktiviert wird, wenn ein Land einen sichtbaren Punkt enthält, während die Koordinaten dieses Landes vorübergehend gespeichert werden. Wenn der Schalter aktiviert ist, wird das Land anhand der gespeicherten Koordinaten gezeichnet. Wir werden auch Polygone anstelle von Punkten zeichnen.
für x im Bereich (Länder): für Pfad in root [4] [0] [x + 1]: für k in re.split ('Z M', Pfad.attribut ['d']): countryIsVisible = Falsches Land = [] für i in re.split ('Z | M | L', k): breakup = re.split (',', re ("[^ - 0123456789.,]", "", i) ) wenn breakup [0] und breakup [1]: sphereCoordinates = sphereCoordinates ['long'] = float (breakup [0]) sphereCoordinates ['lat'] = float (breakup [1]) - 90 #DRAW COUNTRY if occlude (sphereCoordinates): country.append ([sphereCoordinates, radius]) countryIsVisible = True, sonst: country.append ([sphereCoordinates, radius]), wenn countryIsVisible: f.write ('\ n \ n ')
Es ist schwer zu sagen, aber die Länder am Rand des Globus falten sich in sich zusammen, was wir nicht wollen (schauen Sie sich Brasilien an).
Um die Länder an den Rändern des Globus richtig rendern zu lassen, müssen Sie zunächst die Platte des Globus mit einem Polygon nachzeichnen (die von den Punkten aus gesehene Platte ist eine optische Täuschung). Die Platte wird durch die sichtbare Kante des Globus umrahmt - ein Kreis. Die folgenden Operationen berechnen den Radius und den Mittelpunkt dieses Kreises sowie den Abstand der Ebene, die den Kreis von der Kamera enthält, und dem Mittelpunkt des Globus.
#TRACE LIMB limbRadius = math.sqrt (Radius ** 2 - Radius ** 4 / cameraDistanceSquare) cx = Kamera ['x'] * Radius ** 2 / cameraDistanceSquare cy = Kamera ['y'] * Radius ** 2 / cameraDistanceSquare cz = Kamera ['z'] * Radius ** 2 / cameraDistanceSquare planeDistance = Größe (Kamera) * (1 - Radius ** 2 / cameraDistanceSquare) planeDisplacement = math.sqrt (cx ** 2 + cy ** 2 + cz ** 2)
Die Erde und die Kamera (dunkelgrauer Punkt) werden von oben betrachtet. Die rosa Linie repräsentiert den sichtbaren Rand der Erde. Nur der schattierte Sektor ist für die Kamera sichtbar.
Um einen Kreis in dieser Ebene zu zeichnen, konstruieren wir zwei Achsen parallel zu dieser Ebene:
#trade und negiere x und y, um einen senkrechten Vektor zu erhalten unitVectorCamera = unitVector (Kamera) aV = unitVector ('x': -unitVectorCamera ['y']), 'y': unitVectorCamera ['x'], 'z': 0) bV = np.cross (vectorToList (aV), vectorToList (unitVectorCamera))
Dann zeichnen wir diese Achsen nur in Schritten von 2 Grad auf, um einen Kreis in dieser Ebene mit diesem Radius und Mittelpunkt zu zeichnen (siehe die folgenden Erläuterungen zur Mathematik):
für t im Bereich (180): theta = math.radians (2 * t) cosT = math.cos (theta) sinT = math.sin (theta) limbPoint = 'x': cx + limbRadius * (cosT * aV [ 'x'] + sinT * bV [0]), 'y': cy + limbRadius * (cosT * aV ['y'] + sinT * bV [1]), 'z': cz + limbRadius * (cosT * aV ['z'] + sinT * bV [2])
Dann kapseln wir alles mit Polygon-Zeichnungscode:
f.write ('')
Wir erstellen auch eine Kopie dieses Objekts, die später als Schnittmaske für alle unsere Länder verwendet werden kann:
f.write ('')
Das sollte dir das geben:
Mit der neu berechneten Platte können wir unsere ändern sonst
Anweisung im Länderplotcode (wenn Koordinaten auf der verborgenen Seite des Globus liegen), um diese Punkte außerhalb der Platte zu zeichnen:
else: tangentscale = (radius + planeDisplacement) / (pi * 0.5) rr = 1 + abs (math.tan ((distanceToPoint (sphereCoordinates) - planeDistance) / tangentscale)) country.append ([sphereCoordinates, radius * rr])
Dabei werden die verborgenen Punkte über der Erdoberfläche mit einer Tangentialkurve angehoben, so dass sie sich um sie herum ausbreiten:
Dies ist mathematisch nicht völlig gesund (es bricht zusammen, wenn die Kamera nicht ungefähr auf die Mitte des Planeten gerichtet ist), aber es ist einfach und funktioniert meistens. Dann einfach hinzufügen clip-path = "url (#clipglobe)"
Mit dem Polygon-Zeichnungscode können wir die Länder an den Rand des Globus ordnen:
if countryIsVisible: f.write ('Ich hoffe, dir hat dieses Tutorial gefallen! Viel Spaß mit deinen Vektorkugeln!