I had my concerns about using np.linalg.inv() on the transformation array for the rotational transformations before applying the dot product. But mindlessly went with what I had read in a blog post.

When you look at the equations for a geometric rotation, applying an inverse to the transformation array looked to me to muddle the desired affect on apply the transformation array as created to represent the equations. And, the images never seemed to me to come out the way I expected them to look. For one thing the rotations did not seem to reflect the angles used. And, I expected more symmetry than I was getting.

Based on my latest tests, it seems I should have been a great deal more concerned and really checked things out at the start of this current excerise. My bad!

Inverse or Not?

I always expected the rotations to look a specific way based on the angles of rotation. But, they never quite looked like that. That is until I tested not using the inverse.

I added another command for the applicaton interface, another module variable (bw_i, default is False) and then refactored the appropriate function accordingly. Which now looks like the following.

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

Comparative Examples

This first set is based on an underlying curve generated using 5 equilateral triangles. The first image is the underlying image without any transformations. The second is what gets generated when the inverse is applied to the rotational transformation matrix to generate the new image. The third is without the inverse being applied.

btw_rnd(bc='m', r=False, fix=None, mlt=False, sect=32) -> base row: 2
gnarly spirograph image generated with matplotlib fill_between()
4 transformations with inverse
starting point (* 0.625): (0, 0.894440042686542)
gnarly spirograph image generated with matplotlib fill_between(), with linear and rotational transformations
4 transformations without inverse
starting point (* 0.375): (0, 0.5366640256119253)
gnarly spirograph image generated with matplotlib fill_between(), with linear and rotational transformations

And, what I was always expecting to see was that last image above. Including a more overall feeling of symmetry. If only I hadn’t been so slow-witted. The last post might have had better looking images. But, in my mind, there is always a place for a lack of symmetry in art.

This next set I think really demonstrates what I expected to see versus what I was originally getting. They are based on an underlying curve generated using 7 tetracuspids. And, I stayed with 4 transformations.

The base image.
gnarly spirograph image generated with matplotlib fill_between()
inverse applied, larger linear transformations
starting point (* 0.75): (0, 1.2076828090880651)
gnarly spirograph image generated with matplotlib fill_between(), with linear and rotational transformations
inverse applied, narrower linear transformations
starting point (* 0.375): (0, 0.6038414045440326)
gnarly spirograph image generated with matplotlib fill_between(), with linear and rotational transformations

Don’t know if you can see that the rotations, starting in the upper left quadrant going counter-clockwise, don’t seem to follow the 45°, 135°, 225°, 315° sequence I was using for the rotations.

inverse not applied, larger linear transformations
starting point (* 0.875): (0, 1.4089632772694094)
gnarly spirograph image generated with matplotlib fill_between(), with linear and rotational transformations

It seems to me that one can see the expected rotation for each quadrant. And, if we go with narrower linear transformations things get nicely messy. Though the expected rotation angles are still visible.

inverse not applied, narrower linear transformations
starting point (* 0.5): (0, 0.8051218727253768)
gnarly spirograph image generated with matplotlib fill_between(), with linear and rotational transformations

Which way looks better? Eye of the beholder m’ thinks. So, the switch will stay in place giving me the option for additional variations (marginal though they might be).

As I finish this section, I still have not added all the other interface commands I would like in place to allow for selecting more of the available functions and function parameters. Will likely take me some time to do so.

Randomly Sized Coloured Areas

Though while we are at it, we could have a look at the above curve with the coloured areas being randomly resized. For both cases, with and without using the inverse on the rotational transformation matrix. Of course the number of transformations and the area multipliers will also affect the generated image. But, possibly worth a look.

inverse not applied, 4 transformations, narrow linear transformations
starting point (* 0.375): (0, 0.6038414045440326)
multipliers: [1.2, 2.6, 2.4, 2.4, 1.5, 1.5, 1.4]
gnarly spirograph image generated with matplotlib fill_between(), with linear and rotational transformations
inverse applied, 7 transformations, wider linear transformations
starting point (* 0.625): (0, 1.006402340906721)
multipliers: [2.2, 1.6, 1.8, 2.0, 1.5, 1.0, 1.6]
gnarly spirograph image generated with matplotlib fill_between(), with linear and rotational transformations
inverse not applied, 7 transformations, narrower linear transformations
starting point (* 0.5): (0, 0.8051218727253768)
multipliers: [2.0, 1.2, 1.8, 1.4, 1.1, 1.3, 1.0]
gnarly spirograph image generated with matplotlib fill_between(), with linear and rotational transformations

Even with the random multipliers, the above just feels more symmetrical than the one before it.

inverse applied, 16 transformations, narrower linear transformations
starting point (* 0.5): (0, 0.8051218727253768)
multipliers: [2.4, 1.8, 1.2, 2.0, 1.1, 1.0, 1.5]
gnarly spirograph image generated with matplotlib fill_between(), with linear and rotational transformations
inverse not applied, 10 transformations, narrower linear transformations
starting point (* 0.5): (0, 0.8051218727253768)
multipliers: [2.2, 2.6, 2.6, 2.0, 1.6, 1.4, 1.3]
gnarly spirograph image generated with matplotlib fill_between(), with linear and rotational transformations
inverse not applied, 5 transformations, narrower linear transformations
starting point (* 0.5): (0, 0.8051218727253768)
multipliers: [2.4, 1.2, 1.4, 2.0, 1.1, 1.2, 1.1]
gnarly spirograph image generated with matplotlib fill_between(), with linear and rotational transformations

Done

I think that’s it for this one. A little come-uppance for me, my laziness and my lack of good research skills.

The variations possible given a single underlying set of spirograph curve data, truly amazes me. Reminds me of what different media outlets and politicians can do when applying statistics to the same set of economic data.

Hopefully I will get my code refactored in time for the next post. Which will likely just be a gallery of images I like enough to save and post.

Enjoy your time at the keyboard.

Resources