Як ми зробили пошук фото за селфі для DOU Day 2026

💡 Усі статті, обговорення, новини про Front-end — в одному місці. Приєднуйтесь до Front-end спільноти!

Привіт, спільното! Мене звати Олександр Корнієнко, я Senior+ React.JS Software Developer & Founder iqurabooks.com. У цій статті я хочу поділитися тим, як реалізував пошук власних фото серед тисяч фотографій із нещодавньої конференції DOU Day 2026.

Контекст

DOU Day 2026 — наймасштабніша технологічна конференція, яка об’єднує тисячі айтівців в одному місці раз на рік. Після завершення конференції організатори зазвичай вивантажують гігабайти фотографій на Google Drive. Цей рік не став винятком й містив 5 146 фотографій, які сумарно важать 16.3 ГБ.

Проблема

Учасник конференції отримує посилання на цю величезну купу фотографій і дуже хоче знайти усі фото із собою. Але щоб це зробити, доводиться годину гортати тисячі фото. Це довго, нудно й не гарантує, що все ж щось знайдеш. У результаті учасник або здається на першій сотні фотографій, або пропускає фото через те, що дуже швидко аналізував попередні й наступні фото.

Ідея

Не користувач має шукати себе серед фотографій, а фотографії мають самі знайти користувача. Замість того, щоб перебирати тисячі чужих облич, учасник конференції робить селфі, і вебзастосунок повертає всі фото, де був збіг по обличчях.

Аналіз наявних рішень

Ідея не нова, щось подібне я бачив раніше, тому хтось точно вирішив цю проблему до мене. Одним із найвідоміших прикладів є PimEyes — сервіс, який дозволяє знаходити фотографії людини в інтернеті на основі завантаженого селфі. Проте основне обмеження такого підходу полягає у відсутності можливості працювати з власним датасетом користувача або організатора події.

Також були проаналізовані комерційні сервіси для пошуку фотографій на івентах. Основною проблемою таких платформ є модель ціноутворення, де вартість залежить від кількості оброблених фотографій. Для оцінки потенційних витрат було виконано порівняння на прикладі датасету конференції DOU Day 2026, який містив 5 146 фотографій (див. таблицю)

Таблиця існуючих рішень

Альтернативним підходом є використання Google Photos або Apple Photos. Після завантаження фотографій ці сервіси автоматично виконують індексацію облич, що дозволяє згодом фільтрувати фото за людьми. Проте індексація займає певний час, рішення погано масштабується для великих подій, а кожен користувач змушений завантажувати повний датасет фотографій локально, навіть якщо його цікавить лише пошук власних знімків.

Загалом, можна було б використати один з цих варіантів, але мені було цікаво зробити дослідження, яке передбачало створити свій тимчасовий застосунок виключно на одну подію — DOU Day 2026. На івенті дуже багато говорили про АІ (LLM), тому чому б не спробувати й перевірити як працює пошук по фото й залучити учасників події до експерименту й отримати їхні коментарі про те, чи спрацював пошук, як планувалось чи все ж щось пішло не так враховуючи, що одне фото може містити багато облич та різне освітлення.

Мем про вайбкодинг

Вибір моделі

Найважливішою частиною реалізації пошуку фото за селфі був вибір моделі для розпізнавання облич. Основними критеріями були точність, швидкість роботи та можливість запуску локально на CPU без додаткового навчання моделі. Чомусь перша модель, яку я помітив, була buffalo_l з бібліотеки InsightFace.У результаті була обрана саме ця модель, оскільки вона забезпечує якісні face embeddings з точністю 99.8% (дані взяті з їхнього сайту).

Ліцензійні обмеження та оцінка ризиків

Бібліотека InsightFace розповсюджується під ліцензією MIT, ситуація з pretrained моделями є більш складною. Сам код бібліотеки може вільно використовуватись, модифікуватись та інтегруватись у комерційні системи. Водночас pretrained ваги моделей buffalo_l, antelopev2, buffalo_s, buffalo_m поширюються на окремих умовах і для комерційних цілей потребують комерційної ліцензії.

