Jak posortować moje łapy?

121

W poprzednim pytaniu otrzymałem doskonałą odpowiedź, która pomogła mi wykryć, gdzie łapa uderzyła w płytę dociskową, ale teraz staram się powiązać te wyniki z odpowiadającymi im łapami:

tekst alternatywny

Ręcznie zanotowałem łapy (RF = prawy przód, RH = prawa tylna, LF = lewy przód, LH = lewa tylna).

Jak widać wyraźnie powtarzający się wzór powraca w prawie każdym pomiarze. Oto link do prezentacji 6 prób, które zostały ręcznie opisane.

Moją początkową myślą było użycie heurystyki do sortowania, na przykład:

  • Istnieje ~ 60-40% stosunek wagi między przednią i tylną łapą;
  • Tylne łapy mają ogólnie mniejszą powierzchnię;
  • Łapy są (często) przestrzennie podzielone na lewą i prawą.

Jestem jednak trochę sceptyczny co do mojej heurystyki, ponieważ zawiodą one, gdy tylko napotkam odmianę, o której nie pomyślałem. Nie poradzą sobie też z pomiarami kulawych psów, które zapewne rządzą się swoimi prawami.

Co więcej, adnotacja sugerowana przez Joe czasami jest pomieszana i nie uwzględnia tego, jak wygląda łapa.

Opierając się na odpowiedziach, które otrzymałem na moje pytanie dotyczące wykrywania pików w łapie , mam nadzieję, że istnieją bardziej zaawansowane rozwiązania do sortowania łap. Zwłaszcza, że ​​rozkład nacisku i jego progresja są różne dla każdej oddzielnej łapy, prawie jak odcisk palca. Mam nadzieję, że istnieje metoda, która może wykorzystać to do grupowania moich łap, zamiast po prostu sortować je w kolejności występowania.

tekst alternatywny

Dlatego szukam lepszego sposobu sortowania wyników za pomocą odpowiedniej łapy.

Dla każdego, kto sprostał wyzwaniu, przygotowałem słownik ze wszystkimi pokrojonymi tablicami, które zawierają dane dotyczące nacisku każdej łapy (pogrupowane według pomiaru) i wycinek, który opisuje ich lokalizację (położenie na talerzu i w czasie).

Dla jasności: walk_sliced_data to słownik zawierający ['ser_3', 'ser_2', 'sel_1', 'sel_2', 'ser_1', 'sel_3'], które są nazwami pomiarów. Każdy pomiar zawiera inny słownik, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] (przykład z „sel_1”), który reprezentuje wyodrębnione wpływy.

Należy również pamiętać, że „fałszywe” wpływy, takie jak częściowe pomiary łapy (w czasie lub przestrzeni), można zignorować. Są przydatne tylko dlatego, że mogą pomóc w rozpoznaniu wzoru, ale nie będą analizowane.

A dla wszystkich zainteresowanych prowadzę bloga ze wszystkimi aktualizacjami dotyczącymi projektu!

