Распознавание лиц с IP камер. Выбор, хранение и обработка данных для принятия решения (Face recognition from IP cameras. Selection, storage and processing of data for decision making)

Автор: | 18.12.2019

Tags:  IP камера RTSP R-FCNN распознавание лиц видео-поток landmarks Python DLIB

Введение
Подключение IP камеры и доступ к видео-потоку
Решение проблемы торможения видео-потока от IP камеры
Цикличное переключение видеопотоков от разных камер
Детекция лиц с landmarks точками
Формализация задачи распознавания лица
Выбор из видео-потока корректных изображений лиц
Выбор признаков лица
Принятие решения при распознавании лица
Чтение и запись данных
Полезные ссылки

Введение

Получил комментарий к одной из статей по распознаванию лиц: «Как можно внедрить алгоритм распознавания лиц в домашнего робота? В базе данных робота хранится фото всех членов семьи с их именами. Робот через камеру видит кого-нибудь из них, находит у себя в базе это фото и определяет, кто перед ним находится. Далее обычное голосовое приветствие. Ну, это уже по другой программе…»

Ответил: «В базе данных лучше хранить признаки объектов (лиц), а не фото. В качестве признаков могут быть расстояния между характерными точками лица либо набор признаков (дескриптор лица), получаемых на выходе уже обученной сверточной нейронной сети (см.  Face recognition. Python, DLIB). Для оценки похожести полученного списка признаков с теми, что уже есть в базе данных, можно использовать Евклидово расстояние

Очевидно, что такой ответ не дает полной картины всех проблем, которые необходимо решать. Поэтому решил написать статью на эту тему с акцентом на практическое применение распознавания лиц.

В предыдущих работах исследования по распознаванию лиц проводились на основе видео-потока, считываемого с встроенной в notebook Web-камеры — через метод cv2.VideoCapture(0). Дальнейшие исследования  будут проводиться с подключением  IP камер по следующей схеме:

Подключение IP камеры и доступ к видео-потоку

Код для подключения видео-потока  с IP камеры приводится ниже.

import cv2
import numpy as np
#cap = cv2.VideoCapture(0)
cap = cv2.VideoCapture('rtsp://192.168.0.100:554/user=admin_password=tlJwpbo6_channel=0_stream=0.sdp?real_stream')
while(True):
        ret, frame = cap.read()
        cv2.imshow('frame',frame)
        if cv2.waitKey(1) & 0xFF == ord('q'):
           break
cap.release()
cv2.destroyAllWindows()

Для того, чтобы установить камеру и определить значение параметра (адрес доступа к видеопотоку камеры) в методе cv2.VideoCapture(..?????…)  рекомендую прослушать информацию по ссылкам:

Из этой информации я выделил следующую последовательность действий:

  1. Подключаю IP камеру напрямую к роутеру (по кабелю витая пара через разъем LAN с обоих сторон).
  2. Через браузер вхожу в настройки роутера (192.168.0.1>admin-admin>DHCP>Список клиентов DHCP). Здесь определяю локальный адрес камеры (у меня 192.168.0.100).
  3. Выполняю настройки браузера Internet Explorer (выключаю защищенный режим и допускаю использование ActiveX элементов).
  4. Через браузер Internet Explorer вхожу в настройки  камеры (192.168.0.100>admin- у меня без пароля). При этом предлагается подгрузить плагины ActiveX, разрешаю. В итоге должно запуститься приложение NETSurveillince WEB вместе с видео от камеры. Возможно, что после входа будет только серый экран. Это означает, что вы не выполнили все указанные выше настройки браузера Internet Explorer.
  5. В настройках камеры выбираю (Devicecfg>пиктограмма шестеренка>NetService>dblclick на RTSP). В окне отображается порт камеры — 554.
  6. Устанавливаю и запускаю программу ONWIF. В ней нахожу мою камеру по IP адресу и нажимаю Livevideo. Внизу окна для видео копирую адрес к видеопотоку моей камеры.

