【opencv】dnn示例-speech_recognition.cpp 使用DNN模块结合音频信号处理技术实现的英文语音识别...

发布于:2024-04-19 ⋅ 阅读:(24) ⋅ 点赞:(0)

模型下载地址:

https://drive.google.com/drive/folders/1wLtxyao4ItAg8tt4Sb63zt6qXzhcQoR6

终端输出:(audio6.mp3 、audio10.mp3)

[ERROR:0@0.002] global cap_ffmpeg_impl.hpp:1112 open VIDEOIO/FFMPEG: unsupported parameters in .open(), see logger INFO channel for details. Bailout
an american instead of going in a leisure hour to dance merrily
 at some place of public resort as the fellows of his calling 
 continue to do throughout the greater part of europe shuts
  himself up at home to drink
she opened the door softly there sat missus wilson in the old 
rocking chair with one sick death like boy lying on her knee 
crying without let or pause but softly gently as fearing to 
disturb the troubled gasping child while behind her old alice
 let her fast dropping tears fall down on the dead body of the 
 other twin which she was laying out on a board placed on a 
 sort of sofa settee in the corner of the room

源码解析:

#include <opencv2/core.hpp> // 包含OpenCV的核心功能头文件
#include <opencv2/videoio.hpp> // 包含OpenCV用于视频IO操作的功能头文件
#include <opencv2/highgui.hpp> // 包含OpenCV用于GUI操作和读/写图片的功能头文件
#include <opencv2/imgproc.hpp> // 包含OpenCV图片处理的功能头文件
#include <opencv2/dnn.hpp> // 包含OpenCV深度神经网络(DNN)模块的功能头文件
#include <iostream> // 包含标准输入输出流头文件
#include <vector> // 包含标准模板库中的动态数组(vector)相关的头文件
#include <string> // 包含C++字符串相关的头文件
#include <unordered_map> // 包含标准模板库中的哈希表相关的头文件
#include <cmath> // 包含数学函数相关的头文件
#include <random> // 包含随机数生成器相关的头文件
#include <numeric> // 包含数值算法相关的头文件
using namespace cv; // 使用命名空间cv,减少代码中的cv::前缀
using namespace std; // 使用命名空间std,减少代码中的std::前缀


// 下面是关于FilterbankFeatures类的定义
// 这个类用于初始化声音处理参数,根据Jasper架构的默认值进行初始化。详情可参考论文:https://arxiv.org/abs/1904.03288
class FilterbankFeatures {


private:
    int sample_rate = 16000; // 采样率
    double window_size = 0.02; // 窗口大小(以秒为单位)
    double window_stride = 0.01; // 窗口滑动距离(以秒为单位)
    int win_length = static_cast<int>(sample_rate * window_size); // 窗口长度(采样点数)
    int hop_length = static_cast<int>(sample_rate * window_stride); // 帧移(采样点数)
    int n_fft = 512; // 短时傅里叶变换窗口大小


    // 以下是计算滤波器组参数
    int n_filt = 64; // 滤波器个数
    double lowfreq = 0.; // 最低频率
    double highfreq = sample_rate / 2; // 最高频率,由奈奎斯特频率所限制


public:
    // Mel滤波器组的准备工作
    double hz_to_mel(double frequencies)
{
        // 将频率从赫兹转换为梅尔频率尺度
        // 填充线性刻度部分
        double f_min = 0.0; // 最小频率
        double f_sp = 200.0 / 3; // 频率到梅尔尺度的线性转换系数
        double mels = (frequencies - f_min) / f_sp; // 线性转换结果
        // 填充对数刻度部分
        double min_log_hz = 1000.0; // 对数尺度部分的起始赫兹值
        double min_log_mel = (min_log_hz - f_min) / f_sp; // 起始赫兹值对应的梅尔值
        double logstep = std::log(6.4) / 27.0; // 对数尺度区间的步长


        if (frequencies >= min_log_hz)
        {
            // 如果频率值在对数尺度区间,则进行对数尺度的转换
            mels = min_log_mel + std::log(frequencies / min_log_hz) / logstep;
        }
        return mels; // 返回转换后的梅尔值
    }


