PyQtgraph叠加折线散点图时仅折线更新问题及峰值标注咨询
音频频率分析仪峰值标注问题修复
问题背景
我正在基于参考视频中的脚本开发音频频率分析仪,参考视频链接:https://www.youtube.com/watch?v=RHmTgapLu4s&t=84s
程序基础逻辑:
- 实时采集麦克风输入数据绘制时域波形
- 对波形执行傅里叶变换后,在第二个绘图区持续动态更新频谱结果
目前遇到两个待解决问题:
- 尝试在FFT频谱图上叠加散点图实现峰值高亮标注,但仅首次渲染能生成散点,后续散点始终无法动态更新。经排查峰值坐标数据可正常更新,但调用
self.set_plotdata()方法时仅折线图会刷新,散点图无变化。 - 需要为散点添加对应频率值的数据标签,未找到PyQtgraph中实现该功能的相关文档。
完整原始代码
# -*- coding: utf-8 -*- """ Created on Mon Jun 27 16:35:59 2022 @author: corn- """ import numpy as np from pyqtgraph.Qt import QtGui, QtCore import pyqtgraph as pg import struct import pyaudio from scipy.fftpack import fft from scipy.signal import find_peaks import sys import time class AudioStream(object): def __init__(self): # pyqtgraph stuff pg.setConfigOptions(antialias=True) self.traces = dict() self.app = QtGui.QApplication(sys.argv) self.win = pg.GraphicsWindow(title='Spectrum Analyzer') self.win.setWindowTitle('Spectrum Analyzer') self.win.setGeometry(5, 115, 1910, 1070) #X labels for time domain graph wf_xlabels = [(0, '0'), (2048, '2048'), (4096, '4096')] wf_xlabel2 = 'time' wf_xaxis = pg.AxisItem(orientation='bottom') wf_xaxis.setTicks([wf_xlabels]) wf_xaxis.setLabel(text = wf_xlabel2) #Y labels for time domain graph wf_ylabels = [(0, '0'), (127, '128'), (255, '255')] wf_yaxis = pg.AxisItem(orientation='left') wf_yaxis.setTicks([wf_ylabels]) #X labels for frequency domain graph sp_xlabels = [ (np.log10(10), '10'), (np.log10(100), '100'), (np.log10(1000), '1000'), (np.log10(22050), '22050') ] sp_xlabel2 = 'log frequency' sp_xaxis = pg.AxisItem(orientation='bottom') sp_xaxis.setTicks([sp_xlabels]) sp_xaxis.setLabel(sp_xlabel2) self.waveform = self.win.addPlot( title='WAVEFORM', row=1, col=1, axisItems={'bottom': wf_xaxis, 'left': wf_yaxis}, ) self.spectrum = self.win.addPlot( title='SPECTRUM', row=2, col=1, axisItems={'bottom': sp_xaxis}, ) # pyaudio stuff self.FORMAT = pyaudio.paInt16 self.CHANNELS = 1 self.RATE = 44100 self.CHUNK = 1042 * 4 self.p = pyaudio.PyAudio() self.stream = self.p.open( format=self.FORMAT, channels=self.CHANNELS, rate=self.RATE, input=True, output=True, frames_per_buffer=self.CHUNK, ) # waveform and spectrum x points self.x = np.arange(0, 2 * self.CHUNK, 2) self.f = np.linspace(0, self.RATE / 2, self.CHUNK // 2) def start(self): if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): QtGui.QApplication.instance().exec_() def set_plotdata(self, name, data_x, data_y, peak_x, peak_y): if name in self.traces: self.traces[name].setData(data_x, data_y) else: if name == 'waveform': self.traces[name] = self.waveform.plot(pen='c', width=3) self.waveform.setYRange(0, 255, padding=0) self.waveform.setXRange(0, 2 * self.CHUNK, padding=0.005) if name == 'spectrum': self.traces[name] = self.spectrum.plot(pen='m', width=50) self.spectrum.setLogMode(x=True, y=True) self.spectrum.setYRange(-4, 0, padding=0) self.spectrum.setXRange( np.log10(20), np.log10(self.RATE / 2), padding=0.005) #plot the peak tips self.peakTops = self.spectrum.plot() self.peakTops.setData(peak_x, peak_y, pen=None, symbol='o') def get_peaks(self, name, data_x, data_y): if name == 'spectrum': peak_idx, _ = find_peaks(data_y, height=(0.1)) peak_height = data_y[peak_idx] return (peak_idx, peak_height) def update(self): wf_data = self.stream.read(self.CHUNK) wf_data = struct.unpack(str(2 * self.CHUNK) + 'B', wf_data) wf_data = np.array(wf_data, dtype='b')[::2] + 128 temp_x = 0 temp_y = 0 self.set_plotdata(name='waveform', data_x=self.x, data_y=wf_data, peak_x = temp_x, peak_y = temp_y) sp_data = fft(np.array(wf_data, dtype='int8') - 128) sp_data = np.abs(sp_data[0:int(self.CHUNK / 2)] ) * 2 / (128 * self.CHUNK) [peak_idx, peak_height] = self.get_peaks(name = 'spectrum', data_x = self.f, data_y = sp_data) self.set_plotdata(name='spectrum', data_x=self.f, data_y=sp_data, peak_x = peak_idx, peak_y = peak_height) def animation(self): timer = QtCore.QTimer() timer.timeout.connect(self.update) timer.start(20) self.start() if __name__ == '__main__': audio_app = AudioStream() audio_app.animation()
问题原因
- 散点不更新:
self.peakTops的初始化和数据赋值写在set_plotdata()的else分支中,该分支仅在第一次创建频谱曲线时执行。后续更新时走if name in self.traces分支,仅更新了频谱折线的数据,没有调用self.peakTops.setData()传入新的峰值坐标,所以散点一直停留在首次渲染的状态。 - 坐标不匹配:传给散点的x值是峰值在数组中的索引,不是实际频率值,在开启对数x轴的频谱图上坐标完全错位。
- 频率标签:PyQtgraph没有内置的散点标签接口,需要用
pg.TextItem手动创建、管理文本标注。
修复方案
1. 初始化逻辑调整
在__init__方法中创建完频谱绘图区后,直接初始化峰值散点对象和标签存储列表,不要放在set_plotdata的首次创建分支里:
self.spectrum = self.win.addPlot( title='SPECTRUM', row=2, col=1, axisItems={'bottom': sp_xaxis}, ) # 初始化峰值散点,设置为红色圆点 self.peakTops = self.spectrum.plot(pen=None, symbol='o', symbolBrush='r', symbolSize=8) # 存储峰值标签,方便后续更新时清空旧标签 self.peak_labels = []
2. 同步更新散点数据
修改set_plotdata()方法,在更新频谱折线数据时同步更新峰值散点,同时处理标签的清空和重绘:
def set_plotdata(self, name, data_x, data_y, peak_x, peak_y): if name in self.traces: self.traces[name].setData(data_x, data_y) if name == 'spectrum': # 更新峰值散点数据 self.peakTops.setData(peak_x, peak_y) # 清空上一帧的所有标签 for label in self.peak_labels: self.spectrum.removeItem(label) self.peak_labels.clear() # 为每个新峰值添加频率标签 for freq, amp in zip(peak_x, peak_y): # 对数轴下坐标要取log10后赋值 text_label = pg.TextItem(f"{freq:.0f} Hz", color=(255,255,255), anchor=(0.5, 1.2)) text_label.setPos(np.log10(freq), np.log10(amp)) self.spectrum.addItem(text_label) self.peak_labels.append(text_label) else: if name == 'waveform': self.traces[name] = self.waveform.plot(pen='c', width=3) self.waveform.setYRange(0, 255, padding=0) self.waveform.setXRange(0, 2 * self.CHUNK, padding=0.005) if name == 'spectrum': # 原代码pen宽度设为50会完全挡住峰值点,改为3 self.traces[name] = self.spectrum.plot(pen='m', width=3) self.spectrum.setLogMode(x=True, y=True) self.spectrum.setYRange(-4, 0, padding=0) self.spectrum.setXRange( np.log10(20), np.log10(self.RATE / 2), padding=0.005)
3. 修正峰值坐标传值
修改update()方法中峰值传参逻辑,把数组索引转为实际频率值再传入:
[peak_idx, peak_height] = self.get_peaks(name = 'spectrum', data_x = self.f, data_y = sp_data) # 索引转实际频率值 peak_freq = self.f[peak_idx] self.set_plotdata(name='spectrum', data_x=self.f, data_y=sp_data, peak_x = peak_freq, peak_y = peak_height)
修改后运行即可看到峰值点随音频实时更新,每个峰值上方会显示对应频率值。
内容的提问来源于stack exchange,提问作者user18814028
相关产品推荐
相关产品推荐

