Como posso melhorar minha detecção de pata?

198

Após minha pergunta anterior sobre como encontrar dedos dentro de cada pata , comecei a carregar outras medidas para ver como ela se sustentaria. Infelizmente, rapidamente encontrei um problema com uma das etapas anteriores: reconhecer as patas.

Veja bem, minha prova de conceito basicamente tomou a pressão máxima de cada sensor ao longo do tempo e começaria a procurar a soma de cada linha, até encontrar nela! = 0,0. Em seguida, faz o mesmo para as colunas e assim que encontrar mais de 2 linhas com zero novamente. Ele armazena os valores mínimo e máximo de linhas e colunas em algum índice.

texto alternativo

Como você pode ver na figura, isso funciona muito bem na maioria dos casos. No entanto, existem muitas desvantagens nessa abordagem (além de serem muito primitivas):

  • Os seres humanos podem ter "pés ocos", o que significa que existem várias linhas vazias na própria pegada. Como eu temia que isso pudesse acontecer com cães (grandes) também, esperei pelo menos 2 ou 3 linhas vazias antes de cortar a pata.

    Isso cria um problema se outro contato feito em uma coluna diferente antes de atingir várias linhas vazias, expandindo a área. Eu acho que poderia comparar as colunas e ver se elas excedem um determinado valor, elas devem ser patas separadas.

  • O problema piora quando o cão é muito pequeno ou anda em um ritmo mais alto. O que acontece é que os dedos da pata dianteira ainda estão em contato, enquanto os dedos da pata traseira começam a fazer contato na mesma área da pata dianteira!

    Com o meu script simples, ele não será capaz de dividir esses dois, porque teria que determinar quais quadros dessa área pertencem a qual pata, enquanto atualmente eu só precisaria examinar os valores máximos em todos os quadros.

Exemplos de onde começa a dar errado:

texto alternativo texto alternativo

Portanto, agora estou procurando uma maneira melhor de reconhecer e separar as patas (depois disso, chegarei ao problema de decidir qual é a pata!).

Atualizar:

Estou mexendo na implementação da resposta de Joe (incrível!), Mas estou tendo dificuldades para extrair os dados reais das patas dos meus arquivos.

texto alternativo

As coded_paws me mostram todas as patas diferentes, quando aplicadas à imagem de pressão máxima (veja acima). No entanto, a solução passa por cada quadro (para separar as patas sobrepostas) e define os quatro atributos do retângulo, como coordenadas ou altura / largura.

Não consigo descobrir como pegar esses atributos e armazená-los em alguma variável que eu possa aplicar aos dados de medição. Desde que eu preciso saber para cada pata, qual é a sua localização durante quais quadros e associá-lo a qual pata é (frontal / traseira, esquerda / direita).

Então, como posso usar os atributos Retângulos para extrair esses valores para cada pata?

Tenho as medidas que usei na configuração da pergunta na minha pasta pública do Dropbox ( exemplo 1 , exemplo 2 , exemplo 3 ). Para quem estiver interessado, também criei um blog para mantê-lo atualizado :-)

Ivo Flipse
fonte
Parece que você teria que se afastar do algoritmo de linha / coluna e está limitando informações úteis.
Tamara Wijsman
12
Uau! Software de controle Cat?
ALxx
Na verdade, são dados de cães @alxx ;-) Mas sim, serão usados ​​para diagnosticá-los!
Ivo Flipse
4
Por quê? (deixa pra lá, é mais divertido não saber ...)
Ben Regenspan

Respostas:

358

Se você está apenas querendo regiões (semi) contíguas, já existe uma implementação fácil no Python: o módulo ndimage.morphology do SciPy . Esta é uma operação morfológica de imagem bastante comum .


Basicamente, você tem 5 etapas:

def find_paws(data, smooth_radius=5, threshold=0.0001):
    data = sp.ndimage.uniform_filter(data, smooth_radius)
    thresh = data > threshold
    filled = sp.ndimage.morphology.binary_fill_holes(thresh)
    coded_paws, num_paws = sp.ndimage.label(filled)
    data_slices = sp.ndimage.find_objects(coded_paws)
    return object_slices
  1. Desfoque um pouco os dados de entrada para garantir que as patas tenham uma pegada contínua. (Seria mais eficiente usar apenas um kernel maior (o structurekwarg para as várias scipy.ndimage.morphologyfunções), mas isso não está funcionando corretamente por algum motivo ...)

  2. Limite a matriz para que você tenha uma matriz booleana de locais onde a pressão está acima de algum valor limite (ou seja thresh = data > value)

  3. Preencha os orifícios internos para ter regiões mais limpas ( filled = sp.ndimage.morphology.binary_fill_holes(thresh))

  4. Encontre as regiões contíguas separadas ( coded_paws, num_paws = sp.ndimage.label(filled)). Isso retorna uma matriz com as regiões codificadas por número (cada região é uma área contígua de um número inteiro único (1 até o número de patas) com zeros em qualquer outro lugar)).

  5. Isole as regiões contíguas usando data_slices = sp.ndimage.find_objects(coded_paws). Isso retorna uma lista de tuplas de sliceobjetos, para que você possa obter a região dos dados de cada pata [data[x] for x in data_slices]. Em vez disso, desenharemos um retângulo com base nessas fatias, o que exige um pouco mais de trabalho.


