Tudo bem! Finalmente consegui fazer algo funcionar consistentemente! Esse problema me atraiu por vários dias ... Coisas divertidas! Desculpe pela duração desta resposta, mas preciso elaborar um pouco sobre algumas coisas ... (Embora eu possa estabelecer um recorde para a resposta mais longa de fluxo de pilha sem spam de todos os tempos!)
Como uma observação lateral, estou usando o conjunto de dados completo ao qual Ivo forneceu um link na pergunta original . É uma série de arquivos rar (um por cão), cada um contendo várias execuções experimentais diferentes armazenadas como matrizes ascii. Em vez de tentar copiar e colar exemplos de código autônomo nesta pergunta, aqui está um repositório de mercúrio de bitbucket com código completo e autônomo. Você pode cloná-lo com
hg clone https://joferkington@bitbucket.org/joferkington/paw-analysis
Visão geral
Existem essencialmente duas maneiras de abordar o problema, como você observou na sua pergunta. Na verdade, vou usar os dois de maneiras diferentes.
- Use a ordem (temporal e espacial) dos impactos da pata para determinar qual pata é qual.
- Tente identificar o "pawprint" com base apenas em sua forma.
Basicamente, o primeiro método trabalha com as patas do cão segue o padrão trapezoidal mostrado na pergunta de Ivo acima, mas falha sempre que as patas não seguem esse padrão. É bastante fácil detectar programaticamente quando não funciona.
Portanto, podemos usar as medições em que funcionou para criar um conjunto de dados de treinamento (de ~ 2000 impactos da pata de ~ 30 cães diferentes) para reconhecer qual pata é qual e o problema se reduz a uma classificação supervisionada (com algumas rugas adicionais. .. O reconhecimento de imagens é um pouco mais difícil que um problema de classificação supervisionado "normal").
Análise de Padrões
Para elaborar o primeiro método, quando um cachorro está andando (sem correr!) Normalmente (o que alguns desses cães podem não estar), esperamos que as patas impactem na ordem de: Frente Esquerda, Traseira Direita, Frente Direita, Traseira Esquerda , Frente esquerda, etc. O padrão pode começar com a pata dianteira esquerda ou dianteira direita.
Se esse sempre fosse o caso, poderíamos simplesmente classificar os impactos pelo tempo de contato inicial e usar o módulo 4 para agrupá-los por pata.
No entanto, mesmo quando tudo está "normal", isso não funciona. Isso ocorre devido à forma trapezoidal do padrão. Uma pata traseira cai espacialmente atrás da pata anterior anterior.
Portanto, o impacto da pata traseira após o impacto inicial da pata frontal geralmente cai da placa do sensor e não é registrado. Da mesma forma, o último impacto da pata geralmente não é a próxima na seqüência, pois o impacto antes de ocorrer na placa do sensor e não foi registrado.
No entanto, podemos usar a forma do padrão de impacto da pata para determinar quando isso aconteceu e se começamos com uma pata dianteira esquerda ou direita. (Na verdade, estou ignorando problemas com o último impacto aqui. Porém, não é muito difícil adicioná-lo.)
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
Apesar de tudo isso, freqüentemente não funciona corretamente. Muitos dos cães no conjunto de dados completo parecem estar correndo, e os impactos das patas não seguem a mesma ordem temporal de quando o cachorro está andando. (Ou talvez o cão tenha problemas graves no quadril ...)
Felizmente, ainda podemos detectar programaticamente se os impactos das patas seguem ou não o nosso padrão 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
Portanto, mesmo que a classificação espacial simples não funcione o tempo todo, podemos determinar quando ela funciona com confiança razoável.
Conjunto de dados de treinamento
A partir das classificações baseadas em padrões em que funcionou corretamente, podemos criar um conjunto de dados de treinamento muito grande de patas corretamente classificadas (~ 2400 impactos de 32 cães diferentes!).
Agora podemos começar a ver como é a frente "média" esquerda, etc, da pata.
Para fazer isso, precisamos de algum tipo de "métrica da pata" que seja da mesma dimensionalidade para qualquer cão. (No conjunto de dados completo, existem cães muito grandes e muito pequenos!) Uma impressão de pata de um elkhound irlandês será muito mais ampla e muito "mais pesada" do que uma impressão de pata de um poodle toy. Precisamos redimensionar cada impressão de pata para que a) eles tenham o mesmo número de pixels eb) os valores de pressão sejam padronizados. Para fazer isso, ralei novamente cada impressão de pata em uma grade de 20x20 e redimensionei os valores de pressão com base no valor máximo, mínimo e médio de pressão para o impacto da 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
Depois de tudo isso, finalmente podemos dar uma olhada em como é uma pata dianteira esquerda média, traseira direita, etc. Observe que essa média é calculada em mais de 30 cães de tamanhos muito diferentes, e parece que estamos obtendo resultados consistentes!
No entanto, antes de fazer qualquer análise sobre isso, precisamos subtrair a média (a pata média para todas as pernas de todos os cães).
Agora podemos analisar as diferenças em relação à média, que são um pouco mais fáceis de reconhecer:
Reconhecimento de pata com base em imagem
Ok ... Finalmente temos um conjunto de padrões com os quais podemos começar a combinar as patas. Cada pata pode ser tratada como um vetor 400-dimensional (retornado pela paw_image
função) que pode ser comparado a esses quatro vetores 400-dimensionais.
Infelizmente, se apenas usarmos um algoritmo de classificação supervisionado "normal" (ou seja, descobrir qual dos 4 padrões está mais próximo de uma impressão de pata específica usando uma distância simples), ele não funcionará de forma consistente. De fato, não se sai muito melhor do que a chance aleatória no conjunto de dados de treinamento.
Este é um problema comum no reconhecimento de imagens. Devido à alta dimensionalidade dos dados de entrada e à natureza um tanto "confusa" das imagens (ou seja, os pixels adjacentes têm uma alta covariância), simplesmente observar a diferença entre uma imagem e uma imagem de modelo não fornece uma medida muito boa da semelhança de suas formas.
Eigenpaws
Para contornar isso, precisamos construir um conjunto de "patas próprias" (como "faces próprias" no reconhecimento facial) e descrever cada impressão de pata como uma combinação dessas patas próprias. Isso é idêntico à análise dos componentes principais e basicamente fornece uma maneira de reduzir a dimensionalidade dos nossos dados, para que a distância seja uma boa medida de forma.
Como temos mais imagens de treinamento do que dimensões (2400 x 400), não é necessário fazer álgebra linear "sofisticada" para obter velocidade. Podemos trabalhar diretamente com a matriz de covariância do conjunto de dados de treinamento:
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
Estes basis_vecs
são os "eigenpaws".
Para usá-los, simplesmente pontilhamos (ou seja, multiplicação da matriz) cada imagem de pata (como um vetor de 400 dimensões, em vez de uma imagem de 20x20) com os vetores de base. Isso nos dá um vetor 50-dimensional (um elemento por vetor base) que podemos usar para classificar a imagem. Em vez de comparar uma imagem 20x20 com a imagem 20x20 de cada pata "modelo", comparamos a imagem transformada em 50 dimensões com cada pata modelo transformada em 50 dimensões. Isso é muito menos sensível a pequenas variações de exatamente como cada dedo está posicionado etc., e basicamente reduz a dimensionalidade do problema apenas às dimensões relevantes.
Classificação de pata baseada em Eigenpaw
Agora podemos simplesmente usar a distância entre os vetores de 50 dimensões e os vetores de "modelo" para cada perna para classificar qual pata é qual:
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()]
Aqui estão alguns dos resultados:
Problemas restantes
Ainda existem alguns problemas, principalmente em cães pequenos demais para criar uma impressão clara de pata ... (Funciona melhor com cães grandes, pois os dedos dos pés ficam mais claramente separados na resolução do sensor.) enquanto eles podem estar com o sistema baseado em padrões trapezoidais.
No entanto, como a análise da pinha própria inerentemente usa uma métrica de distância, podemos classificar as patas nos dois sentidos e voltar ao sistema baseado em padrões trapezoidais quando a menor distância da análise da pinha própria do "livro de códigos" está acima de algum limite. Ainda não implementei isso.
Ufa ... Isso foi longo! Estou de olho em Ivo por ter uma pergunta tão divertida!