OpenVINO 黑客马拉松:在 Raspberry Pi 上识别声音和情绪

30月1日-XNUMX月XNUMX日在下诺夫哥罗德举行 OpenVINO 黑客马拉松。 参与者被要求使用英特尔 OpenVINO 工具套件创建产品解决方案原型。 组织者提出了一份大致主题列表,供选择任务时参考,但最终决定权仍由团队决定。 此外,还鼓励使用产品中未包含的模型。

OpenVINO 黑客马拉松:在 Raspberry Pi 上识别声音和情绪

在这篇文章中,我们将向您介绍我们如何创建产品原型,并最终获得了第一名。

超过10支队伍参加了此次黑客马拉松。 很高兴他们中的一些人来自其他地区。 黑客马拉松的场地是“Kremlinsky on Pochain”综合体,里面挂着下诺夫哥罗德的古老照片,还有随行人员! (我提醒您,目前英特尔的中央办公室位于下诺夫哥罗德)。 参与者有 26 小时的时间来编写代码,最后他们必须展示他们的解决方案。 一个单独的优势是演示会议的存在,以确保计划的所有内容都得到实际实施,并且不会在演示中留下想法。 商品、零食、食物,一应俱全!

此外,英特尔还可选提供摄像头、Raspberry PI、神经计算棒 2。

任务选择

准备自由形式的黑客马拉松最困难的部分之一是选择挑战。 我们立即决定提出一些产品中尚未包含的东西,因为公告称这是非常受欢迎的。

经过分析 模型,它们包含在当前版本的产品中,我们得出的结论是,它们中的大多数解决了各种计算机视觉问题。 而且,在计算机视觉领域很难想出一个无法使用 OpenVINO 解决的问题,而且即使可以发明一个,也很难在公共领域找到预训练的模型。 我们决定向另一个方向挖掘——语音处理和分析。 让我们考虑一个有趣的任务,从言语中识别情绪。 必须说的是,OpenVINO 已经有了一个根据人脸判断人情绪的模型,但是:

  • 理论上,可以创建一种同时适用于声音和图像的组合算法,这应该会提高准确性。
  • 摄像机通常具有较窄的视角;需要多个摄像机才能覆盖较大的区域;而声音则没有这样的限制。

让我们发展这个想法:让我们以零售领域的想法为基础。 您可以在商店结账时衡量客户满意度。 如果其中一位顾客对服务不满意并开始提高语气,您可以立即致电管理员寻求帮助。
在这种情况下,我们需要添加人声识别功能,这将使我们能够区分商店员工和顾客,并为每个人提供分析。 嗯,此外,还可以分析商店员工本身的行为,评估团队的氛围,听起来不错!

我们制定了解决方案的要求:

  • 目标设备尺寸小
  • 实时操作
  • 低价格
  • 易于扩展

因此,我们选择Raspberry Pi 3 c作为目标设备 英特尔网络控制系统2.

在这里,需要注意 NCS 的一个重要功能 - 它最适合标准 CNN 架构,但如果您需要运行带有自定义层的模型,则需要进行低级优化。

只需要做一件小事:您需要一个麦克风。 普通的 USB 麦克风就可以,但与 RPI 一起看起来不太好。 但即使在这里,解决方案实际上“就在附近”。 为了录制语音,我们决定使用套件中的 Voice Bonnet 板 谷歌 AIY 语音套件,其上有一个有线立体声麦克风。

从以下位置下载 Raspbian AIY项目库 并将其上传到闪存驱动器,使用以下命令测试麦克风是否正常工作(它将录制 5 秒长的音频并将其保存到文件中):

arecord -d 5 -r 16000 test.wav

我应该立即注意到麦克风非常敏感并且可以很好地拾取噪音。 为了解决这个问题,我们进入 alsamixer,选择 Capture devices 并将输入信号电平降低到 50-60%。

OpenVINO 黑客马拉松:在 Raspberry Pi 上识别声音和情绪
我们用锉刀修改主体,一切都合适,您甚至可以用盖子将其关闭

添加指示按钮

拆开AIY Voice Kit,我们记得有一个RGB按钮,其背光可以通过软件控制。 我们搜索“Google AIY Led”并找到文档: https://aiyprojects.readthedocs.io/en/latest/aiy.leds.html
为什么不使用这个按钮来显示识别的情感,我们只有7个类,而按钮有8种颜色,就够了!

我们通过 GPIO 将按钮连接到 Voice Bonnet,加载必要的库(它们已安装在 AIY 项目的分发套件中)

from aiy.leds import Leds, Color
from aiy.leds import RgbLeds

让我们创建一个字典,其中每种情绪都会有一个 RGB 元组形式的相应颜色和 aiy.leds.Leds 类的对象,通过它我们将更新颜色:

led_dict = {'neutral': (255, 255, 255), 'happy': (0, 255, 0), 'sad': (0, 255, 255), 'angry': (255, 0, 0), 'fearful': (0, 0, 0), 'disgusted':  (255, 0, 255), 'surprised':  (255, 255, 0)} 
leds = Leds()

最后,在每次新的情绪预测之后,我们将根据它(按键)更新按钮的颜色。