До сих пор у меня камера была подключена к роутеру по проводу. Для подключения ее к беспроводной WiFi сети выполняю следующие действия:

  1. Через браузер Internet Explorer вхожу в настройки  камеры (192.168.0.100>admin- у меня без пароля).
  2. Далее выбираю   Devicecfg>пиктограмма шестеренка>NetService>dblclick на Wifi>Search>моя сеть (SV)>OK.

Перед нажатием кнопки ОК необходимо установить IP адрес, как у своей сети. У меня 3-е число в IP адресе должно быть 0. В противном случае камера не подключится.

Теперь  снова вхожу через браузер в настройки роутера (192.168.0.1>admin-admin>DHCP>Список клиентов DHCP). Здесь определяю новый локальный адрес камеры:  192.168.0.101. Раньше у камеры был адрес 192.168.0.100. Т.е., адреса меняются (динамические) при каждом подключении камеры.

Решение проблемы торможения видео-потока с IP камеры

На рисунке слева отображается кадр из видеопотока в моей программе (с определением лица и характерных точек). Справа приложение NETSurveillince WEB, запускаемое в браузере Internet Explorer, здесь можно настроить  некоторые из параметров видео-потока.

При первых запусках приложения обнаружил, что происходит торможение видео-потока с IP камеры при подключении детектора лица (detector.detect_faces(frame) ). Возможное решение этой проблемы:

  • обрабатывать не все кадры в потоке;
  • уменьшать размер кадров.

Ниже приводится код, в котором комбинируются оба подхода.

import cv2
import numpy as np
from mtcnn.mtcnn import MTCNN
detector = MTCNN()
cap = cv2.VideoCapture('rtsp://192.168.0.100:554/user=admin_password=tlJwpbo6_channel=0_stream=0.sdp?real_stream')
while(True):
    ret, frame = cap.read()
    for i in range(20): # skip 20 frames
        cap.grab()
    grabbed, frame = cap.read()
 
    scale_percent = 50 # percent of original size
    width = int(frame.shape[1] * scale_percent / 100)
    height = int(frame.shape[0] * scale_percent / 100)
    dim = (width, height)
    # resize image
    frame = cv2.resize(frame, dim, interpolation = cv2.INTER_AREA)
    #frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

    result = detector.detect_faces(frame)

    # Result is an array with all the bounding boxes detected.
    bounding_box = result[0]['box']
    keypoints = result[0]['keypoints']
    cv2.rectangle(frame,
          (bounding_box[0], bounding_box[1]),
          (bounding_box[0]+bounding_box[2], bounding_box[1] + bounding_box[3]),
          (0,155,255),
          1)
    cv2.circle(frame,(keypoints['left_eye']), 3, (0,155,255), 1)
    cv2.circle(frame,(keypoints['right_eye']), 2, (0,155,255), 1)
    cv2.circle(frame,(keypoints['nose']), 3, (0,155,255), 1)
    cv2.circle(frame,(keypoints['mouth_left']), 3, (0,155,255), 1)
    cv2.circle(frame,(keypoints['mouth_right']), 3, (0,155,255), 1)

    cv2.line(frame,(keypoints['left_eye']),keypoints['right_eye'], (0,0,255), 1)
    cv2.line(frame,(keypoints['left_eye']),keypoints['nose'], (0,255,0), 1)
    cv2.line(frame,(keypoints['right_eye']),keypoints['nose'], (255,0,0), 1)

    cv2.imshow('frame',frame)
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break
cap.release()
cv2.destroyAllWindows()

Цикличное переключение видео-потоков от разных камер

Процессор компьютера последовательно обрабатывает информацию. А как быть, если видео-потоки одновременно поступают от разных камер? Можно решить задачу через многопоточность. Но здесь свои проблемы, которые могут даже ухудшить результат. Поэтому решаю задачу по простому — через последовательное переключение видео-потоков от разных камер.

Ниже приводится код, обеспечивающий переключение клавишами 1 и 2 видеопотока от web-камеры, встроенной в notebook, и видеопотока от IP-камеры, подключенной в локальную сеть.   Для создания GUI используется библиотека Tkinter. Кадры с камер отображаются в элементе Canvas.

import sys
# Tkinter selector
if sys.version_info[0] < 3:
    from Tkinter import *
    import Tkinter as tk
