¡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://joferkington@bitbucket.org/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.
- Utilice el orden (temporal y espacial) de los impactos de la pata para determinar qué pata es cuál.
- 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.
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.
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 ...)
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!
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).
Ahora podemos analizar las diferencias con respecto a la media, que son un poco más fáciles de reconocer:
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_image
funció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_vecs
son las "patas propias".
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:
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!