5.32. Zapisywanie i kompresja¶
Każda strona aż do tej pory operowała na obrazach znajdujących się w kamerze: przechwyconych do bufora ramki lub zaalokowanych na stercie MicroPython, przetwarzanych za pomocą metod modułu image i albo wyświetlanych w podglądzie IDE, albo przekazywanych do kolejnego etapu w tym samym skrypcie. Większość aplikacji w pewnym momencie potrzebuje zrobić coś przeciwnego: wziąć obraz znajdujący się aktualnie w pamięci RAM i umieścić go gdzieś trwale – na karcie SD, w hoście USB, w sieci – gdzie może go odczytać coś innego niż kamera.
Moduł image udostępnia dwie ścieżki do tej pracy. Ścieżka save zapisuje obraz do pliku w systemie plików, przy czym format pliku jest wybierany na podstawie rozszerzenia, a szczegóły kodowania są obsługiwane przez metodę. Ścieżka to-format zwraca obiekt Image zawierający zakodowany strumień bajtów, odpowiedni do przekazania do wywołania strumieniowego lub sieciowego bez konieczności dotykania systemu plików. Każda pasuje do innego rodzaju aplikacji; obie opierają się na tym samym silniku kompresji znajdującym się pod spodem.
5.32.1. Zapisywanie do pliku¶
save() zapisuje obraz w systemie plików pod wskazaną ścieżką:
img.save("/sdcard/capture.jpg")
img.save("/sdcard/capture.bmp")
img.save("/sdcard/region.jpg", roi=(40, 60, 200, 150), quality=85)
Format jest wybierany na podstawie rozszerzenia pliku. Rozpoznawanych jest pięć rozszerzeń: .bmp zapisuje mapę bitową Windows (bezstratnie, bez kompresji, piksel po pikselu zgodnie z przechwyconym obrazem); .pgm zapisuje portable graymap (bezstratnie, tylko w skali szarości); .ppm zapisuje portable pixmap (bezstratnie, RGB); .jpg i .jpeg zapisują JPEG (stratnie, skompresowany). Obraz źródłowy musi już być we właściwym formacie koloru dla wybranego kontenera – zapisanie obrazu kolorowego jako .pgm jest błędem.
roi ogranicza zapis do podprostokąta obrazu, w taki sam sposób jak słowo kluczowe roi w każdej innej metodzie modułu image. Domyślnie jest to pełny obraz. Słowo kluczowe jest ignorowane przy zapisywaniu obrazu skompresowanego w formacie JPEG, ponieważ postać zapisana na dysku obejmuje już pełną ramkę, a ponowne kodowanie poprzez wycięcie zniweczyłoby sens zapisywania istniejących skompresowanych bajtów.
quality to jakość kompresji JPEG w zakresie od 0 do 100 i ma znaczenie tylko wtedy, gdy wyjściem jest JPEG (słowo kluczowe jest ignorowane dla formatów bezstratnych). Domyślna wartość 50 stanowi właściwą równowagę dla większości aplikacji; zakres od 70 do 85 to pasmo dla wyższej jakości wizualnej, 30 do 50 to właściwy zakres dla małych miniatur i transmisji o ograniczonej przepustowości, a 90 i więcej jest zarezerwowane dla przypadków, w których obraz będzie oglądany ręcznie lub przetwarzany przez algorytm wrażliwy na artefakty kompresji.
Obraz źródłowy jest zwracany, dzięki czemu wywołanie można łączyć w łańcuch: img.save("/sdcard/x.jpg").draw_string(0, 0, "saved"). Zwracany obiekt to ten sam obraz w pamięci; zapis jest efektem ubocznym.
Typowym zastosowaniem jest wzorzec przechwyć-i-zapisz. Wyzwalacz zostaje uruchomiony (wykryta zostaje plama (blob), naciśnięto przycisk, upłynął czas licznika); skrypt przechwytuje ramkę; dołącza znacznik czasu do nazwy pliku; po czym wywołuje save(), aby przesłać obraz na kartę SD. Podgląd IDE działa nadal, kolejny wyzwalacz zostaje uruchomiony, a zapisane pliki się gromadzą.
5.32.2. Kodowanie do pamięci¶
Gdy miejscem docelowym nie jest system plików, lecz połączenie sieciowe, port szeregowy albo wejście innego modułu, aplikacja potrzebuje zakodowanego strumienia bajtów w pamięci, a nie na dysku. to_jpeg() oraz to_png() zapewniają dokładnie to:
encoded = img.to_jpeg(quality=80, copy=True)
bytes_to_send = encoded.bytearray()
sock.send(bytes_to_send)
Domyślnym zachowaniem jest konwersja w miejscu: obiekt źródłowy zostaje przekonwertowany na obraz JPEG (lub PNG) i ten sam obiekt jest zwracany. Z copy=True konwersja zapisuje wynik do świeżo zaalokowanego obiektu na stercie; z copy_to_fb=True wynik trafia do bufora ramki. Wybór jest taki sam jak ten oferowany przez każdą inną metodę konwersji – domyślnie w miejscu, kopia gdy oryginał jest potrzebny później.
quality i subsampling to te same pokrętła strojenia JPEG, które udostępnia ścieżka zapisu. subsampling wybiera schemat podpróbkowania chrominancji: image.JPEG_SUBSAMPLING_AUTO dobiera najlepszy dla wybranej jakości, image.JPEG_SUBSAMPLING_444 zachowuje chrominancję w pełnej rozdzielczości (największy plik, najlepsza dokładność koloru), image.JPEG_SUBSAMPLING_422 oraz image.JPEG_SUBSAMPLING_420 zmniejszają o połowę rozdzielczość chrominancji wzdłuż jednej lub obu osi (mniejsze pliki, niewielkie zmiękczenie koloru, które jest niewidoczne przy typowych odległościach oglądania). Domyślna wartość AUTO jest właściwym wyborem, chyba że aplikacja ma określoną potrzebę.
PNG za pomocą to_png() jest bezstratny, ale wolniejszy w kodowaniu i tworzy większe pliki niż JPEG w przypadku treści fotograficznej (treść fotograficzna kompresuje się słabo w schemacie predykcji PNG). Używaj PNG, gdy obraz to grafika liniowa, zrzut ekranu lub zawiera grafikę o ostrych krawędziach narysowaną na przechwyconej ramce – kodowanie bezstratne zachowuje ostre krawędzie, które JPEG by zmiękczył. W przeciwnym razie właściwym domyślnym wyborem jest JPEG.
Zarówno to_jpeg(), jak i to_png() przyjmują te same pozycyjne argumenty oraz słowa kluczowe dotyczące rysowania i skalowania, które przyjmują inne metody konwersji – x_scale, y_scale, roi, rgb_channel, alpha, color_palette, alpha_palette, hint – dzięki czemu to samo wywołanie może w jednym kroku zakodować przeskalowaną, wykadrowaną lub odwzorowaną na paletę wersję źródła. compress() to dawna nazwa to_jpeg(); obie przyjmują te same argumenty i dają ten sam wynik.
5.32.3. Co daje kompresja¶
Liczby stojące za kompromisem między JPEG a postacią surową warto raz prześledzić.
Ramka 320 na 240 w formacie RGB565 to 153 600 bajtów (jedna przechwycona ramka w rozdzielczości QVGA). Ramka 640 na 480 to 614 400 bajtów; ramka 1280 na 960 to 2 457 600 bajtów. Żadna z nich nie jest duża w porównaniu z ekranem komputera czy telefonu, ale są znaczne w kontekście kamery, która ma w sumie kilka MB pamięci RAM, karty SD o skończonej przepustowości zapisu oraz łącza z hostem, które zwykle działa przez USB CDC, UART lub moduł bezprzewodowy przy umiarkowanych prędkościach.
JPEG przy quality=50 zwykle kompresuje fotograficzną przechwyconą ramkę od 10 do 20 razy: ta ramka 640 na 480 o rozmiarze 614 KB staje się zakodowanym strumieniem bajtów o wielkości od 30 do 60 KB. Przy quality=85 kompresja spada do zakresu od 5 do 10 razy (od 60 do 120 KB dla tej samej ramki). Przy quality=10 – pełnym artefaktów, ale wciąż rozpoznawalnym – kompresja sięga od 30 do 50 razy (od 12 do 20 KB).
Te liczby decydują o tym, co praktycznie da się zrobić z zapisanymi ramkami. Ścieżka karty SD utrzymująca 10 MB/s obsługuje 30 klatek na sekundę treści VGA zakodowanej jako JPEG przy quality=50 z dużym zapasem (około 1 do 2 MB/s); zapisanie tej samej treści w postaci nieskompresowanej wymaga ponad 18 MB/s, czyli więcej, niż ścieżka systemu plików kamery jest w stanie utrzymać względem karty. Host USB pobierający ramki zakodowane jako JPEG przez CDC z prędkością 1 MB/s odbiera ramki o wielkości od 30 do 60 KB z prędkością mniej więcej od 15 do 30 klatek na sekundę; pobierając surowe ramki przy tej samej prędkości otrzymuje jedną lub dwie klatki na sekundę.
Krótko mówiąc: metody kompresji nie są jedynie udogodnieniem przy zapisywaniu. To one sprawiają, że przechwycona ramka jest użyteczna poza kamerą przy szybkościach klatek, na których zależy aplikacji. Wybór właściwej kompresji – JPEG o jakości 50 do ogólnego rejestrowania, 80 do prac wymagających jakości, PNG do przechwytywania grafiki liniowej – jest częścią rutynowej pracy każdej nietrywialnej aplikacji kamery.