As duas animações abaixo mostram os dados de exemplo "Patas sobrepostas" e "Patas agrupadas". Este método parece estar funcionando perfeitamente. (E para o que vale a pena, isso é executado de maneira muito mais suave do que as imagens GIF abaixo na minha máquina, então o algoritmo de detecção de pata é bastante rápido ...)

Patas sobrepostas Patas agrupadas


Aqui está um exemplo completo (agora com explicações muito mais detalhadas). A grande maioria disso está lendo a entrada e fazendo uma animação. A detecção real da pata é de apenas 5 linhas de código.

import numpy as np
import scipy as sp
import scipy.ndimage

import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle

def animate(input_filename):
    """Detects paws and animates the position and raw data of each frame
    in the input file"""
    # With matplotlib, it's much, much faster to just update the properties
    # of a display object than it is to create a new one, so we'll just update
    # the data and position of the same objects throughout this animation...

    infile = paw_file(input_filename)

    # Since we're making an animation with matplotlib, we need 
    # ion() instead of show()...
    plt.ion()
    fig = plt.figure()
    ax = fig.add_subplot(111)
    fig.suptitle(input_filename)

    # Make an image based on the first frame that we'll update later
    # (The first frame is never actually displayed)
    im = ax.imshow(infile.next()[1])

    # Make 4 rectangles that we can later move to the position of each paw
    rects = [Rectangle((0,0), 1,1, fc='none', ec='red') for i in range(4)]
    [ax.add_patch(rect) for rect in rects]

    title = ax.set_title('Time 0.0 ms')

    # Process and display each frame
    for time, frame in infile:
        paw_slices = find_paws(frame)

        # Hide any rectangles that might be visible
        [rect.set_visible(False) for rect in rects]

        # Set the position and size of a rectangle for each paw and display it
        for slice, rect in zip(paw_slices, rects):
            dy, dx = slice
            rect.set_xy((dx.start, dy.start))
            rect.set_width(dx.stop - dx.start + 1)
            rect.set_height(dy.stop - dy.start + 1)
            rect.set_visible(True)

        # Update the image data and title of the plot
        title.set_text('Time %0.2f ms' % time)
        im.set_data(frame)
        im.set_clim([frame.min(), frame.max()])
        fig.canvas.draw()

def find_paws(data, smooth_radius=5, threshold=0.0001):
    """Detects and isolates contiguous regions in the input array"""
    # Blur the input data a bit so the paws have a continous footprint 
    data = sp.ndimage.uniform_filter(data, smooth_radius)
    # Threshold the blurred data (this needs to be a bit > 0 due to the blur)
    thresh = data > threshold
    # Fill any interior holes in the paws to get cleaner regions...
    filled = sp.ndimage.morphology.binary_fill_holes(thresh)
    # Label each contiguous paw
    coded_paws, num_paws = sp.ndimage.label(filled)
    # Isolate the extent of each paw
    data_slices = sp.ndimage.find_objects(coded_paws)
    return data_slices

def paw_file(filename):
    """Returns a iterator that yields the time and data in each frame
    The infile is an ascii file of timesteps formatted similar to this:

    Frame 0 (0.00 ms)
    0.0 0.0 0.0
    0.0 0.0 0.0

    Frame 1 (0.53 ms)
    0.0 0.0 0.0
    0.0 0.0 0.0
    ...
    """
    with open(filename) as infile:
        while True:
            try:
                time, data = read_frame(infile)
                yield time, data
            except StopIteration:
                break

def read_frame(infile):
    """Reads a frame from the infile."""
    frame_header = infile.next().strip().split()
    time = float(frame_header[-2][1:])
    data = []
    while True:
        line = infile.next().strip().split()
        if line == []:
            break
        data.append(line)
    return time, np.array(data, dtype=np.float)

if __name__ == '__main__':
    animate('Overlapping paws.bin')
    animate('Grouped up paws.bin')
    animate('Normal measurement.bin')

Atualização: Quanto à identificação de qual pata está em contato com o sensor em que horários, a solução mais simples é fazer a mesma análise, mas usar todos os dados de uma só vez. (ou seja, empilhe a entrada em uma matriz 3D e trabalhe com ela, em vez dos períodos de tempo individuais.) Como as funções ndimage do SciPy são destinadas a trabalhar com matrizes n-dimensionais, não precisamos modificar a função original de localização de patas em absoluto.

# This uses functions (and imports) in the previous code example!!
def paw_regions(infile):
    # Read in and stack all data together into a 3D array
    data, time = [], []
    for t, frame in paw_file(infile):
        time.append(t)
        data.append(frame)
    data = np.dstack(data)
    time = np.asarray(time)

    # Find and label the paw impacts
    data_slices, coded_paws = find_paws(data, smooth_radius=4)

    # Sort by time of initial paw impact... This way we can determine which
    # paws are which relative to the first paw with a simple modulo 4.
    # (Assuming a 4-legged dog, where all 4 paws contacted the sensor)
    data_slices.sort(key=lambda dat_slice: dat_slice[2].start)

    # Plot up a simple analysis
    fig = plt.figure()
    ax1 = fig.add_subplot(2,1,1)
    annotate_paw_prints(time, data, data_slices, ax=ax1)
    ax2 = fig.add_subplot(2,1,2)
    plot_paw_impacts(time, data_slices, ax=ax2)
    fig.suptitle(infile)

