8.2.2021

foto Petr Bravenec

Petr Bravenec
Twitter: @BravenecPetr
+420 777 566 384
petr.bravenec@hobrasoft.cz

Úvod

Od minula dokážeme Pásovce ovládat ručně. Není to úplným cílem, ale je to krok nezbytný k natrénování neuronové sítě. Pro neuronovou síť totiž potřebujeme nasbírat spoustu dat. Jak jinak, než ručně.

Ze dvou kamer Pásovce jsou k dispozici dva obrázky. Pásovec díky tomu vidí stereoskopicky a měl by být schopný odhadovat vzdálenosti překážek. Neuronové síti potřebujeme přeložit vždy dvojici těchto obrázků spolu s informací, co je na obrázku vidět. A aby se dokázala neuronová síť něco rozumného naučit, potřebujeme mít takových dvojic obrázků velké množství (tisíce).

Pro jednoduché vyhnutí se překážce stačí pouze binární informace: volno nebo překážka. V praxi je lepší, pokud má Pásovec informací více:

  • Volno, kupředu!
  • Objeď překážku vlevo.
  • Objeď překážku vpravo.
  • Překážka přímo před nosem, otoč to kam chceš.

Gstreamer

Gstreamer je celý komplex utilit pro zpracování obrazu a jeho streamování po síti. Nvidia Jetson Nano zprostředkovává přístup ke kamerám právě prostřednictvím gstreameru. Velkou výhodou je i to, že Jetson Nano dokáže množství operací gstreameru akcelerovat ve své grafické kartě — výsledkem je rychlejší zpracování obrazu a nižší zátěž procesoru.

Gstreamer můžete vyzkoušet přímo na povelové řádce. Například přenos videa z kamery Pásovce na displej počítače (zde 192.168.1.10) by mohl vypadat takto:

# na Pásovci
gst-launch-1.0 nvarguscamerasrc sensor-id=0 ! video/x-raw\(memory:NVMM\), format=NV12, framerate=30/1 ! omxh264enc ! video/x-h264, stream-format=\(string\)byte-stream ! rtph264pay ! udpsink host=192.168.1.10 port=5000
# na PC s monitorem
gst-launch-1.0 udpsrc address=192.168.1.10 port=5000 ! application/x-rtp, encoding-name=H264, payload=96 ! rtph264depay ! h264parse ! avdec_h264 ! autovideosink

IP adresu na počítači s monitorem není úplně nutné uvádět, pokud pracujete pouze s IPv4. Ovšem pokud chcete funovat na IPv6, je adresu nutné uvést (i v podobě hostname, pokud máte nastaveno DNS). Gstreamer má totiž vadu — poslouchá jen na všech IPv4 adresách, všechny IPv6 adresy tiše ignoruje.

Kompresní algoritmus h264 se ale neukazuje jako příliš vhodný pro přenos obrazové informace z Pásovce do počítače. Lepší výsledky dává mjpeg — série JPEG obrázků. Za cenu větší šířky pásma se dá zajistit menší latence a jednodušší zpracování na obou stranách. Video totiž chceme nejen zobrazit na monitoru, ale potřebujeme i uložit potřebné snímky na disk počítače.

Pro přenos všech potřebných obrázků do počítače budeme potřebovat kanálů více, než jen prostý monitor:

  • Monitor
  • Uložení obrázku pro volno
  • Uložení obrázku pro překážku před Pásovcem
  • Uložení obrázku pro objetí překážky vlevo
  • Uložení obrázku pro objetí překážky vpravo

Na počítači jsem si proto udělal jednoduchý skript, který startuje gstreamer pro každý požadovaný kanál a příslušné obrázky ukládá do podadresářů OK (volno), XL (objeď vlevo), XR (objeď vpravo) a XX (překážka).

#!/bin/bash
  
killall gst-launch-1.0
sleep 1
killall -9 gst-launch-1.0
sleep 1

# v souboru number je uložená "serie" obrázků
N=$(cat number)
if [ "$N" == "" ]; then N="00"; fi

rm -fr OK/* XL/* XR/* XX/*

BIND_ADDRESS=192.168.1.10

gst-launch-1.0 udpsrc port=5000 address=${BIND_ADDRESS} ! application/x-rtp, encoding-name=JPEG ! rtpjpegdepay ! jpegparse ! avdec_mjpeg ! videoconvert ! xvimagesink &
gst-launch-1.0 udpsrc port=5001 address=${BIND_ADDRESS} ! application/x-rtp, encoding-name=JPEG ! rtpjpegdepay ! jpegparse ! avdec_mjpeg ! videoconvert ! jpegenc ! multifilesink location = ./OK/OK_$N%04d.jpg &
gst-launch-1.0 udpsrc port=5002 address=${BIND_ADDRESS} ! application/x-rtp, encoding-name=JPEG ! rtpjpegdepay ! jpegparse ! avdec_mjpeg ! videoconvert ! jpegenc ! multifilesink location = ./XL/XL_$N%04d.jpg &
gst-launch-1.0 udpsrc port=5003 address=${BIND_ADDRESS} ! application/x-rtp, encoding-name=JPEG ! rtpjpegdepay ! jpegparse ! avdec_mjpeg ! videoconvert ! jpegenc ! multifilesink location = ./XX/XX_$N%04d.jpg &
gst-launch-1.0 udpsrc port=5004 address=${BIND_ADDRESS} ! application/x-rtp, encoding-name=JPEG ! rtpjpegdepay ! jpegparse ! avdec_mjpeg ! videoconvert ! jpegenc ! multifilesink location = ./XR/XR_$N%04d.jpg &
disown

Obrázky v adresářích OK, XL, XR a XX je nutné ručně projít a opravit případné chyby — zkontrolovat, jestli jsou skutečně zařazené ve správné kategorii. Tato kontrola je extrémně důležitá. Neuronové sítě jsou dost citlivé na kvalitu trénovacích dat. Pokud se vám zatoulá několik obrázků do špatné katogorie, může to snížit úspěšnost vyhnutí se překážce třeba z 85% na 70% — a to už je při autonomním pohybu zatraceně znát!

Objeď překážku vpravo
Objeď překážku vlevo
Překážka přímo před nosem
Volno

Aby se zvýšil počet obrázků, je možné využít toho, že obrázky je možné stranově převrátit. Z levého obrázku se stane pravý, z pravého levý a obrázky ve zbývajících dvou kategoriích své zařazení nezmění. Skript:

#!/bin/bash
  
(
cd OK
for i in *.jpg; do nn=$(echo $i | sed 's/\./f./g'); djpeg $i | pamflip -lr | cjpeg -optimize -progressive > $nn;
)

(
cd XL
mkdir F
for i in *.jpg; do nn=$(echo $i | sed 's/\./f./g'); djpeg $i | pamflip -lr | cjpeg -optimize -progressive > F/$nn; done
)

(
cd XR
mkdir F
for i in *.jpg; do nn=$(echo $i | sed 's/\./f./g'); djpeg $i | pamflip -lr | cjpeg -optimize -progressive > F/$nn; done
)

(
cd XX
for i in *.jpg; do nn=$(echo $i | sed 's/\./f./g'); djpeg $i | pamflip -lr | cjpeg -optimize -progressive > $nn; done
)

mv XL/F/*.jpg XR
mv XR/F/*.jpg XL
rmdir XL/F
rmdir XR/F

Obrázky si po zpracování přesuňte na bezpečné místo. V dalším díle seriálu už budeme trénovat neuronovou síť v Tensorflow 2.

Zbývá doplnit software Pásovce. Ke skriptům z předchozí části seriálu přidejte skript kamera.py:

import cv2 as cv2
import numpy as np
import time
import paho.mqtt.client as mqtt
import paho.mqtt.publish as publish

remoteHost        = "192.168.1.10"
remotePortMonitor = 5000
remotePortOK      = 5001
remotePortXL      = 5002
remotePortXX      = 5003
remotePortXR      = 5004

class Kamera:
    def __init__(self):
        # Open camera
        self.open_cameraL()
        self.open_cameraR()
        self.open_output_XX()
        self.open_output_XL()
        self.open_output_XR()
        self.open_output_OK()
        self.open_output_Monitor()

        # Open mqtt
        publish.single("pasovec/camera", "started", hostname="localhost")
        self.client = mqtt.Client("camera")
        self.client.connect("localhost")
        self.client.subscribe("pasovec/+")
        self.client.on_message = self.on_message
        self.saveOK = False
        self.saveXL = False
        self.saveXR = False
        self.saveXX = False

        # RUN
        self.run()

    def on_message(self, client, userdata, message):
        payload = str(message.payload.decode('utf8'))
        topic = message.topic
        if topic == 'pasovec/kbd':
            if payload == "OK": self.saveOK = True
            if payload == "XX": self.saveXX = True
            if payload == "XL": self.saveXL = True
            if payload == "XR": self.saveXR = True
            return

    def run(self):
        while True:
            stime = time.time();
            imgL = self.captureL()
            imgR = self.captureR()
            np_img = np.concatenate((imgL, imgR),axis=1)

            self.client.loop(0.05)
            if self.saveOK:
                print("Save OK")
                self.saveOK = False
                np_imgL = np.uint8(imgL)
                np_imgR = np.uint8(imgR)
                np_img = np.concatenate((np_imgL, np_imgR),axis=1)
                self.video_output_OK.write(np_img)
            if self.saveXX:
                print("Save XX")
                self.saveXX = False
                np_imgL = np.uint8(imgL)
                np_imgR = np.uint8(imgR)
                np_img = np.concatenate((np_imgL, np_imgR),axis=1)
                self.video_output_XX.write(np_img)
            if self.saveXL:
                print("Save XL")
                self.saveXL = False
                np_imgL = np.uint8(imgL)
                np_imgR = np.uint8(imgR)
                np_img = np.concatenate((np_imgL, np_imgR),axis=1)
                self.video_output_XL.write(np_img)
            if self.saveXR:
                print("Save XR")
                self.saveXR = False
                np_imgL = np.uint8(imgL)
                np_imgR = np.uint8(imgR)
                np_img = np.concatenate((np_imgL, np_imgR),axis=1)
                self.video_output_XR.write(np_img)

            np_img = np.uint8(np_img)
            self.video_output_Monitor.write(np_img)

    def open_cameraL(self):
        string = ('nvarguscamerasrc sensor-id=0 ! ' +
            'video/x-raw(memory:NVMM), width=1280, height=720, format=NV12, framerate=60/1 ! ' +
            'nvvidconv flip-method=0 left=50 right=1230 top=0 bottom=620 ! ' +
            'video/x-raw, width=320, height=160, format=BGRx ! videoconvert ! ' +
            'video/x-raw, format=BGR ! ' +
            'appsink drop=true sync=false max-lateness=6000')
        self.video_captureL = cv2.VideoCapture(string, cv2.CAP_GSTREAMER)
        assert self.video_captureL.isOpened(), "Could not open left camera"

    def open_cameraR(self):
        string = ('nvarguscamerasrc sensor-id=1 ! ' +
            'video/x-raw(memory:NVMM), width=1280, height=720, format=NV12, framerate=60/1 ! ' +
            'nvvidconv flip-method=0 left=45 right=1225 top=52 bottom=672 ! ' +
            'video/x-raw, width=320, height=160, format=BGRx ! videoconvert ! ' +
            'video/x-raw, format=BGR ! ' +
            'appsink drop=true sync=false max-lateness=6000')
        self.video_captureR = cv2.VideoCapture(string, cv2.CAP_GSTREAMER)
        assert self.video_captureR.isOpened(), "Could not open right camera"

    def open_output_XL(self):
        string = ('appsrc ! ' +
            'videoconvert ! ' +
            'video/x-raw, format=YUY2 ! ' +
            'jpegenc ! rtpjpegpay ! ' +
            'udpsink host={} port={}'.format(remoteHost, remotePortXL))
        self.video_output_XL = cv2.VideoWriter(
                string,
                apiPreference = 0,
                fourcc = cv2.VideoWriter.fourcc('M','J','P','G'),
                fps = 25,
                frameSize = (640, 160),
                isColor = True)
        assert self.video_output_XX.isOpened(), "Error Could not open video output XL"

    def open_output_XR(self):
        string = ('appsrc ! ' +
            'videoconvert ! ' +
            'video/x-raw, format=YUY2 ! ' +
            'jpegenc ! rtpjpegpay ! ' +
            'udpsink host={} port={}'.format(remoteHost, remotePortXR))
        self.video_output_XR = cv2.VideoWriter(
                string,
                apiPreference = 0,
                fourcc = cv2.VideoWriter.fourcc('M','J','P','G'),
                fps = 25,
                frameSize = (640, 160),
                isColor = True)
        assert self.video_output_XX.isOpened(), "Error Could not open video output XR"

    def open_output_XX(self):
        string = ('appsrc ! ' +
            'videoconvert ! ' +
            'video/x-raw, format=YUY2 ! ' +
            'jpegenc ! rtpjpegpay ! ' +
            'udpsink host={} port={}'.format(remoteHost, remotePortXX))
        self.video_output_XX = cv2.VideoWriter(
                string,
                apiPreference = 0,
                fourcc = cv2.VideoWriter.fourcc('M','J','P','G'),
                fps = 25,
                frameSize = (640, 160),
                isColor = True)
        assert self.video_output_XX.isOpened(), "Error Could not open video output XX"

    def open_output_OK(self):
        string = ('appsrc ! ' +
            'videoconvert ! ' +
            'video/x-raw, format=YUY2 ! ' +
            'jpegenc ! rtpjpegpay ! ' +
            'udpsink host={} port={}'.format(remoteHost, remotePortOK))
        self.video_output_OK = cv2.VideoWriter(
                string,
                apiPreference = 0,
                fourcc = cv2.VideoWriter.fourcc('M','J','P','G'),
                fps = 25,
                frameSize = (640, 160),
                isColor = True)
        assert self.video_output_OK.isOpened(), "Error Could not open video output OK"

    def open_output_Monitor(self):
        string = ('appsrc ! ' +
            'videoconvert ! ' +
            'video/x-raw, format=YUY2 ! ' +
            'jpegenc ! rtpjpegpay ! ' +
            'udpsink host={} port={}'.format(remoteHost, remotePortMonitor))
        self.video_output_Monitor = cv2.VideoWriter(
                string,
                apiPreference = 0,
                fourcc = cv2.VideoWriter.fourcc('M','J','P','G'),
                fps = 25,
                frameSize = (640, 160),
                isColor = True)
        assert self.video_output_Monitor.isOpened(), "Error Could not open video output"

    def capture(self):
        return captureL(self), captureR(self)

    def captureL(self):
        retL, imgL= self.video_captureL.read()
        assert retL, "Could not capture left image"
        return imgL

    def captureR(self):
        retR, imgR = self.video_captureR.read()
        assert retR, "Could not capture right image"
        return imgR

if __name__ == "__main__":
    kamera = Kamera()

K ovládání Pásovce přibyly další klávesové zkratky:

  • K — překážka před námi
  • J — překážka, objeď ji vlevo
  • L — překážka, objeď ji vpravo
  • I — volno

Závěr

Nyní už se lze věnovat nejnáročnější části práce spojené s neuronovými sítěmi: sběru, úpravě a kategorizaci dat. Příště už se dostaneme k nejzajímavější části: tvorbě a trénování neuronové sítě.

Hobrasoft s.r.o. | Kontakt