OpenVINO хакатон: распазнаем голас і эмоцыі на Raspberry Pi

30 лістапада - 1 снежня ў Ніжнім Ноўгарадзе прайшоў OpenVINO хакатон. Удзельнікам прапаноўвалася стварыць прататып прадуктовага рашэння з выкарыстаннем Intel OpenVINO toolkit. Арганізатарамі быў прапанаваны спіс прыкладных тэм, на якія можна было арыентавацца пры выбары задачы, але фінальнае рашэнне заставалася за камандамі. Акрамя гэтага, заахвочвалася выкарыстанне мадэляў, якія не ўваходзяць у прадукт.

OpenVINO хакатон: распазнаем голас і эмоцыі на Raspberry Pi

У артыкуле распавядзем пра тое, як мы стваралі свой прататып прадукта, з якім у выніку занялі першае месца.

У хакатоне ўдзельнічала больш за 10 каманд. Прыемна, што некаторыя з іх прыехалі з іншых рэгіёнаў. Месцам правядзення хакатона быў абраны комплекс "Крамлёўскі на Пачаіне", дзе ўнутры былі развешаны старадаўнія фатаграфіі Ніжняга Ноўгарада, антуражна! (Нагадваю, што на дадзены момант цэнтральны офіс кампаніі Intel размешчаны менавіта ў Ніжнім Ноўгарадзе). На напісанне кода ўдзельнікам адводзілася 26 гадзін, у канцы было неабходна прэзентаваць сваё рашэнне. Асобным плюсам была наяўнасць дэма-сесіі, каб упэўніцца, што ўсё задуманае праўда рэалізавана, а не засталося ідэямі ў прэзентацыі. Мерч, снэкі, ежа, усё таксама было!

Акрамя гэтага, кампанія Intel па жаданні падала камеры, Raspberry PI, Neural Compute Stick 2.

выбар задачы

Адной з самых складаных частак падрыхтоўкі да хакатон са свабоднай тэматыкай з'яўляецца выбар задачы. Адразу вырашылі прыдумляць нешта, чаго ў прадукце яшчэ няма, бо ў анонсе было сказана, што гэта ўсяляк прывітаецца.

Прааналізаваўшы мадэлі, Якія ўваходзяць у прадукт у бягучым рэлізе, прыходзім да высновы, што большасць з іх вырашаюць розныя задачы кампутарнага зроку. Прычым вельмі складана прыдумаць задачу з вобласці кампутарнага зроку, якую нельга вырашыць з выкарыстаннем OpenVINO, а калі такую ​​і можна прыдумаць, то ў адчыненым доступе складана знайсці прадугледжаныя мадэлі. Вырашаем капаць яшчэ і ў іншым кірунку - у бок апрацоўкі і аналітыкі гаворкі. Разглядаем цікавую задачу па распазнанні эмоцый па гаворцы. Трэба сказаць, што ў OpenVINO ужо ёсць мадэль, якая вызначае эмоцыі чалавека па твары, але:

  • У тэорыі, можна зрабіць сумешчаны алгарытм, які будзе працаваць як па гуку, так і па выяве, што павінна даць прырост у дакладнасці.
  • Камеры звычайна маюць вузкі кут агляду, каб пакрыць вялікую зону, патрабуецца не адна камера, гук не мае такога абмежавання.

Развіваем ідэю: возьмем за аснову ідэю для retail сегмента. Можна вызначаць задаволенасць пакупніка на касах крам. Калі хтосьці з пакупнікоў незадаволены абслугоўваннем і пачынае павышаць тон - можна адразу клікаць адміністратара на дапамогу.
У гэтым выпадку трэба дадаць распазнанне чалавека па голасе, гэта дазволіць нам адрозніваць супрацоўнікаў крамы ад пакупнікоў, выдаваць аналітыку па кожным індывідууму. Ну і акрамя таго, можна будзе аналізаваць паводзіны саміх супрацоўнікаў магазіна, ацэньваць атмасферу ў калектыве, гучыць нядрэнна!

Фарміруем патрабаванні да нашага рашэння:

  • Невялікі памер мэтавага прылады
  • Праца ў рэальным часе
  • нізкая цана
  • Лёгкая маштабаванасць

У выніку ў якасці мэтавага дэвайса выбіраемы Raspberry Pi 3 c Intel NCS 2.

Тут важно отметить одну важную особенность NCS — лучше всего он работает с стандартными CNN архитектурами, если же потребуется запустить на нём модель с кастомными слоями, то ожидайте ̶т̶а̶н̶ц̶е̶в̶ ̶с̶ ̶б̶у̶б̶н̶о̶м̶ низкоуровневой оптимизации.

Справа за малым: трэба здабыць мікрафон. Падыдзе і звычайны USB-мікрафон, праўда ён не будуць глядзецца добра разам з RPI. Але і тут рашэнне літаральна "ляжыць пад бокам". Для запісу голасу вырашаем выкарыстоўваць плату Voice Bonnet з набору Google AIY Voice Kit, На якой ёсць разлітаваны стэрэа мікрафон.