    vector<double> mel_to_hz(vector<double>& mels)
    {
        // 将梅尔尺度转换回赫兹尺度


        // 填充线性刻度部分
        double f_min = 0.0; // 最小频率
        double f_sp = 200.0 / 3; // 梅尔尺度到频率的线性转换系数
        vector<double> freqs; // 存储转换结果的频率向量
        for (size_t i = 0; i < mels.size(); i++)
        {
            // 对于每个梅尔值,转换回对应的频率值,并添加到向量中
            freqs.push_back(f_min + f_sp * mels[i]);
        }


        // 处理非线性刻度部分
        double min_log_hz = 1000.0; // 对数尺度部分的起始赫兹值
        double min_log_mel = (min_log_hz - f_min) / f_sp; // 起始赫兹值对应的梅尔值
        double logstep = std::log(6.4) / 27.0; // 对数尺度区间的步长


        for(size_t i = 0; i < mels.size(); i++)
        {
            // 对梅尔值在对数尺度区间的部分进行赫兹尺度的转换
            if (mels[i] >= min_log_mel)
            {
                freqs[i] = min_log_hz * exp(logstep * (mels[i] - min_log_mel));
            }
        }
        return freqs; // 返回所有转换后的频率值
    }


    vector<double> mel_frequencies(int n_mels, double fmin, double fmax)
    {
        // 计算两个频率之间n个梅尔频率值


        double min_mel = hz_to_mel(fmin); // 将最小频率转换为梅尔尺度
        double max_mel = hz_to_mel(fmax); // 将最大频率转换为梅尔尺度


        vector<double> mels; // 存储梅尔尺度值的向量
        double step = (max_mel - min_mel) / (n_mels - 1); // 梅尔尺度的步长
        for(double i = min_mel; i < max_mel; i += step)
        {
            // 从最小梅尔尺度开始,按照步长逐步增加,直到最大梅尔尺度
            mels.push_back(i);
        }
        mels.push_back(max_mel); // 包含最大梅尔尺度


        vector<double> res = mel_to_hz(mels); // 将梅尔尺度转换回赫兹尺度
        return res; // 返回转换后的频率值
    }
   vector<vector<double>> mel(int n_mels, double fmin, double fmax)
   {
       // 生成梅尔滤波器组矩阵


       double num = 1 + n_fft / 2; // FFT的一半点数
       vector<vector<double>> weights(n_mels, vector<double>(static_cast<int>(num), 0.)); // 初始化n_mels行,每行有num个元素的二维向量数组weights


       // 每个FFT bin的中心频率
       vector<double> fftfreqs;
       double step = (sample_rate / 2) / (num - 1); // 每个FFT bin的频率间隔
       for(double i = 0; i <= sample_rate / 2; i += step)
       {
           fftfreqs.push_back(i); // 计算并填充fftfreqs向量
       }
       // 梅尔带的中心频率 - 在限定范围内均匀分布
       vector<double> mel_f = mel_frequencies(n_mels + 2, fmin, fmax); // 计算梅尔频率


       vector<double> fdiff; // 用于存放相邻梅尔频率之间的差值
       for(size_t i = 1; i < mel_f.size(); ++i)
       {
           fdiff.push_back(mel_f[i]- mel_f[i - 1]); // 计算差值并添加到fdiff向量
       }


       vector<vector<double>> ramps(mel_f.size(), vector<double>(fftfreqs.size()));
       for (size_t i = 0; i < mel_f.size(); ++i)
       {
           for (size_t j = 0; j < fftfreqs.size(); ++j)
           {
               ramps[i][j] = mel_f[i] - fftfreqs[j]; // 计算梅尔频率和FFT频率之间的 "斜率"
           }
       }


       double lower, upper, enorm; // 初始化变量,用于计算滤波器的能量归一化因子
       for (int i = 0; i < n_mels; ++i)
       {
           // 使用Slaney式的梅尔滤波器,使每个频道的能量大致保持一致
           enorm = 2./(mel_f[i + 2] - mel_f[i]);


           for (int j = 0; j < static_cast<int>(num); ++j)
           {
               // 为所有的bins计算上下限斜率
               lower = (-1) * ramps[i][j] / fdiff[i];
               upper = ramps[i + 2][j] / fdiff[i + 1];


               // 比较上下限斜率并取较小的一个,然后乘以能量因子enorm,得到权重
               weights[i][j] = max(0., min(lower, upper)) * enorm;
           }
       }
       return weights; // 返回计算出的梅尔滤波器组矩阵
   }