else:
    from tkinter import *
    import tkinter as tk
import numpy as np
import cv2
from PIL import Image, ImageTk

def opencvToTk(frame):
    """Convert an opencv image to a tkinter image, to display in canvas."""
    rgb_image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
    pil_img = Image.fromarray(rgb_image)
    tk_image = ImageTk.PhotoImage(image=pil_img)
    return tk_image
def drawCamera(canvas, data):
    data.tk_image = opencvToTk(data.frame)
    canvas.create_image(data.width / 2, data.height / 2, image=data.tk_image)
def redrawAll(canvas, data,root):
    _, data.frame = data.camera.read()
    # Redrawing code
    canvas.delete(ALL)
    drawCamera(canvas,data)
    root.after(5, lambda: redrawAll(canvas, data,root))

def new_camera1(data):
      data.camera = cv2.VideoCapture(0)
def new_camera2(data):
      data.camera = cv2.VideoCapture('rtsp://192.168.0.102:554/user=admin_password=tlJwpbo6_channel=0_stream=0.sdp?real_stream')
def keyPressed(event, data):
    if event.keysym == "1":
        new_camera1(data)
    if event.keysym == "2":
        new_camera2(data)
    pass
class Struct(object): pass

def run(width, height): 
    root = Tk() 
    data = Struct()
    data.camera_index = 0
    data.camera = cv2.VideoCapture(0)
    #data.camera = cv2.VideoCapture('rtsp://192.168.0.101:554/user=admin_password=tlJwpbo6_channel=0_stream=0.sdp?real_stream')
    data.width = width
    data.height = height
 
    canvas = Canvas(root, width=width, height=height)
    canvas.pack(side=LEFT)
    redrawAll(canvas, data,root)
    # Переключение камеры через 10сек
    #root.after(10000, lambda: new_camera2(data))
    root.bind("<Key>", lambda event: keyPressed(event, data)) 
    # Loop tkinter
    root.mainloop() 
    # Once the loop is done, release the camera.
    print("Releasing camera!")
    data.camera.release()
if __name__ == "__main__":
    run(500, 500)

Переключение камер клавишами можно заменить автоматическим переключением — при каждом вызове функции redrawAll

def redrawAll(canvas, data,root): 
 if data.camera_index == 0:
    data.camera_index = 1
    data.camera = cv2.VideoCapture('rtsp://192.168.0.101:554/user=admin_password=tlJwpbo6_channel=0_stream=0.sdp?real_stream')
 else:
    data.camera_index = 0
    data.camera = cv2.VideoCapture(0)
    _, data.frame = data.camera.read()
    # Redrawing code
    canvas.delete(ALL)
    drawCamera(canvas,data)
    root.after(5, lambda: redrawAll(canvas, data,root))

Можно также устанавливать период переключения камеры через определенные промежутки времени при помощи таймера. Но это уже совсем другая история.

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

Детекция лиц с landmarks точками

Пришло время добавить в приложение детекцию лиц с landmarks точками. Считается наиболее быстродействующим и точным детектор R-FCNN:

Вот его я и использую в своем комплексном приложении. Детектор R-FCNN подключается через библиотеку DLIB.

import sys
# Tkinter selector
if sys.version_info[0] < 3:
   from Tkinter import *
   import Tkinter as tk
else:
   from tkinter import *
   import tkinter as tk
import numpy as np
import cv2
from PIL import Image, ImageTk
import dlib

def detect_faces_and_landmarks (data):
    # Конвертирование изображения в черно-белое
    grayFrame = cv2.cvtColor(data.frame, cv2.COLOR_BGR2GRAY)
    # Обнаружение лиц и построение прямоугольного контура
    faces = data.detector(grayFrame)
    # Обход списка всех лиц попавших на изображение
    for face in faces:
        # Выводим количество лиц на изображении
        cv2.putText(data.frame, "{} face(s) found".format(len(faces)), (10, 40), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 0), 2)
        # Получение координат вершин прямоугольника и его построение на изображении
        x1 = face.left()
        y1 = face.top()
        x2 = face.right()
        y2 = face.bottom()
        cv2.rectangle(data.frame, (x1, y1), (x2, y2), (255, 0, 0), 1) 
        # Получение координат контрольных точек и их построение на изображении
        landmarks = data.predictor(grayFrame, face)
        for n in range(0, 68):
            x = landmarks.part(n).x
            y = landmarks.part(n).y
            cv2.circle(data.frame, (x, y), 1, (255, 0, 0), 1)