Ivo Flipse
źródło
1
Tak, podejście, którego używałem, nie do końca działa. Aby rozwinąć, podejście, którego użyłem, polega na uporządkowaniu uderzeń i założeniu, że pierwsza dotykana łapa jest tym samym, co piąta, której dotykam, i tak dalej. (tj. zamów uderzenia i użyj modulo 4). Problem w tym, że czasami tylne łapy odbijają się od maty sensorycznej po tym, jak pierwsza łapa dotknie ziemi. W takim przypadku pierwsza łapa uderzająca pasuje do łapy czwartej lub trzeciej uderzającej. Mam nadzieję, że to ma jakiś sens.
Joe Kington
1
Czy zinterpretowałbym poprawnie obrazy, gdyby jeden palec każdej tylnej stopy wywierał znacznie mniejszy nacisk niż reszta? Okazuje się również, że palec u nogi jest zawsze skierowany „do środka”, czyli w kierunku środka masy psa. Czy mógłbyś włączyć to jako heurystykę?
Thomas Langston,
1
Przyznaję, że moje ograniczone umiejętności przetwarzania obrazu są nieco zardzewiałe, ale czy łatwo jest wziąć najmniej stromy gradient dużej środkowej opuszki każdej łapy? Wygląda na to, że kąt najmniejszej stromości bardzo pomógłby (ręcznie rysowany przykład łap opublikowany: imgur.com/y2wBC imgur.com/yVqVU imgur.com/yehOc imgur.com/q0tcD )
user470379
Czy mógłbyś uprzejmie wyjaśnić, w jaki sposób walk_sliced_datauporządkowane są dane ? Widzę słownik słowników tablic 3D. Jeśli naprawię trzeci wymiar i narysuję pierwsze dwa jako obraz, wydaje mi się, że widzę łapy.
Steve Tjoa
@ Thomas, tak, każda łapa jest wyraźnie obciążona w inny sposób. Wiem, co bym chciał, żeby program robił, ale nie mam pojęcia, jak go zaprogramować ... @Steve, dodałem wyjaśnienie na dole :-)
Ivo Flipse

Odpowiedzi:

123

W porządku! W końcu udało mi się uzyskać konsekwentną pracę! Ten problem wciągnął mnie na kilka dni ... Zabawne rzeczy! Przepraszam za długość tej odpowiedzi, ale muszę trochę rozwinąć niektóre rzeczy ... (Chociaż mogę ustanowić rekord dla najdłuższej odpowiedzi na temat przepełnienia stosu, która nie jest spamem!)

Na marginesie, używam pełnego zbioru danych, do którego Ivo podał link w swoim pierwotnym pytaniu . Jest to seria plików rar (po jednym na psa), z których każdy zawiera kilka różnych przebiegów eksperymentów przechowywanych jako tablice ascii. Zamiast próbować kopiować i wklejać przykłady samodzielnego kodu do tego pytania, oto repozytorium bitbucket mercurial z pełnym, samodzielnym kodem. Możesz go sklonować za pomocą

hg clone https://[email protected]/joferkington/paw-analysis


Przegląd

Jak zauważyłeś w swoim pytaniu, zasadniczo istnieją dwa sposoby podejścia do problemu. Właściwie zamierzam używać obu na różne sposoby.

  1. Użyj kolejności (czasowej i przestrzennej) uderzeń łap, aby określić, która łapa jest która.
  2. Spróbuj zidentyfikować „odcisk łapy” wyłącznie na podstawie jego kształtu.

Zasadniczo pierwsza metoda działa z łapami psa zgodnie z trapezoidalnym wzorem pokazanym w pytaniu Ivo powyżej, ale zawodzi, gdy łapy nie podążają za tym wzorem. Dość łatwo jest programowo wykryć, kiedy nie działa.

Dlatego możemy wykorzystać pomiary, w których działało, do zbudowania zestawu danych treningowych (z ~ 2000 uderzeń łap od ~ 30 różnych psów), aby rozpoznać, która łapa jest która, a problem sprowadza się do nadzorowanej klasyfikacji (z kilkoma dodatkowymi zmarszczkami. .. Rozpoznawanie obrazu jest nieco trudniejsze niż „normalny” nadzorowany problem klasyfikacji).


Analiza wzorców

Aby rozwinąć pierwszą metodę, kiedy pies normalnie chodzi (nie biegnie!) (Czego niektóre z tych psów mogą nie być), spodziewamy się uderzenia łap w kolejności: przednia lewa, tylna prawa, przednia prawa, tylna lewa , Przednia lewa itd. Wzór może zaczynać się od przedniej lewej lub przedniej prawej łapy.

Gdyby tak było zawsze, moglibyśmy po prostu posortować uderzenia według czasu początkowego kontaktu i użyć modulo 4, aby pogrupować je według łap.

Normalna sekwencja uderzeń

Jednak nawet jeśli wszystko jest „normalne”, to nie działa. Wynika to z trapezowego kształtu wzoru. Tylna łapa przestrzennie opada za poprzednią przednią łapę.

