Recently, I've come to realize how powerful the convolution operation can be. This notebook is an exploration of image convolution to draw Kanjis.
Convolution is basically the application of a moving signal to an existing signal. Interestingly, this is exactly what we are dowing when we are writing: we move a pen over an imaginary trajectory. Let's try to do illustrate this with a simple example.
%matplotlib inline
import matplotlib.pyplot as plt
from matplotlib.patches import Ellipse
import numpy as np
First we draw an ellipse.
angle = -30
plt.figure(figsize=(6, 6))
plt.xlim(0, 10)
plt.ylim(0, 10)
ellipse = Ellipse((1, 5), 2, 1, angle=angle)
plt.gca().add_artist(ellipse)
<matplotlib.patches.Ellipse at 0x1292518>
But now, let's imagine the ellipse is following a wiggly line.
plt.figure(figsize=(6, 6))
plt.xlim(0, 10)
plt.ylim(0, 10)
ellipse = Ellipse((1, 5), 2, 1, angle=angle)
plt.gca().add_artist(ellipse)
x = np.arange(1, 10, 0.1)
y = 4 * np.sin(2 * np.pi * (x - x.min()) / 10) + 5
plt.plot(x, y)
[<matplotlib.lines.Line2D at 0x7f94c88>]
We can actually draw a lot of ellipses on top of the line to see what effect this yields.
plt.figure(figsize=(6, 6))
plt.xlim(0, 10)
plt.ylim(0, 10)
x = np.arange(1, 10, 0.1)
y = 4 * np.sin(2 * np.pi * (x - x.min()) / 10) + 5
plt.plot(x, y)
for xx, yy in zip(x, y):
ellipse = Ellipse((xx, yy), 2, 1, angle=angle, alpha=0.1)
plt.gca().add_artist(ellipse)
That's it, we are drawing! However, one of the problems we have if we do the drawing like this is that we have to use matplotlib as our drawing backend. It turns out we can do the same operation in a slightly different way without using matplotlib.
Another way of thinking about this drawing is to have two images: one represents the trajectory of the pen (our line) and the over the pattern to be drawn. It turns out a 2D convolution can apply the pattern on top of the trajectory.
Let's first build a trajectory image. To do this, we create an empty image:
n = 128
trajectory = np.zeros((n, n))
We use grid coordinates to rasterize the line (project it to a grid):
X, Y = np.meshgrid(np.linspace(0, 10, n), np.linspace(0, 10, n))
for xx, yy in zip(x, y):
j = np.abs(X-xx).argmin()
i = np.abs(Y[:, j] - yy).argmin()
trajectory[i, j] = 1
Let's look at the result:
plt.imshow(trajectory, origin='lower')
plt.colorbar()
<matplotlib.colorbar.Colorbar at 0x81ab0f0>
Now, let's make a pattern image. We will center the pattern we want to apply. First, let's build rotated coordinate matrices.
X, Y = np.meshgrid(np.linspace(0, 10, n) - 5, np.linspace(0, 10, n) - 5)
Xp = np.cos(np.deg2rad(angle)) * X + np.sin(np.deg2rad(angle)) * Y
Yp = -np.sin(np.deg2rad(angle)) * X + np.cos(np.deg2rad(angle)) * Y
Next, let's define our pattern and plot it:
pattern = Xp**2/2 + Yp**2 < 1
plt.imshow(pattern, origin='lower')
plt.colorbar()
<matplotlib.colorbar.Colorbar at 0x82799e8>
Good, now what we really wand to do is convolve this with our previous matrix.
from scipy.signal import convolve2d
drawing = convolve2d(trajectory, pattern, mode='same')
plt.imshow(drawing, origin='lower')
plt.colorbar()
<matplotlib.colorbar.Colorbar at 0xa0802e8>
Interestingly, we recover the same shape, but the drawing is not the same. Let's try to threshold this:
plt.imshow(drawing > 1, origin='lower')
plt.colorbar()
<matplotlib.colorbar.Colorbar at 0xa143cc0>
Another way to compute the convolution can be to use fftconvolve
. Let's see if this works better.
from scipy.signal import fftconvolve
drawing2 = fftconvolve(trajectory, pattern, mode='same')
Is the result close?
np.allclose(drawing, drawing2)
True
Let's look at it.
plt.imshow(drawing2, origin='lower')
plt.colorbar()
<matplotlib.colorbar.Colorbar at 0xa210208>
plt.imshow(drawing2 > 1, origin='lower')
plt.colorbar()
<matplotlib.colorbar.Colorbar at 0xa2d1780>
Actually, this seems faster than the first convolution. Let's benchmark both of these.
%timeit convolve2d(trajectory, pattern, mode='same')
1 loop, best of 3: 639 ms per loop
%timeit fftconvolve(trajectory, pattern, mode='same')
100 loops, best of 3: 3.97 ms per loop
Indeed, there's nothing to say here, the result is 100 times faster with fftconvolve. Let's move on to our next section.
Kanjivg is a way of describing kanji characters using SVG. This means that it is also valid XML that can be parsed easily. Let's start by downloading a character.
import requests
url = 'https://raw.githubusercontent.com/KanjiVG/kanjivg/master/kanji/04ea2-Kaisho.svg'
r = requests.get(url)
The XML looks like this:
r.text
'<?xml version="1.0" encoding="UTF-8"?>\n<!--\nCopyright (C) 2009/2010/2011 Ulrich Apel.\nThis work is distributed under the conditions of the Creative Commons\nAttribution-Share Alike 3.0 Licence. This means you are free:\n* to Share - to copy, distribute and transmit the work\n* to Remix - to adapt the work\n\nUnder the following conditions:\n* Attribution. You must attribute the work by stating your use of KanjiVG in\n your own copyright header and linking to KanjiVG\'s website\n (http://kanjivg.tagaini.net)\n* Share Alike. If you alter, transform, or build upon this work, you may\n distribute the resulting work only under the same or similar license to this\n one.\n\nSee http://creativecommons.org/licenses/by-sa/3.0/ for more details.\n-->\n<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd" [\n<!ATTLIST g\nxmlns:kvg CDATA #FIXED "http://kanjivg.tagaini.net"\nkvg:element CDATA #IMPLIED\nkvg:variant CDATA #IMPLIED\nkvg:partial CDATA #IMPLIED\nkvg:original CDATA #IMPLIED\nkvg:part CDATA #IMPLIED\nkvg:number CDATA #IMPLIED\nkvg:tradForm CDATA #IMPLIED\nkvg:radicalForm CDATA #IMPLIED\nkvg:position CDATA #IMPLIED\nkvg:radical CDATA #IMPLIED\nkvg:phon CDATA #IMPLIED >\n<!ATTLIST path\nxmlns:kvg CDATA #FIXED "http://kanjivg.tagaini.net"\nkvg:type CDATA #IMPLIED >\n]>\n<svg xmlns="http://www.w3.org/2000/svg" width="109" height="109" viewBox="0 0 109 109">\n<g id="kvg:StrokePaths_04ea2-Kaisho" style="fill:none;stroke:#000000;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;">\n<g id="kvg:04ea2-Kaisho" kvg:element="亢">\n\t<g id="kvg:04ea2-Kaisho-g1" kvg:element="亠" kvg:position="top" kvg:radical="general">\n\t\t<path id="kvg:04ea2-Kaisho-s1" kvg:type="㇑a" d="M49.14,13.75c2.82,1.4,6.97,6.84,7.46,9.58"/>\n\t\t<path id="kvg:04ea2-Kaisho-s2" kvg:type="㇐" d="M19.5,30.11c1.42,0.1,4.67,0.65,6.06,0.55c12.19-0.91,42.94-3.91,57.14-4.2c2.35-0.05,3.55,0.05,5.3,0.7"/>\n\t</g>\n\t<g id="kvg:04ea2-Kaisho-g2" kvg:element="几" kvg:position="bottom">\n\t\t<g id="kvg:04ea2-Kaisho-g3" kvg:element="丿">\n\t\t\t<path id="kvg:04ea2-Kaisho-s3" kvg:type="㇒" d="M39.79,44c0.71,1,0.78,3.36,0.87,5.25c0.18,4.04,0.19,9.72,0.09,17.25C40.5,86,25.25,96.25,16.5,99.5"/>\n\t\t</g>\n\t\t<path id="kvg:04ea2-Kaisho-s4" kvg:type="㇈b" d="M41.75,46c6.5-1,13.5-2.5,20-3.75c2.88-0.55,5,1.5,4.81,4.05c-0.62,8.16-0.41,33.14-0.41,38.7c0,11,6.44,11.53,13.27,11.53c7.21,0,11.08-0.53,12.97-2.62c1.96-2.18,1.56-3,1.75-6.5"/>\n\t</g>\n</g>\n</g>\n<g id="kvg:StrokeNumbers_04ea2-Kaisho" style="font-size:8;fill:#808080">\n\t<text transform="matrix(1 0 0 1 44.50 13.50)">1</text>\n\t<text transform="matrix(1 0 0 1 11.92 30.55)">2</text>\n\t<text transform="matrix(1 0 0 1 32.58 51.48)">3</text>\n\t<text transform="matrix(1 0 0 1 43.25 42.50)">4</text>\n</g>\n</svg>\n'
What it describes is this character:
from IPython.display import SVG
SVG(data=r.text)
We see that there are four strokes in the character. They are described in {http://www.w3.org/2000/svg%7Dpath elements. Let's extract them.
import xml.etree.ElementTree as ET
def extract_paths_from_svg(svg_text):
"""Returns all paths in SVG."""
tree = ET.fromstring(svg_text)
return tree.findall('.//{http://www.w3.org/2000/svg}path')
paths = extract_paths_from_svg(r.text)
paths
[<Element '{http://www.w3.org/2000/svg}path' at 0x00000000154CDF98>, <Element '{http://www.w3.org/2000/svg}path' at 0x0000000012D1DD68>, <Element '{http://www.w3.org/2000/svg}path' at 0x00000000154000E8>, <Element '{http://www.w3.org/2000/svg}path' at 0x0000000015400868>]
paths[0].items()[2]
('d', 'M49.14,13.75c2.82,1.4,6.97,6.84,7.46,9.58')
For each of these elements, the path is described as a succession of movements in the path data.
The M (absolute) or m (relative) indicates a moveto with parameters (x y) that starts a new sub-path at the given (x,y) coordinate
The C (absolute) or c (relative) indicates a curveto with parameters (x1 y1 x2 y2 x y) that draws a cubic Bézier curve from the current point to (x,y) using (x1,y1) as the control point at the beginning of the curve and (x2,y2) as the control point at the end of the curve
Let's look at what a path looks like:
We can easily parse the data string to recover the coordinates for the trajectory.
import re
print(re.split('([McC])', paths[0].items()[2][1]))
['', 'M', '49.14,13.75', 'c', '2.82,1.4,6.97,6.84,7.46,9.58']
This means that the path starts at coordinates 49.14, 13.75 and then continues with a Bézier curve.
Bézier curves are an interesting way of describing parametric curves. As we have seen in the previous section, the $c$ curve-to command in SVG uses cubic Bézier curves.
John D. Cook has a good explanation of what they are. The mathematics is simple: we need to define three points $\vec{Pi}$ and then apply the formula below to every $t \in [0, 1]$ to have a curve.
$$ B(t) = (1-t)^3 \cdot \vec{P0} + 3(1-t)^2 t \cdot \vec{P1} + 3(1-t)t^2 \cdot \vec{P2} + t^3 \cdot \vec{P3} $$Let's write a Bézier curve maker.
def make_bezier_func(points):
"Creates a Bézier function."
return lambda t: (1 - t)**3 * points[0] + \
3 * (1 - t)**2 * t * points[1] + \
3 * (1 - t) * t**2 * points[2] + \
t**3 * points[3]
Let's test this.
points = np.array([[1, 1],
[1, 2],
[2, 3],
[2, 1]])
bz = make_bezier_func(points)
t = np.linspace(0, 1, num=200)[:, np.newaxis]
plt.plot(bz(t)[:, 0], bz(t)[:, 1], label='Bézier curve')
plt.plot(points[:, 0], points[:, 1], '-o', label='control points')
plt.legend()
<matplotlib.legend.Legend at 0x150f0c88>
Now what we need to do is simply to parse the paths into Bézier curves. We can then chain these and draw them on the screen.
We need a way to convert elements into floating points.
def elem_to_points(elem):
"Converts a string in SVG jargon to floats."
elem = elem.strip()
split_points = re.split(',|(-)', elem)
points = []
mult = 1
for item in [_ for _ in split_points if _ is not None if len(_) > 0 if _ != ' ']:
if item == '-':
mult = -1
else:
points.append(mult * float(item))
mult = 1
return points
elem_to_points("0.18,4.04,0.19,9.72,0.09,17.25")
[0.18, 4.04, 0.19, 9.72, 0.09, 17.25]
elem_to_points("12.19-0.91,42.94-3.91,57.14-4.2")
[12.19, -0.91, 42.94, -3.91, 57.14, -4.2]
elem_to_points(' -0.499127,1.753402 -1.680564,3.26525 -2.608477,4.338626 -3.406509,4.993902 -12.418956,11.713536 -19.605535,15.896692')
[-0.499127, 1.753402, -1.680564, 3.26525, -2.608477, 4.338626, -3.406509, 4.993902, -12.418956, 11.713536, -19.605535, 15.896692]
And now we can parse the path.
def parse_path(path_data):
"""Parses a path_data from the SVG into a Bézier curve list."""
curves = []
re_split = re.split('([McCsS])', path_data)
re_split = [_ for _ in re_split if len(_) > 0]
for elem, next_elem in zip(re_split[::2], re_split[1::2]):
if elem == 'M':
current_point = np.array([float(_) for _ in next_elem.split(',')])
if elem == 'c':
points = elem_to_points(next_elem)
points = np.array(points).reshape((-1, 2))
points += current_point # since coords are relative
points = np.vstack((current_point, points))
bz = make_bezier_func(points)
curves.append(bz)
current_point = points[-1, :]
if elem == 'C':
points = elem_to_points(next_elem)
points = np.array(points).reshape((-1, 2))
points = np.vstack((current_point, points))
bz = make_bezier_func(points)
curves.append(bz)
current_point = points[-1, :]
return curves
Let's test this on the data.
plt.figure(figsize=(6, 6))
for path in paths:
path_data = path.items()[2][1]
curves = parse_path(path_data)
t = np.linspace(0, 1, num=200)[:, np.newaxis]
for bz in curves:
plt.plot(bz(t)[:, 0], -bz(t)[:, 1], label='Bézier curve')
plt.xlim(0, 109)
plt.ylim(-109, 0)
Nice! It works. We can now convert this to a rasterized form.
Let's now write a function that rasterizes a list of Bézier curves.
def rasterize(curves, n=128):
"""Computes a rasterized image of the Bézier curves."""
rastered = np.zeros((n, n))
X, Y = np.meshgrid(np.linspace(0, 109, n), np.linspace(-109, 0, n))
t = np.linspace(0, 1, num=200)[:, np.newaxis]
for bz in curves:
x, y = bz(t)[:, 0], -bz(t)[:, 1]
for xx, yy in zip(x, y):
j = np.abs(X-xx).argmin()
i = np.abs(Y[:, j] - yy).argmin()
rastered[i, j] = 1
return rastered
all_curves = []
for path in paths:
path_data = path.items()[2][1]
curves = parse_path(path_data)
all_curves.extend(curves)
rastered = rasterize(all_curves)
plt.figure(figsize=(6, 6))
plt.imshow(rastered, origin='lower')
<matplotlib.image.AxesImage at 0x1534ca20>
What's left missing is now the convolution.
drawing = fftconvolve(rastered, pattern, mode='same')
plt.imshow(drawing, origin='lower')
plt.colorbar()
<matplotlib.colorbar.Colorbar at 0x157c4be0>
plt.imshow(drawing > 2, origin='lower')
plt.colorbar()
<matplotlib.colorbar.Colorbar at 0x1553e3c8>
Well this looks interesting. What remains now is to play with the tools and patterns to draw more interesting figures.
First, let's make a patterning function for the painting sign.
def make_ellipse(n, angle, a, b):
"""Geneartes a rotated ellipse pattern."""
X, Y = np.meshgrid(np.linspace(0, 10, n) - 5, np.linspace(0, 10, n) - 5)
Xp = np.cos(np.deg2rad(angle)) * X + np.sin(np.deg2rad(angle)) * Y
Yp = -np.sin(np.deg2rad(angle)) * X + np.cos(np.deg2rad(angle)) * Y
pattern = Xp**2/a + Yp**2/b < 1
return pattern
We can now generate some patterns and paint them.
from ipywidgets import interact_manual, interact
from skimage.filters import threshold_otsu
@interact
def paint_kanji(a=(0.1, 4), b=(0.1, 4),
angle=(-90, 90)):
plt.figure(figsize=(10, 5))
pattern = make_ellipse(n, angle, a, b)
drawing = fftconvolve(rastered, pattern, mode='same')
plt.subplot(121)
plt.imshow(drawing, origin='lower', cmap='Reds')
plt.colorbar()
plt.subplot(122)
plt.imshow(drawing > threshold_otsu(drawing), origin='lower', cmap='Reds')
plt.colorbar()
@interact_manual
def paint_kanji():
angle = np.random.randint(-90, 90)
a = np.random.rand() * 2
b = np.random.rand() * 3
pattern = make_ellipse(n, angle, a, b)
drawing = fftconvolve(rastered, pattern, mode='same')
plt.subplot(121)
plt.imshow(drawing, origin='lower', cmap='Greys')
plt.colorbar()
plt.subplot(122)
plt.imshow(drawing > threshold_otsu(drawing), origin='lower', cmap='Reds')
plt.colorbar()
Let's study the a and b parameters of the ellipse pattern.
param_dims = (10, 10)
a_s = np.linspace(0.1, 4, num=param_dims[0])
b_s = np.linspace(0.1, 3, num=param_dims[1])
fig, ax = plt.subplots(param_dims[0], param_dims[1], figsize=(12, 12))
for i in range(ax.shape[0]):
for j in range(ax.shape[1]):
pattern = make_ellipse(n, angle, a_s[i], b_s[j])
drawing = fftconvolve(rastered, pattern, mode='same')
ax[i, j].imshow(drawing, cmap='Reds', origin='lower')
ax[i, j].axis('off')
We can do the same thing with a threshold.
fig, ax = plt.subplots(param_dims[0], param_dims[1], figsize=(12, 12))
for i in range(ax.shape[0]):
for j in range(ax.shape[1]):
pattern = make_ellipse(n, angle, a_s[i], b_s[j])
drawing = fftconvolve(rastered, pattern, mode='same')
ax[i, j].imshow(drawing > threshold_otsu(drawing), cmap='Reds', origin='lower')
ax[i, j].axis('off')
We can also explore the angle parameter.
angles = np.linspace(-90, 90, num=param_dims[0])
fig, ax = plt.subplots(10, 10, figsize=(12, 12))
for i in range(ax.shape[0]):
for j in range(ax.shape[1]):
pattern = make_ellipse(n, angles[i], 0.2, b_s[j])
drawing = fftconvolve(rastered, pattern, mode='same')
ax[i, j].imshow(drawing, cmap='Reds', origin='lower')
ax[i, j].axis('off')
fig, ax = plt.subplots(10, 10, figsize=(12, 12))
for i in range(ax.shape[0]):
for j in range(ax.shape[1]):
pattern = make_ellipse(n, angles[i], 0.2, b_s[j])
drawing = fftconvolve(rastered, pattern, mode='same')
ax[i, j].imshow(drawing > threshold_otsu(drawing), cmap='Reds', origin='lower')
ax[i, j].axis('off')
We can easily fetch more characters to display on the KanjiVG website, imitating the mechanism inside their javascript.
First request to get the sha:
r = requests.get("https://api.github.com/repos/KanjiVG/kanjivg/git/refs/heads/master")
sha = r.json()['object']['sha']
Second request: from the SHA we get the repo content.
r = requests.get("https://api.github.com/repos/KanjiVG/kanjivg/git/trees/" + sha)
r
<Response [200]>
for item in r.json()['tree']:
if item['path'] == 'kanji':
print(item)
break
{'sha': '1d160f49915b3f573639347124a5c108e25de5fd', 'path': 'kanji', 'url': 'https://api.github.com/repos/KanjiVG/kanjivg/git/trees/1d160f49915b3f573639347124a5c108e25de5fd', 'mode': '040000', 'type': 'tree'}
Third request to acutally fetch all kanji files in the repo.
r = requests.get(item['url'])
r
<Response [200]>
This request allows us to find the urls and filenames of all kanjis. For instance the first one.
tree = r.json()['tree']
tree[0]
{'mode': '100644', 'path': '00021.svg', 'sha': 'ef913b00bedc63bc7cb4b50f0783bfd417c02693', 'size': 1944, 'type': 'blob', 'url': 'https://api.github.com/repos/KanjiVG/kanjivg/git/blobs/ef913b00bedc63bc7cb4b50f0783bfd417c02693'}
url = tree[1020]['url']
r = requests.get(url)
r
<Response [200]>
content = r.json()['content']
The content is base64 encoded so we need to decode it.
import base64
decoded_content = base64.b64decode(content).decode('utf-8')
decoded_content
'<?xml version="1.0" encoding="UTF-8"?>\n<!--\nCopyright (C) 2009/2010/2011 Ulrich Apel.\nThis work is distributed under the conditions of the Creative Commons\nAttribution-Share Alike 3.0 Licence. This means you are free:\n* to Share - to copy, distribute and transmit the work\n* to Remix - to adapt the work\n\nUnder the following conditions:\n* Attribution. You must attribute the work by stating your use of KanjiVG in\n your own copyright header and linking to KanjiVG\'s website\n (http://kanjivg.tagaini.net)\n* Share Alike. If you alter, transform, or build upon this work, you may\n distribute the resulting work only under the same or similar license to this\n one.\n\nSee http://creativecommons.org/licenses/by-sa/3.0/ for more details.\n-->\n<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd" [\n<!ATTLIST g\nxmlns:kvg CDATA #FIXED "http://kanjivg.tagaini.net"\nkvg:element CDATA #IMPLIED\nkvg:variant CDATA #IMPLIED\nkvg:partial CDATA #IMPLIED\nkvg:original CDATA #IMPLIED\nkvg:part CDATA #IMPLIED\nkvg:number CDATA #IMPLIED\nkvg:tradForm CDATA #IMPLIED\nkvg:radicalForm CDATA #IMPLIED\nkvg:position CDATA #IMPLIED\nkvg:radical CDATA #IMPLIED\nkvg:phon CDATA #IMPLIED >\n<!ATTLIST path\nxmlns:kvg CDATA #FIXED "http://kanjivg.tagaini.net"\nkvg:type CDATA #IMPLIED >\n]>\n<svg xmlns="http://www.w3.org/2000/svg" width="109" height="109" viewBox="0 0 109 109">\n<g id="kvg:StrokePaths_05321" style="fill:none;stroke:#000000;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;">\n<g id="kvg:05321" kvg:element="匡">\n\t<g id="kvg:05321-g1" kvg:element="匚" kvg:part="1" kvg:radical="general">\n\t\t<path id="kvg:05321-s1" kvg:type="㇐" d="M16.63,21.23c3.09,1.1,7.07,0.6,10.26,0.24c15.24-1.72,33.69-3.46,48.86-4.06c3.7-0.15,7.48-0.12,11.13,0.66"/>\n\t</g>\n\t<g id="kvg:05321-g2" kvg:element="王" kvg:original="玉" kvg:partial="true">\n\t\t<path id="kvg:05321-s2" kvg:type="㇐" d="M34.49,35.07c2.09,0.79,5,0.45,7.13,0.23c7.55-0.77,19.09-2.25,27.01-2.86c2.39-0.18,4.94-0.52,7.3,0.06"/>\n\t\t<path id="kvg:05321-s3" kvg:type="㇑a" d="M54.35,35.75c1.31,1.33,1.31,2.5,1.31,4.19c0,7.45-0.01,19.01-0.01,30.95"/>\n\t\t<path id="kvg:05321-s4" kvg:type="㇐" d="M37.07,53.57c2.05,0.43,4.51,0.58,6.3,0.39c6.48-0.68,16.41-1.83,23.63-2.48c2.36-0.21,4.59-0.4,6.92,0.14"/>\n\t\t<path id="kvg:05321-s5" kvg:type="㇐" d="M31,73.33c2.76,0.78,5.71,0.77,8.5,0.44c12.75-1.52,23.21-2.5,36.13-3.01c2.98-0.12,5.75-0.24,8.62,0.69"/>\n\t</g>\n\t<g id="kvg:05321-g3" kvg:element="匚" kvg:part="2" kvg:radical="general">\n\t\t<path id="kvg:05321-s6" kvg:type="㇗" d="M20.77,22.63c1.06,1.06,1.24,2.45,1.23,4.25C21.92,38.78,22.01,72.25,22.01,86c0,4.68,0.26,6.41,5.11,5.79C41,90,66.5,88.38,80.75,87.98c4.13-0.11,8.74-0.23,12.75,0.77"/>\n\t</g>\n</g>\n</g>\n<g id="kvg:StrokeNumbers_05321" style="font-size:8;fill:#808080">\n\t<text transform="matrix(1 0 0 1 23.00 17.88)">1</text>\n\t<text transform="matrix(1 0 0 1 33.25 32.25)">2</text>\n\t<text transform="matrix(1 0 0 1 47.25 44.25)">3</text>\n\t<text transform="matrix(1 0 0 1 36.50 50.88)">4</text>\n\t<text transform="matrix(1 0 0 1 30.25 70.38)">5</text>\n\t<text transform="matrix(1 0 0 1 14.00 32.88)">6</text>\n</g>\n</svg>\n'
Finally, we can use this.
decoded_content
'<?xml version="1.0" encoding="UTF-8"?>\n<!--\nCopyright (C) 2009/2010/2011 Ulrich Apel.\nThis work is distributed under the conditions of the Creative Commons\nAttribution-Share Alike 3.0 Licence. This means you are free:\n* to Share - to copy, distribute and transmit the work\n* to Remix - to adapt the work\n\nUnder the following conditions:\n* Attribution. You must attribute the work by stating your use of KanjiVG in\n your own copyright header and linking to KanjiVG\'s website\n (http://kanjivg.tagaini.net)\n* Share Alike. If you alter, transform, or build upon this work, you may\n distribute the resulting work only under the same or similar license to this\n one.\n\nSee http://creativecommons.org/licenses/by-sa/3.0/ for more details.\n-->\n<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd" [\n<!ATTLIST g\nxmlns:kvg CDATA #FIXED "http://kanjivg.tagaini.net"\nkvg:element CDATA #IMPLIED\nkvg:variant CDATA #IMPLIED\nkvg:partial CDATA #IMPLIED\nkvg:original CDATA #IMPLIED\nkvg:part CDATA #IMPLIED\nkvg:number CDATA #IMPLIED\nkvg:tradForm CDATA #IMPLIED\nkvg:radicalForm CDATA #IMPLIED\nkvg:position CDATA #IMPLIED\nkvg:radical CDATA #IMPLIED\nkvg:phon CDATA #IMPLIED >\n<!ATTLIST path\nxmlns:kvg CDATA #FIXED "http://kanjivg.tagaini.net"\nkvg:type CDATA #IMPLIED >\n]>\n<svg xmlns="http://www.w3.org/2000/svg" width="109" height="109" viewBox="0 0 109 109">\n<g id="kvg:StrokePaths_05321" style="fill:none;stroke:#000000;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;">\n<g id="kvg:05321" kvg:element="匡">\n\t<g id="kvg:05321-g1" kvg:element="匚" kvg:part="1" kvg:radical="general">\n\t\t<path id="kvg:05321-s1" kvg:type="㇐" d="M16.63,21.23c3.09,1.1,7.07,0.6,10.26,0.24c15.24-1.72,33.69-3.46,48.86-4.06c3.7-0.15,7.48-0.12,11.13,0.66"/>\n\t</g>\n\t<g id="kvg:05321-g2" kvg:element="王" kvg:original="玉" kvg:partial="true">\n\t\t<path id="kvg:05321-s2" kvg:type="㇐" d="M34.49,35.07c2.09,0.79,5,0.45,7.13,0.23c7.55-0.77,19.09-2.25,27.01-2.86c2.39-0.18,4.94-0.52,7.3,0.06"/>\n\t\t<path id="kvg:05321-s3" kvg:type="㇑a" d="M54.35,35.75c1.31,1.33,1.31,2.5,1.31,4.19c0,7.45-0.01,19.01-0.01,30.95"/>\n\t\t<path id="kvg:05321-s4" kvg:type="㇐" d="M37.07,53.57c2.05,0.43,4.51,0.58,6.3,0.39c6.48-0.68,16.41-1.83,23.63-2.48c2.36-0.21,4.59-0.4,6.92,0.14"/>\n\t\t<path id="kvg:05321-s5" kvg:type="㇐" d="M31,73.33c2.76,0.78,5.71,0.77,8.5,0.44c12.75-1.52,23.21-2.5,36.13-3.01c2.98-0.12,5.75-0.24,8.62,0.69"/>\n\t</g>\n\t<g id="kvg:05321-g3" kvg:element="匚" kvg:part="2" kvg:radical="general">\n\t\t<path id="kvg:05321-s6" kvg:type="㇗" d="M20.77,22.63c1.06,1.06,1.24,2.45,1.23,4.25C21.92,38.78,22.01,72.25,22.01,86c0,4.68,0.26,6.41,5.11,5.79C41,90,66.5,88.38,80.75,87.98c4.13-0.11,8.74-0.23,12.75,0.77"/>\n\t</g>\n</g>\n</g>\n<g id="kvg:StrokeNumbers_05321" style="font-size:8;fill:#808080">\n\t<text transform="matrix(1 0 0 1 23.00 17.88)">1</text>\n\t<text transform="matrix(1 0 0 1 33.25 32.25)">2</text>\n\t<text transform="matrix(1 0 0 1 47.25 44.25)">3</text>\n\t<text transform="matrix(1 0 0 1 36.50 50.88)">4</text>\n\t<text transform="matrix(1 0 0 1 30.25 70.38)">5</text>\n\t<text transform="matrix(1 0 0 1 14.00 32.88)">6</text>\n</g>\n</svg>\n'
Let's write a function that does all the heavy lifting above.
r.status_code
200
def get_decoded_svg(index):
"Returns decoded SVG from internet."
url = tree[index]['url']
r = requests.get(url)
if r.status_code == 200:
content = r.json()['content']
return base64.b64decode(content).decode('utf-8')
else:
return None
decoded_content = get_decoded_svg(1020)
SVG(data=decoded_content)
<IPython.core.display.SVG object>
paths = extract_paths_from_svg(decoded_content)
plt.figure(figsize=(6, 6))
all_curves = []
for path in paths:
path_data = path.items()[2][1]
curves = parse_path(path_data)
all_curves.extend(curves)
rastered = rasterize(all_curves)
drawing = fftconvolve(rastered, pattern, mode='same')
plt.imshow(drawing, origin='lower', cmap='Reds')
<matplotlib.image.AxesImage at 0x16f7b550>
Let's now build a random gallery using random strokes.
len(tree)
11456
ax.size
100
fig, ax = plt.subplots(10, 10, figsize=(12, 12))
indexes = np.random.choice(np.arange(len(tree)), size=ax.size, replace=False)
for i in range(ax.shape[0]):
for j in range(ax.shape[1]):
decoded_content = get_decoded_svg(indexes.reshape(ax.shape)[i, j])
if decoded_content is not None:
paths = extract_paths_from_svg(decoded_content)
all_curves = []
for path in paths:
path_data = path.items()[2][1]
curves = parse_path(path_data)
all_curves.extend(curves)
rastered = rasterize(all_curves)
pattern = make_ellipse(n, angles[i], 0.2, b_s[j])
drawing = fftconvolve(rastered, pattern, mode='same')
ax[i, j].imshow(drawing > threshold_otsu(drawing), cmap='Reds', origin='lower')
ax[i, j].axis('off')
There is a bug that will prevent the construction of different types of characters... since I was too lazy to parse all the data.