Stereopi+WebRTC=telepresense по-домашнему

  • Tutorial
Для начала ролик с youtube для вдохновения:


Предупреждение: проект на видео — лишь образец, который можно сделать по туториалу в статье в части стереозрения и «поворотов головой». Танки с пультами xbox не прилагаются.

Не смотря на наглядность, скудный рассказ самого автора проекта на видео и наличие ссылок, сходу разобраться как все это работает непросто. Если есть желание собрать что-то подобное и в гораздо более удобном формате, рекомендуется к прочтению.

*Сразу оговорюсь, человек на ролике с ютюб мне незнаком, никаких секретных данных не передавал, в каком состоянии находится его проект сейчас, мне не известно.
**Делать управление движением робота на raspberry по поверхности через пульт от xbox мы не будем, с этим можно справиться самостоятельно.
***Просьба лапти не кидать, так как проект пока находится в разработке.

Итак, нас интересует две вещи:

  • как получить стерео картинку на телефон в шлеме;
  • как управлять сервами поворотами головы.

Концепция, которая используется в ролике, если обобщить выгдядит так:

  • 2 raspberry pi посылают видеопотоки в сеть со своих камер через сервисы webrtc;
  • телефон (в шлеме) принимает потоки в 2 одинаковых приложения на телефоне — float apps.
  • одновременно телефон управляет сервами, подключенными к raspberry.

Все просто. Но diablo, как известно, в деталях и неудобствах, а именно:

  • надо запускать 2 raspberry, следить за настройками 2-х камер, питание raspberry * 2.
  • float apps постоянно сползают в телефоне, приходится выравнивать картинки на экране.
  • ...

Поэтому пересядем на stereopi, благо она появилась в российских магазинах (надеюсь, после этого поста не исчезнет):



Stereopi — это разработка нашего соотечественника, которая сейчас активно завоевывает рынок.

Прелесть ее вытекает из названия — можно подключить 2-е CSI камеры raspberry pi одновременно. При этом все это работает на базе одного raspberry pi Compute Module. К сожалению, сам модуль не входит в комплект, его надо покупать самостоятельно.

О stereopi есть статьи на Хабре.

От нее нам понадобятся 2-а видео потока и управление сервами через GPIO.
В качестве базы для stereopi будем использовать Raspberry Pi Compute Module 3+.

Подготовка stereopi


После сборки stereopi (вставка compute module в stereopi, камер), зальем софт.

Используем уже готовый образ для raspberry pi compute module — Raspbian (stretch). Он есть на сайте stereopi.com — Raspbian Stretch OpenCV image, Google Drive
Зальем его в raspberry.

Если есть сложности с заливкой, иные идем на wiki stereopi.

Установка webrtc.


Проинсталлируем ПО webrtc на stereopi. Частично материал по установке взят с этой страницы: Installation for ARM (Raspberry Pi)


Избежим излишних комментариев, которые уже есть на вышеуказанной странице и попросту установим все, что потребуется.

curl http://www.linux-projects.org/listing/uv4l_repo/lpkey.asc | sudo apt-key add -
sudo nano /etc/apt/sources.list
   deb http://www.linux-projects.org/listing/uv4l_repo/raspbian/stretch stretch main
sudo apt-get update
sudo apt-get install uv4l uv4l-raspicam
sudo apt-get install uv4l-raspicam-extras
sudo raspi-config  далее Anvanced Options далее Memory Split  далее  написать 256 и нажать enter
sudo apt-get install uv4l-server uv4l-uvc uv4l-xscreen uv4l-mjpegstream uv4l-dummy uv4l-raspidisp
sudo apt-get install uv4l-webrtc
sudo apt-get install uv4l-demos
sudo apt-get install uv4l-xmpp-bridge
sudo apt-get install uv4l-raspidisp-extras

Теперь надо (в инструкции это есть) сформровать ssl-ключи, так как Chrome может не показывать видео через соединение http (только через https):

openssl genrsa -out selfsign.key 2048 && openssl req -new -x509 -key selfsign.key -out selfsign.crt -sha256

*при формировании ключей будут задаваться вопросы о компании, регионе и т.п. — можно отвечать на них произвольно.

Сформированные ключи (selfsign.key и selfsign.crt появятся в текущей папке) надо положить в папку:

/etc/ssl/private/

Все настройки webrtc хранятся в 2-х файлах:

/etc/uv4l/uv4l-raspicam.conf
/etc/uv4l/uv4l-raspidisp.conf

Чтобы не утомлять перечислением позиций в файлах, которые необходимо раскоментировать или
подправить, перезапишем файлы настроек своими uv4l-raspicam.conf и uv4l-raspidisp.conf.

Перезагрузим raspberry и зайдем с телефона по ip raspberry, используя chrome:

https://192.168.1.100:8080

WebRTC представляет из себя целый веер возможностей, но ограничимся одной — перейдем по вкладке webrtc:

картинка


Теперь проверим, работает ли видео со stereopi.

Нажмем внизу web страницы на телефоне кнопку «Call».

картинка


Должно появиться видео со стереокамер.

Нажмем на кнопку «Fullscreen» под окном с изображением с web-камер:

картинка


*Страницу на телефоне не перезагружать! Если все же это случилось, надо убить процессы на raspberry:

sudo killall uv4l

И перезагрузить сервисы на ней же:

sudo service uv4l_raspidisp restart
sudo service uv4l_raspicam restart

Далее заново на странице в браузере телефона нажать «Call».
**Call не будет работать, если к raspberry не подключена камера.

Разберемся с сервами.


Чтобы управлять сервами на raspberry с телефона понадобится код, который будет запускаться на raspberry, и действия на телефоне.

Но сначала определимся с сервами. В ютюб ролике используются сервы, подключенные к gpio raspberry напрямую. Так как сервы маломощные, пожалуй можно повесить 2 сервы на gpio raspberry. Эти трюки можно с легкостью проводить над сервами sg-90. Они не требовательны по питанию, но и не особо хороши для нагрузок. В принципе их должно быть достаточно, чтобы держать подвес с двумя камерами от stereopi. Сам подвес можно купить на том же aliexpress, по поиску «pan-tilt». Однако у этих серв есть так же серьезный минус — они «дрожат от страха». Именно этот эффект и наблюдает автор ролика с ютюб. Почему это происходит и что с этим делать не будем рассматривать здесь.

В нашем случае используются сервы mg-996n и сустав робота, который, надеюсь, ему в ближайшее время не понадобится.

Картинка


*Mg-996N не «дрожат».

Stereopi имеет расположение gpio сходное со стандартным на raspberry 3.

Поэтому сигнальные провода от серв пойдут на gpio, а 5V лучше взять не с raspberry, а со стороны, GND серв объединить с GND raspberry и GND внешнего источника.

Теперь самое главное, софт


На raspberry нам понадобится демон, но не лермонтовский, а pigpio. Никаких особых действий по его настройке предпринимать не нужно, главное знать, что он висит на порту 8888 и его предварительно надо запустить:

sudo systemctl start pigpiod.service

Далее создадим файл, который и будет управлять сервами, получая данные с сокета, который сам же и создает:

datachannel_server_tele.py
# python 3
# Taken from:
# https://stackoverflow.com/questions/45364877/interpreting-keypresses-sent-to-raspberry-pi-through-uv4l-webrtc-datachannel
# based on:
# https://raspberrypi.stackexchange.com/questions/29480/how-to-use-pigpio-to-control-a-servo-motor-with-a-keyboard
# public domain

# systemctl status pigpiod.service 
# sudo systemctl start pigpiod.service 
# goto http://raspberrypi:8080/stream/webrtc and press Call!
# video from raspberry pi appear
# run from cmd raspberry: sudo python3 datachannel_server.py
# turn on gps on phone
# put V on 'send device orientation' from phone

import socket
import time
import pigpio
import os
import re
import json

socket_path = '/tmp/uv4l.socket'

try:
    os.unlink(socket_path)
except OSError:
    if os.path.exists(socket_path):
        raise

s = socket.socket(socket.AF_UNIX, socket.SOCK_SEQPACKET)

ROLL_PIN     = 4 #gpio 
PITCH_PIN    = 17 #gpio ! not phisical pin
YAW_PIN      = 15 

MIN_PW = 1000 # 0 degree
MID_PW = 1500 # 90 degree
MAX_PW = 2000 # 180 degree

