Tutto a posto! Sono finalmente riuscito a far funzionare qualcosa in modo coerente! Questo problema mi ha coinvolto per diversi giorni ... Roba divertente! Ci scusiamo per la lunghezza di questa risposta, ma ho bisogno di elaborare un po 'su alcune cose ... (Anche se potrei stabilire un record per la più lunga risposta di stackoverflow non spam mai!)
Come nota a margine, sto usando il set di dati completo di Ivo fornito un collegamento nella sua domanda originale . È una serie di file rar (uno per cane), ciascuno contenente diverse esecuzioni di esperimenti archiviate come array ASCII. Piuttosto che provare a copiare e incollare esempi di codice autonomo in questa domanda, ecco un repository mercurial bitbucket con codice completo e autonomo. Puoi clonarlo con
hg clone https://joferkington@bitbucket.org/joferkington/paw-analysis
Panoramica
Esistono essenzialmente due modi per affrontare il problema, come hai notato nella tua domanda. In realtà userò entrambi in modi diversi.
- Usa l'ordine (temporale e spaziale) degli impatti della zampa per determinare quale zampa è quale.
- Cerca di identificare "l'impronta della zampa" basandoti esclusivamente sulla sua forma.
Fondamentalmente, il primo metodo funziona con le zampe del cane seguendo lo schema trapezoidale mostrato nella domanda di Ivo sopra, ma fallisce ogni volta che le zampe non seguono quello schema. È abbastanza facile rilevare programmaticamente quando non funziona.
Pertanto, possiamo utilizzare le misurazioni in cui ha funzionato per costruire un set di dati di allenamento (di ~ 2000 impatti sulla zampa di ~ 30 cani diversi) per riconoscere quale zampa è quale, e il problema si riduce a una classificazione supervisionata (con alcune rughe aggiuntive. .. Il riconoscimento delle immagini è un po 'più difficile di un "normale" problema di classificazione supervisionato).
Analisi dei modelli
Per elaborare il primo metodo, quando un cane cammina (non corre!) Normalmente (cosa che alcuni di questi cani potrebbero non essere), ci aspettiamo che le zampe abbiano un impatto nell'ordine di: Anteriore sinistro, Posteriore destro, Anteriore destro, Posteriore sinistro , Anteriore sinistra, ecc. Il disegno può iniziare con la zampa anteriore sinistra o anteriore destra.
Se fosse sempre così, potremmo semplicemente ordinare gli impatti in base al tempo di contatto iniziale e utilizzare un modulo 4 per raggrupparli per zampa.
Tuttavia, anche quando tutto è "normale", questo non funziona. Ciò è dovuto alla forma trapezoidale del motivo. Una zampa posteriore cade spazialmente dietro la zampa anteriore precedente.
Pertanto, l'impatto della zampa posteriore dopo l'impatto iniziale della zampa anteriore spesso cade dalla piastra del sensore e non viene registrato. Allo stesso modo, l'ultimo impatto della zampa spesso non è la zampa successiva nella sequenza, poiché l'impatto della zampa prima che si verificasse fuori dalla piastra del sensore e non fosse registrato.
Tuttavia, possiamo usare la forma del modello di impatto della zampa per determinare quando è successo e se abbiamo iniziato con una zampa anteriore sinistra o destra. (In realtà sto ignorando i problemi con l'ultimo impatto qui. Non è troppo difficile aggiungerlo, però.)
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
Nonostante tutto ciò, spesso non funziona correttamente. Molti dei cani nel set di dati completo sembrano correre e gli impatti della zampa non seguono lo stesso ordine temporale di quando il cane sta camminando. (O forse il cane ha solo gravi problemi all'anca ...)
Fortunatamente, possiamo ancora rilevare programmaticamente se gli impatti della zampa seguono o meno il nostro modello spaziale previsto:
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
Pertanto, anche se la semplice classificazione spaziale non funziona sempre, possiamo determinare quando funziona con ragionevole sicurezza.
Set di dati di formazione
Dalle classificazioni basate su schemi in cui ha funzionato correttamente, possiamo costruire un set di dati di allenamento molto ampio di zampe classificate correttamente (~ 2400 impatti delle zampe da 32 cani diversi!).
Ora possiamo iniziare a vedere che aspetto ha una zampa anteriore sinistra "media", ecc.
Per fare questo, abbiamo bisogno di una sorta di "metrica della zampa" che abbia la stessa dimensionalità per qualsiasi cane. (Nel set di dati completo, ci sono sia cani molto grandi che molto piccoli!) Un'impronta della zampa di un elkhound irlandese sarà sia molto più larga che molto più "pesante" di un'impronta della zampa di un barboncino giocattolo. Dobbiamo riscalare ogni impronta in modo che a) abbiano lo stesso numero di pixel eb) i valori di pressione siano standardizzati. Per fare ciò, ho ricampionato ciascuna impronta della zampa su una griglia 20x20 e ho riscalato i valori di pressione in base al valore massimo, minimo e medio per l'impatto della zampa.
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
Dopo tutto questo, possiamo finalmente dare un'occhiata a come appare una zampa media anteriore sinistra, posteriore destra, ecc. Si noti che questa viene calcolata in media su> 30 cani di taglie molto diverse e sembra che stiamo ottenendo risultati coerenti!
Tuttavia, prima di fare qualsiasi analisi su questi, dobbiamo sottrarre la media (la zampa media per tutte le gambe di tutti i cani).
Ora possiamo analizzare le differenze dalla media, che sono un po 'più facili da riconoscere:
Riconoscimento della zampa basato su immagini
Ok ... Finalmente abbiamo una serie di schemi con cui possiamo iniziare a provare ad abbinare le zampe. Ogni zampa può essere trattata come un vettore a 400 dimensioni (restituito dalpaw_image
funzione) che può essere confrontato con questi quattro vettori a 400 dimensioni.
Sfortunatamente, se usiamo solo un algoritmo di classificazione "normale" supervisionato (cioè troviamo quale dei 4 modelli è più vicino a una particolare impronta usando una semplice distanza), non funziona in modo coerente. In effetti, non fa molto meglio delle possibilità casuali sul set di dati di addestramento.
Questo è un problema comune nel riconoscimento delle immagini. A causa dell'elevata dimensionalità dei dati di input e della natura un po '"sfocata" delle immagini (cioè i pixel adiacenti hanno un'elevata covarianza), il semplice esame della differenza di un'immagine da un'immagine modello non fornisce una misura molto buona della somiglianza delle loro forme.
Eigenpaws
Per aggirare questo problema dobbiamo costruire una serie di "autovetture" (proprio come le "autovetture" nel riconoscimento facciale), e descrivere ogni impronta della zampa come una combinazione di queste autovetture. Questo è identico all'analisi delle componenti principali e fondamentalmente fornisce un modo per ridurre la dimensionalità dei nostri dati, in modo che la distanza sia una buona misura della forma.
Poiché abbiamo più immagini di addestramento che dimensioni (2400 contro 400), non c'è bisogno di fare algebra lineare "fantasia" per la velocità. Possiamo lavorare direttamente con la matrice di covarianza del set di dati di addestramento:
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
Questi basis_vecs
sono gli "autovelox".
Per usarli, semplicemente punteggiamo (cioè moltiplicazione di matrici) ogni immagine della zampa (come un vettore a 400 dimensioni, piuttosto che un'immagine 20x20) con i vettori di base. Questo ci dà un vettore a 50 dimensioni (un elemento per vettore base) che possiamo usare per classificare l'immagine. Invece di confrontare un'immagine 20x20 con l'immagine 20x20 di ciascuna zampa "modello", confrontiamo l'immagine trasformata a 50 dimensioni con ciascuna zampa modello trasformata a 50 dimensioni. Questo è molto meno sensibile alle piccole variazioni nel modo esatto in cui ogni dito è posizionato, ecc., E sostanzialmente riduce la dimensionalità del problema solo alle dimensioni rilevanti.
Classificazione della zampa basata su Eigenpaw
Ora possiamo semplicemente usare la distanza tra i vettori a 50 dimensioni e i vettori "modello" per ciascuna gamba per classificare quale zampa è quale:
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()]
Ecco alcuni dei risultati:
Problemi rimanenti
Ci sono ancora alcuni problemi, in particolare con i cani troppo piccoli per fare un'impronta chiara ... (Funziona meglio con i cani di grossa taglia, poiché le dita dei piedi sono più chiaramente separate alla risoluzione del sensore). Inoltre, le impronte parziali non vengono riconosciute con questo , mentre possono essere con il sistema basato su modelli trapezoidali.
Tuttavia, poiché l'analisi autofiorente utilizza intrinsecamente una metrica della distanza, possiamo classificare le zampe in entrambi i modi e ricorrere al sistema basato su schemi trapezoidali quando la distanza minima dell'analisi autofocus dal "libro dei codici" supera una certa soglia. Tuttavia, non l'ho ancora implementato.
Uff ... è stato lungo! Tanto di cappello a Ivo per aver fatto una domanda così divertente!