Como classificar minhas patas?

121

Na minha pergunta anterior, recebi uma excelente resposta que me ajudou a detectar onde uma pata atingiu uma placa de pressão, mas agora estou lutando para vincular esses resultados às correspondentes.

texto alternativo

Anotei manualmente as patas (RF = frente direita, RH = traseira direita, LF = frente esquerda, LH = traseira esquerda).

Como você pode ver, há claramente um padrão de repetição e ele volta em quase todas as medições. Aqui está um link para uma apresentação de 6 tentativas que foram anotadas manualmente.

Meu pensamento inicial era usar heurísticas para fazer a classificação, como:

  • Há uma proporção de ~ 60-40% no suporte de peso entre as patas dianteiras e traseiras;
  • As patas traseiras são geralmente menores na superfície;
  • As patas são (frequentemente) divididas espacialmente em esquerda e direita.

No entanto, sou um pouco cético em relação às minhas heurísticas, pois elas falhariam comigo assim que eu encontrasse uma variação que eu não tinha pensado. Eles também não serão capazes de lidar com as medições de cães coxos, que provavelmente têm regras próprias.

Além disso, a anotação sugerida por Joe às vezes fica bagunçada e não leva em consideração a aparência da pata.

Com base nas respostas que recebi na minha pergunta sobre detecção de pico na pata , espero que haja soluções mais avançadas para classificá-las. Especialmente porque a distribuição da pressão e a progressão da mesma são diferentes para cada pata separada, quase como uma impressão digital. Espero que exista um método que possa usar isso para agrupar minhas patas, em vez de apenas classificá-las em ordem de ocorrência.

texto alternativo

Então, estou procurando uma maneira melhor de classificar os resultados com a pata correspondente.

Para qualquer pessoa que esteja à altura do desafio, escolhi um dicionário com todas as matrizes fatiadas que contêm os dados de pressão de cada pata (agrupada por medição) e a fatia que descreve sua localização (localização na placa e no tempo).

Para esclarecer: walk_sliced_data é um dicionário que contém ['ser_3', 'ser_2', 'sel_1', 'sel_2', 'ser_1', 'sel_3'], que são os nomes das medidas. Cada medida contém outro dicionário, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] (exemplo de 'sel_1') que representa os impactos que foram extraídos.

Observe também que impactos "falsos", como onde a pata é parcialmente medida (no espaço ou no tempo), podem ser ignorados. Eles são úteis apenas porque podem ajudar a reconhecer um padrão, mas não serão analisados.

E para qualquer pessoa interessada, estou mantendo um blog com todas as atualizações sobre o projeto!

Ivo Flipse
fonte
1
Sim, a abordagem que eu estava usando não funciona. Apenas para elaborar, a abordagem que eu estava usando é apenas ordenar os impactos e assumir que a primeira pata a tocar é a mesma que a quinta pata a tocar, e assim por diante. (ou seja, encomende os impactos e use o módulo 4). O problema é que, às vezes, as patas traseiras atingem o sensor depois que a primeira pata toca. Nesse caso, a primeira pata a impactar corresponde à quarta ou terceira pata a impactar. Espero que isso faça algum sentido.
Joe Kington
1
Eu interpretaria as imagens corretamente, pois um dedo de cada pé traseiro exerce significativamente menos pressão que o resto? Parece também que o dedo está sempre voltado para o 'interior', ou seja, para o centro de massa do cão. Você poderia incorporar isso como uma heurística?
Thomas Langston
1
Admito que minhas habilidades limitadas de processamento de imagem estão um pouco enferrujadas, mas é facilmente possível obter o gradiente menos acentuado do grande bloco do meio de cada pata? Parece que o ângulo de menor inclinação ajudaria imensamente (um exemplo desenhado à mão para as patas postadas: imgur.com/y2wBC imgur.com/yVqVU imgur.com/yehOc imgur.com/q0tcD )
user470379
Você poderia esclarecer como os dados walk_sliced_dataestão estruturados? Eu vejo um dicionário de dicionários de matrizes 3D. Se eu fixar a terceira dimensão e plotar as duas primeiras como uma imagem, acho que vejo patas.
Steve Tjoa
@ Thomas, sim, cada pata está claramente carregada de uma maneira distinta. Eu sei o que eu gostaria que o programa para fazer, mas não tenho idéia de como programá-lo ... @ Steve, eu adicionei esclarecimentos na parte inferior :-)
Ivo Flipse