Dlatego uderzenie tylnej łapy po początkowym uderzeniu przedniej łapy często spada z płytki czujnika i nie jest rejestrowane. Podobnie, ostatnie uderzenie łapy często nie jest kolejną łapą w sekwencji, ponieważ uderzenie łapy, zanim zdarzyło się to z płytki czujnika, nie zostało zarejestrowane.

Nieudana tylna łapa

Niemniej jednak, możemy użyć kształtu wzoru uderzenia łapy, aby określić, kiedy to się stało i czy zaczęliśmy od lewej czy prawej przedniej łapy. (Właściwie to ignoruję tutaj problemy z ostatnim wpływem. Jednak dodanie go nie jest trudne.)

def group_paws(data_slices, time):   
    # Sort slices by initial contact time
    data_slices.sort(key=lambda s: s[-1].start)

    # Get the centroid for each paw impact...
    paw_coords = []
    for x,y,z in data_slices:
        paw_coords.append([(item.stop + item.start) / 2.0 for item in (x,y)])
    paw_coords = np.array(paw_coords)

    # Make a vector between each sucessive impact...
    dx, dy = np.diff(paw_coords, axis=0).T

    #-- Group paws -------------------------------------------
    paw_code = {0:'LF', 1:'RH', 2:'RF', 3:'LH'}
    paw_number = np.arange(len(paw_coords))

    # Did we miss the hind paw impact after the first 
    # front paw impact? If so, first dx will be positive...
    if dx[0] > 0: 
        paw_number[1:] += 1

    # Are we starting with the left or right front paw...
    # We assume we're starting with the left, and check dy[0].
    # If dy[0] > 0 (i.e. the next paw impacts to the left), then
    # it's actually the right front paw, instead of the left.
    if dy[0] > 0: # Right front paw impact...
        paw_number += 2

    # Now we can determine the paw with a simple modulo 4..
    paw_codes = paw_number % 4
    paw_labels = [paw_code[code] for code in paw_codes]

    return paw_labels

Mimo wszystko często nie działa poprawnie. Wydaje się, że wiele psów w pełnym zbiorze danych biegnie, a uderzenia łap nie są zgodne w tej samej kolejności czasowej, co podczas spaceru. (A może pies ma po prostu poważne problemy z biodrami ...)

Nieprawidłowa sekwencja uderzeń

Na szczęście nadal możemy programowo wykrywać, czy uderzenia łap są zgodne z naszym oczekiwanym wzorcem przestrzennym:

def paw_pattern_problems(paw_labels, dx, dy):
    """Check whether or not the label sequence "paw_labels" conforms to our
    expected spatial pattern of paw impacts. "paw_labels" should be a sequence
    of the strings: "LH", "RH", "LF", "RF" corresponding to the different paws"""
    # Check for problems... (This could be written a _lot_ more cleanly...)
    problems = False
    last = paw_labels[0]
    for paw, dy, dx in zip(paw_labels[1:], dy, dx):
        # Going from a left paw to a right, dy should be negative
        if last.startswith('L') and paw.startswith('R') and (dy > 0):
            problems = True
            break
        # Going from a right paw to a left, dy should be positive
        if last.startswith('R') and paw.startswith('L') and (dy < 0):
            problems = True
            break
        # Going from a front paw to a hind paw, dx should be negative
        if last.endswith('F') and paw.endswith('H') and (dx > 0):
            problems = True
            break
        # Going from a hind paw to a front paw, dx should be positive
        if last.endswith('H') and paw.endswith('F') and (dx < 0):
            problems = True
            break
        last = paw
    return problems

Dlatego nawet jeśli prosta klasyfikacja przestrzenna nie działa przez cały czas, możemy określić, kiedy działa z rozsądną pewnością.

Zestaw danych treningowych

