2010-11-03 13 views
184

Después de mi pregunta anterior en finding toes within each paw, comencé a cargar otras medidas para ver cómo se mantendría. Desafortunadamente, rápidamente encontré un problema con uno de los pasos anteriores: reconocer las patas.¿Cómo puedo mejorar mi detección de pata?

Verá, mi prueba de concepto básicamente tomó la presión máxima de cada sensor en el tiempo y comenzaría a buscar la suma de cada fila, hasta que encuentre en eso! = 0.0. Luego hace lo mismo para las columnas y tan pronto como encuentra más de 2 filas con cero nuevamente. Almacena los valores mínimos y máximos de fila y columna en algún índice.

alt text

Como se puede ver en la figura, esto funciona bastante bien en la mayoría de los casos. Sin embargo, hay una gran cantidad de desventajas a este enfoque (aparte de ser muy primitiva):

  • Los seres humanos pueden tener 'pies huecos', que significa que hay varias filas vacías dentro de la propia huella. Como temía que esto también pudiera pasar con perros (grandes), esperé al menos 2 o 3 hileras vacías antes de cortar la pata.

    Esto crea un problema si otro contacto se hace en una columna diferente antes de que llegue a varias filas vacías, expandiendo así el área. Me imagino que podría comparar las columnas y ver si exceden un cierto valor, deben ser patas separadas.

  • El problema empeora cuando el perro es muy pequeño o camina a un ritmo más alto. ¡Lo que sucede es que los dedos de los pies de la pata delantera siguen haciendo contacto, mientras que los dedos de los pies de la pata trasera comienzan a hacer contacto dentro de la misma área que la pata delantera!

    Con mi script simple, no será capaz de dividir estos dos, ya que tendría que determinar qué marcos de esa área pertenecen a cada pata, mientras que actualmente solo tendría que mirar los valores máximos sobre todos marcos.

ejemplos de donde empieza van mal:

alt text alt text

así que ahora estoy buscando una mejor manera de reconocer y separar las patas (después de lo cual voy llegar al problema de decidir qué pata es!).

Actualización:

He estado jugando para obtener respuesta (¡increíble!) De Joe implementado, pero estoy teniendo dificultades para extraer los datos reales pata de mis archivos.

alt text

Los coded_paws me muestra todas las diferentes patas, cuando se aplica a la imagen de la presión máxima (ver arriba). Sin embargo, la solución pasa por cada cuadro (para separar las patas superpuestas) y establece los cuatro atributos Rectangle, como coordenadas o alto/ancho.

No puedo entender cómo tomar estos atributos y almacenarlos en alguna variable que pueda aplicar a los datos de medición. Como necesito saber para cada pata, cuál es su ubicación durante la cual encuadra y acopla esto a qué pata es (adelante/atrás, izquierda/derecha).

Entonces, ¿cómo puedo usar los atributos de Rectángulos para extraer estos valores para cada pata?

que tienen las mediciones que utilizan para la configuración pregunta en mi carpeta de Dropbox pública (example 1, example 2, example 3). For anyone interested I also set up a blog para mantenerlo al día :-)

+0

Parece que tendría que dar la espalda al algoritmo de fila/columna que está limitando una información útil. –

+12

¡Guau! Software de control de gato? – alxx

+0

Son datos de perros en realidad @alxx ;-) ¡Pero sí, se usarán para diagnosticarlos! –

Respuesta

344

Si solo quieres regiones (semi) contiguas, ya hay una implementación fácil en Python: SciPy's ndimage.morphology módulo. Esta es una operación bastante común image morphology.


