Okay, let’s see what we can do about resizing the image so that the rotated images are nicely contained within the frameable area. And hopefully more or less centred.

An Important Reqirement

With the linear translations, I was doing the resizing after everything was plotted. This worked because of the DPI safe method employed in doing the transformations. There is no DPI safe way, that I can find, to do that for the rotational transformations. If I resize the image after plotting the transformations, things shift in undesirable ways. So the resizing needs to be done before I plot those transformations. And, before I do any data to display transformations or vice versa. Before doing those the data limits need to be set and fixed for the duration.

First Attempt

My first attempt to sort the new data limits probably started out smart enough. But as I went along I kept adding hacks to make adjustments for various issues. Some of which were related to how a particular wheel shape affected the middle or the extreme end of the base image.

I started by using the transformation matrix for each rotation to sort the maximum x and y translations in both directions. I figured if I used the those values to adjust the minimum and maximum curve values for each axis, I should be close to what I was looking for.

I wrote a small function, get_matrix(qd), to calculate the transformation for a given quadrant and return the transformation matrix. I got the translation values from the matrix and sorted my maximum translation in all four directions.

I had to reorder some of my previous code to make sure the variables I needed were available when I called the function.

      def get_matrix(qd):
        ry, rx = ax.transData.transform((tq_xs[qd], tq_ys[qd]))
        # rx, ry = p_tqs[tq][1], p_tqs[tq][0]
        rot = transforms.Affine2D()
        rot.rotate_around(tq_xs[qd], tq_ys[qd], r_rot[0])
        # r_t = ax.transData + rot
        return rot.get_matrix()

... ... ... ...

      pxn26, pxx26, pyn26, pyx26 = get_plot_bnds(t_xs, t_ys, x_adj=0)

      x_tmp = (pxx26 + pxn26) / 2
      y_tmp = (pyx26 + pyn26) / 2

      # approach #2 used rotated lower left corner of image for rotation pivot points
      i_blx, i_bly = pxn26, pyn26
      print(f"\timage bottom left: ({i_blx}, {i_bly}) in data coord")
      tq_xs = [(i_blx - x_tmp)*math.cos(llr_rot[i]) - (i_bly - y_tmp)*math.sin(llr_rot[i]) + x_tmp for i in range(nbr_qds)]
      tq_ys = [(i_blx - x_tmp)*math.sin(llr_rot[i]) + (i_bly - y_tmp)*math.cos(llr_rot[i]) + y_tmp for i in range(nbr_qds)]

      minx, maxx, miny, maxy = 0, 0, 0, 0
      do_rot_adj = [False, False]
      # yet another attempt
      for i in range(4):
        m_rt = get_matrix(i)
        print(m_rt)
        mty, mtx = ax.transData.inverted().transform((m_rt[0][2], m_rt[1][2]))
        mtx, mty = m_rt[0][2], m_rt[1][2]
        mrx, mry = abs(m_rt[0][0]), abs(m_rt[1][0])
        if mtx < 0:
          minx = min(minx, mtx)
        else:
          maxx = max(maxx, mtx)
        if mty < 0:
          miny = min(miny, mty)
        else:
          maxy = max(maxy, mty)
      print(f"\tinit (minx, maxx, miny, maxy): ({minx}, {maxx}, {miny}, {maxy})")

      ax.set_xlim(pxn26+minx, pxx26+maxx)
      ax.set_ylim(pyn26+miny, pyx26+maxy)
      print(f"\tsetting data limits to: ({pxn26+minx}, {pxx26+maxx}) and ({pyn26+miny}, {pyx26+maxy})")

      xmn26, xmx26 = ax.get_xlim()
      ymn26, ymx26 = ax.get_ylim()
      print(f"\tcurrent data limits: ({xmn26}, {xmx26}) and ({ymn26}, {ymx26})")

Some came out quite nicely.