Z klasyfikacji opartych na wzorcach, gdzie działało to poprawnie, możemy zbudować bardzo duży zestaw danych treningowych prawidłowo sklasyfikowanych łap (~ 2400 uderzeń łap z 32 różnych psów!).

Możemy teraz zacząć przyglądać się, jak wygląda „przeciętna” przednia lewa łapa.

Aby to zrobić, potrzebujemy pewnego rodzaju „metryki łapy”, która ma taką samą wymiarowość dla każdego psa. (W pełnym zestawie danych znajdują się zarówno bardzo duże, jak i bardzo małe psy!) Odcisk łapy irlandzkiego elkhounda będzie zarówno znacznie szerszy, jak i znacznie „cięższy” niż odcisk łapy pudla zabawkowego. Musimy przeskalować każdy odcisk łapy, aby a) miał taką samą liczbę pikseli oraz b) wartości nacisku były znormalizowane. Aby to zrobić, ponownie próbowałem każdy odcisk łapy na siatkę 20x20 i przeskalowałem wartości nacisku w oparciu o maksymalną, minimalną i średnią wartość nacisku dla uderzenia łapy.

def paw_image(paw):
    from scipy.ndimage import map_coordinates
    ny, nx = paw.shape

    # Trim off any "blank" edges around the paw...
    mask = paw > 0.01 * paw.max()
    y, x = np.mgrid[:ny, :nx]
    ymin, ymax = y[mask].min(), y[mask].max()
    xmin, xmax = x[mask].min(), x[mask].max()

    # Make a 20x20 grid to resample the paw pressure values onto
    numx, numy = 20, 20
    xi = np.linspace(xmin, xmax, numx)
    yi = np.linspace(ymin, ymax, numy)
    xi, yi = np.meshgrid(xi, yi)  

    # Resample the values onto the 20x20 grid
    coords = np.vstack([yi.flatten(), xi.flatten()])
    zi = map_coordinates(paw, coords)
    zi = zi.reshape((numy, numx))

    # Rescale the pressure values
    zi -= zi.min()
    zi /= zi.max()
    zi -= zi.mean() #<- Helps distinguish front from hind paws...
    return zi

Po tym wszystkim, możemy wreszcie przyjrzeć się, jak wygląda przeciętna lewa przednia, tylna prawa itd. Łapa. Zwróć uwagę, że jest to uśrednione dla> 30 psów o bardzo różnych rozmiarach i wydaje się, że uzyskujemy spójne wyniki!

Przeciętne łapy

Jednak zanim przeprowadzimy na nich jakąkolwiek analizę, musimy odjąć średnią (średnią łapę dla wszystkich nóg wszystkich psów).

Średnia łapa

Teraz możemy przeanalizować różnice od średniej, które są nieco łatwiejsze do rozpoznania:

Różnicowe łapy

Rozpoznawanie łapy na podstawie obrazu

Ok ... Wreszcie mamy zestaw wzorców, do których możemy zacząć dopasowywać łapy. Każda łapa może być traktowana jako 400-wymiarowy wektor (zwracany przez paw_imagefunkcję), który można porównać do tych czterech 400-wymiarowych wektorów.

Niestety, jeśli użyjemy tylko „normalnego” nadzorowanego algorytmu klasyfikacji (tj. Stwierdzimy, który z 4 wzorców jest najbliższy danemu odciskowi łapy przy użyciu prostej odległości), nie działa on konsekwentnie. W rzeczywistości nie jest to dużo lepsze niż przypadkowa szansa na zestawie danych treningowych.

Jest to częsty problem w rozpoznawaniu obrazu. Ze względu na dużą wymiarowość danych wejściowych i nieco „rozmyty” charakter obrazów (tj. Sąsiednie piksele mają wysoką kowariancję), samo spojrzenie na różnicę obrazu z obrazu szablonu nie daje bardzo dobrego pomiaru podobieństwo ich kształtów.

Łapy własne