   vector<double> pad_window_center(vector<double>& data, int size)
   {
       // 将窗口填充至n_fft大小的长度
       int n = static_cast<int>(data.size()); // 原始数据大小
       int lpad = static_cast<int>((size - n) / 2); // 左侧填充的长度
       vector<double> pad_array; // 构建用于填充的数组


       for(int i = 0; i < lpad; ++i)
       {
           pad_array.push_back(0.); // 在窗口左侧填充0
       }


       for(size_t i = 0; i < data.size(); ++i)
       {
           pad_array.push_back(data[i]); // 添加原始数据
       }


       for(int i = 0; i < lpad; ++i)
       {
           pad_array.push_back(0.); // 在窗口右侧填充0
       }
       return pad_array; // 返回填充后的窗口数据
   }


   vector<vector<double>> frame(vector<double>& x)
   {
       // 将数据数组切割成重叠的帧
       int n_frames = static_cast<int>(1 + (x.size() - n_fft) / hop_length); // 计算帧的数量
       vector<vector<double>> new_x(n_fft, vector<double>(n_frames)); // 初始化帧的二维数组


       for (int i = 0; i < n_fft; ++i)
       {
           for (int j = 0; j < n_frames; ++j)
           {
               new_x[i][j] = x[i + j * hop_length]; // 从原始数据抽取每一帧
           }
       }
       return new_x; // 返回分割出的所有帧
   }


   vector<double> hanning()
   {
       // 实现汉宁窗函数,详情访问:https://en.wikipedia.org/wiki/Window_function#Hann_and_Hamming_windows
       vector<double> window_tensor; // 初始化窗函数tensor
       for (int j = 1 - win_length; j < win_length; j+=2)
       {
           // 计算汉宁窗的值,对于窗口内每一个点,根据汉宁窗公式进行计算
           window_tensor.push_back(1 - (0.5 * (1 - cos(CV_PI * j / (win_length - 1)))));
       }
       return window_tensor; // 返回计算出的汉宁窗
   }
   
    vector<vector<double>> stft_power(vector<double>& y)
    {
        // 短时傅里叶变换(STFT)。STFT通过在短时重叠窗口上计算离散傅里叶变换(DFT)将信号表示在时频域上。
        // 填充时间序列以使帧居中
        vector<double> new_y;
        int num = int(n_fft / 2);


        // 在序列前后进行对称填充,以保证DFT时窗口能够居中
        for (int i = 0; i < num; ++i)
        {
            new_y.push_back(y[num - i]);
        }
        for (size_t i = 0; i < y.size(); ++i)
        {
            new_y.push_back(y[i]);
        }
        for (size_t i = y.size() - 2; i >= y.size() - num - 1; --i)
        {
            new_y.push_back(y[i]);
        }


        // 计算窗函数
        vector<double> window_tensor = hanning();


        // 将窗函数长度填充至n_fft大小
        vector<double> fft_window = pad_window_center(window_tensor, n_fft);


        // 对时间序列进行窗函数处理
        vector<vector<double>> y_frames = frame(new_y);


        // 应用窗函数
        for (size_t i = 0; i < y_frames.size(); ++i)
        {
            for (size_t j = 0; j < y_frames[0].size(); ++j)
            {
                y_frames[i][j] *= fft_window[i];
            }
        }


        // 转置帧以进行STFT计算
        vector<vector<double>> y_frames_transpose(y_frames[0].size(), vector<double>(y_frames.size()));
        for (size_t i = 0; i < y_frames[0].size(); ++i)
        {
            for (size_t j = 0; j < y_frames.size(); ++j)
            {
                y_frames_transpose[i][j] = y_frames[j][i];
            }
        }


        // 执行短时傅里叶变换并获取谱的功率
        vector<vector<double>> spectrum_power(y_frames_transpose[0].size() / 2 + 1 );
        for (size_t i = 0; i < y_frames_transpose.size(); ++i)
        {
            Mat dstMat;
            dft(y_frames_transpose[i], dstMat, DFT_COMPLEX_OUTPUT);


            // 只需要频谱的前半部分,因为第二部分是对称的
            for (int j = 0; j < static_cast<int>(y_frames_transpose[0].size()) / 2 + 1; ++j)
            {
                double power_re = dstMat.at<double>(2 * j) * dstMat.at<double>(2 * j);
                double power_im = dstMat.at<double>(2 * j + 1) * dstMat.at<double>(2 * j + 1);
                spectrum_power[j].push_back(power_re + power_im);
            }
        }
        return spectrum_power;
    }