Архітектура системи та вибір технологій

Клієнт

Закритий застосунок з паролем, який не повинен індексуватись в пошукових системах і для цього можна обрати будь-який client-side фреймворк/бібліотеку. Для цієї задачі не має значення, чи це Vue.JS, React.JS чи Angular і тд. Але для себе я обрав React.JS .

Бекенд

Модель працювала на Python. При цьому це не означає, що весь backend обов’язково треба писати на Python, модель можна винести в окремий API-сервіс і інтегрувати, наприклад, у Node.js чи ASP.NET. Але для мого випадку це лише ускладнило б систему, тому я використав FastAPI і реалізував API напряму на Python.

База даних

Можна було використати PostgreSQL + pgVector для роботи з embeddings, але для такої невеликої задачі це було б зайвим ускладненням. Тому вибір припав на SQLite — база даних зберігається у вигляді одного файлу, не потребує окремого сервера чи додаткових ресурсів і добре підходить для такого об’єму даних.

DevOps

Backend на Python був контейнеризований за допомогою Docker. Frontend на React.js збілджений у статичні файли та розгорнутий через Nginx. Nginx також використовувався для маршрутизації запитів, отримання логів, роздачі фотографій із датасету та обмеження доступу до застосунку через Basic Authentication. Уся система хостилася на VPS-сервері.

nginx конфігурація

Конфігурація Nginx

В ідеалі щоб працювати з фото, то вони мають зберігатись десь на CDN, AWS S3 або Cloudflare, але оскільки це демопроєкт, то я хостив це все на своєму VPS з певною конфігурацію кешу для фото. Де я вказав, що вебзастосунок не повинен індексуватись пошуковими системами, надав підтримку кешу, та можливість повернення на 404 сторінку, якщо не знайдено фото.

