Время прочтения: 4 мин.

Исходные данные

Имея доступ к API камер видеонаблюдения можно запрашивать картинки в реальном времени с желаемой периодичностью. В зонах самообслуживания клиентов нет сидячих\лежачих мест, поэтому было принято информировать дежурного при присутствии более 5 минут в кадре человека с согнутыми коленями.  Мы собрали и разметили датасет с АЭ включающий около 1000 изображений. В процессе улучшения моделей датасет был расширен до 12000 изображений.

Алгоритм работы

Первым делом пришла мысль определять «вертикальность» положения человека. Но это оказалось невозможным ввиду искажения изображения камерами и их поворота относительно горизонта. Встречались камеры, повернутые на 90-180 градусов.

Было принято решение опробовать библиотеку PyTorch PoseEstimator. Она позволяет вычислить положения основных суставов человека.

На выходе мы получаем координаты точек и уверенность алгоритма в их определении. Имея координаты трех точек в 2х-мерном пространстве можно вычислить угол между двумя векторами из любой из них:

def CalcDec(points):
    p1=points[0]
    p2=points[1]
    p3=points[2]
    def CaclLen(x1,y1,x2,y2):
        return sqrt((x1-x2)**2+(y1-y2)**2)
    lenP21=CaclLen(p2[0],p2[1],p1[0],p1[1])
    lenP23=CaclLen(p2[0],p2[1],p3[0],p3[1])
    lenP13=CaclLen(p1[0],p1[1],p3[0],p3[1])
    result=acos((lenP21**2+lenP23**2-lenP13**2)/(2*lenP21*lenP23))*57.2958
    return result

Я вычислял изгиб в точках 9 и 12 (коленных суставах) и точках 8 и 11. Если угол меньше 120° алгоритм относил человека к АЭ.

К сожалению, библиотека работала медленно и качество распознавания ключевых точек нас не устраивало. Большинство камер искажают картинку, т.к. расположены под потолком и в зимнее время люди плотно одеты. Все это не способствовало правильной работе модели:

Ключевые точки определены неверно, вычислив углы алгоритм решил, что это сидящий человек.

Увеличить быстродействие удалось подавая на вход PyTorch PoseEstimator не кадр целиком, а обрезанные изображения, переобучить модель не представлялось возможным в виду сложности разметки датасета и отсутсвия желанияJ.

У нас уже был опыт работы с библиотекой Yolo. Она неплохо справляется с задачей object detection. Я взял предобученную сеть, и переобучил ее на своих данных. Yolo гораздо быстрее PoseEstimator и показала неплохой результат в классификации людей. Но, не достаточно хороший – присутствовали ошибки первого и второго рода. Мы несколько раз расширяли обучающую выборку, но значительно улучшить качество помогло только внедрение классификатора RESNET. Он так же, как Yolo был переобучен для разделения изображений на 2 класса – АЭ и другие люди.

В результате алгоритм выглядит следующим образом:

Исходный код метода predict:

def Predict():
    batch=len(frames_queue.queue)
    global frames_processed_cnt  # Очередь из кадров для обработки
    files_list = []
    channel_ids_list = []
    processedFrames=0
    for i in range(batch):
        try:
            start=datetime.now()
            item = frames_queue.get_nowait()
            channelName='channel_'+str(item['channel_id'])
            # YOLO. находим в кадре положения AЭ и других людей
            predictResult,image=yolo_model.predictPath(item['img_path'])
            cntHomeless=0
            classBoxes=dict()
            classBoxes['homeless']=[]
            classBoxes['people']=[]
            classBoxes['seated']=[]
            for predict in predictResult['homeless']+predictResult['people']+predictResult['seated']:
                # классификатор RESNET
                classifireLabel,classifireConf=homeless_classifire.predict_img(predict.image) 
                if classifireConf>0.4:
                    if classifireLabel==0: #people
                        classBoxes['people'].append(predict)
                    elif classifireLabel==1: #homeless
                        classBoxes['homeless'].append(predict)
                    elif classifireLabel==2: #seated
                        classBoxes['seated'].append(predict)
            fileName=os.path.basename(item['img_path'])
            if len(classBoxes['seated'])>0:
                imageWithBoxes=Yolo.drawBoxes(image,classBoxes,yolo_model.colors)
                SaveResult(imageWithBoxes,item['datetime'],item['server'],channelName,'seated_'+fileName)
            if len(classBoxes['homeless'])>0:
                imageWithBoxes=Yolo.drawBoxes(image,classBoxes,yolo_model.colors)
                SaveResult(imageWithBoxes,item['datetime'],item['server'],channelName,'homeless_'+fileName)
            else:
                # для каждого класса рисую скелет и проверяю на принадлежность к классу АЭ
                for i,people in enumerate(predictResult['people']):
                    #PoseEstimator                    
                    candidate, subset, peopleBodyImage = PredictBody(people.image)
                    finder=HomelessFinder(candidate,subset)
                    if finder.CntHomeless()>0:
                        cntHomeless+=finder.CntHomeless()
                        SaveResult(peopleBodyImage,item['datetime'],item['server'],channelName,"homeless_pose_"+fileName)
                # нарисовать все скелеты на исходном кадре если найден хотябы один АЭ
                if cntHomeless>0:
                    fileName=os.path.basename(item['img_path'])
                    image = cv2.imread(item['img_path'])
                    _,_,allBodyImage = PredictBody(image)
                    SaveResult(allBodyImage,item['datetime'],item['server'],channelName,fileName)
            frames_processed_cnt+=1
            processedFrames+=1
            if cntHomeless>0:
                print(datetime.now()-start,"Человек в кадре: ",len(predictResult['people'])," АЭ:",cntHomeless)
        except Exception as e:
            print('Ошибка: ',e,item)

При нахождении Yolo АЭ – кадр отправляется в отчет. В остальных случаях все изображения найденных людей отправляются на вход обученному классификатору RESNET, который так же относит объект к одному из классов. Если и этот алгоритм не нашел АЭ происходит определение ключевых точек человека с помощью библиотеки PyTorch PoseEstimator. На выходе мы получаем координаты основных суставов и вычисляем углы в ключевых точках. Если углы <120 градусов отправляем кадр в отчет.

Последним этапом происходит анализ отчета, если присутствие АЭ продолжается в течении заданного времени происходит информирование дежурного.

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