This article was written according to the Advent calendar, which is written by a DMM group candidate who graduated in 2020.
DMM Group '20 Graduate Advent Calendar 2019
I regret that I should have written a little more self-introduction or something, but after writing the contents below, planetarium development was a fairly core activity in my student life, so this article I will introduce myself instead.
As usual, a certain circle that belongs to the rotten edge will exhibit the planetarium at the technical college festival, and I will make the stellar master that is necessary for making the light source device. (All the coordinate conversion of the stars output to the star master was done by our super super super talented junior. I & this article just stippling it.) I used to output a similar master in Java and Gnuplot, but the result was somewhat bimyo. Since Gnuplot is a graph display tool, it seems that it is not good at outputting fine points like planetarium stars. So, this time, I decided to use the Python wrapper Pycairo of cairo, which is a lower level image processing library, to output the stellar master. The drawn one (the first page) is shown below.
By printing this pdf format (JIS A4 size x 12 sheets) on a transparent printing paper called an OHP sheet and then developing it on a film with high contrast performance called a squirrel film, a pinhole type planetarium light source device can be used. You can create a filter. This time's goal is to make this film.
cairo is a low-level library for drawing 2D vector images.
When I looked it up, it seems that it is used in various major oss.
As mentioned earlier, Pycairo is cairo's Python wrapper. This is Pycairo, but I feel that there are relatively few Japanese documents. Well, English documents are all over the net, so I don't have a problem with the amount of information.
If you have any questions on the internet, you can read these documents and try them out. Especially, if you read Cairo Tutorial, you can understand the operating principle of the cairo library itself, so it is a must read.
Please install python3 and numpy. (Procedure omitted) It is also recommended to install virtualenv and Jupyter Notebook (Colaboratory extension)
Complete with the following command without any special
pip install pycairo
Only the functions used for the development of the stellar master are listed.
JIS A4 size is specified as "210 mm x 297 mm" and is explained in the metric system, while Pycairo is specified as "1pt = 1 / 72th of 1in" for the smallest unit of 1pt, which is imperial. I feel murderous in the law. So, I have no choice but to define a bridging function between the metric system and the imperial system.
def inch(meter):
return 0.0393701 * meter * 72.0
In Pycairo, RGB color information is passed as a numerical value (0.0 to 1.0), but since it is troublesome, we also define a function to convert the rgb character string to a vector.
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) #Move to the coordinates (reference point) that is the reference point for drawing
context.rectangle(0,0,width, height) #Draw by passing each parameter from the reference point
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() #Create a new page&switching
Create a PDF object with cairo.PDFSurface
. Others include cairo.PSSurface
, cairo.SVGSurface
, and cairo.ImageSurface
.
Also, surface.show_page
is a function to create a new page of the same size in pdf.
Then note that the process of creating a shape is written between context.save
and context.restore
.
This time, I received a csv file from my junior, so I will import it as data in the star catalog.
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
This time, I output a regular pentagon diagram, but in order to increase the flexibility of the code, I will implement a code that outputs a set of vertices of a regular polygon that is larger than a triangle.
Assume a regular polygon with a circumscribed circle of radius radius centered on center_pos. It calculates the coordinates of each vertex of the n-sided polygon, and then outputs a group of vertices multiplied by a rotation matrix that rotates by any raduis at any point in two dimensions.
Generating a regular N-sided vertex vector
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
Also, if you make it a 50-sided polygon, a polygon close to a circle will be drawn as shown in the figure below.
You can screw it down with the power of mathematics. However, I only use simple formulas.
\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}
After that, you can make a margin by connecting these in order and drawing a line.
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
# [One end A,Edge A side corner,Edge B side corner,One end B]Arrange in the order of, and pass this to the context
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()
Is it more accurate to say "circle" output than point output?
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()
It is possible to use the font installed in the OS. Let's set the font name in context.select_font_face in advance. Maybe draw without it.
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]) #Move to drawing point
context.rotate(2*math.pi*rotate/360) #rotation
context.set_source_rgb(color_as_vec[0], color_as_vec[1], color_as_vec[2])
context.show_text(text) #Character drawing
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()
Finally, compare the image drawn this time with the image drawn with Gnuplot last time.
Black-and-white inversion is derived due to the difference in the purpose of use of the master, but even if it is subtracted, not only the stars are plotted as round dots, but also the difference in the grade of each star is carefully regarded as the size of the circle. You can see that it can be displayed in. As you can see, Pycairo is a relatively low-level library, which is why it is a library that allows flexible graphic display. However, since the Pycairo library itself is designed to draw by procedural description, it is somewhat difficult to separate the side effect drawing part from the pure description in the functional language sense when writing code. I felt it in the neck. This feels like a technical challenge. However, I think that the ability to create flexible graphic documents by using Pycairo, which can output pdf, can be applied to general work.
Finally, I will paste the screenshot of the PDF document drawn this time and the entire source code. This time I used Goole Colaboratory with Jupyter Notebook, so the source will be adapted accordingly.
import cairo, math, csv, ast
from IPython.display import SVG, display
import numpy as np
#Drawing a string
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()
#Background color drawing
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()
#Draw vertex vector
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()
#Draw a star
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()
#Generating a regular N-sided vertex vector
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-inch conversion
def inch(meter):
return 0.0393701 * meter * 72.0
#Vectorization of 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)])
#Drawing of glue
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()
#Cutting out a star chart
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
#Glue allowance index allocation
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]]
#Hyperparameters
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]
#Drafting table data array
star_catalogue = gen_star_catalogue(file_name="./starout.txt", face_num=12)
#PDF file output
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()
When printing a planetarium master, I use transparencies that are almost fossil, but printers at home and school are clogged or the print density is very thin (even if it is the darkest in the setting). There are many things like that. If you fall into this situation, it's quick and easy to go to a printing shop that can print transparencies such as Kinko's, and here (at least at local stores) you can print deeply on transparencies. Is done. However, when printing transparencies, the printing shop only supports black-and-white printing in the first place, and even if it is possible, the colors are super light, so the special color information is not utilized, so be careful about that point. Should be. Also, not only OHP sheets, but also the sheets and hands may get dirty with ink at the moment of printing, so it is a good idea to provide a zone that does not print around as shown in the above figure.
Recommended Posts