В данной статье я опишу свой опыт создания Face ID для входной двери. Я не разработчик, поэтому сразу прошу прощения за качество кода. Однако все работает отлично уже несколько месяцев.

Для реализации данной идеи у меня уже было:

  • Умный дом на базе homeassistant (необязательно)

  • MQTT сервер

  • Умный замок

  • Камера с возможностью забирать с неe фото

Осталось только реализовать функционал распознавания лица на фото полученного с камеры. Логичнее было бы написать плагин для умного дома, но я выбрал более простой путь, хотя и не такой правильный. Я написал скрипт на python с использованием проекта https://pypi.org/project/face-recognition/

На схеме показано как распределен функционал
На схеме показано как распределен функционал

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

Опишу словами логику работы.

  1. Скрипт стартует как демон, распознает лица из папки KNOWN и подписывается на топик.

  2. В умном доме включена автоматизация с триггером для запуска которой является детекция движения. Умный дом отправляет в нужный топик цифру с количеством попыток распознавания лица.

  3. Скрипт получив в топике цифру запрашивает с камеры фото, при необходимости обрезает фото, чтобы увеличить скорость распознавания. У меня, например, 4х мегапиксельная камера, которая захватывает весь коридор, а лицо видно только в непосредственной близости от камеры. При обрезании фото цикл распознавания снизился с 3 секунд, до менее 1 секунды.

  4. Скрипт пытается найти лица на фото, если лица найдены, то сравнивает их с теми, что считаны при запуске из папки KNOWN.

  5. Скрипт пишет в топик MQTT сервера результат распознавания и сохраняет фото в соответствующую папку.

  6. Умный дом получив UNKNOWN включает дверной звонок, получив имя известного человека открывает замок и произносит: "Username пришел".

Каталоги автоматически не создаются, поэтому перед запуском скрипта нужно создать каталоги: known, new, nofaces, unknown и каталоги для известных людей (имя каталога = имени файла с фото до точки в папку known). Ну и естественно положить файлы с фото. Я брал фото прям из папки unknown.

Нужно создать структуру каталогов

├── kat
├── known
│   ├── kat.jpg
│   ├── max.jpg
│   └── sasha.jpg
├── max
├── new
├── nofaces
├── sasha
└── unknown

Ссылка на Gitlab https://gitlab.com/tmv002/face-recognition-mqtt/

__main.py__
import requests
import configparser
import logging
import os
import sys
import io
import face_recognition
import configparser
import paho.mqtt.client as mqtt
from datetime import datetime
from PIL import Image

delimeter = '/'
if(os.name=='nt'):
    delimeter = '\\'

root_path = os.path.dirname(__file__) + delimeter

config = configparser.ConfigParser()
config.read(root_path + "fr.conf")
url = config['image']['url']
topic_subscribe = config['mqtt']['topic_subscribe']
topic_publish = config['mqtt']['topic_publish']
mqtt_server = config['mqtt']['mqtt_server']
mqtt_port = int(config['mqtt']['mqtt_port'])
try:
    mqtt_user = config['mqtt']['mqtt_user']
    mqtt_password = config['mqtt']['mqtt_password']
except:
    pass

known_path = config['dirs']['known']
new_path = config['dirs']['new']
unknown_path = config['dirs']['unknown']
no_faces = config['dirs']['no_faces']
logfile = config['log']['logfile']
loglevel = 10 * int(config['log']['loglevel'])
comparison_threshold = float(config['compare']['comparison_threshold'])

def parse_int_tuple(input):
    return tuple(int(k.strip()) for k in input[1:-1].split(','))

try:
    crop_rect = parse_int_tuple(config['image']['crop_rect'])
except:
    pass


logging.basicConfig(level=loglevel, filename=(root_path + logfile),filemode="w",format="%(asctime)s %(levelname)s %(message)s")
persons = []