    Mat calculate_features(vector<double>& x)
{
        // 计算滤波器组特征矩阵


        // 执行预加重处理
        std::default_random_engine generator;
        std::normal_distribution<double> normal_distr(0, 1);
        double dither = 1e-5;
        for(size_t i = 0; i < x.size(); ++i)
        {
            x[i] += dither * static_cast<double>(normal_distr(generator));
        }
        double preemph = 0.97;
        for (size_t i =  x.size() - 1; i > 0; --i)
        {
            x[i] -= preemph * x[i-1];
        }


        // 计算短时傅里叶变换并获取谱的功率
        auto spectrum_power = stft_power(x);


        vector<vector<double>> filterbanks = mel(n_filt, lowfreq, highfreq);


        // 计算滤波器矩阵和谱的功率矩阵的乘积的对数
        vector<vector<double>> x_stft(filterbanks.size(), vector<double>(spectrum_power[0].size(), 0));


        for (size_t i = 0; i < filterbanks.size(); ++i)
        {
            for (size_t j = 0; j < filterbanks[0].size(); ++j)
            {
                for (size_t k = 0; k < spectrum_power[0].size(); ++k)
                {
                    x_stft[i][k] += filterbanks[i][j] * spectrum_power[j][k];
                }
            }
            for (size_t k = 0; k < spectrum_power[0].size(); ++k)
            {
                x_stft[i][k] = std::log(x_stft[i][k] + 1e-20);
            }
        }


        // 标准化数据
        auto elments_num = x_stft[0].size();
        for(size_t i = 0; i < x_stft.size(); ++i)
        {
            double x_mean = std::accumulate(x_stft[i].begin(), x_stft[i].end(), 0.) / elments_num; // 计算算术平均值
            double x_std = 0; // 标准差
            for(size_t j = 0; j < elments_num; ++j)
            {
                double subtract = x_stft[i][j] - x_mean;
                x_std += subtract * subtract;
            }
            x_std /= elments_num;
            x_std = sqrt(x_std) + 1e-10; // 确保x_std不为零


            for(size_t j = 0; j < elments_num; ++j)
            {
                x_stft[i][j] = (x_stft[i][j] - x_mean) / x_std; // 计算标准分数
            }
        }


        // 将计算好的特征矩阵转换为OpenCV的Mat类型
        Mat calculate_features(static_cast<int>(x_stft.size()), static_cast<int>(x_stft[0].size()), CV_32F);
        for(int i = 0; i < calculate_features.size[0]; ++i)
        {
            for(int j = 0; j < calculate_features.size[1]; ++j)
            {
                calculate_features.at<float>(i, j) = static_cast<float>(x_stft[i][j]);
            }
        }
        return calculate_features;
    }
};


class Decoder {
    // 用于解码jasper模型的输出
private:
    unordered_map<int, char> labels_map = fillMap(); // 将索引映射到字符的哈希表
    int blank_id = 28; // 特殊的空白符号标识符


public:
    unordered_map<int, char> fillMap()
    {
        // 填充索引到字符的映射
        vector<char> labels={' ','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p'
                                ,'q','r','s','t','u','v','w','x','y','z','\''};
        unordered_map<int, char> map;
        for(int i = 0; i < static_cast<int>(labels.size()); ++i)
        {
            map[i] = labels[i]; // 将索引与字符关联
        }
        return map;
    }


