Применение LicheePi 4A для задач видео-аналитики под управлением ОС openEuler

Экосистема архитектуры RISC-V на текущий момент активно развивается, в том числе в области систем для искусственного интеллекта и компьютерного зрения. Одноплатники под управлением RISC-V процессоров могут использоваться, в том числе для задач видео-аналитики. На волне всеобщего интереса к RISC-V устройствам, мы решили провести собственное тестирование возможностей современных плат и проверить совместимость подобных устройств с операционной системой openEuler.

Для тестирования возможностей подобных систем была выбрана плата LicheePi 4A с чипом TH1520, который имеет NPU с пиковой производительностью 4 TOPS с int8 числами:

Одноплатник использует свой формат моделей и фреймворк HHB, NPU поддерживает запуск квантизированных моделей (Int8/Int16). Полный список поддерживаемых NPU моделей можно найти по ссылке.

В целях тестирования модуля возьмем предобученную на датасете COCO модель yolov5n и замерим скорость обнаружения объектов на видео. Для подготовки модели необходимо осуществить следующие шаги:

  1. Установка ОС.
  2. Подготовка окружения.
  3. Конвертация обученной модели в ONNX-формат.
  4. Квантизация сконвертированной модели с помощью HHB.
  5. Компиляция кода пре-процессинга, вызова модели и пост-процессинга.
  6. Запуск модели.

Установка ОС

В качестве ОС используется openEuler, образ можно получить по ссылке. ОС будет располагаться на eMMC. Подготовка платы к установке подробно описана в вики производителя.

В дальнейшем с помощью fastboot устанавливаем ОС:

sudo ./fastboot flash boot openEuler-23.03-V1-boot.ext4
sudo ./fastboot flash root openEuler-23.03-V1-base.ext4

После завершения необходимо перезагрузить устройство.

Подготовка окружения

Для использования NPU необходимо установить библиотеку shl-python:

pip3 install shl-python

В дальнейшем необходимо скопировать *.so файлы установленного пакета в /usr/lib.

python3 –m shl –whereis th1520

Конвертация модели

Конвертацию и компиляцию будем выполнять с помощью предоставляемого docker-контейнера:
docker pull hhb4tools/hhb:2.4.5
docker run -itd --name=your.hhb2.4 –p22 “hhb4tools/hhb:2.4.5”
docker exec –it your.hhb2.4 /bin/bash
export PATH=/tools/Xuantie-900-gcc-linux-5.10.4-glibc-x86_64-V2.6.1-light.1/bin/:$PATH

Проверим наличие необходимых инструментов командой

hhb –version

Следующим шагом сконвертируем Yolov5n:

git clone https://github.com/ultralytics/yolov5.git
cd yolov5
pip3 install ultralytics
python3 export.py –weights yolov5n.pt –include onnx

Квантизация модели

Далее с помощью HHB квантизируем модель:

cd /home/example/th1520_npu/yolov5n/yolov5_video_tutorial
hhb -D –model-file yolov5n.onnx –data-scale-div 255 –board th1520 –input-name “images” –output-name “/model.24/m.0/Conv_output_0;/model.24/m.1/Conv_output_0;/model.24/m.2/Conv_output_0” –input-shape “1 3 384 640” –calibrate-dataset kite.jpg –quantization-scheme “int8_asym”

Компиляция кода

В дальнейшем используется код одного из примеров с использованием видеопотока внешней камеры. В целях упрощения, возьмем видео с автомобильным трафиком и изменим управляющий код.
Скомпилируем код выполнения модели:

riscv64-unknown-linux-gnu-gcc yolov5n.c -o yolov5n_example hhb_out/io.c hhb_out/model.c -Wl,–gc-sections -O2 -g -mabi=lp64d -I hhb_out/ -L /usr/local/lib/python3.8/dist-packages/hhb/install_nn2/th1520/lib/ -lshl -L /usr/local/lib/python3.8/dist-packages/hhb/prebuilt/decode/install/lib/rv -L /usr/local/lib/python3.8/dist-packages/hhb/prebuilt/runtime/riscv_linux -lprebuilt_runtime -ljpeg -lpng -lz -lstdc++ -lm -I /usr/local/lib/python3.8/dist-packages/hhb/install_nn2/th1520/include/ -mabi=lp64d -march=rv64gcv0p7_zfh_xtheadc -Wl,-unresolved-symbols=ignore-in-shared-libs

Пре-процессинг кадров будем проводить с помощью OpenCV, для этого понадобиться предварительно собранный OpenCV для RISC-V:

cd ..
git clone https://github.com/zhangwm-pt/prebuilt_opencv.git
cd yolov5_video_tutorial
riscv64-unknown-linux-gnu-g++ main.cpp -I../prebuilt_opencv/include/opencv4 -L../prebuilt_opencv/lib -lopencv_imgproc -lopencv_imgcodecs -L../prebuilt_opencv/lib/opencv4/3rdparty/ -llibjpeg-turbo -llibwebp -llibpng -llibtiff -llibopenjp2 -lopencv_core -ldl -lpthread -lrt -lzlib -lcsi_cv -latomic -static -o yolov5n_image_proces

