Comment puis-je améliorer ma détection de patte?

198

Après ma question précédente sur la recherche d'orteils dans chaque patte , j'ai commencé à charger d'autres mesures pour voir comment elles se maintiendraient. Malheureusement, j'ai rapidement rencontré un problème avec l'une des étapes précédentes: reconnaître les pattes.

Vous voyez, ma preuve de concept a essentiellement pris la pression maximale de chaque capteur au fil du temps et commencerait à rechercher la somme de chaque ligne, jusqu'à ce qu'elle trouve cela! = 0,0. Ensuite, il fait de même pour les colonnes et dès qu'il trouve plus de 2 lignes avec zéro. Il stocke les valeurs minimale et maximale de ligne et de colonne dans un certain index.

texte alternatif

Comme vous pouvez le voir sur la figure, cela fonctionne assez bien dans la plupart des cas. Cependant, cette approche présente de nombreux inconvénients (en plus d'être très primitive):

  • Les humains peuvent avoir des «pieds creux», ce qui signifie qu'il y a plusieurs rangées vides dans l'empreinte elle-même. Comme je craignais que cela ne se produise également avec les (gros) chiens, j'ai attendu au moins 2 ou 3 rangées vides avant de couper la patte.

    Cela crée un problème si un autre contact établi dans une colonne différente avant d'atteindre plusieurs lignes vides, élargissant ainsi la zone. Je pense que je pourrais comparer les colonnes et voir si elles dépassent une certaine valeur, elles doivent être des pattes séparées.

  • Le problème s'aggrave lorsque le chien est très petit ou marche à un rythme plus élevé. Ce qui se passe, c'est que les orteils de la patte avant sont toujours en contact, tandis que les orteils de la patte arrière commencent tout juste à entrer en contact dans la même zone que la patte avant!

    Avec mon script simple, il ne sera pas en mesure de diviser ces deux, car il devrait déterminer quelles images de cette zone appartiennent à quelle patte, alors qu'actuellement je n'aurais qu'à regarder les valeurs maximales sur toutes les images.

Exemples de cas où cela commence à mal tourner:

texte alternatif texte alternatif

Alors maintenant, je cherche un meilleur moyen de reconnaître et de séparer les pattes (après quoi j'arriverai au problème de décider de quelle patte il s'agit!).

Mettre à jour:

J'ai essayé de faire implémenter la réponse de Joe (génial!), Mais j'ai des difficultés à extraire les données de patte réelles de mes fichiers.

texte alternatif

Le coded_paws me montre toutes les différentes pattes, lorsqu'il est appliqué à l'image de pression maximale (voir ci-dessus). Cependant, la solution passe sur chaque cadre (pour séparer les pattes qui se chevauchent) et définit les quatre attributs Rectangle, tels que les coordonnées ou la hauteur / largeur.

Je ne peux pas comprendre comment prendre ces attributs et les stocker dans une variable que je peux appliquer aux données de mesure. Étant donné que je dois savoir pour chaque patte, quel est son emplacement pendant quelles images et coupler cela à quelle patte elle est (avant / arrière, gauche / droite).

Alors, comment puis-je utiliser les attributs Rectangles pour extraire ces valeurs pour chaque patte?

J'ai les mesures que j'ai utilisées dans la configuration des questions dans mon dossier Dropbox public ( exemple 1 , exemple 2 , exemple 3 ). Pour toute personne intéressée, j'ai également créé un blog pour vous tenir au courant :-)

Ivo Flipse
la source
Il semble que vous deviez vous détourner de l'algorithme de ligne / colonne et limiter les informations utiles.
Tamara Wijsman
12
Hou la la! Logiciel de contrôle Cat?
alxx
Ce sont des données de chiens en fait @alxx ;-) Mais oui, elles seront utilisées pour les diagnostiquer!
Ivo Flipse
4
Pourquoi? (peu importe, c'est plus amusant de ne pas savoir ...)
Ben Regenspan

Réponses:

358

Si vous simplement vouloir (semi) régions contiguës, il y a déjà une implémentation facile en Python: SciPy de » ndimage.morphology module. Il s'agit d'une opération de morphologie d'image assez courante .


Fondamentalement, vous avez 5 étapes:

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. Brouillez un peu les données d'entrée pour vous assurer que les pattes ont une empreinte continue. (Il serait plus efficace d'utiliser simplement un noyau plus grand (le structurekwarg des diverses scipy.ndimage.morphologyfonctions) mais cela ne fonctionne pas tout à fait correctement pour une raison quelconque ...)

  2. Seuil du tableau de sorte que vous ayez un tableau booléen d'endroits où la pression dépasse une certaine valeur de seuil (c.-à-d. thresh = data > value)

  3. Remplissez les trous internes afin d'avoir des régions plus propres ( filled = sp.ndimage.morphology.binary_fill_holes(thresh))

  4. Trouvez les régions contiguës distinctes ( coded_paws, num_paws = sp.ndimage.label(filled)). Cela renvoie un tableau avec les régions codées par un numéro (chaque région est une zone contiguë d'un entier unique (1 jusqu'au nombre de pattes) avec des zéros partout ailleurs)).

  5. Isolez les régions contiguës à l'aide de data_slices = sp.ndimage.find_objects(coded_paws). Cela renvoie une liste de tuples d' sliceobjets, vous pouvez donc obtenir la région des données pour chaque patte avec [data[x] for x in data_slices]. Au lieu de cela, nous allons dessiner un rectangle basé sur ces tranches, ce qui prend un peu plus de travail.


