In Ordung! Ich habe es endlich geschafft, dass etwas konsequent funktioniert! Dieses Problem hat mich mehrere Tage lang angezogen ... Lustiges Zeug! Entschuldigen Sie die Länge dieser Antwort, aber ich muss einige Dinge etwas näher erläutern ... (Obwohl ich möglicherweise einen Rekord für die längste Nicht-Spam-Stackoverflow-Antwort aller Zeiten aufstellen kann!)
Als Randnotiz verwende ich den vollständigen Datensatz, zu dem Ivo in seiner ursprünglichen Frage einen Link bereitgestellt hat . Es handelt sich um eine Reihe von rar-Dateien (eine pro Hund), die jeweils mehrere verschiedene Versuchsläufe enthalten, die als ASCII-Arrays gespeichert sind. Anstatt zu versuchen, eigenständige Codebeispiele in diese Frage zu kopieren und einzufügen , finden Sie hier ein Bitbucket-Quecksilber-Repository mit vollständigem, eigenständigem Code. Sie können es mit klonen
hg clone https://joferkington@bitbucket.org/joferkington/paw-analysis
Überblick
Wie Sie in Ihrer Frage festgestellt haben, gibt es im Wesentlichen zwei Möglichkeiten, um das Problem anzugehen. Ich werde beide auf unterschiedliche Weise verwenden.
- Verwenden Sie die (zeitliche und räumliche) Reihenfolge der Pfotenstöße, um zu bestimmen, welche Pfote welche ist.
- Versuchen Sie, den "Pfotenabdruck" nur anhand seiner Form zu identifizieren.
Grundsätzlich funktioniert die erste Methode so, dass die Pfoten des Hundes dem in Ivos Frage oben gezeigten trapezartigen Muster folgen, schlägt jedoch fehl, wenn die Pfoten diesem Muster nicht folgen. Es ist ziemlich einfach, programmgesteuert zu erkennen, wenn es nicht funktioniert.
Daher können wir die Messungen, bei denen es funktioniert hat, verwenden, um einen Trainingsdatensatz (von ~ 2000 Pfotenstößen von ~ 30 verschiedenen Hunden) zu erstellen, um zu erkennen, welche Pfote welche ist, und das Problem reduziert sich auf eine überwachte Klassifizierung (mit einigen zusätzlichen Falten). .. Die Bilderkennung ist etwas schwieriger als ein "normales" überwachtes Klassifizierungsproblem.
Musteranalyse
Um auf die erste Methode einzugehen: Wenn ein Hund normal läuft (nicht rennt!) (Was einige dieser Hunde möglicherweise nicht sind), erwarten wir, dass die Pfoten in der folgenden Reihenfolge aufschlagen: vorne links, hinten rechts, vorne rechts, hinten links , Vorne links usw. Das Muster kann entweder mit der vorderen linken oder der vorderen rechten Pfote beginnen.
Wenn dies immer der Fall wäre, könnten wir die Auswirkungen einfach nach der anfänglichen Kontaktzeit sortieren und sie mit einem Modulo 4 nach Pfoten gruppieren.
Selbst wenn alles "normal" ist, funktioniert dies nicht. Dies ist auf die trapezartige Form des Musters zurückzuführen. Eine Hinterpfote fällt räumlich hinter die vorherige Vorderpfote.
Daher fällt der Aufprall der Hinterpfote nach dem ersten Aufprall der Vorderpfote häufig von der Sensorplatte und wird nicht aufgezeichnet. In ähnlicher Weise ist der letzte Pfotenaufprall oft nicht der nächste Pfote in der Sequenz, da der Pfotenaufprall, bevor er von der Sensorplatte auftrat und nicht aufgezeichnet wurde.
Trotzdem können wir anhand der Form des Pfotenaufprallmusters bestimmen, wann dies geschehen ist und ob wir mit einer linken oder rechten Vorderpfote begonnen haben. (Ich ignoriere tatsächlich Probleme mit dem letzten Aufprall hier. Es ist jedoch nicht allzu schwer, ihn hinzuzufügen.)
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
Trotz alledem funktioniert es häufig nicht richtig. Viele der Hunde im vollständigen Datensatz scheinen zu rennen, und die Pfotenstöße folgen nicht der gleichen zeitlichen Reihenfolge wie beim Gehen des Hundes. (Oder vielleicht hat der Hund nur schwere Hüftprobleme ...)
Glücklicherweise können wir immer noch programmgesteuert feststellen, ob die Pfotenstöße unserem erwarteten räumlichen Muster folgen oder nicht:
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
Obwohl die einfache räumliche Klassifizierung nicht immer funktioniert, können wir daher mit hinreichender Sicherheit feststellen, wann sie funktioniert.
Trainingsdatensatz
Aus den musterbasierten Klassifikationen, bei denen es richtig funktioniert hat, können wir einen sehr großen Trainingsdatensatz korrekt klassifizierter Pfoten erstellen (~ 2400 Pfotenstöße von 32 verschiedenen Hunden!).
Wir können jetzt anfangen zu sehen, wie eine "durchschnittliche" vordere linke Pfote usw. aussieht.
Dazu benötigen wir eine Art "Pfotenmetrik", die für jeden Hund die gleiche Dimension hat. (Im vollständigen Datensatz gibt es sowohl sehr große als auch sehr kleine Hunde!) Ein Pfotenabdruck eines irischen Elchhundes ist sowohl viel breiter als auch viel "schwerer" als ein Pfotenabdruck eines Zwergpudels. Wir müssen jeden Pfotenabdruck neu skalieren, damit a) sie die gleiche Anzahl von Pixeln haben und b) die Druckwerte standardisiert sind. Zu diesem Zweck habe ich jeden Pfotenabdruck erneut auf ein 20x20-Raster abgetastet und die Druckwerte basierend auf dem maximalen, minimalen und mittleren Druckwert für den Pfotenaufprall neu skaliert.
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
Nach all dem können wir uns endlich ansehen, wie eine durchschnittliche Pfote links vorne, hinten rechts usw. aussieht. Beachten Sie, dass dies über> 30 Hunde mit sehr unterschiedlichen Größen gemittelt wird und wir anscheinend konsistente Ergebnisse erzielen!
Bevor wir jedoch eine Analyse dieser durchführen, müssen wir den Mittelwert (die durchschnittliche Pfote für alle Beine aller Hunde) subtrahieren.
Jetzt können wir die Unterschiede zum Mittelwert analysieren, die etwas leichter zu erkennen sind:
Bildbasierte Pfotenerkennung
Ok ... Wir haben endlich eine Reihe von Mustern, mit denen wir versuchen können, die Pfoten abzugleichen. Jede Pfote kann als 400-dimensionaler Vektor (von der paw_image
Funktion zurückgegeben) behandelt werden, der mit diesen vier 400-dimensionalen Vektoren verglichen werden kann.
Wenn wir nur einen "normalen" überwachten Klassifizierungsalgorithmus verwenden (dh anhand eines einfachen Abstands herausfinden, welches der 4 Muster einem bestimmten Pfotenabdruck am nächsten kommt), funktioniert dies leider nicht konsistent. Tatsächlich ist es nicht viel besser als zufällige Zufälle im Trainingsdatensatz.
Dies ist ein häufiges Problem bei der Bilderkennung. Aufgrund der hohen Dimensionalität der Eingabedaten und der etwas "unscharfen" Natur der Bilder (dh benachbarte Pixel haben eine hohe Kovarianz) ergibt ein einfaches Betrachten des Unterschieds eines Bildes von einem Vorlagenbild kein sehr gutes Maß für die Ähnlichkeit ihrer Formen.
Eigenpfoten
Um dies zu umgehen, müssen wir eine Reihe von "Eigenpfoten" erstellen (genau wie "Eigengesichter" bei der Gesichtserkennung) und jeden Pfotenabdruck als eine Kombination dieser Eigenpfoten beschreiben. Dies ist identisch mit der Hauptkomponentenanalyse und bietet im Grunde eine Möglichkeit, die Dimensionalität unserer Daten zu reduzieren, sodass der Abstand ein gutes Maß für die Form ist.
Da wir mehr Trainingsbilder als Dimensionen haben (2400 vs 400), ist es nicht erforderlich, eine "ausgefallene" lineare Algebra für die Geschwindigkeit zu erstellen. Wir können direkt mit der Kovarianzmatrix des Trainingsdatensatzes arbeiten:
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
Dies basis_vecs
sind die "Eigenpfoten".
Um diese zu verwenden, punktieren wir einfach (dh Matrixmultiplikation) jedes Pfotenbild (als 400-dimensionaler Vektor anstelle eines 20x20-Bildes) mit den Basisvektoren. Dies gibt uns einen 50-dimensionalen Vektor (ein Element pro Basisvektor), mit dem wir das Bild klassifizieren können. Anstatt ein 20x20-Bild mit dem 20x20-Bild jeder "Vorlagen" -Pfote zu vergleichen, vergleichen wir das 50-dimensionale, transformierte Bild mit jeder 50-dimensionalen transformierten Vorlagenpfote. Dies ist viel weniger empfindlich gegenüber kleinen Abweichungen in der genauen Positionierung jedes Zehs usw. und reduziert die Dimensionalität des Problems im Grunde genommen auf die relevanten Abmessungen.
Eigenpfotenbasierte Pfotenklassifikation
Jetzt können wir einfach den Abstand zwischen den 50-dimensionalen Vektoren und den "Schablonen" -Vektoren für jedes Bein verwenden, um zu klassifizieren, welche Pfote welche ist:
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()]
Hier sind einige der Ergebnisse:
Verbleibende Probleme
Es gibt immer noch einige Probleme, insbesondere bei Hunden, die zu klein sind, um einen klaren Pfotenabdruck zu erzeugen ... (Dies funktioniert am besten bei großen Hunden, da die Zehen bei der Auflösung des Sensors deutlicher voneinander getrennt sind.) Auch teilweise Pfotenabdrücke werden damit nicht erkannt System, während sie mit dem trapezförmigen Muster-basierten System sein können.
Da die Eigenpfotenanalyse jedoch von Natur aus eine Abstandsmetrik verwendet, können wir die Pfoten in beide Richtungen klassifizieren und auf das auf Trapezmustern basierende System zurückgreifen, wenn der kleinste Abstand der Eigenpfotenanalyse vom "Codebuch" einen bestimmten Schwellenwert überschreitet. Ich habe dies jedoch noch nicht implementiert.
Puh ... das war lang! Mein Hut ist weg von Ivo, weil ich so eine lustige Frage habe!