Компьютерное зрение на Raspberry Pi
DariaМой робот на Raspberry Pi 4 учился распознавать объекты. Первая нейросеть назвала банан феном, машинку — поездом, а меня подписала «???». Вторая — того же размера — узнала всё с первого взгляда. Это разбор, почему так вышло: с архитектурой, кодом и граблями, на которые я наступила. И в конце — список моделей, которые вообще имеет смысл запускать на «малине».
Дуэлянты
MobileNet SSD v1 (2018): 4,2 МБ, вход 300×300, квантованная в int8. Точность на датасете COCO — около 21 mAP. На Pi 4 выдаёт ~7–8 кадров в секунду.
EfficientDet-Lite0 (2021): 4,5 МБ, вход 320×320, тоже int8. Точность — около 26–27 mAP. На Pi 4 — ~3–5 кадров в секунду.
Разница в весе — семь процентов. Разница в точности — почти на треть. Дальше — почему.
Как устроена старая
MobileNet SSD — это два блока. Бэкбон (MobileNet) прогоняет картинку через свёртки и выжимает карты признаков: «здесь края, здесь текстура, здесь что-то жёлтое». Чтобы влезть в 4 МБ, свёртки разложены на depthwise separable — приём, который режет число умножений примерно в девять раз, но и выразительность режет тоже.
Детекторная голова (SSD — Single Shot Detector) смотрит на несколько карт признаков разного масштаба и на каждой предсказывает рамки и классы за один проход. Звучит неплохо, но есть тонкость: карты разных масштабов не общаются между собой. Мелкая карта видит детали, но не знает контекста; крупная видит силуэт, но не различает мелочи. Каждая гадает в одиночку.
Итог для банана: на входе 300×300 от него остаётся жёлтое вытянутое пятно. Карта, которая могла бы заметить хвостик, ничего не знает о том, что силуэт продолговатый. «Жёлтое вытянутое» — это и банан, и фен. Модель выбирает наугад — с уверенностью 59%.
Что добавила новая
EfficientDet-Lite0 отличается двумя вещами.
Бэкбон EfficientNet. Построен через compound scaling: глубину, ширину и разрешение сети масштабируют не на глаз, а по формуле, которая ищет оптимум под заданный бюджет параметров. Из тех же мегабайт выжимается более осмысленный набор признаков.
BiFPN — Bidirectional Feature Pyramid Network. Это главное. Карты признаков разных масштабов теперь обмениваются информацией в обе стороны: сверху вниз (контекст спускается к деталям) и снизу вверх (детали поднимаются к контексту). И не один раз — блок повторяется несколько раз подряд. Вклад каждого масштаба взвешивается обучаемыми коэффициентами: сеть сама выясняет, когда важнее силуэт, а когда текстура.
Итог для того же банана: мелкая карта видит характерный кончик, средняя — изгиб, крупная — силуэт, и всё это складывается в одно решение, а не голосует вразнобой. Поэтому тонкие ножницы — пара серых линий для старой модели! — новая берёт за характерные кольца ручек.
Код: обеим моделям — один скрипт
Обе модели — формат TFLite, крутятся в одном интерпретаторе:
from ai_edge_litert.interpreter import Interpreter
interp = Interpreter(model_path="efficientdet_lite0.tflite", num_threads=4)
interp.allocate_tensors()
inp = interp.get_input_details()[0]
outs = interp.get_output_details()
IN_H, IN_W = inp["shape"][1], inp["shape"][2] # 320 или 300 — подстроится сам
num_threads=4 — у Pi 4 четыре ядра, задействуем все. allocate_tensors() один раз при старте выделяет память под все массивы.
Прогон кадра:
img = cv2.resize(frame_rgb, (IN_W, IN_H)) interp.set_tensor(inp["index"], np.expand_dims(img, 0).astype(np.uint8)) interp.invoke()
Грабля №1: порядок выходов
Детектор возвращает три массива: рамки, классы, уверенности. Сюрприз: разные модели отдают их в разном порядке. Перепутаешь — код нарисует проценты вместо координат.
Я определяю, кто есть кто, автоматически. Рамки узнаются по форме — это единственный массив, где на строку приходится 4 числа (два угла прямоугольника). А классы от уверенностей отличаются по дробной части: классы всегда целые (16.0 — это «cat»), уверенности дробные (0.73):
def _map_outputs():
boxes_i, flat = None, []
for i, o in enumerate(outs):
shp = list(o["shape"])
if len(shp) == 3 and shp[2] == 4:
boxes_i = i # рамки: (1, N, 4)
elif len(shp) == 2 and shp[1] > 1:
flat.append(i) # классы или уверенности
a, b = flat
ta = interp.get_tensor(outs[a]["index"])[0]
tb = interp.get_tensor(outs[b]["index"])[0]
frac_a = float(np.abs(ta - np.round(ta)).sum())
frac_b = float(np.abs(tb - np.round(tb)).sum())
classes_i, scores_i = (a, b) if frac_a <= frac_b else (b, a)
return boxes_i, classes_i, scores_i
Благодаря этому переключение между моделями — одна переменная окружения, без правки кода.
Грабля №2: «???» вместо человека
Самый поучительный фейл. Старая модель меня находила — рамка была. Но подписывала «???».
Модель отвечает номером класса, а имя даёт обычный текстовый файл — labelmap. В старых комплектах часть строк — заглушки «???» (в COCO есть «дырки», удалённые классы). Если нумерация модели и файла разъезжается хоть на единицу, «person» указывает ровно в дырку:
with open("coco_labels.txt") as f:
labels = [l.strip() for l in f.readlines()]
cid = int(classes[i])
label = labels[cid] if 0 <= cid < len(labels) else str(cid)
Модель видела человека — она не могла вспомнить слово. Мораль, ради которой стоило писать статью: половина «глупостей ИИ» — не модель, а обвязка. Сначала проверь labelmap, потом ругай нейросеть.
Почему не взять модель побольше
YOLO последней версии порвала бы обе. Но на Pi 4 без ускорителя большие модели дают секунды на кадр — робот, который думает над кадром три секунды, это не зрение, а слайд-шоу. Лёгкие int8-модели — честный компромисс: 3–5 fps хватает и для стрима, и для реакции на кота.
Какие CV-модели вообще идут на Raspberry Pi
Всё ниже — TFLite, квантованное, работает на CPU без Coral-стиков. Скорости — ориентир для Pi 4; на Pi 5 примерно вдвое быстрее.
Детекция объектов:
— SSD MobileNet V1 · 4 МБ · ~21 mAP · ~8 fps — та самая «банан-фен». Только если fps важнее всего:
github.com/google-coral/test_data/raw/master/ssd_mobilenet_v1_coco_quant_postprocess.tflite
— SSD MobileNet V2 · 6 МБ · ~22 mAP · ~6 fps — чуть точнее, тот же формат:
github.com/google-coral/test_data/raw/master/ssd_mobilenet_v2_coco_quant_postprocess.tflite
— SSDLite MobileDet · 4 МБ · ~28 mAP · ~5–6 fps — тёмная лошадка: точность уровня EfficientDet при скорости почти как у SSD. Отличный выбор для робота:
github.com/google-coral/test_data/raw/master/ssdlite_mobiledet_coco_qat_postprocess.tflite
— EfficientDet-Lite0 · 4,5 МБ · ~26 mAP · ~3–5 fps — герой этой статьи:
github.com/google-coral/test_data/raw/master/efficientdet_lite0_320_ptq.tflite
— EfficientDet-Lite1 / Lite2 · 6–7 МБ · ~31/34 mAP · ~2–3 / ~1,5 fps — когда точность важнее скорости:
github.com/google-coral/test_data/raw/master/efficientdet_lite1_384_ptq.tflite
github.com/google-coral/test_data/raw/master/efficientdet_lite2_448_ptq.tflite
— YOLOv8n / YOLO11n · ~6 МБ · ~37 mAP · ~2–4 fps — самая точная из лёгких, но нужен экспорт (yolo export format=tflite int8=True) и у неё свой формат выходов:
docs.ultralytics.com/guides/raspberry-pi
— NanoDet-Plus · 2–4 МБ · ~27–30 mAP — анкер-фри малютка на движке NCNN, для тех, кто выжимает fps:
github.com/RangiLyu/nanodet
Словарь классов (общий для COCO-моделей):
raw.githubusercontent.com/google-coral/test_data/master/coco_labels.txt
Не только детекция:
— MoveNet Lightning — скелет человека, 17 точек, 10+ fps (искать «movenet singlepose lightning tflite» на Kaggle Models)
— MediaPipe — готовые лица, руки, жесты: pip install mediapipe · ai.google.dev/edge/mediapipe
— MobileNetV3 / EfficientNet-Lite — классификация: называет картинку целиком, в разы быстрее детекции
— DeepLabV3-MobileNetV2 — сегментация, вырезает силуэты попиксельно, ~1–2 fps
Как выбирать. Нужна скорость (робот едет) — MobileNet V2 или MobileDet. Баланс — EfficientDet-Lite0. Максимум точности на фото — Lite2 или YOLO. Люди и жесты — MediaPipe, не изобретайте велосипед.
Повторить у себя
Понадобится любой компьютер с камерой — Raspberry Pi, ноутбук, что угодно: мой скрипт сам определяет, есть ли Pi Camera, а если нет — берёт обычную вебку через OpenCV.
Зависимости: pip install opencv-python flask numpy ai-edge-litert (на Raspberry ещё picamera2). Скачайте модель и словарь по ссылкам выше, запустите python3 detect_web.py — и откройте http://<ip>:5001 в браузере: там будет живое видео с рамками.