5

I'm writing code which displays features matches between images. The code runs fairly slow at the moment. I have some ideas on how to speed it up, but I'm not 100% comfortable with matplotlib yet or how its working behind the scenes.

The basic structure of the code is: (I'm leaving things out to make it more readable)

from matplotlib.patches import Rectangle, Circle, Ellipse
import matplotlib.gridspec as gridspec
from matplotlib.transforms import Affine2D
from scipy.linalg import inv, sqrtm
import matplotlib.pyplot as plt
import numpy as np
  • Add a list images. Each image gets its own axes: ax, and remembers ax.transData

    gs = gridspec.GridSpec( nr, nc )
    for i in range(num_images):
         dm.ax_list[i] = plt.subplot(gs[i])
         dm.ax_list[i].imshow( img_list[i])
         transData_list[i] = dm.ax_list[i].transData
    
  • Visualize the feature representation as Ellipses

    for i in range(num_chips):
         axi =  chips[i].axi 
         ax  =  dm.ax_list[axi]
         transData = dm.transData_list[axi]
         chip_feats = chips[i].features
         for feat in chip_feats:
             (x,y,a,c,d) = feat
             A = numpy.array( [ ( a, 0, 0 ) ,
                                      ( c, d, 0 ) ,
                                      ( 0, 0, 1 ) ] , dtype=np.float64)
             EllShape = Affine2D( numpy.array(sqrtm( inv(A) ), dtype=np.float64) )
             transEll  = EllShape.translate(x,y)
             unitCirc = Circle((0,0),1,transform=transEll+transData)
             ax.add_patch(unitCirc)
    

I've used RunSnakeRun to profile the code, and all I really gather from that is it is taking a long time to draw everything. The basic idea that I had when I learned about the transformation in matplotlib was to draw each image in its own coordinates, and then maintain several transformations so I could do cool things with them later, but I suspect that it's not going to scale well.

The actual output of the draw looks like this:

The figure takes about 4 seconds to redraw when I resize it, and I'm going to be wanting to pan/zoom.

I was adding two patches for each feature, and about (300 features per image) so I could see an outline and some transparency. So, there's obviously overhead for that. But its also relatively slow even without any ellipses.

I also need to write some code to put lines in between the matching features, but now I'm not so sure using multiple axes is a very good idea, especially when this is a relatively small dataset.

So, for more concrete questions:

  • Would it be more efficient to plot Ellipses vs Transformed Circles? What is the overhead of using matplotlib transformations?
  • Is there a way to combine a group of patches so they transform together or more efficiently?
  • Would it be more efficient to pack everything into a single axes? Can the paradigm of transforms still be used if I do that? Or is the transform the main culprit here?
  • Is there a quick way to do the sqrtm( inv( A ) ) on a list of matrices A? Or is it just as well that I have them in a for loop?
  • Should I switch to something like pyqtgraph? I'm not planning on their being any animation beyond pan and zoom. ( maybe in the future I'll want to embed these into a interactive graph )

EDITS:

I've been able to increase drawing efficiency by computing the form of the square root inverted matrix by hand. Its a pretty big speed up too.

In the above code:

 A = numpy.array( [ ( a, 0, 0 ) ,
                          ( c, d, 0 ) ,
                          ( 0, 0, 1 ) ] , dtype=np.float64)
 EllShape = Affine2D( numpy.array(sqrtm( inv(A) ), dtype=np.float64) )

is replaced by

 EllShape = Affine2D([\
 ( 1/sqrt(a),         0, 0),\
 ((c/sqrt(a) - c/sqrt(d))/(a - d), 1/sqrt(d), 0),\
 (         0,         0, 1)])

I found some interesting timing results too:

  num_to_run = 100000
  all_setup  = ''' import numpy as np ; from scipy.linalg import sqrtm ; from numpy.linalg import inv ; from numpy import sqrt
  a=.1 ; c=43.2 ; d=32.343'''

  timeit( \
  'sqrtm(inv(np.array([ ( a, 0, 0 ) , ( c, d, 0 ) , ( 0, 0, 1 ) ])))',\
  setup=all_setup, number=num_to_run)
   >> 22.2588094075 #(Matlab reports 8 seconds for this run) 

  timeit(\
  '[ (1/sqrt(a), 0, 0), ((c/sqrt(a) - c/sqrt(d))/(a - d), 1/sqrt(d), 0), (0, 0, 1) ]',\
  setup=all_setup,  number=num_to_run)
  >> 1.10265190941 #(Matlab reports .1 seconds for this run) 

EDIT 2

