views:

384

answers:

3

How can I plot an 2D array as an image with Matplotlib having the y scale relative to the power of two of the y value?

For instance the first row of my array will have a height in the image of 1, the second row will have a height of 4, etc. (units are irrelevant) It's not simple to explain with words so look at this image please (that's the kind of result I want):

alt text

As you can see the first row is 2 times smaller that the upper one, and so on.

For those interested in why I am trying to do this:

I have a pretty big array (10, 700000) of floats, representing the discrete wavelet transform coefficients of a sound file. I am trying to plot the scalogram using those coefficients. I could copy the array x times until I get the desired image row size but the memory cannot hold so much information...

+2  A: 

You can look at matplotlib.image.NonUniformImage. But that only assists with having nonuniform axis - I don't think you're going to be able to plot adaptively like you want to (I think each point in the image is always going to have the same area - so you are going to have to have the wider rows multiple times). Is there any reason you need to plot the full array? Obviously the full detail isn't going to show up in any plot - so I would suggest heavily downsampling the original matrix so you can copy rows as required to get the image without running out of memory.

thrope
Yes I tought of downsampling but occasionally I have to zoom pretty deep on the last details coefficients and the precision loss is often too high.I'll look into NonUniformImage anyway, thanks for the tip I wasn't aware of it.
attwad
I think that this would work for drawing the image that the original poster wants, and that this would more efficient than duplicating "pixels" through a duplication of rows. Getting the corresponding axis ticks is still an open question, though.
EOL
+5  A: 

Have you tried to transform the axis? For example:

ax = subplot(111)
ax.yaxis.set_ticks([0, 2, 4, 8])
imshow(data)

This means there must be gaps in the data for the non-existent coordinates, unless there is a way to provide a transform function instead of just lists (never tried).

Edit:

I admit it was just a lead, not a complete solution. Here is what I meant in more details.

Let's assume you have your data in an array, a. You can use a transform like this one:

class arr(object):
    @staticmethod
    def mylog2(x):
        lx = 0
        while x > 1:
            x >>= 1
            lx += 1
        return lx
    def __init__(self, array):
        self.array = array
    def __getitem__(self, index):
        return self.array[arr.mylog2(index+1)]
    def __len__(self):
        return 1 << len(self.array)

Basically it will transform the first coordinate of an array or list with the mylog2 function (that you can transform as you wish - it's home-made as a simplification of log2). The advantage is, you can re-use that for another transform should you need it, and you can easily control it too.

Then map your array to this one, which doesn't make a copy but a local reference in the instance:

b = arr(a)

Now you can display it, for example:

ax = subplot(111)
ax.yaxis.set_ticks([16, 8, 4, 2, 1, 0])
axis([-0.5, 4.5, 31.5, 0.5])
imshow(b, interpolation="nearest")

Here is a sample (with an array containing random values):

alt text

RedGlyph
-1: selecting what ticks appear on the y axis does not solve the original question. With set_ticks(), the y axis remains linear, and imshow() still draws the array linearly. The original poster wants "variable size, rectangular pixels".
EOL
That's why I say the data had to be adapted, it's still worth mentioning for the axes in themselves.
RedGlyph
@EOL: ... and now with the correct plot as well. I understand what you meant but sometimes putting pieces together does help ;-)
RedGlyph
+1: Very interesting, with all the pieces! :) I'd like to note that this solution makes Matplotlib plot lots of little "pixels" (many more than the number of points in the original array), where as thrope's plots only as many "pixels" as they are points in the array. I'd be curious to see timing tests! :)
EOL
Yes, it's a bit of hack to fool Matplotlib, I'd rather it were native (maybe it is, just well-hidden...). In any case there will be much more accesses to the array and as you rightly say, it won't be optimal. If it is too slow and thrope's solution finally proves to be stuck to constant sizes, your idea may be the solution.
RedGlyph
+1  A: 

If you want both to be able to zoom and save memory, you could do the drawing "by hand". Matplotlib allows you to draw rectangles (they would be your "rectangular pixels"):

from matplotlib import patches
axes = subplot(111)
axes.add_patch(patches.Rectangle((0.2, 0.2), 0.5, 0.5))

Note that the extents of the axes are not set by add_patch(), but you can set them yourself to the values you want (axes.set_xlim,…).

PS: I looks to me like thrope's response (matplotlib.image.NonUniformImage) can actually do what you want, in a simpler way that the "manual" method described here!

EOL
+1 for an efficient alternative, even if it looks like a little more work it would speed up rendering (as discussed in other solution).
RedGlyph
@RedGlyph Thanks! I would guess that thrope's solution does draw rectangles like this (with the `nearest` neighbor option), but directly through Matplotlib.
EOL