¿Cómo ordenar mis patas?

121

En mi pregunta anterior obtuve una excelente respuesta que me ayudó a detectar dónde una pata golpeó una placa de presión, pero ahora estoy luchando por vincular estos resultados con sus patas correspondientes:

texto alternativo

Anoté manualmente las patas (RF = delantero derecho, RH = trasero derecho, LF = delantero izquierdo, LH = trasero izquierdo).

Como puede ver, claramente hay un patrón repetitivo y regresa en casi todas las mediciones. Aquí hay un enlace a una presentación de 6 ensayos que fueron anotados manualmente.

Mi pensamiento inicial fue usar la heurística para hacer la clasificación, como:

  • Hay una relación de ~ 60-40% en la carga de peso entre las patas delanteras y traseras;
  • Las patas traseras son generalmente más pequeñas en superficie;
  • Las patas están (a menudo) divididas espacialmente en izquierda y derecha.

Sin embargo, soy un poco escéptico sobre mis heurísticas, ya que me fallarían tan pronto como encuentre una variación que no había pensado. Tampoco podrán hacer frente a las mediciones de perros cojos, que probablemente tienen sus propias reglas.

Además, la anotación sugerida por Joe a veces se equivoca y no tiene en cuenta cómo se ve realmente la pata.

Según las respuestas que recibí sobre mi pregunta sobre la detección de picos dentro de la pata , espero que haya soluciones más avanzadas para clasificar las patas. Especialmente porque la distribución de presión y la progresión de la misma son diferentes para cada pata separada, casi como una huella digital. Espero que haya un método que pueda usar esto para agrupar mis patas, en lugar de solo ordenarlas en orden de aparición.

texto alternativo

Así que estoy buscando una mejor manera de ordenar los resultados con su pata correspondiente.

Para cualquiera que esté preparado para el desafío, he seleccionado un diccionario con todos los arreglos en rodajas que contienen los datos de presión de cada pata (agrupados por medición) y el corte que describe su ubicación (ubicación en la placa y a tiempo).

Para aclarar: walk_sliced_data es un diccionario que contiene ['ser_3', 'ser_2', 'sel_1', 'sel_2', 'ser_1', 'sel_3'], que son los nombres de las mediciones. Cada medida contiene otro diccionario, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] (ejemplo de 'sel_1') que representan los impactos que se extrajeron.

También tenga en cuenta que los impactos 'falsos', como donde la pata se mide parcialmente (en el espacio o el tiempo) pueden ser ignorados. Solo son útiles porque pueden ayudar a reconocer un patrón, pero no se analizarán.

¡Y para cualquier persona interesada, mantengo un blog con todas las actualizaciones relacionadas con el proyecto!

Ivo Flipse
fuente
1
Sí, el enfoque que estaba usando no funciona del todo. Solo para elaborar, el enfoque que estaba usando es simplemente ordenar los impactos, y asumir que la primera pata para tocar es la misma que la quinta pata para tocar, y así sucesivamente. (es decir, ordenar los impactos y usar un módulo 4). El problema con esto es que a veces las patas traseras impactan en la almohadilla del sensor después de que toca la primera pata. En ese caso, la primera pata que impacta coincide con la cuarta o tercera pata que impacta. Esperemos que esto tenga algún sentido.
Joe Kington
1
¿Interpretaría las imágenes correctamente en un dedo de cada pata trasera que ejerza una presión significativamente menor que el resto? También parece que el dedo del pie está siempre hacia el "interior", es decir, hacia el centro de masa del perro. ¿Podrías incorporar eso como una heurística?
Thomas Langston
1
Admito que mis habilidades limitadas de procesamiento de imágenes son algo oxidadas, pero ¿es fácilmente posible tomar el gradiente menos pronunciado de la almohadilla media grande de cada pata? Parece que el ángulo de menor inclinación ayudaría inmensamente (un ejemplo dibujado a mano para las patas publicadas: imgur.com/y2wBC imgur.com/yVqVU imgur.com/yehOc imgur.com/q0tcD )
user470379
¿Podría aclarar cómo walk_sliced_datase estructuran los datos ? Veo un diccionario de diccionarios de matrices 3D. Si arreglo la tercera dimensión y trazo las dos primeras como una imagen, creo que veo patas.
Steve Tjoa
@Thomas, sí, cada pata está claramente cargada de una manera distinta. Sé lo que me gustaría que hiciera el programa, pero no tengo idea de cómo programarlo ... @ Steve, agregué una aclaración en la parte inferior :-)
Ivo Flipse

