В данной статье я опишу свой опыт создания Face ID для входной двери. Я не разработчик, поэтому сразу прошу прощения за качество кода. Однако все работает отлично уже несколько месяцев.
Для реализации данной идеи у меня уже было:
Умный дом на базе homeassistant (необязательно)
MQTT сервер
Умный замок
Камера с возможностью забирать с неe фото
Осталось только реализовать функционал распознавания лица на фото полученного с камеры. Логичнее было бы написать плагин для умного дома, но я выбрал более простой путь, хотя и не такой правильный. Я написал скрипт на python с использованием проекта https://pypi.org/project/face-recognition/

Поскольку у меня уже был MQTT сервер, то я решил, что это самый простой путь интегрировать умный дом и скрипт.
Опишу словами логику работы.
Скрипт стартует как демон, распознает лица из папки KNOWN и подписывается на топик.
В умном доме включена автоматизация с триггером для запуска которой является детекция движения. Умный дом отправляет в нужный топик цифру с количеством попыток распознавания лица.
Скрипт получив в топике цифру запрашивает с камеры фото, при необходимости обрезает фото, чтобы увеличить скорость распознавания. У меня, например, 4х мегапиксельная камера, которая захватывает весь коридор, а лицо видно только в непосредственной близости от камеры. При обрезании фото цикл распознавания снизился с 3 секунд, до менее 1 секунды.
Скрипт пытается найти лица на фото, если лица найдены, то сравнивает их с теми, что считаны при запуске из папки KNOWN.
Скрипт пишет в топик MQTT сервера результат распознавания и сохраняет фото в соответствующую папку.
Умный дом получив 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
UPDATE.
Решил дополнить статью после комментариев. Конечно, можно попробовать подставить фото камере и есть большой шанс, что face-recognition распознает лицо по фото, но у меня дверь на этаж закрывается на домофон и посторонних людей очень мало, поэтому лично меня такой вариант устраивает. Кроме того как ни странно, но для меня это повышение уровня безопасности, потому что замок входной двери был практически всегда открыт, а также дети регулярно теряли ключи в округе. Теперь замок закрывается автоматически, а дети не используют ключи.
Вопрос с попаданием домой после отказа системы и замка также предусмотрен. И в этом вопросе я тоже получил плюс, так как ранее дети периодически забывали ключи дома и ждали под дверью или у друзей, когда их пустят домой, теперь такая проблема исчезла полностью.
Я никого не призываю использовать именно такой сценарий, но вы можете воспользоваться им и добавить 2FA в виде скрытой кнопки, кодовой панели, nfc метки, телеграм-бота, трекеров: gps, bluetooth, wifi. Можете использовать сервис, только для уведомлений, чтобы знать кто и когда к вам приходил. Можете интегрировать со СКУД в фитнесе и уведомлять СБ о проходах в зал по чужому абонементу и придумать еще сотню других сценариев. Я лишь показал как из уже доступного мне набора инструментов и устройств сделать новый сервис.