def add_known_person():
    listdir = None
    try:
        listdir = os.listdir((root_path + known_path))
    except Exception as exc:
        logging.error("Can't list known person dir " + (root_path + known_path) + " with error: " + str(exc))
        sys.exit(1)

    for file_path in listdir:
        # check if current file_path is a file
        if os.path.isfile(os.path.join((root_path + known_path), file_path)):
            person_name = file_path.split(".")[0]
            logging.info("Try to add known person from file " + file_path)
            # load image from file
            known_image = face_recognition.load_image_file(root_path + known_path + delimeter + file_path)
            # recognize face
            face_enc = face_recognition.face_encodings(known_image)
            # if face found
            if(len(face_enc) > 0):
                # get first face
                person_encoding = face_enc[0]
                # add face to known persons array
                persons.append([person_name, [person_encoding]])
                logging.info("Add known person: " + person_name)
            else:
                logging.error("Face not found in file " + file_path)

def get_image_from_url(file_name):
    result = False
    r = None
    try:
        # Get image from URL
        r = requests.get(url)
        logging.info("Get image success")
    except Exception as exc:
         logging.error("Can't get image error:" + str(exc))
    if r is not None:
        new_file_name = file_name
        new_file_path = new_path + delimeter + new_file_name
        # Load image from request content
        im = Image.open(io.BytesIO(r.content))
        try:
            crop_rect
            # Crop image
            im = im.crop(crop_rect)
        except:
            pass
        try:
            # Save image to folder new
            im.save(root_path + new_file_path)
            result = True
            logging.info("Save image to " + (root_path + new_file_path))
        except Exception as exc:
            logging.error("Can't save image " + root_path + new_file_path + " with error: " + str(exc))
        return result
            

# The callback for when the client receives a CONNACK response from the server.
def on_connect(client, userdata, flags, rc):
    if(rc != 0):
        logging.info("MQTT connection error with result code "+str(rc))
        print("MQTT connection error with result code "+str(rc))
    else:
        logging.info("MQTT connection successful with result code "+str(rc))
        print("MQTT connection successful with result code "+str(rc))

    # Subscribing in on_connect() means that if we lose the connection and
    # reconnect then subscriptions will be renewed.
    client.subscribe(topic_subscribe)

# The callback for when a PUBLISH message is received from the server.
def on_message(client, userdata, msg):
    known_person = "unknown"
    logging.info(msg.topic+" "+str(msg.payload.decode()))
    # Loop face recognition
    i = 0
    try:
        # Read value from topic
        i = int(msg.payload.decode())
    except ValueError:
        logging.error("Receive non int value in topic " + msg.topic)
    for j in range(i):
        now = datetime.now()
        new_file_name = now.strftime("%Y_%m_%d-%H_%M_%S-") + str(j) + '.jpg'
        if get_image_from_url(new_file_name):
            new_file_path = new_path + delimeter + new_file_name
            # Load image from file
            new_image = face_recognition.load_image_file(root_path + new_file_path)
            # Check face count on image
            face_locations = face_recognition.face_locations(new_image)
            logging.info("Face location count: " + str(len(face_locations)))
            if(len(face_locations) > 0):
                # Recognize face on image
                unknown_faces = face_recognition.face_encodings(new_image, face_locations)
                logging.info("face recognition count: " + str(len(unknown_faces)))
                # Compare face with known faces
                for unknown_face in unknown_faces:
                    for i in range(len(persons)):
                        results = face_recognition.compare_faces(persons[i][1], unknown_face, comparison_threshold)
                        logging.info("compare face with " + persons[i][0] + " result: " + str(results))
                        if(results[0]):
                            known_person = persons[i][0]
                            client.publish(topic_publish + 'person', "{ \"known_person\": \"" + known_person + "\" }", 0, False)
                            logging.info("client publish { \"known_person\": \"" + known_person + "\" }")
            else:
                #print("no faces")
                known_person = "nofaces"
            logging.info("person: " + known_person)
            client.publish(topic_publish + known_person, now.strftime("%Y-%m-%d %H:%M:%S"), 0, True)
            if(known_person == "nofaces"):
                try:
                    os.remove(root_path + new_file_path)
                    logging.info("Remove file " + root_path + new_file_path)
                except Exception as exc:
                    logging.error("Can't remove file " + root_path + new_file_path + ": " + str(exc))
            else:
                try:
                    os.replace(root_path + new_file_path, root_path + known_person + delimeter + new_file_name)
                    logging.info("Move file " + root_path + new_file_path + " to " + root_path + known_person + delimeter + new_file_name)
                except Exception as exc:
                    logging.error("Can't move file " + root_path + new_file_path + " to " + root_path + known_person + delimeter + new_file_name + ": " + str(exc))
            if(known_person not in ("nofaces", "unknown")):
                break

