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

模型下载地址:

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/555915.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

# 从浅入深 学习 SpringCloud 微服务架构(一)基础知识

从浅入深 学习 SpringCloud 微服务架构&#xff08;一&#xff09;基础知识 1、系统架构演变&#xff1a; 1&#xff09;单体应用架构。如电商项目。 用户管理、商品管理、订单管理&#xff0c;在一个模块里。 优点&#xff1a;开发简单&#xff0c;快速&#xff0c;适用于…

Mac下brew安装php7.4

这里作者挂了梯子&#xff0c;所以很流畅&#xff01; brew的下载&#xff0c;可参考另外一篇博文&#xff5e;Homebrew 安装与卸载 1、将第三方仓库加入brew brew tap shivammathur/php2、安装指定版本的PHP brew install php7.43、替换Mac自带PHP环境并刷新环境变量 -> …

基于simulink的模拟锁相环和数字锁相环建模与对比仿真

目录 1.课题概述 2.系统仿真结果 3.核心程序与模型 4.系统原理简介 4.1 模拟锁相环&#xff08;PLL&#xff09;的基本原理 4.2 数字锁相环&#xff08;DPLL&#xff09;的基本原理 5.完整工程文件 1.课题概述 模拟锁相环和数字锁相环建模的simulink建模&#xff0c;对…

OpenHarmony UI动画-recyclerview_animators

简介 带有添加删除动画效果以及整体动画效果的list组件库 下载安装 ohpm install ohos/recyclerview-animatorsOpenHarmony ohpm 环境配置等更多内容&#xff0c;请参考如何安装OpenHarmony ohpm 包 使用说明 引入组件库 import { RecyclerView } from "ohos/recycler…

Qt/C++音视频开发70-无感切换通道/无缝切换播放视频/多通道流畅切换/不同视频打开无缝切换

一、前言 之前就写过这个方案&#xff0c;当时做的是ffmpeg内核版本&#xff0c;由于ffmpeg内核解析都是代码实现&#xff0c;所以无缝切换非常完美&#xff0c;看不到丝毫的中间切换过程&#xff0c;看起来就像是在一个通道画面中。其实这种切换只能说是取巧办法&#xff0c;…

排序算法之计数排序

目录 一、简介二、代码实现三、应用场景 一、简介 算法平均时间复杂度最好时间复杂度最坏时间复杂度空间复杂度排序方式稳定性计数排序O(nk )O(nk)O(nk)O(k)Out-place稳定 稳定&#xff1a;如果A原本在B前面&#xff0c;而AB&#xff0c;排序之后A仍然在B的前面&#xff1b; 不…

稀碎从零算法笔记Day52-LeetCode:从双倍数组中还原原数组

题型&#xff1a;数组、贪心 链接&#xff1a;2007. 从双倍数组中还原原数组 - 力扣&#xff08;LeetCode&#xff09; 来源&#xff1a;LeetCode 题目描述 一个整数数组 original 可以转变成一个 双倍 数组 changed &#xff0c;转变方式为将 original 中每个元素 值乘以 …

记录ubuntu20.04安装nvidia-525.85.05显卡驱动(学习笔记2024.4.15、4.16)

电脑&#xff1a;华硕天选X2024 显卡&#xff1a;4060Ti i5-14400F 架构&#xff1a;x86_64 我需要使用Linux系统使用IsaacSim进行仿真&#xff0c;所以安装的都是IsaacSim中的推荐版本。 一.对新鲜的电脑进行分盘 电脑刚到手&#xff0c;900多个G全在C盘里&#xff0c;给它…

学习笔记(4月18日)vector底层模拟实现(1)

1.迭代器 vector实际上是由迭代器进行维护的&#xff0c;关于迭代器是什么&#xff0c;为什么要叫这个名字&#xff0c;后面的学习会逐渐了解&#xff0c;现在先将迭代器是作为指针即可。 vector底层有三个迭代器&#xff0c;用来起到容量、数组头、元素个数的作用。 同时为…

数字零售力航母-看微软如何重塑媒体