server {
   root /var/www/projects/photos/face-recognition/UI/dist;
   sendfile on;
   tcp_nopush on;
   tcp_nodelay on;

   location /dou-photos/ {
        alias /var/www/projects/photos/dataset/;
        autoindex off;
        add_header X-Robots-Tag "noindex, nofollow, nosnippet, noarchive" always;
        expires 4d;
        add_header Cache-Control "public, no-transform, max-age=2592000";
        error_page 404 =404;
   }

   location / {
        auth_basic "Restricted Area";
        auth_basic_user_file /etc/nginx/passwords/.htpasswd_photos;
        try_files $uri $uri/ /index.html =404;
   }

   location /api/ {
        proxy_pass http://IPv4:port/;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }

}

Auth_basic_user_file — доступ до контенту виключно по паролю, який згенеровано у .htpasswd_photos

Логи та базова аналітика

Для збору мінімальної аналітики та дебагу використовувалися стандартні можливості Nginx. Основним джерелом інформації був access.log, у якому зберігалися IP-адреса клієнта, endpoint, HTTP-метод, user-agent та статус відповіді сервера.

Приклад запису:

IPv4 - dou-photos [21/May/2026:17:17:51 +0000] "GET /api/stats HTTP/1.1" 200 29 
"https://photos.iqurabooks.com/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) 
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36"

Оскільки окремої системи аналітики не використовувалося, для підрахунку кількості унікальних користувачів, які хоча б раз скористалися пошуком, застосовувався простий аналіз логів через shell-команди:

zgrep -h 'https://photos.iqurabooks.com/' /var/log/nginx/access.log* \
  | grep '/api/search' \
  | awk '{print $1}' \
  | sort \
  | uniq \
  | wc -l

Було враховано, що Nginx може автоматично архівувати старі логи у форматі access.log.*.gz. Окремо використовувався error.log, який допомагав знаходити проблеми з доступом або конфігурацією. Наприклад, під час тестування було помітно, що деякі користувачі не могли пройти Basic Authentication через неправильні credentials:

2026/05/21 14:18:43 [error] 1564093#1564093: *5765 user "111" was not found in 
"/etc/nginx/passwords/.htpasswd_photos", client: IPv4, server: photos.iqurabooks.com, 
request: "GET / HTTP/1.1", host: "photos.iqurabooks.com"

Генерація коду

Для генерації коду використовував безкоштовні браузерні версії моделей Claude, ChatGPT та Gemini. Робота будувалася ітеративно: коли впирався в ліміти запитів в одному АІ, то переходив вже до іншого АІ чат бота допоки було куди йти. Після оновлення лімітів повертався до чат бота з якого починав. Можна було б ще використати безкоштовну версію Cursor, але і без нього впорався.

Фронтенд

100% фронтенд коду я навайбкодив. На цьому я міг би зупинитись і не писати про клієнтську частину, але трошки додам. Фронтенд мав виконувати такі задачі:

  1. Демонстрація Terms of Use та блокування доступу до застосунку до моменту прийняття умов користувачем.
  2. Отримання селфі користувача як з камери, так і з галереї.
  3. Можливість вибору обличчя на фото для підвищення точності пошуку.
  4. Обробка кейсу, коли обличчя не розпізнається системою, з відповідним повідомленням та повторною спробою.
  5. Відображення результатів пошуку у вигляді сітки знайдених фотографій.
  6. Можливість завантаження оригінальних фотографій.
  7. Оптимізації продуктивності:
    7.1 Lazy loading для зображень.
    7.2 Кешування запитів до статистики.
  8. Робота з API через чистий fetch без додаткових обгорток:
    8.1 Отримання статистики через GET import.meta.env.VITE_API_URL/api/stats.
    8.2 Відправка селфі на сервер через POST import.meta.env.VITE_API_URL/api/search з file: blob.

Для реалізації використовував React, Tailwind CSS, lucide-react та react-easy-crop.

Ось приблизно так виглядав запит до Claude Code, але англійською й попросив також план того що він буде робити, і 80% коду, який використовується зараз був згенерований саме цим промптом. Усе інше я йому більш точково задавав.

Dou Day 2026 Finder

Що таке Embedding

Embedding у контексті face recognition — це числовий вектор, який модель генерує для кожного обличчя і який містить його унікальні ознаки у багатовимірному просторі. Обличчя однієї людини мають близькі embedding-и, а різних людей — віддалені.

Там, де живе магія

Black Box Magic

А тепер до цікавого. Пошук по селфі виглядає як магія. На вході селфі, а на виході — фото з конференції. Посередині між ними знаходиться Black box. Що ж там? ChatGPT? Claude? Grok? АІ модель, яка перенавчена на наборі даних? З цього моменту вже більш детально як був побудований процес пошуку. Спойлер: я не перенавчав модель і не займався навчанням.

Щоб шукати щось, потрібно розуміти де шукати, для цього я завантажив увесь датасет, який складався з 5146 фотографій. Проаналізував датасет на шум, видалив усі фотографії, які не містять облич, так як ці фото не несуть користі, таким чином отримав 4385 фото на яких є принаймні одне обличчя.

Маючи датасет ідея була перебрати кожне фото, знайти на ньому усі обличчя і кожному обличчю присвоїти унікальний візерунок. Тобто знайти усі обличчя і отримати embedding для кожного лиця й зберегти у базі даних.

Face Recognition Flow

Структура бази даних

id INTEGER PRIMARY KEY AUTOINCREMENT,
filename TEXT NOT NULL,
face_index INTEGER NOT NULL DEFAULT 0,
embedding BLOB NOT NULL

Так як одне фото може мати декілька облич, пронумерував кожне через face_index. Тепер маючи дані, потрібно реалізовувати пошук. Користувач відправляє своє селфі у модель, а вона повертає нам embedding.

Black Box Search

Навіть при 100%-му збігу embedding input_embedding != output_embedding. Як їх порівнювати? Існують різні варіації порівнянь, і важко сказати, який найкраще підходить для цієї задачі, але я вирішив обрати «Косинус подібності».

A = input_embedding, B=stored_embeddings[i], де i = 0...n-1, де n — кількість embedding в базі даних

Косинус Подібності

Для цієї формули була створена cosine_similarity функція, яка повертає результат подібності векторів від 0.00 до 1.00, де 1 = точний збіг.

def cosine_similarity(a, b):
    return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))

