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

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

pip установить opencv-python == 3.4.5.20

Несмотря на то, что это всего лишь одна строка, поскольку OpenCV - большая библиотека, которая использует дополнительные инструменты, она установит некоторые зависимости, такие как NumPy. Мы указываем версию 3.4, потому что, если мы этого не сделаем, будет установлена ​​версия 4.x, и все они либо содержат ошибки, либо не работают.

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

Для классификации нужен классификатор. Доступны классификаторы лиц и глаз (каскады хаара), которые поставляются с библиотекой OpenCV, вы можете скачать их из официального репозитория github: Eye Classifier, Face Classifier

Чтобы загрузить их, щелкните правой кнопкой мыши «Raw» = ›« Сохранить ссылку как ». Убедитесь, что они находятся в вашем рабочем каталоге.

Закончив введение, приступим к программированию. Создайте файл track.py в своем рабочем каталоге и напишите туда следующие строки. Они импортируют и инициируют все, что нам нужно.

import cv2
import numpy
face_cascade = cv2.CascadeClassifier('haarcascade_frontalface_default.xml')
eye_cascade = cv2.CascadeClassifier('haarcascade_eye.xml')

Хотя в конечном итоге мы будем отслеживать взгляд на видео, мы начнем с изображения, поскольку оно намного быстрее, а код, который работает с изображением, будет работать и с видео, потому что любое видео - это всего N изображений (кадров) в секунду. Так что скачайте где-нибудь портрет или используйте для этого свою фотографию. Я буду использовать стоковую картинку.

Как только он окажется в вашем рабочем каталоге, добавьте в свой код следующую строку:

img = cv2.imread(“your_image_name.jpg”)

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

Чтобы распознать лица на картинке, нам сначала нужно сделать ее серой. Затем мы обнаружим лица.

gray_picture = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)#make picture gray
faces = face_cascade.detectMultiScale(gray_picture, 1.3, 5)

Объект Faces - это просто массив с небольшими подмассивами, состоящими из четырех чисел. Это X, Y, ширина и высота обнаруженного лица. Например, это может быть что-то вроде этого:

[[356  87 212 212]
 [ 50  88 207 207]]

Это будет означать, что на изображении есть два лица. 212x212 и 207x207 - их размеры, а (356,87) и (50, 88) - их координаты.

Чтобы проверить, работает ли это для нас, нарисуем прямоугольник в (X, Y) шириной и высотой:

for (x,y,w,h) in faces:
    cv2.rectangle(img,(x,y),(x+w,y+h),(255,255,0),2)

Эти линии рисуют прямоугольники на нашем изображении с цветом (255, 255, 0) в пространстве RGB и толщиной контура 2 пикселя.

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

cv2.imshow('my image',img)
cv2.waitKey(0)
cv2.destroyAllWindows()

Теперь, когда мы убедились, что все работает, можно продолжить. Точно так же мы обнаружим глаза. Но сейчас на рамке лица, а не на всей картине. В строке cv2.rectangle (img, (x, y), (x + w, y + h), (255,255,0), 2) добавьте:

gray_face = gray_picture[y:y+h, x:x+w] # cut the gray face frame out
face = img[y:y+h, x:x+w] # cut the face frame out
eyes = eye_cascade.detectMultiScale(gray_face)

Объект eyes аналогичен объекту faces - он содержит X, Y, ширину и высоту рамки глаз. Вы можете отобразить его аналогичным образом:

for (ex,ey,ew,eh) in eyes: 
    cv2.rectangle(face,(ex,ey),(ex+ew,ey+eh),(0,225,255),2)

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

Похоже, у нас впервые возникли проблемы:

Наш детектор почему-то считает, что подбородок - это тоже глаз. Что тут можно сделать? Конечно, вы можете собрать несколько лиц в Интернете и научить модель быть более опытной. Но есть другой способ - компьютерное зрение.

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

Мы поместим все в отдельную функцию с именем detect_eyes:

def detect_eyes(img, img_gray, classifier):
    coords = cascade.detectMultiScale(img_gray, 1.3, 5)# detect eyes
    height = np.size(image, 0) # get face frame height
    for (x, y, w, h) in coords:
        if y+h > height/2: # pass if the eye is at the bottom
            pass

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

Мы разрежем изображение пополам, введя переменную ширины:

def detect_eyes(img, classifier):
    gray_frame = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    eyes = cascade.detectMultiScale(gray_frame, 1.3, 5) # detect eyes
    width = np.size(image, 1) # get face frame width
    height = np.size(image, 0) # get face frame height
    for (x, y, w, h) in eyes:
        if y > height / 2:
            pass
        eyecenter = x + w / 2  # get the eye center
        if eyecenter < width * 0.5:
            left_eye = img[y:y + h, x:x + w]
        else:
            right_eye = img[y:y + h, x:x + w]
    return left_eye, right_eye

Но что делать, если глаз не обнаружено? Тогда программа выйдет из строя, потому что функция пытается вернуть переменные left_eye и right_eye, которые не были определены. Чтобы этого избежать, мы добавим две строки, которые предопределяют наши переменные для левого и правого глаза:

def detect_eyes(img, classifier):
    gray_frame = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    eyes = cascade.detectMultiScale(gray_frame, 1.3, 5) # detect eyes
    width = np.size(image, 1) # get face frame width
    height = np.size(image, 0) # get face frame height
    left_eye = None
    right_eye = None
    for (x, y, w, h) in coords:
        ....

Теперь, если глаз по какой-то причине не обнаружен, для этого глаза будет возвращено значение None.

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

def detect_faces(img, classifier):
    gray_frame = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    coords = cascade.detectMultiScale(gray_frame, 1.3, 5)
    if len(coords) > 1:
        biggest = (0, 0, 0, 0)
        for i in coords:
            if i[3] > biggest[3]:
                biggest = i
        biggest = np.array([i], np.int32)
    elif len(coords) == 1:
        biggest = coords
    else:
        return None
    for (x, y, w, h) in biggest:
        frame = img[y:y + h, x:x + w]
    return frame

Также обратите внимание, как мы снова обнаруживаем все на сером изображении, но работаем с цветным.

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

detector_params = cv2.SimpleBlobDetector_Params()
detector_params.filterByArea = True
detector_params.maxArea = 1500
detector = cv2.SimpleBlobDetector_create(detector_params)

Его нужно инициализировать только один раз, поэтому лучше поместить эти строки в самое начало, среди других строк инициализации. Кроме того, нам нужна фильтрация областей для лучших результатов. Поверьте, ни один зрачок не будет больше 1500 пикселей. Но много ложных срабатываний.

Теперь к отслеживающим глазам. Наша глазная рамка выглядит примерно так:

Нам нужно эффективно определить зрачок вот так:

Детектор капель обнаруживает то, что предполагает его название: капли. Плюс в том, что он работает с двоичными изображениями (только два цвета). Чтобы получить двоичное изображение, нам сначала нужно изображение в оттенках серого. К счастью, они у нас есть.

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

_, img = cv2.threshold(img, threshold, 255, cv2.THRESH_BINARY)

_ означает ненужную переменную, в нашем случае retval, она нам не нужна. Так что мы просто делаем это _ и забываем об этом. В результате изображение с порогом = 127 будет примерно таким:

Выглядит ужасно, так что давайте снизим порог. При пороге = 86 это выглядит так:

Уже лучше, но все еще недостаточно. Нам нужно сделать зрачок различимым, так что давайте пока поэкспериментируем. Возможно, на вашем фото освещение другое, и лучше всего подходит другой порог. В любом случае результат должен быть таким:

Зрачок здесь представляет собой огромную черную точку, а его окружение - лишь несколько узких линий. Кроме того, на этом этапе мы воспользуемся еще одним трюком, основанным на CV-анализе: брови всегда занимают ~ 25% изображения, начиная сверху, поэтому мы создадим функцию cut_eyebrows, которая срезает брови из рамки для глаз, потому что иногда они обнаруживаются нашим детектором капель вместо зрачка.

def cut_eyebrows(img):
    height, width = img.shape[:2]
    eyebrow_h = int(height / 4)
    img = img[eyebrow_h:height, 0:width]  # cut eyebrows out (15 px)
return img

Теперь, после того, что мы сделали, оправы для глаз выглядят так:

Давайте попробуем обнаружить и нарисовать капли на этих кадрах:

def blob_process(img, detector):
    gray_frame = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    _, img = cv2.threshold(gray_frame, 42, 255, cv2.THRESH_BINARY)
    keypoints = detector.detect(img)
    return keypoints
keypoints = blob_process(eye, detector)
cv2.drawKeypoints(eye, keypoints, eye, (0, 0, 255), cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)

