¿Cómo ordenar mis patas?

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

texto alternativo

Anoté manualmente las patas (RF = parte delantera derecha, RH = parte posterior derecha, LF = parte delantera izquierda, LH = parte posterior izquierda).

Como puede ver, claramente hay un patrón que se repite 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 heurísticas para hacer la clasificación, como:

  • Hay una relación de ~ 60-40% en peso que soporta 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 con respecto a mi heurística, ya que me fallarían en cuanto encontrase una variación que no había pensado. Tampoco podrán hacer frente a las medidas de los perros cojos, que probablemente tengan sus propias reglas.

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

Según las respuestas que recibí en mi pregunta sobre la detección de picos dentro de la pata , espero que haya soluciones más avanzadas para ordenar las patas. Especialmente porque la distribución de la 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 simplemente ordenarlas por orden de ocurrencia.

texto alternativo

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

Para cualquier persona que esté a la altura del desafío, he seleccionado un diccionario con todas las matrices cortadas que contienen los datos de presión de cada pata (agrupados por medición) y la división que describe su ubicación (ubicación en la placa y en el 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 medidas. Cada medida contiene otro diccionario, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] (ejemplo de ‘sel_1’) que representa los impactos que se extrajeron.

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

Y para cualquier persona interesada, estoy manteniendo un blog con todas las actualizaciones relacionadas con el proyecto.

¡Bien! ¡Finalmente he logrado que algo funcione de manera consistente! Este problema me detuvo por 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 más larga sin stack de 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) que contienen varias ejecuciones de experimentos diferentes almacenadas como arreglos ascii. En lugar de intentar copiar y pegar ejemplos de código autónomo en esta pregunta, aquí hay un repository mercurial de bitbucket con código completo e independiente. Puedes clonarlo con

hg clone https://joferkington@bitbucket.org/joferkington/paw-analysis


Visión general

Básicamente, hay dos formas de abordar el problema, como anotó en su pregunta. En realidad 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. Trate de identificar la “huella” basada únicamente en su forma.

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

Por lo tanto, podemos usar las mediciones donde funcionó para crear 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 elaborar el primer método, cuando un perro camina (¡no corre!) Normalmente (lo que algunos de estos perros no pueden ser), esperamos que las patas impacten en el orden de: Delantero izquierdo, Atrás derecha, Delantero derecho, Atrás izquierda , Frontal Izquierdo, etc. El patrón puede comenzar con la pata delantera izquierda o delantera derecha.

Si este fuera siempre el caso, podríamos simplemente ordenar los impactos por el 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 espacialmente cae detrás de la pata delantera anterior.

Por lo tanto, el impacto de la pata trasera después del impacto de la pata delantera inicial a menudo se cae de la placa del sensor y no se registra. De manera similar, el último impacto de la pata a menudo no es la siguiente pata de la secuencia, ya que el impacto de la pata antes de que ocurriera fuera de la placa del sensor y no se registró.

Perdida pata hind

No obstante, podemos usar la forma del patrón de impacto de la pata para determinar cuándo ha ocurrido esto, y si hemos empezado 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 (ie 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 severos de cadera …)

Secuencia de impacto anormal

Afortunadamente, aún podemos detectar de manera programática 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

Desde las clasificaciones basadas en patrones en las que funcionó correctamente, podemos crear un conjunto de datos de entrenamiento muy grande de patas clasificadas correctamente (¡~ 2400 impactos de pata de 32 perros diferentes!).

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

Para hacer esto, necesitamos algún tipo de "pata métrica" ​​que sea la misma dimensionalidad para cualquier perro. (En el conjunto de datos completo, ¡hay perros muy grandes y muy pequeños!) Una huella de una pata irlandesa será mucho más ancha y mucho más "pesada" que una huella de un caniche de juguete. Tenemos que volver a escalar cada impresión de la pata 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 impresión de pata en una cuadrícula de 20x20 y volví a escalar los valores de presión en función del máximo, mínimo y valor de presión media 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 una pata delantera izquierda promedio, trasera derecha, etc. Tenga en cuenta que esto se promedia en más de 30 perros de tamaños muy diferentes, ¡y parece que estamos obteniendo resultados consistentes!

Patas promedio

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

Mean Paw

Ahora podemos analizar las diferencias de 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 tratar de emparejar las patas contra. Cada pata puede tratarse como un vector de 400 dimensiones (devuelto por la función paw_image ) que puede compararse con estos cuatro vectores de 400 dimensiones.

Desafortunadamente, si solo usamos un algoritmo de clasificación supervisada "normal" (es decir, encontramos cuál de los 4 patrones está más cerca de una huella en particular usando una distancia simple), no funciona de manera consistente. De hecho, no lo hace 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 observar la diferencia de una imagen desde una imagen de plantilla no proporciona una buena medida de la imagen. Similitud de sus formas.

Eigenpaws

Para solucionar esto, necesitamos crear un conjunto de "eigenpaws" (como "eigenfaces" en el reconocimiento facial), y describir cada impresión de la pata como una combinación de estos eigenpaws. Esto es idéntico al análisis de componentes principales, y básicamente proporciona una manera de reducir la dimensionalidad de nuestros datos, de modo 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 hay necesidad de hacer álgebra lineal "de lujo" 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 

Estos basis_vecs son los "eigenpaws".

Eigenpaws

Para usar estos, simplemente punteamos (es decir, multiplicación de matriz) cada imagen de pata (como un vector de 400 dimensiones, en lugar de una imagen de 20x20) con los vectores de 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 las pequeñas variaciones en la forma exacta en que se coloca cada dedo, etc., y básicamente reduce la dimensionalidad del problema a las dimensiones relevantes.

Clasificación de pata basada en eigenpaws

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 alternativotexto alternativotexto alternativo

Problemas pendientes

Todavía hay algunos problemas, especialmente con perros demasiado pequeños para dejar una huella clara ... (Funciona mejor con perros grandes, ya que los dedos de los pies están más claramente separados en 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 el patrón trapezoidal.

Sin embargo, debido a que el análisis eigenpaw utiliza de forma inherente una métrica de distancia, podemos clasificar las patas en ambos sentidos y volver al sistema basado en patrones trapezoidales cuando la distancia más pequeña del análisis eigenpaw del "libro de códigos" está por encima de algún umbral. Aunque no he implementado esto todavía.

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

Usando la información puramente basada en la duración, creo que podría aplicar técnicas de modelado de 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 acotados (o tuplas), que podría usar para ordenar por tamaño de paso y luego por paw-ness [índice].

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

  • Muestre a la tecnología el orden de la imagen de los pasos y solicíteles que anoten la primera pata.
  • Etiquete las otras patas según la primera pata y permita que el técnico haga correcciones o vuelva a ejecutar la prueba. Esto permite perros cojos o de 3 patas.