Have been playing with a couple of spirograph image varitions. Not sure I should any longer be calling these things spirographs. But the underlining curve is generated using a spirograph-like algorithm. So, I will continue using the label spirograph.

Regardles, as a result, my potential return to machine learning will have to wait a while longer.

The first new variation was to simply translate the curve by the same distance diagonally into each quadrant. But, matplotlib clipped the images at the existing data limits for the given axes used for the plots. autoscale didn’t seem to work — still not sure why. I resorted to changing the data limits in code. An attempt at a scaled estimate the first way and a little less guessing the second and current way.

The second variation was a result of a locker room conversation with Gary (yes, that Gary). He had seen some artistic wood mosiacs at a local art fair. Made me wonder if I could some how determine all the polygons/shapes generated by the curve. Then colour each one in a pleasing way. Turns out to be beyond my current (dys)functional mathematical skills. But, in the process I stumbled upon matplotlib’s axes.Axes.fill_between.

Don’t expect the results will be to everyone’s liking, but it does give me some more options for my own entertainment.

Linear Translation

I basically decided to make four copies of the existing image, shifted by some distance at 45°. I.E. diagonally into each of the four quadrants. I based my code on an example in the Matplotlib Transformations Tutorial. That example was about adding a shadow to a given curve. It used a transform based on DPI. That way, when I save my images at a much higher DPI value, the plot will look the same.

Here we apply the transforms in the opposite order to the use of ScaledTranslation above. The plot is first made in data coordinates (ax.transData) and then shifted by dx and dy points using fig.dpi_scale_trans. (In typography, a point is 1/72 inches, and by specifying your offsets in points, your figure will look the same regardless of the dpi resolution it is saved in.)

For the most part, I am shifting all four the same distance. But, I did provide myself the option of having each translation shift a different distance. And to also not shift into all the diagonals. Don’t know if there will be any code for or examples of that sort of arrangement.

As mentioned, matplotlib clipped the translations at the edge of the box I was plotting them in. I couldn’t find anyway to get matplotlib to automatically scale the bounding box to accomodate the full size of the modified image. So, I worked on sorting some code to more or less do so. I added a command line switch to flip-flop between the clipped image and the extended image.

Some Code

This will not represent the complete code I wrote/used. It is just for the initial test plot (26) I added to my module. I also added code later in the module to apply the translations (if/as appropriate based on parameters set at the command line) to some of the other plot types. I have not sorted doing this for the plot types attempting to simulate a cycling line size. Mainly because I am plotting lines or markers individually for all the data points. No simple way to do a mass translation.

Let’s start with the code for the basic linear translations. We’ll deal with scaling the bounding box later. I chose to select from 3 possible translation lengths: 72, 108 or 144 points. Though I think in the long run, the value used should in some fashion be determined by the density of the base image. I.E. the more lines on the image, the larger the translation distance. I think you will see why in some of the example images. Though, don’t know how to determine a useable value for image density.

I wanted the base image to be on top, so I am using the zorder= parameter when plotting. Set accordingly. I originally used the selected, random alpha value for the base curve. And, half that for the translated curves. But, I have decided to not have any transparency in the plotted elements. I think that will likely produce a better looking image.