Проблема в том, что наша картинка недостаточно обработана и результат выглядит так:

Но мы почти у цели! Просто немного магии обработки изображений, и рамка глаза, которая у нас была, превращается в чистую каплю зрачка:

Просто добавьте следующие строки в свою функцию обработки больших двоичных объектов:

def blob_process(img, detector):
    gray_frame = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    _, img = cv2.threshold(gray_frame, 42, 255, cv2.THRESH_BINARY)
    img = cv2.erode(img, None, iterations=2) #1
    img = cv2.dilate(img, None, iterations=4) #2
    img = cv2.medianBlur(img, 5) #3
    keypoints = detector.detect(img)
    return keypoints

Мы сделали серию эрозий и дилатаций, чтобы уменьшить «шум», который у нас был. Этот трюк обычно используется в различных сценариях CV, но он хорошо работает в нашей ситуации. После этого мы размыли изображение, чтобы оно получилось более плавным.

Теперь, если мы попытаемся обнаружить капли на этом изображении, мы получим следующее:

И поскольку изначально это было изображение нашего глаза, мы можем нарисовать такой же круг на изображении глаза:

Хотите верьте, хотите нет, но это в основном все. Осталось только настроить захват камеры и передать каждый ее кадр нашим функциям. Давайте определим функцию main (), которая будет запускать запись видео и обрабатывать каждый кадр с помощью наших функций. Обратите внимание на условия if not None, они предназначены для случаев, когда ничего не было обнаружено. Если бы не они, программа вылетела бы, если бы вы моргнули.

def main():
    cap = cv2.VideoCapture(0)
    while True:
        _, frame = cap.read()
        face_frame = detect_faces(frame, face_cascade)
        if face_frame is not None:
            eyes = detect_eyes(face_frame, eye_cascade)
            for eye in eyes:
                if eye is not None:
                    eye = cut_eyebrows(eye)
                    keypoints = blob_process(eye, detector)
                    eye = cv2.drawKeypoints(eye, keypoints, eye, (0, 0, 255), cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
        cv2.imshow('my image', face_frame)
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break
    cap.release()
    cv2.destroyAllWindows()

Здесь все было бы хорошо, если бы у вас было такое же освещение, как на моем стоковом изображении. На нем нужен порог 42. Но у вас, скорее всего, другие условия освещения. И никаких капель обнаружено не будет. Вам нужен другой порог.

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

def main():
    cap = cv2.VideoCapture(0)
    cv2.namedWindow('image')
    cv2.createTrackbar('threshold', 'image', 0, 255, nothing)
    while True:
        _, frame = cap.read()
        face_frame = detect_faces(frame, face_cascade)
        if face_frame is not None:
            eyes = detect_eyes(face_frame, eye_cascade)
            for eye in eyes:
                if eye is not None:
                    threshold = cv2.getTrackbarPos('threshold', 'image')
                    eye = cut_eyebrows(eye)
                    keypoints = blob_process(eye, threshold, detector)
                    eye = cv2.drawKeypoints(eye, keypoints, eye, (0, 0, 255), cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
        cv2.imshow('image', frame)
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break
    cap.release()
    cv2.destroyAllWindows()

Теперь на каждой итерации он захватывает значение порога и передает его вашей функции blob_process, которую мы сейчас изменим, чтобы она также принимала пороговое значение:

def blob_process(img, threshold, detector):
    gray_frame = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    _, img = cv2.threshold(gray_frame, threshold, 255, cv2.THRESH_BINARY)
    img = cv2.erode(img, None, iterations=2)
    img = cv2.dilate(img, None, iterations=4)
    img = cv2.medianBlur(img, 5)
    keypoints = detector.detect(img)
    print(keypoints)
    return keypoints

Теперь это не жестко запрограммированный порог 42, а порог, который вы устанавливаете сами.

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

def nothing(x):
    pass

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

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

Мой github называется http://github.com/stepacool/ - здесь вы можете найти код отслеживания взгляда, который использует некоторые продвинутые методы для большей точности.

Вы можете найти то, что мы написали сегодня, в ветке Без графического интерфейса: https://github.com/stepacool/Eye-Tracker/tree/No_GUI

Демонстрация YouTube доступна здесь:

Https://www.youtube.com/watch?v=zDN-wwd5cfo

Не стесняйтесь обращаться ко мне на [email protected]