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
mosaic like image with coloured background
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
mosaic like image with coloured background
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
gnarly like image with coloured background

And couple for which I failed to save the details.

gnarly like image with coloured background basic spirograph with coloured background

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