leds.update(Leds.rgb_on(led_dict.get(classes[prediction])))

OpenVINO 黑客马拉松:在 Raspberry Pi 上识别声音和情绪
按钮,燃烧!

使用语音工作

我们将使用 pyaudio 捕获来自麦克风的流,并使用 webrtcvad 过滤噪音并检测语音。 此外,我们将创建一个队列,我们​​将异步添加和删除语音摘录。

由于 webrtcvad 对所提供片段的大小有限制 - 它必须等于 10/20/30ms,并且用于识别情绪的模型的训练(稍后我们将了解到)是在 48kHz 数据集上进行的,因此我们将捕获大小为 48000×20ms/1000×1(单声道)=960 字节的块。 Webrtcvad 将为每个块返回 True/False,这对应于块中是否存在投票。

让我们实现以下逻辑:

  • 我们将把那些有投票的块添加到列表中;如果没有投票,那么我们将增加空块的计数器。
  • 如果空块的计数器> = 30(600毫秒),那么我们查看累积块列表的大小;如果它> 250,那么我们将其添加到队列中;如果不是,我们认为长度记录的内容不足以将其提供给模型来识别说话者。
  • 如果空块的计数器仍然< 30,并且累积块列表的大小超过300,那么我们会将片段添加到队列中以进行更准确的预测。 (因为情绪会随着时间而改变)

 def to_queue(frames):
    d = np.frombuffer(b''.join(frames), dtype=np.int16)
    return d

framesQueue = queue.Queue()
def framesThreadBody():
    CHUNK = 960
    FORMAT = pyaudio.paInt16
    CHANNELS = 1
    RATE = 48000

    p = pyaudio.PyAudio()
    vad = webrtcvad.Vad()
    vad.set_mode(2)
    stream = p.open(format=FORMAT,
                channels=CHANNELS,
                rate=RATE,
                input=True,
                frames_per_buffer=CHUNK)
    false_counter = 0
    audio_frame = []
    while process:
        data = stream.read(CHUNK)
        if not vad.is_speech(data, RATE):
            false_counter += 1
            if false_counter >= 30:
                if len(audio_frame) > 250:              
                    framesQueue.put(to_queue(audio_frame,timestamp_start))
                    audio_frame = []
                    false_counter = 0

        if vad.is_speech(data, RATE):
            false_counter = 0
            audio_frame.append(data)
            if len(audio_frame) > 300:                
                    framesQueue.put(to_queue(audio_frame,timestamp_start))
                    audio_frame = []

是时候在公共领域寻找预先训练的模型了,去 github、Google,但请记住,我们对所使用的架构有限制。 这是一个相当困难的部分,因为您必须在输入数据上测试模型,此外,还将它们转换为 OpenVINO 的内部格式 - IR(中间表示)。 我们尝试了来自 github 的大约 5-7 种不同的解决方案,如果用于识别情绪的模型立即起作用,那么对于语音识别我们必须等待更长时间 - 他们使用更复杂的架构。

我们重点关注以下几个方面:

接下来我们将从理论开始讨论模型转换。 OpenVINO 包含几个模块:

  • 打开模型动物园,可以使用其中的模型并将其包含在您的产品中
  • Model Optimzer,借助它,您可以将模型从各种框架格式(Tensorflow、ONNX 等)转换为中间表示格式,我们将使用该格式进一步工作
  • 推理引擎允许您在 Intel 处理器、Myriad 芯片和神经计算棒加速器上运行 IR 格式的模型
  • 最高效的 OpenCV 版本(支持推理引擎)
    IR 格式的每个模型由两个文件描述:.xml 和.bin。
    模型通过模型优化器转换为 IR 格式,如下所示:

    python /opt/intel/openvino/deployment_tools/model_optimizer/mo_tf.py --input_model speaker.hdf5.pb --data_type=FP16 --input_shape [1,512,1000,1]

    --data_type 允许您选择模型将使用的数据格式。 支持 FP32、FP16、INT8。 选择最佳的数据类型可以带来良好的性能提升。
    --input_shape 表示输入数据的维度。 动态更改它的能力似乎存在于 C++ API 中,但我们没有深入研究,只是针对其中一个模型修复了它。
    接下来,我们尝试通过 DNN 模块将已转换的 IR 格式的模型加载到 OpenCV 中并将其转发给它。

    import cv2 as cv
    emotionsNet = cv.dnn.readNet('emotions_model.bin',
                              'emotions_model.xml')
    emotionsNet.setPreferableTarget(cv.dnn.DNN_TARGET_MYRIAD)

    在这种情况下,最后一行允许您将计算重定向到神经计算棒,基本计算是在处理器上执行的,但在 Raspberry Pi 的情况下,这不起作用,您将需要一个神经计算棒。

    接下来,逻辑如下:我们将音频划分为一定大小的窗口(对我们来说是 0.4 秒),我们将每个窗口转换为 MFCC,然后将其馈送到网格:

    emotionsNet.setInput(MFCC_from_window)
    result = emotionsNet.forward()

    接下来,我们来看所有窗口最常见的类。 这是一个简单的解决方案,但对于黑客马拉松来说,只要你有时间,你就不需要想出太深奥的东西。 我们还有很多工作要做,所以让我们继续——我们将处理语音识别问题。 有必要创建某种数据库来存储预先录制的声音的频谱图。 由于时间所剩无几,我们将尽力解决这个问题。

    也就是说,我们创建一个用于录制语音摘录的脚本(其工作方式与上述相同,只有当键盘中断时,它才会将语音保存到文件中)。

    咱们试试吧:

    python3 voice_db/record_voice.py test.wav

    我们录制几个人的声音(在我们的例子中,是三个团队成员)
    接下来,对于每个录制的声音,我们执行快速傅立叶变换,获得频谱图并将其保存为 numpy 数组 (.npy):

    for file in glob.glob("voice_db/*.wav"):
            spec = get_fft_spectrum(file)
            np.save(file[:-4] + '.npy', spec)

    文件中的更多详细信息 create_base.py
    因此,当我们运行主脚本时,我们将从一开始就从这些频谱图中获得嵌入:

    for file in glob.glob("voice_db/*.npy"):
        spec = np.load(file)
        spec = spec.astype('float32')
        spec_reshaped = spec.reshape(1, 1, spec.shape[0], spec.shape[1])
        srNet.setInput(spec_reshaped)
        pred = srNet.forward()
        emb = np.squeeze(pred)

    收到发声片段的嵌入后,我们将能够通过从段落到数据库中所有声音的余弦距离来确定它属于谁(越小,可能性越大) - 对于演示,我们设置了阈值至 0.3):

            dist_list = cdist(emb, enroll_embs, metric="cosine")
            distances = pd.DataFrame(dist_list, columns = df.speaker)

    最后,我想指出的是,推理速度很快,并且可以添加 1-2 个模型(对于 7 秒长的示例,推理需要 2.5 秒)。 我们不再有时间添加新模型,而是专注于编写 Web 应用程序的原型。

    Web应用程序

    重要的一点:我们从家里携带路由器并设置我们的本地网络,它有助于通过网络连接设备和笔记本电脑。

    后端是前端和Raspberry Pi之间的端到端消息通道,基于websocket技术(http over tcp协议)。

    第一阶段是接收树莓派处理后的信息,即以json封装的预测变量,中途保存在数据库中,以便统计用户在这段时间内的情绪背景。 然后,该数据包被发送到前端,前端使用订阅并从 websocket 端点接收数据包。 整个后端机制是用 golang 语言构建的;选择它是因为它非常适合异步任务,而 goroutine 可以很好地处理异步任务。
    当访问端点时,用户被注册并输入到结构中,然后接收他的消息。 用户和消息都输入到一个公共集线器中,消息已经从该集线器进一步发送(到订阅的前端),如果用户关闭连接(树莓派或前端),则他的订阅将被取消,并且他将从中删除枢纽。

    OpenVINO 黑客马拉松:在 Raspberry Pi 上识别声音和情绪
    我们正在等待后面的连接

    前端是一个使用 JavaScript 编写的 Web 应用程序,使用 React 库来加速和简化开发过程。 该应用程序的目的是可视化使用在后端运行的算法和直接在 Raspberry Pi 上运行的算法获得的数据。 该页面具有使用react-router实现的分段路由,但我们感兴趣的主页面是主页面,其中使用WebSocket技术从服务器实时接收连续的数据流。 Raspberry Pi 检测到声音,从注册数据库中确定该声音是否属于特定人,并将概率列表发送给客户端。 客户端显示最新的相关数据,显示最有可能对着麦克风说话的人的头像,以及他发音时的情绪。

    OpenVINO 黑客马拉松:在 Raspberry Pi 上识别声音和情绪
    包含更新预测的主页

    结论

    按计划完成所有事情是不可能的,我们根本没有时间,所以主要希望是在演示中,一切都能正常进行。 在演讲中,他们谈论了一切是如何运作的,他们采用了什么模型,他们遇到了什么问题。 接下来是演示部分 - 专家们以随机顺序在房间里走动,并接近每个团队查看工作原型。 他们也向我们提出了问题,每个人都回答了自己的部分,他们将网络留在了笔记本电脑上,一切都按预期进行。

    请注意,我们解决方案的总成本为 150 美元:

    • 树莓派 3 ~ 35 美元
    • Google AIY Voice Bonnet(您可以收取接听费)~ 15$
    • 英特尔NCS 2 ~ 100$

    如何提高:

    • 使用客户端注册 - 要求阅读随机生成的文本
    • 添加更多模型:可以通过声音确定性别和年龄
    • 分离同时发声的声音(分类)

    存储库: https://github.com/vladimirwest/OpenEMO

    OpenVINO 黑客马拉松:在 Raspberry Pi 上识别声音和情绪
    我们很累但很快乐

    最后,我想对主办方和参与者表示感谢。 在其他团队的项目中,我们个人很喜欢监控免费停车位的解决方案。 对我们来说,这是一次非常酷的沉浸在产品和开发中的体验。 我希望这些地区能够举办越来越多有趣的活动,包括人工智能主题的活动。

来源: habr.com

添加评论