Come posso migliorare il rilevamento della mia zampa?


198

Dopo la mia precedente domanda su come trovare le dita dei piedi all'interno di ogni zampa , ho iniziato a caricare altre misurazioni per vedere come avrebbe resistito. Sfortunatamente, ho subito riscontrato un problema con uno dei passaggi precedenti: riconoscere le zampe.

Vedete, la mia dimostrazione del concetto ha sostanzialmente preso la pressione massima di ciascun sensore nel tempo e avrebbe iniziato a cercare la somma di ogni riga, fino a quando non ne ha trovato! = 0.0. Quindi fa lo stesso per le colonne e non appena trova più di 2 righe che sono di nuovo zero. Memorizza i valori minimi e massimi di riga e colonna in alcuni indici.

testo alternativo

Come puoi vedere nella figura, nella maggior parte dei casi funziona abbastanza bene. Tuttavia, ci sono molti aspetti negativi di questo approccio (oltre ad essere molto primitivo):

  • Gli umani possono avere "piedi cavi", il che significa che ci sono diverse file vuote all'interno dell'impronta stessa. Poiché temevo che ciò potesse accadere anche con cani (di grandi dimensioni), ho aspettato almeno 2 o 3 file vuote prima di tagliare la zampa.

    Questo crea un problema se un altro contatto viene effettuato in una colonna diversa prima che raggiunga più righe vuote, espandendo così l'area. Immagino di poter confrontare le colonne e vedere se superano un certo valore, devono essere zampe separate.

  • Il problema peggiora quando il cane è molto piccolo o cammina a un ritmo più elevato. Quello che succede è che le dita dei piedi della zampa anteriore stanno ancora entrando in contatto, mentre le dita dei piedi della zampa posteriore iniziano a prendere contatto nella stessa area della zampa anteriore!

    Con il mio semplice script, non sarà in grado di dividere questi due, perché dovrebbe determinare quali frame di quell'area appartengono a quale zampa, mentre attualmente dovrei solo guardare i valori massimi su tutti i frame.

Esempi di dove inizia a sbagliare:

testo alternativo testo alternativo

Quindi ora sto cercando un modo migliore per riconoscere e separare le zampe (dopo di che arriverò al problema di decidere quale sia la zampa!).

Aggiornare:

Ho cercato di implementare la risposta (fantastica!) Di Joe, ma ho difficoltà a estrarre i dati delle zampe reali dai miei file.

testo alternativo

Il coded_paws mi mostra tutte le diverse zampe, quando applicate all'immagine della pressione massima (vedi sopra). Tuttavia, la soluzione va su ogni frame (per separare le zampe sovrapposte) e imposta i quattro attributi Rectangle, come coordinate o altezza / larghezza.

Non riesco a capire come prendere questi attributi e memorizzarli in alcune variabili che posso applicare ai dati di misurazione. Dal momento che ho bisogno di sapere per ogni zampa, qual è la sua posizione durante quali cornici e abbina questa a quale zampa è (anteriore / posteriore, sinistra / destra).

Quindi, come posso usare gli attributi Rectangles per estrarre questi valori per ogni zampa?

Ho le misure che ho usato nella configurazione della domanda nella mia cartella Dropbox pubblica ( esempio 1 , esempio 2 , esempio 3 ). Per chiunque sia interessato ho anche creato un blog per tenerti aggiornato :-)


Sembra che dovresti allontanarti dall'algoritmo di riga / colonna e stai limitando informazioni utili.
Tamara Wijsman,

12
Wow! Software di controllo cat?
alxx,

In realtà sono i dati del cane @alxx ;-) Ma sì, verranno utilizzati per diagnosticare!
Ivo Flipse,

4
Perché? (non importa, è più divertente non sapere ...)
Ben Regenspan,

Risposte:


358

Se siete semplicemente desideroso (semi) regioni contigue, c'è già una facile implementazione in Python: SciPy 's ndimage.morphology modulo. Questa è un'operazione di morfologia dell'immagine abbastanza comune .


