Okay, decided to tackle rotational transformations of the colour between images. Didn’t think I could do this using Matplotlib functions/methods. But never really tried. Took a quick look at SciPy, but didn’t see an obviously useful function/method there either. So decided to do the arithmetic myself.

Going to use numpy functions. The curve data is already in numpy arrays, so should go reasonably well.

Rotational Transform

This should just be a simple bit of matrix arithmetic. Wrote a function to take the x and y data and a transformation matrix. Guess I could have just passed in the angle and sort the matrix within the function. But I was thinking I might also use this function to do the linear translation. But, couldn’t get that to work. And simple addition proved much more efficient.

The rotatoinal transformation turned out to be pretty straightforward. Just looped through the data for each wheel, did a bit of matrix arithmetic, added the changed data to a Python list and, finally, returned the two lists as numpy arrays.

I inverted the transformation array because of something I read. Haven’t really tried to figure out why. And, haven’t tried not inverting to see what happens.

def affine_transformation(srcx, srcy, a):
  dstx = []
  dsty = []
  for i in range(len(srcx)):
    src = np.array([srcx[i], srcy[i]], dtype='float')
    new_points = np.linalg.inv(a).dot(src).astype(float)
    dstx.append(new_points[0])
    dsty.append(new_points[1])
  return np.array(dstx, dtype="float"), np.array(dsty, dtype="float")

Now for the rotational transformation matrix. I am starting with four linear+rotational transformations. So using angles that are multiples of 45°. Created list with the angles and then in a loop generate the transformed data and plot. For now these will be on top of each other as no linear translation yet being applied.

For now I am just using one plot between function, btw_rnd, for all my plots. And, I am using a goodly number of colour sections.

theta = [np.pi/4, np.pi*3/4, np.pi*5/4, np.pi*7/4]

ax.autoscale(True)

for i in range(4):

  a = np.array([[np.cos(theta[i]), -np.sin(theta[i])],
                [np.sin(theta[i]), np.cos(theta[i])]])

  dstx, dsty = affine_transformation(r_xs, r_ys, a)

  btw_rnd(ax, dstx, dsty, bc='m', r=False, fix=None, mlt=False, sect=32)
  ax.autoscale(True)

Linear Translation

I decided to shift the rotational plots by half the current minimums and/or maximums for each axis. That proved fairly simple. I took advantange of a previously written function.

# above the loop
mnx41, mxx41, mny41, mxy41 = get_plot_bnds(r_xs, r_ys, x_adj=0)
dx = [mnx41/2, mnx41/2, mxx41/2, mxx41/2]
dy = [mxy41/2, mny41/2, mny41/2, mxy41/2]

...

# within the loop, before the plotting function call
...
dstx = dstx + dx[i]
dsty = dsty + dy[i]
...

Initial Examples

The first image in each set is the base colour between curve. The second is with the transformations applied. Because I am using only 128 colours when reducing the image for the post, the background does get distorted.

This first set uses a curve generated by 12 circles.

gnarly spirograph image generated with matplotlib fill_between()
gnarly spirograph image generated with matplotlib fill_between(), with linear and rotational transformations

This next set used 4 ellipses to generate the base curve data.

gnarly spirograph image generated with matplotlib fill_between()
gnarly spirograph image generated with matplotlib fill_between(), with linear and rotational transformations

No don’t know where the gaps are coming from. Expect because I am only using 500 data points per wheel that there are some gaps in the coverage over the x-axis. But, it does add a slightly different artistic note to the images.

One more. The underlying curve is based on 9 squares.

gnarly spirograph image generated with matplotlib fill_between()
gnarly spirograph image generated with matplotlib fill_between(), with linear and rotational transformations

Enhancements

1. Varying the Number of Transformations

Thought I’d like to play with different numbers of rotations as I did in an earlier post. I will reuse the functions and logic discussed in that post. As well as the application’s interface commands. (If I may be allowed to call it an application.) E.G. entering trq10 at the command line interface will set the number of transformations to 10.

The code for this plot now looks like the following.

# sort number of quadrants
do_qd = range(nbr_qds)
if tr_qd and tq_r:
  do_qd = range(tq_r)
  nbr_qds = max(list(do_qd)) + 1

lld_rot = get_angles(nbr_qds)
print(f"\tnbr_qds: {nbr_qds}, lld_rot: {lld_rot}")

mnx41, mxx41, mny41, mxy41 = get_plot_bnds(r_xs, r_ys, x_adj=0)
print(f"\tmins: {mnx41}, {mny41}; maxs: {mxx41}, {mxy41}")

# sort base point for future linear translations
mdy = max(mxx41, mxy41)
if not t_sv:
  y_mlt = random.choice([.875, .75, .625, .5, .375])
trdy = mdy * y_mlt
# following only present so can print out related info
dx, dy = rotate_pt(0, trdy, angle=lld_rot[0], cx=0, cy=0)
print(f"\tstarting point ({mdy} * {y_mlt}): (0, {trdy}), init rotation pt: ({dx}, {dy})")

