I am liking this latest image type—cycling line widths using fill_between()—enough that I think I will continue playing with it. So, I thought I’d look at applying 2D transforms to see what we can get.
2D Transforms with Cycling Line Width Images
I already have that affine_transformation()
function in the app library module and some experience generating transforms. So, this will hopefully be a fairly quick and painless process.
New/Refactored Functions
I thought I’d start by copying one of the previous 2D transform route functions and refactoring it to work for this image type. If it works, I will then go on to refactor the rest of the app to provide access to and support for the route. I am hoping the existing global variables will get the job done, without needing to add any new ones.
Well, this will in fact require some considerable refactoring and/or a new function or two. The function I wrote for this style of cycling line width image, plotted the data it generated and did not return it to the caller. To generate the transforms, I need the initial image data to which I can apply the desired number of transforms.
I don’t, at this point, really want to refactor the function and the route from the last post. So, I am going to add another parameter to the function definition. It will default to false, but if set to true, the function will not plot anything, but will instead return the 2 new sets of y
values to the caller. A bit of a pain to have two different sets of return values depending on a function argument, but such is life when one adds functionality on the fly. I.E. no design/plan for the app was developed before I started coding.
I am not going to show the refactored function, as it is a simple and straightforward modification.
Trouble! As I went on, I realized I’d have to add all the plotting code in the cycle_lw_btw()
function to the route’s function. Not really a great idea. So, I am going to refactor the function afterall. I will break it into two separate functions. One to generate the cycling line width data and one to plot a given set of cycling line width data. For now I will leave the original function so I don’t have to immediately refactor the earlier route. So two new functions with different names: clw_btw_make()
and clw_btw_plot()
.
Mostly just copy & paste, with a touch of refactoring.
def clw_btw_make(rxs, rys, u_pcnt=False, n_cyc=8, dppc=12, mx_grw=.25):
# rxs, rys 1D arrays
n_dots = len(rxs)
f_cyc = n_dots // n_cyc
h_cyc = f_cyc // 2
# need return values even if not used to generate image
rtn_vals = {
'h_cyc': h_cyc,
'min_i': None,
'mx_sz': None,
'c_inc': None,
'nbr_max': None,
}
ys_cyc = [[], []]
if u_pcnt:
min_i = 1
c_inc = mx_grw / h_cyc
mlt_1 = min_i
mx_sz = mlt_1 + mx_grw
rtn_vals['min_i'] = min_i
else:
mx_sz = rys.max() * mx_grw
c_inc = mx_sz / h_cyc
f_inc = 0
rtn_vals['mx_sz'] = mx_sz
rtn_vals['c_inc'] = c_inc
i_dir = +1
nbr_max = 0
for i in range(n_dots):
if u_pcnt:
mlt_1 += c_inc * i_dir
if mlt_1 > mx_sz or mlt_1 < min_i:
i_dir *= -1
if mlt_1 > mx_sz:
nbr_max += 1
mlt_1 += (c_inc * i_dir)
ys_cyc[0].append(rys[i] * mlt_1)
ys_cyc[1].append(rys[i] * (1 / mlt_1))
else:
f_inc += c_inc * i_dir
if f_inc > mx_sz or f_inc < 0:
if f_inc > mx_sz:
nbr_max += 1
i_dir *= -1
f_inc += c_inc * i_dir
ys_cyc[0].append(rys[i] + f_inc)
ys_cyc[1].append(rys[i] - f_inc)
rtn_vals['nbr_max'] = nbr_max
return ys_cyc[0], ys_cyc[1], rtn_vals
def clw_btw_plot(axt, rxs, rys1, rys2, dppc=12):
# rxs, rys* 1D arrays
n_dots = len(rxs)
c_len = len(g.cycle)
c_ndx = rng.integers(0, c_len-2)
c_jmp = c_len // 40
c_jmp = int(c_len * 0.15)
if c_jmp % 2 == 0:
c_jmp += 1
l_alph = 1
axt.autoscale()
pp_clr = int(dppc * (n_dots // 512))
print(f"\tclw_btw_plot(..., dppc={dppc} -> n_dots = {n_dots} & pp_clr = {pp_clr}")
for i in range(0, n_dots, pp_clr):
c_end = i + pp_clr
if i == 0:
axt.fill_between(rxs[i:c_end], rys1[i:c_end], rys2[i:c_end], alpha=l_alph, color=g.cycle[c_ndx])
else:
axt.fill_between(rxs[i-1:c_end], rys1[i-1:c_end], rys2[i-1:c_end], alpha=l_alph, color=g.cycle[c_ndx])
c_ndx = (c_ndx + c_jmp) % c_len
axt.autoscale()
if c_end < n_dots:
i += pp_clr - 1
axt.fill_between(rxs[i:], rys1[i:], rys2[i:], alpha=l_alph, color=g.cycle[c_ndx])
axt.autoscale()
return pp_clr
New Route
Again, mostly copy & paste, with a touch of refactoring.
But I did run into a bug; well, an error on my part. Because I originally wrote get_transform_base()
and get_transform_data()
to work with gnarly style images, these two functions expect an array of arrays (or list of lists) for the x
and y
data arguments. Took me a bit of time to sort that out and get things working. After that things went rather smoothly.
@app.route('/spirograph/basic_clw_btw_t', methods=['GET', 'POST'])
def sp_clw_btw_t():
pg_ttl = "Cycling Line Width Spirograph Image with 2D Transforms"
if request.method == 'GET':
f_data = {'pttl': pg_ttl, 'shapes': splt.shp_nm, 'cxsts': g.n_whl is not None,
'cmaps': g.clr_opt, 'mntr': g.mn_tr, 'mxtr': g.mx_tr, 'subtyp': 'clw_btw'
}
return render_template('get_curve.html', type='basic', f_data=f_data)
elif request.method == 'POST':
g.t_pts = 2048
g.rds = np.linspace(0, 2*np.pi, g.t_pts)
sal.proc_curve_form(request.form, 'basic')
t_xs, t_ys, t_shp = sal.init_curve()
ax.cla()
g.rcm, g.cycle = sal.set_clr_map(ax, g.lc_frq, g.rcm)
ax.autoscale()
clwy_1, clwy_2, i_param = sal.clw_btw_make(t_xs[-1], t_ys[-1], u_pcnt=g.use_pct, n_cyc=g.n_lwcyc, dppc=g.dpp_clr)
# get angles and such
lld_rot, lld_deg, trdy, y_mlt = sal.get_transform_base([t_xs[-1]], [clwy_1])
# 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:
dstx1, dsty1 = sal.get_transform_data(t_xs[-1], clwy_1, lld_rot[i], trdy)
dstx2, dsty2 = sal.get_transform_data(t_xs[-1], clwy_2, lld_rot[i], trdy)
pp_clr = sal.clw_btw_plot(ax, dstx1, dsty1, dsty2, dppc=g.dpp_clr)
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 = {'mx_mlt': i_param['mx_sz'], 'mn_mlt': i_param['min_i'], 'm_inc': i_param['c_inc'],
'hf_frq': i_param['h_cyc'], 'n_max': i_param['nbr_max'], 'pp_clr': pp_clr,
'u_pct': g.use_pct, 'n_lwc': g.n_lwcyc,
'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: 8 wheels (shape(s): ellipse (e))
Symmetry: k_fold = 7, congruency = 5
Frequencies: [12, -2, -9, -23, -9, -16, -2, -23]
Widths: [1, 0.61340685429815, 0.4704852896897198j, 0.36490132705418155j, 0.3184763890009844, 0.16227766668617918, 0.125, 0.125]
Heights: [1, 0.5786476947298501, 0.5742218653462878j, 0.5128714750763494j, 0.39665236293192846j, 0.25710801322339905, 0.1338623541828563, 0.125]
Drawing Parameters
Colour map: turbo
BG colour map: rainbow
Line width (if used): 2
Transformations
Number of transforms: 5
Angles: [36.0, 108.0, 180.0, 252.0, 324.0]
Base linear translation point: (0, 2.136) (0.75)
Order: [4, 3, 1, 2, 0]
Cycling Line Width
Use percentage increment: False
Number cycles: 8
Half cycle points: 128
Multiplier increment: 0.005084479531953572
Number of maximums: 8
Datapoints per colour section: 48
Not bad!
Curve Parameters
Wheels: 3 wheels (shape(s): equilateral triangle (q))
Symmetry: k_fold = 2, congruency = 1
Frequencies: [-1, 1, 5]
Widths: [1, 0.6936470360208918, 0.5093037606701534]
Heights: [1, 0.5409909237943519j, 0.48655539976755313]
Drawing Parameters
Colour map: winter
BG colour map: bone
Line width (if used): 4
Transformations
Number of transforms: 4
Angles: [45.0, 135.0, 225.0, 315.0]
Base linear translation point: (0, 0.826) (0.75)
Order: [0, 3, 2, 1]
Cycling Line Width
Use percentage increment: False
Number cycles: 8
Half cycle points: 128
Multiplier increment: 0.0018575934496081262
Number of maximums: 8
Datapoints per colour section: 48
Curve Parameters
Wheels: 8 wheels (shape(s): rhombus (r))
Symmetry: k_fold = 4, congruency = 3
Frequencies: [-1, 7, -1, -13, 19, 19, 11, 11]
Widths: [1, 0.673910461407562j, 0.4540126503471738, 0.367398104948511j, 0.23266584642107202, 0.16895498351999938j, 0.125, 0.125]
Heights: [1, 0.7161225970165781, 0.6204515259999254j, 0.42579256108056995, 0.3176138434293651j, 0.16734833935138504j, 0.13824074330930122j, 0.125j]
Drawing Parameters
Colour map: cubehelix
BG colour map: Greys
Line width (if used): 7
Transformations
Number of transforms: 5
Angles: [36.0, 108.0, 180.0, 252.0, 324.0]
Base linear translation point: (0, 1.377) (0.875)
Order: [3, 2, 4, 1, 0]
Cycling Line Width
Use percentage increment: False
Number cycles: 8
Half cycle points: 128
Multiplier increment: 0.0016208371736539007
Number of maximums: 8
Datapoints per colour section: 48
I do like this image variation!
Bug?
I kept seeing unexpectedly thin lines in the images. Then, for an image with 3 transforms, one of the transforms had no cycling line width at all. What the h…? Here’s an example image with 6 transforms.
Curve Parameters
Wheels: 11 wheels (shape(s): square (s))
Symmetry: k_fold = 5, congruency = 2
Frequencies: [-3, 22, -8, -18, 2, -13, -13, 17, 22, 22, 22]
Widths: [1, 0.6548539846253743j, 0.3777398702102073, 0.28852912358429295j, 0.18948559280111515j, 0.125j, 0.125j, 0.125j, 0.125j, 0.125, 0.125j]
Heights: [1, 0.7309820139844311j, 0.4351271752180745, 0.2782390032690119j, 0.15084091881220765, 0.14160123869674454, 0.125j, 0.125, 0.125, 0.125j, 0.125]
Drawing Parameters
Colour map: viridis
BG colour map: bone
Line width (if used): 7
Transformations
Number of transforms: 6
Angles: [29.999999999999996, 90.0, 149.99999999999997, 210.0, 270.0, 330.0]
Base linear translation point: (0, 1.609) (0.875)
Order: [3, 0, 4, 5, 1, 2]
Cycling Line Width
Use percentage increment: False
Number cycles: 8
Half cycle points: 128
Multiplier increment: 0.0032829983886528058
Number of maximums: 8
Datapoints per colour section: 48
And, there are 2 transforms without any cycling line width. ????
So I added some print statements. And, for a rotational transform at 90° or 270°, the two y
datasets returned by get_transform_data()
were the same. Here’s an example.
Curve Parameters
Wheels: 3 wheels (shape(s): ellipse (e))
Symmetry: k_fold = 3, congruency = 1
Frequencies: [4, 10, 7]
Widths: [1, 0.5827070566094672, 0.44746593095019244]
Heights: [1, 0.591293217062774, 0.30797800190853897j]
Drawing Parameters
Colour map: magma
BG colour map: plasma
Line width (if used): 3
Transformations
Number of transforms: 8
Angles: [45.0, 90.0, 135.0, 180.0, 225.0, 270.0, 315.0, 360.0]
Base linear translation point: (0, 1.009) (0.75)
Order: [6, 1, 0, 7, 3, 5, 4, 2]
Cycling Line Width
Use percentage increment: False
Number cycles: 8
Half cycle points: 128
Multiplier increment: 0.0021257311897505274
Number of maximums: 8
Datapoints per colour section: 48
== and the output of those debug print statements I mentioned
sum(clwy_1[1:10]) - sum(clwy_2[1:10]): 0.2295789684930567
do_qd: 6, angle: 315.0, trdy: 1.0094823483898667, sum(dsty1[1:10]) - sum(dsty2[1:10]): 0.16233684543925375
do_qd: 1, angle: 90.0, trdy: 1.0094823483898667, sum(dsty1[1:10]) - sum(dsty2[1:10]): 0.0
do_qd: 0, angle: 45.0, trdy: 1.0094823483898667, sum(dsty1[1:10]) - sum(dsty2[1:10]): 0.16233684543925264
do_qd: 7, angle: 360.0, trdy: 1.0094823483898667, sum(dsty1[1:10]) - sum(dsty2[1:10]): 0.22957896849305826
do_qd: 3, angle: 180.0, trdy: 1.0094823483898667, sum(dsty1[1:10]) - sum(dsty2[1:10]): -0.2295789684930547
do_qd: 5, angle: 270.0, trdy: 1.0094823483898667, sum(dsty1[1:10]) - sum(dsty2[1:10]): 0.0
do_qd: 4, angle: 225.0, trdy: 1.0094823483898667, sum(dsty1[1:10]) - sum(dsty2[1:10]): -0.1623368454392491
do_qd: 2, angle: 135.0, trdy: 1.0094823483898667, sum(dsty1[1:10]) - sum(dsty2[1:10]): -0.16233684543925286
Similar situation to the one above it.
After some thinking, I realized that the cosine of 90° and 270° is 0
. I figured, there might also a similar issue for 0° and 180° where the sine is 0
. So for those cases I will change the angle for the rotational transform.
My first thought was to use one of the other angles in the list. But that would just result in the given transform being plotted twice in the same location and potentially empty space(s) in the final image.
Also, I didn’t want to change the angle used to determine the linear translation/transform. So, I refactored get_transform_data()
to take two angles. One for the rotational transform and one for the linear one. Won’t bother showing that, as it is pretty straightforward. And, in my route I modified the section where the calls to get_transform_data()
are made as follows. That 1.2
multiplier is just a guess. If it is too small the cosine or sine will still be too close to 0
to generate a decent maximum line width. If too large, the symmetry of the final image could be significantly compromised.
... ...
# Generate transformed datasets
for i in do_qd2:
if lld_deg[i] in [0.0, 90.0, 180.0, 270.0]:
tmp_rot = 1.2 * lld_rot[i]
dstx1, dsty1 = sal.get_transform_data(t_xs[-1], clwy_1, tmp_rot, lld_rot[i], trdy)
dstx2, dsty2 = sal.get_transform_data(t_xs[-1], clwy_2, tmp_rot, lld_rot[i], trdy)
else:
dstx1, dsty1 = sal.get_transform_data(t_xs[-1], clwy_1, lld_rot[i], lld_rot[i], trdy)
dstx2, dsty2 = sal.get_transform_data(t_xs[-1], clwy_2, lld_rot[i], lld_rot[i], trdy)
... ...
And, that seems to get the job done.
Curve Parameters
Wheels: 8 wheels (shape(s): circle (c))
Symmetry: k_fold = 7, congruency = 5
Frequencies: [-2, -9, -2, 12, 12, -23, -9, 5]
Widths: [1, 0.6830826663611991j, 0.44957079238986053j, 0.27924678442078166j, 0.2177164807434988, 0.13604463796930064, 0.125, 0.125]
Heights: [1, 0.6016346830352731j, 0.4611643062520087, 0.2776867678572044, 0.17998130272117854j, 0.1530489348145748, 0.125, 0.125]
Drawing Parameters
Colour map: RdPu
BG colour map: Greys
Line width (if used): 1
Transformations
Number of transforms: 6
Angles: [29.999999999999996, 90.0, 149.99999999999997, 210.0, 270.0, 330.0]
Base linear translation point: (0, 1.008) (0.625)
Order: [2, 1, 3, 5, 4, 0]
Cycling Line Width
Use percentage increment: False
Number cycles: 8
Half cycle points: 128
Multiplier increment: 0.002876239835480084
Number of maximums: 8
Datapoints per colour section: 48
sum(clwy_1[1:10]) - sum(clwy_2[1:10]): 0.310633902231849
do_qd: 2, angle: 150.0, trdy: 1.0077281626198644, sum(dsty1[1:10]) - sum(dsty2[1:10]): -0.2690168506094729
do_qd: 1, angle: 90.0 ==> 108.0, trdy: 1.0077281626198644, sum(dsty1[1:10]) - sum(dsty2[1:10]): -0.09599115481865184
do_qd: 3, angle: 210.0, trdy: 1.0077281626198644, sum(dsty1[1:10]) - sum(dsty2[1:10]): -0.26901685060947145
do_qd: 5, angle: 330.0, trdy: 1.0077281626198644, sum(dsty1[1:10]) - sum(dsty2[1:10]): 0.2690168506094729
do_qd: 4, angle: 270.0 ==> 324.0, trdy: 1.0077281626198644, sum(dsty1[1:10]) - sum(dsty2[1:10]): 0.251308105934573
do_qd: 0, angle: 30.0, trdy: 1.0077281626198644, sum(dsty1[1:10]) - sum(dsty2[1:10]): 0.26901685060947145
Done
Well, that was a bit of fun. But, I think that will be it for this post.
I did modify, as appropriate, all the app pages to provide access to this image variation. And, I did update gcloud (gcloud app deploy
). Don’t think I need to include any of that in the post as the process has been covered in previous posts.
I hope you get the opportunity to have as much fun coding as I did with this image variation. Whether plain or with transforms.