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.
Others not so much.
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.
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.
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 ther_xs
in others. Since we are plotting the gnarly style plots, I went with ther_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.
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…
So, I figured, let’s drop the base image. It’s not really necessary given the rotations. Eh, voilà!
A Couple More Examples
Think that’s going to be just about it for this one.
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.
- Why matplotlib circle/patchCollection’s point of rotation get changed