数字零售力航母-看微软如何重塑媒体 - 从2024全美广播协会展会看微软如何整合营销媒体AI技术和AI平台公司 2024年&#xff0c;微软公司联合英伟达总司&#xff0c;赞助全美广播协会展会。本次展会微软通过搭建一个由全面的合作伙伴生态系统支持的可信和安全的平台&#xff0c;…

RIP最短路实验(华为)

思科设备参考&#xff1a;RIP最短路实验&#xff08;思科&#xff09; 一&#xff0c;技术简介 RIP&#xff08;Routing Information Protocol&#xff0c;路由信息协议&#xff09;是一种基于距离矢量的内部网关协议&#xff0c;工作原理是每个路由器周期性地向邻居路由器发…

【网站项目】新生报到系统小程序

&#x1f64a;作者简介&#xff1a;拥有多年开发工作经验&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的项目或者毕业设计。 代码可以私聊博主获取。&#x1f339;赠送计算机毕业设计600个选题excel文件&#xff0c;帮助大学选题。赠送开题报告模板&#xff…

Spring Boot | Spring Boot “缓存管理“

目录: 一、Spring Boot 默认 "缓存" 管理 :1.1 基础环境搭建① 准备数据② 创建项目③ 编写 "数据库表" 对应的 "实体类"④ 编写 "操作数据库" 的 Repository接口文件⑤ 编写 "业务操作列" Service文件⑥ 编写 "applic…

Vue2进阶之Vue2高级用法

Vue2高级用法 mixin示例一示例二 plugin插件自定义指令vue-element-admin slot插槽filter过滤器 mixin 示例一 App.vue <template><div id"app"></div> </template><script> const mixin2{created(){console.log("mixin creat…

美团财务科技后端一面:如何保证数据一致性?延时双删第二次失败如何解决?

更多大厂面试内容可见 -> http://11come.cn 美团财务科技后端一面&#xff1a;项目内容拷打 美团财务科技后端一面&#xff1a;项目相关面试题&#xff0c;主要包含 Zset、延时双删失败重试、热点数据解决、ThreadLocal 这几个方面相关的内容 由于前几个问题是对个人项目的…

展览展会媒体媒体邀约执行应该怎么做?

传媒如春雨&#xff0c;润物细无声&#xff0c;大家好&#xff0c;我是51媒体网胡老师。 展览展会邀请媒体跟其他活动邀请媒体流程大致相同&#xff0c;包括 制定媒体邀约计划&#xff0c;准备新闻稿&#xff0c;发送邀请函&#xff0c;确认媒体参会&#xff0c;现场媒体接待及…

神经网络中正则化和正则化率的含义

在神经网络中&#xff0c;正则化是一种用于防止模型过拟合的技术。过拟合是指模型在训练数据上表现得很好&#xff0c;但是对于未见过的新数据&#xff0c;其泛化能力却很差。正则化通过在损失函数中添加一个额外的项来惩罚模型的复杂度&#xff0c;从而鼓励模型学习更加简单、…

OpenHarmony UI动画-lottie

简介 lottie是一个适用于OpenHarmony的动画库&#xff0c;它可以解析Adobe After Effects软件通过Bodymovin插件导出的json格式的动画&#xff0c;并在移动设备上进行本地渲染。 下载安裝 ohpm install ohos/lottieOpenHarmony ohpm 环境配置等更多内容&#xff0c;请参考如何…

浅写个登录(无js文件)

全部代码如下&#xff0c;无需编写wxss文件&#xff0c;渲染都在style里面&#xff1a; <view style"height: 250rpx;width: 100%;"> <!-- 背景图片 --><view style"position: absolute; background-color: antiquewhite; height: 250rpx;width…

Java学习-Module的概念和使用、IDEA的常用设置及常用快捷键

Module的概念和使用 【1】在Eclipse中我们有Workspace (工作空间)和Project (工程)的概念&#xff0c;在IDEA中只有Project (工程)和Module (模块)的概念。 这里的对应关系为: IDEA官网说明: An Eclipse workspace is similar to a project in IntelliJ IDEA An Eclipse pr…