Jak mogę poprawić wykrywanie łap?


198

Po moim poprzednim pytaniu dotyczącym znalezienia palców u każdej łapy , zacząłem ładować inne pomiary, aby zobaczyć, jak by to wytrzymało. Niestety szybko napotkałem problem z jednym z poprzednich kroków: rozpoznaniem łap.

Widzisz, mój dowód koncepcji w zasadzie wziął maksymalne ciśnienie każdego czujnika w czasie i zaczął szukać sumy każdego rzędu, dopóki się nie dowie! = 0,0. Następnie robi to samo dla kolumn i gdy tylko znajdzie więcej niż 2 wiersze, które są ponownie zerowe. Przechowuje minimalne i maksymalne wartości wierszy i kolumn w jakimś indeksie.

alternatywny tekst

Jak widać na rysunku, w większości przypadków działa to całkiem dobrze. Istnieje jednak wiele wad tego podejścia (poza byciem bardzo prymitywnym):

  • Ludzie mogą mieć „puste stopy”, co oznacza, że ​​w samym śladzie jest kilka pustych rzędów. Ponieważ obawiałem się, że to może się zdarzyć także w przypadku (dużych) psów, odciąłem łapę co najmniej 2 lub 3 puste rzędy.

    Stwarza to problem, jeśli inny kontakt zostanie nawiązany w innej kolumnie, zanim osiągnie kilka pustych wierszy, rozszerzając w ten sposób obszar. Myślę, że mógłbym porównać kolumny i sprawdzić, czy przekraczają pewną wartość, muszą to być osobne łapy.

  • Problem pogarsza się, gdy pies jest bardzo mały lub chodzi w wyższym tempie. To, co się dzieje, polega na tym, że palce przednich łap wciąż się stykają, podczas gdy palce tylnych łap zaczynają się stykać w tym samym obszarze co przednia łapa!

    Za pomocą mojego prostego skryptu nie będzie w stanie podzielić tych dwóch, ponieważ musiałby określić, które klatki tego obszaru należą do której łapy, podczas gdy obecnie musiałbym jedynie patrzeć na maksymalne wartości we wszystkich klatkach.

Przykłady sytuacji, w których zaczyna się źle:

alternatywny tekst alternatywny tekst

Więc teraz szukam lepszego sposobu rozpoznawania i rozdzielania łap (po czym przejdę do problemu decydowania, która to łapa!).

Aktualizacja:

Majstrowałem, żeby zaimplementować odpowiedź Joe (niesamowite!), Ale mam trudności z wyodrębnieniem rzeczywistych danych łap z moich plików.

alternatywny tekst

Coded_paws pokazuje mi wszystkie różne łapy po zastosowaniu do obrazu maksymalnego nacisku (patrz wyżej). Jednak rozwiązanie obejmuje każdą ramkę (aby oddzielić nakładające się łapy) i ustawia cztery atrybuty prostokąta, takie jak współrzędne lub wysokość / szerokość.

Nie mogę wymyślić, jak wziąć te atrybuty i zapisać je w jakiejś zmiennej, którą mogę zastosować do danych pomiarowych. Ponieważ muszę wiedzieć dla każdej łapy, jakie jest jej położenie, podczas których ramek i połączyć ją z tą łapą (przednia / tylna, lewa / prawa).

Jak więc użyć atrybutów Prostokąty, aby wyodrębnić te wartości dla każdej łapy?

Pomiary, których użyłem w konfiguracji pytania, mam w moim publicznym folderze Dropbox ( przykład 1 , przykład 2 , przykład 3 ). Dla wszystkich zainteresowanych założyłem również blog, aby informować Cię na bieżąco :-)


Wygląda na to, że musiałbyś odwrócić się od algorytmu wiersza / kolumny i ograniczasz przydatne informacje.
Tamara Wijsman,

12
Łał! Oprogramowanie sterujące Cat?
alxx

To są dane psów właściwie @alxx ;-) Ale tak, zostaną wykorzystane do ich zdiagnozowania!
Ivo Flipse,