Запуск будет производиться будет с помощью inference.py:

import numpy as np
import cv2
import os
input_hight = 384
input_width = 640

cap = cv2.VideoCapture(‘videofile-name’)
writer = cv2.VideoWriter(‘result.mp4’,
cv2.VideoWriter_fourcc(*’mp4v’),
30, (input_width, input_hight))

def image_preprocess(image, target_size):
ih, iw = target_size
h, w, _ = image.shape

scale = min(iw/w, ih/h)
nw, nh = int(scale * w), int(scale * h)
image_resized = cv2.resize(image, (nw, nh))

image_padded = np.full(shape=[ih, iw, 3], fill_value=128.0)
dw, dh = (iw – nw) // 2, (ih-nh) // 2
image_padded[dh:nh+dh, dw:nw+dw, :] = image_resized

return image_padded

def read_class_names(class_file_name):
”’loads class name from a file”’
names = {}
with open(class_file_name, ‘r’) as data:
for ID, name in enumerate(data):
names[ID] = name.strip(‘\n’)
return names

def draw_bbox(image, bboxes, classes=read_class_names(“coco.names”), show_label=True):
“””
bboxes: [x_min, y_min, x_max, y_max, probability, cls_id] format coordinates.
“””

for i, bbox in enumerate(bboxes):
coor = np.array(bbox[:4], dtype=np.int32)
fontScale = 0.5
score = bbox[4]
class_ind = int(bbox[5])
bbox_color = (255,0,0)
bbox_thick = 1
c1, c2 = (coor[0], coor[1]), (coor[2], coor[3])
cv2.rectangle(image, c1, c2, bbox_color, bbox_thick)
if show_label:
bbox_mess = ‘%s: %.2f’ % (classes[class_ind], score)
t_size = cv2.getTextSize(bbox_mess, 0, fontScale, thickness=bbox_thick)[0]
cv2.rectangle(image, c1, (c1[0] + t_size[0], c1[1] – t_size[1] – 3), bbox_color, -1)

cv2.putText(image, bbox_mess, (c1[0], c1[1]-2), cv2.FONT_HERSHEY_SIMPLEX,
fontScale, (255, 255, 255), bbox_thick, lineType=cv2.LINE_AA)

return image

model_image_process_command = “./yolov5n_image_process”
model_inference_command = “./yolov5n_example ./shl.hhb.bm image_preprocessed_wxw.bin”

while True:
ret, frame = cap.read()
if ret is False:
break
success = cv2.imwrite(‘hat.jpg’, frame)
if success:
os.system(model_image_process_command)
os.system(model_inference_command)
bboxes = []
with open(“detect.txt”, ‘r’) as f:
x_min = f.readline().strip()
while x_min:
y_min = f.readline().strip()
x_max = f.readline().strip()
y_max = f.readline().strip()
probability = f.readline().strip()
cls_id = f.readline().strip()
bbox = [float(x_min), float(y_min), float(x_max), float(y_max), float(probability), int(cls_id)]
print(bbox)
bboxes.append(bbox)
x_min = f.readline().strip()
image_data = image_preprocess(np.copy(frame), [input_hight, input_width])
image = draw_bbox(image_data, bboxes)
image_copy = (image).astype(np.uint8)
writer.write(image_copy)

cap.release()
writer.release()

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

Запуск модели

Необходимо перейти в директорию с скомпилированным кодом на плате и запустить его командой:

python3 inference.py

yolov5n, запущенная на NPU LicheePi 4A, способна обрабатывать до 80 кадров в секунду. Весь пайплайн, от пре-процессинга до записи кадра с детектированными объектами, занимает около 120-150 мс.

Тесты других моделей

В целях сравнения эффективности запуска моделей на NPU, были проведены дополнительные тесты с запуском сети только на CPU. Для компиляции модели под CPU с помощью HHB необходимо изменить параметр –board:

hhb -D –model-file yolov5n.onnx –data-scale-div 255 –board cp920 –input-name “images” –output-name “/model.24/m.0/Conv_output_0;/model.24/m.1/Conv_output_0;/model.24/m.2/Conv_output_0” –input-shape “1 3 384 640” –calibrate-dataset kite.jpg –quantization-scheme “int8_asym”

Модель Устройство запуска Фреймворк Скорость выполнения графа сети, FPS Скорость выполенния графа пайплайна, FPS
YOLOv5 (n) (int8 asym) NPU (TH1520) HHB ~ 80 ~ 8
YOLOv5 (n) (int8 asym) CPU (C920) HHB < 1 < 1
YOLOX CPU (C920) ONNX < 1 < 1
MobileNetV2 (int8 asym) CPU (C920) HHB ~ 12 ~ 3.5
MobileNetV2 (int8 asym) NPU (TH1520) HHB ~ 110 ~ 10

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

С другой стороны, легкость и корректность установки openEuler и отсутствие критических проблем при использовании устройства под управлением ОС открывает другие возможности по использованию одноплатника, в том числе за пределами области искусственного интеллекта.