    string decode(Mat& x)
{
        // 接收jasper模型的输出,并执行CTC解码算法来移除重复和特殊符号,返回预测


        vector<int> prediction;
        for(int i = 0; i < x.size[1]; ++i)
        {
            double maxEl = -1e10;
            int ind = 0;
            for(int j = 0; j < x.size[2]; ++j)
            {
                if (maxEl <= x.at<float>(0, i, j))
                {
                    maxEl = x.at<float>(0, i, j); // 找到最大概率的标签
                    ind = j; // 记录索引
                }
            }
            prediction.push_back(ind); // 将索引添加到预测列表
        }
        // CTC解码过程
        vector<int> decoded_prediction = {};
        int previous = blank_id; // 初始化前一个字符索引为blank_id


        for(int i = 0; i < static_cast<int>(prediction.size()); ++i)
        {
            // 移除重复字符和blank_id
            if ((prediction[i] != previous || previous == blank_id) && prediction[i] != blank_id)
            {
                decoded_prediction.push_back(prediction[i]); // 将索引添加到解码预测列表
            }
            previous = prediction[i]; // 更新前一个字符索引
        }


        string hypotheses = {}; // 初始化假设字符串
        for(size_t i = 0; i < decoded_prediction.size(); ++i)
        {
            auto it = labels_map.find(decoded_prediction[i]); // 从映射查找字符
            if (it != labels_map.end())
                hypotheses.push_back(it->second); // 如果找到,添加到假设字符串
        }
        return hypotheses; // 返回解码的结果
    }


};


static string predict(Mat& features, dnn::Net net, Decoder decoder)
{
    // 通过Jasper模型传递特征,并解码输出为英语语音转录


    // 将2D特征矩阵展开成3D
    vector<int> sizes = {1, static_cast<int>(features.size[0]), static_cast<int>(features.size[1])};
    features = features.reshape(0, sizes);


    // 进行预测
    net.setInput(features);
    Mat output = net.forward(); // 获取网络的输出


    // 解码输出为语音转录
    auto prediction = decoder.decode(output);
    return prediction;
}


static int readAudioFile(vector<double>& inputAudio, string file, int audioStream)
{
    // 读取音频文件并返回采样率
    VideoCapture cap;
    int samplingRate = 16000; // 定义采样率
    vector<int> params {    CAP_PROP_AUDIO_STREAM, audioStream,
                            CAP_PROP_VIDEO_STREAM, -1,
                            CAP_PROP_AUDIO_DATA_DEPTH, CV_32F,
                            CAP_PROP_AUDIO_SAMPLES_PER_SECOND, samplingRate
                            };
    cap.open(file, CAP_ANY, params); // 打开文件并设置参数
    if (!cap.isOpened())
    {
        cerr << "Error : Can't read audio file: '" << file << "' with audioStream = " << audioStream << endl;
        return -1; // 如果文件打开失败,返回错误代码-1
    }
    const int audioBaseIndex = (int)cap.get(CAP_PROP_AUDIO_BASE_INDEX);
    vector<double> frameVec;
    Mat frame;
    for (;;)
    {
        if (cap.grab())
        {
            cap.retrieve(frame, audioBaseIndex);
            frameVec = frame; // 从视频捕获对象中取得音频帧
            inputAudio.insert(inputAudio.end(), frameVec.begin(), frameVec.end()); // 将音频帧加入到audio向量中
        }
        else
        {
            break; // 如果没有数据,退出循环
        }
    }
    return samplingRate; // 返回采样率
}
static int readAudioMicrophone(vector<double>& inputAudio, int microTime)
{
    // 从麦克风读取音频数据到指定时长
    VideoCapture cap;
    int samplingRate = 16000; // 定义采样率为16kHz
    vector<int> params {    CAP_PROP_AUDIO_STREAM, 0,
                            CAP_PROP_VIDEO_STREAM, -1,
                            CAP_PROP_AUDIO_DATA_DEPTH, CV_32F,
                            CAP_PROP_AUDIO_SAMPLES_PER_SECOND, samplingRate
                            };
    cap.open(0, CAP_ANY, params); // 打开麦克风设备
    if (!cap.isOpened())
    {
        cerr << "Error: Can't open microphone" << endl;
        return -1; // 如果无法打开,返回错误
    }


    const int audioBaseIndex = (int)cap.get(CAP_PROP_AUDIO_BASE_INDEX);
    vector<double> frameVec;
    Mat frame;
    if (microTime <= 0)
    {
        cerr << "Error: Duration of audio chunk must be > 0" << endl;
        return -1; // 如果指定的录音时长不合法,返回错误
    }
    size_t sizeOfData = static_cast<size_t>(microTime * samplingRate);
    while (inputAudio.size() < sizeOfData)
    {
        if (cap.grab())
        {
            cap.retrieve(frame, audioBaseIndex);
            frameVec = frame;
            inputAudio.insert(inputAudio.end(), frameVec.begin(), frameVec.end()); // 读取音频数据到向量
        }
        else
        {
            cerr << "Error: Grab error" << endl;
            break; // 如果读取失败,输出错误并跳出循环
        }
    }
    return samplingRate; // 返回采样率
}