4
Czemu? (nieważne,
fajniej jest

Odpowiedzi:


358

Jeśli jesteś po prostu chcąc (pół) regiony sąsiadujące, nie ma już łatwa implementacja w Pythonie: scipy „s ndimage.morphology moduł. Jest to dość powszechna operacja morfologii obrazu .


Zasadniczo masz 5 kroków:

def find_paws(data, smooth_radius=5, threshold=0.0001):
    data = sp.ndimage.uniform_filter(data, smooth_radius)
    thresh = data > threshold
    filled = sp.ndimage.morphology.binary_fill_holes(thresh)
    coded_paws, num_paws = sp.ndimage.label(filled)
    data_slices = sp.ndimage.find_objects(coded_paws)
    return object_slices
  1. Rozmyj nieco dane wejściowe, aby upewnić się, że łapy mają ciągły ślad. (Bardziej wydajne byłoby po prostu użycie większego jądra ( structurekwarg do różnych scipy.ndimage.morphologyfunkcji), ale z jakiegoś powodu to nie działa poprawnie ...)

  2. Przekrocz próg tablicy, aby uzyskać boolowską tablicę miejsc, w których ciśnienie przekracza pewną wartość progową (tj. thresh = data > value)

  3. Wypełnij wszelkie wewnętrzne otwory, aby uzyskać czystsze regiony ( filled = sp.ndimage.morphology.binary_fill_holes(thresh))

  4. Znajdź oddzielne sąsiadujące regiony ( coded_paws, num_paws = sp.ndimage.label(filled)). Zwraca tablicę z regionami kodowanymi przez liczbę (każdy region jest ciągłym obszarem unikalnej liczby całkowitej (1 do liczby łap) z zerami wszędzie indziej).

  5. Wyizoluj sąsiednie regiony za pomocą data_slices = sp.ndimage.find_objects(coded_paws). Zwraca listę krotek sliceobiektów, dzięki czemu można uzyskać region danych dla każdej łapy [data[x] for x in data_slices]. Zamiast tego narysujemy prostokąt na podstawie tych plasterków, co wymaga nieco więcej pracy.


Dwie poniższe animacje pokazują przykładowe dane „Nakładające się łapy” i „Zgrupowane łapy”. Ta metoda wydaje się działać idealnie. (I niezależnie od tego, co jest warte, działa to znacznie płynniej niż poniższe obrazy GIF na mojej maszynie, więc algorytm wykrywania łap jest dość szybki ...)

Nakładające się łapy Zgrupowane łapy


Oto pełny przykład (teraz ze znacznie bardziej szczegółowymi wyjaśnieniami). Zdecydowana większość z nich czyta dane wejściowe i tworzy animację. Rzeczywiste wykrywanie łap to tylko 5 linii kodu.

import numpy as np
import scipy as sp
import scipy.ndimage

import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle

def animate(input_filename):
    """Detects paws and animates the position and raw data of each frame
    in the input file"""
    # With matplotlib, it's much, much faster to just update the properties
    # of a display object than it is to create a new one, so we'll just update
    # the data and position of the same objects throughout this animation...

    infile = paw_file(input_filename)

    # Since we're making an animation with matplotlib, we need 
    # ion() instead of show()...
    plt.ion()
    fig = plt.figure()
    ax = fig.add_subplot(111)
    fig.suptitle(input_filename)

    # Make an image based on the first frame that we'll update later
    # (The first frame is never actually displayed)
    im = ax.imshow(infile.next()[1])

    # Make 4 rectangles that we can later move to the position of each paw
    rects = [Rectangle((0,0), 1,1, fc='none', ec='red') for i in range(4)]
    [ax.add_patch(rect) for rect in rects]

    title = ax.set_title('Time 0.0 ms')

    # Process and display each frame
    for time, frame in infile:
        paw_slices = find_paws(frame)

        # Hide any rectangles that might be visible
        [rect.set_visible(False) for rect in rects]

        # Set the position and size of a rectangle for each paw and display it
        for slice, rect in zip(paw_slices, rects):
            dy, dx = slice
            rect.set_xy((dx.start, dy.start))
            rect.set_width(dx.stop - dx.start + 1)
            rect.set_height(dy.stop - dy.start + 1)
            rect.set_visible(True)

        # Update the image data and title of the plot
        title.set_text('Time %0.2f ms' % time)
        im.set_data(frame)
        im.set_clim([frame.min(), frame.max()])
        fig.canvas.draw()

def find_paws(data, smooth_radius=5, threshold=0.0001):
    """Detects and isolates contiguous regions in the input array"""
    # Blur the input data a bit so the paws have a continous footprint 
    data = sp.ndimage.uniform_filter(data, smooth_radius)
    # Threshold the blurred data (this needs to be a bit > 0 due to the blur)
    thresh = data > threshold
    # Fill any interior holes in the paws to get cleaner regions...
    filled = sp.ndimage.morphology.binary_fill_holes(thresh)
    # Label each contiguous paw
    coded_paws, num_paws = sp.ndimage.label(filled)
    # Isolate the extent of each paw
    data_slices = sp.ndimage.find_objects(coded_paws)
    return data_slices

def paw_file(filename):
    """Returns a iterator that yields the time and data in each frame
    The infile is an ascii file of timesteps formatted similar to this:

    Frame 0 (0.00 ms)
    0.0 0.0 0.0
    0.0 0.0 0.0

    Frame 1 (0.53 ms)
    0.0 0.0 0.0
    0.0 0.0 0.0
    ...
    """
    with open(filename) as infile:
        while True:
            try:
                time, data = read_frame(infile)
                yield time, data
            except StopIteration:
                break

def read_frame(infile):
    """Reads a frame from the infile."""
    frame_header = infile.next().strip().split()
    time = float(frame_header[-2][1:])
    data = []
    while True:
        line = infile.next().strip().split()
        if line == []:
            break
        data.append(line)
    return time, np.array(data, dtype=np.float)

if __name__ == '__main__':
    animate('Overlapping paws.bin')
    animate('Grouped up paws.bin')
    animate('Normal measurement.bin')

Aktualizacja: Jeśli chodzi o określenie, która łapa ma kontakt z czujnikiem w danym momencie, najprostszym rozwiązaniem jest po prostu wykonanie tej samej analizy, ale wykorzystanie wszystkich danych jednocześnie. (tj. ułóż dane wejściowe w tablicy 3D i pracuj z nimi zamiast z pojedynczymi ramami czasowymi.) Ponieważ funkcje ndimage SciPy są przeznaczone do pracy z tablicami n-wymiarowymi, nie musimy modyfikować oryginalnej funkcji wyszukiwania łap w ogóle.

# This uses functions (and imports) in the previous code example!!
def paw_regions(infile):
    # Read in and stack all data together into a 3D array
    data, time = [], []
    for t, frame in paw_file(infile):
        time.append(t)
        data.append(frame)
    data = np.dstack(data)
    time = np.asarray(time)

    # Find and label the paw impacts
    data_slices, coded_paws = find_paws(data, smooth_radius=4)

    # Sort by time of initial paw impact... This way we can determine which
    # paws are which relative to the first paw with a simple modulo 4.
    # (Assuming a 4-legged dog, where all 4 paws contacted the sensor)
    data_slices.sort(key=lambda dat_slice: dat_slice[2].start)

    # Plot up a simple analysis
    fig = plt.figure()
    ax1 = fig.add_subplot(2,1,1)
    annotate_paw_prints(time, data, data_slices, ax=ax1)
    ax2 = fig.add_subplot(2,1,2)
    plot_paw_impacts(time, data_slices, ax=ax2)
    fig.suptitle(infile)

def plot_paw_impacts(time, data_slices, ax=None):
    if ax is None:
        ax = plt.gca()

    # Group impacts by paw...
    for i, dat_slice in enumerate(data_slices):
        dx, dy, dt = dat_slice
        paw = i%4 + 1
        # Draw a bar over the time interval where each paw is in contact
        ax.barh(bottom=paw, width=time[dt].ptp(), height=0.2, 
                left=time[dt].min(), align='center', color='red')
    ax.set_yticks(range(1, 5))
    ax.set_yticklabels(['Paw 1', 'Paw 2', 'Paw 3', 'Paw 4'])
    ax.set_xlabel('Time (ms) Since Beginning of Experiment')
    ax.yaxis.grid(True)
    ax.set_title('Periods of Paw Contact')

def annotate_paw_prints(time, data, data_slices, ax=None):
    if ax is None:
        ax = plt.gca()

    # Display all paw impacts (sum over time)
    ax.imshow(data.sum(axis=2).T)

    # Annotate each impact with which paw it is
    # (Relative to the first paw to hit the sensor)
    x, y = [], []
    for i, region in enumerate(data_slices):
        dx, dy, dz = region
        # Get x,y center of slice...
        x0 = 0.5 * (dx.start + dx.stop)
        y0 = 0.5 * (dy.start + dy.stop)
        x.append(x0); y.append(y0)

        # Annotate the paw impacts         
        ax.annotate('Paw %i' % (i%4 +1), (x0, y0),  
            color='red', ha='center', va='bottom')

    # Plot line connecting paw impacts
    ax.plot(x,y, '-wo')
    ax.axis('image')
    ax.set_title('Order of Steps')

alternatywny tekst


alternatywny tekst


alternatywny tekst


82
Nie potrafię nawet wyjaśnić, jak niesamowita jest twoja odpowiedź!
Ivo Flipse,

1
@Ivo: Tak, wolałbym jeszcze trochę głosować na Joe'ego :) ale czy powinienem zacząć nowe pytanie, a może @Joe, jeśli tak, odpowiedz tutaj? stackoverflow.com/questions/2546780/…
unutbu

2
Właśnie zrzuciłem plik .png i zrobiłem convert *.png output.gif. Na pewno wyobrażałem sobie, że wyobraźnia już wcześniej rzuciła moją maszynę na kolana, choć w tym przykładzie działała dobrze. W przeszłości używałem tego skryptu: svn.effbot.python-hosting.com/pil/Scripts/gifmaker.py, aby bezpośrednio napisać animowany gif z pythona bez zapisywania poszczególnych ramek. Mam nadzieję, że to pomaga! Podam przykład na wspomniane pytanie @unutbu.
Joe Kington,

1
Dzięki za informację, @Joe. Część mojego problemu zaniedbuje używać bbox_inches='tight'w plt.savefig, druga była niecierpliwość :)
unutbu

