All about turning vector graphics into a set of points.
Remark:
- required packages not yet specified :p,(but it's numpy and fontTools).
Motivation:
- machine learning on vector graphics using point cloud representations
This is a B rendered from 0 points to 128 points.
These are glyphs of a certain font rendered as a point sequence.
This package contains a basis for handling vector graphics.
It provides the powerful class called VectorGraphic
which is
defined by:
- starting point: Tuple[float, float]
- ending point: Tuple[float, float]
- function: Callable[[float], Tuple[float, float]]:
- this is a function that takes in a float and outputs the correspoinding point
- f_t : it can either be a generic parameterization f(t) where t is not remarkable; or
- f_portion_s: it can be f(s/L) where s is the arc length and L is a constant that refers to the length of this vector graphic
- while you can specify both, it only keeps
f_portion_s
, if you pass in af_t
- number of points for approximating this vector graphic as a point cloud/sequence: int
This class can have any weird f_t/f_portion_s as you like, but many times, you would like to use simpler segments and
compose them into more complicated graphics. For this you have SVG inspired components: Line, QuadraticCurve
provided by carrot
. They have ready f_t
/f_portion_s
that they pass to VectorGraphic and they themselves are
implementations of VectorGraphic. Do note that VectorGraphic is not an abstract class and can be used directly (if you
can write down their parameterized functions).
This is how you create a vector graphic. Here we create them using the VectorGraphic
class and the QuadraticCurve
class.
from carrot.vector_graphic import VectorGraphic
from carrot.svg import QuadraticCurve
vg = VectorGraphic(
start_point=(0,1),
end_point=(1,0),
f_t=lambda t: (t, 1-t),
num_points_for_approximation=10
)
curve = QuadraticCurve(
start_point=(1,0),
end_point=(2,3),
control_point=(2.5, 1.5),
num_points_for_approximation=10
)
A vector graphic has some highlight methods
get_as_point_sequence()
which returns an iterator of points equal to that passed in at the constructor(num_points_for_approximation
)get_approximate_length()
which returns the length of this vector graphic__call__(portion_s)
which accepts a number in[0,1]
which denotess/L
~arc_length/total_length
One thing you might want to do is get the vector graphic as a sequence of points.
vg_points: List[Tuple[float, float]] = list(vg.get_as_point_sequence())
If you like them to be a numpy array, you can do the following.
import numpy as np
vg_points: np.ndarray = np.array(list(vg.get_as_point_sequence()))
The neat thing about these, is you can add them together into one vector graphic.
new_vg = vg + curve
The result is itself a vector graphic.
No the result is not a collection of the two - there is no list that collects the pieces together - the new vector graphic
is itself a VectorGraphic
object which means it defines its shape by f_portion_s
; this means that it doesn't know
it was composed of smaller segments and it acts just like any other vector graphic.
Another cool feature is that you don't need the smaller pieces to be connected (connected means: end point of the first == start point of the second). So you can create entire logos, figures, with all just 1 vector graphic object.
This feature wherein the sum of 2 vector graphics is also a vector graphic allows it to be rendered as a point sequence where the adjacent points are equally spaced.
This package uses carrot
in order to deliver the following.
- a TrueType
ttf
font reader that has aread_font(...)
method that returns aFont
object Font
class that has a name, and a__call__(glyph_name)
that returns aGlyph
object if the glyph exists, and raises aKeyError
, otherwise.Glyph
class which is aVectorGraphic
object and has an additional name property
broccoli
was made to fill the needs that I had in the past, for which I could'nt find good solutions for. The solutions i had were hard to engineer and has perfomance issues because it was wildly different solutions stuck together - even then, the resulting point clouds were not as neat.
The TTF Reader that broccoli provides can read a ttf font file for 52 glyphs (a-z, A-Z) in about 0.5s
. This reader can also serves as an example how the carrot
package can be used - other use cases can be otf
font files, svg
files, and others.
Say you have a ttf
file somewhere and you want to get the glyphs A,B,a,b and you want to make point clouds out of them.
You first read the font to the get a Font
object back. After that, you can access the glyphs in the font.
from broccoli.ttf.ttf_reader import TTFReader
font_file_path = "/somewhere/out/there/font_name-italic.ttf"
glyphs_you_want: str = "ABab"
reader = TTFReader(glyph_names_to_read_in_font_files=glyphs_you_want, num_points_for_glyph_as_sequence=128, num_points_for_internal_approximation=2)
font = reader.read_font(font_file_path)
# get the glyph you want
point_cloud = {}
for glyph_name in glyphs_you_want:
glyph = font(glyph_name)
# remember that glyph is a VectorGraphic
point_cloud[glyph.name] = list(glyph.get_as_point_sequence())
Additionally, you can make numpy arrays from them with the coordinates scaled from -1 to 1 (which is useful for machine learning).
from broccoli.utils.font_to_numpy import font_to_numpy
font_as_numpy_array = font_to_numpy(font)
This reader works by reading the ttf
and finding the ttglyfs
that you specified. For each of those glyph specification it then finds the contours of a glpyh. Each contour is a sequence of lines or
curves (encoded as a sequence of on and off points) and each of those is converted as a VectorGraphic
object.
Then, the glyph is then just the sum of all these VectorGraphic
objects.
Given a parametrization of a curve in 2d: f(t:float)=(x,y)
where t in [0,1]
, we first derive g(s)
where s
is the arc length - reparameterization to arc length. To do this we need to solve for t
in terms of s
then replace t in f(t)
to get a new function g(s)=f(t_in_terms_of_s)
.
The arc length from t=0
to some t=t
is given by the integral below
$ s(t) = \int_0^t |f'(t)| dt $
At this, point, we should realize that for a line, the above integral is easy, but even for a parabola (a quadratic curve) it has no closed form, and therefore t in terms of s also has no closed form.
To continue, we should realize another way of computing for the arc length - subdivide the curve into many many linear pieces and add the lengths together (this is an interpretation of the integral above).We have f(t)
at our disposal, which means if we wanted to compute the arc length for 0 to T
we can calculate points: f(0), f(0.01),...f(T)
with as many points as we like (more points yield more accuracy) and compute the lengths between pairs of adjacent points and sum the lengths together.
This means given t
we can compute s
, but we would like to know t
in terms of s
- in other words, if i has s
, i want to know t
.
To do this we do a binary search, given S
we search for t
such that s(t) - S ~=0
. This now gives us a function t(s)
or t
given s
.
With this to derive g(s)
we simply compose t(s)
and f(t)
together to form g(s)=f(t(s))
.
Now that we have reparameterized f
with s
as the arc length, we now proceed to reparameterizing in terms of portion_s
which is simply portion_s=s/L
where L is the length of the segment (computed above calculating the arc length from t=0
to t=1
). Solving for s
we have s=portion_s/L
.
Combining all of these yields: h(portion_s)=g(portion_s*L)=f(t(portion_s*L))
, for simplicity we just call this f(portion_s)
.
At this point, we can think of a VectorGraphic
object as simply f(portion_s)
. If we have two vector graphics left
and right
that we want to add as sum = left + right
(addition does not commute for vector graphics), we can come up with a reasonable way to combine them on the basis of their lengths.
Remember that portion_s
will be inside [0,1]
for any vector graphic. An intuitive addition for left
and right
is that:
sum(0)
tosum(m)
producesleft(0)
toleft(1)
sum(m)
tosum(1)
producesright(0)
toright(1)
In words, this means, that if you travel alongsum
, you will first travel alongleft
then at some point you switch to traveling alongright
.
With this, we can now define the function for sum
(i use z
for a generic variable, we can worry if it is s
,t
, or portion_s
later):
sum(z) = left(k(z)) if z in [0,m]
right(j(z)) else
where k,j
are functions to map z
to portion_s
of left
and right
, respectively.
We now determine an intuitive m
. If left
was just as long as right
, then traveling along sum
(at constant speed), you would expect to reach the end of left
half way way through the journey. Which means sum(0.5)=left(1)=sum(m)
. In another case, if left
was a quarter of the whole journey, then m=0.25
would be intuitive. Generically, m
is the length of left
over the total length of sum
.
m = len(left)/(len(left)+len(right))
Right now it is helpful to notice that we are intuitively passing portion_s
in sum
, so z
will be portion_s
.
The last part is figuring out k
and j
.
We know that k(0) = 0
and k(m)=1
, a natural(linear) solution would just be k(z) = z/m
, then in a similar fashion we set j(z) = (z-m)/(1-m)
(j(m)=0, j(1)=1).
Finally, putting them together:
sum(z) = left(z/m) if z in [0,m]
right((z-m)/(1-m)) else
where z
is portion_s
and m=len(left)/(len(left)+len(right))
.
If you use this project, please cite it as follows. Thank you.
@software{Untalan_Vector_Graphics_Vegetable_2022,
author = {Untalan, Marko Zolo G.},
title = {{Vegetable}},
url = {https://github.com/mzguntalan/vegetable},
year = {2022}
}