¿Cómo puedo mejorar la detección de mi pata?

198

Después de mi pregunta anterior sobre cómo encontrar los dedos de los pies dentro de cada pata , comencé a cargar otras medidas para ver cómo se mantendría. Desafortunadamente, me encontré rápidamente con un problema con uno de los pasos anteriores: reconocer las patas.

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

texto alternativo

Como puede ver en la figura, esto funciona bastante bien en la mayoría de los casos. Sin embargo, hay muchas desventajas en este enfoque (además de ser muy primitivo):

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

    Esto crea un problema si otro contacto se hizo en una columna diferente antes de que alcance 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 la pata delantera todavía están haciendo contacto, mientras que los dedos 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, porque tendría que determinar qué cuadros de esa área pertenecen a cada pata, mientras que actualmente solo tendría que mirar los valores máximos en todos los cuadros.

Ejemplos de dónde empieza a salir mal:

texto alternativo texto alternativo

Así que ahora estoy buscando una mejor manera de reconocer y separar las patas (¡después de lo cual abordaré el problema de decidir qué pata es!).

Actualizar:

He estado jugando para implementar la respuesta de Joe (¡increíble!), Pero tengo dificultades para extraer los datos reales de la pata de mis archivos.

texto alternativo

Coded_paws me muestra todas las patas diferentes, cuando se aplica a la imagen de 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 de Rectángulo, como las coordenadas o la altura / anchura.

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 qué cuadros y acoplar esto a qué pata es (frontal / posterior, izquierda / derecha).

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

Tengo las medidas que utilicé en la configuración de preguntas en mi carpeta pública de Dropbox ( ejemplo 1 , ejemplo 2 , ejemplo 3 ). Para cualquier persona interesada, también configuré un blog para mantenerlo actualizado :-)

Ivo Flipse
fuente
Parece que tendría que alejarse del algoritmo de fila / columna y está limitando información útil.
Tamara Wijsman
12
¡Guauu! Software de control de gato?
alxx
Son datos de perros en realidad @alxx ;-) ¡Pero sí, se usarán para diagnosticarlos!
Ivo Flipse
44
¿Por qué? (no importa, es más divertido no saber ...)
Ben Regenspan

Respuestas:

358

Si solo quiere regiones (semi) contiguas, ya hay una implementación fácil en Python: el módulo ndimage.morphology de SciPy . Esta es una operación de morfología de imagen bastante común .


Básicamente, tienes 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. Desenfoque un poco los datos de entrada para asegurarse de que las patas tengan una huella continua. (Sería más eficiente usar un kernel más grande (el structurekwarg para las diversas scipy.ndimage.morphologyfunciones) pero esto no funciona correctamente por alguna razón ...)

  2. Umbralice la matriz para que tenga una matriz booleana de lugares donde la presión supere algún valor umbral (es decir thresh = data > value)

  3. Rellene los agujeros internos, de modo que tenga regiones más limpias ( 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 número entero único (1 hasta el número de patas) con ceros en todas partes).

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


Las dos animaciones a continuación muestran los datos de ejemplo de "Patas superpuestas" y "Patas agrupadas". Este método parece estar funcionando perfectamente. (Y para lo que sea que valga, esto funciona 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 ...)

Patas superpuestas Patas Agrupadas


Aquí hay un ejemplo completo (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 de 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 identificar qué pata está en contacto con el sensor en qué momentos, la solución más simple es simplemente hacer el mismo análisis, pero usar 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 ndimage de SciPy están destinadas a trabajar con matrices n-dimensionales, no tenemos que modificar la función original de búsqueda de patas 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')

texto alternativo


texto alternativo


texto alternativo

Joe Kington
fuente
82
¡Ni siquiera puedo comenzar a explicar lo increíble que es tu respuesta!
Ivo Flipse
1
@Ivo: Sí, también me gustaría votar a Joe un poco más :) pero ¿debería comenzar una nueva pregunta, o tal vez @Joe, si es así, responda aquí? stackoverflow.com/questions/2546780/…
unutbu el
2
De hecho, acabo de deshacerme de .png's e hice un convert *.png output.gif. Ciertamente, imagemagick hizo que mi máquina se pusiera de rodillas antes, aunque funcionó bien para este ejemplo. En el pasado, he usado este script: svn.effbot.python-hosting.com/pil/Scripts/gifmaker.py para escribir directamente un gif animado desde python sin guardar los cuadros individuales. ¡Espero que ayude! Publicaré un ejemplo en la pregunta que @unutbu mencionó.
Joe Kington el
1
Gracias por la información, @ Joe. Parte de mi problema era descuidar el uso bbox_inches='tight'en el plt.savefig, el otro era impaciencia :)
unutbu
44
Santa vaca, solo tengo que decir wow por lo genial que es esta respuesta.
andersoj
4

No soy un experto en detección de imágenes, y no conozco Python, pero le daré un golpe ...

Para detectar patas individuales, primero debe seleccionar todo con una presión mayor que un umbral pequeño, muy cerca de ninguna presión. Cada píxel / punto que esté por encima de este 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ían masas totalmente conectadas, por lo que tiene objetos distintos. Luego, cada "objeto" tiene un valor mínimo y máximo de x e y, por lo que los cuadros delimitadores se pueden empaquetar perfectamente alrededor de ellos.

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 ser suficiente.

TaslemGuy
fuente
0

Nota: Digo píxel, pero esto podría ser regiones que usan 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 dónde gira la función (cuando cambia> X en la otra dirección se considera un giro para contrarrestar los errores).

Si sabe en qué cuadros gira, sabrá el cuadro donde la presión fue más fuerte y sabrá dónde fue la menos dura entre las dos patas. En teoría, entonces conocerías los dos cuadros en los que las patas presionaron más y podrás 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.

Tamara Wijsman
fuente