Wheels: 3 (Tetracuspid)
attempt to set appropriate data limits for gnarly spirograph images expanded through applying 4 symmetrical rotational translations

Others not so much.

Wheels: 6 (Equilateral Triangle)
attempt to set appropriate data limits for gnarly spirograph images expanded through applying 4 symmetrical rotational translations

And, if you are doing this yourself, you know that the wheel shape has a lot to do with how well this adjustment works or does not.

In my original trials and tribulations, I made attempts to account for the variations related to shape.

Second Attempt

Looking at a number of images using the above approach, it seems pretty clear that in most cases:

  • rotation 1 (red) defines the top data limit
  • rotation 2 (blue) defines the right data limit
  • rotation 3 (green) defines the bottom data limit
  • rotation 4 (yellow) defines the left data limit

Though the actual point defining that limit is not so obvious, or easy to come by. And of course varies considerably by wheel shape, number of wheels, k-fold value, etc.

Two make my life easier I decided to, during further development, to use the 5 basic colours for the base and transformed images. And, I will plot various spots as needed. I am going to plot the extreme points for the base image (star markers) so that I can see what I am basing my new data limits on.

I start by trying to determine the extreme points on the base image. A few functions written to get that done. In getting those points, I went through every row of the curve data. Which required a re-write of get_plot_bnds. Turns out the last row did not always contain the points furthest from the center. I used the function from above to get the transformation matrix without plotting anything.

For each of the extreme points I rotate them appropriately and then apply the linear transformation to each one from what I believe to be the appropriate transformation matrix. Then I take the minimums and maximums for the x and y values to determine my plot limits.

I am printing a great deal of info about the various points and numbers I am using to try and determine those new plot limits. Almost too much, often difficult to follow. But at the moment I am thinking more is better than less. Not often I feel that way—highlights my confusion.

I am not going to bother with the code. Is messy, muddle-minded, full of various, commented out, attempts to sort various issues. All those comments have doubled or tripled the lines in that plot’s section of the file.

Examples

Here are a few of my debugging images. The first one shows an example where my code was more or less successful. Though I do think it ended up over-estimating the data limits.

Wheels: 8 (Circle)
attempt to set appropriate data limits for gnarly spirograph images expanded through applying 4 symmetrical rotational translations

The next two show the more typical case — failure! I isolated the base image with the extreme points and the rotated and translated points plotted. Pretty easy to see which one determined the resized data limits.

Wheels: 4 (Equilateral Triangle), no rotatons
attempt to set appropriate data limits for gnarly spirograph images expanded through applying 4 symmetrical rotational translations
wheels: 4 (Equilateral Triangle), with rotations
attempt to set appropriate data limits for gnarly spirograph images expanded through applying 4 symmetrical rotational translations

There were considerably more images with inadequate data limits than those that, more or less, worked.

In the process there were a number of bugs and logic errors. For example:

  • at some point realized that I was, for some cases, working out the exteneded data limits before I modified the plot data to shift the image centre to the axes centre
  • as I was working on fixing the above, I realized that I was using the t_xs curve data in some cases and the r_xs in others. Since we are plotting the gnarly style plots, I went with the r_xs datasets.

Third Iteration

Since the above was not working I figured I needed to some how figure out where the extreme points actually ended up after each rotation. Didn’t want to go about sorting out a function to do the matrix arithmetic required. So went searching on the web.

That’s when I stumbled on this stackoverflow post:

Why matplotlib circle/patchCollection’s point of rotation get changed

It was not specifically related to my problem. But, I noticed that the solution by JohanC was calling ax.autoscale() after the transformation was plotted.

As I mentioned before, I couldn’t seem to get autoscale to work. But in my case, I was turning autoscale on before plotting anything. Since that didn’t seem to work, I decided to follow the above example and do so after I plot the rotations and see what happens. I turned off my attempt to set the data limits as I wanted to see what autoscale could do for me. I also stopped plotting the various points shown above. Didn’t want anything messing with autoscale’s work.

