Wie kann ich meine Pfotenerkennung verbessern?

198

Nach meiner vorherigen Frage zum Finden von Zehen in jeder Pfote begann ich, andere Messungen zu laden, um zu sehen, wie es halten würde. Leider stieß ich schnell auf ein Problem mit einem der vorhergehenden Schritte: Erkennen der Pfoten.

Sie sehen, mein Proof of Concept hat im Grunde genommen den maximalen Druck jedes Sensors über die Zeit genommen und würde anfangen, nach der Summe jeder Zeile zu suchen, bis er darauf findet! = 0.0. Dann macht es dasselbe für die Spalten und sobald es mehr als 2 Zeilen findet, sind diese wieder Null. Es speichert die minimalen und maximalen Zeilen- und Spaltenwerte in einem Index.

Alt-Text

Wie Sie in der Abbildung sehen können, funktioniert dies in den meisten Fällen recht gut. Dieser Ansatz hat jedoch viele Nachteile (abgesehen davon, dass er sehr primitiv ist):

  • Menschen können "hohle Füße" haben, was bedeutet, dass sich innerhalb des Fußabdrucks selbst mehrere leere Reihen befinden. Da ich befürchtete, dass dies auch bei (großen) Hunden passieren könnte, wartete ich auf mindestens 2 oder 3 leere Reihen, bevor ich die Pfote abschnitt.

    Dies führt zu einem Problem, wenn ein anderer Kontakt in einer anderen Spalte hergestellt wird, bevor er mehrere leere Zeilen erreicht, wodurch der Bereich erweitert wird. Ich glaube, ich könnte die Spalten vergleichen und sehen, ob sie einen bestimmten Wert überschreiten. Es müssen separate Pfoten sein.

  • Das Problem wird schlimmer, wenn der Hund sehr klein ist oder schneller läuft. Was passiert ist, dass die Zehen der Vorderpfote immer noch Kontakt haben, während die Zehen der Hinterpfote gerade im gleichen Bereich wie die Vorderpfote Kontakt aufnehmen!

    Mit meinem einfachen Skript kann es diese beiden nicht aufteilen, da es bestimmen müsste, welche Frames dieses Bereichs zu welcher Pfote gehören, während ich derzeit nur die Maximalwerte über alle Frames betrachten müsste.

Beispiele dafür, wo es schief geht:

Alt-Text Alt-Text

Jetzt suche ich nach einer besseren Möglichkeit, die Pfoten zu erkennen und zu trennen (danach komme ich zum Problem, zu entscheiden, um welche Pfote es sich handelt!).

Aktualisieren:

Ich habe versucht, Joes (großartige!) Antwort umzusetzen, aber ich habe Schwierigkeiten, die tatsächlichen Pfoten-Daten aus meinen Dateien zu extrahieren.

Alt-Text

Die coded_paws zeigen mir alle verschiedenen Pfoten, wenn sie auf das Bild mit maximalem Druck angewendet werden (siehe oben). Die Lösung geht jedoch über jeden Frame (um überlappende Pfoten zu trennen) und legt die vier Rechteckattribute fest, z. B. Koordinaten oder Höhe / Breite.

Ich kann nicht herausfinden, wie ich diese Attribute in einer Variablen speichern kann, die ich auf die Messdaten anwenden kann. Da ich für jede Pfote wissen muss, wo sie sich in welchen Frames befindet, und diese mit der Pfote koppeln muss (vorne / hinten, links / rechts).

Wie kann ich die Rechteck-Attribute verwenden, um diese Werte für jede Pfote zu extrahieren?

Ich habe die Messungen, die ich im Fragen-Setup verwendet habe, in meinem öffentlichen Dropbox-Ordner ( Beispiel 1 , Beispiel 2 , Beispiel 3 ). Für alle Interessierten habe ich auch einen Blog eingerichtet , um euch auf dem Laufenden zu halten :-)

Ivo Flipse
quelle
Es scheint, als müssten Sie sich vom Zeilen- / Spaltenalgorithmus abwenden und nützliche Informationen einschränken.
Tamara Wijsman
12
Beeindruckend! Katzenkontrollsoftware?
Alxx
Es sind tatsächlich Hundedaten @alxx ;-) Aber ja, es wird verwendet, um sie zu diagnostizieren!
Ivo Flipse
4
Warum? (egal, es macht mehr Spaß, nicht zu wissen ...)
Ben Regenspan

Antworten:

358

Wenn Sie nur (halb) zusammenhängende Regionen möchten, gibt es in Python bereits eine einfache Implementierung: SciPy ‚s ndimage.morphology Modul. Dies ist eine ziemlich häufige Bildmorphologieoperation .


Grundsätzlich haben Sie 5 Schritte:

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. Verwischen Sie die Eingabedaten ein wenig, um sicherzustellen, dass die Pfoten einen kontinuierlichen Fußabdruck haben. (Es wäre effizienter, nur einen größeren Kernel zu verwenden (das structureKwarg für die verschiedenen scipy.ndimage.morphologyFunktionen), aber dies funktioniert aus irgendeinem Grund nicht ganz richtig ...)

  2. Schwellenwert für das Array, sodass Sie ein boolesches Array von Stellen haben, an denen der Druck über einem bestimmten Schwellenwert liegt (dh thresh = data > value)

  3. Füllen Sie alle inneren Löcher, damit Sie sauberere Bereiche haben ( filled = sp.ndimage.morphology.binary_fill_holes(thresh))

  4. Finden Sie die einzelnen zusammenhängenden Regionen (coded_paws, num_paws = sp.ndimage.label(filled) ). Dies gibt ein Array mit den durch die Nummer codierten Regionen zurück (jede Region ist ein zusammenhängender Bereich einer eindeutigen Ganzzahl (1 bis zur Anzahl der Pfoten) mit Nullen überall sonst)).

  5. Isolieren Sie die zusammenhängenden Bereiche mit data_slices = sp.ndimage.find_objects(coded_paws). Dies gibt eine Liste von Tupeln von sliceObjekten zurück, sodass Sie den Bereich der Daten für jede Pfote mit abrufen können [data[x] for x in data_slices]. Stattdessen zeichnen wir ein Rechteck basierend auf diesen Slices, was etwas mehr Arbeit erfordert.


