8.2.2021
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:
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:
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!
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:
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ě.