Таким чином, після порівняння отримуємо результат збігу і залишилось визначити, коли ми вважаємо, що на фото точно та людина, яку ми очікуємо. Потрібно визначити поріг подібності, але як?

Google говорить, що для цієї моделі, яка використовує ArcFace алгоритм оптимально ставити поріг у 0.45 і це буде означати, що 45% ймовірність що на фото саме те обличчя, яке ми шукали. Але я зробив по-іншому. Я взяв 10 людей, які є в датасеті і почав шукати їх за допомогою цього підходу. В середньому так і було, де був результат 45% то це була точно та людина, усе що було нижче 34% була зазвичай інша схожа людина. Приклад надаю як я пробував знайти себе, але з імовірністю у 33% знаходило спікера, який має подібну масу тіла, теж має окуляри і має схожу коротку зачіску. Але попри це, декілька разів знаходив людей з імовірністю 31%, але просто вони були у великій сукупності людей, або в тіні, або боком. Тому для себе я прийняв рішення, що краще поставлю поріг у 30% і покажу декілька зайвих фотографій, якщо такі будуть, ніж пропущу цікаве фото.

Спікер з подібністю 33%

Подібність 33%

Локальна обробка даних

Генерація embedding-векторів виконувалась локально на ноутбуці без використання хмарних GPU-сервісів. Основною причиною була конфіденційність фотографій, оскільки всі зображення залишались на локальному пристрої та не передавались у сторонні сервіси. Додатково це дозволило уникнути витрат на оренду GPU та спростило процес розробки й тестування. Для обробки використовувався CPUExecutionProvider, оскільки модель buffalo_l стабільно працює навіть без відеокарти. Усього було проіндексовано понад 5146 фотографій, а генерація embedding-векторів зайняла приблизно 35 хвилин на ноутбуці MacBook Pro 2020 (M1, 8GB RAM).

Локальне отримання embeddings

Приватність

Перед відправкою селфі на backend користувач повинен погодитися з правилами сайту та політикою обробки даних. Таким чином користувач отримує пояснення, як саме буде використовуватись його фотографія, а також дає згоду на тимчасову обробку зображення для пошуку фото. Допоки правила не будуть прийняті, доступ до функціоналу вебзастосунку залишається заблокованим.

Модель працювала локально, тому усі embeddings, які були взяті з датасету були опрацьовані локально на моєму ноуті і збереглися у моїй SQlite базі даних.

Під час розробки застосунку безпека була одним із пріоритетів. На стороні backend усі завантажені зображення проходили базову валідацію: перевірявся тип файлу, розмір зображення та можливість його коректного декодування перед подальшою обробкою моделлю. Доступ до застосунку був обмежений через Nginx Basic Authentication, а сам сервіс працював у закритому середовищі лише для учасників конференції. Окремо варто відзначити, що майже одразу після запуску в логах почали з’являтися запити від ботів, які намагалися отримати доступ до .env файлів та інших конфігураційних шляхів. Це ще раз показало, що навіть невеликий тестовий вебзастосунок в інтернеті автоматично стає ціллю для базового сканування та пошуку вразливостей.

Основна ідея під час розробки полягала в тому, щоб мінімізувати рівень довіри до будь-яких даних, які надходять від користувача, навіть попри те, що застосунок мав тестовий та дослідницький характер.

Фідбек пошуку за селфі