4
Święta krowa, muszę powiedzieć wow, jak wspaniała jest ta odpowiedź.
andersoj

4

Nie jestem ekspertem w wykrywaniu obrazów i nie znam Pythona, ale dam mu to ...

Aby wykryć pojedyncze łapy, należy najpierw wybrać wszystko z naciskiem większym niż jakiś niewielki próg, bardzo zbliżonym do braku nacisku. Każdy piksel / punkt powyżej tego powinien być „oznaczony”. Następnie każdy piksel sąsiadujący ze wszystkimi „zaznaczonymi” pikselami zostaje zaznaczony, a proces ten powtarza się kilka razy. Tworzą się całkowicie połączone masy, więc masz wyraźne obiekty. Następnie każdy „obiekt” ma minimalną i maksymalną wartość xiy, dzięki czemu obwiednia może być starannie zapakowana wokół nich.

Pseudo kod:

(MARK) ALL PIXELS ABOVE (0.5)

(MARK) ALL PIXELS (ADJACENT) TO (MARK) PIXELS

REPEAT (STEP 2) (5) TIMES

SEPARATE EACH TOTALLY CONNECTED MASS INTO A SINGLE OBJECT

MARK THE EDGES OF EACH OBJECT, AND CUT APART TO FORM SLICES.

To powinno zrobić.


0

Uwaga: mówię piksel, ale mogą to być regiony wykorzystujące średnią liczbę pikseli. Optymalizacja to kolejny problem ...

Wygląda na to, że musisz przeanalizować funkcję (ciśnienie w czasie) dla każdego piksela i określić, gdzie funkcja się obraca (kiedy zmienia się> X w innym kierunku, uważa się, że zwrot jest przeciwny błędom).

Jeśli wiesz, w których klatkach się obraca, poznasz klatkę, w której nacisk był najcięższy, i będziesz wiedział, gdzie była najmniej twarda między dwiema łapami. Teoretycznie znałbyś wtedy dwie klatki, w których łapy naciskały najmocniej, i mógłbyś obliczyć średnią z tych przedziałów.

po czym przejdę do problemu decydowania, która to łapa!

To ta sama trasa, co wcześniej, wiedząc, kiedy każda łapa wywiera największy nacisk, pomaga podjąć decyzję.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.