czwartek, 25 marca 2021

PIRek + python - ekeltroniczna niania z zegarem - #tataprogramuje

Rozwijamy dalej PIRka :)
W poprzednim poście opisałem w pełni jak możemy stworzyć samemu elektroniczną nianię przy pomocy Raspberry PI Zero W i kamery dedykowanej do układów Raspberry PI (tu kamery IR). Tym razem zamiast używać raspivid i vlc do strumieniowania, wykorzystamy program napisany w pythonie. Dodamy też widok zegara na strumieniu wideo.

Wykorzystanie raspivid i vlc bardzo uprościło stworzenie elektronicznej niani, jednak zauważyć można pewną niedogodność: ciężko zorientować się, czy w którymś momencie układ się nie zawiesił (czy to Malina przez niedobór mocy, czy sam VLC na komputerze/smartfonie przestał odświeżać widok).
Musimy więc dodać jakiś element, który zmienia się cały czas w widoku kamery.
Idealny jest więc zegar. Nikt oczywiście raczej nie chce ustawiać zegara z sekundnikiem w łóżeczku obok dziecka ;) Jako że Raspberry PI to miniaturowy komputer z systemem operacyjnym linux na pokładzie, możemy wykorzystać zegar systemowy i napisać program w pythonie, który będzie nam zarządzał strumieniem. Dodatkowo mamy wsparcie dla pythona ze strony Raspberry PI, więc możemy po prostu wykorzystać bibliotekę dla kamerki.
Jest jeszcze jedna zauważalna zaleta - w tym sposobie nie zauważyłem chwilowych zmrożeń obrazu, które pojawiały mi się co kilkanaście sekund przy użyciu raspivid.

Zarządzanie kamerą IR na Raspberry PI Zero przez pythona

Całość programu umieszczam w repozytorium na Bitbucket. Poniżej zaś analizujemy większość fragmentów, wyszczególniając niektóre elementy, które można zmienić.
Program pirek_cam.py powstał na podstawie przykładu z dokumentacji biblioteki picamera.

Import bibliotek

Większość potrzebnych bibliotek jest już w pakiecie z Raspbianem. Jeśli jest coś, czego nie ma na pokładzie, możemy to zainstalować:

 pip3 install jakasbiblioteka 
Zwracam przy okazji uwagę, że piszemy program dla pythona3, więc uruchamiać będziemy wszystko przez ten interpreter. Polecam też zrobić cobie jakiś alias albo przełożyć domyślne ścieżki na pythona3, bo aktualnie (marzec 2021) domyślnym pythonem na raspberry jest python 2.7.* (to słabo).

Te użyte przez nas to: 

import io
import picamera
import logging
import socketserver
import threading
import time
import datetime as dt

from threading  import Condition
from http       import server
from subprocess import check_output
from socket     import gethostname

Biblioteka picamera jest tu jedyną, której raczej nie użyjemy poza Malinami.

Port, rozdzielczość, ilość klatek

Port ustawiam tak samo jak wcześniej - na wartość 8160, dzięki czemu nie trzeba będzie nic zmieniać we wcześniejszych ustawieniach VLC, czy to na telefonie, czy w komputerze.
Względem rozdzielczości zaś, to sprawa jest do pewnego stopnia dowolna - możemy wybrać odpowiednią dla nas z pewnymi zastrzeżeniami. 

Po pierwsze - układ sterujący kamerami stworzonymi do Raspberry PI to OV5647. Może on wyprowadzać dane z maksymalną rozdzielczością 2592x1944, jednak wraz ze wzrostem rozdzielczości maleje maksymalna ilość klatek - w tym przypadku, przy maksymalnej rozdzielczości fps powinna być nie większa niż 15. Warto więc sprawdzić w dokumentacji jakie możemy nadać ustawienia. 

Po drugie - zauważyłem, że od pewnego momentu wybór większej rozdzielczości zmniejsza pole, jakie widzimy na obrazie z kamery. Warto więc zwrócić na to uwagę, bo raczej interesuje nas największy możliwy kąt widzenia.

port = 8160
#w,h = 320, 240
#w,h = 800, 600
#w,h = 1600, 1200
w,h = 1640, 1232
#w,h = 1920, 1080
#w,h = 2000, 1500
#w,h = 2592, 1944 # W module to max, ale framerate musi być 15.