Die beiden folgenden Animationen zeigen Ihre Beispieldaten "Überlappende Pfoten" und "Gruppierte Pfoten". Diese Methode scheint perfekt zu funktionieren. (Und was auch immer es wert ist, dies läuft viel reibungsloser als die GIF-Bilder unten auf meinem Computer, so dass der Pfotenerkennungsalgorithmus ziemlich schnell ist ...)

Überlappende Pfoten Gruppierte Pfoten


Hier ist ein vollständiges Beispiel (jetzt mit viel detaillierteren Erklärungen). Die überwiegende Mehrheit davon liest die Eingabe und erstellt eine Animation. Die eigentliche Pfotenerkennung besteht nur aus 5 Codezeilen.

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

Update: Um festzustellen, welche Pfote zu welchen Zeiten mit dem Sensor in Kontakt steht, besteht die einfachste Lösung darin, nur dieselbe Analyse durchzuführen, aber alle Daten gleichzeitig zu verwenden. (dh stapeln Sie die Eingabe in ein 3D-Array und arbeiten Sie damit anstelle der einzelnen Zeitrahmen.) Da die ndimage-Funktionen von SciPy für n-dimensionale Arrays vorgesehen sind, müssen wir die ursprüngliche Pfotenfindungsfunktion nicht ändern überhaupt.

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

Alt-Text


Alt-Text


Alt-Text

Joe Kington
quelle
82
Ich kann gar nicht erklären, wie großartig deine Antwort ist!
Ivo Flipse
1
@Ivo: Ja, ich würde es auch genießen, Joe noch mehr zu bewerten :) aber sollte ich eine neue Frage stellen oder vielleicht @Joe, wenn Sie möchten, hier antworten? stackoverflow.com/questions/2546780/…
unutbu
2
Ich habe gerade .png's rausgeschmissen und a convert *.png output.gif. Ich hatte sicherlich schon einmal Imagemagick, der meine Maschine in die Knie zwang, obwohl es in diesem Beispiel gut funktioniert hat. In der Vergangenheit habe ich dieses Skript verwendet: svn.effbot.python-hosting.com/pil/Scripts/gifmaker.py , um ein animiertes GIF direkt aus Python zu schreiben, ohne die einzelnen Frames zu speichern. Hoffentlich hilft das! Ich werde ein Beispiel bei der genannten Frage @unutbu posten.
Joe Kington
1
Danke für die Information, @Joe. Ein Teil meines Problems war die Vernachlässigung der Verwendung bbox_inches='tight'in der plt.savefig, der andere war Ungeduld :)
Unutbu
4
Heilige Kuh, muss ich nur sagen wow, wie großartig diese Antwort ist.
andersoj
4

Ich bin kein Experte für Bilderkennung und kenne Python nicht, aber ich werde es versuchen ...

Um einzelne Pfoten zu erkennen, sollten Sie zunächst nur alles auswählen, dessen Druck über einem kleinen Schwellenwert liegt und der nahezu keinem Druck entspricht. Jedes Pixel / jeder Punkt darüber sollte "markiert" sein. Dann wird jedes Pixel neben allen "markierten" Pixeln markiert, und dieser Vorgang wird einige Male wiederholt. Es würden sich Massen bilden, die vollständig miteinander verbunden sind, sodass Sie unterschiedliche Objekte haben. Dann hat jedes "Objekt" einen minimalen und maximalen x- und y-Wert, so dass Begrenzungsrahmen ordentlich um sie herum gepackt werden können.

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.

Das sollte es ungefähr tun.

TaslemGuy
quelle
0

Hinweis: Ich sage Pixel, aber dies können Regionen sein, die einen Durchschnitt der Pixel verwenden. Optimierung ist ein weiteres Problem ...

Klingt so, als müssten Sie für jedes Pixel eine Funktion (Druck über die Zeit) analysieren und bestimmen, wohin sich die Funktion dreht (wenn sich> X in die andere Richtung ändert, wird dies als Drehung angesehen, um Fehlern entgegenzuwirken).

Wenn Sie wissen, an welchen Rahmen es sich dreht, kennen Sie den Rahmen, in dem der Druck am stärksten war, und Sie wissen, wo er zwischen den beiden Pfoten am wenigsten hart war. Theoretisch kennen Sie dann die beiden Frames, in denen die Pfoten am stärksten gedrückt haben, und können einen Durchschnitt dieser Intervalle berechnen.

Danach komme ich zum Problem der Entscheidung, um welche Pfote es sich handelt!

Dies ist die gleiche Tour wie zuvor. Wenn Sie wissen, wann jede Pfote den größten Druck ausübt, können Sie sich entscheiden.

Tamara Wijsman
quelle