At the end of the last post wasn’t too sure where I’d be going next. But, ended up deciding that coloured backgrounds would like be something people might like as an option. So, that’s where we will be going next—after a bit of an aside.
And I apologize for the size of the images below. But, I wanted to use true colour so that you could really see what the background looks like. And, I am saving them in PNG format. If could have gotten away with 256 colours or somesuch would have been much smaller. But that would distort the backgrounds.
Same Curve?
I was not happy with the way I was handling some of the image parameters when the same curve data was being used again. The question being what is part of the curve data and what is not? In my personal app, because I wanted to be able to save exactly what I had just seen, pretty much every image related value was kept unchanged.
For this app, that is not really necessary. There is currently no way to save an image to a file. So, I decided to be a touch more liberal. Same curve means only those values needed to actually generate the underlying curve, not the image. So, for example, I no longer keep the same multipliers when the same curve is being reused. Therefore, refreshing the display page or changing the related field on the form will cause the multipliers to change. As said, for this app that makes more sense to me.
I won’t bother showing the code changes. I am sure you will be able to figure that out.
I am still not entirely happy with the current approach, but can’t seem to come up with a better solution. We will see what a real user has to say.
Background Colour
I hadn’t been planning to add a background colour to the images even though I had done so in my personal command line app. But, it does add another dimension of artistic inclination to the images. So, I am going to try and implement that in this web app. With, of course, another form field to control the behaviour.
In the command line app, I used two different axes
to generate the images. One specifically for the spirograph and the other for the background. I won’t go into the reasons as they don’t apply here. I did try doing it the same way, but was having issues. So went to drawing the spirograph and background on the same axes
.
In another change, I had a few different variations for the background image. A solid colour, a gradient and a blotchy image. For this app, I am only going to use the last one. I just like it better than the others. So, the user will have a choice of a blotchy background or no background. They will not have a choice for the colour map for the background. I have selected a set of possible background colour maps for each possible image colour map. One of those will be selected at random each time an image is generated.
New Global Variables
I added a few new global variables for use with the background colour. Because I am not using multiple background variations, I dropped use_bg
and in my code use use_bb2
instead. Easy enough to change if need be. But for now one less globabl to think about. And, my thinking about needs all the help it can get. You may note that by default a background is not added to the image.
# colour background variables
bg_cmnm = None # nmae of background colour map
bg_cmap = None # colour map to use for background
bg_lpha = 0.58 # default alpha value for background
use_bb2 = False # use 2nd version blotchy background
bb2_dim = 8 # dimension for colour array
bb2_dim = 4 # dimension for colour array
use_rand_bg = True # select bg colour map from list available for current curve colour map
v_min = 0.2
v_max = 0.8
Add Functions to Library Module
I could likely have done this with one function. But, in case I decide to add one or both of the other variations, I am keeping my options open. And, there is a large dictionary with the background choices for the foreground choices.
For the call to imshow
I am passing the parameter transform=axb.transAxes
. This allows me to tell the method to fill the whole plot area without actually knowing the plot limits. But, by itself that didn’t work as expected. It did not end up filling the whole figure area. I also needed to add aspect='auto'
.
I expect that the aspect='auto'
might have fixed the problem I was having using the two axes approach.
... ...
bg_cmap = {
'autumn': ['autumn', 'YlOrBr', 'YlOrRd', 'gist_earth', 'gnuplot', 'Wistia', 'Greys', 'bone'],
'bone': ['bone', 'winter', 'gray', 'cividis', 'Greys'],
'BuGn': ['BuGn', 'GnBu', 'winter', 'YlGnBu', 'Greys', 'bone'],
'BuPu': ['BuPu', 'PuBu', 'PuBuGn', 'summer', 'Greys', 'bone'],
'cividis': ['cividis', 'copper', 'terrain', 'gist_earth', 'Greys', 'bone'],
'cubehelix': ['cubehelix', 'gist_earth', 'ocean', 'Greys', 'bone'],
'cool': ['cool', 'BuPu', 'PuBu', 'hsv', 'Greys', 'bone'],
'copper': ['copper', 'ocean', 'terrain', 'gist_earth', 'cividis', 'Greys', 'bone'],
'default': ['default', 'tab20', 'tab20c', 'Greys', 'bone'],
'GnBu': ['GnBu', 'viridis', 'YlGnBu', 'summer', 'Greys', 'bone'],
'gist_earth': ['gist_earth', 'copper', 'ocean', 'terrain', 'cividis', 'Greys', 'bone'],
'gnuplot': ['gnuplot', 'cividis', 'viridis', 'cubehelix'],
'hot': ['hot', 'YlOrRd', 'reds', 'autumn', 'gnuplot', 'Greys', 'bone'],
'inferno': ['inferno', 'magma', 'plasma', 'RdPu', 'Greys', 'bone'],
'jet': ['jet', 'rainbow', 'turbo', 'terrain', 'Greys', 'bone'],
'magma': ['magma', 'plasma', 'RdPu', 'inferno', 'Greys', 'bone'],
'plasma': ['plasma', 'RdPu', 'inferno', 'magma', 'Greys', 'bone'],
'PuBu': ['PuBu', 'PuBuGn', 'summer', 'Greys', 'bone'],
'PuBuGn': ['PuBuGn', 'PuBu', 'BuPu', 'Greys', 'bone'],
'PuRd': ['PuRd', 'RdPu', 'inferno', 'magma', 'Greys', 'bone'],
'rainbow': ['rainbow', 'jet', 'turbo', 'cool', 'Greys', 'bone'],
'RdPu': ['RdPu', 'PuRd', 'inferno', 'magma', 'Greys', 'bone'],
'summer': ['summer', 'viridis', 'YlOrBr', 'YlOrRd', 'ocean', 'gist_earth', 'terrain'],
'tab20': ['default', 'tab20', 'tab20c', 'Greys', 'bone'],
'tab20c': ['default', 'tab20', 'tab20c', 'Greys', 'bone'],
'terrain': ['terrain', 'ocean', 'gist_earth', 'copper', 'Greys', 'bone'],
'turbo': ['turbo', 'rainbow', 'jet', 'cool', 'Greys', 'bone'],
'twilight_shifted': ['twilight_shifted', 'gist_earth', 'cubehelix', 'copper', 'Greys', 'bone'],
'viridis': ['GnBu', 'YlGnBu', 'summer', 'Greys', 'bone'],
'winter': ['winter', 'GnBu', 'BuGn', 'Greys', 'bone'],
'YlGnBu': ['YlGnBu', 'BuGn', 'GnBu', 'viridis', 'Greys', 'bone'],
'YlOrRd': ['YlOrRd', 'summer', 'hot', 'autumn', 'Greys', 'bone']
}
... ...
# the actual background painting function
def blotch_bg_2(axb, extent, abg=None, dim=5, **kwargs):
if abg is not None:
A = abg
else:
A = np.random.rand(dim, dim)
c = axb.imshow(A, extent=extent, interpolation='bilinear', **kwargs)
axb.autoscale()
return c, A
# the function called from the routing code which calls the above
def set_bg(axb):
abg = None
baxt = None
if g.use_bb2:
baxt, abg = blotch_bg_2(axb, extent=(0, 1, 0, 1), dim=g.bb2_dim, cmap=g.bg_cmap, alpha=g.bg_lpha, transform=axb.transAxes, zorder=1, aspect='auto')
return baxt, abg
... ...
New Form Field
A simple radio button field to determine whether or not to add the background to the generated image. For now this is the last field on the form.
... ...
<div class="radio">
<p style="margin-left:1rem;margin-top:0;"><b>Add background colour:</b></p>
<p>
<label for="bgno">No: </label>
<input type="radio" name="do_bg" id="bgno" value="false" />
<label for="bgyes">Yes: </label>
<input type="radio" name="do_bg" id="bgyes" value="true" />
</p>
</div>
... ...
Refactor Form Processor
And, of course, we need to do something with this new bit of user specified information. I will only show the code added to proc_curve_form()
. I put it after the code for the foreground colour map as that information is required in order to select an appropriate (to my eyes) background colour map.
And, you will note I don’t check to see if the curve data is being used again, so the colour map, alpha value, etc. will change each time an image is generate. Well as appropriate.
if 'do_bg' in f_data:
g.use_bb2 = True if f_data['do_bg'] == 'true' else False
# else:
# g.use_bb2 = g.use_bb2
if g.use_bb2 and g.use_rand_bg:
g.bg_cmnm = np.random.choice(bg_cmap[g.rcm])
g.bg_cmap = bgcm_mpl[g.bg_cmnm]
else:
g.bg_cmnm = g.rcm
g.bg_cmap = bgcm_mpl[g.rcm]
if "tab" in g.bg_cmnm or g.bg_cmnm == 'default':
g.bg_lpha = np.random.randint(5, 15) / 100
else:
g.bg_lpha = np.random.randint(15, 33) / 100
Refactor Routing Code
This was pretty simple. I added a call to autoscale()
after the image and copyright plotting is done. Then a single function call gets us the background (assuming it is requested).
... ...
ax.autoscale()
bax2, _ = sal.set_bg(ax)
# Save it to a temporary buffer.
... ...
Examples
Curve Parameters
Wheels: 7 wheels (shape(s): ['q', 'e', 'c', 't', 'c', 's', 'r'])
Symmetry: k_fold = 7, congruency = 1
Frequencies: [1, -6, -13, -27, 8, 29, 29]
Widths: [1, 0.5669709821724747, 0.4347546584625024, 0.3883477569234929, 0.2851075662606766, 0.26021366721703976, 0.17832055280855555]
Heights: [1, 0.5501623531618329, 0.5026376393359808j, 0.41611836832070437, 0.41284478638200534j, 0.3562011643488639, 0.20509818544394587j]
Image Type Parameters
Colouring: between adjacent datarows with overlap
Sections: 24 colour sections per datarow pairing
Multipliers: [2.6, 2.0, 1.2, 1.0, 1.6, 1.0]
Drawing Parameters
Colour map: inferno
BG colour map: inferno
Line width (if used): 9
Curve Parameters
Wheels: 9 wheels (shape(s): ellipse (e))
Symmetry: k_fold = 8, congruency = 5
Frequencies: [-3, 37, -11, 37, 29, 13, 13, -3, 29]
Widths: [1, 0.6091062912333832, 0.3346798175745608, 0.3137254807468091, 0.31230220626637617, 0.26163199422596245, 0.24367166394763135j, 0.14362015846549384, 0.125j]
Heights: [1, 0.5961823498982441, 0.5935272852434413j, 0.5278869244509318j, 0.5152302162521647, 0.4355953689713816, 0.36093815416620056, 0.32523532605293604j, 0.262624056422016]
Image Type Parameters
Colouring: between adjacent datarows with overlap
Sections: 32 colour sections per datarow pairing
Multipliers: [2.0, 1.8, 1.6, 1.2, 1.2, 1.2, 1.5, 1.5, 1.5]
Drawing Parameters
Colour map: turbo
BG colour map: bone
Line width (if used): None
Curve Parameters
Wheels: 11 wheels (shape(s): ellipse (e))
Symmetry: k_fold = 9, congruency = 7
Frequencies: [-2, 25, 25, -2, -2, -2, -11, 7, 34, 43, -2]
Widths: [1, 0.7497773960184834, 0.45236236931015067, 0.2672543647789131, 0.23752022396007655, 0.15817367468602164, 0.1365675000461169j, 0.125, 0.125, 0.125j, 0.125j]
Heights: [1, 0.5151715990708005j, 0.2819246746908735j, 0.1615304280016076, 0.125j, 0.125j, 0.125j, 0.125, 0.125, 0.125j, 0.125]
Image Type Parameters
Drop data rows: 0 datarow
From: dropped from the 'top' of the curve dataset
Drawing Parameters
Colour map: hot
BG colour map: hot
Line width (if used): 14
And couple for which I failed to save the details.
Bug
While generating images with coloured backgrounds using a randomly selected foreground colour the app crashed with the error KeyError: 'hsv'
. Turns out I was using the list of background colours in the library module to select a foreground colour rather than the list of foreground colours in the global variables module. The two lists are not the same. So, when I looked for key hsv
in the dictionary providing the list of background colours for the foreground colours, it wasn’t found. Refactored the code accordingly.
if u_cmap:
if u_cmap in g.clr_opt:
g.rcm = u_cmap
elif u_cmap == 'r':
g.rcm = rng.choice(g.clr_opt)
# else: use the current colour again
if g.rcm is None:
g.rcm = rng.choice(g.clr_opt)
Done
You know I think that’s it for this one. Until next time, happy hours coding.
Resources
- matplotlib.pyplot.imshow
- Transformations Tutorial
- Matplotlib imshow() stretch to “fit width”