如何在x轴上以正确的频率绘制信号的FFT?

时间:2018-03-22 22:33:12

标签: python numpy matplotlib scipy

我可以使用Matplotlib的plt.psd()方法绘制从RTL-SDR收到的信号,结果如下: enter image description here 我试图实现的最终目标是检索高于某一功率水平的所有峰值的坐标,例如-20。当我从时域接收信号时,我必须先将它们转换为频域,这可以通过以下代码完成:

signal = []
sdr = RtlSdr()
sdr.sample_rate = 2.8e
sdr.center_freq = 434.42e6

samples = sdr.read_samples(1024*1024)
signal.append(samples)

from scipy.fftpack import fft, fftfreq
window = np.hanning(len(signal[0]))
sig_fft = fft(signal[0]*window)
power = 20*np.log10(np.abs(sig_fft))
sample_freq = fftfreq(signal[0].size, sdr.sample_rate/signal[0].size)
plt.figure(figsize=(9.84, 3.94))
plt.plot(sample_freq, power)
plt.xlabel("Frequency (MHz)")
plt.ylabel("Relative power (dB)")
plt.show()

idx = np.argmax(np.abs(sig_fft))
freq = sample_freq[idx]
peak_freq = abs(freq)
print(peak_freq)

此代码生成以下图表: enter image description here 我未能实现的是,首先,摆脱所有的噪音,并在psd()情节中仅绘制一条细线。其次,要在x轴上显示正确的频率值。

所以,我的问题是:

  • 我是否以错误的方式应用Hanning窗口,或者我还能如何摆脱所有噪音?
  • 如何在我的情节的x轴上获得正确的频率值?

[编辑]

以下是我使用welch()方法的尝试:

from scipy.signal import welch
sample_freq, power = welch(signal[0], sdr.sample_rate, window="hamming")
plt.figure(figsize=(9.84, 3.94))
plt.semilogy(sample_freq, power)
plt.xlabel("Frequency (MHz)")
plt.ylabel("Relative power (dB)")
plt.show()

结果: enter image description here 这样我就无法在任一轴上获得正确的值。此外,中心峰的一部分缺失,我完全不理解,并且该图具有连接信号两端的恼人线。

[编辑2]

根据Francois Gosselin的回答:以下代码产生的结果与mpl.psd()方法产生的结果最相似:

from scipy.signal import welch
corr = 1.5
sample_freq, power = welch(signal[0], fs=sdr.sample_rate, window="hann", nperseg=2048, scaling="spectrum")
sample_freq = fftshift(sample_freq)
power = fftshift(power)/corr
print(sum(power))
plt.figure(figsize=(9.84, 3.94))
plt.semilogy(sample_freq, power)
plt.xlabel("Frequency (MHz)")
plt.ylabel("Relative power (dB)")
plt.show()

enter image description here 现在,唯一剩下的就是弄清楚如何在相应的轴上获得正确的频率(以MHz为单位)和功率(以dB为单位)......

[编辑3]

使用编辑2中的代码,但使用以下代码而不是plt.semilogy(...),

plt.plot((sample_freq+sdr.center_freq)/1e6, np.log10(power))

我得到: enter image description here 没有必要添加一些"额外的计算"但是,应该在plot()方法中使用它吗? welch()方法不应该返回正确的功率水平吗?

[编辑4]

在尝试了你编写的所有内容之后,我发现抓取plt.psd()方法返回的频率和功率数组是最简单的解决方案,无论是理解还是在我的代码中使用:

Pxx, freqs = plt.psd(signals[0], NFFT=2048, Fs=sdr.sample_rate/1e6, Fc=sdr.center_freq/1e6, scale_by_freq=True, color="green")

power_lvls = 10*log10(Pxx/(sdr.sample_rate/1e6))
plt.plot(freqs, power_lvls)
plt.show()