if __name__ == "__main__":
    logging.info("Start face recognition")
    add_known_person()
    connected_flag = 0
    client = mqtt.Client()
    try:
        mqtt_password
        client.username_pw_set(mqtt_user, mqtt_password)
    except NameError:
        logging.warn("MQTT password is not set. Trying to connect without password.")
    client.enable_logger(logger=logging)
    client.on_connect = on_connect
    client.on_message = on_message
    while not connected_flag:
        try:
            client.connect(mqtt_server, mqtt_port, 60)
            connected_flag = 1
        except Exception as exc:
            logging.error("MQTT connection error: " + str(exc))
    client.loop_forever()

fr.conf
[image]
url = http://user:password@cam.ip.or.dns/ISAPI/Streaming/channels/1/picture
#Set crop_rect to crop orginal image. Script is faster face locating on smaller image. (left_x, top_y, right_x, bottom_y)
#crop_rect = (400, 500, 1350, 1700)

[mqtt]
topic_subscribe = homeassistant/sensor/facerec/cam3
topic_publish = homeassistant/sensor/facerec/
mqtt_server = 192.168.12.5
mqtt_port = 1883
#mqtt_user = mqttusername
#mqtt_password = mqttpassword

[dirs]
known = known
new = new
unknown = unknown
no_faces = nofaces

[log]
logfile = face-recognition.log
#Loglevel 1 - DEBUG, 2 - INFO, 3 - WARN, 4 - ERROR
loglevel = 2

[compare]
# Lowest threshold = 1. Highest threshold = 0.1
comparison_threshold = 0.5

Пример unit file для запуска в качестве сервиса:

face-recognition.service
[Unit]
Description=Face Recognition

[Service]
Restart=always
RestartSec=30
WorkingDirectory=/opt/face-recognition/
TimeoutStartSec=120
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=face-recognition
ExecStart=/usr/bin/python3 /opt/face-recognition/

[Install]
WantedBy=multi-user.target