Спампоўваем Raspbian з рэпазітара AIY projects і заліваем на флешку, тэстуем, што мікрафон працуе з дапамогай наступнай каманды (яна запіша аўдыё даўжынёй у 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 projects)

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

Створым dict, у якім кожнай эмоцыі будзе адпавядаць колер у выглядзе RGB Tuple і аб'ект класа 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/30мс, а навучанне мадэлі для распазнання эмоцый (як мы далей даведаемся) праводзілася на датасеце 48кГц, будзем захопліваць чанкі памеру 48000×20мс/1000 мона) = 1 байт. Webrtcvad будзе вяртаць True/False для кожнага з такіх чанкаў, што адпавядае наяўнасці ці адсутнасці голасу ў чанцы.

Рэалізуем наступную логіку:

  • Будзем дадаваць у list тыя чанкі, дзе ёсць голас, калі галасы няма, то інкрыментуем лічыльнік пустых чанкаў.
  • Калі лічыльнік пустых чанков>=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, гуглым, але памятаем, што ў нас ёсць абмежаванне на выкарыстоўваную архітэктуру. Гэта даволі складаная частка, таму што даводзіцца тэставаць мадэлі на сваіх уваходных дадзеных, а акрамя таго, канвертаваць ва ўнутраны фармат OpenVINO – IR (Intermediate Representation). Мы спрабавалі каля 5-7 розных рашэнняў з github, і калі мадэль для распазнання эмоцый зарабіла адразу, то вось з распазнаннем па голасе прыйшлося пасядзець даўжэй - там выкарыстоўваюцца больш складаныя архітэктуры.

Спыняемся на наступных:

  • Эмоцыі з голасу - https://github.com/alexmuhr/Voice_Emotion
    Працуе яна па наступным прынцыпе: аўдыё наразаецца на ўрыўкі вызначанага памеру, для кожнага з гэтых урыўкаў вылучаны MFCC і далей падаем іх на ўваход у CNN
  • Распазнаванне па голасе - https://github.com/linhdvu14/vggvox-speaker-identification
    Тут замест MFCC працуем са спектраграмай, пасля FFT падаем сігнал у CNN, дзе на вынахадзе атрымліваем вектарнае паданне голасу.