Aby obejść ten problem, musimy zbudować zestaw „własnych łap” (tak jak „własne twarze” w rozpoznawaniu twarzy) i opisać każdy odcisk łapy jako kombinację tych własnych łap. Jest to identyczne z analizą głównych komponentów i zasadniczo zapewnia sposób na zmniejszenie wymiarowości naszych danych, dzięki czemu odległość jest dobrą miarą kształtu.

Ponieważ mamy więcej obrazów szkoleniowych niż wymiarów (2400 vs 400), nie ma potrzeby robienia „wymyślnej” algebry liniowej dla szybkości. Możemy pracować bezpośrednio z macierzą kowariancji zbioru danych uczących:

def make_eigenpaws(paw_data):
    """Creates a set of eigenpaws based on paw_data.
    paw_data is a numdata by numdimensions matrix of all of the observations."""
    average_paw = paw_data.mean(axis=0)
    paw_data -= average_paw

    # Determine the eigenvectors of the covariance matrix of the data
    cov = np.cov(paw_data.T)
    eigvals, eigvecs = np.linalg.eig(cov)

    # Sort the eigenvectors by ascending eigenvalue (largest is last)
    eig_idx = np.argsort(eigvals)
    sorted_eigvecs = eigvecs[:,eig_idx]
    sorted_eigvals = eigvals[:,eig_idx]

    # Now choose a cutoff number of eigenvectors to use 
    # (50 seems to work well, but it's arbirtrary...
    num_basis_vecs = 50
    basis_vecs = sorted_eigvecs[:,-num_basis_vecs:]

    return basis_vecs

To basis_vecssą „własne łapy”.

Łapy własne

Aby ich użyć, po prostu kropkujemy (tj. Mnożenie macierzy) każdy obraz łapy (jako wektor o 400 wymiarach, a nie obraz 20x20) wektorami bazowymi. To daje nam 50-wymiarowy wektor (jeden element na wektor bazowy), którego możemy użyć do sklasyfikowania obrazu. Zamiast porównywać obraz 20x20 z obrazem 20x20 każdej łapy „szablonu”, porównujemy 50-wymiarowy, transformowany obraz z każdą 50-wymiarową transformowaną łapą szablonu. Jest to znacznie mniej wrażliwe na małe różnice w dokładnym ustawieniu każdego palca itp., I zasadniczo ogranicza wymiarowość problemu tylko do odpowiednich wymiarów.

Klasyfikacja łapy na podstawie własnej łapy

Teraz możemy po prostu użyć odległości między 50-wymiarowymi wektorami a wektorami „szablonowymi” dla każdej nogi, aby sklasyfikować, która łapa jest którą:

codebook = np.load('codebook.npy') # Template vectors for each paw
average_paw = np.load('average_paw.npy')
basis_stds = np.load('basis_stds.npy') # Needed to "whiten" the dataset...
basis_vecs = np.load('basis_vecs.npy')
paw_code = {0:'LF', 1:'RH', 2:'RF', 3:'LH'}
def classify(paw):
    paw = paw.flatten()
    paw -= average_paw
    scores = paw.dot(basis_vecs) / basis_stds
    diff = codebook - scores
    diff *= diff
    diff = np.sqrt(diff.sum(axis=1))
    return paw_code[diff.argmin()]

Oto niektóre wyniki: tekst alternatywny tekst alternatywny tekst alternatywny

Pozostałe problemy

Nadal są pewne problemy, szczególnie w przypadku psów zbyt małych, aby zrobić wyraźny odcisk łapy ... (Działa najlepiej w przypadku dużych psów, ponieważ palce są wyraźniej oddzielone przy rozdzielczości czujnika). Ponadto częściowe odciski łap nie są rozpoznawane w tym przypadku systemu, podczas gdy mogą być z systemem opartym na wzorze trapezowym.

Jednakże, ponieważ analiza łapy własnej z natury wykorzystuje metrykę odległości, możemy sklasyfikować łapy w obie strony i wrócić do systemu opartego na wzorze trapezoidalnym, gdy najmniejsza odległość analizy łapy własnej od „książki kodów” przekracza pewien próg. Jednak jeszcze tego nie zaimplementowałem.