Respuestas:

123

¡Bien! ¡Finalmente logré que algo funcione de manera consistente! Este problema me atrajo durante varios días ... ¡Cosas divertidas! Perdón por la longitud de esta respuesta, pero necesito elaborar un poco sobre algunas cosas ... (¡Aunque puedo establecer un récord para la respuesta de stackoverflow más larga que no sea spam!)

Como nota al margen, estoy usando el conjunto de datos completo al que Ivo proporcionó un enlace en su pregunta original . Es una serie de archivos rar (uno por perro), cada uno de los cuales contiene varias ejecuciones de experimentos diferentes almacenadas como matrices ASCII. En lugar de intentar copiar y pegar ejemplos de código autónomo en esta pregunta, aquí hay un repositorio mercurial de bitbucket con código completo e independiente. Puedes clonarlo con

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


Visión general

Básicamente, hay dos formas de abordar el problema, como señaló en su pregunta. De hecho, voy a usar ambos de diferentes maneras.

  1. Utilice el orden (temporal y espacial) de los impactos de la pata para determinar qué pata es cuál.
  2. Intente identificar la "huella de la pata" basándose únicamente en su forma.

Básicamente, el primer método funciona con las patas del perro siguiendo el patrón trapezoidal que se muestra en la pregunta anterior de Ivo, pero falla cuando las patas no siguen ese patrón. Es bastante fácil detectar mediante programación cuando no funciona.

Por lo tanto, podemos usar las mediciones donde funcionó para construir un conjunto de datos de entrenamiento (de ~ 2000 impactos de pata de ~ 30 perros diferentes) para reconocer qué pata es cuál, y el problema se reduce a una clasificación supervisada (con algunas arrugas adicionales. .. El reconocimiento de imágenes es un poco más difícil que un problema de clasificación supervisada "normal").


Análisis de patrones

Para desarrollar el primer método, cuando un perro camina (¡no corre!) Normalmente (que algunos de estos perros pueden no estar), esperamos que las patas impacten en el orden de: Delantero izquierdo, trasero derecho, delantero derecho, trasero izquierdo , Delantero izquierdo, etc. El patrón puede comenzar con la pata delantera izquierda o delantera derecha.

Si este fuera siempre el caso, podríamos simplemente clasificar los impactos por tiempo de contacto inicial y usar un módulo 4 para agruparlos por pata.

Secuencia de impacto normal

Sin embargo, incluso cuando todo es "normal", esto no funciona. Esto se debe a la forma trapezoidal del patrón. Una pata trasera cae espacialmente detrás de la pata delantera anterior.

Por lo tanto, el impacto de la pata trasera después del impacto inicial de la pata delantera a menudo se cae de la placa del sensor y no se registra. Del mismo modo, el último impacto de la pata a menudo no es la siguiente pata en la secuencia, ya que el impacto de la pata antes de que ocurriera fuera de la placa del sensor y no se registrara.

Pata trasera perdida

No obstante, podemos usar la forma del patrón de impacto de la pata para determinar cuándo sucedió esto y si hemos comenzado con una pata delantera izquierda o derecha. (De hecho, estoy ignorando los problemas con el último impacto aquí. Sin embargo, no es demasiado difícil agregarlo).

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

A pesar de todo esto, con frecuencia no funciona correctamente. Muchos de los perros en el conjunto de datos completo parecen estar corriendo, y los impactos de las patas no siguen el mismo orden temporal que cuando el perro está caminando. (O tal vez el perro solo tiene problemas graves de cadera ...)

Secuencia de Impacto Anormal

Afortunadamente, aún podemos detectar programáticamente si los impactos de la pata siguen o no nuestro patrón espacial esperado:

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

Por lo tanto, aunque la clasificación espacial simple no funciona todo el tiempo, podemos determinar cuándo funciona con una confianza razonable.

Conjunto de datos de entrenamiento

