I have decided to look at adding 2D transforms to the mosaic style images. I will apply some number of linear and rotational affine transforms to the image. Rotational transforms by themselves won’t really do much as they’d end up drawn overtop of each other. So, a linear shift seems obligatory.
In my first attempt at these transforms, I used some matplolib methods to draw the transforms. But that presented its own issues. Subsequently I used Numpy methods to do the work. A bit more effort on my part, but well worth the bit of trouble.
I don’t at the moment recall how many, but generating/drawing the transformed images is going to require a few new functions and perhaps a lengthy route function. A new route by the way. I am currently thinking /spirograph/mosaic_t
, with a function named sp_mosaic_t
.
Affine Transforms
I am going to allow the user to select the number of rotational transforms. Something like 3-18. Not sure how that will work with the free Google App Engine account. Will add a bit more computation and memory requirement for each image. We shall see. But for development and testing I will use 4 transforms.
Generating Transformed Data
The first function will take the base curve’s data and return the transformed data. That will hopefully allow me to reuse some of the previous methods with minimal refactoring. Given Numpy’s power, pretty straightforward.
def affine_transformation(srcx, srcy, a_tr):
dstx = []
dsty = []
for i in range(len(srcx)):
# srcx/y are python lists, convert to numpy array
src = np.array([srcx[i], srcy[i]], dtype='float')
# apply transform matrix, dot product
new_points = a_tr @ src
dstx.append(new_points[0])
dsty.append(new_points[1])
# convert lists of transformed x and y data to numpy arrays
return np.array(dstx, dtype="float"), np.array(dsty, dtype="float")
In order to generate the transformation array, I need to know the angles of all the rotations. Which of course will depend on how many of them there are. So, another function. I am using radians as that’s the default for Numpy’s methods. And a module level variable.
rd_45 = math.radians(45)
def get_angles(nbr_rots):
tr_ang = 2 * np.pi / nbr_rots
st_ang = tr_ang
# want first rotation in upper left quadrant
while st_ang > rd_45:
st_ang = st_ang / 2
rot_ang = [st_ang + (tr_ang * i) for i in range(nbr_rots)]
return rot_ang
The approach I used for the linear translations is as follows.
- get max x and y values used by the image curves,
mdy
- take a random fraction of that value,
trdy = mdy * ##
- to get the center for the related rotation, rotate
(0, trdy)
by the angle of the rotational transform - add the
x
andy
values following the rotation to the appropriate data arrays
Most of that will be done within the routing code, at least for now. But, I did write a function to do the rotation.
def rotate_pt(px, py, angle=rd_45, cx=0, cy=0):
a_cos = math.cos(angle)
a_sin = math.sin(angle)
xp_c = px - cx
yp_c = py - cy
x_rt = (xp_c * a_cos) - (yp_c * a_sin) + cx
y_rt = (xp_c * a_sin) + (yp_c * a_cos) + cy
return x_rt, y_rt
One last function. This one doesn’t have anything to do with calculating the transformed data. I started by drawing the rotations in order. But eventually decided to do them in a random order. So, I wrote a function to crank out that random order. Rather simple— but perhaps tidier code.
def get_rnd_qds(qds1):
# remember to get copy not alias, don't want to alter previous list
t_qds = qds1[:]
rng.shuffle(t_qds)
return t_qds
New Route
I will eventually add a new field to the image parameter interface form. But for now, I will just go with 4 transforms.
Some additional global variables. And function(s).
First function gets the estimated plot bounds for a given curve dataset. I am using a function because I check the min and max values for every data row in the x
and y
datasets. (I just copied the code from my earlier project, so please ignore the x_adj
parameter.)
def get_plot_bnds(f_xs, f_ys, x_adj=0):
x_nlim, x_xlim, y_nlim, y_xlim = 0, 0, 0, 0
n_rws = len(f_xs)
for i in range(n_rws):
x_nlim = min(min(f_xs[i]), x_nlim)
x_xlim = max(max(f_xs[i]), x_xlim)
y_nlim = min(min(f_ys[i]), y_nlim)
y_xlim = max(max(f_ys[i]), y_xlim)
return x_nlim - x_adj, x_xlim + x_adj, y_nlim - x_adj, y_xlim + x_adj
And the route code.
@app.route('/spirograph/mosaic_t', methods=['GET', 'POST'])
def sp_mosaic_t():
pg_ttl = "Mosaic Like Spirograph Image with 2D Transforms"
if request.method == 'GET':
f_data = {'pttl': pg_ttl, 'shapes': splt.shp_nm, 'cmaps': g.clr_opt, 'mxs': g.mx_s,
'mntr': g.mn_tr, 'mxtr': g.mx_tr
}
return render_template('get_curve.html', type='mosaic', f_data=f_data)
elif request.method == 'POST':
# print(f"POST[shape]: {request.form.get('shape')}, POST[shp_mlt]: {request.form.get('shp_mlt')}")
sal.proc_curve_form(request.form, 'mosaic')
t_xs, t_ys, t_shp = sal.init_curve()
r_xs, r_ys, m_xs, m_ys, m2_xs, m2_ys = sal.get_gnarly_dset(t_xs, t_ys, g.r_skp, g.drp_f, g.t_sy)
ax.clear()
# ax.patch.set_alpha(0.01)
g.rcm, g.cycle = sal.set_clr_map(ax, g.mx_s, g.rcm)
# get angles
lld_rot = sal.get_angles(g.nbr_tr)
lld_deg = [math.degrees(ang) for ang in lld_rot]
# get init point for linear translations
mnx41, mxx41, mny41, mxy41 = sal.get_plot_bnds(r_xs, r_ys, x_adj=0)
mdy = max(mxx41, mxy41)
y_mlt = rng.choice([.875, .75, .625, .5, .375])
trdy = mdy * y_mlt
# drop first data row?
if g.n_whl == 3:
src_st = 0
else:
src_st = 1
src = np.array([r_xs[src_st:], r_ys[src_st:]], dtype="float")
# get rand quadrant order
do_qd = list(range(g.nbr_tr))
do_qd2 = sal.get_rnd_qds(do_qd)
# Generate transformed datasets
qd_data = []
for i in do_qd:
a = np.array([[np.cos(lld_rot[i]), -np.sin(lld_rot[i])],
[np.sin(lld_rot[i]), np.cos(lld_rot[i])]])
dstx, dsty = sal.affine_transformation(src[0], src[1], a)
dx, dy = sal.rotate_pt(0, trdy, angle=lld_rot[i], cx=0, cy=0)
dstx = dstx + dx
dsty = dsty + dy
# print(f"\trot angle: {math.degrees(lld_rot[i])}, pt angle: {math.degrees(pt_rot[i])}, dx: {dx}, dy: {dy}")
qd_data.append([dstx, dsty])
# only change multipliers the first time, not on later loops
sal.cnt_btw = 0
for i in do_qd2:
tr_xs = qd_data[i][0]
tr_ys = qd_data[i][1]
sal.btw_n_apart(ax, tr_xs, tr_ys, dx=g.bw_dx, ol=g.bw_o, r=g.bw_r, fix=None, mlt=g.bw_m, sect=g.bw_s, alpha=1)
ax.autoscale()
sal.cnt_btw += 1
ax.text(0.5, 0.01, "© koach (barkncats) 2022", ha="center", transform=ax.transAxes,
fontsize=10, alpha=.3, c=g.cycle[len(g.cycle)//2])
ax.autoscale()
bax2, _ = sal.set_bg(ax)
# Save it to a temporary buffer.
buf = BytesIO()
fig.savefig(buf, format="png", bbox_inches="tight", dpi=72)
# Embed the result in the html output.
data = base64.b64encode(buf.getbuffer()).decode("ascii")
c_data = sal.get_curve_dtl()
i_data = sal.get_image_dtl(pg_ttl)
p_data = {'bdx': g.bw_dx, 'bol': g.bw_o, 'bcs': g.bw_s,
'bmlt': g.bw_m, 'mlts': sal.c_mlts,
'ntr': g.nbr_tr, 'trang': lld_deg, 'trpt': f'(0, {round(trdy, 3)}) ({y_mlt})',
'trord': do_qd2
}
return render_template('disp_image.html', sp_img=data, type='mosaic', c_data=c_data, i_data = i_data, p_data=p_data)
I am thinking some refactoring is in order. A few new functions should help. But, first let’s add that new field to the curve parameter form.
Refactor Curve Parameter Form
The global variables I added were:
# transform related vars
mn_tr = 3 # min number of transforms
mx_tr = 18 # max number of transforms
nbr_tr = 4 # number of rotational transformations
The form will be used to set the value for nbr_tr
. I added the following to the bottom of the form in the template get_curve.html
.
{% if 'mntr' in f_data %}
<div>
<p style="margin-left:1rem;margin-top:0;font-size:110%;"><b>2D Transforms (Rotational + Linear)</b></p>
<div>
<label for="ntr">Number of transforms: </label><br>
<input type="text" name="ntr" placeholder="'r' for random, or number {{ f_data['mntr'] }}-{{ f_data['mxtr'] }} inclusive"/>
</div>
</div>
{% endif %}
And in sp_app_lib.proc_curve_form()
I added some code to handle the new field. After the code handling the colour fields and before the code specific to gnarly type curves.
if 'ntr' in f_data:
if f_data['ntr'].isnumeric():
t_ntr = int(f_data['ntr'])
if t_ntr >= g.mn_tr and t_ntr <= g.mx_tr:
g.nbr_tr = t_ntr
# else no chg
elif f_data['ntr'].lower() == 'r':
g.nbr_tr = rng.integers(g.mn_tr, g.mx_tr)
if i_typ == 'gnarly':
... ...
Examples
I converted the images to JPEG at 67% compression to save on the pages bandwidth. Would have required many times more bytes with PNG images. As in the previous post.
Curve Parameters
Wheels: 12 wheels (shape(s): rhombus (r))
Symmetry: k_fold = 4, congruency = 2
Frequencies: [6, 18, 2, 10, -6, -10, 6, 10, 2, -6, 2, 18]
Widths: [1, 0.7024617958184918, 0.5373063556098061j, 0.504807330598898, 0.2555269738267228, 0.17173890848301623, 0.14489734026061388j, 0.125j, 0.125j, 0.125, 0.125j, 0.125]
Heights: [1, 0.549946135963451j, 0.5482816219270289j, 0.30769529402676415, 0.23455291057675048j, 0.157304722143189j, 0.125, 0.125j, 0.125, 0.125, 0.125, 0.125]
Image Type Parameters
Colouring: between adjacent datarows with overlap
Sections: 61 colour sections per datarow pairing
Drawing Parameters
Colour map: rainbow
BG colour map: turbo
Line width (if used): None
Transformations
Number of transforms: 7
Angles: [25.714285714285715, 77.14285714285714, 128.57142857142858, 180.0, 231.42857142857142, 282.85714285714283, 334.2857142857143]
Order: [3, 2, 6, 0, 1, 4, 5]
Base linear translation point: (0, 1.725) (0.875)
Makes me think of west coast indigenous art.
Curve Parameters
Wheels: 10 wheels (shape(s): tetracuspid (t))
Symmetry: k_fold = 8, congruency = 6
Frequencies: [14, 30, 38, -2, -10, -18, -26, -18, -26, 14]
Widths: [1, 0.6929355032592682, 0.6687992545983391, 0.5594322945124343, 0.3212345078031467, 0.3048750735230951, 0.24262540007665565, 0.1598007565465859j, 0.125, 0.125]
Heights: [1, 0.5242505147398607, 0.27143112363550687, 0.1420110019970596j, 0.125j, 0.125j, 0.125, 0.125, 0.125, 0.125]
Image Type Parameters
Colouring: between adjacent datarows with overlap
Sections: 62 colour sections per datarow pairing
Drawing Parameters
Colour map: bone
BG colour map: cividis
Line width (if used): None
Transformations
Number of transforms: 7
Angles: [25.714285714285715, 77.14285714285714, 128.57142857142858, 180.0, 231.42857142857142, 282.85714285714283, 334.2857142857143]
Order: [5, 4, 3, 6, 2, 1, 0]
Base linear translation point: (0, 1.837) (0.875)
Middle eastern geometric imagery?
The remainder all use multipliers which appears to skew things considerably.
Curve Parameters
Wheels: 10 wheels (shape(s): ellipse (e))
Symmetry: k_fold = 7, congruency = 4
Frequencies: [11, 25, 18, 25, -10, 11, -17, -24, 11, 32]
Widths: [1, 0.5617347737320201, 0.30584295813282814j, 0.20340383525799205, 0.17659189687002594, 0.125, 0.125, 0.125, 0.125j, 0.125]
Heights: [1, 0.6076107431523396, 0.39361256537243894, 0.3745299183357966, 0.3671978084731096, 0.2940575082869049, 0.16149169524701482, 0.14099465665227723, 0.125, 0.125]
Image Type Parameters
Colouring: between adjacent datarows with overlap
Sections: 24 colour sections per datarow pairing
Multipliers: [1.8, 1.6, 1.4, 1.4, 1.5, 1.0, 1.3, 1.3, 1.0]
Drawing Parameters
Colour map: inferno
BG colour map: Greys
Line width (if used): None
Transformations
Number of transforms: 6
Angles: [29.999999999999996, 90.0, 149.99999999999997, 210.0, 270.0, 330.0]
Order: [5, 0, 3, 4, 2, 1]
Base linear translation point: (0, 1.642) (0.625)
Curve Parameters
Wheels: 5 wheels (shape(s): tetracuspid (t))
Symmetry: k_fold = 2, congruency = 1
Frequencies: [1, 1, 3, -1, 3]
Widths: [1, 0.561044923904825, 0.3578867065151112, 0.3076901881741516j, 0.19414148029590605]
Heights: [1, 0.5466306349074822, 0.3888512095372273j, 0.3707025393355153, 0.288516972650256j]
Image Type Parameters
Colouring: between adjacent datarows with overlap
Sections: 44 colour sections per datarow pairing
Multipliers: [1.6, 1.4, 1.3, 1.1]
Drawing Parameters
Colour map: magma
BG colour map: bone
Line width (if used): None
Transformations
Number of transforms: 5
Angles: [36.0, 108.0, 180.0, 252.0, 324.0]
Order: [2, 4, 0, 1, 3]
Base linear translation point: (0, 0.605) (0.5)
Curve Parameters
Wheels: 4 wheels (shape(s): ['e', 'e', 'c', 's'])
Symmetry: k_fold = 3, congruency = 2
Frequencies: [-1, 11, -4, 5]
Widths: [1, 0.6115908489197277j, 0.5955385614160326, 0.4618608232563048]
Heights: [1, 0.6510049477927171, 0.4307331185113737, 0.28748770321252837]
Image Type Parameters
Colouring: between adjacent datarows with overlap
Sections: 32 colour sections per datarow pairing
Multipliers: [2.4, 1.0, 1.4]
Drawing Parameters
Colour map: gnuplot
BG colour map: cividis
Line width (if used): None
Transformations
Number of transforms: 5
Angles: [36.0, 108.0, 180.0, 252.0, 324.0]
Order: [1, 4, 3, 2, 0]
Base linear translation point: (0, 0.55) (0.375)
Curve Parameters
Wheels: 11 wheels (shape(s): ['c', 't', 't', 's', 'r', 'q', 't', 't', 'q', 'r', 'c'])
Symmetry: k_fold = 4, congruency = 2
Frequencies: [2, -14, -6, -6, 2, -2, -14, -6, 10, 14, 6]
Widths: [1, 0.6797152826352928j, 0.35898719517836136, 0.33371033936805855, 0.21239913716735612, 0.21050337599639382, 0.20482751917331649j, 0.1365750765854171, 0.1345293622071587j, 0.125, 0.125]
Heights: [1, 0.6062509331672192, 0.35752405062679454j, 0.2720942121783654, 0.22722771045899226j, 0.15866955863845744, 0.125, 0.125, 0.125j, 0.125, 0.125j]
Image Type Parameters
Colouring: between adjacent datarows with overlap
Sections: 63 colour sections per datarow pairing
Multipliers: [1.6, 1.6, 2.0, 2.2, 1.2, 1.1, 1.6, 1.4, 1.3, 1.4]
Drawing Parameters
Colour map: jet
BG colour map: bone
Line width (if used): None
Transformations
Number of transforms: 5
Angles: [36.0, 108.0, 180.0, 252.0, 324.0]
Order: [2, 1, 4, 0, 3]
Base linear translation point: (0, 0.88) (0.5)
But not the worst images I have ever seen or produced.
Refactor Route Code
Okay, decided to move the code for converting maplotlib figure to base64 into a function and call that from the routes. In the latest route, I was creating a huge array with all the transforms stored in it. Then plotting them in the desired order. That really is not necessary, I can simply generate the desired transform within the plotting loop at the point where it is needed. So, did just that.
That spirograph/mosaic_t
route now looks like this.
@app.route('/spirograph/mosaic_t', methods=['GET', 'POST'])
def sp_mosaic_t():
pg_ttl = "Mosaic Like Spirograph Image with 2D Transforms"
if request.method == 'GET':
f_data = {'pttl': pg_ttl, 'shapes': splt.shp_nm, 'cmaps': g.clr_opt, 'mxs': g.mx_s,
'mntr': g.mn_tr, 'mxtr': g.mx_tr
}
return render_template('get_curve.html', type='mosaic', f_data=f_data)
elif request.method == 'POST':
# print(f"POST[shape]: {request.form.get('shape')}, POST[shp_mlt]: {request.form.get('shp_mlt')}")
g.DEBUG = True
sal.proc_curve_form(request.form, 'mosaic')
t_xs, t_ys, t_shp = sal.init_curve()
r_xs, r_ys, m_xs, m_ys, m2_xs, m2_ys = sal.get_gnarly_dset(t_xs, t_ys, g.r_skp, g.drp_f, g.t_sy)
ax.clear()
# ax.patch.set_alpha(0.01)
g.rcm, g.cycle = sal.set_clr_map(ax, g.mx_s, g.rcm)
# get angles
lld_rot = sal.get_angles(g.nbr_tr)
lld_deg = [math.degrees(ang) for ang in lld_rot]
# get init point for linear translations
mnx41, mxx41, mny41, mxy41 = sal.get_plot_bnds(r_xs, r_ys, x_adj=0)
mdy = max(mxx41, mxy41)
y_mlt = rng.choice([.875, .75, .625, .5, .375])
trdy = mdy * y_mlt
# drop first data row?
if g.n_whl == 3:
src_st = 0
else:
src_st = 1
src = np.array([r_xs[src_st:], r_ys[src_st:]], dtype="float")
# get rand quadrant order
do_qd = list(range(g.nbr_tr))
do_qd2 = sal.get_rnd_qds(do_qd)
# only change multipliers the first time, not on later loops
sal.cnt_btw = 0
# Generate transformed datasets
for i in do_qd2:
a = np.array([[np.cos(lld_rot[i]), -np.sin(lld_rot[i])],
[np.sin(lld_rot[i]), np.cos(lld_rot[i])]])
dstx, dsty = sal.affine_transformation(src[0], src[1], a)
dx, dy = sal.rotate_pt(0, trdy, angle=lld_rot[i], cx=0, cy=0)
dstx = dstx + dx
dsty = dsty + dy
sal.btw_n_apart(ax, dstx, dsty, dx=g.bw_dx, ol=g.bw_o, r=g.bw_r, fix=None, mlt=g.bw_m, sect=g.bw_s)
ax.autoscale()
sal.cnt_btw += 1
ax.text(0.5, 0.01, "© koach (barkncats) 2022", ha="center", transform=ax.transAxes,
fontsize=10, alpha=.3, c=g.cycle[len(g.cycle)//2])
ax.autoscale()
bax2, _ = sal.set_bg(ax)
# convert image to base64 for embedding in html page
data = sal.fig_2_base64(fig)
c_data = sal.get_curve_dtl()
i_data = sal.get_image_dtl(pg_ttl)
p_data = {'bdx': g.bw_dx, 'bol': g.bw_o, 'bcs': g.bw_s,
'bmlt': g.bw_m, 'mlts': sal.c_mlts,
'ntr': g.nbr_tr, 'trang': lld_deg, 'trpt': f'(0, {round(trdy, 3)}) ({y_mlt})',
'trord': do_qd2
}
return render_template('disp_image.html', sp_img=data, type='mosaic', c_data=c_data, i_data = i_data, p_data=p_data)
Done
That’s it for this one I think. Plenty of babbling, lots of code, and a generous number of images.
Will tackle adding transforms to the basic and gnarly images next time. I will as well, quite likely move the transform generation code into a separate function. Expect that will be repeated in the next set of image routes. Similarly for the get angles and initial linear transformation point. But for now…
Until then, play hard, code harder.