// 这段代码展示了如何使用FilterbankFeatures类和Decoder类以及DNN模块进行语音识别。
// 首先,它通过读取录音或麦克风来获取声音信号,然后将其转换为数学特征,之后它将这些特征通过Jasper语音识别模型进行处理,最后通过解码器将模型输出的数据解码为文本。
// 在main函数中还包含了错误处理、警告输出以及可视化频谱图的选项。
// 整个程序的执行流程是:
// 1. 定义命令行参数解析器和解析参数;
// 2. 根据命令行参数加载DNN网络模型;
// 3. 读取音频文件或通过麦克风捕获音频;
// 4. 判断音频数据是否有效;
// 5. 使用FilterbankFeatures类实例化的对象来计算特征;
// 6. 可选地显示频谱图;
// 7. 使用DNN模型进行预测;
// 8. 打印预测结果。
int main(int argc, char** argv)
{
    // 主程序的输入参数定义
    const String keys =
        "{help h usage ?     |                          | 运行 Jasper 语音识别模型的脚本 }"
        "{input_file i       | audio6.mp3              | 输入音频文件的路径. 如果没有指定,则使用麦克风输入 }"
        "{audio_duration t   | 15                       | 从麦克风捕获的音频块的持续时间 }"
        "{audio_stream a     | 0                        | CAP_PROP_AUDIO_STREAM 的值 }"
        "{show_spectrogram s | false                    | 是否展示输入音频的频谱图: true / false / 1 / 0 }"
        "{model m            | jasper_reshape.onnx      | Jasper 的 onnx 文件路径. 你可以从链接下载转换后的 onnx 模型 }"
        "{backend b          | dnn::DNN_BACKEND_DEFAULT | 选择计算后端: "
                                                          "dnn::DNN_BACKEND_DEFAULT, "
                                                          "dnn::DNN_BACKEND_INFERENCE_ENGINE, "
                                                          "dnn::DNN_BACKEND_OPENCV }"
        "{target t           | dnn::DNN_TARGET_CPU      | 选择目标设备: "
                                                          "dnn::DNN_TARGET_CPU, "
                                                          "dnn::DNN_TARGET_OPENCL, "
                                                          "dnn::DNN_TARGET_OPENCL_FP16 }"
        ;
    // 命令行参数解析器
    CommandLineParser parser(argc, argv, keys);
    if (parser.has("help"))
    {
        parser.printMessage(); // 如果有帮助选项,则打印帮助信息并退出
        return 0;
    }


    // 加载模型网络
    dnn::Net net = dnn::readNetFromONNX(parser.get<std::string>("model"));
    net.setPreferableBackend(parser.get<int>("backend")); // 设置模型计算后端
    net.setPreferableTarget(parser.get<int>("target")); // 设置模型计算目标


    // 获取音频
    vector<double> inputAudio = {};
    int samplingRate = 0;
    if (parser.has("input_file"))
    {
        // 如果指定了输入文件,从该文件读取音频
        string audio = samples::findFile(parser.get<std::string>("input_file"));
        samplingRate = readAudioFile(inputAudio, audio, parser.get<int>("audio_stream"));
    }
    else
    {
        // 否则,从麦克风读取音频
        samplingRate = readAudioMicrophone(inputAudio, parser.get<int>("audio_duration"));
    }


    if ((inputAudio.size() == 0) || samplingRate <= 0)
    {
        // 如果读取音频时出错,输出错误信息并退出
        cerr << "Error: problems with audio reading, check input arguments" << endl;
        return -1;
    }


    if (inputAudio.size() / samplingRate < 6)
    {
        // 如果读取的音频时长不足6秒,进行警告并用0填充至6秒
        cout << "Warning: For predictable network performance duration of audio must exceed 6 sec."
                " Audio will be extended with zero samples" << endl;
        for(int i = static_cast<int>(inputAudio.size()); i < samplingRate * 6; ++i)
        {
            inputAudio.push_back(0);
        }
    }


    // 计算特征
    FilterbankFeatures filter;
    auto calculated_features = filter.calculate_features(inputAudio);


    // 是否显示频谱图
    if (parser.get<bool>("show_spectrogram"))
    {
        // 计算并显示频谱图
        Mat spectogram;
        normalize(calculated_features, spectogram, 0, 255, NORM_MINMAX, CV_8U);
        applyColorMap(spectogram, spectogram, COLORMAP_INFERNO);
        imshow("spectogram", spectogram); 
        waitKey(0); // 等待用户按键之后退出
    }


    // 使用解码器并预测结果
    Decoder decoder;
    string prediction = predict(calculated_features, net, decoder);
    // 输出识别结果
    cout << prediction << endl;


    return 0; // 主程序退出
}

