Компьютерное зрение на Raspberry Pi

 Компьютерное зрение на 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 в браузере: там будет живое видео с рамками.



Report Page