Luckfox думає, STM32 діє: face detection від камери до реле
Серія «STM32 з нуля без HAL» · Частина 10 · Місяць 3
У Статті 9 ми змусили NPU видавати координати обличчя в консоль. Красиво — але рядки в терміналі це не система. Система — коли хтось підходить до камери і фізично клацає реле. Саме це ми зробимо сьогодні.
Фінальний результат: обличчя в кадрі → Luckfox детектує через NPU → фото летить на сервер → DeepFace звіряє з базою → SSH команда на Luckfox → STM32 отримує on по UART → реле спрацьовує.
П’ять стрибків. Розберемо кожен.
◆ Архітектура — три вузли
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
Камера (MIPI) ↓ Luckfox Pico Pro (192.168.1.125) NPU: RetinaFace детектує обличчя curl: відправляє ROI фото на сервер UART1 (/dev/ttyS1): отримує команди від сервера через STM32 ↓ ethernet PC сервер (192.168.1.121) Flask: приймає фото DeepFace: звіряє з базою known_faces/ SSH: якщо OK → команда на Luckfox → stm32_controller.py ↓ SSH → UART STM32 Blue Pill Отримує on/off по UART Керує реле через SN74HC595N |
Чому саме так розподілені ролі:
Luckfox детектує — NPU апаратно, швидко, в реальному часі. CPU на це не витрачається.
Сервер розпізнає — база облич може рости, моделі змінюватись, поріг підлаштовуватись. Все це без перезбірки прошивки.
STM32 діє — надійний, детермінований. Отримав команду — виконав. Без Linux overhead.
До речі, поки налаштовував ethernet між Luckfox і сервером — задумався: а як в реальних автономних системах з’єднують vision модулі з мозком? CAN? Automotive Ethernet? Luckfox не має WiFi — тільки ethernet і UART. Для стаціонарної системи це нормально, для мобільного робота — обмеження. Якщо є досвід з robotics або automotive — цікаво почути в коментарях.
◆ Модифікація main.cc — мінімально і чисто
Оригінальний main.cc виводить координати в консоль і все. Нам треба додати два блоки: зберегти ROI обличчя як JPEG і відправити на сервер через curl.
Повний код вже готовий — зупинимось на ключових рішеннях.
IP сервера — в аргументи, не в код:
|
1 2 3 4 5 6 |
// Не так: #define SERVER_URL "http://192.168.1.121:5000/face" // перезбірка при зміні IP // А так — п'ятий аргумент командного рядка: const char *server_url = argv[4]; |
Міняєш IP — просто інший аргумент при запуску. Без make.
Throttling — не більше одного запиту на 5 секунд:
|
1 2 3 4 5 6 7 8 |
static time_t last_send = 0; time_t now = time(NULL); if (now - last_send >= 5) { cv::imwrite("/tmp/face.jpg", face_img); // curl POST... last_send = now; } |
Без throttling — при кожному кадрі де є обличчя летить запит. Це 10-15 POST на секунду. Плата гріється, сервер задихається.
Відправка — асинхронно:
|
1 2 3 4 5 6 7 8 9 |
char cmd[512]; snprintf(cmd, sizeof(cmd), "curl -s -X POST %s " "-F \"image=@/tmp/face.jpg\" " "-F \"norm=%.3f\" " "-o /tmp/face_result.txt &", // & = у фон server_url, norm); system(cmd); |
& в кінці — curl іде у фоновий процес. Детекція не зупиняється поки curl чекає відповідь.
⚠
system(curl ...)— не найелегантніше рішення з точки зору C++. Правильніше використовуватиlibcurlAPI напряму. Але для статті це чесно і читабельно — кожен розуміє що відбувається без знання libcurl.
Збірка — підводний камінь з file(GLOB):
CMakeLists використовує file(GLOB SRC_FILES "${SRC_DIR}/*.cc") — бере всі .cc файли в директорії. Якщо перейменував старий main.cc замість видалення — CMake підхопить обидва файли і отримаєш конфлікт символів:
|
1 2 |
make[2]: *** Error 1 ← конфлікт main() з двох файлів |
Рішення: видаляти старий файл, не перейменовувати.
◆ Сервер — три варіанти і що з них вийшло
Для розпізнавання облич по базі є кілька Python бібліотек. Спробував усі три — ось чесний результат.
Варіант 1 — face_recognition + dlib
Найпопулярніший на GitHub, найбільше туторіалів.
|
1 2 |
pip install face_recognition |
|
1 2 3 |
ERROR: Failed building wheel for dlib subprocess.CalledProcessError: Command '['cmake', '--build', ...]' returned non-zero exit status 2 |
⚠
dlib— це C++ бібліотека яка компілюється при встановленні. Потребує:
1234 sudo apt install cmake libopenblas-dev liblapack-dev libx11-devpip install dlibpip install face_recognitionЯкщо і після цього не збирається — залежить від версії gcc і cmake. На Ubuntu 22.04 зазвичай проходить після встановлення залежностей.
Варіант 2 — DeepFace + TensorFlow
|
1 2 |
pip install deepface |
Встановлюється без компіляції. Під капотом може використовувати VGG-Face, FaceNet, ArcFace — вибираєш модель. Ми використовуємо FaceNet + cosine similarity.
|
1 2 |
ERROR: ModuleNotFoundError: No module named 'tf_keras' |
⚠ TensorFlow 2.21 відокремив Keras в окремий пакет.
deepfaceще не оновив залежності автоматично:
12 pip install tf-keras
Після цього запускається. Перший виклик DeepFace.verify() автоматично скачує модель (~92MB FaceNet weights) в ~/.deepface/weights/. Це один раз.
⚠ Перший POST запит може прийти поки модель ще качається. Flask обробляє його в окремому потоці — система не падає, але перший результат може бути повільним.
Варіант 3 — cosine similarity на numpy
Найлегший підхід — без важких бібліотек взагалі. FaceNet вже рахує 128-мірний embedding на Luckfox. Якщо відправляти цей вектор на сервер замість фото — порівняння це буквально 5 рядків numpy:
|
1 2 3 4 5 |
import numpy as np def cosine_similarity(a, b): return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)) |
Потребує модифікації main.cc щоб відправляв вектор out_fp32[128] разом з фото. Залишаємо як вправу — хто хоче найлегше рішення без TensorFlow.
Обрали Варіант 2 — DeepFace встановлюється, працює одразу, база облич — просто папка з фото.
◆ Enrollment — як додати себе в базу
Найелегантніший момент системи — і я його не планував.
Щоб сервер тебе впізнав — потрібне еталонне фото. Де його взяти? Я просто підійшов до камери. Система сфоткала моє обличчя і зберегла в received/. Я взяв це фото і поклав в базу:
|
1 2 |
cp received/face_20260521_103524_UNKNOWN.jpg known_faces/Alex.jpg |
Перезавантажую базу без рестарту сервера:
|
1 2 |
curl -X POST http://localhost:5000/reload |
Наступний раз — OK:Alex. Enrollment через саму систему.
Для кількох людей — та сама схема. Кожен підходить до камери, фото падає в received/, перекладаєш в known_faces/Ім'я.jpg, робиш /reload.
◆ SSH ключі на Buildroot — чому не спрацювало
Щоб сервер міг по SSH заходити на Luckfox без пароля — треба SSH ключ. Генеруємо і копіюємо:
|
1 2 3 |
ssh-keygen -t rsa -f ~/.ssh/luckfox -N "" ssh-copy-id -o PubkeyAuthentication=no -i ~/.ssh/luckfox.pub root@192.168.1.125 |
Перевіряємо:
|
1 2 |
ssh -i ~/.ssh/luckfox -o IdentitiesOnly=yes root@192.168.1.125 "echo test" |
|
1 2 |
root@192.168.1.125's password: ← питає пароль — ключ не спрацював |
Дивимось на платі:
|
1 2 3 |
ls -la /root/.ssh/ ls -la / | grep root |
|
1 2 3 4 |
drwx------ 2 root root .ssh/ ← права правильні: 700 -rw------- 1 root root authorized_keys ← права правильні: 600 drwx------ 4 1000 1000 /root ← а ось це проблема! |
/root директорія належить uid 1000 а не root (uid 0). Buildroot створює її під іншим користувачем. SSH демон перевіряє власника home директорії — і якщо вона не належить користувачу який логіниться, відхиляє ключ з міркувань безпеки. Навіть якщо authorized_keys має правильні права.
Виправити можна через chown root:root /root — але на Buildroot це не персистентно між перезавантаженнями якщо /root монтується з tmpfs.
Практичне рішення — sshpass:
|
1 2 3 4 5 6 7 |
sudo apt install sshpass sshpass -pluckfox ssh \ -o StrictHostKeyChecking=no \ -o PubkeyAuthentication=no \ root@192.168.1.125 "echo test" |
Так, передавати пароль через аргумент — не найкраща практика безпеки. Для домашньої лабораторії і статті — прийнятно. Для продакшн — виправляти через chown в init скрипті Buildroot.
◆ Реле спрацьовує
Сервер отримує фото → DeepFace порівнює з базою:
|
1 2 3 4 5 |
Alex: dist=0.073 verified=True [20260521_113501] norm(rknn)=1.442 | dist(deepface)=0.073 | OK:Alex [реле] відкриваємо для Alex на 5с [STM32] on → ok |
dist=0.073 при порозі 0.40 — DeepFace дуже впевнений. Два числа в логу:
normвід RKNN на Luckfox — детекція на NPUdistвід DeepFace на PC — розпізнавання по базі
Захист від паралельних викликів:
Luckfox відправляє фото раз на 5 секунд. Але Flask обробляє запити в кількох потоках — без захисту реле спрацьовувало б кілька разів підряд:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
relay_lock = threading.Lock() relay_active = False def relay_open(name): global relay_active with relay_lock: if relay_active: print(" [реле] вже активне — пропускаємо") return relay_active = True # ... on → sleep → off → relay_active = False |
Тепер в логах:
|
1 2 3 4 5 6 7 |
[реле] відкриваємо для Alex на 5с ← перше спрацювання [реле] вже активне — пропускаємо ← паралельні запити ігноруються [реле] вже активне — пропускаємо [STM32] on → ok [STM32] off → ok [реле] закрито ← готове до наступного |
◆ Клонування плати
Питання яке виникає коли система працює: як перенести на іншу плату?
update.img в ~/luckfox-pico/output/image/ — це Buildroot образ. Він містить все що зібрано через make: ядро, rootfs, наші пакети (go-dashboard, статичний IP). Прошивається одною командою:
|
1 2 |
./tools/linux/Linux_Upgrade_Tool/upgrade_tool uf output/image/update.img |
⚠ Але файли які клали через
scp— бінарник детекції, моделі,stm32_controller.py— в образі не збережені. Вони живуть в/root/який не входить в rootfs образу.
Після прошивки нової плати треба скопіювати:
|
1 2 3 4 5 6 7 |
scp -o PubkeyAuthentication=no -r \ luckfox_pico_rknn_example/install/uclibc/luckfox_pico_retinaface_facenet_demo \ root@NEW_IP:/root/ scp -o PubkeyAuthentication=no \ stm32_controller.py root@NEW_IP:/root/ |
Щоб файли були в образі автоматично — треба Buildroot overlay або пакет. Це тема для окремої статті.
◆ Гайд-шпаргалка
Одноразово — збірка бінарника на PC:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
export LUCKFOX_SDK_PATH=/home/alex/luckfox-pico cd luckfox_pico_rknn_example ./build.sh # вибираємо: 1 (uclibc) → 1 (retinaface_facenet) scp -o PubkeyAuthentication=no -r \ install/uclibc/luckfox_pico_retinaface_facenet_demo \ root@192.168.1.125:/root/ scp -o PubkeyAuthentication=no \ stm32_controller.py root@192.168.1.125:/root/ |
Кожного разу — запуск:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# Термінал 1: сервер на PC cd ~/Sasha/www/STM32/python python3 face_server_deepface.py # Термінал 2: детекція на платі ssh -o PubkeyAuthentication=no root@192.168.1.125 RkLunch-stop.sh cd /root/luckfox_pico_retinaface_facenet_demo ./luckfox_pico_retinaface_facenet \ model/RetinaFace.rknn \ model/mobilefacenet.rknn \ model/test.jpg \ http://192.168.1.121:5000/face |
Додати людину в базу:
|
1 2 3 4 |
# Підійди до камери → фото в received/ cp received/face_TIMESTAMP_UNKNOWN.jpg known_faces/Maria.jpg curl -X POST http://localhost:5000/reload |
Перевірити стан:
|
1 2 |
curl http://192.168.1.121:5000/status |
Перевірити STM32 окремо:
|
1 2 3 4 |
# На платі STM32_PORT=/dev/ttyS1 python3 /root/stm32_controller.py on STM32_PORT=/dev/ttyS1 python3 /root/stm32_controller.py off |
Блог серії: nodejs.jdymora.com/category/embedded/stm32 Репозиторій: github.com/pipicosim800-maker/stm32F103
◆ Бонус: SD карта, swap і додаткове сховище
Поки налаштовував систему — знайшов вільний SanDisk Ultra 32GB. Вставив в Luckfox і прийшла ідея: система працює, фото летять в received/ на PC — але що якщо перенести сховище прямо на плату?
Але спочатку важливіше — пам’ять.
Скільки RAM на RV1106:
|
1 2 |
free -h |
|
1 2 3 |
Mem: 53.3M 23.8M 10.3M Swap: 0 0 0 |
53MB RAM і нуль swap. RetinaFace запускається — він відносно легкий. Але якщо захочеш YOLOv8 або важчі моделі:
⚠
Can't allocate memory— класична помилка RKNN при нестачі RAM. Автор статті на Habr про YOLOv8 на Luckfox Pico Mini зіткнувся з тим самим. Рішення — swap.
Перевіряємо SD карту:
|
1 2 3 |
ls /dev/mmcblk* df -h | grep mmcblk |
|
1 2 |
/dev/mmcblk1p1 14.1G 1.6G 12.3G 12% /mnt/sdcard |
Карта змонтована як /mnt/sdcard. Замість перерозбивки — простіший шлях через swap файл:
|
1 2 3 4 5 6 |
dd if=/dev/zero of=/mnt/sdcard/swapfile bs=1M count=512 mkswap /mnt/sdcard/swapfile chmod 600 /mnt/sdcard/swapfile swapon /mnt/sdcard/swapfile free -h |
|
1 2 3 |
Mem: 53.3M 24.3M 2.1M Swap: 512.0M 0 512.0M |
512MB swap — YOLOv8 тепер запуститься.
Автозапуск після перезавантаження:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
cat > /etc/init.d/S90autoswap << 'EOF' #!/bin/sh case $1 in start) swapon /mnt/sdcard/swapfile ;; stop) swapoff /mnt/sdcard/swapfile ;; *) exit 1 ;; esac EOF chmod +x /etc/init.d/S90autoswap |
Для Автобота — що це дає:
SD карта на мобільній платформі це зручно:
received/з фото облич — прямо на карті, не на PC- логи детекції — зберігаємо локально
- embeddings бази — переносити карту замість SSH
|
1 2 3 4 5 |
# Переносимо received/ на SD mkdir -p /mnt/sdcard/received # В face_server_deepface.py: # SAVE_DIR = "/mnt/sdcard/received" ← якщо сервер на платі |
Карта виймається — переносиш дані на PC фізично. Без мережі, без SSH. Для польових умов Автобота це практично.
Практична ідея для Автобота:
|
1 2 3 4 |
<span class="token token"># При старті — все підхоплюється з карти автоматично</span> /mnt/sdcard/models/RetinaFace.rknn /mnt/sdcard/models/mobilefacenet.rknn /mnt/sdcard/known_faces/Alex.jpg |
Міняєш карту — міняєш конфігурацію системи. Без перепрошивки.