该代码是一个使用OpenCV的DNN模块结合音频信号处理技术实现的语音识别的C++程序。这个程序首先定义了声音特征提取的类FilterbankFeatures和Jasper模型的解码器Decoder,在main函数中,程序将加载和运行Jasper模型,并通过麦克风或音频文件获取声音数据,然后对其进行处理以提取特征,最后给出语音识别的预测结果。代码中引入了多个音频信号处理相关的函数,以及语音识别模型运用和结果解析的部分,体现了将深度学习应用于音频分析处理的一种实际方法。

  • mel: 生成梅尔滤波器矩阵,接收梅尔滤波器的数量n_mels和频率范围fmin和fmax,用于后续提取音频特征。

  • pad_window_center: 填充窗函数至n_fft大小的长度,这是为了确保窗函数在进行短时傅里叶变换时具有相同的大小。

  • frame: 将音频信号按照窗口大小n_fft和步长hop_length分割成一系列有重叠的帧。

  • hanning: 这是一个窗函数,用于生成汉宁窗,在进行短时傅里叶变换时应用到每一帧上,以减少边缘效应。

  • stft_power 函数实现了短时傅里叶变换(STFT),通过在短时重叠窗口上计算离散傅里叶变换(DFT),使信号在时频域上表示。它还计算了每个频率组件的功率,作为处理的一部分。

  • calculate_features 函数计算滤波器组特征矩阵,首先对音频信号加噪(抖动)以增加其动态范围,然后进行预加重处理以增强高频部分。接下来,使用STFT计算频谱的功率,然后使用梅尔滤波器组筛选出这些功率特征,并对结果取对数。最后,将特征矩阵标准化并将其转换为OpenCV的Mat类型以便后续处理。

  • Decoder类:从深度学习模型输出中提取文本结果,使用连接时序分类(CTC)解码。

  • predict函数:传递特征通过预训练的神经网络模型(如Jasper),并将输出解码为文本。

  • readAudioFile函数:读取音频文件,将音频流转换为一个可以被模型处理的样本数组。

  • Decoder中的decode函数通过处理CTC模型输出,消除重复和特殊的空字符(通常表示为 "-1"或最后一个索引),生成最终的文本预测结果。

  • predict函数通过网络模型计算特征的值,将输出交给Decoder来产出最终文本结果。

  • readAudioFile函数用于从给定文件中读取音频数据,并将其转换为浮点数值数组,该数组随后可以用于音频特征提取。返回值是音频文件的采样率,这对于后续处理至关重要。

  • readAudioMicrophone 函数用于从麦克风捕获音频数据,直到达到指定时长 microTime。

  • main 函数则是程序的入口点,解析命令行参数以设置参数(默认值如输入文件、是否显示频谱图等),加载预训练的神经网络模型(如 Jasper 模型),读取音频文件或者麦克风录音,确保读取的音频长度,对音频数据进行处理和预测,并输出最终的结果。

  • 当录音时长少于6秒时,程序会输出警告,并将音频数据以0填充至6秒长,以保证模型能正常识别音频内容。

  • 显示频谱图的功能被省略了代码,可以利用Mat和imshow实现。

c552efdc4e31fe7bedb08e11b2ea8cc6.png