Uff ... To było długo! Mam głowę do Ivo za takie zabawne pytanie!

Joe Kington
źródło
2
Świetna odpowiedź. Próbowałem też zastosować metodę własnej łapy, ale nie byłem tak wytrwały jak ty. Jedynym problemem, który widzę, jest rejestracja łapy, tj. Rejestracja twarzy polega na rozpoznawaniu twarzy. Czy napotkałeś jakieś problemy w normalizacji położenia i rotacji każdej łapy? Jeśli tak, to być może łapa może zostać wstępnie przetworzona do jakiejś niezmiennej cechy translacji i rotacji przed wykonaniem PCA.
Steve Tjoa
2
@Steve, nie próbowałem ich obracać, chociaż rozmawiałem z Joe o tym, jak jeszcze je ulepszyć. Jednak, aby na razie zakończyć projekt, ręcznie oznaczyłem wszystkie łapy adnotacjami, aby móc go zakończyć. Na szczęście pozwala nam to również tworzyć różne zestawy treningowe, aby rozpoznawanie było bardziej czułe. Do obracania łap planowałem użyć palców u nóg, ale jak możesz przeczytać na moim blogu, to nie jest tak proste, jak moje pierwsze pytanie sprawiło, że wyglądało ...
Ivo Flipse
@Basic yeah Przerzuciłem się na hosting własnej witryny i przeniosłem całą zawartość Wordpressa, ale nie mogłem już edytować tutaj mojego komentarza. Powinieneś być w stanie je znaleźć tutaj: flipserd.com/blog/ivoflipse/post/improving-the-paw-detection
Ivo Flipse
4

Korzystając z informacji wyłącznie na podstawie czasu trwania, myślę, że można zastosować techniki modelowania kinematyki; mianowicie kinematyka odwrotna . W połączeniu z orientacją, długością, czasem trwania i całkowitą wagą daje to pewien poziom okresowości, co, mam nadzieję, mogłoby być pierwszym krokiem do rozwiązania problemu „sortowania łap”.

Wszystkie te dane mogą zostać użyte do stworzenia listy ograniczonych wielokątów (lub krotek), których można by użyć do sortowania według wielkości kroku, a następnie według łap [indeks].

Lam Chau
źródło
2

Czy możesz poprosić technika, który przeprowadzi test, ręcznie wprowadzić pierwszą łapę (lub dwie pierwsze)? Proces może wyglądać następująco:

  • Pokaż technikę kolejność kroków obrazka i zażądaj, aby opisali pierwszą łapę.
  • Oznacz pozostałe łapy w oparciu o pierwszą łapę i pozwól technologii na wprowadzenie poprawek lub powtórz test. Pozwala to na kulawe lub trójnożne psy.
Jamie Ide
źródło
Właściwie mam adnotacje do pierwszych łap, chociaż nie są one bezbłędne. Jednak pierwsza łapa jest zawsze przednią i nie pomogłaby mi rozdzielić tylne łapy. Co więcej, kolejność nie jest idealna, jak wspomniał Joe, ponieważ wymaga to, aby oba przednie strony dotykały płyty na początku.
Ivo Flipse,
Adnotacje byłyby przydatne podczas korzystania z rozpoznawania obrazu, ponieważ z 24 pomiarów, które mam, co najmniej 24 łapy byłyby już opisane. Jeśli następnie zostałyby zgrupowane w 4 grupy, dwie z nich powinny zawierać rozsądną ilość obu przednich łap, wystarczającą, aby algorytm był dość pewny zgrupowania.
Ivo Flipse,
O ile nie czytam ich niepoprawnie, połączone próby z adnotacjami pokazują, że tylna łapa dotyka jako pierwsza w 4 z 6 prób.
Jamie Ide
Ach, miałem na myśli czas. Jeśli przelecisz pilnik, przednia łapa powinna zawsze jako pierwsza dotykać płytki.
Ivo Flipse