As mentioned last time, I am going to look at using 2D transforms with the basic and gnarly image types. Not sure what it will do for the basic image type, but likely worth a look. Expect using transforms on the gnarly image will generate some additional interest as it did for the mosaic style images.
Bug
I have corrected this bug I don’t know how many times. At least I think I have. The code should run every time btw_n_apart()
gets called. But for some reason it keeps getting shifted into the if
block above. And, only runs once. Future calls then do not generate the proper second data set for the colouring between datasets. Don’t know if it’s me periodically reverting something or some autoformatting issue in the editor.
The malfunctioning code looked like this.
if not g.u_again:
if cnt_btw == 0 or len(c_mlts) == 0:
if mlt:
c_mlts, i_data = incr_data(rys, mlt=mlt)
else:
c_mlts = [1.0 for _ in range(len(rys))]
i_data = rys
c_ndx = (c_ndx + c_jmp) % c_len
t_c_ndx = c_ndx
cnt_btw += 1
c_ndx = t_c_ndx
c_mlts, i_data = incr_data(rys, mlt=mlt, nw_mlt=nw_mlt)
It should look like the following.
if not g.u_again:
if cnt_btw == 0 or len(c_mlts) == 0:
if mlt:
c_mlts, i_data = incr_data(rys, mlt=mlt)
else:
c_mlts = [1.0 for _ in range(len(rys))]
i_data = rys
c_ndx = (c_ndx + c_jmp) % c_len
t_c_ndx = c_ndx
cnt_btw += 1
c_ndx = t_c_ndx
c_mlts, i_data = incr_data(rys, mlt=mlt, nw_mlt=nw_mlt)
I also decided to refactor incr_data()
and consequently btw_n_apart()
. I was beginning to keep track of or modify cnt_btw
in too many places. Thought I should change that. They now look as follows.
def btw_n_apart(axt, rxs, rys, dx=1, ol=True, r=False, fix=None, mlt=False, sect=1):
global c_mlts, i_data, t_c_ndx, cnt_btw
# between 0,0+dx ; 1,1+dx; etc
c_ndx = 0
c_len = len(g.cycle)
c_ndx = rng.integers(0, c_len-2)
c_jmp = c_len // 20
if c_jmp % 2 == 0:
c_jmp += 1
if r:
n_fs = len(rxs) - dx - 1
else:
n_fs = len(rxs) - dx
if dx == 1 and not ol:
if r:
b_rng = range(n_fs, 0, -2)
else:
b_rng = range(1, n_fs, 2)
else:
if r:
b_rng = range(n_fs, 0, -1)
else:
b_rng = range(1, n_fs)
nw_mlt = cnt_btw == 0 or len(c_mlts) == 0
if not g.u_again:
if mlt:
_, i_data = incr_data(rys, mlt=mlt, nw_mlt=nw_mlt)
else:
c_mlts = [1.0 for _ in range(len(rys))]
i_data = rys
c_ndx = (c_ndx + c_jmp) % c_len
t_c_ndx = c_ndx
cnt_btw += 1
nw_mlt = cnt_btw == 0 or len(c_mlts) == 0
c_ndx = t_c_ndx
_, i_data = incr_data(rys, mlt=mlt, nw_mlt=nw_mlt)
s_sz = len(rxs[0]) // sect
for i in b_rng:
if i == i+dx and c_mlts[i] == 1.0:
continue
if fix is not None:
axt.fill_between(rxs[fix], rys[i], i_data[i+dx], alpha=1, color=g.cycle[c_ndx])
c_ndx = (c_ndx + c_jmp) % c_len
else:
if sect == 1:
axt.fill_between(rxs[i], rys[i], i_data[i+dx], alpha=1, color=g.cycle[c_ndx])
c_ndx = (c_ndx + c_jmp) % c_len
else:
for j in range(sect-1):
s_st = s_sz * j
s_nd = s_sz * (j + 1)
axt.fill_between(rxs[i][s_st:s_nd], rys[i][s_st:s_nd], i_data[i+dx][s_st:s_nd], alpha=1, color=g.cycle[c_ndx])
c_ndx = (c_ndx + c_jmp) % c_len
axt.fill_between(rxs[i][s_nd:], rys[i][s_nd:], i_data[i+dx][s_nd:], alpha=1, color=g.cycle[c_ndx])
def incr_data(a_rows, mlt=False, nw_mlt=False):
# multiply data rows by random value between 1 and 2 inclusive
# only going to use a few values, with prob of 1 slightly higher than rest
global cnt_btw, c_mlts
m_rows = []
n_rw = len(a_rows)
r_hlf = n_rw // 2
if nw_mlt:
# if new multipliers requested
if mlt:
mlts_f = rng.choice(p_mlt2, r_hlf)
mlts_b = rng.choice(p_mlt1, n_rw - r_hlf)
c_mlts = np.concatenate((mlts_f, mlts_b))
else:
c_mlts = [1.0 for _ in range(n_rw)]
# else use the existing ones
for i in range(n_rw):
c_mlt = c_mlts[i]
m_rows.append(a_rows[i] * c_mlt)
return c_mlts, m_rows
Gnarly Image Transforms
Back to the matter at hand. I will start with the gnarly images. I expect whatever I end up coding/refactoring will carry over directly to the basic image case.
Refactor
I wasn’t going to do so, but in the end I decided to move some of the repetitive bits into their own functions. So, rather than show you the before and after, I am going to show you the two new functions. Both functions were added to the library module, sp_app_lib.py
. Refactoring the mosaic transforms route should be easy enough, so it’s not included in the post.
... ...
def get_transform_base(rxs, rys):
# get angles
lld_rot = 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 = get_plot_bnds(rxs, rys, x_adj=0)
mdy = max(mxx41, mxy41)
y_mlt = rng.choice([.875, .75, .625, .5, .375])
trdy = mdy * y_mlt
return lld_rot, lld_deg, trdy, y_mlt
def get_transform_data(rxs, rys, ang, trdy):
a = np.array([[np.cos(ang), -np.sin(ang)],
[np.sin(ang), np.cos(ang)]])
dstx, dsty = affine_transformation(rxs, rys, a)
dx, dy = rotate_pt(0, trdy, angle=ang, cx=0, cy=0)
dstx = dstx + dx
dsty = dsty + dy
return dstx, dsty
... ...
New Route
I am going to start by coding the new route, /spirograph/gnarly_t
. I will reuse as much existing code as possible. Refactoring any duplicate code in the new route and /spirograph/mosaic_t
. With any luck, no really new code will be needed.
That was in fact quite painless. Copied the original gnarly route. Refactored title and such. Added bits and pieces related to the transfroms from sp_mosaic_t()
and bingo. Though it is pretty slow generating the image.
@app.route('/spirograph/gnarly_t', methods=['GET', 'POST'])
def sp_gnarly_t():
pg_ttl = "Gnarly Spirograph Image with 2D Transforms"
if request.method == 'GET':
f_data = {'pttl': pg_ttl, 'shapes': splt.shp_nm, 'cmaps': g.clr_opt,
'mntr': g.mn_tr, 'mxtr': g.mx_tr
}
return render_template('get_curve.html', type='gnarly', 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, 'gnarly')
t_xs, t_ys, t_shp = sal.init_curve()
r_xs, r_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.lc_frq, g.rcm)
# get angles and such
lld_rot, lld_deg, trdy, y_mlt = sal.get_transform_base(r_xs, r_ys)
# get rand quadrant order
do_qd = list(range(g.nbr_tr))
do_qd2 = sal.get_rnd_qds(do_qd)
# Generate transformed datasets
for i in do_qd2:
dstx, dsty = sal.get_transform_data(r_xs, r_ys, lld_rot[i], trdy)
ax.plot(dstx, dsty, lw=g.ln_w, alpha=g.alph, zorder=g.pz_ord)
ax.autoscale()
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)
d_end = "top" if g.drp_f else "bottom"
c_data = sal.get_curve_dtl()
i_data = sal.get_image_dtl(pg_ttl)
p_data = {
'd_end': d_end, 'd_nbr': g.r_skp,
'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='gnarly', c_data=c_data, i_data=i_data, p_data=p_data)
This route is much slower than the others at producing an image. Not sure why. But, given I am doing much the same here as I am with the mosaic images, I expect it has more to do with matplotlib than my code. Though the Numpy operations are likely fairly demanding from a resource point of view.
When I put the code into production on Google cloud, this route had more problems. Still very slow. But every so often Google App Engine would generate an error and tell me to wait 30 seconds and try again. I expect the code is taking too long to complete (safety valve) or surpassing some other resource restriction. Consequently the engine stops processing the current request and resets.
Examples
Curve Parameters
Wheels: 6 wheels (shape(s): ellipse (e))
Symmetry: k_fold = 6, congruency = 2
Frequencies: [-4, 26, 26, -10, 20, -22]
Widths: [1, 0.585398292599137, 0.40456082845342, 0.32837345296429793j, 0.24474847367573913, 0.17312780508562073j]
Heights: [1, 0.6883351014520809, 0.5468725087278253j, 0.4397271991308957j, 0.3361310521506485j, 0.33389191435322474]
Image Type Parameters
Drop data rows: 1 datarow
From: dropped from the 'top' of the curve dataset
Drawing Parameters
Colour map: turbo
BG colour map: Greys
Line width (if used): 3
Transformations
Number of transforms: 6
Angles: [29.999999999999996, 90.0, 149.99999999999997, 210.0, 270.0, 330.0]
Order: [5, 2, 4, 3, 1, 0]
Base linear translation point: (0, 1.194) (0.5)
Curve Parameters
Wheels: 4 wheels (shape(s): equilateral triangle (q))
Symmetry: k_fold = 2, congruency = 1
Frequencies: [3, 7, 5, 7]
Widths: [1, 0.5615310720304078, 0.2929231230865153j, 0.1532381553031087j]
Heights: [1, 0.5396588769681948j, 0.5300024753641639j, 0.30874204909562275j]
Image Type Parameters
Drop data rows: 1 datarow
From: dropped from the 'top' of the curve dataset
Drawing Parameters
Colour map: plasma
BG colour map: magma
Line width (if used): 3
Transformations
Number of transforms: 5
Angles: [36.0, 108.0, 180.0, 252.0, 324.0]
Order: [1, 3, 4, 0, 2]
Base linear translation point: (0, 0.376) (0.375)
Curve Parameters
Wheels: 5 wheels (shape(s): circle (c))
Symmetry: k_fold = 2, congruency = 1
Frequencies: [-1, 5, -1, 5, 5]
Widths: [1, 0.6613879650713143, 0.561885389505404, 0.3918693298474105j, 0.29481877528508743]
Heights: [1, 0.6877778639704673j, 0.3894099039436909, 0.20619387731560404, 0.19292089024513723]
Image Type Parameters
Drop data rows: 1 datarow
From: dropped from the 'top' of the curve dataset
Drawing Parameters
Colour map: viridis
BG colour map: Greys
Line width (if used): 8
Transformations
Number of transforms: 3
Angles: [29.999999999999996, 149.99999999999997, 270.0]
Order: [0, 1, 2]
Base linear translation point: (0, 0.909) (0.625)
Curve Parameters
Wheels: 10 wheels (shape(s): tetracuspid (t))
Symmetry: k_fold = 2, congruency = 1
Frequencies: [-1, -7, -5, -7, 9, -1, -1, 5, 7, 3]
Widths: [1, 0.721895311630224j, 0.5915976836746908, 0.310120723976356, 0.2778588634471638, 0.20841542658112058, 0.1301579320539617j, 0.125, 0.125, 0.125]
Heights: [1, 0.6755032300724298, 0.5442945816944038, 0.44844213045951997, 0.3100814075221635, 0.16794712566017175, 0.125, 0.125j, 0.125, 0.125]
Image Type Parameters
Drop data rows: 1 datarow
From: dropped from the 'top' of the curve dataset
Drawing Parameters
Colour map: magma
BG colour map: magma
Line width (if used): 2
Transformations
Number of transforms: 3
Angles: [29.999999999999996, 149.99999999999997, 270.0]
Order: [1, 0, 2]
Base linear translation point: (0, 0.904) (0.5)
Basic Spirograph Transforms
New Route
Basically repeated what I did above for the route /spirograph/basic_t
. Minor bit of refactoring with respect to available curve data variables. For now I have not bothered to move the really repetitive bits into their own functions.
@app.route('/spirograph/basic_t', methods=['GET', 'POST'])
def sp_basic_t():
pg_ttl = "Basic Spirograph Image with 2D Transforms"
if request.method == 'GET':
f_data = {'pttl': pg_ttl, 'shapes': splt.shp_nm, 'cmaps': g.clr_opt,
'mntr': g.mn_tr, 'mxtr': g.mx_tr
}
return render_template('get_curve.html', type='basic', f_data=f_data)
elif request.method == 'POST':
# app.logger.info(f"POST[shape]: {request.form.get('shape')}, POST[shp_mlt]: {request.form.get('shp_mlt')}")
# print(f"POST[shape]: {request.form.get('shape')}, POST[shp_mlt]: {request.form.get('shp_mlt')}")
sal.proc_curve_form(request.form, 'basic')
t_xs, t_ys, t_shp = sal.init_curve()
ax.clear()
# ax.patch.set_alpha(0.01)
g.rcm, g.cycle = sal.set_clr_map(ax, g.lc_frq, g.rcm)
# get angles and such
lld_rot, lld_deg, trdy, y_mlt = sal.get_transform_base(t_xs, t_ys)
# get rand quadrant order
do_qd = list(range(g.nbr_tr))
do_qd2 = sal.get_rnd_qds(do_qd)
# Generate transformed datasets
for i in do_qd2:
dstx, dsty = sal.get_transform_data(t_xs, t_ys, lld_rot[i], trdy)
ax.plot(dstx[-1], dsty[-1], lw=g.ln_w, alpha=1)
ax.autoscale()
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 = {'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='basic', c_data=c_data, i_data=i_data, p_data=p_data)
Examples
Curve Parameters
Wheels: 7 wheels (shape(s): tetracuspid (t))
Symmetry: k_fold = 2, congruency = 1
Frequencies: [3, 5, 5, 3, 5, -7, 3]
Widths: [1, 0.5066047593394428, 0.3470390727382054, 0.20839418705227622, 0.13185322148862264, 0.125, 0.125]
Heights: [1, 0.5309519557205008j, 0.3696601849776622, 0.2767212513533699j, 0.20717680378545328j, 0.1401937286385749j, 0.125]
Drawing Parameters
Colour map: hot
BG colour map: YlOrRd
Line width (if used): 6
Transformations
Number of transforms: 5
Angles: [36.0, 108.0, 180.0, 252.0, 324.0]
Order: [1, 3, 2, 4, 0]
Base linear translation point: (0, 0.458) (0.375)
Curve Parameters
Wheels: 12 wheels (shape(s): circle (c))
Symmetry: k_fold = 3, congruency = 1
Frequencies: [-2, 10, 7, 13, -5, -5, 1, 10, 7, -8, -2, 10]
Widths: [1, 0.5505461490785912, 0.4075101704769578j, 0.3637199617567362j, 0.28199291996722764j, 0.16910511223006836, 0.1615143241039201, 0.125j, 0.125, 0.125, 0.125, 0.125]
Heights: [1, 0.5635565664960167, 0.44300320225242357, 0.3394684842989719, 0.3007103196449747, 0.25912386857968395j, 0.15366790628340057j, 0.1278398208792956j, 0.125, 0.125, 0.125, 0.125j]
Drawing Parameters
Colour map: gist_earth
BG colour map: cividis
Line width (if used): 5
Transformations
Number of transforms: 6
Angles: [29.999999999999996, 90.0, 149.99999999999997, 210.0, 270.0, 330.0]
Order: [2, 1, 3, 0, 5, 4]
Base linear translation point: (0, 1.335) (0.75)
Curve Parameters
Wheels: 6 wheels (shape(s): square (s))
Symmetry: k_fold = 5, congruency = 4
Frequencies: [-1, 9, 9, -11, 19, 24]
Widths: [1, 0.6743617994418798j, 0.5308580908391798, 0.35950437842250804j, 0.21510661297314643j, 0.14782096011581788]
Heights: [1, 0.6347747861660389, 0.5197903000456291j, 0.3960090088200574j, 0.36970219868766924j, 0.35058217332304425j]
Drawing Parameters
Colour map: GnBu
BG colour map: bone
Line width (if used): 3
Transformations
Number of transforms: 6
Angles: [29.999999999999996, 90.0, 149.99999999999997, 210.0, 270.0, 330.0]
Order: [3, 2, 1, 4, 0, 5]
Base linear translation point: (0, 1.431) (0.875)
Done
I think that’s it for another one. Must say I do like the images. Especially the gnarly and mosaic ones. Though the basic spirograph with transforms has potential. However, I think in that case I should likely be using the same colour to draw each transform. Differing colours seem a touch distracting, probably ruining the look and feel.
I am also not happy with the interface for using the same curve for subsequent images. Once I have selected my state variables (with some of them being random) on the form and generated the first image, I don’t normally go back to the form. I simply do a reload in the browser window to get another image. Hopefully a different image.
Sometimes I want to change one of the parameters (e.g. use or do not use multipliers for the mosaic images). But then I get the same curve over and over. I’d like to be able for instance to leave the curve shape random for repeated images and just change the multiplier state. Can’t do that right now. Something for next time.
I’d also like to add one or more the cycling line width variations to the app.
Be happy and keep those fingers tapping.