Система контроля доступа на основе распознавания лиц
Замена пропусков и PIN-кодов на биометрию лица — задача, где разница между «работает на демо» и «работает в продакшене» особенно велика. Демо: фото 10 сотрудников, офисное освещение, камера в лоб. Продакшен: 500 человек, боковые ракурсы, маски, очки, тёмные очки, подсветка сзади, 4 утра.
Пайплайн распознавания лиц для СКУД
Камера (RTSP) → Детекция лица → Выравнивание (alignment) → Эмбеддинг (ArcFace/AdaFace) → Поиск в базе → Решение → Управление замком
Критичный компонент — качество эмбеддинга. ArcFace (InsightFace) и AdaFace сегодня — стандарт де-факто для face recognition в контроле доступа.
import insightface
import numpy as np
import faiss
from pathlib import Path
import cv2
class FaceAccessControl:
def __init__(self, db_path: str, threshold: float = 0.5):
# InsightFace buffalo_l: detection + ArcFace embedding
self.app = insightface.app.FaceAnalysis(
name='buffalo_l',
providers=['CUDAExecutionProvider', 'CPUExecutionProvider']
)
self.app.prepare(ctx_id=0, det_size=(640, 640))
self.threshold = threshold # cosine distance
self.index, self.id_map = self._load_db(db_path)
def _load_db(self, db_path: str):
"""Загрузка базы эмбеддингов в FAISS"""
embeddings = []
id_map = {}
for i, emb_file in enumerate(Path(db_path).glob('*.npy')):
emb = np.load(emb_file)
embeddings.append(emb)
id_map[i] = emb_file.stem # employee_id
if not embeddings:
return None, {}
emb_matrix = np.vstack(embeddings).astype('float32')
faiss.normalize_L2(emb_matrix) # cosine similarity через IP
index = faiss.IndexFlatIP(512) # ArcFace dim = 512
index.add(emb_matrix)
return index, id_map
def recognize(self, frame: np.ndarray) -> list[dict]:
faces = self.app.get(frame)
results = []
for face in faces:
if face.det_score < 0.7:
continue # низкое качество детекции
emb = face.embedding.reshape(1, -1).astype('float32')
faiss.normalize_L2(emb)
D, I = self.index.search(emb, k=1)
score = float(D[0][0])
if score >= self.threshold:
results.append({
'employee_id': self.id_map[I[0][0]],
'confidence': score,
'bbox': face.bbox.astype(int).tolist(),
'decision': 'ALLOW'
})
else:
results.append({
'employee_id': None,
'confidence': score,
'bbox': face.bbox.astype(int).tolist(),
'decision': 'DENY'
})
return results
Главная проблема: порог решения (threshold)
Это самый болезненный параметр. Cosine distance 0.5 для ArcFace означает:
- FAR (False Accept Rate) ~0.1% — чужой проходит 1 раз на 1000 попыток
- FRR (False Reject Rate) ~3% — сотрудник получает отказ в 3% случаев
Реальные цифры сдвигаются при: очках (+2–4% FRR), масках (+8–15% FRR), боковом ракурсе > 45° (+5–10% FRR), плохом освещении (+6–12% FRR).
Решение для сложных условий — adaptive threshold: понижаем порог при низком качестве входного кадра и требуем повторного захвата.
def adaptive_threshold(face_quality: float,
base_threshold: float = 0.5) -> float:
"""Качество 0–1: liveness score * illumination * sharpness"""
if face_quality > 0.85:
return base_threshold # хорошие условия
elif face_quality > 0.65:
return base_threshold + 0.05 # чуть строже
else:
return 1.1 # отказ, запрос повторного кадра
Liveness detection: защита от фото и видео-атак
Без anti-spoofing система бесполезна — фотография на смартфоне открывает дверь.
| Метод | Защита от | Задержка | Точность |
|---|---|---|---|
| Texture analysis (LBP/CNN) | Печатное фото | +10ms | 96–98% |
| Depth camera (IR) | Фото + видео | +5ms | 99%+ |
| Challenge-response (blinking) | Фото + видео | 1–2 сек | 99%+ |
| 3D face model | Маски, 3D-печать | +20ms | 97–99% |
Для турникетов с высокой пропускной способностью — texture analysis + passive IR (без challenge). Для серверных комнат и высококритичных зон — 3D depth camera обязательна.
Кейс: бизнес-центр, 800 сотрудников, 12 точек доступа
Использовали InsightFace buffalo_l + FAISS IVF256 (approximated, ускоряет поиск при > 500 лицах). Камеры Hikvision 4MP с ИК-подсветкой, установлены на высоте 1.4–1.6м.
Проблема при запуске: FRR 12% — слишком много отказов. Причина — в базе у ряда сотрудников было только одно фото анфас. После дообучения базы на 5 фото с разными углами (±30°, +/-15° pitch) и в очках при наличии:
- FRR снизился до 1.8%
- FAR: 0.02% за 3 месяца эксплуатации
- Пропускная способность: 40 человек/мин на турникет (задержка 120–180ms)
Инференс на Intel Core i7 + NVIDIA RTX 3060 Ti: 35ms на лицо, 8 параллельных потоков.
Регистрация новых сотрудников
def enroll_employee(employee_id: str, photos: list[np.ndarray],
min_photos: int = 3) -> np.ndarray:
"""Усреднённый эмбеддинг из нескольких фото"""
embeddings = []
for photo in photos:
faces = app.get(photo)
if faces and faces[0].det_score > 0.85:
embeddings.append(faces[0].embedding)
if len(embeddings) < min_photos:
raise ValueError(f"Недостаточно качественных фото: {len(embeddings)}")
# Среднее нормализованное — лучше чем просто первое фото
mean_emb = np.mean(embeddings, axis=0)
mean_emb /= np.linalg.norm(mean_emb)
return mean_emb
| Масштаб | Срок |
|---|---|
| До 100 сотрудников, 1–2 точки | 2–4 недели |
| До 500 сотрудников, 5–15 точек | 5–8 недель |
| Enterprise 1000+ сотрудников | 10–16 недель |







