How do I plot a spectrogram the same way that pylab's specgram() does?


Question

In Pylab, the specgram() function creates a spectrogram for a given list of amplitudes and automatically creates a window for the spectrogram.

I would like to generate the spectrogram (instantaneous power is given by Pxx), modify it by running an edge detector on it, and then plot the result.

(Pxx, freqs, bins, im) = pylab.specgram( self.data, Fs=self.rate, ...... )

The problem is that whenever I try to plot the modified Pxx using imshow or even NonUniformImage, I run into the error message below.

/opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/matplotlib/image.py:336: UserWarning: Images are not supported on non-linear axes. warnings.warn("Images are not supported on non-linear axes.")

For example, a part of the code I'm working on right is below.

    # how many instantaneous spectra did we calculate
    (numBins, numSpectra) = Pxx.shape

    # how many seconds in entire audio recording
    numSeconds = float(self.data.size) / self.rate


    ax = fig.add_subplot(212)
    im = NonUniformImage(ax, interpolation='bilinear')

    x = np.arange(0, numSpectra)
    y = np.arange(0, numBins)
    z = Pxx
    im.set_data(x, y, z)
    ax.images.append(im) 
    ax.set_xlim(0, numSpectra)
    ax.set_ylim(0, numBins)
    ax.set_yscale('symlog') # see http://matplotlib.org/api/axes_api.html#matplotlib.axes.Axes.set_yscale
    ax.set_title('Spectrogram 2')

Actual Question

How do you plot image-like data with a logarithmic y axis with matplotlib/pylab?

1
12
4/12/2013 2:12:49 AM

Accepted Answer

Use pcolor or pcolormesh. pcolormesh is much faster, but is limited to rectilinear grids, where as pcolor can handle arbitrary shaped cells. specgram uses pcolormesh, if I recall correctly. (It uses imshow.)

As a quick example:

import numpy as np
import matplotlib.pyplot as plt

z = np.random.random((11,11))
x, y = np.mgrid[:11, :11]

fig, ax = plt.subplots()
ax.set_yscale('symlog')
ax.pcolormesh(x, y, z)
plt.show()

enter image description here

The differences you're seeing are due to plotting the "raw" values that specgram returns. What specgram actually plots is a scaled version.

import matplotlib.pyplot as plt
import numpy as np

x = np.cumsum(np.random.random(1000) - 0.5)

fig, (ax1, ax2) = plt.subplots(nrows=2)
data, freqs, bins, im = ax1.specgram(x)
ax1.axis('tight')

# "specgram" actually plots 10 * log10(data)...
ax2.pcolormesh(bins, freqs, 10 * np.log10(data))
ax2.axis('tight')

plt.show()

enter image description here

Notice that when we plot things using pcolormesh, there's no interpolation. (That's part of the point of pcolormesh--it's just vector rectangles instead of an image.)

If you want things on a log scale, you can use pcolormesh with it:

import matplotlib.pyplot as plt
import numpy as np

x = np.cumsum(np.random.random(1000) - 0.5)

fig, (ax1, ax2) = plt.subplots(nrows=2)
data, freqs, bins, im = ax1.specgram(x)
ax1.axis('tight')

# We need to explictly set the linear threshold in this case...
# Ideally you should calculate this from your bin size...
ax2.set_yscale('symlog', linthreshy=0.01)

ax2.pcolormesh(bins, freqs, 10 * np.log10(data))
ax2.axis('tight')

plt.show()

enter image description here

19
4/12/2013 4:07:54 AM

Just to add to Joe's answer... I was getting small differences between the visual output of specgram compared to pcolormesh (as noisygecko also was) that were bugging me.

Turns out that if you pass frequency and time bins returned from specgram to pcolormesh, it treats these values as values on which to centre the rectangles rather than edges of them.

A bit of fiddling gets them to allign better (though still not 100% perfect). The colours are identical now also.

x = np.cumsum(np.random.random(1024) - 0.2)
overlap_frac = 0
plt.subplot(3,1,1)
data, freqs, bins, im = pylab.specgram(x, NFFT=128, Fs=44100, noverlap = 128*overlap_frac, cmap='plasma')
plt.title("specgram plot")

plt.subplot(3,1,2)
plt.pcolormesh(bins, freqs, 20 * np.log10(data), cmap='plasma')
plt.title("pcolormesh no adj.")

# bins actually returns middle value of each chunk
# so need to add an extra element at zero, and then add first to all
bins = bins+(bins[0]*(1-overlap_frac))
bins = np.concatenate((np.zeros(1),bins))
max_freq = freqs.max()
diff = (max_freq/freqs.shape[0]) - (max_freq/(freqs.shape[0]-1))
temp_vec = np.arange(freqs.shape[0])
freqs = freqs+(temp_vec*diff)
freqs = np.concatenate((freqs,np.ones(1)*max_freq))

plt.subplot(3,1,3)
plt.pcolormesh(bins, freqs, 20 * np.log10(data), cmap='plasma')
plt.title("pcolormesh post adj.")

spectrogram_explain_v01


Licensed under: CC-BY-SA with attribution
Not affiliated with: Stack Overflow
Icon