Последние пару недель были непростыми для нашей команды. Выпускали OpenCV 4, а вместе с ним готовились к Intel's OpenVINO toolkit R4, в состав которого входит OpenCV. Думаешь, отвлекусь на время, посмотрю, как обычно, форумы про OpenCV, да комментарии пользователей, и тут на тебе, модно стало говорить что OpenCV не IoT, что под Raspberry Pi собрать — припоя не хватает, что на ночь make -j2
ставить — утром будет готово, если повезёт.
Поэтому предлагаю дружно взяться за руки и посмотреть, как же можно собирать библиотеку OpenCV для 32-битной операционной системы, исполняемой на ARM процессоре, используя ресурсы машины с 64-битной OS, движимой отличной архитектурой CPU. Колдовство Кросс-компиляция, не иначе!
Постановка задачи
Компиляция непосредственно на плате, принято названная нативной, действительно трудоёмка, поэтому мы здесь рассмотрим такой способ сборки проекта, который позволяет более сильным вычислительным устройствам (назовём их хостами) подготавливать бинарники для своих малых сородичей. Более того, обе машины могут иметь разные архитектуры CPU. Это и есть кросс-компиляция.
Итак, для приготовления малинового пирога с начинкой из OpenCV нам потребуются:
- Тушка docker образа Ubuntu 16.04
- Хостовая машина мощнее Raspberry Pi (иначе какой смысл, isn't it?)
- Кросс-компилятор под ARMhf, а так же библиотеки соответствующей архитектуры
Весь процесс сборки OpenCV будет происходить именно на хостовой машине. У себя я использую Ubuntu. С другой версией Linux проблем с воcпроизведением возникнуть не должно. Для пользователей Windows — мои искренние пожелания не сдаваться и попробовать разобраться самим.
Установка Docker
Своё знакомство с docker начал около недели назад, поэтому гурманам — соль и синтаксический сахар добавлять по вкусу. Нам же с тобой хватит трёх ингредиентов — Dockerfile, понятие образа и контейнера.
Сам по себе docker — инструмент по созданию и воспроизведению конфигурации любой операционной системы с необходимым набором компонент. Dockerfile — это набор shell команд, которые вы обычно используете на хостовой машине, но в данном случае — все они применяются к так называемому docker
образу.
Для того, чтобы поставить docker, рассмотрим самый простой способ: закажем пакет через сервис доставки apt-get
:
sudo apt-get install -y docker.io
Дадим docker демону всё, что он попросит и сделаем logout из системы (замет login соответственно).
sudo usermod -a -G docker $USER
Подготавливаем рабочее пространство
Raspberry Pi (в моём случае RPI 2 Model B) в самом распространнёном приготовлении — это ARMv7 CPU с операционной системой Raspbian (Debian based). Мы же создадим docker
образ на основе Ubuntu 16.04, в которую доложим кросс-компилятор, армовые библиотеки и там же соберём OpenCV.
Создаём папочку, где будет лежать наш Dockerfile
:
mkdir ubuntu16_armhf_opencv && cd ubuntu16_armhf_opencv
touch Dockerfile
Добавим информацию о базовой OS и armhf
архитектуру для установщика пакетов apt-get
:
FROM ubuntu:16.04
USER root
RUN dpkg --add-architecture armhf
RUN apt-get update
Обратите внимание, команды типа FROM ...
, RUN ...
— это синтаксис docker
и пишутся в созданном тестовом файле Dockerfile
.
Вернёмся в родительскую директорию ubuntu16_armhf_opencv
и попробуем создать наш docker образ:
docker image build ubuntu16_armhf_opencv
Во время выполнения команды apt-get update
вам должно повести увидеть ошибки следующего рода: Err:[число] [url] xenial[чего-нибудь] armhf Packages
Ign:30 http://archive.ubuntu.com/ubuntu xenial-backports/main armhf Packages
Ign:32 http://archive.ubuntu.com/ubuntu xenial-backports/universe armhf Packages
Err:7 http://archive.ubuntu.com/ubuntu xenial/main armhf Packages
404 Not Found
Ign:9 http://archive.ubuntu.com/ubuntu xenial/restricted armhf Packages
Ign:18 http://archive.ubuntu.com/ubuntu xenial/universe armhf Packages
Ign:20 http://archive.ubuntu.com/ubuntu xenial/multiverse armhf Packages
Err:22 http://archive.ubuntu.com/ubuntu xenial-updates/main armhf Packages
404 Not Found
Ign:24 http://archive.ubuntu.com/ubuntu xenial-updates/restricted armhf Packages
Ign:26 http://archive.ubuntu.com/ubuntu xenial-updates/universe armhf Packages
Ign:28 http://archive.ubuntu.com/ubuntu xenial-updates/multiverse armhf Packages
Err:30 http://archive.ubuntu.com/ubuntu xenial-backports/main armhf Packages
404 Not Found
Ign:32 http://archive.ubuntu.com/ubuntu xenial-backports/universe armhf Packages
Если подсмотреть в файлик /etc/apt/sources.list
то каждой такой ошибке соответствует какая-то строчка, например:
Ошибка
Err:22 http://archive.ubuntu.com/ubuntu xenial-updates/main armhf Packages
404 Not Found
Строчка в /etc/apt/sources.list:
deb http://archive.ubuntu.com/ubuntu/ xenial-updates main restricted
Решение:
Разбить на две:
deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ xenial-updates main restricted
deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports/ xenial-updates main restricted
Таким образом придётся заменить несколько источников пакетов. В нашем докере мы заменим их все одной командой:
RUN sed -i -E 's|^deb ([^ ]+) (.*)$|deb [arch=amd64] \1 \2\ndeb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports/ \2|' /etc/apt/sources.list
Теперь apt-get update
должен отработать без ошибок.
Ставим необходимые пакеты
Нам необходимо поставить хостовые пакеты такие как git
, python-pip
, cmake
и pkg-config
, а так же crossbuild-essential-armhf
, что есть набор из gcc/g++ кросс-компиляторов (arm-linux-gnueabihf-gcc
и arm-linux-gnueabihf-g++
) и системных библиотек соответствующей архитектуры:
RUN apt-get install -y git python-pip cmake pkg-config crossbuild-essential-armhf
Из необычного — мы так же скачаем GTK (используется для рисования окошек в модуле highgui), GStreamer и Python, но уже с явным указанием инородной архитектуры:
RUN apt-get install -y --no-install-recommends libgtk2.0-dev:armhf libpython-dev:armhf libgstreamer1.0-dev:armhf libgstreamer-plugins-base1.0-dev:armhf libgstreamer-plugins-good1.0-dev:armhf libgstreamer-plugins-bad1.0-dev:armhf
A дальше — клонируем и собираем, указывая нужные флаги:
RUN git clone https://github.com/opencv/opencv --depth 1
RUN mkdir opencv/build && cd opencv/build && export PKG_CONFIG_PATH=/usr/lib/arm-linux-gnueabihf/pkgconfig && cmake -DCMAKE_BUILD_TYPE=Release -DOPENCV_CONFIG_INSTALL_PATH="cmake" -DCMAKE_TOOLCHAIN_FILE="../opencv/platforms/linux/arm-gnueabi.toolchain.cmake" -DWITH_IPP=OFF -DBUILD_TESTS=OFF -DBUILD_PERF_TESTS=OFF -DOPENCV_ENABLE_PKG_CONFIG=ON -DPYTHON2_INCLUDE_PATH="/usr/include/python2.7" -DPYTHON2_NUMPY_INCLUDE_DIRS="/usr/local/lib/python2.7/dist-packages/numpy/core/include" -DENABLE_NEON=ON -DCPU_BASELINE="NEON" ..
где
CMAKE_TOOLCHAIN_FILE
— путь к cmake файлу, который определяет процесс кросс-компиляции (выставляет нужный компилятор, ограничивает использование хостовых библиотек.
WITH_IPP=OFF
, — отключаем тяжеловесные зависимости.
BUILD_TESTS=OFF
,BUILD_PERF_TESTS=OFF
, отключаем сборку тестов.
OPENCV_ENABLE_PKG_CONFIG=ON
— чтобы pkg-config смог найти такие зависимости как GTK.PKG_CONFIG_PATH
— правильный путь, гдеpkg-config
будет искать библиотеки.
PYTHON2_INCLUDE_PATH
,PYTHON2_NUMPY_INCLUDE_DIRS
— пути, необходимые для кросс-компиляции обёрток для python2.
ENABLE_NEON=ON
,CPU_BASELINE="NEON"
— разрешаем NEON оптимизации.
OPENCV_CONFIG_INSTALL_PATH
— регулирует расположение файлов вinstall
директории.
Главное, на что стоит обратить внимание после исполнения cmake
, что все необходимые модули собираются (python2, например):
-- OpenCV modules:
-- To be built: calib3d core dnn features2d flann gapi highgui imgcodecs imgproc java_bindings_generator ml objdetect photo python2 python_bindings_generator stitching ts video videoio
-- Disabled: world
-- Disabled by dependency: -
-- Unavailable: java js python3
-- Applications: tests perf_tests apps
-- Documentation: NO
-- Non-free algorithms: NO
а необходимые зависимости, такие как GTK, нашлись:
-- GUI:
-- GTK+: YES (ver 2.24.30)
-- GThread : YES (ver 2.48.2)
-- GtkGlExt: NO
--
-- Video I/O:
-- GStreamer:
-- base: YES (ver 1.8.3)
-- video: YES (ver 1.8.3)
-- app: YES (ver 1.8.3)
-- riff: YES (ver 1.8.3)
-- pbutils: YES (ver 1.8.3)
-- v4l/v4l2: linux/videodev2.h
Остаётся только вызвать make
, make install
и дождаться окончания сборки:
Successfully built 4dae6b1a7d32
Изпользуйте этот id
образа для того, чтобы поставить тег и создать контейнер:
docker tag 4dae6b1a7d32 ubuntu16_armhf_opencv:latest
docker run ubuntu16_armhf_opencv
А нам осталось выкачать собранную OpenCV из контейнера. Сперва подсмотрим идентификатор созданного контейнера:
$ docker container ls --all
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
e94667fe60d2 ubuntu16_armhf_opencv "/bin/bash" 6 seconds ago Exited (0) 5 seconds ago clever_yalow
И скопируем install директорию с установленной OpenCV:
docker cp e94667fe60d2:/opencv/build/install/ ./
mv install ocv_install
Накрываем на стол
Копируем ocv_install
на Raspberry Pi, устанавливаем пути и пробуем запустить OpenCV из питона.
export LD_LIBRARY_PATH=/path/to/ocv_install/lib/:$LD_LIBRARY_PATH
export PYTHONPATH=/path/to/ocv_install/python/:$PYTHONPATH
Запустим пример по детектированию, используя нейронную сеть MobileNet-SSD из https://github.com/chuanqi305/MobileNet-SSD:
import cv2 as cv
print cv.__file__
classes = ['backgroud', 'aeroplane', 'bicycle', 'bird', 'boat', 'bottle', 'bus', 'car', 'cat',
'chair', 'cow', 'diningtable', 'dog', 'horse', 'motorbike', 'person',
'pottedplant', 'sheep', 'sofa', 'train', 'tvmonitor']
cap = cv.VideoCapture(0)
net = cv.dnn.readNet('MobileNetSSD_deploy.caffemodel', 'MobileNetSSD_deploy.prototxt')
cv.namedWindow('Object detection', cv.WINDOW_NORMAL)
while cv.waitKey(1) != 27:
hasFrame, frame = cap.read()
if not hasFrame:
break
frame_height, frame_width = frame.shape[0], frame.shape[1]
blob = cv.dnn.blobFromImage(frame, scalefactor=0.007843, size=(300, 300),
mean=(127.5, 127.5, 127.5))
net.setInput(blob)
out = net.forward()
for detection in out.reshape(-1, 7):
classId = int(detection[1])
confidence = float(detection[2])
xmin = int(detection[3] * frame_width)
ymin = int(detection[4] * frame_height)
xmax = int(detection[5] * frame_width)
ymax = int(detection[6] * frame_height)
if confidence > 0.5:
cv.rectangle(frame, (xmin, ymin), (xmax, ymax), color=(255, 0, 255), thickness=3)
label = '%s: %.2f' % (classes[classId], confidence)
labelSize, baseLine = cv.getTextSize(label, cv.FONT_HERSHEY_SIMPLEX, 0.5, 1)
ymin = max(ymin, labelSize[1])
cv.rectangle(frame, (xmin, ymin - labelSize[1]), (xmin + labelSize[0], ymin + baseLine), (255, 0, 255), cv.FILLED)
cv.putText(frame, label, (xmin, ymin), cv.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0))
cv.imshow('Object detection', frame)
Вот и всё, полная сборка занимает не более 20 минут. Прикладываю финальный вариант Dockerfile
ниже и пользуясь случаем предлагаю пройти небольшой опрос от команды OpenCV для тех кто когда-то имел опыт работы с библиотекой: https://opencv.org/survey-2018.html.
И да, поздравляю с OpenCV 4! Это не просто работа отдельной команды, это работа всего комьюнити — OpenCV 4 you.
FROM ubuntu:16.04
USER root
RUN dpkg --add-architecture armhf
RUN sed -i -E 's|^deb ([^ ]+) (.*)$|deb [arch=amd64] \1 \2\ndeb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports/ \2|' /etc/apt/sources.list
RUN apt-get update && apt-get install -y --no-install-recommends cmake pkg-config crossbuild-essential-armhf git python-pip libgtk2.0-dev:armhf libpython-dev:armhf libgstreamer1.0-dev:armhf libgstreamer-plugins-base1.0-dev:armhf libgstreamer-plugins-good1.0-dev:armhf libgstreamer-plugins-bad1.0-dev:armhf
RUN pip install numpy==1.12.1
RUN git clone https://github.com/opencv/opencv --depth 1
RUN mkdir opencv/build && cd opencv/build && export PKG_CONFIG_PATH=/usr/lib/arm-linux-gnueabihf/pkgconfig && cmake -DCMAKE_BUILD_TYPE=Release -DOPENCV_CONFIG_INSTALL_PATH="cmake" -DCMAKE_TOOLCHAIN_FILE="../opencv/platforms/linux/arm-gnueabi.toolchain.cmake" -DWITH_IPP=OFF -DBUILD_TESTS=OFF -DBUILD_PERF_TESTS=OFF -DOPENCV_ENABLE_PKG_CONFIG=ON -DPYTHON2_INCLUDE_PATH="/usr/include/python2.7" -DPYTHON2_NUMPY_INCLUDE_DIRS="/usr/local/lib/python2.7/dist-packages/numpy/core/include" -DENABLE_NEON=ON -DCPU_BASELINE="NEON" .. && make -j4 && make install
Комментарии (13)
molnij
24.11.2018 12:46Извините, а последняя картинка с двумя промахнувшимися областями — это такой троллинг в сторону OCV? :)
dkurt Автор
24.11.2018 12:58А что тут от OCV, встречный вопрос?) deep learning модуль выдаёт предсказания, которые совпадают с оригинальным фреймворком (авторский Caffe в данном случае). Всё зависит от качества тренировки исходной нейросети.
Я то вообще удивился, что нашлось хоть что-то, с учётом качества картинки. Да ещё и матрёшку признали антропоморфной (хотя по правде сказать, сеть была крайне неуверена и чаще предсказывала, что это «bottle»).
iig
24.11.2018 13:33Если для кросскомпиляции нужно специфическое окружение — не проще ли chroot вместо docker'а?
dkurt Автор
24.11.2018 18:19Да, можно и действительно проще. Нужно ещё будет использовать qemu, чтобы докачивать пакеты в rootfs.
Проблема в переносимости — такой подход не заинтегрировать гладко в CI, если нужно, посколько трюк с qemu сработает только тогда, когда на самой хостовой машине, выделенной под CI поставлен qemu-user-static. Поправьте меня, если не прав.iig
24.11.2018 21:38Непонятно, при чем тут qemu.
Я процесс кросс-компиляции представляю так:
Берём кросс-компилятор, который умеет целевую платформу. Делаем sysroot, точно как на целевой платформе. После make install получим набор файлов, который нужно перенести в вашу машинку. Можно завернуть в .deb, можно завернуть в образ sd карты через genext2fs… CI и qemu это уже совершенно другие задачи.dkurt Автор
24.11.2018 22:39Извиняюсь, наверное стоило сразу попробовать показать примером то, что имелось ввиду. Да, можем взять нашу плату, выкачать оттуда заголовочные файлы с нативными библиотеками на хост и указывать кросс-компилятору в качестве зависимостей. Но это не совсем прозрачный подход. Никто не будет запоминать, в каком порядке и откуда на конечном устройстве появились нужные пакеты зависимостей. Для себя — нормально, но как только появляется новый пользователей этого процесса — для него это магия типа "не трогай эти папочки — сломаешь сборку".
То есть ещё раз — Ваше решение правильное, но хочется иметь воспроизводимый набор инструкций. Например, взять публичный чистый Raspbian образ, докачать в него зависимостей и использовать в качестве rootfs донора. Тогда если по шагам:
Качаем Raspbian Lite
wget http://downloads.raspberrypi.org/raspbian_lite/archive/2018-11-15-21:03/root.tar.xz
Распаковываем и внедряемся:
mkdir sysroot sudo tar -xf root.tar.xz -C sysroot sudo chroot sysroot
Только на chroot получим ошибку
chroot: failed to run command ‘/bin/bash’: Exec format error
Потому что OS то для ARM, да ещё и 32 битная, а мы (я в данном случае) работаем на 64-битном Intel® Core™ i5.
Тогда то и можно попробовать qemu:
sudo apt-get install -y qemu-user-static sudo cp /usr/bin/qemu-arm-static sysroot/usr/bin/ sudo chroot sysroot
Готово, я внутри и могу доустанавливать пакеты.
iig
24.11.2018 23:32Как-то сложно. Я это вижу так. От каких пакетов зависит opencv известно? По-моему, да ;). Raspbian это же вариация debian? Значит, можно распаковать -dev пакеты, вместе с их зависимостями, именно тех версий, что используются в релизе вашего raspbian.
Все это вполне автоматизируется. Магии apt должно хватить.
WondeRu
24.11.2018 14:06Может на основе этого сделаете готовый Jenkins-agent и выложите на hub.docker.com? Это поможет не строить велосипеды и за минуты готовить среду для сборки opencv проекты
dkurt Автор
24.11.2018 17:59Вопрос насколько долго это будет актуально и гибко. Выложить может любой пользователь, но поддерживать будет не каждый. Чего далеко ходить, python-opencv: Maintainer Kubuntu Developers, не OpenCV team.
rPman
25.11.2018 00:01Полученные библиотеки работают с gpu?
У малинки вроде бы не плохая видеокарта 24gflops (3-яя модель уже 93gflops) в теории раз в 30 быстрее cpu.
гугл даже выдает открытые реализации opencl, т.е. в теории это решаемо.
Stantin
25.11.2018 13:19Здорово, собирал сам 3.4.1 для Raspbian Jessie как раз из-за dnn. Недавно задумался переходить на Стретч, и тут ваша статья. А собранный deb можно где-нибудь стянуть?
prs123
Здорово все, конечно. Но когда малинка стала микроконтроллером? По-моему, это все таки процесор с перефирией
dkurt Автор
Спасибо за замечание, поправил! Сам привык называть их бордами.