В данной статье я опишу свой опыт создания 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
Комментарии (18)
LuWan
10.09.2023 12:21+1Качество распознавания у таких библиотек оставляет желать лучшего и сильно зависит от того, как меняются внешние условия (дневная/ночная съемка, освещенность, блики от солнца/светильников). И самый главный нюанс в том, что открыть замок можно фотографией владельца.
Ранее пользовался готовым модулем интеграции с deepstack ( https://github.com/robmarkcole/HASS-Deepstack-face ). Удобен тем, что вся логика остается внутри HomeAssistant.
Сейчас перешел на аппаратный модуль Intel RealSenseID F455. Решает все вышеперечисленные проблемы. Единственное, их сняли с производства и с покупкой могут возникнуть сложности.
tmv002 Автор
10.09.2023 12:21Intel RealSenseID F455 забавная штука, даже не знал про существование таких продуктов, спасибо за информацию. В даташитах вижу, что на текущий момент работает только с Windows. Я правильно понял, что она к компу с виндой через USB интерфейс подключается? Аналоги не могу нагуглить, они есть вообще?
LuWan
10.09.2023 12:21Вариант поиграться - да, винда и USB.
У них есть SDK под linux и android, но там достаточно простой протокол и для своих задач я написал простенькое приложение на nodejs, которое отвечает за работу с модулем.
Аналогов, чтобы был полноценный сканер, я не находил. У Hikvision есть подобное решение, но там сразу большой комплект.
MetallRaven
10.09.2023 12:21Естественно, это все just for fan.
Абсолютной безопасности не существует
Можно выключить свет в щитке, и, если нет генератора или ИБП, то все.
aborouhin
Распечатанной фотографией лица проверяли? Справляется?
tmv002 Автор
По фото тоже распознает. В описании фреймворка даже фото с телефона распознает. Для меня это приемлемый уровень безопасности.
baldr
То есть если кто-то покажет вашу фотографию этому замку, то он любезно впустит внутрь? Это приемлемый уровень??
tmv002 Автор
До моей двери ещё надо пробраться, знать про фотографию и знать куда ее показывать. Всегда можно поставить дополнительный фактор в виде gps, blutooth или wifi трекера.
SUNsung
И тогда распознание лица нафиг не надо.
А еще вы можете потолстеть, удачно "упасть" и многое многое.
Распознание лиц хорошо для аналитики, но абсолютно контрпродуктивно для безопасности
tmv002 Автор
Вы правда думаете, что квартира закрытая на ключ в безопасности? Все замки за пару минут открываются специально обученным человеком. И какой сценарий ограбления? Домушник с моей фотографией будет прыгать перед камерой? Кроме того, все уведомления и фотографии приходят в телегу. А трекер как единственное средство аутентификации совершенно не подходит.
SUNsung
Вы просто никогда не попадали в ситуацию, когда нужно попасть в дом, а "не пускает".
из личного опыта - самое топовое это сделать возможность открывать любой своей банковской картой (то есть и часами и телефоном).
для ситуаций "выбежал из дома" самое то.
а вот для длительных поездок два независимых механических замка и запасные ключи в ячейке хранения.
все остальное свистоперделки на поигратся.
если поставят цель то попадут куда угодно и обойдут какую угодно защиту, потому такое "решение" оно больше от честных людей.
(к слову за оповещения - я сходу минимум три способа вижу обойти это ограничение)
предоставление доступа к физическим обьектам должно быть или отказоустойчиво или не влияще на общую картину безопасности.
laronov
Вот по банковским картам (nfc) это мечта. Есть готовые варианты?
tmv002 Автор
У меня как раз статья про самоделки на Ардуино есть. Можно её использовать))
SUNsung
Обычный nfc ридер и просто забиваешь в базу карты. Есть хорошие уличные варианты в продаже.
С телефоном сложнее, там динамика, но я даже не копал особо, взял готовую либу на ардуино и ее даже допиливать не надо было.
Я по сути сделал узел который читает карты на esp32 и обменивается уже фактами с локальным сервером.
И если ОК то идет команда на замок на ZigBee.
Это позволяет работать быстро и при необходимости открыть дверь с телеграм-бота.
А заводское исполнение замка уже давно подумало за отказы - его можно и ключом открыть в случае чего.
tmv002 Автор
А почему вы решили что у меня этого нет? Резервный вариант попадания в квартиру есть. Статья в общем-то совсем не про это. Я и до распознавания лица без ключа долгое время ходил.
На счёт обхода оповещений, если Том Круз в очередной миссии захочет ко мне залезть, он конечно залезет. А обычным ворам зачем выбирать дверь с видеонаблюдением, если в этом же доме ещё 1000 квартир без видеонаблюдения?
fedorovAleks
Если действительно параноить на тему безопасности, то можно купить надежный замок, с защитой от взлома. По крайней мере такой замок сильно усложнит проникновение в квартиру.
А если кто-то будет специально прорабатывать возможность взлома, то замок, открывающий дверь по фотографии, только упростит дело.
aborouhin
Ой, да ладно, прикольно же :) У меня в загородном доме дверь вообще пластиковая со стеклом на бóльшую часть полотна (ну потому что смешно ставить стальную дверь, когда у тебя по всему первому этажу окна). Так что, может, такую же игрушку just for fun и реализовал бы, если было бы не лень.
Хотя вот ноут разблокировать лицом, хоть в нём и камера, сертифицированная для Windows Hello, которую, по идее, фотографией не обманешь, - уже страшно. Там уровень рисков гораздо больше.
IvanPetrof
Да и в квартиру зачастую можно проникнуть с балкона соседа (или через пожарную лестницу)