В неутихающей сейчас пандемии коронавируса Covid-19 поддержание социальной дистанции между людьми является эффективной мерой сдерживания распространения этого вируса. За обеспечением социальной дистанции в настоящее время следят практически во всех общественных местах: банках, торговых центрах, промышленных предприятиях, центрах социальной помощи и т.д.
В данной статье мы рассмотрим автоматическое определение социальной дистанции между людьми с помощью платы Raspberry Pi и библиотеки OpenCV. Результаты этого процесса будут в наглядном виде отображаться на экране дисплея. Для решения задачи определения социальной дистанции мы будем использовать алгоритм обнаружения объектов (Object Detection Algorithm) YOLO v3 с модулем "продвинутой" нейронной сети (Deep Neural Network module).
Плата Raspberry Pi является отличным выбором для решения сложных задач обработки изображений, поскольку она отличается от других популярных сейчас микроконтроллеров и плат на их основе (Arduino) наличием большого объема памяти и значительной вычислительной мощностью. Ранее мы использовали библиотеку OpenCV на Raspberry Pi для решения таких задач обработки изображений как распознавание лиц, оптическое распознавание текста и обнаружение движения на транслируемом видео.
Также на нашем сайте вы можете посмотреть другие проекты, связанные с текущей пандемией коронавируса:
- пульсоксиметр на основе платы Arduino и датчике MAX30100 своими руками;
- робот на Arduino для автоматической дезинфекции помещений с помощью ультрафиолетовых лучей;
- бесконтактный настенный термометр на Arduino с логгером данных на SD карту;
- бесконтактный умный инфракрасный термометр на Arduino и смартфоне.
Необходимые компоненты
Плата Raspberry Pi 4 (купить на AliExpress) (Реклама: ООО "АЛИБАБА.КОМ (РУ)" ИНН: 7703380158).
Для обработки изображений используется библиотека OpenCV. Ее необходимо предварительно установить на плату Raspberry Pi.
Алгоритм YOLO
YOLO (You Only Look Once) – это свёрточная нейронная сеть (Convolution neural network, CNN) для обнаружения объектов (Object Detection) в режиме реального времени. На момент написания оригинала данной статьи (сентябрь 2020 г., ссылка на оригинал приведена в конце статьи) ее самой последней версией являлась YOLOv3 (на момент прочтения вами этой статьи, возможно, есть уже более свежие версии этого алгоритма).
YOLO может распознавать до 80 различных объектов на изображениях и видео потоках, отличается высокой скоростью работы и превосходной точностью распознавания. Алгоритм использует одинарную (single) нейронную сеть для анализа целого изображения, затем разделяет изображение на части (области) и рассчитывает граничные условия и вероятности для каждой области. Базовая модель алгоритма YOLO может обрабатывать изображения в режиме реального времени, следующие с частотой 45 кадров в секунду. Алгоритм YOLO использует такие методы обнаружения объектов как SSD и R-CNN.
Модель алгоритма YOLOV3, которую мы будем использовать в данном проекте, можно скачать по следующей ссылке.
Установка OpenCV на Raspberry Pi
Перед тем как устанавливать OpenCV и необходимые ей приложения на Raspberry Pi необходимо обновить программное обеспечение платы до последней версии с помощью команды:
1 |
sudo apt-get update |
Затем установим приложения (дополнения), необходимые для установки OpenCV, с помощью команд:
1 2 3 4 5 6 |
sudo apt-get install libhdf5-dev -y sudo apt-get install libhdf5-serial-dev –y sudo apt-get install libatlas-base-dev –y sudo apt-get install libjasper-dev -y sudo apt-get install libqtgui4 –y sudo apt-get install libqt4-test –y |
И, наконец, установим OpenCV на Raspberry Pi с помощью команды:
1 |
pip3 install opencv-contrib-python==4.1.0.25 |
Если вам необходима более "глубокая" (с компиляцией) установка OpenCV на Raspberry Pi, то вы можете сделать это с помощью CMake – но в этом случае процесс ее установки займет несколько часов.
Установка других необходимых пакетов на Raspberry Pi
Кроме OpenCV нам необходимо будет установить еще инструмент под названием imutils, который позволяет выполнять базовые функции обработки изображений: перевод, ротация, изменение размера, скелетирование и другие. Для его установки используйте следующую команду:
1 |
pip3 install imutils |
Объяснение программы для Raspberry Pi
Полный код программы приведен в конце статьи, здесь же мы кратко рассмотрим его основные фрагменты.
Первым делом нам необходимо подключить (импортировать) все используемые в проекте библиотеки.
1 2 3 4 5 |
import numpy as np import cv2 import imutils import os import time |
Далее запрограммируем функцию Check() для расчета расстояния между двумя объектами или двумя точками на кадре видеопотока. В этой функции точки a и b обозначают два объекта в кадре. Эти две точки используются для расчета евклидова расстояния между объектами.
1 2 3 4 5 6 7 |
def Check(a, b): dist = ((a[0] - b[0]) ** 2 + 550 / ((a[1] + b[1]) / 2) * (a[1] - b[1]) ** 2) ** 0.5 calibration = (a[1] + b[1]) / 2 if 0 < dist < 0.25 * calibration: return True else: return False |
Функция Setup(yolo) используется для установки маршрутов для весов/весовых коэффициентов (weights) в алгоритме YOLO, файла cfg, файла COCO names. Модуль os.path используется для операций с путями к файлам. Модуль os.path.join() является под-модулем модуля os.path и предназначен для интеллектуального объединения одного или более компонентов пути. Метод cv2.dnn.readNetFromDarknet() используется для загрузки сохраненных весов в сеть. После загрузки весов мы будем извлекать список всех слоев, используемых в сети, с помощью модели net.getLayerNames.
1 2 3 4 5 6 7 8 9 |
def Setup(yolo): global neural_net, ln, LABELS weights = os.path.sep.join([yolo, "yolov3.weights"]) config = os.path.sep.join([yolo, "yolov3.cfg"]) labelsPath = os.path.sep.join([yolo, "coco.names"]) LABELS = open(labelsPath).read().strip().split("\n") neural_net = cv2.dnn.readNetFromDarknet(config, weights) ln = neural_net.getLayerNames() ln = [ln[i[0] - 1] for i in neural_net.getUnconnectedOutLayers()] |
Внутри функции обработки изображений мы будем захватывать одиночный кадр с видео потока и производить его анализ, а именно определение социальной дистанции между всеми людьми, присутствующими на изображении кадра. В первых двух строках этой функции мы устанавливаем размеры видео кадра (W, H), для начала равные (None, None). В следующей строке мы используем метод cv2.dnn.blobFromImage() для загрузки кадров в пакет и затем прогоняем этот пакет через нейронную сеть. Также внутри этой функции выполняется вычитание, масштабирование и обмен каналами в кадре.
1 2 3 4 5 6 7 8 |
(H, W) = (None, None) frame = image.copy() if W is None or H is None: (H, W) = frame.shape[:2] blob = cv2.dnn.blobFromImage(frame, 1 / 255.0, (416, 416), swapRB=True, crop=False) neural_net.setInput(blob) starttime = time.time() layerOutputs = neural_net.forward(ln) |
Выход слоя в алгоритме YOLO состоит из набора значений. Эти значения помогают нам определить класс, к которому относится анализируемый объект. Когда мы на изображении будем обнаруживать людей, мы будем устанавливать класс метки как "person". Для каждого обнаруженного человека мы будем формировать ограничивающий его прямоугольник, для которого нам будут известны координаты его центра X center и Y center, ширина (Width) и высота (Height).
1 2 3 4 5 6 7 8 9 10 11 |
scores = detection[5:] maxi_class = np.argmax(scores) confidence = scores[maxi_class] if LABELS[maxi_class] == "person": if confidence > 0.5: box = detection[0:4] * np.array([W, H, W, H]) (centerX, centerY, width, height) = box.astype("int") x = int(centerX - (width / 2)) y = int(centerY - (height / 2)) outline.append([x, y, int(width), int(height)]) confidences.append(float(confidence)) |
После этого мы будем рассчитывать расстояние между центром текущего анализируемого прямоугольника и центрами другим обнаруженных прямоугольников. Если эти прямоугольники будут слишком близко друг к другу, мы будем изменять status на true.
1 2 3 4 5 6 7 8 |
for i in range(len(center)): for j in range(len(center)): close = Check(center[i], center[j]) if close: pairs.append([center[i], center[j]]) status[i] = True status[j] = True index = 0 |
В следующем фрагменте кода мы будем рисовать прямоугольники вокруг обнаруженных людей, используя размеры этих прямоугольников, определенные нами ранее. Затем мы будем проверять находятся ли эти прямоугольники на безопасном друг от друга расстоянии или нет. Если расстояние между прямоугольниками маленькое, то мы будем рисовать их красным цветом, если же расстояние между ними большое – мы будем рисовать их зеленым цветом.
1 2 3 4 5 6 |
(x, y) = (outline[i][0], outline[i][1]) (w, h) = (outline[i][2], outline[i][3]) if status[index] == True: cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 0, 150), 2) elif status[index] == False: cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2) |
Затем в цикле мы будем считывать каждый кадр видеопотока и для каждого кадра будем рассчитывать расстояние между людьми на нем.
1 2 3 4 5 6 7 8 9 10 11 |
ret, frame = cap.read() if not ret: break current_img = frame.copy() current_img = imutils.resize(current_img, width=480) video = current_img.shape frameno += 1 if(frameno%2 == 0 or frameno == 1): Setup(yolo) ImageProcess(current_img) Frame = processedImg |
Далее мы будем использовать функцию cv2.VideoWriter() для хранения видео выхода в позиции, определенной переменной opname, которую мы определили ранее.
1 2 3 4 |
if create is None: fourcc = cv2.VideoWriter_fourcc(*'XVID') create = cv2.VideoWriter(opname, fourcc, 30, (Frame.shape[1], Frame.shape[0]), True) create.write(Frame) |
Тестирование работы проекта
После того, как код программы будет готов, откройте терминал платы Raspberry Pi и его помощью перейдите в каталог с вашим проектом. Код программы, модель Yolo и видео для теста (demo video) должны располагаться в одном и том же каталоге как показано на следующем рисунке.
Каталог с YoloV3 мы можете скачать по этой ссылке, видео для тестирования работы проекта можно скачать с Pexels, код программы находится в конце данной статьи, его нужно просто поместить в файл с расширением .py и скопировать этот файл в каталог проекта.
Когда вы будете находиться в каталоге со всеми файлами вашего проекта, выполните в нем следующую команду чтобы запустить код программы на выполнение:
1 |
python3 detector.py |
Автор проекта тестировал программу на видео, полученном от Pexels. В процессе теста он обнаружил, что число кадров в секунду (FPS) было достаточно маленьким, а на обработку всего видео уходило от 10 до 11 минут. Более подробно работу проекта вы можете посмотреть на видео, приведенном в конце статьи.
Вместо использования готового видео вы можете использовать видеопоток, получаемый с камеры Raspberry Pi. В этом случае в 98-й строчке кода необходимо заменить cv2.VideoCapture(input) на cv2.VideoCapture(0).
Исходный код программы на Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 |
import numpy as np import cv2 import imutils import os import time def Check(a, b): dist = ((a[0] - b[0]) ** 2 + 550 / ((a[1] + b[1]) / 2) * (a[1] - b[1]) ** 2) ** 0.5 calibration = (a[1] + b[1]) / 2 if 0 < dist < 0.25 * calibration: return True else: return False def Setup(yolo): global net, ln, LABELS weights = os.path.sep.join([yolo, "yolov3.weights"]) config = os.path.sep.join([yolo, "yolov3.cfg"]) labelsPath = os.path.sep.join([yolo, "coco.names"]) LABELS = open(labelsPath).read().strip().split("\n") net = cv2.dnn.readNetFromDarknet(config, weights) ln = net.getLayerNames() ln = [ln[i[0] - 1] for i in net.getUnconnectedOutLayers()] def ImageProcess(image): global processedImg (H, W) = (None, None) frame = image.copy() if W is None or H is None: (H, W) = frame.shape[:2] blob = cv2.dnn.blobFromImage(frame, 1 / 255.0, (416, 416), swapRB=True, crop=False) net.setInput(blob) starttime = time.time() layerOutputs = net.forward(ln) stoptime = time.time() print("Video is Getting Processed at {:.4f} seconds per frame".format((stoptime-starttime))) confidences = [] outline = [] for output in layerOutputs: for detection in output: scores = detection[5:] maxi_class = np.argmax(scores) confidence = scores[maxi_class] if LABELS[maxi_class] == "person": if confidence > 0.5: box = detection[0:4] * np.array([W, H, W, H]) (centerX, centerY, width, height) = box.astype("int") x = int(centerX - (width / 2)) y = int(centerY - (height / 2)) outline.append([x, y, int(width), int(height)]) confidences.append(float(confidence)) box_line = cv2.dnn.NMSBoxes(outline, confidences, 0.5, 0.3) if len(box_line) > 0: flat_box = box_line.flatten() pairs = [] center = [] status = [] for i in flat_box: (x, y) = (outline[i][0], outline[i][1]) (w, h) = (outline[i][2], outline[i][3]) center.append([int(x + w / 2), int(y + h / 2)]) status.append(False) for i in range(len(center)): for j in range(len(center)): close = Check(center[i], center[j]) if close: pairs.append([center[i], center[j]]) status[i] = True status[j] = True index = 0 for i in flat_box: (x, y) = (outline[i][0], outline[i][1]) (w, h) = (outline[i][2], outline[i][3]) if status[index] == True: cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 0, 150), 2) elif status[index] == False: cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2) index += 1 for h in pairs: cv2.line(frame, tuple(h[0]), tuple(h[1]), (0, 0, 255), 2) processedImg = frame.copy() create = None frameno = 0 filename = "newVideo.mp4" yolo = "yolov3/" opname = "output2.avi" cap = cv2.VideoCapture(filename) time1 = time.time() while(True): ret, frame = cap.read() if not ret: break current_img = frame.copy() current_img = imutils.resize(current_img, width=480) video = current_img.shape frameno += 1 if(frameno%2 == 0 or frameno == 1): Setup(yolo) ImageProcess(current_img) Frame = processedImg cv2.imshow("Image", Frame) if create is None: fourcc = cv2.VideoWriter_fourcc(*'XVID') create = cv2.VideoWriter(opname, fourcc, 30, (Frame.shape[1], Frame.shape[0]), True) create.write(Frame) if cv2.waitKey(1) & 0xFF == ord('s'): break time2 = time.time() print("Completed. Total Time Taken: {} minutes".format((time2-time1)/60)) cap.release() cv2.destroyAllWindows() |