def plot_paw_impacts(time, data_slices, ax=None):
    if ax is None:
        ax = plt.gca()

    # Group impacts by paw...
    for i, dat_slice in enumerate(data_slices):
        dx, dy, dt = dat_slice
        paw = i%4 + 1
        # Draw a bar over the time interval where each paw is in contact
        ax.barh(bottom=paw, width=time[dt].ptp(), height=0.2, 
                left=time[dt].min(), align='center', color='red')
    ax.set_yticks(range(1, 5))
    ax.set_yticklabels(['Paw 1', 'Paw 2', 'Paw 3', 'Paw 4'])
    ax.set_xlabel('Time (ms) Since Beginning of Experiment')
    ax.yaxis.grid(True)
    ax.set_title('Periods of Paw Contact')

def annotate_paw_prints(time, data, data_slices, ax=None):
    if ax is None:
        ax = plt.gca()

    # Display all paw impacts (sum over time)
    ax.imshow(data.sum(axis=2).T)

    # Annotate each impact with which paw it is
    # (Relative to the first paw to hit the sensor)
    x, y = [], []
    for i, region in enumerate(data_slices):
        dx, dy, dz = region
        # Get x,y center of slice...
        x0 = 0.5 * (dx.start + dx.stop)
        y0 = 0.5 * (dy.start + dy.stop)
        x.append(x0); y.append(y0)

        # Annotate the paw impacts         
        ax.annotate('Paw %i' % (i%4 +1), (x0, y0),  
            color='red', ha='center', va='bottom')

    # Plot line connecting paw impacts
    ax.plot(x,y, '-wo')
    ax.axis('image')
    ax.set_title('Order of Steps')

texto alternativo


texto alternativo


texto alternativo

Joe Kington
fonte
82
Eu não posso nem começar a explicar o quão incrível você responde!
Ivo Flipse
1
@ Ivo: Sim, eu gostaria de votar em Joe também :), mas devo começar uma nova pergunta, ou talvez @ Joe, se quiser, responda aqui? stackoverflow.com/questions/2546780/…
unutbu
2
Na verdade, eu apenas joguei fora .png's e fiz um convert *.png output.gif. Certamente, já tive o imagemagick colocando minha máquina de joelhos antes, embora funcionasse bem neste exemplo. No passado, eu usei este script: svn.effbot.python-hosting.com/pil/Scripts/gifmaker.py para escrever diretamente um gif animado de python sem salvar os quadros individuais. Espero que ajude! Vou postar um exemplo na pergunta @unutbu mencionada.
9788 Joe Joneston
1
Obrigado pela informação, @ Joe. Parte do meu problema foi deixar de usar bbox_inches='tight'no plt.savefig, o outro era impaciência :)
unutbu
4
Vaca sagrada, eu apenas tenho que dizer uau em quão grande é essa resposta.
andersoj
4

Eu não sou especialista em detecção de imagens e não conheço Python, mas vou tentar ...

Para detectar patas individuais, você deve primeiro selecionar apenas tudo com uma pressão maior que um pequeno limiar, muito próximo de nenhuma pressão. Todo pixel / ponto acima disso deve ser "marcado". Em seguida, todos os pixels adjacentes a todos os pixels "marcados" ficam marcados e esse processo é repetido algumas vezes. Massas totalmente conectadas seriam formadas, para que você tenha objetos distintos. Então, cada "objeto" possui um valor mínimo e máximo de x e y, para que as caixas delimitadoras possam ser agrupadas em torno delas.

Pseudo-código:

(MARK) ALL PIXELS ABOVE (0.5)

(MARK) ALL PIXELS (ADJACENT) TO (MARK) PIXELS

REPEAT (STEP 2) (5) TIMES

SEPARATE EACH TOTALLY CONNECTED MASS INTO A SINGLE OBJECT

MARK THE EDGES OF EACH OBJECT, AND CUT APART TO FORM SLICES.

Isso deveria fazê-lo.

TaslemGuy
fonte
0

Nota: digo pixel, mas isso pode ser regiões usando uma média dos pixels. A otimização é outra questão ...

Parece que você precisa analisar uma função (pressão ao longo do tempo) para cada pixel e determinar onde a função gira (quando ela muda> X na outra direção, é considerada uma vez para combater erros).

Se você souber em que quadros ele gira, você saberá o quadro em que a pressão foi mais difícil e saberá onde foi o menos difícil entre as duas patas. Em teoria, você conheceria os dois quadros em que as patas pressionavam mais e pode calcular uma média desses intervalos.

Depois disso, chegarei ao problema de decidir qual é a pata!

Este é o mesmo passeio de antes, saber quando cada pata aplica mais pressão ajuda a decidir.

Tamara Wijsman
fonte