fps = 24
res = '%dx%d' % (w,h)    # Sklejenie w jedno rozdzielczości.

 Ja wybrałem 1640x1232, bo chyba jest najbardziej optymalny - nie zauważyłem zmniejszenia pola widzenia, a w tym wypadku max fps to 30, więc obejmuje wybrane przeze mnie 24 klatki na sekundę.

Wyjście na przeglądarkę

 Tym razem będziemy mogli odtwarzać strumień tak z programu VLC, jak i w oknie przeglądarki. W tym celu definiujemy wygląd strony w html. Możemy oczywiście dodać coś do niej, jeśli się nam podoba.

PAGE="""\
<html>
<head>
<title>PIRek</title>
</head>
<body>
<center><h1>PIRek</h1></center>
<center><img src="stream.mjpg" width="%d" height="%d"></center>
</body>
</html>
""" % (w,h)

Klasy obsługi strumienia

Klasy StreamingOutput, StreamingHandler i StreamingServer posłużą nam do pchnięcia strumienia wideo w sieć. W StreamingHandler zmodyfikowałem metodę do_GET względem przykładu z dokumentacji picam. Wolę po prostu, by domyślnie strumień działał tak jak wcześniej (znów nie trzeba nic zmieniać w VLC). Dodatkowo zaś mogę wpisać adres w przeglądarkę z końcówką /index.html, dzięki czem ten sam strumień mogę oglądać np w firefoxie.

class StreamingOutput(object):
    def __init__(self):
        self.frame = None
        self.buffer = io.BytesIO()
        self.condition = Condition()

    def write(self, buf):
        if buf.startswith(b'\xff\xd8'):
            # New frame, copy the existing buffer's content and notify all
            # clients it's available
            self.buffer.truncate()
            with self.condition:
                self.frame = self.buffer.getvalue()
                self.condition.notify_all()
            self.buffer.seek(0)
        return self.buffer.write(buf)

class StreamingHandler(server.BaseHTTPRequestHandler):
    def do_GET(self):
        # Aktualnie wolę, by domyślnie był to strumień dla VLC.
        #if self.path == '/':
        #    self.send_response(301)
        #    self.send_header('Location', '/index.html')
        #    self.end_headers()
        if self.path == '/index.html':
            content = PAGE.encode('utf-8')
            self.send_response(200)
            self.send_header('Content-Type', 'text/html')
            self.send_header('Content-Length', len(content))
            self.end_headers()
            self.wfile.write(content)
        elif self.path == '/stream.mjpg' or self.path == '/':
            self.send_response(200)
            self.send_header('Age', 0)
            self.send_header('Cache-Control', 'no-cache, private')
            self.send_header('Pragma', 'no-cache')
            self.send_header('Content-Type', 'multipart/x-mixed-replace; boundary=FRAME')
            self.end_headers()
            try:
                while True:
                    with output.condition:
                        output.condition.wait()
                        frame = output.frame
                    self.wfile.write(b'--FRAME\r\n')
                    self.send_header('Content-Type', 'image/jpeg')
                    self.send_header('Content-Length', len(frame))
                    self.end_headers()
                    self.wfile.write(frame)
                    self.wfile.write(b'\r\n')
            except Exception as e:
                logging.warning(
                    'Removed streaming client %s: %s',
                    self.client_address, str(e))
        else:
            self.send_error(404)
            self.end_headers()

class StreamingServer(socketserver.ThreadingMixIn, server.HTTPServer):
    allow_reuse_address = True
    daemon_threads = True

Funkcja wyświetlająca czas

Biblioteka picam umożliwia nam wyświetlanie napisu na obrazie z kamery. Żeby osiągnąć nasz cel - widoczne zmiany sekund - należy to jednak wykonywać w osobnym wątku, stąd dla czystości tworzymy osobną funkcję dla tego zadania.

# Funkcja zmieniająca czas.
def annotate_thread(cam):
    while 1:
        #cam.annotate_text = dt.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
        #cam.annotate_text = dt.datetime.now().strftime(30*' ' + '%H:%M:%S')
        cam.annotate_text = dt.datetime.now().strftime('%H:%M:%S')
        time.sleep(0.1)

W funkcji tej po prostu w nieskończonej pętli podstawiamy aktualny czas do zmiennej zawierającej tekst w polu kamery. Należy też nieco ograniczyć ilość takich odświeżeń, by niepotrzebnie nie marnować mocy procesora - stąd time.sleep(0.1). Częstotliwość 10/s w zupełności wystarczy do płynnych zmian.
Zostawiłem też zakomentowane różne formaty - może będziemy chcieli mieć wyświetloną również datę, bądź godzina wygodniejsza byłaby bardziej po prawej?

Część robocza programu

Wywołanie nagrywania przerzuciłem za warunek uruchomienia programu jako główny. Przydać się to może, jeśli chcielibyśmy kiedyś importować coś z pliku, np. do testów w sesji interaktywnej, jednocześnie nie chcąc uruchamiać od razu strumienia.

if __name__ == '__main__':
    with picamera.PiCamera(resolution=res, framerate=fps) as camera:
        print("Typ kamery: ", camera.revision)
        print("Wysyłana rozdzielczość: %dx%d, %dfps" % (w, h, fps) )
        myip = check_output(['hostname', '--all-ip-addresses']).decode().split()[0]
        hostname = gethostname()
        print("Adresy: \n ",
                       "\t http://%s.local:%d \n" % (hostname, port),
                       "\t http://%s.local:%d/index.html \n" % (hostname, port),
                       "\t http://%s:%d \n" % (myip, port) )
        output = StreamingOutput()
        #Uncomment the next line to change your Pi's Camera rotation (in degrees)
        camera.rotation = 180
        camera.start_recording(output, format='mjpeg')
        #camera.annotate_background = picamera.Color("#afafaf")
        camera.annotate_foreground = picamera.Color("#101010")
        camera.annotate_text = dt.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
        camera.annotate_text_size = 100
        threading.Thread(target=annotate_thread, args=(camera,) ).start()
        try:
            address = ('', port)
            server = StreamingServer(address, StreamingHandler)
            server.serve_forever()
        finally:
            camera.stop_recording()

Jak widać na powyższym fragmencie, do przykładu z dokumentacji dodałem kilka wygodnych informacji. Urządzenie sprawdza nazwę hosta oraz jakie ma IP w sieci wewnętrznej, po czym podaje kilka przykładów z jakimi możemy uruchomić strumień.

Mamy także zmianę koloru i rozmiaru tekstu z czasem, oraz uruchamiamy wątek zmiany czasu. Wątek oczywiście potrzebuje wskaźnika na obiekt camera w argumencie.

No i mi akurat pasował obrót widoku o 180 stopni, więc go aktywowałem.

Testowanie i uruchomienie

Możemy już przetestować nasz program. Jeśli nasz PIRek strumieniuje wideo starym sposobem, należy wpierw ubić ten proces. Możemy po prostu wykonać komendę:

pkill raspivid

Następnie uruchamiamy nasz nowy program:

python3 pirek_cam.py

Uruchomienie w vlc nie różni się niczym od poprzedniego. Tym razem jednak możemy również odpalić przeglądarkę i przejść np. do adresu:

http://nazwanaszegoraspberry.local:8160/index.html

Strumień video po starcie systemu

Nie musimy wszystkiego ustawiać ponownie w pliku rc.local itp. Wszystko już jest gotowe. Wystarczy edytować plik /home/pi/stream, do którego wpisywaliśmy naszą komendę ostatnio. Najlepiej jej nie wyrzucać, tylko zakomentować, a uruchomienie nowego programu wpisać w nowej linii. Tyle. Przykładowa zawartość:

# Wcześniejsza komenda uruchamiająca strumień:
# raspivid -o - -t 0 -hf -w 800 -h 400 -fps 24 |cvlc -vvv stream:///dev/stdin --sout '#standard{access=http,mux=ts,dst=:8160}' :demux=h264
# Uruchomienie nowego programu:
python3 /home/pi/pirek_cam.py

Po restarcie naszego urządzenia powinno automatycznie włączać strumień.

Czytaj także:


Brak komentarzy:

Prześlij komentarz