Чтобы улучшить наш веб-сайт и сделать его удобнее для Вас в части предоставляемых сервисов, мы используем файлы cookie. Нажимая кнопку "Принять", я соглашаюсь с использованием файлов cookie владельцем сайта в соответствии с Политикой в отношении файлов cookie , в том числе на передачу данных, указанных в Политике, третьим лицам (статистическим службам сети Интернет).

Официальный сайт Huawei
Выберите страну или регион

Легкое построение пайплайнов для машинного обучения с MindX SDK

01 сентября 2021
Когда приходится реализовывать реальные проекты с использованием моделей для машинного обучения-разработчик часто сталкивается с большим количеством проблем ...
2021-09-01

Вступление

Когда приходится реализовывать реальные проекты с использованием моделей для машинного обучения-разработчик часто сталкивается с большим количеством проблем:

  1. Как получать данные?

  2. Как их обрабатывать?

  3. Как подавать на инференс?

  4. Как подружить между собой модели из ансабля?

  5. Как адекватно получить результат для дальнейшего использования?

Сегодня я расскажу о MindX SDK-инструменте, который позволяет решить эти проблемы быстро и просто.

Но, для начала необходимо немного рассказать про платформу, на которой мы и будем работать.

Atlas-это ИИ платформа, включающая в себя набор продуктов и решений, работающих на NPU (Neural processing Unit) Ascend.


Ascend – это чип, разработанный на основе архитектуры Da Vinci. 


Его особенностью является наличие отдельных сопроцессоров для различных операций. Например,

Cube-кубический сопроцессор. Используется для перемножения матриц

Vector-используется для исполнения операций над векторами

DVPP-сопроцессор для нативной обработки изображений и видео.

Все эти операции выполняются на аппаратном уровне с помощью Minx DL. Mindx DL-это программная архитектура устройств Huawei для работы с машинным обучением.


Теперь, после того как вы получили представление о том, на чем мы будем работать, давайте перейдем к примерам.

Пример 1

MindX SDK позволяет строить пайплайны машинного обучения в виде JSON из готовых плагинов. После чего вызывать пайплайн из кода и работать с ним дальше. Теперь, давайте рассмотри конкретный пример.

Допустим, вам необходимо определить объект на .jpg изображении с помощью YOLOv3 и вывести результаты в командную строку.

Для начала, рассмотрим наш пайплайн.

	 {
	     "classification+detection": {
	        "stream_config": {
	             "deviceId": "0"
	         },
	         "mxpi_imagedecoder0": {
	             "factory": "mxpi_imagedecoder",
	             "next": "mxpi_imageresize0"
	         },
	         "mxpi_imageresize0": {
	             "factory": "mxpi_imageresize",
	             "next": "mxpi_modelinfer0"
	         },
	         "mxpi_modelinfer0": {
	             "props": {
	                 "modelPath": "../models/yolov3/yolov3_tf_bs1_fp16.om",
	                 "postProcessConfigPath": "../models/yolov3/yolov3_tf_bs1_fp16.cfg",
	                 "labelPath": "../models/yolov3/coco.names",
	                 "postProcessLibPath": "libMpYOLOv3PostProcessor.so"
	             },
	             "factory": "mxpi_modelinfer",
	             "next": " mxpi_dataserialize0"
	         },
	         "mxpi_dataserialize0": {
	             "props": {
	                 "outputDataKeys": "mxpi_modelinfer0 "
	             },
	             "factory": "mxpi_dataserialize",
	             "next": "appsink0"
	         },
	         "appsrc0": {
	             "props": {
	                 "blocksize": "409600"
	             },
	             "factory": "appsrc",
	             "next": "mxpi_imagedecoder0"
	         },
	         "appsink0": {
	             "props": {
	                 "blocksize": "4096000"
	             },
	             "factory": "appsink"
	         }
	     }
	 }

Пайплайн представляет собой JSON файл с разрешением .pipeline

Каждый элемент этого пайплайна-это отдельный плагин, реализующий типичную функцию. Мы выстраиваем их в нужном нам порядке. Давайте посмотрим на них поближе:

	 "classification+detection": {
	         "stream_config": {
	             "deviceId": "0"
	         },

Первый элемент пайплайна, в котором мы определяем его имя и ID устройства на котором он будет исполняться. ID устройства-ID чипа Ascend, на котором мы хотим исполнить наш пайплайн.

	 "appsrc0": {
	             "props": {
	                 "blocksize": "409600"
	             },
	             "factory": "appsrc",
	             "next": "mxpi_imagedecoder0"
	         },

Элемент, отвечающий за подачу данных. Именно считывает нашу картинку. Здесь можно поговорить об устройстве плагина. Каждый плагин строится на основании существующей фабрики-нам нужно лишь указать его параметры.

	         "mxpi_imagedecoder0": {
	             "factory": "mxpi_imagedecoder",
	             "next": "mxpi_imageresize0"
         },

Декодер изображений. Именно он отвечает за преобработку изображений для выполнения операций на NPU. Декодирование происходит на аппаратном уровне, при помощи DVPP.

Здесь можно заметить, что у каждого плагина есть свой предок и свой потомок, при этом, непосредственно в коде, мы можем выстраивать их как нам угодно: главное указать правильного потомка.

	         "mxpi_imageresize0": {
	             "factory": "mxpi_imageresize",
	             "next": "mxpi_modelinfer0"
	         },

Плагин, отвечающий за изменение размерности поданного на вход ихображения. По дефолту подстраивается под нашу модель, но в некоторых случаях можно указать конкретный размер. Ресайз также происходит на DVPP.

	         "mxpi_modelinfer0": {
	             "props": {
	                 "modelPath": "../models/yolov3/yolov3_tf_bs1_fp16.om",
	                 "postProcessConfigPath": "../models/yolov3/yolov3_tf_bs1_fp16.cfg",
	                 "labelPath": "../models/yolov3/coco.names",
	                 "postProcessLibPath": "libMpYOLOv3PostProcessor.so"
	             },
	             "factory": "mxpi_modelinfer",
	             "next": " mxpi_dataserialize0"
	         },

Здесь начинается самое интересное. Плагин modelinfer отвечает непосредственно за инференс нашей модели. На нем я остановлюсь поподробнее.

Вы можете видеть четыре параметра:

modelPath-параметр отвечающий за расположение модели, относительно директории, откуда запускается код

postProcessConfigPath-путь к конфигу постпроцессинга

labelpath-путь к файлу, содержащему леблы нашиз классов

postProcessLibPath-путь к библиотеке постпроцессинга. Каждой модели соответсвует своя библиотека поспроцессинга. Это просто скомпилированный алгоритм на c++, отвечающий за корректный вывод информации. По умолчанию в пакет MindxSDK включены библиотеки для обработки абсолютного большинства актуальных моделей. 

	         "mxpi_dataserialize0": {
	             "props": {
	                 "outputDataKeys": "mxpi_modelinfer0,mxpi_modelinfer1"
	             },
	             "factory": "mxpi_dataserialize",
	             "next": "appsink0"
	         },

Сериализация данных. Нужна для формирования JSON на выходе.

	         "appsink0": {
	             "props": {
	                 "blocksize": "4096000"
	             },
	             "factory": "appsink"
	         }

Финальный плагин, выводящий данные.

Вот и все! Теперь давайте разберемся как работать в коде с нашим пайплайном.

Для найчала импортируем наш API, после чего инциируем его:

	 from StreamManagerApi import *

	 if __name__ == '__main__':
	     streamManagerApi = StreamManagerApi()
	     ret = streamManagerApi.InitManager()
	     if ret != 0:
	         print("Failed to init Stream manager, ret=%s" % str(ret))
	         exit()

После, создаем стрим на основе нашего пайплайна:

	     with open("../pipeline/Sample.pipeline", 'rb') as f:
	         pipelineStr = f.read()
	     ret = streamManagerApi.CreateMultipleStreams(pipelineStr)
	     if ret != 0:
	         print("Failed to create Stream, ret=%s" % str(ret))
	         exit()

Считываем наш файл:

	 dataInput = MxDataInput()
	     with open("test.jpg", 'rb') as f:
	         dataInput.data = f.read()
	  

Подаем данные на вход пайплайну:

	     streamName = b'classification+detection'
	     inPluginId = 0
	     uniqueId = streamManagerApi.SendDataWithUniqueId(streamName, inPluginId, dataInput)
	     if uniqueId < 0:
	         print("Failed to send data to stream.")
	         exit()

Получаем результат:

	     inferResult = streamManagerApi.GetResultWithUniqueId(streamName, uniqueId, 3000)
	     if inferResult.errorCode != 0:
	         print("GetResultWithUniqueId error. errorCode=%d, errorMsg=%s" % (
	             inferResult.errorCode, inferResult.data.decode()))
	         exit()

И выводим его в командную строку:

	 print(inferResult.data.decode())

На входе у нас была вот такая милая собачка:


На выходе же получили:
	 [{"classId":16,"className":"dog","confidence":0.99774956699999995,"headerVec":[]}],"x0":148.74237099999999,"x1":886.13958700000001, 
         "y0":138.61973599999999,"y1":607.11614999999995}]

Теперь давайте разберем менее тривиальный кейс. А конкретно-мы будем получать данные из RTSP потока и обрабатывать в реальном времени, после чего отображать их в окне. Для теста я использую YOLOv3 и стримлю видео локально с помощью live555MediaServer.

Для начала, вот наш пайплайн:

	 {
	     "detection": {
	         "stream_config": {
	             "deviceId": "0"
	         },
	         "mxpi_rtspsrc0": {
	             "factory": "mxpi_rtspsrc",
	             "props": {
	                 "rtspUrl": "rtsp://183.16.4.12/output.mkv",
	                 "channelId": "0"
	             },
	             "next": "mxpi_videodecoder0"
	         },
	         "mxpi_videodecoder0": {
	             "factory": "mxpi_videodecoder",
	             "props": {
	                 "inputVideoFormat": "H264",
	                 "outputImageFormat": "YUV420SP_NV12",
	                 "vdecChannelId": "0"
	             },
	             "former": "mxpi_rtspsrc0",
	             "next": "mxpi_imageresize0"
	         },
	           "mxpi_imageresize0": {
	             "props": {
	                 "dataSource": "mxpi_videodecoder0",
	                 "resizeHeight": "416",
	                 "resizeWidth": "416",
                 "resizeType": "Resizer_KeepAspectRatio_Fit"
	             },
	             "factory": "mxpi_imageresize",
	             "next": "mxpi_modelinfer0"
	         },
	         "mxpi_modelinfer0":{
	                                                "next":"appsink0",
	                                                "factory":"mxpi_modelinfer",
	                                                "props":{
	                                                                "postProcessLibPath":"../../../lib/libMpYOLOv3PostProcessor.so",
	                                                                "modelPath":"../models/yolov3/yolov3_tf_bs1_fp16.om",
	                                                                "dataSource":"mxpi_imageresize0",
	                                                                "labelPath":"../models/yolov3/coco.names",
	                                                                "postProcessConfigPath":"../models/yolov3/yolov3_tf_bs1_fp16.cfg"
	                                                }
	                                },
	         "appsink0": {
	             "factory": "appsink"
	         }
	    }
	 }

Из нового- здесь плагины mxpi_rtspsrc, где мы указываем адрес нашего стрима и mxpi_videodecoder, который отвечает за обработку видео на сопроцессоре DVPP.

Теперь о том, как работать в коде с этим пайплайном. Для рисования рамок и отображения в окне я буду использовать OpenCV. 

Работа со стримом практически ничем не отличается от работы с картинкой, кроме того, что нам нужно получать не только результат, но и рисовать рамки на изображении. Для этого, чы должны получать данные самого изображения.

	     key1 = b'mxpi_videodecoder0'
	     key2 = b'mxpi_modelinfer0'
	     key_vec = StringVector()
	     key_vec.push_back(key1)
	     key_vec.push_back(key2)
	 infer_result = streamManagerApi.GetResult(b'detection', b'appsink0', key_vec, 3000)

Поэтому, мы берем данные сразу с двух плагинов.

По дефолту, изображения обрабатываются в YUV. C Помощью встроенной функции OpenCV мы переводим YUV в BGR.

	 frame = np.frombuffer(dataStr, dtype=np.uint8)
	 height = visionInfo.height
	 width = visionInfo.width
	 shape = (int(height*1.5),width)
	 frame_yuv = frame.reshape(shape)
	 frame_bgr = cv2.cvtColor(frame_yuv,cv2.COLOR_YUV2RGB_NV21)

После чего рисуем наши боксы:

	 out_frame = draw_img_fun(frame_bgr, str(frame_index), bbox, confidence, classname)

Складываем наши изображения и выводим на экран:

	 vis = np.concatenate((frame_bgr, out_frame), axis=1)
	 cv2.imshow("video",vis)

Результат:

 

Вот и все. Как вы можете заметить, писать пайпланы с помощью готовых плагинов – это быстро просто и удобно.

Технический эксперт по платформе Atlas

Вверх