ax.autoscale(True)

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 = affine_transformation(src[0], src[1], a)
  dx, dy = 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])}, dx: {dx}, dy: {dy}")

  btw_rnd(ax, dstx, dsty, bc='m', r=False, fix=None, mlt=False, sect=32)
  ax.autoscale(True)

And, unlike above where I more or less translated each rotation to the middle of one of the four quadrants, this time the translation is slightly more random. I had to change things in order to generate a different linear translation for each rotational transformation. I take the maximum of the the maximum x and y values. Multiply that by one of 5 randomly selected values to get a point on the y axis. Then for each rotational transformation, that point on the y-axis is rotated by an appropriate number of degrees to determine the linear translation. Another of my cataclysmic variables.

I have stolen that term from astronomy to describe my use of a number of random values to generate each and every image. A significant lack of control in some cases.

Examples with Varying Numbers of Rotations

That randomly initialized linear translation vector, will affect the images to some extent. I think that will actually be fairly noticeable in some of the following examples.

The base curve was generated using 8 circles.

Base colour-between image
gnarly spirograph image generated with matplotlib fill_between()
10 transformations
starting point (1.4599321303318247 * 0.875): (0, 1.2774406140403465)
gnarly spirograph image generated with matplotlib fill_between(), with linear and rotational transformations
7 transformations
starting point (1.4599321303318247 * 0.5): (0, 0.7299660651659123)
gnarly spirograph image generated with matplotlib fill_between(), with linear and rotational transformations
16 transformations
starting point (1.4599321303318247 * 0.625): (0, 0.9124575814573904)
gnarly spirograph image generated with matplotlib fill_between(), with linear and rotational transformations
3 transformations
starting point (1.4599321303318247 * 0.375): (0, 0.5474745488744343)
gnarly spirograph image generated with matplotlib fill_between(), with linear and rotational transformations

The way my code is working, that last transformation is at 360°. Will have to look at changing that somehow. Maybe a small additive factor on all the angles. But really only obvious in the above case.

5 transformations
starting point (1.4599321303318247 * 0.75): (0, 1.0949490977488685)
gnarly spirograph image generated with matplotlib fill_between(), with linear and rotational transformations

2. Random Block Size Multiplier

The other thing I’d like to try is adding that attempt at randomly modifying the size of the coloured areas. In a previous post I controlled the use of this approach with one of the questions asked the user each time the plot type (40) was inititated. For this case I think I will just add another interface command, bwm (i.e. between-multiply). Each time the command is entered it will toggle the value of a boolean module variable, t_wm (default: False). The code generating the plot type, will use the value of that variable to determine whether or not to apply randomly selected multipliers to the data rows. (Another of the cataclysmic variables.)

As the various functions have been modified to accept a parameter to invoke this behaviour, things should be fairly simple. But, upon reflection, as usual, not entirely so. The previous code was written so the that functions would be used once per image. Since I call the colour-between function once for each transformation in the image I need to make sure the functions use the same set of multipliers each time. And that the whole thing will be accurately regenerated if I save the image to file. So, maybe not so simple afterall.

Well another module variable telling functions whether or not to use a new set of multipliers or the existing set seems to have sorted that issue. With a slight refactoring of the multiplier generator function and the code for the plot type also required.

There’s no really new concepts or code, so no code to be shown.

Random Block Sizes Examples

Not sure this adds anything to images generated using transformations. But you should be shown an example or two. So…

These all use the same base curve generated using 4 squares.

4 transformations
starting point (* 0.875): (0, 1.0243648739880733)
mutlipliers: [2.4, 2.6, 2.2, 1.1]
gnarly spirograph image generated with matplotlib fill_between(), with linear and rotational transformations
7 transformations
tarting point (* 0.5): (0, 0.5853513565646133)
multipliers: [1.6, 2.6, 2.4, 1.3]
gnarly spirograph image generated with matplotlib fill_between(), with linear and rotational transformations
5 transformations
starting point (* 0.75): (0, 0.87802703484692)
multipliers: [1.4, 2.6, 1.6, 1.5]
gnarly spirograph image generated with matplotlib fill_between(), with linear and rotational transformations

Done

I think that’s it for this one. Lot’s of images, and generally speaking long enough in terms of reading time.

I would like to look at using both types of colour-between functions. And look at using different parameters for each. But, I don’t really want to go through the question sequence used by the previous plot type (40). So, I think I will add a bunch more commands to the applications command line interface. Which of course means a number of new module variables as well. And, likely a bit of refactoring here and there.

Until the next time, try to remember that coding is fun!

Resources

P.S.

I did modify my code to skip the np.linalg.inv() on the rotational transformation matrix. Things still seem to work just fine. Will add some code to allow me to compare the same image with or without the inv(). Wonder how much difference it makes in the final image.