A partir de las clasificaciones basadas en patrones donde funcionó correctamente, podemos construir un conjunto de datos de entrenamiento muy grande de patas clasificadas correctamente (¡~ 2400 impactos de patas de 32 perros diferentes!).

Ahora podemos empezar a ver cómo se ve una pata delantera "promedio", etc., etc.

Para hacer esto, necesitamos algún tipo de "métrica de pata" que tenga la misma dimensionalidad para cualquier perro. (¡En el conjunto de datos completo, hay perros muy grandes y muy pequeños!) Una huella de pata de un elkhound irlandés será mucho más ancha y mucho "más pesada" que una huella de pata de un caniche de juguete. Necesitamos reescalar cada huella para que a) tengan el mismo número de píxeles yb) los valores de presión estén estandarizados. Para hacer esto, volví a muestrear cada huella en una cuadrícula de 20x20 y volví a escalar los valores de presión en función del valor de presión máximo, mínimo y medio para el impacto de la pata.

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

Después de todo esto, finalmente podemos echar un vistazo a cómo se ve un promedio de la pata delantera izquierda, trasera derecha, etc. Tenga en cuenta que esto se promedia en> 30 perros de tamaños muy diferentes, ¡y parece que estamos obteniendo resultados consistentes!

Patas promedio

Sin embargo, antes de hacer cualquier análisis sobre estos, necesitamos restar la media (la pata promedio para todas las patas de todos los perros).

Pata media

Ahora podemos analizar las diferencias con respecto a la media, que son un poco más fáciles de reconocer:

Patas diferenciales

Reconocimiento de pata basado en imágenes

Ok ... Finalmente tenemos un conjunto de patrones con los que podemos comenzar a intentar hacer coincidir las patas. Cada pata se puede tratar como un vector de 400 dimensiones (devuelto por la paw_imagefunción) que se puede comparar con estos cuatro vectores de 400 dimensiones.

Desafortunadamente, si solo usamos un algoritmo de clasificación supervisado "normal" (es decir, encontrar cuál de los 4 patrones está más cerca de una huella de pata particular usando una distancia simple), no funciona de manera consistente. De hecho, no funciona mucho mejor que la posibilidad aleatoria en el conjunto de datos de entrenamiento.

Este es un problema común en el reconocimiento de imágenes. Debido a la alta dimensionalidad de los datos de entrada y la naturaleza algo "difusa" de las imágenes (es decir, los píxeles adyacentes tienen una alta covarianza), simplemente mirar la diferencia de una imagen de una imagen de plantilla no da una muy buena medida de similitud de sus formas.

Eigenpaws

Para evitar esto, necesitamos construir un conjunto de "patas propias" (al igual que las "caras propias" en el reconocimiento facial) y describir cada huella como una combinación de estas patas propias. Esto es idéntico al análisis de componentes principales, y básicamente proporciona una forma de reducir la dimensionalidad de nuestros datos, por lo que la distancia es una buena medida de la forma.

Debido a que tenemos más imágenes de entrenamiento que dimensiones (2400 vs 400), no es necesario hacer álgebra lineal "elegante" para la velocidad. Podemos trabajar directamente con la matriz de covarianza del conjunto de datos de entrenamiento:

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

Estas basis_vecsson las "patas propias".

Eigenpaws

Para usarlos, simplemente punteamos (es decir, la multiplicación de matrices) cada imagen de la pata (como un vector de 400 dimensiones, en lugar de una imagen de 20x20) con los vectores base. Esto nos da un vector de 50 dimensiones (un elemento por vector base) que podemos usar para clasificar la imagen. En lugar de comparar una imagen de 20x20 con la imagen de 20x20 de cada pata de "plantilla", comparamos la imagen transformada de 50 dimensiones con cada pata de plantilla transformada de 50 dimensiones. Esto es mucho menos sensible a pequeñas variaciones en la forma exacta en que se coloca cada dedo del pie, etc., y básicamente reduce la dimensionalidad del problema a solo las dimensiones relevantes.

Clasificación de la pata basada en Eigenpaw

Ahora podemos simplemente usar la distancia entre los vectores de 50 dimensiones y los vectores de "plantilla" para cada pata para clasificar qué pata es cuál:

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()]