Далей гаворка пойдзе аб канвертацыі мадэляў, пачнем з тэорыі. OpenVINO уключае ў сябе некалькі модуляў:

  • Open Model Zoo, мадэлі з якога можна было выкарыстоўваць і ўключаць у свой прадукт
  • Model Optimzer, дзякуючы якому можна пераканвертаваць мадэль з розных фарматаў фрэймворкаў (Tensorflow, ONNX etc) у фармат Intermediate Representation, з якім далей мы і будзем працаваць
  • Inference Engine дазваляе запускаць мадэлі ў IR фармаце на працэсарах Intel, чыпах Myriad і паскаральніках Neural Compute Stick
  • Найбольш эфектыўная версія OpenCV (з падтрымкай Inference Engine)
    Кожная мадэль у фармаце IR апісваецца двума файламі: .xml і .bin.
    Мадэлі канвертуюцца ў фармат IR праз Model Optimizer наступным чынам:

    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, але мы так далёка капаць не сталі і для адной з мадэляў проста зафіксавалі яе.
    Далей паспрабуем загрузіць ужо сканвертаваную мадэль у IR фармаце праз DNN модуль у OpenCV і зрабіць forward на яе.

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

    Апошні радок у дадзеным выпадку дазваляе перанакіраваць вылічэнні на Neural Compute Stick, базава вылічэнні выконваюцца на працэсары, але ў выпадку з Raspberry Pi гэта не пракоціць, спатрэбіцца сцік.

    Далей логіка наступная: падзелім наша аўдыё на вокны вызначанага памеру (у нас гэта 0.4с), кожнае з гэтых вокнаў пераўтворым у MFCC, якія затым пададзім на сетку:

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

    Пасля возьмем найболей часта сустракаемы клас для ўсіх вокнаў. Простае рашэнне, але для хакатона і не трэба выдумляць нешта занадта мудрагелісты, толькі калі ёсць час. У нас працы яшчэ шмат, таму ідзем далей - разбіраемся з распазнаннем па голасе. Трэба зрабіць нейкую базу, у якой захоўваліся б спектраграмы загадзя запісаных галасоў. Бо часу засталося няшмат, вырашаем гэтае пытанне як можам.

    У прыватнасці, ствараем скрыпт для запісу ўрыўка голасу (працуе ён гэтак жа, як апісана вышэй, толькі пры перапыненні з клавіятуры ён будзе захоўваць голас у файлік).

    Спрабуем.:

    python3 voice_db/record_voice.py test.wav

    Запісваем галасы некалькіх чалавек (у нашым выпадку траіх чальцоў каманды)
    Далей для кожнага запісанага голасу выконваем fast fourier transform, атрымліваем спектраграму і захоўваемы ў выглядзе numpy array (.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)

    Пасля атрымання эмбедынгу з прагучалага адрэзка зможам вызначыць, каму ён належыць, узяўшы cosine distance ад урыўка да ўсіх галасоў у базе (чым менш, тым верагодней) – для дэма мы выставілі парог 0.3):

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

    У канцы адзначу тое, што хуткасць інферэнсу была хуткай і дазваляла дадаць яшчэ 1-2 мадэлі (на сэмпл даўжынёй 7 секунд на інферэнс сыходзіла 2.5). Дадаць новыя мадэлі мы ўжо не паспявалі і сфакусаваліся на напісанні прататыпа вэб-прыкладанні.

    Вэб-дадатак

    Важны пункт: бярэм з сабой роўтэр з дому і наладжваем сваю лакалку, дапамагае злучыць дэвайс і ноуты па сетцы.

    Бэкенд уяўляе з сябе скразны канал паведамленняў паміж фронтам і Raspberry Pi, заснаваны на тэхналогіі websocket (http over tcp protocol).

    Першым этапам з'яўляецца атрыманне апрацаванай інфармацыі з распберы, гэта значыць спакаваныя ў json прэдыкты, якія на сярэдзіне свайго шляху захоўваюцца ў базу дадзеных, каб можна было фармаваць статыстыку аб эмацыйным фоне карыстача за перыяд. Далей гэты пакет адпраўляецца на фронтэнд, які выкарыстоўвае падпіску і атрыманне пакетаў з эндпоінта вэбсокета. Увесь механізм бэкэнд пабудаваны на мове golang, выбар на яго упаў тым, што ён добра падыходзіць для асінхронных задач, з якімі гаруціны добра спраўляюцца.
    Пры доступе да эндпоинту карыстач рэгіструецца і заносіцца ў структуру, затым адбываецца атрыманне яго паведамлення. І карыстач, і паведамленне заносяцца ў агульны hub, з якога паведамленні ўжо адпраўляюцца далей (на падпісаны фронт), а калі карыстач зачыняе злучэнне (распберы ці фронт), тое яго падпіска анулюецца, і ён выдаляецца з hub.

    OpenVINO хакатон: распазнаем голас і эмоцыі на Raspberry Pi
    Чакаем канэкт з бэка

    Front-end уяўляе сабой web-дадатак, напісаны на JavaScript з выкарыстаннем бібліятэкі React для паскарэння і спрашчэння працэсу распрацоўкі. Мэтай дадзенага прыкладання з'яўляецца візуалізацыя дадзеных, атрыманых пры дапамозе алгарытмаў, запушчаных на back-end баку і непасрэдна Raspberry Pi. На старонцы маецца роўтынг па раздзелах, рэалізаваны пры дапамозе react-router, але асноўную цікавасць уяўляе галоўная старонка, дзе ў рэжыме рэальнага часу паступае бесперапынны струмень дадзеных з сервера па тэхналогіі WebSocket. Raspberry Pi дэтэктуе голас, вызначае прыналежнасць да пэўнага чалавека з зарэгістраванай базы і дасылае спіс probability кліенту. Кліент адлюстроўвае апошнія актуальныя дадзеныя, выводзіць аватарку чалавека, які з найбольшай верагоднасцю казаў у мікрафон, а таксама эмоцыю, з якой ён прамаўляе словы.

    OpenVINO хакатон: распазнаем голас і эмоцыі на Raspberry Pi
    Галоўная старонка з прэдыктамі, якія абнаўляюцца

    Заключэнне

    Не атрымалася дарабіць усё да задуманага, банальна не паспелі, таму галоўная надзея была на дэма, на тое, што ўсё заробіць. У прэзентацыі распавялі пра тое, як усё зроблена, якія мадэлі ўзялі, з якімі праблемамі сутыкнуліся. Далей была частка дэма - эксперты хадзілі па зале ў адвольным парадку і падыходзілі да кожнай каманды, каб паглядзець на працуючы прататып. Задавалі пытанні і нам, кожны адказваў па сваёй частцы, на ноўце пакінулі вэб, і ўсё сапраўды працавала, як і чакалася.

    Адзначу, што агульны кошт нашага рашэння склаў 150$:

    • Raspberry Pi 3 ~ 35$
    • Google AIY Voice Bonnet (можна ўзяць плату respeaker) ~ 15$
    • Intel NCS 2 ~ 100$

    Як палепшыць:

    • Выкарыстоўваць рэгістрацыю з кліента - прасіць прачытаць тэкст, які генерым выпадкова
    • Дадаць яшчэ некалькі мадэляў: па голасе можна вызначаць падлогу і ўзрост
    • Падзяляць адначасова гукі голасу (дыярызацыя)

    Рэпазітар: https://github.com/vladimirwest/OpenEMO

    OpenVINO хакатон: распазнаем голас і эмоцыі на Raspberry Pi
    Стомленыя, але шчаслівыя мы

    Напрыканцы хочацца сказаць дзякуй арганізатарам і ўдзельнікам. З праектаў іншых каманд асабіста нам спадабалася рашэнне для маніторынгу свабодных парковачных месцаў. Для нас гэта быў дзіка круты вопыт апускання ў прадукт і распрацоўкі. Спадзяюся, што ў рэгіёнах будзе праводзіцца ўсё больш цікавых мерапрыемстваў, у тым ліку і па AI тэматыцы.

Крыніца: habr.com

Дадаць каментар