Ive gotten the ellipses to be computed and drawn very quickly (in about a second, I didn't profile it) using a PatchCollection and some manual calculations. The only drawback is I can't seem to set the fill of the ellipses to be false

 from matplotlib.collections import PatchCollection
 ell_list = []
 for i in range(num_chips):
     axi =  chips[i].axi 
     ax  =  dm.ax_list[axi]
     transData = dm.transData_list[axi]
     chip_feats = chips[i].features
     for feat in chip_feats:
         (x,y,a,c,d) = feat
         EllShape = Affine2D([\
            ( 1/sqrt(a),         0, x),\
            ((c/sqrt(a) - c/sqrt(d))/(a - d), 1/sqrt(d), y),\
            (         0,         0, 1)])
         unitCirc = Circle((0,0),1,transform=EllShape)
         ell_list = [unitCirc] + ell_list
    ellipses = PatchCollection(ell_list)
    ellipses.set_color([1,1,1])
    ellipses.face_color('none') #'none' gives no fill, while None will default to [0,0,1]
    ellipses.set_alpha(.05)
    ellipses.set_transformation(transData)
    ax.add_collection(ellipses)
ali_m
  • 71,714
  • 23
  • 223
  • 298
Erotemic
  • 4,806
  • 4
  • 39
  • 80
  • do you need to be able to modify the patches independently after you have drawn them? – tacaswell Feb 22 '13 at 21:34
  • The ellipses will not need to change shape once they are drawn on the plot. They may scale or transform with the plot, but not independently of one another. I'm transforming unit circles to ellipses because thats easier than trying to get the xscale, yscale, and rotation out of the matrix. – Erotemic Feb 22 '13 at 22:57
  • 3
    You might be able to put all the ellipses into a patch collection which may speed up the drawing as well. – tacaswell Feb 25 '13 at 15:52
  • Ah, those look like they are the answer. My google-fu must be off. – Erotemic Mar 02 '13 at 19:38
  • If you get it to work, could you write up an answer? – tacaswell Mar 02 '13 at 19:57
  • I will do so, and I did get it to work except for one thing. I can't seem to plot the ellipses without any fill. I'd like them to just show the edges, but a patch collection doesn't seem to have that property. – Erotemic Mar 04 '13 at 01:29
  • 1
    maybe try `PathCollection` instead ? It is the same idea but with paths instead of patches. Also try adding the kwarg `match_orginal=True` (http://stackoverflow.com/questions/14492241/why-is-matplotlib-patchcollection-messing-with-color-of-the-patches/14493391#14493391) – tacaswell Mar 04 '13 at 01:33
  • I was playing with PathCollection (my dyslexia is killing me switching between Path and Patch), and if I can get a unit circle drawn and I can apply an affine transformation to it, then I think it should work, as it doesn't seem to inherently fill the space. But I keep getting a "Type Error: Invalid affine transformation matrix" when trying the dummy transform: Affine2D([(2, 0, 0), (2, 1, 0), (0, 0, 1)]) – Erotemic Mar 04 '13 at 02:02
  • 1
    I figured out an easier way. You can set the facecolor of the patch collection to the string: 'none'. (Note, it doesn't work if you use None, then it just defaults) – Erotemic Mar 04 '13 at 02:08
  • 1
    yes, `'none'` is a string object and `None` is the `None` object. Sorry, I should have thought through my earlier comment a bit more carefully. – tacaswell Mar 04 '13 at 03:18
  • 2
    Can you post your last comment as a solution, as you seemed to have solved your own problem? – tacaswell Apr 07 '13 at 05:30
  • Agree with @tcaswell - if you have a solution, please post it as an answer. – pelson Oct 09 '13 at 08:27

1 Answers1

0

I've been able to increase drawing efficiency by computing the form of the square root inverted matrix by hand. Its a pretty big speed up too.

In the above code:

 A = numpy.array( [ ( a, 0, 0 ) ,
                          ( c, d, 0 ) ,
                          ( 0, 0, 1 ) ] , dtype=np.float64)
 EllShape = Affine2D( numpy.array(sqrtm( inv(A) ), dtype=np.float64) )

is replaced by

 EllShape = Affine2D([\
 ( 1/sqrt(a),         0, 0),\
 ((c/sqrt(a) - c/sqrt(d))/(a - d), 1/sqrt(d), 0),\
 (         0,         0, 1)])

I found some interesting timing results too:

  num_to_run = 100000
  all_setup  = ''' import numpy as np ; from scipy.linalg import sqrtm ; from numpy.linalg import inv ; from numpy import sqrt
  a=.1 ; c=43.2 ; d=32.343'''

  timeit( \
  'sqrtm(inv(np.array([ ( a, 0, 0 ) , ( c, d, 0 ) , ( 0, 0, 1 ) ])))',\
  setup=all_setup, number=num_to_run)
   >> 22.2588094075 #(Matlab reports 8 seconds for this run) 

  timeit(\
  '[ (1/sqrt(a), 0, 0), ((c/sqrt(a) - c/sqrt(d))/(a - d), 1/sqrt(d), 0), (0, 0, 1) ]',\
  setup=all_setup,  number=num_to_run)
  >> 1.10265190941 #(Matlab reports .1 seconds for this run) 

EDIT 2

Ive gotten the ellipses to be computed and drawn very quickly (in about a second, I didn't profile it) using a PatchCollection and some manual calculations. The only drawback is I can't seem to set the fill of the ellipses to be false

 from matplotlib.collections import PatchCollection
 ell_list = []
 for i in range(num_chips):
     axi =  chips[i].axi 
     ax  =  dm.ax_list[axi]
     transData = dm.transData_list[axi]
     chip_feats = chips[i].features
     for feat in chip_feats:
         (x,y,a,c,d) = feat
         EllShape = Affine2D([\
            ( 1/sqrt(a),         0, x),\
            ((c/sqrt(a) - c/sqrt(d))/(a - d), 1/sqrt(d), y),\
            (         0,         0, 1)])
         unitCirc = Circle((0,0),1,transform=EllShape)
         ell_list = [unitCirc] + ell_list
    ellipses = PatchCollection(ell_list)
    ellipses.set_color([1,1,1])
    ellipses.face_color('none') #'none' gives no fill, while None will default to [0,0,1]
    ellipses.set_alpha(.05)
    ellipses.set_transformation(transData)
    ax.add_collection(ellipses)
Erotemic
  • 4,806
  • 4
  • 39
  • 80