Aquí están algunos de los resultados: texto alternativo texto alternativo texto alternativo

Problemas restantes

Todavía hay algunos problemas, particularmente con perros demasiado pequeños para dejar una huella clara ... (Funciona mejor con perros grandes, ya que los dedos de los pies se separan más claramente a la resolución del sensor). Además, las huellas parciales no se reconocen con esto. sistema, mientras que pueden estar con el sistema basado en patrón trapezoidal.

Sin embargo, debido a que el análisis de la pata propia usa inherentemente una métrica de distancia, podemos clasificar las patas en ambos sentidos y recurrir al sistema basado en el patrón trapezoidal cuando la distancia más pequeña del análisis de la pata propia del "libro de códigos" supera algún umbral. Sin embargo, aún no he implementado esto.

Uf ... ¡Eso fue largo! ¡Me quito el sombrero ante Ivo por tener una pregunta tan divertida!

Joe Kington
fuente
2
Gran respuesta. También probé el método eigenpaw, pero no fui tan perseverante como tú. Un problema que veo es el registro de la pata, es decir, ya que el registro facial es el reconocimiento facial. ¿Encontró algún problema en la normalización de la ubicación y rotación de cada pata? Si es así, entonces la pata puede ser preprocesada en alguna característica invariante de rotación de traslación antes de hacer PCA.
Steve Tjoa
2
@ Steve, no he intentado rotarlos, aunque tuve algunas discusiones con Joe sobre cómo mejorarlo más. Sin embargo, para terminar mi proyecto por ahora, anoté manualmente todas las patas para poder concluirlo. Afortunadamente, esto también nos permite crear diferentes conjuntos de entrenamiento para hacer que el reconocimiento sea más sensible. Para rotar las patas, estaba planeando usar los dedos de los pies, pero como puedes leer en mi blog, eso no es tan fácil como mi primera pregunta lo hizo parecer ...
Ivo Flipse
@Basic sí, cambié a alojar mi propio sitio web y moví todo el contenido de Wordpress, pero ya no pude editar mi comentario aquí. Debería poder encontrarlos aquí: flipserd.com/blog/ivoflipse/post/improving-the-paw-detection
Ivo Flipse
4

Usando la información basada únicamente en la duración, creo que podría aplicar técnicas de modelación cinemática; a saber, cinemática inversa . Combinado con la orientación, la longitud, la duración y el peso total, proporciona un cierto nivel de periodicidad que, espero, podría ser el primer paso para tratar de resolver su problema de "clasificación de patas".

Todos esos datos podrían usarse para crear una lista de polígonos (o tuplas) delimitados, que podría usar para ordenar por tamaño de paso y luego por paw-ness [índice].

Lam Chau
fuente
2

¿Puede hacer que el técnico que ejecuta la prueba ingrese manualmente la primera pata (o las dos primeras)? El proceso puede ser:

  • Muestre a los técnicos la imagen del orden de los pasos y pídales que anoten la primera pata.
  • Etiquete las otras patas según la primera pata y permita que el técnico realice correcciones o vuelva a ejecutar la prueba. Esto permite perros cojos o de 3 patas.
Jamie Ide
fuente
De hecho, tengo anotaciones de las primeras patas, aunque no son perfectas. Sin embargo, la primera pata siempre es una pata delantera y no me ayudaría a separar las patas traseras. Además, el pedido no es perfecto como Joe mencionó, porque eso requiere que tanto el frente toque la placa al comienzo.
Ivo Flipse
Las anotaciones serían útiles al usar el reconocimiento de imágenes, debido a las 24 mediciones que tengo, al menos 24 patas ya estarían anotadas. Si luego se agruparían en 4 grupos, dos de ellos deberían contener una cantidad razonable de cualquiera de las patas delanteras lo suficiente como para hacer que un algoritmo esté bastante seguro del agrupamiento.
Ivo Flipse
A menos que los lea incorrectamente, los ensayos anotados vinculados muestran que la pata trasera se toca primero en 4 de 6 ensayos.
Jamie Ide
Ah, quise decir en cuanto al tiempo. Si recorre el archivo, la pata delantera siempre debe ser la primera en contactar con la placa.
Ivo Flipse