Декілька класних випадків, коли вебзастосунок знаходив фото там, де людина скоріше всього б пропустила. Ці фото взяті від людей, які вирішили поділитись своїм досвідом використання вебзастосунку.

Павло

Артур

Інна

Інна та Назарій

Бонусний випадок

Під час тестування один з учасників помітив цікавий баг: якщо завантажити фотографію з «Акулою зборів», вебзастосунок починав показувати 94 результати різних людей із доволі високою схожістю 54%.

Проблема була в тому, що у мене вже була перевірка на наявність обличчя, але не було додаткової перевірки наскільки модель впевнена, що це справді людське обличчя. Через це система могла генерувати некоректні embeddings і показувати некоректні результати.

Після цього я додав додаткову перевірку для випадків, де модель була недостатньо впевнена у детекції обличчя, а також почав відкидати занадто слабкі або некоректні embeddings. Після цього моменти з «Акулою зборів» не приходили.

Акула

Human Face Recognition

Результати перевершили очікування

Метрики були зібрані з логів вебзастосунку та анонімного опитування користувачів у Google Forms. Це дозволило поєднати кількісні дані використання з якісним фідбеком про досвід взаємодії із системою.

  • 481 унікальних користувачів відкрили вебзастосунок
  • 357 користувачів хоча б один раз скористалися пошуком
  • 887 опрацьованих селфі
  • 100% опитаних користувачів знайшли свої фотографії
  • Середня оцінка точності пошуку — 9.6/10
  • 100% користувачів хотіли б бачити такий сервіс на інших івентах
  • Більшість пошукових запитів виконувалися менш ніж за 5 секунд
  • Користувачі позитивно оцінили здатність моделі знаходити фотографії навіть при поганому освітленні, боковому ракурсі, на задньому плані та у випадках, коли фото були частково розмиті

Аналітика

Окремо, у Google Forms користувачі ділилися суб’єктивними враженнями про роботу системи.

Що найбільше сподобалось:

«Знайшло фото, де я боком у кутку кадру — при мануальному пошуку я навіть не звернула на нього уваги»

Що варто покращити:

«Додати папки з 2025»

Висновки

Проєкт вийшов доволі експериментальним, але водночас дуже практичним. Основною задачею було максимально швидко і при цьому зберігаючи якість запустити вебзастосунок до конференції.

Найбільшим інсайтом стало те, наскільки добре спрацював пошук облич на реальних фотографіях з конференції. Модель успішно знаходила людей навіть у складних умовах: при поганому освітленні, бокових ракурсах, частковому перекритті обличчя та навіть на задньому плані кадру з розмитим фокусом.

Окремим викликом стали питання безпеки та legal-частини проєкту: від моменту генерації embeddings для датасету до обробки селфі користувачів. Саме тому застосунок працював у форматі закритого тестового середовища з доступом лише для учасників конференції через пароль, а перед використанням функціоналу користувачі повинні були погодитися з правилами обробки даних.

За результатами тестування усі користувачі позитивно оцінили якість пошуку та відзначили зручність такого формату доступу до фотографій після конференції. Також люди хотіли б використовувати подібну системи на майбутніх івентах.

Тестування тривало 5 днів та мало виключно дослідницький характер. У проєкті використовувалися моделі InsightFace, однак у випадку переходу до повноцінного комерційного використання необхідно додатково враховувати ліцензійні умови конкретних моделей та компонентів.

Цікаво почути, як ви зазвичай шукаєте свої фото з конференцій? Також цікава думка тих, хто щодня працює з подібними математичними моделями. І якщо тут є ті, хто користувався вебзастосунком, буду радий бачити ваш фідбек у коментарях.

Також отримав дуже багато теплих відгуків у приватних повідомленнях ❤️

👍ПодобаєтьсяСподобалось22
До обраногоВ обраному6
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

А за допомогою цього сервісу можна знайти мого батька? Який у 2019 пішов за хлібом. Знайомі подейкують що він був присутній на DOU Day 2026 р.

Підписатись на коментарі