结果情节: enter image description here 有趣的是,plt.psd()方法对于自己的绘图似乎使用的功率水平与从返回的Pxx阵列计算它们后得到的功率水平略有不同。绿色和红色信号是使用plt.psd()绘制来自同一源的两个不同信号的结果,而蓝色信号是通过使用plt.psd()返回的数组提供简单的plot()方法生成的( Pxx除以sample_rate,log10应用于结果。

[编辑4的小补充]

我刚看到将计算出的power_lvls数组中的值除以1.1大致将信号置于与plt.psd()绘制的相同的功率水平上:

plt.plot(freqs, power_lvls/1.1)

现在,可能是什么原因?默认情况下,plt.psd()使用Hanning窗口,其校正值为1.5,或者我认为是这样。

..........

使用以下两行我现在还可以检索多个峰的坐标:

indexes = peakutils.peak.indexes(np.array(power_lvls), thres=0.6/max(power_lvls), min_dist=120)
print("\nX: {}\n\nY: {}\n".format(freqs_1[indexes], np.array(power_lvls)[indexes]))

enter image description here 如果您将这些值与上图中的蓝色信号进行比较,您会发现它们非常准确。

1 个答案:

答案 0 :(得分:4)

我认为你的问题来自于整个信号的FFT,导致频率分辨率过高而导致你看到的噪音。 Matplotlib psd在较短的重叠块中打破信号,计算每个块的FFT和平均值。 Scipy信号中的功能welch也可以做到这一点。您将得到一个以0 Hz为中心的频谱。然后,您可以通过将中心频率添加到频率向量来偏移返回的频率向量以获得原始频率范围。

使用welch时,返回的频率和功率向量不按升序频率排序。在绘图之前,您必须先移出输出。此外,您通过welch的采样频率必须是浮点数。确保使用scaling =" spectrum"获得功率而不是功率密度的选项。要获得正确的功率值,您还需要调整功率以考虑窗口效应。对于hann窗口,您需要除以1.5。这是一个有效的例子:

from scipy.signal import welch
from numpy.fft import fftshift, fft
from numpy import arange, exp, pi, mean
import matplotlib.pyplot as plt

#generate a 1000 Hz complex tone singnal
sample_rate = 48000. #sample rate must be a float
t=arange(1024*1024)/sample_rate
signal=exp(1j*2000*pi*t)
#amplitude correction factor
corr=1.5

#calculate the psd with welch
sample_freq, power = welch(signal, fs=sample_rate, window="hann", nperseg=256, noverlap=128, scaling='spectrum')

#fftshift the output 
sample_freq=fftshift(sample_freq)
power=fftshift(power)/corr

#check that the power sum is right
print sum(power)

plt.figure(figsize=(9.84, 3.94))
plt.plot(sample_freq, power)
plt.xlabel("Frequency (MHz)")
plt.ylabel("Relative power (dB)")  
plt.show()

修改

我发现有三个原因可以解释为什么你没有得到与Matplotlib PSD功能相同的振幅。顺便说一句,如果你看一下doc,Matplotlib PSD有三个返回参数:PSD,频率向量和线对象,所以如果你想要Matplotlib函数得到的相同的PSD,你可以在那里获取数据。但我建议你进一步阅读,以确保你知道自己在做什么(仅仅因为Matplotlib向你返回一个值,并不意味着它是对的,或者它是你需要的价值)。

  1. 计算分贝的方式是错误的。首先,以分贝绘制与在对数轴上绘图不同。分贝标度是对数的,而您在对数图上绘制的数据仍然是线性的,因此显然您不会获得相同的值。分贝比例是相对的,这意味着您将您的值与参考值进行比较。计算分贝的方法取决于您正在处理的单位类型。在主要物理单位(伏特,帕斯卡,米/秒等)的情况下,如果我们假设单位为伏特,则可以这样做:20 * log10(V / Vref),其中Vref是参考值。现在,如果你处理平方单位:能量,强度,功率密度等,你可以:10 * log10(P / Pref),其中P是平方数量。这是因为线性域中的平方等效于对数域中的乘以2。无论如何,在您的情况下,10 * log10表格适用。至于参考值,它是任意的,并且通常是特定领域的惯例。例如,声学中声压的国际参考是2e-5 Pa。由于参考是任意的,当你没有任何标准值可以比较时,你可以将它设置为1。

    < / LI>
  2. 其次,Matplotlib psd的默认设置是&#34; scale_by_freq&#34;选项设置为true。这可以通过Hz或Mhz为您提供功率。另一方面,welch中的频谱选项为您提供每频段的功率。因此,在Matplotlib中,功率除以频率范围(2.8 MHz),而在其中它除以频带数(2048)。如果我取两者的分贝比,我得到10 * log10(2048 / 2.8)= 28.6 dB,这看起来非常接近你得到的差异。

  3. 最后,您使用的修正系数会因您想要达到的目标而有所不同。首先需要校正因子的原因是由于窗口函数引入的信号的修改。窗口有效地降低了信号的总能量。您需要乘以校正以获得正确的能量。窗口化还通过将能量扩散到相邻频带来影响频谱。其结果是修改了信号中峰值的高度。因此,如果您想要正确的峰高,则必须使用校正因子,而如果您想要正确的能量,则使用另一个。汉窗的两者之比为1.5。通常,幅度校正(正确峰值高度)用于显示目的,而当总能量很重要时使用能量校正。我认为Matplotlib PSD是幅度校正的,而我给你的例子是能量校正的,所以那里可能有1.5倍。

  4. 因为您想要做的是在光谱中找到峰值,也许整个幅度事物毕竟不是那么重要。峰值发现通常适用于频段之间的相对差异。