At first I just ran ax.autoscale(True) after all the plotting was done. That didn’t seem to work. All I was getting in the plot was the base image filling the plot area. No rotations. So, added ax.autoscale(True) after the plot of each transformation. That still only left a plot with the base image filling the plot area.

Then it occurred to me that autoscale didn’t have anything to work with after the rotations. I had deferred plotting the base image until after plotting the transformations. So, I moved plotting of the base image ahead of the plotting of the transformations. Well, higher than that. Because I had turned off setting data limits, I needed the plot to be high enough that later coordinate transforms worked properly. That was why I wasn’t getting any transformations to show up.

I also had to somehow set the data limits, so I once again used get_xlim() and get_ylim after the plot and before any transformations. I finally got the transformations to show. But, the plot area was filled by the base image and the rotated images were mostly outside the plot area. So, autoscale still not working?

Fourth Iteration

I finally thought about something else JohanC had said.

To rotate around a certain point, you can first subtract its coordinates, do the rotation and then add these coordinates again.

So, I decided to give that a try. I decided to use roughly the middle of each quadrant for the rotation point. And to rotate them

      dx, dy = pxx26 / 2, pyx26 / 2
      tst_x = [-dx, -dx, dx, dx]
      tst_y = [dy, -dy, -dy, dy]

And, within the transformatio loop, I calculate the transform as follows.

          xc = tst_x[tq]
          yc = tst_y[tq]
          shadow_transform = transforms.Affine2D().translate(-xc, -yc).rotate(tr_rot[tq]).translate(xc, yc) + ax.transData

Here’s an example of what I started getting.

Wheels: 3 (Equilateral Triangle)
attempt to set appropriate data limits for gnarly spirograph images expanded through applying 4 symmetrical rotational translations

Finally some real progress. And equilateral triangle shaped wheels always caused the most trouble getting the data limits correctly. But why does autoscale now work? And, not quite where I expected to see the rotations. I took another good look at that post, and realized something else.

The rotation point essentially specified a single spot on a circle about the center of the plot. So, I decided to do all the rotations (different angles) around the same point.

          xc = tst_x[0]
          yc = tst_y[0]

And, wow! Some serious progress. But…

Wheels: 8 (Ellipse)
attempt to set appropriate data limits for gnarly spirograph images expanded through applying 4 symmetrical rotational translations

So, I figured, let’s drop the base image. It’s not really necessary given the rotations. Eh, voilà!

Wheels: 7 (Rhombus)
attempt to set appropriate data limits for gnarly spirograph images expanded through applying 4 symmetrical rotational translations

A Couple More Examples

Think that’s going to be just about it for this one.

Wheels: 5 (Square)
attempt to set appropriate data limits for gnarly spirograph images expanded through applying 4 symmetrical rotational translations
Wheels: 6 (Square)
attempt to set appropriate data limits for gnarly spirograph images expanded through applying 4 symmetrical rotational translations

Ciao for Now

Still don’t understand how or why it works. But, it works. Good enough for me. And, this post has taken quite some time to put together. So I think it best to call it done (like a good steak on the barbeque, don’t want it overdone).

But, I like the look enough, that I think I am going to do one more post on rotations. I want to look at adding more rotations to the image. And trying different locations for the base rotation point. Including (0, 0). Not sure how I’ll handle it all, but most likely more random choices before each plot. And, for my own piece of mind, I will rework the code to remove all the unecessary bits and pieces. I’d like to see just how simple it now is.

And finally, JohanC, thank you very much. Wish I knew as much as you apparently do.

The truly sad thing is that I only found that post yesterday afternoon, approximately 1 day ago. I have been muddling with setting good data limits for a week or more. Basically two lines of code solved my problem. I have 95 lines of code in that block of code. Though not all of that can thrown away. But likely something like ⅔ of it can deleted.

Resources

A little light on resources. But I sure wish I had found this a long time ago.