Respostas:

123

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://[email protected]/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.

  1. Use a ordem (temporal e espacial) dos impactos da pata para determinar qual pata é qual.
  2. 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.

Sequência de Impacto Normal

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.

Pata traseira perdida

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 ...)

Sequência de Impacto Anormal

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!

Patas médias

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).

Pata média

Agora podemos analisar as diferenças em relação à média, que são um pouco mais fáceis de reconhecer:

Patas Diferenciais

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_imagefunçã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_vecssão os "eigenpaws".

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: texto alternativo texto alternativo texto alternativo

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!

Joe Kington
fonte
2
Ótima resposta. Também tentei o método eigenpaw, mas não fui tão perseverante quanto você. Um problema que vejo é o registro da pata, ou seja, como o registro facial é enfrentar o reconhecimento. Você encontrou algum problema na normalização da localização e rotação de cada pata? Nesse caso, talvez a pata possa ser pré-processada em algum recurso invariável de rotação de tradução antes de executar o PCA.
precisa saber é o seguinte
2
@ Steve, eu não tentei rotacioná-los, embora eu tenha discutido com Joe sobre como melhorá-lo ainda mais. No entanto, para finalizar meu projeto por enquanto, anotei manualmente todas as patas para que eu possa encerrá-lo. Felizmente, isso também nos permite criar diferentes conjuntos de treinamento para tornar o reconhecimento mais sensível. Para girar as patas, eu estava pensando em usar os dedos, mas como você pode ler no meu blog, isso não é tão fácil como a minha primeira pergunta fez com que pareça ...
Ivo Flipse
@ Basic Sim, mudei para hospedar meu próprio site e movi todo o conteúdo do Wordpress, mas não consegui mais editar meu comentário aqui. Você deve ser capaz de encontrá-los aqui: flipserd.com/blog/ivoflipse/post/improving-the-paw-detection
Ivo Flipse
4

Usando as informações puramente baseadas na duração, acho que você poderia aplicar técnicas de modelagem cinemática; ou seja, cinemática inversa . Combinado com orientação, comprimento, duração e peso total, fornece algum nível de periodicidade que, espero, seja o primeiro passo para resolver o seu problema de "classificação das patas".

Todos esses dados podem ser usados ​​para criar uma lista de polígonos delimitados (ou tuplas), que você pode usar para classificar por tamanho da etapa e depois por paw-ness [index].

Lam Chau
fonte
2

Você pode fazer com que o técnico que executa o teste insira manualmente a primeira pata (ou as duas primeiras)? O processo pode ser:

  • Mostre à imagem a ordem das etapas da técnica e exija que anote a primeira pata.
  • Rotule as outras patas com base na primeira e permita que o técnico faça correções ou execute novamente o teste. Isso permite cães coxos ou de três pernas.
Jamie Ide
fonte
Na verdade, tenho anotações das primeiras patas, embora não sejam perfeitas. No entanto, a primeira pata é sempre uma pata frontal e não me ajuda a separar as patas traseiras. Além disso, a encomenda não é perfeita, como Joe mencionou, porque isso exige que a parte da frente toque o prato no início.
Ivo Flipse
As anotações seriam úteis ao usar o reconhecimento de imagem, por causa das 24 medidas que eu tenho, pelo menos 24 patas já seriam anotadas. Se eles fossem agrupados em 4 grupos, dois deles deveriam conter uma quantidade razoável de qualquer pata frontal o suficiente para tornar um algoritmo razoavelmente certo do agrupamento.
Ivo Flipse
A menos que eu os esteja lendo incorretamente, os ensaios anotados vinculados mostram a pata traseira tocando primeiro em 4 de 6 ensaios.
precisa
Ah, eu quis dizer com respeito ao tempo. Se você percorrer o arquivo, a pata frontal deve sempre ser a primeira a entrar em contato com a placa.
Ivo Flipse