Básicamente, usted tiene 5 pasos:

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. falta de definición de los datos de entrada un poco para asegurarse de que las patas tienen una huella continua. (Sería más eficiente de usar sólo un núcleo más grande (el structure kwarg a las diversas funciones scipy.ndimage.morphology), pero esto no es del todo funciona correctamente por alguna razón ...)

  2. Umbral de la matriz para que tenga una matriz booleana de los lugares donde la presión es más de un cierto valor umbral (es decir thresh = data > value)

  3. Rellene los agujeros internos, para que tenga regiones más limpios (filled = sp.ndimage.morphology.binary_fill_holes(thresh))

  4. Encuentra las regiones contiguas separadas (coded_paws, num_paws = sp.ndimage.label(filled)). Esto devuelve una matriz con las regiones codificadas por número (cada región es un área contigua de un entero único (1 hasta el número de patas) con ceros en cualquier otro lugar)).

  5. Aislar las regiones contiguas usando data_slices = sp.ndimage.find_objects(coded_paws). Esto devuelve una lista de tuplas de objetos slice, por lo que podría obtener la región de los datos para cada pata con [data[x] for x in data_slices]. En su lugar, dibujaremos un rectángulo basado en estas divisiones, lo que requiere un poco más de trabajo.


Las dos animaciones de abajo muestran sus patas "superposición" y "agrupados" Patas de datos de ejemplo. Este método parece estar funcionando perfectamente. (Y por si sirve de algo, esto va mucho más suavemente que las imágenes GIF a continuación en mi máquina, por lo que el algoritmo de detección de la pata es bastante rápido ...)

Overlapping Paws Grouped Paws


Aquí hay una completa ejemplo (ahora con explicaciones mucho más detalladas). La gran mayoría de esto es leer la entrada y hacer una animación. La detección real de la pata es solo 5 líneas de código.

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

Actualización: En cuanto a la identificación de la que la pata está en contacto con el sensor en qué momento, la solución más sencilla es simplemente hacer el mismo análisis, pero el uso de todos los datos a la vez. (es decir, apilar la entrada en una matriz 3D y trabajar con ella, en lugar de los marcos de tiempo individuales). Debido a que las funciones de nimagen de SciPy están pensadas para trabajar con matrices n-dimensionales, no tenemos que modificar la función original de búsqueda de la pata en absoluto.

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

alt text


alt text


alt text

+79

¡Ni siquiera puedo empezar a explicar lo increíble que es tu respuesta! –

+0

@Ivo - ¡Me alegra ayudar! Es un conjunto de datos geniales y un problema claro. –

+0

@Joe: ¡Excelente respuesta! ¿Cómo hiciste los gifs? Puedo 'plt.savefig (...)' crear una colección de pngs, pero 'convert * .png output.gif' de imagemagick pone a mi máquina de rodillas ... – unutbu

3

No soy un experto en la detección de imágenes, y no sé Python, pero voy a darle un golpe ...

Para detectar las patas individuales , primero debería seleccionar todo con una presión mayor que un pequeño umbral, muy cerca de ninguna presión. Cada píxel/punto que está por encima de esto debe estar "marcado". Luego, cada píxel adyacente a todos los píxeles "marcados" se marca y este proceso se repite varias veces. Se formarán masas que están totalmente conectadas, por lo que tiene objetos distintos. Entonces, cada "objeto" tiene un valor mínimo y máximo de xey, por lo que los recuadros delimitadores pueden empaquetarse ordenadamente a su alrededor.

Pseudocódigo:

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

Eso debería hacerlo.

0

Nota: Digo pixel, pero esto podría ser regiones con un promedio de los píxeles. La optimización es otro problema ...

Parece que necesita analizar una función (presión en el tiempo) para cada píxel y determinar where the function turns (cuando cambia> X en la otra dirección, se considera un turno para contrarrestar los errores).

Si sabe en qué marcos gira, sabrá el marco donde la presión fue más dura y sabrá dónde fue la menos dura entre las dos patas. En teoría, entonces sabría los dos marcos donde las patas presionaron más fuerte y puede calcular un promedio de esos intervalos.

después de lo cual ¡llegaré al problema de decidir qué pata es!

Este es el mismo recorrido que antes, saber cuándo cada pata aplica la mayor presión lo ayuda a decidir.

Cuestiones relacionadas