def skip_and_scale_frames(data,skip,scale):
    for i in range(skip): # skip frames
      data.camera.grab()
    grabbed, data.frame = data.camera.read()

    scale_percent = scale # percent of original size
    data.width = int(data.frame.shape[1] * scale_percent / 100)
    data.height = int(data.frame.shape[0] * scale_percent / 100)
    dim = (data.width, data.height)
    # resize image
    data.frame = cv2.resize(data.frame, dim, interpolation = cv2.INTER_AREA)
    #data.frame = cv2.cvtColor(data.frame, cv2.COLOR_BGR2GRAY)

def opencvToTk(frame):
    """Convert an opencv image to a tkinter image, to display in canvas."""
    rgb_image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
    pil_img = Image.fromarray(rgb_image)
    tk_image = ImageTk.PhotoImage(image=pil_img)
    return tk_image
def drawCamera(canvas, data):
    data.tk_image = opencvToTk(data.frame)
    canvas.create_image(data.width / 2, data.height / 2, image=data.tk_image)

def redrawAll(canvas, data,root): 
    if data.camera_index == 0:
        data.camera_index = 1
        data.camera = cv2.VideoCapture('rtsp://192.168.0.102:554/user=admin_password=tlJwpbo6_channel=0_stream=0.sdp?real_stream')
    else:
        data.camera_index = 0
        data.camera = cv2.VideoCapture(0)
 
    _, data.frame = data.camera.read()
    skip_and_scale_frames(data,40,50)

    detect_faces_and_landmarks (data)

    # Redrawing code
    canvas.delete(ALL)
    drawCamera(canvas,data)
    root.after(1, lambda: redrawAll(canvas, data,root)) 
def new_camera1(data):
      data.camera = cv2.VideoCapture(0)
def new_camera2(data):
      data.camera = cv2.VideoCapture('rtsp://192.168.0.102:554/user=admin_password=tlJwpbo6_channel=0_stream=0.sdp?real_stream')
def keyPressed(event, data):
    if event.keysym == "1":
        new_camera1(data)
    if event.keysym == "2":
        new_camera2(data)
    pass
class Struct(object): pass

def run(width, height): 
    root = Tk()
 
    data = Struct()
    # Подключение детектора, настроенного на поиск человеческих лиц
    data.detector = dlib.get_frontal_face_detector()
    data.predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat")

    data.camera_index = 0
    data.camera = cv2.VideoCapture(0)
    #data.camera = cv2.VideoCapture('rtsp://192.168.0.101:554/user=admin_password=tlJwpbo6_channel=0_stream=0.sdp?real_stream')
    data.width = width
    data.height = height
 
    canvas = Canvas(root, width=width, height=height)
    canvas.pack(side=LEFT)
 
    redrawAll(canvas, data,root)
    # Переключение камеры через 10сек
    #root.after(10000, lambda: new_camera2(data))
    root.bind("<Key>", lambda event: keyPressed(event, data)) 
    # Loop tkinter
    root.mainloop() 
    # Once the loop is done, release the camera.
    print("Releasing camera!")
    data.camera.release()
if __name__ == "__main__":
    run(500, 500)

Результат запуска программы:

Формализация задачи распознавания лиц

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

Компьютер понимает язык цифр, может их сравнивать. Цифровое фото представляет описание цвета множества точек (см. BMP format). Сравнивать все точки фото из базы данных с точками кадра (frame), полученного от веб камеры, едва ли имеет смысл по ряду причин:

  • точки лица занимают только часть изображения;
  • цвет точек может меняться в зависимости от освещения;
  • положение лица относительно камеры разное.

Известны методы (реализованы в библиотеках), которые позволяют выделить на изображении фрагмент лица и особые точки на нем (см. Обнаружение лица и выделение характерных точек). Расстояния между этими точками могут служить признаками, чтобы различать одно лицо от другого (см. Распознавание лиц на основе OpenCV для C++).

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

Значения признаков могут быть зашиты в программу как обычные условия (см. Распознавание лиц на основе OpenCV для C++). Также значения признаков могут определяться через обучение  (см. Распознавание фигур на основе «выборки для обучения»), затем они вместе с идентификатором объекта  записываются в  базу данных (текстовый файла, как описано в разделе Создание базы данных признаков из статьи Идентификация образов по цвету, текстуре и форме).

Один из важнейших моментов  задачи распознавания объектов — как определить субъекта среди претендентов на сходство с ним по множеству признаков. Обычно степень соответствия распознаваемого лица и лиц из базы данных определяется через Евклидово расстояние. Возможны и другие подходы, они  рассмотрены в статьях Идентификация по множеству признаков. Основы и Идентификация по множеству признаков при помощи нейросети»

Машинное обучение на основе нейронной сети приходит на смену традиционному алгоритмическому подходу во многих задачах, включая и распознавание лиц. Если в классическом подходе к распознаванию программист анализирует объект для распознавания и выделяет отличительные признаки, то  нейронные сети сами решают эту задачу (см. Нейросетевая видеоаналитика. Как это работает. Возможности и ограничения.).

В статьях  Машинное обучение распознавать поворот лица а также Адекватность и оптимальность нейронной сети  затронуты проблемы, которые вплотную относятся и к задаче распознавания лица на основе НС.

Методы получения признаков и распознавания лиц на основе нейронных сетей сегодня считаются более предпочтительными (см. в youtube лекцию Андрея Созыкина  Распознавание человека по лицу). Хотя, могут быть и компромиссные решения. Например, для точности сравнения лиц рекомендуется их выравнивать  к положению в анфас. Это можно выполнить на основе  3D-реконструкции лица по характерным точкам. По характерным точкам также можно отфильтровывать не совсем корректные изображения (см.  Выбор признаков для фильтрации изображений).

Выбор из видеопотока корректных изображений лица

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

Из видеопотока нужно отбирать только корректные изображения, вот такие: 

Формальные признаки для выбора корректных изображений лиц — параллельность линий, которые проходят через наиболее характерные симметричные точки лица.

Как формализуется признак параллельности можно посмотреть в статье Алгоритмы распознавания геометрических фигур.

При определении признаков обратите внимание еще на  одну особенность детектора точек.

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

Выбор признаков лица

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

  1. классический, при котором вы самостоятельно определяете признаки;
  2. выбор признаков с помощью нейронной сети.

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

Будем выбирать признаки на основе исследований ученых-антропологов, которые выделили различные типы и особенности лица.

Эти особенности могут быть в определенной степени описаны расстояниями между характерными  точками лица, которые генерирует для нас детектор лица (R-FCNN).

Расстояние-признак должно быть нормализовано — соотнесено к другому расстоянию, которое пропорционально изменяется при движении лица относительно камеры. Для этого оба расстояния, по возможности, должны быть:

  • параллельными;
  • близкими;
  • соизмеримыми (отношение около 1).

Разбиваем все признаки на группы, в каждой из которой составляются пары размеров, больший из которых используется для нормализации. Если в группе n расстояний, то признаков будет (n-1).

  1. f1= L4_12/L8_27             овал лица
  2. f2= L7_9/L6_10               овал лица
  3. f3= L5_11/L4_12              овал лица
  4. f4= L3_13/L0_16             овал лица
  5. f5= Leyes/L19_24            овал лица
  6. f6= L19_24/L17_26         овал лица
  7. f7= L21_22/L19_24         овал лица
  8. f8= LeyeL_4/LeyeL_6    овал лица
  9. f9= LeyeL_6/LeyeL_8     овал лица
  10. f10= L13_16/L13_26       овал лица
  11. f11= L31_35/L48_54       ширина носа и рта
  12. f12= L27_33/L8_33         длина носа и подбородка
  13. f13= L33_51/L8_57          положение рта
  14. f14= L51_57/L8_57          толщина рта
  15. f15= L43_47/L44_46       ширина глаз
  16. f16= L44_46/L42_45      ширина и длина глаз
  17. f17= LeyeL_17/LeyeL_18       форма бровей
  18. f18= LeyeL_18/LeyeL_19      форма бровей
  19. f19= LeyeL_19/LeyeL_20     форма бровей
  20. f20= LeyeL_20/LeyeL_21     форма бровей
  21. f21= L33_50/L33_51        форма рта
  22. f22= L51_62/L66_57        форма рта
  23. f23= L49_61/L59_67        форма рта
  24. f24= L49_60/L59_60       форма рта

Силу признака можно оценить отношением D/d, где  d — допуск признака; D —  расстояние между соседними средними значениями признаков (подробнее см. Идентификация по множеству признаков).

Можно существенно увеличить силу признака, если брать его усредненные значения по нескольким кадрам. Для сравнения на рисунке слева приводятся значения признака f1, усредняемого с каждым кадром, и значения этого признака (неусредненные) на каждом из 20 кадров. На рисунке справа сопоставляются среднее значения признака f1 (для выборки от 10 до 20 кадров) и диапазон (Range) значений этого признака для 10 разных лиц.

Информация по другим  признакам приведена в таблицах 1 и 2.

В табл.2  диапазон (Range) разброса признаков для различных лиц отнесен к диапазону разброса признаков для одного лица (табл.1). По этому показателю легко оценить силу признака.

Итого у нас 24 признака, каждый из которых имеет  средний относительный диапазон значений  больше 10,  т.е., один признак может отсеивать  1/10 часть претендентов. Оценим, много это или мало?

Допустим, у нас есть около 1000000 претендентов, из которых необходимо распознать одного субъекта. Если по каждому признаку отсеивать 1/10 часть претендентов, то получим следующую геометрическую прогрессию:

1000000->100000->10000 ->1000->100->10->1.

Т.е., для распознавания субъекта достаточно использовать фильтр только из 6 признаков.  Еще 18 признаков остаются в резерве.

Принятие решений при распознавании лица

Для оценки соответствия признаков детектируемого лица субъекта (Subject)  признакам лица претендента (Applicant) из базы данных, можно использовать комплексный признак, например,  Евклидово расстояние. Но, прежде чем сравнивать субъекта и претендента по комплексному признаку, имеет смысл провести оценку схожести лиц по каждому из признаков в отдельности.

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

Такой подход дает возможность дифференцировано подходить к оценке каждой пары субъекта и претендента. Одна пара может отличаться шириной рта, другая — шириной носа. При сравнении только лишь по комплексному признаку такие специфические отличия субъекта и претендента могут быть нивелированы.

Таким образом, процесс распознавание лица разбивается на 2 этапа:

  1. Последовательная проверка лиц претендентов на схожесть с лицом субъекта по каждому из признаков в отдельности. Если разница между значениями признака претендента и субъекта окажется меньше допустимого значения, то претендент игнорируется.  Если все претенденты игнорируются на 1-м этапе, то 2-го этапа не будет. Субъект в этом случае сразу же добавляется в базу данных.
  2. Оценка степени схожести отобранных претендентов по комплексному признаку  (Евклидово расстояние).  На этом этапе выбирается один претендент из множества отобранных на 1 этапе.  Если Евклидово расстояние  для всех претендентов окажется больше допустимого значения. то игнорируются все претенденты. Субъект в этом случае добавляется в базу данных.

На первом этапе претенденты сначала обрабатываются наиболее сильным признаком, затем послабее. Это обеспечивает более быстрое отсеивание претендентов на сходство с субъектом.

Ниже приводится простой код для тестирования алгоритма по данным из таблиц 1 и 2.

Тестирование этапа 1:

limits = ["",0.005,0.004,0.002,0.002,0.006,0.006,0.001,0.005,0.001,0.011,
             0.005,0.028,0.008,0.005,0.019,0.022,0.010,0.025,0.007,0.026,
             0.010,0.018,0.012,0.019]
# subject = applicants[10]
subject = ["",0.938,0.564,0.851,0.882,0.865,0.622,0.260,0.736,0.848,0.911,
              0.454,0.748,0.557,0.264,1.004,0.333,1.138,1.307,1.036,0.759,
              0.912,0.362,0.979,0.846]
check = ["",1,1,1,1,1,1,1,1,1,1]

applicants = [
    ["1", 1.052,0.532,0.820,0.882,0.879,0.613,0.306,0.774,0.848,0.881,
          0.472,0.800,0.499,0.524,1.009,0.352,1.191,1.218,1.080,0.785,
          1.001,0.499,0.819,0.790],
    ["2", 0.918,0.540,0.820,0.891,0.822,0.638,0.297,0.743,0.841,0.939,
          0.521,0.750,0.499,0.357,1.009,0.383,1.081,1.297,1.166,0.795,
          0.976,0.467,0.800,0.867],
    ["3", 0.960,0.542,0.824,0.862,0.830,0.653,0.308,0.742,0.849,0.860,
          0.535,0.862,0.323,0.334,0.957,0.349,0.996,1.161,1.040,0.856,
          1.038,0.766,1.079,0.823],
    ["4", 1.184,0.509,0.820,0.890,0.845,0.604,0.243,0.836,0.862,0.875,
          0.560,0.921,0.420,0.235,1.0, 0.281,1.236,1.252,0.944,0.794,
          1.0, 0.542,0.880,0.922],
    ["5", 1.035,0.548,0.842,0.898,0.853,0.619,0.268,0.821,0.891,0.855,
          0.525,0.755,0.381,0.385,0.857,0.333,1.300,1.379,0.872,0.676,
          0.969,0.496,0.868,0.810],
    ["6", 0.948,0.532,0.826,0.872,0.766,0.655,0.379,0.812,0.874,0.741,
          0.523,0.717,0.406,0.406,0.986,0.264,1.055,1.114,1.043,0.976,
          1.041,0.75 ,0.877,1.0 ],
    ["7", 0.999,0.535,0.838,0.866,0.901,0.626,0.338,0.789,0.885,0.796,
          0.478,0.686,0.540,0.352,1.0 ,0.423,1.235,1.215,0.900,0.806,
          0.949,0.755,0.749,1.220],
    ["8", 1.047,0.517,0.819,0.944,0.837,0.622,0.217,0.787,0.854,0.940,
          0.501,0.721,0.420,0.301,1.0 ,0.207,1.177,1.127,0.960,0.821,
          1.026,0.5 ,0.922,0.875],
    ["9", 1.019,0.517,0.793,0.907,0.763,0.656,0.334,0.781,0.837,0.957,
          0.551,0.651,0.343,0.396,1.0 ,0.250,1.072,1.242,1.212,0.901,
          1.029,0.4 ,0.827,0.780],
    ["10",0.938,0.564,0.851,0.882,0.865,0.622,0.260,0.736,0.848,0.911,
          0.454,0.748,0.557,0.264,1.004,0.333,1.138,1.307,1.036,0.759,
          0.912,0.362,0.979,0.846]]

for i in range(0, 10):
    if(check[i] ==1):
       for j in range(1, 25):
         dif= abs(subject[j]-applicants[i][j])
         tol = limits[j]
         if (dif>tol):
             check[i]=0 
for i in range(0, 10):
        if(check[i] ==1):
            print(applicants[i][0])

Результат (претендент №10) печатается в окне консоли:

Тестирование этапа 2 (данные используются те же):

import cmath
# subject = applicants[10]
subject = [0.939,0.565,0.852,0.882,0.865,0.622,0.260,0.736,0.848,0.911,
           0.454,0.748,0.557,0.264,1.004,0.333,1.138,1.307,1.036,0.759,
           0.912,0.362,0.979,0.846]
dif = [0,0,0,0,0,0,0,0,0,0]
applicants = [ 
    [1.052,0.532,0.820,0.882,0.879,0.613,0.306,0.774,0.848,0.881,
     0.472,0.800,0.499,0.524,1.009,0.352,1.191,1.218,1.080,0.785,
     1.001,0.499,0.819,0.790],
    [0.918,0.540,0.820,0.891,0.822,0.638,0.297,0.743,0.841,0.939,
     0.521,0.750,0.499,0.357,1.009,0.383,1.081,1.297,1.166,0.795,
     0.976,0.467,0.800,0.867],
    [0.960,0.542,0.824,0.862,0.830,0.653,0.308,0.742,0.849,0.860,
     0.535,0.862,0.323,0.334,0.957,0.349,0.996,1.161,1.040,0.856,
     1.038,0.766,1.079,0.823],
    [1.184,0.509,0.820,0.890,0.845,0.604,0.243,0.836,0.862,0.875,
     0.560,0.921,0.420,0.235,1.0, 0.281,1.236,1.252,0.944,0.794,
     1.0, 0.542,0.880,0.922],
    [1.035,0.548,0.842,0.898,0.853,0.619,0.268,0.821,0.891,0.855,
     0.525,0.755,0.381,0.385,0.857,0.333,1.300,1.379,0.872,0.676,
     0.969,0.496,0.868,0.810],
    [0.948,0.532,0.826,0.872,0.766,0.655,0.379,0.812,0.874,0.741,
     0.523,0.717,0.406,0.406,0.986,0.264,1.055,1.114,1.043,0.976,
     1.041,0.75 ,0.877,1.0 ],
    [0.999,0.535,0.838,0.866,0.901,0.626,0.338,0.789,0.885,0.796,
     0.478,0.686,0.540,0.352,1.0 ,0.423,1.235,1.215,0.900,0.806,
     0.949,0.755,0.749,1.220],
    [1.047,0.517,0.819,0.944,0.837,0.622,0.217,0.787,0.854,0.940,
     0.501,0.721,0.420,0.301,1.0 ,0.207,1.177,1.127,0.960,0.821,
     1.026,0.5 ,0.922,0.875],
    [1.019,0.517,0.793,0.907,0.763,0.656,0.334,0.781,0.837,0.957,
     0.551,0.651,0.343,0.396,1.0 ,0.250,1.072,1.242,1.212,0.901,
     1.029,0.4 ,0.827,0.780],
    [0.938,0.564,0.851,0.882,0.865,0.622,0.260,0.736,0.848,0.911,
     0.454,0.748,0.557,0.264,1.004,0.333,1.138,1.307,1.036,0.759,
     0.912,0.362,0.979,0.846]]
for i in range(0, 10):
         for j in range(0, 24):
            dif[i]= dif[i]+(subject[j]-applicants[i][j])**2
for i in range(0, 10):
    dif[i]= cmath.sqrt(dif[i])
    print (dif[i])

Результат получен аналогичный. Претендент №10 имеет наименьшее расхождение по комплексному признаку  (Евклидово расстояние):

Чтение и запись данных

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

Робот детектирует лицо субъекта на какую-либо из 2-х видеокамер (вместо глаз)   и определяет по 10-20 кадрам усредненные признаки для распознавания. Затем  считывает  из файла base.txt признаки претендентов и сравнивает их с признаками субъекта. Фрагмент кода для считывания какого-либо признака из файла приводится ниже:

handle = open("base.txt", "r") 
data = handle.readlines() # read ALL the lines!
#print(data)
print(data[0])
d=data[0].split(' ') # line in list
print(d)
print(d[0])
print(float(d[1]),float(d[2])) 
handle.close()

Если признаки какого-либо из претендентов (например, Ap1) соответствуют признакам субъекта, тогда приветствие «Hi, Ap1». Если признаки ни одного из претендентов не соответствуют признакам субъекта, тогда «Nice to meet you. What is your name?». Имя незнакомца (например, Ap11) вместе с его признаками добавляется в базу данных. Фрагмент кода для добавления какого-либо субъекта в файл base.txt приводится ниже:

handle = open("base.txt", "a")
subject = ["Ap11", 0.938,0.564,0.851,0.882,0.865,0.622,0.260,0.736,0.848,0.911,
 0.454,0.748,0.557,0.264,1.004,0.333,1.138,1.307,1.036,0.759,
 0.912,0.362,0.979,0.846]
d = str(subject[0])
for i in range(1, 25):
    d = d +' '+ str(subject[i])
handle.write('\n'+d)
handle.close()

Полезные ссылки:

 

Автор: Николай Свирневский