Qt尽管非常强大,但对时频分析的控件支持不是很好。以前主要靠Qwt的Spectrogram来做,但眼瞅着Qt Charts 开源后,Qwt的更新越来越少,真的怕那天它凉凉了。Qt Charts 美工要比Qwt更加摩登,可是显然背后的行业背景不是信号处理,其距离数据分析更进一步(股票啦、人口啦等等),支持二维时频不是很好。经过一段时间尝试,找到了至少三种办法,这里做一个记录。
这是最简单的方法,参照例子 Qt Quick 2 Spectrogram Example。其主要原理是利用 Surface 例子中的框架,用正摄投影(不是透视投影)把照相机放在在正上方,显示出一个二维的着色图形。
这个方法显然有些小题大做,原因:
GNU-Radio下的gqrx是一款基于Qt的SDR接收机,对时频分析做的很棒哦!首先,从GitHub 里checkout,找到文件夹 qtgui,里面的plotter就是时频图。非常值得肯定的是,这个开源项目中,时频图没有和特定功能耦合起来,可以直接提取出使用。
把时频图相关控件提取出来后,可以方便的进行测试。这个控件做的非常棒,功能很多,接口非常清晰,可以慢慢研究。提取出的纯控件参考我的资源,这里给出提取后的界面。
也使用Udp封装了一个接口,参考
我的临时Git仓库
上述提取的控件虽然强大,但是还是带有很多不必要的资源以及结构定义,前前后后拖了一堆小h,cpp,rc,icon文件,甚是烦恼。既然Qt Charts显示波形已经很棒了,自带橡皮筋缩放等功能,何不结合它撸一个呢?
主要的思路很简单,每刷新一次Qt-Charts,就把当前的波形作为像素,增补到一个QImage中,实现时频图显示。
FFT后的幅度谱结果一般是dB或者dBm,取决于归一化的方法。假设AD时量化范围为正负1伏特映射到正负2048整形,而后做16384点FFT,则可以在FFT后,把16384、2048这些量化值除掉(或对数减去),换算成浮点的分贝数。正常情况下,该值都是一个负数,范围在-200~0之间。
时频显示控件都会有一个动态范围,我们可以通过线性映射,把dB数映射为RGB
const double vii = (v-m_minBound)/(m_maxBound-m_minBound);
int red = 0,green = 0,blue = 0;
if (vii>=0 && vii<0.25)
blue = vii/0.25*255;
else if (vii>=0.25 && vii<0.5 )
{
blue = (0.5-vii)/0.25*255;
red = (vii-0.25)/0.25*255;
}
else if (vii>=0.5 && vii<0.75 )
{
red = 255;
green = (vii-0.5)/0.25*255;
}
else if (vii>=0.75 && vii<1)
{
red = 255;
green = 255;
blue = (vii-0.75)/0.25*255;
}
else if (vii>=1)
red = 255, blue = 255, green = 255;
取不同的映射色彩,即可得到不同的风格,上述风格只是例子。
当QtCharts的视图被用户缩放后,波形的显示范围也会发生变化。此时,要对数据的范围进行调整。比如,16384点FFT,由于QtCharts波形控件的范围目前只显示3451~4956点,我们的时频图也要同步上这个变化。
同步变化的方法就是获取QtChart当前的视图参数:
//...QChart * ca;
//...QValueAxis * axx;
//...QVector data;//16384 fft
//获得当前视图的X坐标范围
double dmin = axx->min();
double dmax = axx->max();
//获得当前视图的屏幕坐标范围
QRectF r = ca->plotArea();
//推入一行像素。
apectroWidget->append_data(
data,
r.left(),
r.right(),
dmin,
dmax
);
//...
class SpectroWidget{...
//...
QImage m_image;
};
//...
void SpectroWidget::append_data(QVector<double> vec_data,
double left,
double right,
double left_x,
double right_x)
{
const int width = m_image.width();
const int height = m_image.height();
const int scsize = m_image.bytesPerLine();
//当前视图下走一行
for (int l = height-1;l>0;--l)
memcpy(m_image.scanLine(l),m_image.scanLine(l-1),scsize);
const int datasz = vec_data.size();
for (int r = 0;r<width;++r)
{
if (r<left || r>right)
continue;
//计算当前点对应的原始数据位置
int from = (r-left)/(right-left)*(right_x-left_x) + left_x+.5;
//归一化幅度到0-1
const double v = (from>=0 && from <datasz)?vec_data[from]:m_minBound;
const double vii = (v-m_minBound)/(m_maxBound-m_minBound);
//从0-1归一化幅度到彩色
qRgb pixcolor = colorMap(style,vii);
//设置像素
m_image.setPixel(r,0,pixcolor);
}
update();
}
上述步骤比较简化,X坐标直接采用的是点自然下标0-16383。若根据采样率和中心频率映射到实际Hz单位,还要加上一个线性变换。通过上述步骤,即可实现简易的实时时频控件,且刷新性能很棒。
例子参考这里。
如果您正在以linux或者msys2为开发环境,继续使用Qwt也是很棒的选择。在类Unix环境中,大部分库的安装都是非常方便的,有各种apt\pacman\yum之类的可用。但由于Qwt是第三方控件,要考虑到windows下的兼容性——为每个Qt版本(其实是Qt主次版本+编译器)都构建一次Qwt是非常繁琐的。
离线分析控件需要基于文件进行更加细致的控制,如时间尺度的改变、回溯。目前尚未有非常棒的开源离线时频分析控件,如果哪位大神知道的,恳请分享!