Dieser Artikel wurde gemäß dem Adventskalender verfasst, der von einem DMM-Gruppenkandidaten verfasst wurde, der 2020 seinen Abschluss gemacht hat.
Adventskalender für Absolventen der DMM Group '20 2019
Ich bedauere, dass ich etwas mehr Selbsteinführung oder so etwas hätte schreiben sollen, aber nachdem ich die folgenden Inhalte geschrieben habe, war die Entwicklung des Planetariums eine ziemlich zentrale Aktivität in meinem Studentenleben, daher dieser Artikel Ich werde mich stattdessen vorstellen.
Wie üblich wird ein bestimmter Kreis, der zur faulen Kante gehört, das Planetarium auf dem Festival der Technischen Hochschule ausstellen, und ich werde den Sternmeister machen, der für die Herstellung des Lichtquellengeräts erforderlich ist. (Die gesamte Koordinatenumwandlung der an den Sternmeister ausgegebenen Sterne wurde von unserem super super super exzellenten Junior durchgeführt. Ich und dieser Artikel skizzieren es nur.) Ich habe früher einen ähnlichen Master in Java und Gnuplot ausgegeben, aber das Ergebnis war etwas bimyo. Da Gnuplot ein Diagrammanzeigewerkzeug ist, scheint es nicht gut zu sein, feine Punkte wie Planetariumsterne auszugeben. Deshalb habe ich mich dieses Mal entschlossen, den Python-Wrapper Pycairo von cairo, eine Bildverarbeitungsbibliothek auf niedrigerer Ebene, zu verwenden, um den Sternmaster auszugeben. Die gezeichnete (die erste Seite) wird unten gezeigt.
Durch Drucken dieses PDF-Formats (JIS A4-Format x 12 Blatt) auf ein transparentes Druckpapier, das als OHP-Blatt bezeichnet wird, und anschließendes Entwickeln auf einem Film mit hoher Kontrastleistung, der als Eichhörnchenfilm bezeichnet wird, einem Planetarium-Lichtquellengerät vom Lochtyp Sie können einen Filter erstellen. Dieses Mal ist es das Ziel, diesen Film zu machen.
cairo ist eine Bibliothek auf niedriger Ebene zum Zeichnen von 2D-Vektorbildern.
Als ich es nachgeschlagen habe, scheint es, dass es in verschiedenen großen Oss verwendet wird.
Wie bereits erwähnt, ist Pycairo der Python-Wrapper von cairo. Dies ist Pycairo, aber ich glaube, dass es relativ wenige japanische Dokumente gibt. Nun, englische Dokumente sind überall im Internet verfügbar, daher habe ich kein Problem mit der Informationsmenge.
Wenn Sie im Internet Fragen haben, können Sie diese Dokumente lesen und ausprobieren. Insbesondere wenn Sie das Kairo-Tutorial lesen, können Sie das Funktionsprinzip der Kairo-Bibliothek selbst verstehen. Es ist daher ein Muss.
Bitte installieren Sie python3 und numpy. (Verfahren weggelassen) Es wird auch empfohlen, virtualenv und Jupyter Notebook (Colaboratory-Erweiterung) zu installieren.
Vervollständigen Sie mit dem folgenden Befehl ohne besondere
pip install pycairo
Es werden nur die Funktionen aufgelistet, die für die Entwicklung des Sternmeisters verwendet werden.
Die Größe von JIS A4 wird als "210 mm x 297 mm" angegeben und in metrischen Methoden erläutert, während Pycairo als "1pt = 1/72 von 1 Zoll" für 1pt, die kleinste Einheit, und Yard-Pfund angegeben wird. Ich fühle mich im Gesetz mörderisch. Daher haben wir keine andere Wahl, als eine Überbrückungsfunktion zwischen der metrischen Methode und der Yard-Pound-Methode zu definieren.
def inch(meter):
return 0.0393701 * meter * 72.0
In Pycairo werden RGB-Farbinformationen als numerischer Wert (0,0 bis 1,0) übergeben. Da dies jedoch problematisch ist, definieren wir auch eine Funktion zum Konvertieren der RGB-Zeichenfolge in einen Vektor.
def color(color_string):
return np.array([int(color_string[1:3], 16), int(color_string[3:5], 16), int(color_string[5:7], 16)])
def draw_face(context, color_as_vector, width=100, height=100, margine=0):
context.save()
context.set_source_rgb(
color_as_vector[0],
color_as_vector[1],
color_as_vector[2])
context.translate(margine, margine) #Gehen Sie zum Zeichnen zur Referenzkoordinate (Referenzpunkt)
context.rectangle(0,0,width, height) #Zeichnen Sie, indem Sie jeden Parameter vom Referenzpunkt aus übergeben
context.fill()
context.restore()
with cairo.PDFSurface("example.pdf", canvas_width, canvas_height) as surface:
for face_num in range(12):
context = cairo.Context(surface)
context.scale(1, 1)
draw_face(context,
color_as_vector=color('#000000'),
width=canvas_width-inch(30),
height=canvas_height-inch(30),
margine=inch(15))
surface.show_page() #Erstellen Sie eine neue Seite&Schalten
Erstellen Sie ein PDF-Objekt mit cairo.PDFSurface
. Andere sind "cairo.PSSurface", "cairo.SVGSurface" und "cairo.ImageSurface".
Surface.show_page
ist auch eine Funktion zum Erstellen einer neuen Seite derselben Größe im PDF-Format.
Beachten Sie dann, dass der Prozess zum Erstellen der Figur zwischen "context.save" und "context.restore" geschrieben wird.
Dieses Mal habe ich eine CSV-Datei von meinem Junior erhalten, daher werde ich sie als Daten des Sternenkatalogs importieren.
def gen_star_catalogue(file_name=None, face_num=0):
face_vector = [[] for _ in range(face_num)]
with open(file_name, mode='r') as csv_file:
for line in csv.reader(csv_file, delimiter=' '):
face_vector[int(line[0])].append([ast.literal_eval(line[1]),
ast.literal_eval(line[2]),
ast.literal_eval(line[3])])
for face in range(face_num):
face_vector[face] = np.array(face_vector[face])
return face_vector
Dieses Mal gebe ich eine Figur eines regulären Fünfecks aus, aber um die Flexibilität des Codes zu erhöhen, implementiere ich einen Code, der eine Reihe von regulären Polygonen aus Dreiecken oder mehr ausgibt.
Angenommen, ein reguläres Polygon mit einem umschreibenden Kreis mit einem Radiusradius, der auf center_pos zentriert ist. Berechnen Sie die Koordinaten jedes Scheitelpunkts der n-seitigen Form entsprechend und geben Sie dann die Scheitelpunktgruppe multipliziert mit der Rotationsmatrix aus, die sich an einem beliebigen Punkt in den beiden Dimensionen um ein beliebiges Raduis dreht.
Erzeugung eines positiven N-seitigen Scheitelpunktvektors
def gen_regular_polygon(center_pos=None, n=3, radius=1, rotate=0):
theta = 2 * math.pi / n
rot_rad = 2 * math.pi * rotate / 360.0
verts = np.array([[[math.cos(i*theta) * radius + center_pos[0]],
[math.sin(i*theta) * radius + center_pos[1]],
[1]]
for i in range(n)])
rcos = math.cos(rot_rad)
rsin = math.sin(rot_rad)
r31 = -center_pos[0]*rcos + center_pos[1]*rsin + center_pos[0]
r32 = -center_pos[0]*rsin - center_pos[1]*rcos + center_pos[1]
rot_vec = np.array([[rcos, -rsin, r31],
[rsin, rcos, r32],
[0, 0, 1]])
verts = np.concatenate([[np.dot(rot_vec, vert)] for vert in verts])
verts = np.apply_along_axis(lambda x: np.array([x[0], x[1]]), arr=verts, axis=1).reshape([n, 2])
return verts
Wenn Sie es zu einem 50-seitigen Polygon machen, wird ein Polygon in der Nähe eines Kreises gezeichnet, wie in der folgenden Abbildung gezeigt.
Sie können es mit der Kraft der Mathematik vermasseln. Ich verwende jedoch nur einfache Formeln.
\vec{n}=\frac{\vec{oc}}{|\vec{oc}|}
\vec{m_a}=len\cdot\vec{n}+\vec{a}
\vec{m_b}=len\cdot\vec{n}+\vec{b}
Danach können Sie einen Rand erstellen, indem Sie diese der Reihe nach verbinden und eine Linie zeichnen.
def draw_margines(context=None, verts=None, center_pos=None, edge_size=1, line_color_as_vec=None, frame_width=1, margin_face_num_list=None):
vert_multi = verts.repeat(2, axis=0)
vert_pairs = np.roll(vert_multi, -2).reshape([verts.shape[0], 2, verts.shape[1]])
midpoints = np.apply_along_axis(func1d=lambda x: np.sum(a=x, axis=0)/2, axis=1, arr=vert_pairs)
orth_vecs = midpoints - center_pos
euclid_dists = np.mean(np.apply_along_axis(func1d=lambda x: np.linalg.norm(x), axis=1, arr=orth_vecs))
normals = orth_vecs / euclid_dists
normals_pairs = normals.repeat(2, axis=0).reshape([normals.shape[0], 2, 2])
edges_arr = edge_size * normals_pairs + vert_pairs
# [Ein Ende A.,Kante Eine Seitenecke,Kante B Seitenecke,Ein Ende B.]Ordnen Sie dies in der Reihenfolge an und übergeben Sie es an den Kontext
edges_arr = np.array([[vert_pairs[x,0], edges_arr[x,0], edges_arr[x,1], vert_pairs[x,1]] for x in range(edges_arr.shape[0])])
context.save()
for edges in edges_arr:
first_edge = edges[0]
context.move_to(first_edge[0], first_edge[1])
for edge in edges[1:]:
context.line_to(edge[0], edge[1])
context.set_source_rgb(line_color_as_vec[0],line_color_as_vec[1],line_color_as_vec[2])
context.set_line_width(frame_width)
context.stroke()
context.restore()
Ist es genauer, "Kreis" -Ausgabe als Punktausgabe zu sagen?
def draw_stars(context=None, stars=None, radius=1.0, center_pos=None, brightness=1.0, color_as_vector=None):
context.save()
for star in stars:
context.set_source_rgb(color_as_vector[0],
color_as_vector[1],
color_as_vector[2])
context.arc(star[0]*radius+center_pos[0],
star[1]*radius+center_pos[1],
np.math.sqrt(star[2]) *brightness,
0, 2*math.pi)
context.fill()
context.restore()
Es ist möglich, die im Betriebssystem installierte Schriftart zu verwenden. Lassen Sie uns den Schriftnamen im Voraus in context.select_font_face festlegen. Vielleicht ohne zeichnen.
def draw_text(context=None, text=None, font_size=1, pos_as_vec=None, rotate=0, color_as_vec=None):
context.save()
context.set_font_size(font_size)
context.move_to(pos_as_vec[0], pos_as_vec[1]) #Zum Zeichenpunkt gehen
context.rotate(2*math.pi*rotate/360) #Drehung
context.set_source_rgb(color_as_vec[0], color_as_vec[1], color_as_vec[2])
context.show_text(text) #Zeichen zeichnen
context.restore()
with cairo.PDFSurface("example.pdf", canvas_width, canvas_height) as surface:
for face_num in range(12):
context = cairo.Context(surface)
context.scale(1, 1)
context.select_font_face("Futura")
draw_text(context=context,
text='Face/{:0=2}'.format(face_num+1),
font_size=100,
pos_as_vec=np.array([inch(250),inch(170)]),
rotate=-90,
color_as_vec=color("#ffffff"))
draw_text(context=context,
text="Copyright(C) 2019 Space Science Research Club, National Institute of Technology, Kitakyushu College All rights reserved.",
font_size=10,
pos_as_vec=np.array([inch(40),inch(193)]),
rotate=0,
color_as_vec=color("#ffffff"))
surface.show_page()
Vergleichen Sie abschließend das diesmal gezeichnete Bild mit dem zuletzt mit Gnuplot gezeichneten Bild.
Die Schwarz-Weiß-Inversion wird aufgrund des unterschiedlichen Verwendungszwecks des Masters abgeleitet. Selbst wenn sie abgezogen wird, werden nicht nur die Sterne als runde Punkte dargestellt, sondern auch der Unterschied in der Note jedes Sterns wird sorgfältig als Größe des Kreises verwendet. Sie können sehen, dass es in angezeigt werden kann. Wie Sie sehen können, handelt es sich bei Pycairo um eine Bibliothek auf relativ niedriger Ebene, weshalb es sich um eine Bibliothek handelt, die eine flexible grafische Anzeige ermöglicht. Da die Pycairo-Bibliothek selbst so konzipiert ist, dass sie durch prozedurale Beschreibung gezeichnet wird, ist es beim Schreiben von Code etwas schwierig, den Teil zum Zeichnen von Nebenwirkungen von der reinen Beschreibung im Sinne der funktionalen Sprache zu trennen. Ich fühlte es im Nacken. Das fühlt sich wie eine technische Herausforderung an. Ich denke jedoch, dass die Möglichkeit, mithilfe von Pycairo, das PDF-Dateien ausgeben kann, flexible Grafikdokumente zu erstellen, auf allgemeine Arbeiten angewendet werden kann.
Zum Schluss füge ich die Skizze des diesmal gezeichneten PDF-Dokuments und den gesamten Quellcode ein. Dieses Mal habe ich Goole Colaboratory mit Jupyter Notebook verwendet, damit die Quelle entsprechend angepasst wird.
import cairo, math, csv, ast
from IPython.display import SVG, display
import numpy as np
#Zeichne eine Schnur
def draw_text(context=None, text=None, font_size=1, pos_as_vec=None, rotate=0, color_as_vec=None):
context.save()
context.set_font_size(font_size)
context.move_to(pos_as_vec[0], pos_as_vec[1])
context.rotate(2*math.pi*rotate/360)
context.set_source_rgb(color_as_vec[0], color_as_vec[1], color_as_vec[2])
context.show_text(text)
context.restore()
#Hintergrundfarbe zeichnen
def draw_face(context, color_as_vector, width=100, height=100, margine=0):
context.save()
context.set_source_rgb(
color_as_vector[0],
color_as_vector[1],
color_as_vector[2])
context.translate(margine, margine)
context.rectangle(0,0,width, height)
context.fill()
context.restore()
#Zeichnung des Scheitelpunktvektors
def draw_frame(
context,
verts_array,
fill_color_as_rgb_vec,
frame_color_as_rgb_vec,
frame_width):
if verts_array.shape[0] < 3:
print("ERROR")
exit()
else:
context.save()
first_vert = verts_array[0]
tail_vert_array = verts_array[1:]
context.move_to(first_vert[0], first_vert[1])
for vert in tail_vert_array:
context.line_to(vert[0], vert[1])
context.close_path()
context.set_source_rgb(
fill_color_as_rgb_vec[0],
fill_color_as_rgb_vec[1],
fill_color_as_rgb_vec[2])
context.fill_preserve()
context.set_source_rgb(
frame_color_as_rgb_vec[0],
frame_color_as_rgb_vec[1],
frame_color_as_rgb_vec[2]
)
context.set_line_width(frame_width)
context.stroke()
context.restore()
#Zeichne einen Stern
def draw_stars(context=None, stars=None, radius=1.0, center_pos=None, brightness=1.0, color_as_vector=None):
context.save()
for star in stars:
context.set_source_rgb(color_as_vector[0],
color_as_vector[1],
color_as_vector[2])
context.arc(star[0]*radius+center_pos[0],
star[1]*radius+center_pos[1],
np.math.sqrt(star[2]) *brightness,
0, 2*math.pi)
context.fill()
context.restore()
#Erzeugung eines positiven N-seitigen Scheitelpunktvektors
def gen_regular_polygon(center_pos=None, n=3, radius=1, rotate=0):
theta = 2 * math.pi / n
rot_rad = 2 * math.pi * rotate / 360.0
verts = np.array([[[math.cos(i*theta) * radius + center_pos[0]],
[math.sin(i*theta) * radius + center_pos[1]],
[1]]
for i in range(n)])
rcos = math.cos(rot_rad)
rsin = math.sin(rot_rad)
r31 = -center_pos[0]*rcos + center_pos[1]*rsin + center_pos[0]
r32 = -center_pos[0]*rsin - center_pos[1]*rcos + center_pos[1]
rot_vec = np.array([[rcos, -rsin, r31],
[rsin, rcos, r32],
[0, 0, 1]])
verts = np.concatenate([[np.dot(rot_vec, vert)] for vert in verts])
verts = np.apply_along_axis(lambda x: np.array([x[0], x[1]]), arr=verts, axis=1).reshape([n, 2])
return verts
#Meter-Zoll-Umrechnung
def inch(meter):
return 0.0393701 * meter * 72.0
#Vektorisierung von Agb-Strings
def color(color_string):
return np.array([int(color_string[1:3], 16), int(color_string[3:5], 16), int(color_string[5:7], 16)])
#Leimzeichnung
def draw_margines(context=None, verts=None, center_pos=None, edge_size=1, line_color_as_vec=None, frame_width=1, margin_face_num_list=None):
vert_multi = verts.repeat(2, axis=0)
vert_pairs = np.roll(vert_multi, -2).reshape([verts.shape[0], 2, verts.shape[1]])
midpoints = np.apply_along_axis(func1d=lambda x: np.sum(a=x, axis=0)/2, axis=1, arr=vert_pairs)
orth_vecs = midpoints - center_pos
euclid_dists = np.mean(np.apply_along_axis(func1d=lambda x: np.linalg.norm(x), axis=1, arr=orth_vecs))
normals = orth_vecs / euclid_dists
normals_pairs = normals.repeat(2, axis=0).reshape([normals.shape[0], 2, 2])
edges_arr = edge_size * normals_pairs + vert_pairs
edges_arr = np.array([[vert_pairs[x,0], edges_arr[x,0], edges_arr[x,1], vert_pairs[x,1]] for x in range(edges_arr.shape[0])])
context.save()
for edges in edges_arr:
first_edge = edges[0]
context.move_to(first_edge[0], first_edge[1])
for edge in edges[1:]:
context.line_to(edge[0], edge[1])
context.set_source_rgb(line_color_as_vec[0],line_color_as_vec[1],line_color_as_vec[2])
context.set_line_width(frame_width)
context.stroke()
context.restore()
inner_product = np.apply_along_axis(lambda x: np.dot(x, np.array([0,1])), axis=1, arr=normals)
thetas = np.apply_along_axis(lambda x: np.arccos(x)/(2*np.pi)*360, axis=0, arr=inner_product)
sign = np.apply_along_axis(lambda x: -1 if x[0]>0 else 1, axis=1, arr=normals)
signed_thetas = sign * thetas
print(signed_thetas)
context.save()
for index, theta in enumerate(signed_thetas):
draw_text(context=context,
text='Face/{:0=2}'.format(margin_face_num_list[index]),
font_size=15,
pos_as_vec=orth_vecs[index] + center_pos + normals[index] * edge_size*0.7,
rotate=theta,
color_as_vec=color("#ffffff"))
context.restore()
#Eine Sternenkarte ausschneiden
def gen_star_catalogue(file_name=None, face_num=0):
face_vector = [[] for _ in range(face_num)]
with open(file_name, mode='r') as csv_file:
for line in csv.reader(csv_file, delimiter=' '):
face_vector[int(line[0])].append([ast.literal_eval(line[1]),
ast.literal_eval(line[2]),
ast.literal_eval(line[3])])
for face in range(face_num):
face_vector[face] = np.array(face_vector[face])
return face_vector
#Zuordnung des Kleberzugabeindex
margin_index = [[6, 5, 4, 3, 2], [7, 6, 1, 3, 8], [8, 2, 1, 4, 9],
[9, 3, 1, 5, 10], [10, 4, 1, 6, 11], [11, 5, 1, 2, 7],
[2, 8, 12, 11, 6], [3, 9, 12, 7, 2], [4, 10, 12, 8, 3],
[5, 11, 12, 9, 4], [6, 7, 12, 10, 5], [10, 11, 7, 8, 9]]
#Hyperparameter
normal_scale = 100
canvas_height = inch(210)
canvas_width = inch(297)
face_center_position = np.array([inch(125), inch(105)])
face_radius = inch(74.85727113)
face_mid_dist = inch(77.15727113)
face_rotate_list = [0]+[180]*10+[0]
#Entwurf eines Tabellendatenarrays
star_catalogue = gen_star_catalogue(file_name="./starout.txt", face_num=12)
#Ausgabe der PDF-Datei
with cairo.PDFSurface("example.pdf", canvas_width, canvas_height) as surface:
for face_num in range(12):
context = cairo.Context(surface)
context.scale(1, 1)
context.select_font_face("Futura")
draw_face(context,
color_as_vector=color('#000000'),
width=canvas_width-inch(30),
height=canvas_height-inch(30),
margine=inch(15))
verts = gen_regular_polygon(
center_pos=face_center_position,
n=5,
radius=face_radius,
rotate=face_rotate_list[face_num])
draw_frame(context=context,
verts_array=verts,
fill_color_as_rgb_vec=color('#ffffff'),
frame_color_as_rgb_vec=color('#000000'),
frame_width=0.02)
draw_margines(context=context,
verts=verts,
center_pos=face_center_position,
edge_size=inch(10),
line_color_as_vec=color("#ff0000"),
frame_width=1,
margin_face_num_list=margin_index[face_num])
stars = star_catalogue[face_num]
draw_stars(context=context,
stars=stars,
radius=face_mid_dist,
center_pos=face_center_position,
brightness=0.5,
color_as_vector=color('#000000'))
draw_text(context=context,
text='Face/{:0=2}'.format(face_num+1),
font_size=100,
pos_as_vec=np.array([inch(250),inch(170)]),
rotate=-90,
color_as_vec=color("#ffffff"))
draw_text(context=context,
text="Copyright(C) 2019 Space Science Research Club, National Institute of Technology, Kitakyushu College All rights reserved.",
font_size=10,
pos_as_vec=np.array([inch(40),inch(193)]),
rotate=0,
color_as_vec=color("#ffffff"))
surface.show_page()
Beim Drucken eines Planetarium-Masters verwende ich ein OHP-Blatt, das fast fossil entspricht, aber bei Druckern zu Hause und in der Schule ist das Blatt verstopft und die Druckdichte ist sehr dünn (selbst wenn es in der Umgebung am dunkelsten ist). Es gibt viele solche Dinge. Wenn Sie in diese Situation geraten, können Sie schnell zu einer Druckerei gehen, die OHP-Blätter wie Kinko drucken kann, und hier (zumindest in lokalen Geschäften) können Sie dunkel auf OHP-Blätter drucken. Ist fertig. Beim Drucken von OHP-Blättern unterstützt die Druckerei jedoch in erster Linie nur den Schwarzweißdruck. Selbst wenn dies möglich ist, ist die Farbe superhell, sodass die speziellen Farbinformationen nicht verwendet werden. Seien Sie also in diesem Punkt vorsichtig. Sollte sein. Es ist auch möglich, dass das Blatt und die Hände zum Zeitpunkt des Druckens mit Tinte verschmutzt werden, nicht nur für OHP-Blätter. Daher ist es eine gute Idee, eine Zone bereitzustellen, die nicht wie in der obigen Abbildung gezeigt herumdruckt.