Fondamentalmente, hai 5 passaggi:

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. Sfocare un po 'i dati di input per assicurarsi che le zampe abbiano un footprint continuo. (Sarebbe più efficiente usare solo un kernel più grande (il structurekwarg per le varie scipy.ndimage.morphologyfunzioni) ma questo non funziona abbastanza correttamente per qualche motivo ...)

  2. Soglia dell'array in modo da disporre di un array booleano di posizioni in cui la pressione supera un valore di soglia (ovvero thresh = data > value)

  3. Riempi tutti i fori interni, in modo da avere regioni più pulite ( filled = sp.ndimage.morphology.binary_fill_holes(thresh))

  4. Trova le regioni contigue separate ( coded_paws, num_paws = sp.ndimage.label(filled)). Ciò restituisce un array con le regioni codificate per numero (ogni regione è un'area contigua di un numero intero univoco (1 fino al numero di zampe) con zeri ovunque).

  5. Isolare le regioni contigue usando data_slices = sp.ndimage.find_objects(coded_paws). Questo restituisce un elenco di tuple di sliceoggetti, in modo da poter ottenere la regione dei dati per ogni zampa con [data[x] for x in data_slices]. Invece, disegneremo un rettangolo basato su queste sezioni, che richiede un po 'più di lavoro.


Le due animazioni seguenti mostrano i dati di esempio "Paws sovrapposti" e "Paws raggruppati". Questo metodo sembra funzionare perfettamente. (E per quello che vale, funziona molto più agevolmente delle immagini GIF qui sotto sulla mia macchina, quindi l'algoritmo di rilevamento della zampa è abbastanza veloce ...)

Zampe sovrapposte Zampe raggruppate


Ecco un esempio completo (ora con spiegazioni molto più dettagliate). La stragrande maggioranza di questi sta leggendo l'input e facendo un'animazione. Il rilevamento effettivo della zampa è solo 5 righe di codice.

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')

Aggiornamento: per quanto riguarda l'identificazione di quale zampa è in contatto con il sensore in quali orari, la soluzione più semplice è semplicemente fare la stessa analisi, ma utilizzare tutti i dati contemporaneamente. (ovvero impilare l'input in un array 3D e lavorare con esso, anziché i singoli intervalli di tempo). Poiché le funzioni ndimage di SciPy sono pensate per funzionare con array n-dimensionali, non è necessario modificare la funzione di individuazione della zampa originale affatto.

# 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')

testo alternativo


testo alternativo


testo alternativo


82
Non riesco nemmeno a spiegare quanto sia fantastica la tua risposta!
Ivo Flipse,

1
@Ivo: Sì, mi piacerebbe aggiungere un po 'di più anche a Joe :) Ma dovrei iniziare una nuova domanda, o forse @Joe, se vuoi, rispondere qui? stackoverflow.com/questions/2546780/...
unutbu

2
In realtà ho appena scaricato .png's, e ho fatto un convert *.png output.gif. Sicuramente ho già avuto l'immaginazione di mettere in ginocchio la mia macchina, anche se ha funzionato bene per questo esempio. In passato ho usato questo script: svn.effbot.python-hosting.com/pil/Scripts/gifmaker.py per scrivere direttamente una gif animata da Python senza salvare i singoli frame. Spero che aiuti! Pubblicherò un esempio alla domanda menzionata da @unutbu.
Joe Kington,

1
Grazie per l'informazione, @Joe. Parte del mio problema era trascurare di usarlo bbox_inches='tight'in plt.savefig, l'altro era l'impazienza :)
unutbu

4
Santa mucca, devo solo dire wow a quanto è grande questa risposta.
andersoj,

4

Non sono un esperto nel rilevamento di immagini e non conosco Python, ma lo darò un colpo ...

Per rilevare le singole zampe, devi prima selezionare tutto con una pressione maggiore di una piccola soglia, molto vicino a nessuna pressione. Ogni pixel / punto sopra questo dovrebbe essere "contrassegnato". Quindi, ogni pixel adiacente a tutti i pixel "contrassegnati" viene contrassegnato e questo processo viene ripetuto alcune volte. Si formerebbero masse totalmente connesse, quindi si hanno oggetti distinti. Quindi, ogni "oggetto" ha un valore minimo e massimo xey, quindi i riquadri di delimitazione possono essere impaccati ordinatamente attorno ad essi.

pseudocodice:

(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.

Questo dovrebbe farlo.


0

Nota: dico pixel, ma potrebbero trattarsi di regioni che utilizzano una media dei pixel. L'ottimizzazione è un altro problema ...

Sembra che tu debba analizzare una funzione (pressione nel tempo) per ciascun pixel e determinare dove gira la funzione (quando cambia> X nell'altra direzione viene considerata una svolta per contrastare gli errori).

Se sai a quali frame gira, conoscerai il frame in cui la pressione era più forte e saprai dove era il meno duro tra le due zampe. In teoria, allora conosceresti i due frame in cui le zampe premevano più forte e potevano calcolare una media di quegli intervalli.

dopo di che arriverò al problema di decidere quale zampa è!

Questo è lo stesso tour di prima, sapere quando ogni zampa applica la massima pressione ti aiuta a decidere.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.