I also didn’t like the result for large line sizes. So I am currently forcing to a smaller line size.

    elif do_plt == 26:
      # 'gnarly' curve, a random or user selected single shape
      # going add translated copies of the curve to the images

      if t_tl:
        m_ttl = get_plt_ttl(shp=splt.shp_nm[su], ld=r_skp, df=drp_f, lw=ln_w)
        fig.suptitle(m_ttl)

      # set up data for plot
      p_lw = 3
      # p_alph = alph
      p_alph = 1
      ax.plot(r_xs, r_ys, lw=p_lw, alpha=p_alph, zorder=9)
      ax.plot(m_xs, m_ys, lw=p_lw, alpha=p_alph, zorder=9)
      ax.plot(m2_xs, m2_ys, lw=p_lw, alpha=p_alph, zorder=9)

      # don't change things if saving to file, or using hardcoded curve data
      if not (t_sv or t_hc):
        tsz = random.randint(1,3)
        a_mlt = 1.25
        if tsz == 1:
          tx = [-72, -72, 72, 72]
          ty = [72, -72, -72, 72]
        elif tsz == 2:
          tx = [-108, -108, 108, 108]
          ty = [108, -108, -108, 108]
          a_mlt = 1.3
        else:
          tx = [-144, -144, 144, 144]
          ty = [144, -144, -144, 144]
          a_mlt = 1.5
        tq = 0

      print(f"DEBUG {do_plt}: translate (c) -> nbr qs 4 @ range(4) => {tx}, {ty} (lw: {ln_w})")

      # this doesn't seem to do anything
      ax.autoscale(True)
      ax2.autoscale(False)

      # t_alph = alph / 2
      t_alph = 1
      for tq in range(4):
        # shift the object over 2 points, and down 2 points
        dx, dy = tx[tq]/72., ty[tq]/72.
        offset = transforms.ScaledTranslation(dx, dy, fig.dpi_scale_trans)
        shadow_transform = ax.transData + offset

        # now plot the same data with our offset transform;
        # use the zorder to make sure we are below the line
        # clip_on=False,
        ax.plot(r_xs, r_ys, lw=p_lw, alpha=t_alph,
                transform=shadow_transform,
                zorder=5)

Some Examples

My apologies for the first two using the same colour scheme.

Shifted 108 points, clipped to original bounding box
gnarly spirograph images expanded through applying 4 symmetrical linear translations
Shifted 144 points, clipped to original bounding box
gnarly spirograph images expanded through applying 4 symmetrical linear translations
Shifted 144 points, clipped to original bounding box
gnarly spirograph images expanded through applying 4 symmetrical linear translations

Expanding the Image Limits

For my first attempt to figure out how much I had to alter the data limits to display the full, translated image I tried using a simple ratio. From previous work I had some idea of the physical dimensions of axes to which I was plotting the image (not the background). So I figured if I used the ratio of the translation distance to the axes box size, I could adjust the data limits by the same ratio. Didn’t really work. Had to add a fudge factor based on the number of points being used for the translation.

Then I started thinking about stuff I had read in the Transformation Tutorial. And decided to convert the point distance (display coordinates?) to data coordinates. Then use that latter value to modify the axes’ data limits. That worked reasonably well except one particular case. For which I make an adjustment.

I did initially approach the use of this transformation incorrectly, but eventually came to grips with the result. The think that took awhile to sort was that the d_pt = ax.transData.inverted().transform(dpi_pt) transformation was giving the distance, in data coordinates, from the bottom right corner of the whole plot (14 x 14 inches), not from the bottom left of the axes to which I was plotting things.

Still don’t know why I get extreme values for the distance in data coordinates. But my cut in half fudge seems to work.