print ('socket_path: %s' % socket_path)
s.bind(socket_path)
s.listen(1)

def cleanup():
    pi.set_servo_pulsewidth(ROLL_PIN, 0)
    pi.set_servo_pulsewidth(PITCH_PIN, 0)
    pi.set_servo_pulsewidth(YAW_PIN, 0)
    pi.stop()

while True:
    print ('awaiting connection...')
    connection, client_address = s.accept()
    print ('client_address %s' % client_address)
    try:
        print ('established connection with', client_address)

        pi = pigpio.pi()
        #pi = pigpio.pi('soft', 9080)

        rollPulsewidth     = MID_PW  
        pitchPulsewidth    = MID_PW 
        yawPulsewidth      = MID_PW 

        pi.set_servo_pulsewidth(ROLL_PIN, rollPulsewidth)
        pi.set_servo_pulsewidth(PITCH_PIN, pitchPulsewidth)
        pi.set_servo_pulsewidth(YAW_PIN, yawPulsewidth)

        while True:
            try:
                data = json.loads(connection.recv(200).decode('utf-8')) # dict
            except ValueError: # no data return
                continue
            # data
            #{"do":{"alpha":0.1,"beta":-0.3,"gamma":-0.2,"absolute":false},
            # "dm":{"x":0,"y":0,"z":-0.2,"gx":0,"gy":0,"gz":-9.6,"alpha":-0.1,"beta":-0.1,"gamma":0.1}
            
            #print ('received message"%s"' % data)
            #print ('received message"%s"' % data['dm']['x']) # coordinate x from data
            #print ('received message"%s"' % data['dm']['y']) # coordinate y from data           
                      
            time.sleep(0.01)
            
            key1 = float(data['do']['alpha']) # os x 0 to 360 degree
            #key2 = float(data['do']['beta']) # os y
            #print(key1)            
            #print(key2)

            rollPW     = rollPulsewidth
            pitchPW    = pitchPulsewidth
            yawPW      = yawPulsewidth
           
            pitchPW = key1*5+500          
            print ('x: '+str(pitchPW))
            #if pitchPW > MAX_PW:
            #    pitchPW = MAX_PW
            #elif pitchPW < MIN_PW:
            #    pitchPW = MIN_PW

            #rollPW = int(key2 + 1000)
            #print ('y: '+ str(int(rollPW)))
            #if rollPW > MAX_PW:
            #    rollPW = MAX_PW
            #elif rollPW < MIN_PW:
            #    rollPW = MIN_PW

            if rollPW != rollPulsewidth:
                rollPulsewidth = rollPW
                pi.set_servo_pulsewidth(ROLL_PIN, rollPulsewidth)
            if pitchPW != pitchPulsewidth:
                pitchPulsewidth = pitchPW
                pi.set_servo_pulsewidth(PITCH_PIN, pitchPulsewidth)
            if yawPW != yawPulsewidth:
                yawPulsewidth = yawPW
                pi.set_servo_pulsewidth(YAW_PIN, yawPulsewidth)

            #if data:
                #print ('echo data to client')
                #connection.sendall(str(data))
            #else:
                #print ('no more data from', client_address)
                #break

    finally:
        # Clean up the connection
        cleanup()
        connection.close()


В тексте оставлены комменты, чтобы понять, откуда код родился и, что еще можно подправить.
Общий смысл кода следующий:

  • при старте сервы выставляются в среднее положение.
  • есть 3 пина (gpio), на которых висят сигнальные провода серв. В нашем случае 2 пина (подвес из 2 серв).
  • gpio управлются путем подачи сигнала в диапазоне PWM от 1000 до 2000.
  • с телефона прилетает строка, которая парсится json (можно чем-то еще), далее из нее берутся значения x и y. Далее эти значения переводятся в значения PWM для поворота сервы.

*Проблема заключается втом, что x принимает значения от 0 до 360 (поворот телефона вокруг своей оси), как и y. И эти значения надо привязать к PWM, которые принимают значения от 1000 до 2000. В коде используется формула pitchPW = key1*5+500. 500 — это минимальное значение PWM servo (хотя в коде допущение 1000). И умножение на 5 условно. Этот момент требует доработки, так как при x=360, значение PWM выше максимального в разы. Сервы защищены от выхода за максимальные углы поворота во избежание их повреждения, но это не сильно радует.