Les deux animations ci-dessous montrent vos données d'exemple "Pattes superposées" et "Pattes groupées". Cette méthode semble fonctionner parfaitement. (Et pour tout ce que ça vaut, cela fonctionne beaucoup plus facilement que les images GIF ci-dessous sur ma machine, donc l'algorithme de détection de patte est assez rapide ...)

Pattes qui se chevauchent Pattes groupées


Voici un exemple complet (maintenant avec des explications beaucoup plus détaillées). La grande majorité de ceci est la lecture de l'entrée et la création d'une animation. La détection de patte réelle n'est que de 5 lignes de code.

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

Mise à jour: En ce qui concerne l'identification de la patte en contact avec le capteur à quels moments, la solution la plus simple consiste à effectuer la même analyse, mais à utiliser toutes les données en même temps. (c.-à-d. empiler l'entrée dans un tableau 3D et travailler avec elle, au lieu des délais individuels.) Parce que les fonctions ndimage de SciPy sont censées fonctionner avec des tableaux à n dimensions, nous n'avons pas à modifier la fonction de recherche de patte d'origine du tout.

# 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')

texte alternatif


texte alternatif


texte alternatif

Joe Kington
la source
82
Je ne peux même pas commencer à expliquer à quel point vous répondez est génial!
Ivo Flipse
1
@Ivo: Oui, j'apprécierais aussi de voter pour Joe :) mais devrais-je commencer une nouvelle question, ou peut-être @Joe, si vous voulez, répondre ici? stackoverflow.com/questions/2546780/…
unutbu
2
En fait, je viens de vider les .png et j'ai fait un convert *.png output.gif. J'ai certainement eu imagemagick mettre ma machine à genoux auparavant, même si cela a bien fonctionné pour cet exemple. Dans le passé, j'ai utilisé ce script: svn.effbot.python-hosting.com/pil/Scripts/gifmaker.py pour écrire directement un gif animé à partir de python sans enregistrer les images individuelles. J'espère que cela pourra aider! Je posterai un exemple à la question mentionnée par @unutbu.
Joe Kington
1
Merci pour l'information, @Joe. Une partie de mon problème était de ne pas l'utiliser bbox_inches='tight'dans l'autre plt.savefig, l'autre était l'impatience :)
unutbu
4
Vache sacrée, je viens de dire wow à quel point cette réponse est.
andersoj
4

Je ne suis pas un expert en détection d'images, et je ne connais pas Python, mais je vais lui donner un coup ...

Pour détecter des pattes individuelles, vous devez d'abord sélectionner tout ce qui a une pression supérieure à un petit seuil, très proche de l'absence de pression du tout. Chaque pixel / point au-dessus doit être "marqué". Ensuite, chaque pixel adjacent à tous les pixels "marqués" devient marqué, et ce processus est répété plusieurs fois. Des masses totalement connectées seraient formées, vous avez donc des objets distincts. Ensuite, chaque "objet" a une valeur minimale et maximale x et y, de sorte que les boîtes englobantes peuvent être soigneusement rangées autour d'eux.

Pseudocode:

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

Cela devrait le faire.

TaslemGuy
la source
0

Remarque: je dis pixel, mais cela pourrait être des régions utilisant une moyenne des pixels. L'optimisation est un autre problème ...

On dirait que vous devez analyser une fonction (pression dans le temps) pour chaque pixel et déterminer où la fonction tourne (lorsqu'elle change> X dans l'autre sens, elle est considérée comme un tour pour contrer les erreurs).

Si vous savez à quels cadres il tourne, vous saurez où la pression a été la plus dure et vous saurez où elle a été la moins dure entre les deux pattes. En théorie, vous sauriez alors les deux images où les pattes ont le plus pressé et pouvez calculer une moyenne de ces intervalles.

après quoi j'arriverai au problème de décider de quelle patte il s'agit!

C'est le même tour qu'avant, savoir quand chaque patte applique le plus de pression vous aide à décider.

Tamara Wijsman
la source