I will include, commented out, my original attempt as well as the current code. t_trx is my command line set flag for using the expanded data limits for the final image.

      if not (t_sv or t_hc):
        tsz = random.randint(1,3)
        a_mlt = 1.25
        if tsz == 1:
          tx = [-72, -72, 72, 72]
          ty = [72, -72, -72, 72]
        elif tsz == 2:
          tx = [-108, -108, 108, 108]
          ty = [108, -108, -108, 108]
          a_mlt = 1.3
        else:
          tx = [-144, -144, 144, 144]
          ty = [144, -144, -144, 144]
          a_mlt = 1.5
        tq = 0

      print(f"DEBUG {do_plt}: translate (c) -> nbr qs 4 @ range(4) => {tx}, {ty} (lw: {ln_w})")

      ax.autoscale(True)
      ax2.autoscale(False)
      if t_trx:
        # !!! trying to find away to fit translated curves to axes space
        xmn26, xmx26 = ax.get_xlim()
        ymn26, ymx26 = ax.get_ylim()
        # ax_sz = fig_sz * wdv
        # xmn26b = xmn26 *(1 + ((tx[2]/72) / ax_sz))
        # xmx26b = xmx26 * (1 + ((tx[2]/72) / ax_sz))
        # ymn26b = ymn26 * (1 + ((ty[0]/72) / ax_sz))
        # ymx26b = ymx26 * (1 + ((ty[0]/72) / ax_sz))
        # if su == 'q':
        #   ymx26 *= 1.1
        # ax.set_xlim(xmn26b*a_mlt, xmx26b*a_mlt)  
        # ax.set_ylim(ymn26b*a_mlt, ymx26b*a_mlt)

        ttx, tty = tx[2], ty[0]
        dpi_pt = (ttx*100/72, tty*100/72)
        d_pt = ax.transData.inverted().transform(dpi_pt)
        # image too small if d_pt[i] greater than the relevant data limit
        if abs(d_pt[0]) > abs(xmn26) and abs(d_pt[1]) > abs(ymn26):
          d_pt = (d_pt[0] / 2, d_pt[1] / 2)
        elif abs(d_pt[0]) > abs(xmn26):
          d_pt = (d_pt[0] / 2, d_pt[1])
        elif abs(d_pt[1]) > abs(ymn26):
          d_pt = (d_pt[0], d_pt[1] / 2)

        print(f"DBG {do_plt}: dpi {dpi_pt} -> {d_pt} in data coordinates?")
        print(f"\tbefore, ax -> x = {ax.get_xlim()}; y = {ax.get_ylim()}")

        ax.set_xlim(xmn26 + d_pt[0], xmx26 - d_pt[0])  
        ax.set_ylim(ymn26 + d_pt[1], ymx26 - d_pt[1])

        print(f"\tafter, ax -> x = {ax.get_xlim()}; y = {ax.get_ylim()}")

Examples with Expanded Data Limits

These examples are the earlier clipped examples with expanded data limits.

Shifted 108 points, data limits increased to limit/prevent clipping
gnarly spirograph images expanded through applying 4 symmetrical linear translations as well as expanded data limits
Shifted 144 points, data limits increased to limit/prevent clipping
gnarly spirograph images expanded through applying 4 symmetrical linear translations as well as expanded data limits
Shifted 144 points, data limits increased to limit/prevent clipping
gnarly spirograph images expanded through applying 4 symmetrical linear translations as well as expanded data limits

For the most part, the full image seems to look considerably better than the clipped version. So, probably worth the effor to sort it out. And, I will likely make that the default behaviour for this style of plot.

A Few More Examples

All of the above plots used the plotting code I used while working on linear translations. The next few have the translation applied to other gnarly style plot types.

I realized that my code for applying linear transformations to other plot types did not set the z order correctly. So fixed that before plotting these further examples.

Shifted 108 points
gnarly spirograph images expanded through applying 4 symmetrical linear translations
Shifted 144 points
gnarly spirograph images expanded through applying 4 symmetrical linear translations
Shifted 144 points
gnarly spirograph images expanded through applying 4 symmetrical linear translations

The following two use the same curve data. But I used to slightly different underlying plots (22 and 23) before applying the transformation. The first plotted the following rows: [8, 6, 4, 2, 0], The second: [7, 5, 3, 1]. It was based on 9 circular wheels.

Shifted 144 points
gnarly spirograph images expanded through applying 4 symmetrical linear translations
Shifted 144 points
gnarly spirograph images expanded through applying 4 symmetrical linear translations

It would seem I am more inclined to save plots with the largest translation distance.

Done m’thinks

I had planned to cover the other plot type I mentioned in the introduction. But, this post is getting plenty long already.

And, after I started it, I spent a few days working on getting some sort of rotational transformation to work. A lot of wasted time. Due to a lack of sufficient knowledge and/or thought, bugs, math I was too lazy to sort, etc. Didn’t keep notes, but I am going to try to recreate my rough ride in another post. (Might even take two to get it done.) Will eventually get to that “paint by numbers” plot style.

Take care. Have fun coding. I am — I think.

Resources