Kodowanie PackBits jest jednym z podstawowych przykładów kompresji danych opartych na długości ciągów powtarzających się elementów, znane także jako kodowanie długości ciągów (RLE, Run-Length Encoding). Choć na pierwszy rzut oka wydaje się to skuteczną metodą kompresji, w praktyce nie jest idealne dla wszystkich rodzajów danych. Istnieją przypadki, w których takie podejście okazuje się mniej efektywne niż przechowywanie surowych danych w niezmienionej formie. Przykładowo, ciąg znaków "ABC" w tej metodzie będzie zapisany jako 1A1B1C, co podwaja rozmiar danych.
Jednakże, w praktyce często można znaleźć kompromis, w którym kodowanie uwzględnia zarówno powtórzenia znaków, jak i ciągi literalne, które nie powtarzają się. W takim przypadku ciąg "ABC" mógłby zostać zapisany jako 3ABC. Choć nadal będzie to nieco dłuższe niż zapis surowy, takie rozwiązanie daje lepsze wyniki w bardziej złożonych przypadkach. Na przykład ciąg "AAAAABCBCAAAAAA" mógłby zostać zapisany jako 5A4BCBC6A.
Jest to zbliżone do formatu MacPaint, ale pojawia się tu pytanie: jak odróżnić, kiedy liczba oznacza powtórzenie, a kiedy jest częścią literału? Aby rozwiązać ten problem, w systemie PackBits liczby są reprezentowane przez liczby całkowite jedno-bajtowe (signed 1-byte integers). Zasada jest prosta: liczba z zakresu 0–127 oznacza, że po niej następuje n + 1 literalnych bajtów, natomiast liczba z zakresu od -1 do -127 wskazuje, że następny bajt ma się powtarzać 1 - n razy. Z kolei liczba -128 nie jest używana. Dzięki temu systemowi kodowanie literali i powtórzeń może być rozróżniane bez ryzyka niejednoznaczności.
PackBits jest wykorzystywane w wielu popularnych formatach plików, w tym w MacPaint, gdzie kompresja tekstów osiąga średni rozmiar 10 KB. Zaletą tej metody jest to, że działa na poziomie wiersza, co oznacza, że dane w pliku są kompresowane w sposób niezależny dla każdego wiersza. Przykładem może być obraz, którego każdy wiersz zawiera do 576 pikseli (72 bajty), co jest mniejsze niż 128, a zatem nie ma ryzyka przekroczenia limitu rozmiaru ciągu.
PackBits może wydawać się nieco bardziej skomplikowane, ale jego mechanizm jest bardzo prosty. Pierwszym krokiem jest znalezienie ciągów powtarzających się znaków, co można zrobić przy pomocy funkcji take_same(). Ta funkcja, analizując dane wejściowe, znajduje sekwencje powtarzających się bajtów i liczy ich długość. Następnie, w zależności od tego, czy znaleziono ciąg powtórzeń, czy literał, dane są dodawane do kodowanego strumienia w odpowiedni sposób. Kodowanie w tym przypadku może wyglądać jak poniżej:
-
Jeśli znaleziono ciąg powtórzeń, zapisuje się liczbę powtórzeń oraz bajt.
-
Jeśli znaleziono ciąg literali, zapisuje się liczbę literali oraz same literali w formie niezmienionej.
Ta procedura jest wykonywana na każdym wierszu, co zapewnia efektywność i minimalizuje ryzyko błędów w procesie kompresji. Warto zauważyć, że kodowanie PackBits nie ma mechanizmu obsługi pojedynczych powtórzeń (np. "repeat once"), co oznacza, że takie przypadki będą traktowane jako literały.
Metoda ta ma jednak swoje ograniczenia. Zasadniczym jest fakt, że nie obsługuje ona ciągów o długości większej niż 128 bajtów w jednej sekwencji, ale dzięki sposobowi przetwarzania danych w wierszach (np. w plikach MacPaint) to ograniczenie nie stanowi problemu, ponieważ każdy wiersz zawiera mniej niż 128 bajtów.
PackBits jest stosunkowo prostym, ale wydajnym rozwiązaniem dla niektórych typów plików graficznych i innych zastosowań, gdzie dominują powtarzające się dane. W jego obrębie kodowanie długości ciągów powtarzających się elementów jest realizowane w sposób optymalny, a rozmiar pliku może być znacznie zmniejszony bez utraty jakości danych.
Co istotne, oprócz samego sposobu kodowania PackBits, warto pamiętać o jego zastosowaniach. Dzięki temu algorytmowi możliwe jest szybkie kompresowanie danych, które charakteryzują się dużą liczbą powtórzeń. W wielu przypadkach, zwłaszcza w grafice rastrowej, gdzie jedne kolory dominują przez szereg pikseli, metoda ta jest niezwykle skuteczna. Jednakże dla danych bardziej zróżnicowanych, takich jak obrazy z dużą liczbą różnorodnych kolorów, efektywność PackBits może znacząco spadać, co warto mieć na uwadze przy wyborze techniki kompresji.
Jak stworzyć prosty rozpoznawacz cyfr ręcznych przy użyciu Pygame i algorytmu KNN?
W pierwszych linijkach kodu rozpoczynamy od inicjalizacji Pygame i utworzenia okna wyświetlacza. Używamy funkcji pygame.init() do załadowania wszystkich potrzebnych modułów, a następnie tworzymy główne okno za pomocą pygame.display.set_mode(), gdzie określamy wymiary okna oraz flagi umożliwiające jego rozciąganie i zmianę rozmiaru. Okno, mimo że przedstawia matrycę 8x8 pikseli, będzie mogło być dowolnie powiększane lub zmniejszane, a jego wymiary pozostaną proporcjonalne, co uzyskujemy dzięki ustawieniu flag pygame.SCALED i pygame.RESIZABLE. Dodatkowo, za pomocą pygame.display.set_caption(), nadamy tytuł aplikacji "Digit Recognizer".
Po uruchomieniu tego kodu, nasza aplikacja będzie gotowa do dalszych interakcji. W kolejnym kroku musimy stworzyć pętlę główną programu, która będzie odpowiedzialna za renderowanie obrazu na ekranie oraz obsługę zdarzeń, takich jak naciskanie klawiszy czy kliknięcia myszką.
Pętla główna zaczyna się od stałego wyświetlania tablicy pikseli reprezentujących cyfry na ekranie. Kod:
Umożliwia nam to ciągłe rysowanie i odświeżanie obrazu. Następnie, obsługujemy różne zdarzenia, które mogą wystąpić, w tym naciskanie klawiszy. Klawisz "c" jest używany do klasyfikacji wprowadzonej cyfry. Proces klasyfikacji wymaga przekształcenia danych wejściowych — obrazek przedstawiający cyfrę jest konwertowany z formatu wielowymiarowego na jednowymiarowy wektor. Wartości pikseli są przekształcane na odpowiednią skalę, a następnie klasa KNN (k najbliższych sąsiadów) dokonuje rozpoznania.
Kod do klasyfikacji:
Kolejny klawisz — "e" — umożliwia wyczyszczenie matrycy, czyli usunięcie rysunku, a klawisz "p" służy do przewidywania tego, jak powinna wyglądać narysowana cyfra. W tym przypadku proces jest podobny do klasyfikacji, ale zamiast klasyfikować obrazek, używamy algorytmu KNN do przewidywania, jak ten obrazek może wyglądać na podstawie innych podobnych w zbiorze uczącym.
Kod przewidywania:
Program rozpoznawania cyfr obsługuje również kliknięcia myszką — każdy klik na ekranie rysuje biały punkt na matrycy. Program kończy działanie, kiedy użytkownik zamknie okno.
Choć nasz program jest stosunkowo krótki (około 50 linii kodu), potrafi poprawnie rozpoznać większość rysunków cyfr, co świadczy o efektywności i prostocie algorytmu KNN. Mimo że algorytm ten jest prosty, działa zaskakująco dobrze i daje możliwość łatwego prototypowania rozpoznawania obrazów.
Ważne jest jednak, aby zrozumieć, że bardziej zaawansowane algorytmy, takie jak sieci neuronowe, choć mogą zaoferować wyższą precyzję i wydajność w bardziej złożonych zadaniach, nie zawsze będą najlepszym wyborem. W wielu przypadkach prostsze algorytmy, takie jak KNN, mogą być bardziej efektywne, zwłaszcza w przypadku małych zbiorów danych.
Algorytm KNN, pomimo swojej prostoty, znajduje szerokie zastosowanie w różnych dziedzinach, takich jak analiza tekstu, przewidywanie wyników finansowych, czy nawet medycyna. W badaniach dotyczących długości pobytu pacjentów w szpitalu w czasie pandemii COVID-19, KNN wykazał się podobną skutecznością co bardziej zaawansowane metody, takie jak regresja logistyczna czy lasy losowe. Co więcej, KNN jest stosunkowo łatwy do zaimplementowania i nie wymaga skomplikowanego strojenia parametrów, co czyni go idealnym narzędziem w przypadku, gdy zależy nam na prostocie i szybkości.
Należy jednak pamiętać, że algorytm KNN nie sprawdza się w przypadku danych hałaśliwych lub zbyt dużych zbiorów, w których czas obliczeń staje się nieakceptowalnie długi. W takich przypadkach warto rozważyć inne algorytmy, takie jak SVM (maszyny wektorów nośnych) czy sieci neuronowe, które lepiej radzą sobie z dużymi i złożonymi zbiorami danych.
Pomimo tych ograniczeń, KNN pozostaje jednym z najłatwiejszych algorytmów do implementacji, a także jednym z najczęściej wykorzystywanych w edukacyjnych i prostych projektach. Dzięki łatwości użycia i intuicyjnej naturze stanowi doskonały punkt wyjścia do nauki o algorytmach klasyfikacyjnych.
Jak działa NanoBASIC: Zrozumienie podstaw i składni
NanoBASIC, uproszczona wersja języka BASIC, ma na celu nauczenie podstaw programowania i rozwiązywania problemów poprzez proste skrypty. Składa się z prostych poleceń, które pozwalają na manipulowanie danymi, wykonywanie obliczeń matematycznych oraz sterowanie przebiegiem programu. Choć język ten jest ograniczony, to jednak stanowi doskonałą okazję do zrozumienia fundamentów programowania, które pozostają niezmienne niezależnie od używanego języka.
Jednym z kluczowych elementów NanoBASIC jest polecenie PRINT, które służy do wyświetlania informacji na ekranie. Może ono wyświetlać zarówno literały tekstowe, jak i wynik obliczeń. W NanoBASIC wystarczy użyć prostych instrukcji, aby np. wyświetlić ciąg tekstowy, jak:
Programy w tym języku mogą również wyświetlać wyniki działań matematycznych, takich jak dodawanie, mnożenie, czy inne operacje arytmetyczne. Na przykład:
Wynik tego polecenia to:
Warto zauważyć, że między wyrażeniami dodawane są tabulatory, które mogą wyglądać różnie w zależności od ustawień konsoli.
Instrukcje warunkowe i wyrażenia logiczne w NanoBASIC
Podobnie jak w innych językach programowania, NanoBASIC obsługuje instrukcje warunkowe. Jednak są one bardzo uproszczone w porównaniu do standardowych rozwiązań. Instrukcja IF sprawdza warunek, a jeśli jest on prawdziwy, wykonuje przypisaną akcję. W NanoBASIC IF nie obsługuje operatorów logicznych jak AND czy OR i nie posiada klauzuli ELSE. Przykładowo, aby sprawdzić, czy liczba jest mniejsza od 10, i w zależności od tego wyświetlić komunikat, użyjemy takiego zapisu:
Wyrażenia logiczne w NanoBASIC różnią się od tych w bardziej zaawansowanych językach programowania, np. operator nierówności to <> lub ><, a porównanie równości wykonuje się za pomocą znaku =, a nie ==.
Skok do etykiety: GOTO, GOSUB i RETURN
NanoBASIC korzysta z instrukcji sterujących takich jak GOTO, GOSUB oraz RETURN. Instrukcja GOTO przerywa bieżący ciąg wykonywania programu i natychmiast przenosi do wskazanej linii kodu. Z kolei GOSUB umożliwia przejście do określonej linii, a potem po wykonaniu kodu powrót do miejsca, z którego zostało wywołane. Kluczową różnicą jest to, że po zakończeniu GOSUB należy użyć instrukcji RETURN, aby wrócić do miejsca wywołania. Na przykład:
W tym przypadku program wyświetli 10, ponieważ po wykonaniu GOSUB zmienna A została ustawiona na wartość 10.
Styl programowania w NanoBASIC
NanoBASIC nie posiada zaawansowanych narzędzi do organizowania kodu, dlatego warto wprowadzać odpowiednie komentarze, które pomogą w zrozumieniu struktury programu. Dobrą praktyką jest także pisanie słów kluczowych w języku BASIC wielkimi literami, co zwiększa czytelność. Z racji, że NanoBASIC nie oferuje złożonych mechanizmów kontrolujących przepływ programu, takich jak pętle FOR, często korzysta się z instrukcji GOTO, co może prowadzić do tzw. "spaghetti code". Choć to rozwiązanie jest prymitywne, jest ono typowe dla starszych wersji języka BASIC, a także dla niektórych języków maszynowych i assemblera.
Mimo swoich ograniczeń, NanoBASIC stanowi przykład języka, który był wykorzystywany do nauki podstaw programowania, a jego prostota jest zarazem jego siłą. Później, gdy programowanie stawało się bardziej skomplikowane, powstały takie ruchy jak strukturalne programowanie, które miały na celu poprawienie jakości kodu poprzez eliminację nadmiernego stosowania GOTO.
Przykład programu: Liczby Fibonacciego
Aby lepiej zrozumieć działanie NanoBASIC, warto przyjrzeć się przykładowi programu, który wypisuje liczby Fibonacciego mniejsze niż 100. Sekwencja Fibonacciego to ciąg liczb, w którym każda liczba (oprócz pierwszych dwóch) jest sumą dwóch poprzednich. Program w NanoBASIC, który generuje te liczby, wygląda następująco:
Program zaczyna od ustawienia pierwszych dwóch liczb (0 i 1), a następnie w każdym kroku sumuje poprzednie dwie, aby uzyskać kolejne liczby w ciągu. Jeśli wartość B jest mniejsza niż 100, program powraca do linii 21 i kontynuuje wyświetlanie kolejnych liczb Fibonacciego.
Zrozumienie składni języka
Aby jeszcze głębiej zrozumieć działanie NanoBASIC, warto przyjrzeć się składni, która opiera się na tzw. formie Backusa-Naur’a (BNF). Jest to metoda formalnego opisania gramatyki języka programowania. Dzięki tej gramatyce możemy określić, jakie konstrukcje są poprawne, a które nie. Dla przykładu:
Gramatyka pozwala na generowanie poprawnych łańcuchów znaków, a także sprawdzanie, czy dany ciąg tekstowy spełnia zasady języka.
Rozważania na temat przyszłości
Mimo że NanoBASIC jest celowo uproszczony, jego projekt oparty na rzeczywistej wersji Tiny BASIC czyni go interesującym przykładem języka, który może pomóc w nauce podstaw programowania. Choć brak zaawansowanych funkcji programistycznych, takich jak pętle FOR czy strukturalne instrukcje sterujące, może ograniczać możliwości, to i tak pozostaje użyteczny w nauce podstawowych zasad działania komputerów.
Warto pamiętać, że programowanie w NanoBASIC może być dobrym wprowadzeniem do bardziej zaawansowanych technik i języków. Poznanie podstawowych zasad działania takich jak zmienne, operacje arytmetyczne, instrukcje warunkowe i skokowe jest kluczowe dla zrozumienia bardziej zaawansowanych paradygmatów programowania.

Deutsch
Francais
Nederlands
Svenska
Norsk
Dansk
Suomi
Espanol
Italiano
Portugues
Magyar
Polski
Cestina
Русский