TI/Programowanie II/Wejście i wyjście
Spis treści
Wczytywanie z pliku
Pliki binarne
Przyjemnie jest skorzystać z wygodnej funkcji, która wczyta nasz plik lub jego fragment i następnie stworzy na jego podstawie (i na podstawie informacji, które my podamy) obiekt odpowiedniego typu (ndarray), tak samo, jak korzystaliśmy z funkcji numpy.loadtxt(), wczytując dane z plików tekstowych.
Przypomnij sobie składnię funkcji [1]
numpy.fromfile(file, dtype=float, count=-1, sep='')
. Parametry funkcji:
- file — otwarty plik lub nazwa pliku.
- dtype — typ danych. Informacja ta będzie służyła do konstrukcji tablicy. W przypadku plików binarnych używana jest również do ustalenia przez funkcję wielkości i kolejności liczb w pliku.
- count — ile próbek ma wczytać. Domyślna ilość -1 oznacza wczytanie całego pliku.
- sep — separator między kolejnymi elementami pliku w pliku tekstowym. Gdy podany jest pusty string, oznacza to, że plik jest binarny.
Wczytując pliki, lubisz nadać wynikowi własny kształt. W tym celu używasz metody reshape(), której argumentem jest krotka z docelowy rozmiarem. Być może tablicę wynikową będziesz musiał transponować, żeby miała porządany kształt (np. kolejny sygnał w kolejnej kolumnie).
Pliki *.mat
Plików matlabowe — z końcówką .mat — wczytywać można za pomocą modułu scipy.io. Ma ona następujący nagłówek:
scipy.io.loadmat(file_name, mdict=None, appendmat=True, **kwargs)
- file_name — nazwa pliku. Jeżeli parametr appendmat ma wartość True, w file_name nie musi mieć końcówki .mat, bo zostanie ona automatycznie dodana.
- mdict — słownik, w który moga zosrtać wsadzone zmienne matlabowe. Domyślna wartość None.
- appendmat — dołącz do file_name końcówkę .mat, jeżeli jej tam nie ma.
- byte_order — porządek bajtów. Domyślnie None — zgadywany z pliku. Może też przyjmować wartość: ‘native’, ‘=’, ‘little’, ‘<’, ‘BIG’, ‘>’.
- mat_dtype — domyślnie True. Jeżeli True zwraca macierze tego samego typu, jaki zostałby załadowany do Matlaba (a nie w takim w jakim zostały zachowane)/
- squeeze_me
- chars_as_strings — czy ma zamienić char na napisy.
- matlab_compatible — (squeeze_me=False, chars_as_strings=False, mat_dtype=True, struct_as_record=True).
- struct_as_record —Whether to load MATLAB structs as numpy record arrays, or as old-style numpy arrays with dtype=object. Setting this flag to False replicates the behaviour of scipy version 0.7.x (returning numpy object arrays). The default setting is True.
- variable_names — if None (the default) — read all variables in file. Otherwise variable_names should be a sequence of strings, giving names of the matlab variables to read from the file. The reader will skip any variable with a name not in this sequence, possibly saving some read processing.
Funkcja zwaraca słownik mat_dict z nazwami zmiennych jako kluczami i macierzami jako wartościami.
Widmo
Wartości zwracane przez fft(a,n) (a sygnał, n ilość punktów transformaty) mają następujący standardowy porządek: Jeśli A = fft(a, n), to
- A[0] zawiera składową stałą (średnią sygnału),
- A[1:n/2] zawiera współczynniki odpowiadające dodatnim częstościom,
- A[n/2+1:] zawiera współczynniki odpowiadające ujemnym częstościom w kolejności od bardziej do mniej ujemnych.
- Dla parzystego n A[n/2] reprezentuje dodatnia i ujemną częstość Nyquista i dla sygnałów rzeczywistych jest liczbą rzeczywistą.
- Dla nieparzystego n, element A[(n-1)/2] zawiera współczynnik dla największej częstości dodatniej a element A[(n+1)/2] zawiera współczynnik dla największej częstości ujemnej.
numpy.fft.fftfreq
Funkcja numpy.fft.fftfreq(len(A),1.0/Fs) zwraca macierz częstości odpowiadających poszczególnym elementom wyjściowym.
Składnia: numpy.fft.fftfreq(n, d=1.0) Parametry:
- n : int — długość okna.
- d : skalar — okres próbkowania (odwrotność częstości próbkowania).
- Zwracane częstości są obliczane w następujący sposób:
- f = [0,1,...,n/2-1,-n/2,...,-1]/(d*n) jeśli n jest przyste
- f = [0,1,...,(n-1)/2,-(n-1)/2,...,-1]/(d*n) jeśli n jest nieparzyste.
Funkcja numpy.fft.fftshift(A) przestawia wektor wyjściowy fft i wektor częstości, tak aby częstość zero wypadała w środku. Zastosowanie funkcji numpy.fft.ifftshift(A) odwraca działanie numpy.fft.fftshift(.).
Zadanie 1
Multipleksowany sygnał binarny w pliku ma następujące parametry:
- częstość próbkowania: 1024 hz
- liczba kanałów: 26
- typ liczb: double (8 bajtów)
Wczytaj pierwsze 10 sekund z pierwszych 5 kanałów, i zapisz je w pliku tekstowym sygnal.txt, tak, że plik sygnal.txt będzie miał następującą postać:
ch1[0];ch2[0];ch3[0];ch4[0];ch5[0]
ch1[1];ch2[1]... ch5[1]
.
.
ch1[n];ch2[n]... ch5[n]
gdzie chi[j] to liczby całkowite.
Narysuj cztery wykresy — 4 sekundy sygnału z dwóch pierwszych oraz dwóch ostatnich kanałów, oraz jego widmo; na jednym rysunku, wykorzystując polecenie sublot. Rysunek powinien mieć następujący układ:
sygnal 1 widmo1
sygnal 2 widmo2
sygnal 3 widmo3
sygnal 4 widmo4
Wszystkie wykresy powinny być wyskalowane — osie rzędnych i odciętych wykresów przedstawiających sygnały powinny mieć takie same podziałki i wartości, tak samo osie rzędnych i odciętych na wykresach widma.
Przyjrzyj się uważniej sygnałowi, z poszczególnych kanałów. Co możesz o nim powiedzieć? Czym jest sygnał z pierwszego i drugiego kanału? A czym z ostatniego? Przedostatniego?
Jak długi (w sekundach) jest sygnał w pliku?
Zadanie 2
Obejrzyj następujący plik z danymi. Jego opis można znaleźć tu. Wybierz fragment sygnału z środka pliku i spróbuj narysować widmo na różne sposoby. Co zauważyłeś interesującego w sygnale?
Zadanie 3
Pobierz dane z jeden z plików z IV BCI Competition. Zapoznaj się z opisem tych danych. Napisz program, który:
- zwróci sygnał z danego kanału,
- zwroci listę wszystkich fragmentów sygnału odpowiadający danej klasie bodźca i danemu kanałowi,
- zwróci uśredniony sygnał odpowiadający danej klasie bodźca i danemu kanałowi (uśredniamy "triale"),
- narysuje i zapisze do pliku widma z triali odpowiadających danej klasie bodźca i danemu kanałowi (w nazwie pliku ma się znaleźć informacja o tym dla którego triala, której klasy i którego kanału jest to widmo),
- narysuje widmo po uśrednieniu widm ze wszystkich triali dla danej klasy bodźca.
Zapisywanie do pliku
Sposoby dostępu do plików - przypomnienie
- Korzystanie z obiektu file:
napis = "42"
f1 = open('plik_tekstowy', 'w') # 'w' oznacza, że będziemy pisać do pliku
f1.write(napis)
f1.close() # 'wypchnięcie' zmian do pliku przez jego zamknięcie
Zapisaliśmy do pliku napis "42".
Teraz chcemy zapisać do pliku liczbę 42. Aby zapisać do pliku coś innego niż napis, przy pomocy funkcji write(), musimy najpierw zrobić z tego czegoś napis. Z liczby całkowitej z zakresu 0-127, którą chcemy zapisać na 8 bitach, robimy napis używając funkcji chr().
Z bardziej skomplikowanych typów robimy napis korzystając z funkcji struck.pack() modul struct
- korzystanie z modułu numpy:
- do danych binarnych — fromfile
- do danych ASCII — loadtxt
- wczytywanie plików matlabowych — loadmat:
Podstawy o reprezentacji danych
- Liczby naturalne są zapisywane w postaci ciągu zer i jedynek oddających ich reprezentację binarną
- Liczby całkowite wymagają jeszcze określenia w jakiś sposób znaku, na przykład poprzez uznanie pierwszego bitu za znak. Dlatego też typy integer oraz unsigned integer mają różne zakresy wartości. Najczęściej stosowanym sposobem zapisu liczb ze znakiem jest tzw kod uzupełnieniowy
- "Ułamki" wymagają już reprezentacji zmiennoprzecinkowej. Reprezentacja zmiennoprzecinkowa przedstawia się następująco:
- [math]x = (-1)^S \cdot M \cdot 2^{E - \textrm{bias[/math] gdzie S to bit znaku; liczba jest ujemna, gdy bit znaku jest równy 1, w przeciwnej sytuacji ma on wartość 0.
Typowe wartości przesunięcia (bias) dla koprocesora x87 (występującego w procesorach x86) wynoszą:
- 127 (7FH) w formacie 32-bitowym
- 1023 (3FFH) w formacie 64-bitowym
- 16383 (3FFFH) w formacie 80-bitowym
- Kolejność w jakiej odczytujemy kolejne bajty i składamy w liczbę zależy od architektury danego komputera:
- little endian (spotykane także cienkokońcowość) to forma zapisu danych, w której mniej znaczący bajt (zwany też dolnym bajtem, low-order byte) umieszczony jest jako pierwszy. Procesory, które używają formy little endian, to między innymi wszystkie z rodziny x86. Jest ona odwrotna do używanego na co dzień sposobu zapisu liczb. Procesor zapisujący 32-bitowe wartości w pamięci, przykładowo 4A3B2C1D pod adresem 100, umieszcza dane zajmując adresy od 100 do 103 w następującej kolejności:
100 | 101 | 102 | 103 | ||
... | 1D | 2C | 3B | 4A | ... |
- big endian (spotykane także grubokońcowość) to forma zapisu danych, w której najbardziej znaczący bajt (high-order byte) umieszczony jest jako pierwszy. Procesory, które używają formy big endian, to między innymi SPARC, PowerPC 970, IBM System/360, Siemens SIMATIC S7. Jest ona analogiczna do używanego na co dzień sposobu zapisu liczb. Procesor zapisujący 32-bitowe wartości w pamięci, przykładowo 4A3B2C1D pod adresem 100, umieszcza dane, zajmując adresy od 100 do 103 w następującej kolejności:
100 | 101 | 102 | 103 | ||
... | 4A | 3B | 2C | 1D | ... |
- Przyjrzyjmy się reprezentacji liczb:
>>> y = struct.pack("i", 1)
>>> y
'\x01\x00\x00\x00'
Napis 0x.. oznacza zapis szesnastkowy jednego bajtu. Zapis 0xx_{1}x_{2} = x_{2} * 16^0 + x_{1} * 16. x_{i} należy do zbioru {0,1,...,9,a,b,...,f}, gdzie przyjmujemy:
- a = 10
- b = 11
- c = 12
- d = 13
- e = 14
- f = 15
Widzimy, że liczba 1 została zapisana na 4 bajtach = 32 bitach. Pierwszy bajt, bajt o najniższym adresie, jest najmniej znaczący, jak przystało na architekturę little endian — gdybyśmy chcięli ją zapisać na kartce na 32 bitach, zrobilibyśmy to odwrotnie — pierwsze 31 bitów to byłyby zera, dopiero ostatni byłby jedynką. Tak byłoby też w systemie Big Endian.
>>> import struct
>>> x = struct.pack("i", -1)
>>> x
'\xff\xff\xff\xff'
Przy zapisaniu liczby -1 musimy już w jakiś sposób uwzględnić znak. Widzimy, że nie jest tu stosowane zapisywanie znaku na pierwszym bicie, lecz kod uzupełnieniowy
>>> z = struct.pack("h", 1)
>>> z
'\x01\x00'
"h" oznacza typ short, zajmujący 16 bitów.
>>> chr(1)
'\x01'
chr() zapisuje liczbę na 8 bitach.
Zadania
- Zapoznaj się z działaniem funkcji chr(), ord(), struct.pack/unpack, numpy.fromfile, bin(), int()
- Utwórz plik file_string, do którego zapiszesz liczbę 42 poleceniem f.write('42'), oraz plik file_bin, do ktorego zapiszesz liczbę 42 jako integer korzystając z polecenia struct.pack("i",42). Otwórz plik edytorem tekstu. Jaka jest różnica i dlaczego?
- Zapisz liczbę 42 do pliku, tak, aby zajmowała 64 bity w formacie little endian
- Zapisz liczbę 42 do pliku, tak, aby zajmowała 64 bity w formacie big endian. Jaka będzie wartość takiej liczby zinterpretowanej jako little endian?
- Zapisz liczbę 42 do pliku jako 64 bitową liczbę zmiennoprzecinkową.
- Zapisz do pliku liczbę 0.1 jako double, korzystając z polecenia struct.pack("d",0.1). Następnie otwierając plik do odczytu binarnego, odczytaj wartość: znaku, wykładnika i mantysy (tzn napisz funkcję, która zwraca krotkę (znak, wykładnik, mantysa)). (pierwszy bit to znak, kolejne 11 bitów to wykładnik, kolejne 52 bity to mantysa (Liczba zmiennoprzecinkowa))
- Utwórz plik o nazwie "binarny", otwórz go do zapisu i zapisz sekwnencję bajtów oznaczającą zdanie "Hello world". Psłuż się do pomocy tablicą ASCII z wikipedii. Otwórz plik edytorem tekstowym i zwróć uwagę na to, że nie widzisz tekstu, tylko "krzaczki". Otwórz następnie powyższy plik do odczytu w formie binarnej, i zobacz co się dzieje, jak korzystasz z funkcji fromfile podając jej różne typy (np. int8, int16, int32).
Rysowanie plansz — przygotowanie bodźców na Pracownię Sygnałów Biologicznych
Na zajeciach z pracowni będziecie przeprowadzać następujący eksperyment:
- pacjentowi są prezentowane "bodźce" — obrazki, z różnymi wzorami
- obrazki wyglądają następująco: ekran jest podzielony na 9 pól, część pól jest biała, część czarna.
- pacjent dostaje instrukcję, że ma zareagować na konkretny układ pól na ekranie — tzn. na przykład, gdy 3 górne pola są czarne, pozostałe natomiast białe
- pacjent dostaje do ręki przycisk, który jest podłączony do jednego z kanałów wzmacniacza. Dodatkowo, na kciuku i ręce ma przyczepione elektrody do zbierania sygnałów z mięśni. Pacjent ma reagować na umówioną planszę wciskając przycisk. Dodatkowo, reakcja będzie widoczna w sygnale z mięśnie (szczegóły zostaną omówione na pracowni)
Waszym zadaniem jest napisanie programu, który będzie prezentował bodźce, i zapisywał do pliku czas ich wystąpienia. Należy ustalić jeden wzór planszy "właściwej", na którą pacjent ma reagowac, oraz kilka "plansz" szumów. Plansze to układy pól białych i czarnych. Będą prezentowane w losowej kolejności. Gdy wyświetlana jest plansza "właściwa", na którą pacjent ma zareagować, należy zapisać czas systemowy do pliku. Program ma pobierac parametry:
- liczba sekund na ile ma być wyświetlana plansza
- liczba powtórzeń plansz (każda plansza ma być tyle samo razy wyświetlona, gdyby ten wymóg stwarzał problem, można z niego zrezygnować)
- nazwa pliku, do którego mają zostac zapisane momenty wystąpienia planszy właściwej
Rysować plansze mozna w dowolny sposób, w jaki umiecie. Proponujemy tu dwa sposoby:
- korzystając z biblioteki PyQt4. Poniżej fragment kodu wyjaśniający jak skorzystać z biblioteki PyQt4
#!/usr/bin/python
import sys, random
import time
from PyQt4 import QtGui, QtCore
class Colors(QtGui.QWidget):
def __init__(self, parent=None):
QtGui.QWidget.__init__(self, parent)
self.setGeometry(300, 300, 350, 280)
self.setWindowTitle('Colors')
self.timer = QtCore.QTimer(self)
self.connect(self.timer, QtCore.SIGNAL("timeout()"), self, QtCore.SLOT("update()"))
self.timer.start(10000) # to decyduje o tym co ile bedzie sie wykonywac metoda paintEvent
def paintEvent(self, event):
"""
Tu nalezy umiescic losowanie, ktora plansza ma sie rysowac
Ponizsze fragmenty rysujace prostokaty sluza tylko zeby sie z nich nauczyc jak rysowac prostokac o okreslonych wymiarach i kolorze.
mozna je przeniesc odpowiednio zmodyfikowane do odpowiednich funkcji
"""
paint = QtGui.QPainter()
paint.begin(self)
color = QtGui.QColor(0, 0, 0)
color.setNamedColor('#d4d4d4')
paint.setPen(color)
paint.setBrush(QtGui.QColor(255, 0, 0, 80))
paint.drawRect(10, 15, 90, 60)
paint.setBrush(QtGui.QColor(255, 0, 0, 160))
paint.drawRect(130, 15, 90, 60)
paint.setBrush(QtGui.QColor(255, 0, 0, 255))
paint.drawRect(250, 15, 90, 60)
paint.setBrush(QtGui.QColor(10, 163, 2, 55))
paint.drawRect(10, 105, 90, 60)
paint.setBrush(QtGui.QColor(160, 100, 0, 255))
paint.drawRect(130, 105, 90, 60)
paint.setBrush(QtGui.QColor(60, 100, 60, 255))
paint.drawRect(250, 105, 90, 60)
paint.setBrush(QtGui.QColor(50, 50, 50, 255))
paint.drawRect(10, 195, 90, 60)
paint.setBrush(QtGui.QColor(50, 150, 50, 255))
paint.drawRect(130, 195, 90, 60)
paint.setBrush(QtGui.QColor(223, 135, 19, 255))
paint.drawRect(250, 195, 90, 60)
paint.end()
app = QtGui.QApplication(sys.argv)
dt = Colors()
dt.show()
app.exec_()
- korzystając z funkcji subplot(numRows, numCols, plotNum), dzieląc ekran na 9 subplotów i zmieniając ich kolory.
Poniższy kawałek kodu pokazuje w jaki sposób można to zrobić:
import numpy as np
import matplotlib.pyplot as plt
numPlots = 9
numRows = 3
numCols = 3
plt.figure(figsize=(10,10))
for i in range(numPlots):
plt.subplot(numRows,numCols,i + 1, axisbg='y')
plt.show()
A tu jest wskazówka jak uzyskać dynamicznie zmieniające się subploty korzystając z matplotlib:
import numpy as np
import matplotlib.pyplot as plt
import time
numPlots = 9
numRows = 3
numCols = 3
plt.ion()
for j in range(5):
plt.clf()
for i in range(j):
plt.subplot(numRows,numCols,i + 1, axisbg='y')
plt.draw()
plt.clf()
time.sleep(2)
plt.show()
Przykładowym prostym podejściem do rozwiązania zadania, może być stworzenie kilku funkcji — każda odpowiada za rysowanie pojedynczej planszy — określonego układu pól białych i czarnych. Następnie korzystając z modułu random losujemy liczbę z przedziału [1,liczba_plansz], i w konstrukcji "if" wywołujemy odpowiednią funkcję.
Przydatne kawałki
- moduł random
# tasowanie listy w miejscu
>>> a = 10 * [0] + 10*[1]
>>> a
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
>>> import random
>>> random.shuffle(a)
>>> a
[1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0]
# losowanie liczby z przedziału
>>> random.randint(5,10)
6
- moduł sys:
- poleceniem sys.exit(0) możemy zakończyć program, kiedy już wyrysujemy wszystkie plansze
- kiedy odpalamy program polecenim: python moj_program,py arg1 arg2 arg3
to wartości arg1, arg2 i arg3 znajdują się odpowiednio na sys.argv[1], sys.argv[2], sys.argv[3] -- należy z tego skorzystać do podania liczby powtórzeń plansz oraz nazwy pliku
- Przy zapisie timestampu do pliku f.write(repr(time.time)) daje nam większą dokładność niż f.write(str(time.time()))
- Własne funkcje nalezy definiować wewnątrz klasy Color, definiując taką funkcję wewnątrz klasy należy pamiętać, że pierwszym parametrem jest zawsze self:
def moja_funkcja(self, pozostale_argumenty)
wywołując ją wewnątrz tej klasy, należy pisać: self.moja_funkcja(pozostale_argumenty)
Skrócony przepis -- jeśli ktoś nie ma pomysłu, jak to zrobić, można się posłużyć tym schematem
Dla ułatwienia opis możliwej realizacji programu (używając wersji z PyQt):
- tworzymy zmienną klasową self.ilosc_plansz i wpisujemy "na sztywno" jakąć ilość plansz
- tworzymy zmienną klasową self.sekwencja, i umieszczamy na niej przetasowaną sekwencję plansz do pokazywania
- tworzymy zmienną klasową self.licznik -- będzie mówiła, którą planszę z sekwencji należy teraz narysować
- tworzymy obok funkcji paintEvent naszą własną funkcję def rysuj(self, nr_planszy)
- w tej funkcji robimy wielką konstrukcję "if",
if nr_plansza == 1:
9 instrukcji rysowania prostokatow o odpowiednich kolorach w odpowiednich miejscach -- tak by tworzyły planszę 1
elif nr_planszy == 2:
9 instrukcji rysowania prostokątów tworzących planszę nr 2
itd
- funkcja paintEvent wywołuje się sama co określony czas, z jakim ją inicjowaliśmy. Zatem wystarczy wstawić w jej wnętrzu wybieranie kolejnego elementu z sekwencji plansz, i wywolanie naszej funkcji self.rysuj(nr_planszy).
trick jesli nie dziala close()
import numpy as np
#import matplotlib.pyplot as plt
import time
import matplotlib as mpl
mpl.rcParams['backend'] = "Qt4Agg"
import matplotlib.pyplot as plt
numPlots = 9
numRows = 3
numCols = 3
plt.ion()
plt.clf()
for i in range(9):
plt.subplot(numRows,numCols,i + 1, axisbg='y')
plt.draw()
time.sleep(2)
plt.close()
time.sleep(2)
print "closed"