Комментарии (18)


  1. aborouhin
    10.09.2023 12:21
    +4

    Распечатанной фотографией лица проверяли? Справляется?


    1. tmv002 Автор
      10.09.2023 12:21
      +1

      По фото тоже распознает. В описании фреймворка даже фото с телефона распознает. Для меня это приемлемый уровень безопасности.


      1. baldr
        10.09.2023 12:21
        +1

        То есть если кто-то покажет вашу фотографию этому замку, то он любезно впустит внутрь? Это приемлемый уровень??


        1. tmv002 Автор
          10.09.2023 12:21

          До моей двери ещё надо пробраться, знать про фотографию и знать куда ее показывать. Всегда можно поставить дополнительный фактор в виде gps, blutooth или wifi трекера.


          1. SUNsung
            10.09.2023 12:21
            +1

            И тогда распознание лица нафиг не надо.

            А еще вы можете потолстеть, удачно "упасть" и многое многое.

            Распознание лиц хорошо для аналитики, но абсолютно контрпродуктивно для безопасности


            1. tmv002 Автор
              10.09.2023 12:21

              Вы правда думаете, что квартира закрытая на ключ в безопасности? Все замки за пару минут открываются специально обученным человеком. И какой сценарий ограбления? Домушник с моей фотографией будет прыгать перед камерой? Кроме того, все уведомления и фотографии приходят в телегу. А трекер как единственное средство аутентификации совершенно не подходит.


              1. SUNsung
                10.09.2023 12:21

                Вы просто никогда не попадали в ситуацию, когда нужно попасть в дом, а "не пускает".

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

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


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


                1. laronov
                  10.09.2023 12:21
                  +1

                  Вот по банковским картам (nfc) это мечта. Есть готовые варианты?


                  1. tmv002 Автор
                    10.09.2023 12:21

                    У меня как раз статья про самоделки на Ардуино есть. Можно её использовать))


                  1. SUNsung
                    10.09.2023 12:21

                    Обычный nfc ридер и просто забиваешь в базу карты. Есть хорошие уличные варианты в продаже.

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

                    Я по сути сделал узел который читает карты на esp32 и обменивается уже фактами с локальным сервером.

                    И если ОК то идет команда на замок на ZigBee.

                    Это позволяет работать быстро и при необходимости открыть дверь с телеграм-бота.

                    А заводское исполнение замка уже давно подумало за отказы - его можно и ключом открыть в случае чего.


                1. tmv002 Автор
                  10.09.2023 12:21

                  А почему вы решили что у меня этого нет? Резервный вариант попадания в квартиру есть. Статья в общем-то совсем не про это. Я и до распознавания лица без ключа долгое время ходил.

                  На счёт обхода оповещений, если Том Круз в очередной миссии захочет ко мне залезть, он конечно залезет. А обычным ворам зачем выбирать дверь с видеонаблюдением, если в этом же доме ещё 1000 квартир без видеонаблюдения?


              1. fedorovAleks
                10.09.2023 12:21

                Если действительно параноить на тему безопасности, то можно купить надежный замок, с защитой от взлома. По крайней мере такой замок сильно усложнит проникновение в квартиру.

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


            1. aborouhin
              10.09.2023 12:21
              +1

              Ой, да ладно, прикольно же :) У меня в загородном доме дверь вообще пластиковая со стеклом на бóльшую часть полотна (ну потому что смешно ставить стальную дверь, когда у тебя по всему первому этажу окна). Так что, может, такую же игрушку just for fun и реализовал бы, если было бы не лень.

              Хотя вот ноут разблокировать лицом, хоть в нём и камера, сертифицированная для Windows Hello, которую, по идее, фотографией не обманешь, - уже страшно. Там уровень рисков гораздо больше.


              1. IvanPetrof
                10.09.2023 12:21
                +1

                Да и в квартиру зачастую можно проникнуть с балкона соседа (или через пожарную лестницу)


  1. LuWan
    10.09.2023 12:21
    +1

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

    Ранее пользовался готовым модулем интеграции с deepstack ( https://github.com/robmarkcole/HASS-Deepstack-face ). Удобен тем, что вся логика остается внутри HomeAssistant.

    Сейчас перешел на аппаратный модуль Intel RealSenseID F455. Решает все вышеперечисленные проблемы. Единственное, их сняли с производства и с покупкой могут возникнуть сложности.


    1. tmv002 Автор
      10.09.2023 12:21

      Intel RealSenseID F455 забавная штука, даже не знал про существование таких продуктов, спасибо за информацию. В даташитах вижу, что на текущий момент работает только с Windows. Я правильно понял, что она к компу с виндой через USB интерфейс подключается? Аналоги не могу нагуглить, они есть вообще?


      1. LuWan
        10.09.2023 12:21

        Вариант поиграться - да, винда и USB.

        У них есть SDK под linux и android, но там достаточно простой протокол и для своих задач я написал простенькое приложение на nodejs, которое отвечает за работу с модулем.

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


  1. MetallRaven
    10.09.2023 12:21

    Естественно, это все just for fan.

    1. Абсолютной безопасности не существует

    2. Можно выключить свет в щитке, и, если нет генератора или ИБП, то все.