Запустим код в терминале raspberry:

sudo python3 datachannel_server_tele.py

На телефоне включим GPS (в каждом телефоне есть соответствующий значок в настройках) и зайдем по ip raspberry.

https://192.168.1.100:8080/stream/webrtc

Нажмем «Call». После того как соединение установится, на телефоне в браузере на странице поставим галочку «send device orientation angles alpha, beta, gamma».

В терминал со скриптом поедут значения x. И, если повращать телефоном, они будут изменяться.
Также будут двигаться сервы.

*На текущий момент одна из них (вторая закоментирована).

Из приятных бонусов webrtc также дает возможность:

  • создать подобие телемоста между телефоном и raspberry (ваш собеседник будет объемным),
  • транслировать звук в обе стороны (не проверялось, но в настройках учтено),
  • стримить на web-страницу, youtube в 3d.
  • создать конферец-call из нескольких товарищей (jitsi meet).
  • через web-интерфейс на лету менять настройки камер (почему не работает rotate!&?).

Теперь о грустном.


1. Схалтурить, соединив на школьной линейке две разные камеры с рыбьими глазами не получилось. У рыб, оказывается, бывают разные глаза. Нужны однотипные камеры:



2. Развернуть картинку со стереокамер через настройки web-интерфейса webrtc не удалось. Пока картинки узковаты, как штаны француза.



3. Сервы MG996N ограничены углами поворота -180. По факту — 160. Возможно, кто-то посоветует с 360, но без continuous rotation.

4. Софт требует шлифовки.

5. Call иногда отваливается, приходится переподключаться.

Приложение:

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 8

    0
    А зачем ему интересно два приложения на телефоне? Можно же на одно выводить. Я только что на скорую руку сляпал, вроде работает
    Заголовок спойлера

      0
      Возможно, чтобы развернуть изображение либо такое решение на тот момент было доступнее.
      0
      Вау! Круто! :-)
      Про «картинки узковаты» — я не игрался с uv4l оберткой, но если она тянет штатные распивидовские стереорежимы, то там можно играть с режимом склейки стерео. Либо '-dec' — это сжатие по горизонтали в 2 раза, либо (дефолтно) — кроп каждого кадра посередине. В этом случае соотношение сторон не портится и геометрия каждого полукадра сохраняется.

      UPD> Вот в приведенном конфиге есть закомментированные строчки:
      ### dual camera options:
      #stereoscopic-mode = side_by_side
      #camera-number = 1
      #decimate = yes
      #swap-eyes = yes

      Параметр '-dec' тут выведен в строчке #decimate = yes
        0
        Пардон, старый конфиг выложен. Естественно, стерео режим должен быть раскоментирован для работы.
        decimate потестим.
        Пробовался также режим top-bottom (читай over/under) вместо side-by-side, но тогда изображения хотя и в полную длину, но друг над другом.
          0
          — decimate сильно погоды не делает ( разве что в режиме top-bottom пропадает зеленая полоса между изображениями. Надо как-то развернуть сами изображения.
          — rotate 90 в настройках (в uv4l-raspicam.conf) работает, как выяснилось, если закомментировать одновременный вывод изображения на raspberry (дублирование). Но работает так: по факту устанавливается режим top-bottom (одно изображение над другим), одна камера работает на полную длину fish eye, вторая — зеленый цвет.
          В raspi-config крутить нечего. Возможно, как-то можно повернуть изображение камер при загрузке самой raspberry, чтобы не лезть в тело webrtc…
            0
            decimate для поворота картинки не поможет, он играет роль при обычном положении камер.
            rotate 90 хорошая штука, но, по нашеиму опыту, в стереорежиме он глючит. Поэтому не используем. :-)
            В одном из наших проектов надо было в реальном времени поворачивать обе камеры на 90 градусов — в итоге просто повернули их на пластине для крепления камер, и режим стерео перевели в top-bottom.
              0
              Разворот камер на планке не помог вкупе с режимом top-bottom ( —
              картинка



              картинка


                0
                Ну при переходе с sbs на tb надо махнуть местами разрешение по X и Y :-)
                Если было -3d sbs -w 1280 -h 720, то должно стать -3d tb -w 720 -h 1280

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое