Учим нейросеть распознавать возраст по фотографии
medium

Учим нейросеть распознавать возраст по фотографии

Также: нейросеть раскрыла всю правду о Моне Лизе

Продолжаем проект с распознаванием лиц и компьютерным зрением. Сегодня мы научим скрипт распознавать возраст и пол человека по фотографии. 

Вот что мы уже сделали к этому моменту:

Короткая версия для быстрого погружения:

  • Для распознавания лица компьютер должен получить изображение — через камеру или готовый файл.
  • Компьютер использует особый алгоритм, который разбивает изображение на прямоугольники.
  • С помощью этих прямоугольников алгоритм пытается найти на картинке знакомые ему переходы между светлыми и тёмными областями.
  • Если в одном месте программа находит много таких совпадений, то, скорее всего, это лицо человека.
  • Чтобы программистам каждый раз не писать свой код распознавания с нуля, сделали специальную библиотеку компьютерного зрения — cv2. Если в неё загрузить заранее подготовленные параметры лиц, она сможет распознавать их намного точнее.
  • С помощью этой библиотеки можно находить на картинке не только лица, но и другие предметы — для этого нужно использовать дополнительные библиотеки либо обучать систему самому.
  • Если то, что нейросеть распознала как лицо, отправить в другую нейросеть в этом же скрипте, то можно сразу получить её оценку. Например, что она думает про пол и возраст человека в кадре.

Вот текущий код скрипта — с ним мы будем сегодня работать.

# подключаем библиотеку компьютерного зрения 
import cv2

# функция определения лиц
def highlightFace(net, frame, conf_threshold=0.7):
    # делаем копию текущего кадра
    frameOpencvDnn=frame.copy()
    # высота и ширина кадра
    frameHeight=frameOpencvDnn.shape[0]
    frameWidth=frameOpencvDnn.shape[1]
    # преобразуем картинку в двоичный пиксельный объект
    blob=cv2.dnn.blobFromImage(frameOpencvDnn, 1.0, (300, 300), [104, 117, 123], True, False)
    # устанавливаем этот объект как входной параметр для нейросети
    net.setInput(blob)
    # выполняем прямой проход для распознавания лиц
    detections=net.forward()
    # переменная для рамок вокруг лица
    faceBoxes=[]
    # перебираем все блоки после распознавания
    for i in range(detections.shape[2]):
        # получаем результат вычислений для очередного элемента
        confidence=detections[0,0,i,2]
        # если результат превышает порог срабатывания — это лицо
        if confidence>conf_threshold:
            # формируем координаты рамки
            x1=int(detections[0,0,i,3]*frameWidth)
            y1=int(detections[0,0,i,4]*frameHeight)
            x2=int(detections[0,0,i,5]*frameWidth)
            y2=int(detections[0,0,i,6]*frameHeight)
            # добавляем их в общую переменную
            faceBoxes.append([x1,y1,x2,y2])
            # рисуем рамку на кадре
            cv2.rectangle(frameOpencvDnn, (x1,y1), (x2,y2), (0,255,0), int(round(frameHeight/150)), 8)
    # возвращаем кадр с рамками
    return frameOpencvDnn,faceBoxes

# загружаем веса для распознавания лиц
faceProto="opencv_face_detector.pbtxt"
# и конфигурацию самой нейросети — слои и связи нейронов
faceModel="opencv_face_detector_uint8.pb"
# точно так же загружаем модели для определения пола и возраста
genderProto="gender_deploy.prototxt"
genderModel="gender_net.caffemodel"
ageProto="age_deploy.prototxt"
ageModel="age_net.caffemodel"

# настраиваем свет
MODEL_MEAN_VALUES=(78.4263377603, 87.7689143744, 114.895847746)
# итоговые результаты работы нейросетей для пола и возраста
genderList=['Male ','Female']
ageList=['(0-2)', '(4-6)', '(8-12)', '(15-20)', '(25-32)', '(38-43)', '(48-53)', '(60-100)']

# запускаем нейросеть по распознаванию лиц
faceNet=cv2.dnn.readNet(faceModel,faceProto)
# и запускаем нейросети по определению пола и возраста
genderNet=cv2.dnn.readNet(genderModel,genderProto)
ageNet=cv2.dnn.readNet(ageModel,ageProto)

# получаем видео с камеры
video=cv2.VideoCapture(0)
# пока не нажата любая клавиша — выполняем цикл
while cv2.waitKey(1)<0:
    # получаем очередной кадр с камеры
    hasFrame,frame=video.read()
    # если кадра нет
    if not hasFrame:
        # останавливаемся и выходим из цикла
        cv2.waitKey()
        break
    # определяем лица в кадре
    resultImg,faceBoxes=highlightFace(faceNet,frame)
    # перебираем все найденные лица в кадре
    for faceBox in faceBoxes:
        # получаем изображение лица на основе рамки
        face=frame[max(0,faceBox[1]):
                   min(faceBox[3],frame.shape[0]-1),max(0,faceBox[0])
                   :min(faceBox[2], frame.shape[1]-1)]
        # получаем на этой основе новый бинарный пиксельный объект
        blob=cv2.dnn.blobFromImage(face, 1.0, (227,227), MODEL_MEAN_VALUES, swapRB=False)
        # отправляем его в нейросеть для определения пола
        genderNet.setInput(blob)
        # получаем результат работы нейросети
        genderPreds=genderNet.forward()
        # выбираем пол на основе этого результата
        gender=genderList[genderPreds[0].argmax()]
        # отправляем результат в переменную с полом
        print(f'Gender: {gender}')

        # делаем то же самое для возраста
        ageNet.setInput(blob)
        agePreds=ageNet.forward()
        age=ageList[agePreds[0].argmax()]
        print(f'Age: {age[1:-1]} years')

        # добавляем текст возле каждой рамки в кадре
        cv2.putText(resultImg, f'{gender}, {age}', (faceBox[0], faceBox[1]-10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0,255,255), 2, cv2.LINE_AA)
        # выводим итоговую картинку
        cv2.imshow("Detecting age and gender", resultImg)

Что делаем

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

После этого мы сможем скормить скрипту фотографию Моны Лизы и получить неожиданный результат:

Учим нейросеть распознавать возраст по фотографии

Можно представить, что аргументы — это текстовая настройка программы. Когда мы работаем в программе с графическим интерфейсом, эта настройка делается кнопками и ползунками, а когда работаем с командной строкой — текстовыми аргументами. Задача аргументов — передать программе дополнительные параметры, которые ей нужно учесть в работе. 

Например, если мы в командной строке просто напишем python или python3, то попадём в режим выполнения python-команд. А если мы укажем аргумент при вызове, например имя скрипта, то Python поймёт, что мы хотим не вводить команды вручную, а выполнить этот файл:

python3 script.pyздесь script.py — это аргумент для программы python3Ещё мы сталкивались с аргументами, когда вручную генерировали картинки с помощью Stable Diffusion:

python scripts/txt2img.py --prompt "photo of a programmer at a desktop in the style of the game cyberpunk 2077" --ckpt v2-1_512-ema-pruned.ckpt --config configs/stable-diffusion/v2-inference-v.yaml --H 512 --W 512

Переведём это в более читаемый вид:

python scripts/txt2img.py
--prompt "photo of a programmer at a desktop in the style of the game cyberpunk 2077"
--ckpt v2-1_512-ema-pruned.ckpt
--config configs/stable-diffusion/v2-inference-v.yaml
--H 512
--W 512

Всё, что выделено жирным, — это аргументы, которые говорят нейросети:

  • что мы от неё хотим (prompt);
  • где лежат веса (ckpt);
  • настройку сети (config);
  • какого размера должны быть итоговые картинки (h и w).

Добавляем парсер аргументов в python-скрипт

Чтобы программа умела работать с аргументами при вызове, её нужно этому научить: добавить специальный парсер аргументов и привязать разные аргументы к нужным действиям внутри программы. Чаще всего для этого используется стандартный модуль argparse, который подключается так:

# подключаем модуль для работы с аргументами при вызове
import argparse

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

# подключаем парсер аргументов командной строки
parser=argparse.ArgumentParser()
# добавляем аргумент для работы с изображениями
parser.add_argument('--image')
# сохраняем аргументы в отдельную переменную
args=parser.parse_args()

Мы больше не указываем никаких параметров, потому что это необязательный аргумент — если программу запустить без него, то она выполнится как обычно. 

Обрабатываем параметры в коде

Логика новой версии проекта такая:

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

Чтобы такое сделать, нужно заменить строчку:

# получаем видео с камеры
video=cv2.VideoCapture(0)

на такую:

# если был указан аргумент с картинкой — берём картинку как источник
video=cv2.VideoCapture(args.image if args.image else 0)

Если перевести новую строчку на понятный язык, она будет звучать так: 

«Посмотри, указан ли параметр --image при запуске программы. Если указан — проверь, есть ли после него имя файла. Если есть — используй его в качестве основы для распознавания лиц».

Когда мы просто запустим скрипт из среды разработки, он будет работать как и раньше — брать картинку с камеры и искать лица там. Но если мы запустим скрипт из командной строки и попросим его обработать картинку, то вместо камеры мы получим разметку с лицами на фотографии. Найдём в интернете фотографию портрета Моны Лизы, положим в ту же папку, что и исходный код, и отдадим её скрипту такой командой:

python3 dnn.py --image mona.png

Учим нейросеть распознавать возраст по фотографии

Любопытно, что в этот раз модель распознала Мону Лизу как женщину — получается, в этот раз связи в узлах нейросети совпали так, что победил ответ «женщина».

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

Учим нейросеть распознавать возраст по фотографии

Получается, сейчас нейросеть на 0,38 единицы уверена, что перед ней мужчина и на 0,62 — что женщина. Что это значит:

У нашей нейросети есть два выходных узла «Мужчина» и «Женщина». В результате обработки картинки на каждом узле появляется число от 0 до 1. Если в узле «Мужчина» число большое, мы предполагаем, что нейросеть увидела на этой фотографии мужчину. И наоборот.

Само по себе число нам не так важно, сколько разница между этими двумя числами. Например, если в первом узле уверенность 0,7, а во втором — 0,00001 — очевидно, что активировался первый узел, то есть нейронка решила, что перед ней мужчина. 

Что мы видим здесь: нейронка на 38% уверена, что перед ней мужчина и на 61% — что женщина. Разница даже не в два раза. Уверенности особо нет. 

Короче: нейронка не может ничего достоверно сказать про Мону Лизу. Перед ней вроде женщина, но вроде и мужчина. Чтобы быть уверенной, нейросеть должна увидеть разницу между узлами хотя бы в 10 раз. А здесь разница в полтора раза всего. 

Так как алгоритм не настроен отвечать «Не уверен», он просто берёт самое большое значение узла и выводит его. И получается, что один раз на одной картинке нейросеть видит в Моне Лизе женщину, а на другой — мужчину. Хотя на самом деле она ничего не понимает. 

Ещё одно подтверждение, что нейронки — это просто математика.

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

Учим нейросеть распознавать возраст по фотографии
Джеймс Хетфилд и Кирк Хэмметт не распознались

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

Учим нейросеть распознавать возраст по фотографии

# подключаем библиотеку компьютерного зрения 
import cv2
# подключаем модуль для работы с аргументами при вызове
import argparse

# подключаем парсер аргументов командной строки
parser=argparse.ArgumentParser()
# добавляем аргумент для работы с изображениями
parser.add_argument('--image')
# сохраняем аргумент в отдельную переменную
args=parser.parse_args()
# прописываем цвет по умолчанию
color = (0,255,0)
# функция определения лиц
def highlightFace(net, frame, conf_threshold=0.7):
    # делаем копию текущего кадра
    frameOpencvDnn=frame.copy()
    # высота и ширина кадра
    frameHeight=frameOpencvDnn.shape[0]
    frameWidth=frameOpencvDnn.shape[1]
    # преобразуем картинку в двоичный пиксельный объект
    blob=cv2.dnn.blobFromImage(frameOpencvDnn, 1.0, (300, 300), [104, 117, 123], True, False)
    # устанавливаем этот объект как входной параметр для нейросети
    net.setInput(blob)
    # выполняем прямой проход для распознавания лиц
    detections=net.forward()
    # переменная для рамок вокруг лица
    faceBoxes=[]
    # перебираем все блоки после распознавания
    for i in range(detections.shape[2]):
        # получаем результат вычислений для очередного элемента
        confidence=detections[0,0,i,2]
        # если результат превышает порог срабатывания — это лицо
        if confidence>conf_threshold:
            # формируем координаты рамки
            x1=int(detections[0,0,i,3]*frameWidth)
            y1=int(detections[0,0,i,4]*frameHeight)
            x2=int(detections[0,0,i,5]*frameWidth)
            y2=int(detections[0,0,i,6]*frameHeight)
            # добавляем их в общую переменную
            faceBoxes.append([x1,y1,x2,y2])
            # рисуем рамку на кадре
            cv2.rectangle(frameOpencvDnn, (x1,y1), (x2,y2), color, int(round(frameHeight/150)), 8)
    # возвращаем кадр с рамками
    return frameOpencvDnn,faceBoxes

# загружаем веса для распознавания лиц
faceProto="opencv_face_detector.pbtxt"
# и конфигурацию самой нейросети — слои и связи нейронов
faceModel="opencv_face_detector_uint8.pb"
# точно так же загружаем модели для определения пола и возраста
genderProto="gender_deploy.prototxt"
genderModel="gender_net.caffemodel"
ageProto="age_deploy.prototxt"
ageModel="age_net.caffemodel"

# настраиваем свет
MODEL_MEAN_VALUES=(78.4263377603, 87.7689143744, 114.895847746)
# итоговые результаты работы нейросетей для пола и возраста
genderList=['Male ','Female']
ageList=['(0-2)', '(4-6)', '(8-12)', '(15-20)', '(25-32)', '(38-43)', '(48-53)', '(60-100)']

# запускаем нейросеть по распознаванию лиц
faceNet=cv2.dnn.readNet(faceModel,faceProto)
# и запускаем нейросети по определению пола и возраста
genderNet=cv2.dnn.readNet(genderModel,genderProto)
ageNet=cv2.dnn.readNet(ageModel,ageProto)

# если был указан аргумент с картинкой — берём картинку как источник
video=cv2.VideoCapture(args.image if args.image else 0)
# пока не нажата любая клавиша — выполняем цикл
while cv2.waitKey(1)<0:
    # получаем очередной кадр с камеры
    hasFrame,frame=video.read()
    # если кадра нет
    if not hasFrame:
        # останавливаемся и выходим из цикла
        cv2.waitKey()
        break
    # определяем лица в кадре
    resultImg,faceBoxes=highlightFace(faceNet,frame)
    # перебираем все найденные лица в кадре
    for faceBox in faceBoxes:
        # получаем изображение лица на основе рамки
        face=frame[max(0,faceBox[1]):
                   min(faceBox[3],frame.shape[0]-1),max(0,faceBox[0])
                   :min(faceBox[2], frame.shape[1]-1)]
        # получаем на этой основе новый бинарный пиксельный объект
        blob=cv2.dnn.blobFromImage(face, 1.0, (227,227), MODEL_MEAN_VALUES, swapRB=False)
        # отправляем его в нейросеть для определения пола
        genderNet.setInput(blob)
        # получаем результат работы нейросети
        genderPreds=genderNet.forward()
        # выбираем пол на основе этого результата
        gender=genderList[genderPreds[0].argmax()]
        # отправляем результат в переменную с полом
        print(f'Gender: {gender}')

        # делаем то же самое для возраста
        ageNet.setInput(blob)
        agePreds=ageNet.forward()
        age=ageList[agePreds[0].argmax()]
        print(f'Age: {age[1:-1]} years')

        # добавляем текст возле каждой рамки в кадре
        cv2.putText(resultImg, f'{gender}, {age}', (faceBox[0], faceBox[1]-10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0,255,255), 2, cv2.LINE_AA)
        # выводим итоговую картинку
        cv2.imshow("Detecting age and gender", resultImg)

Что дальше

А дальше будем учить нейросеть запоминать наше лицо — посмотрим, как проходит процесс обучения и что для этого нужно.

Корректор:

Ира Михеева

Художник:

Алексей Сухов

Вёрстка:

Кирилл Климентьев

Получите ИТ-профессию
В «Яндекс Практикуме» можно стать разработчиком, тестировщиком, аналитиком и менеджером цифровых продуктов. Первая часть обучения всегда бесплатная, чтобы попробовать и найти то, что вам по душе. Дальше — программы трудоустройства.
Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию
Вам может быть интересно
medium