Добре! Нарешті мені вдалося змусити щось працювати послідовно! Ця проблема затягнула мене на кілька днів ... Веселі речі! Вибачте за тривалість цієї відповіді, але мені потрібно трохи детальніше зупинитися на деяких речах ... (Хоча я можу встановити рекорд для найдовшої відповіді на стаціонарний потік без спаму!)
В якості побічної записки я використовую повний набір даних, на який Іво надав посилання в своєму первісному запитанні . Це серія rar-файлів (один на собаку), кожен з яких містить кілька різних експериментальних циклів, що зберігаються у вигляді масивів ascii. Замість того, щоб спробувати скопіювати вставте окремі приклади коду в це запитання, тут є сховище біткоїна ртутного сховища з повним автономним кодом. Ви можете його клонувати
hg clone https://joferkington@bitbucket.org/joferkington/paw-analysis
Огляд
По суті, два способи наблизитись до проблеми, як ви зазначили у своєму запитанні. Я фактично збираюся використовувати обидва по-різному.
- Використовуйте (часовий та просторовий) порядок ударів лапи, щоб визначити, яка саме лапа.
- Спробуйте визначити "відбиток лапи" виходячи виключно з його форми.
В основному, перший метод працює з лапами собаки за трапецієподібним малюнком, показаним у вищезазначеному питанні Іво, але виходить з ладу, коли лапи не дотримуються цього шаблону. Програмно виявити, коли це не працює, досить легко.
Таким чином, ми можемо використовувати вимірювання, де це працювало, щоб створити навчальний набір даних (~ 2000 ударів лапи від ~ 30 різних собак), щоб визначити, яка саме лапа, і проблема зводиться до контрольованої класифікації (З деякими додатковими зморшками). .. Розпізнавання зображень трохи складніше, ніж "звичайна" контрольована проблема класифікації).
Аналіз шаблону
Для розробки першого методу, коли собака нормально ходить (не бігає!) (Чого деякі з цих собак можуть не бути), ми очікуємо, що лапи вдарять у порядку: Передня ліва, Задня права, Передня права, Задня Ліва , Передня ліва і т. Д. Візерунок може починатися з передньої лівої або передньої правої лапи.
Якби це завжди було так, ми могли б просто сортувати удари за початковим часом контакту та використовувати модуль 4, щоб згрупувати їх за лапою.
Однак, навіть коли все "нормально", це не працює. Це пов’язано з трапецієподібною формою візерунка. Задня лапа просторово відстає від попередньої передньої лапи.
Тому удар задньої лапи після початкового удару передньою лапою часто падає з сенсорної пластини і не реєструється. Аналогічно, останній удар лапою часто не є наступною лапою в послідовності, оскільки удар лапи перед тим, як відбувся від сенсорної пластини і не був записаний.
Тим не менш, ми можемо використовувати форму малюнка удару лапи, щоб визначити, коли це сталося, і чи почали ми з лівої або правої передньої лапи. (Я фактично ігнорую проблеми з останнім впливом тут. Однак це не так важко додати.)
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
Незважаючи на все це, воно часто працює неправильно. Багато собак у повному наборі даних, здається, бігають, а удари лапою не відповідають тому ж часовому порядку, як коли вигулює собака. (А може, у собаки просто виникають серйозні проблеми з стегнами ...)
На щастя, ми все ще можемо програмно виявити, чи впливають лапи на нашу очікувану просторову схему:
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
Тому, хоча проста просторова класифікація не працює весь час, ми можемо визначити, коли вона працює з розумною впевненістю.
Набір навчальних даних
З класифікацій, заснованих на шаблонах, де він працював правильно, ми можемо створити дуже великий навчальний набір правильно класифікованих лап (~ 2400 ударів лапи у 32 різних собак!).
Тепер ми можемо почати розглядати, як виглядає "середня" передня ліва сторона тощо.
Для цього нам потрібна якась «метрика лапи», яка має однакову розмірність для будь-якої собаки. (У повному наборі даних є як дуже великі, так і дуже маленькі собаки!) Принт лапи з ірландського ельхаунда буде і набагато ширшим, і набагато «важчим», ніж принт лапи з іграшкового пуделя. Нам потрібно змінити масштаб кожного друку лапи, щоб а) вони мали однакову кількість пікселів, і б) значення тиску були стандартизовані. Для цього я перекомпонував кожен відбиток лапи на сітку 20х20 і змінив масштаби значень тиску виходячи з максимального, мінімуму та середнього значення тиску для удару лапи.
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
Після всього цього ми можемо нарешті подивитися на те, як виглядає середня ліва передня, задня права тощо. Лапа. Зауважте, що це в середньому для> 30 собак дуже різного розміру, і ми, здається, отримуємо стійкі результати!
Однак, перш ніж робити аналіз на них, нам потрібно відняти середнє значення (середня лапа для всіх ніг усіх собак).
Тепер ми можемо проаналізувати відмінності від середнього значення, які трохи простіше розпізнати:
Розпізнавання лап на основі зображення
Гаразд ... У нас нарешті є набір шаблонів, з якими ми можемо почати намагатися співставити лапи. Кожну лапу можна розглядати як 400-мірний вектор (повертається paw_image
функцією), який можна порівняти з цими чотирма 400-мірними векторами.
На жаль, якщо ми просто використовуємо "звичайний" алгоритм класифікації (керований наглядом) (тобто знаходимо, який із 4 шаблонів найближчий до певного друку лапи за допомогою простої відстані), він не працює послідовно. Насправді це не набагато краще, ніж випадковий шанс на навчальному наборі даних.
Це поширена проблема в розпізнаванні зображень. Через високу розмірність вхідних даних та дещо «нечіткий» характер зображень (тобто сусідні пікселі мають високу коваріацію), просто дивлячись на відмінність зображення від зображення шаблону не дає дуже гарної міри подібність їх форм.
Власні лапи
Щоб обійти це, нам потрібно побудувати набір "власних лап" (подібно до "власних поверхонь" для розпізнавання обличчя) та описати кожен друк лап як комбінацію цих власних лап. Це ідентично аналізу основних компонентів і, в основному, забезпечує спосіб зменшення розмірності наших даних, так що відстань є хорошим показником форми.
Оскільки у нас більше навчальних зображень, ніж розмірів (2400 проти 400), для швидкості немає необхідності робити "фантазійні" лінійні алгебри. Ми можемо працювати безпосередньо з коваріаційною матрицею набору навчальних даних:
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
Це basis_vecs
"власні лапи".
Для їх використання ми просто крапкуємо (тобто матричне множення) кожного зображення лапи (як 400-мірний вектор, а не зображення 20х20) основними векторами. Це дає нам 50-мірний вектор (один елемент на базовий вектор), який ми можемо використовувати для класифікації зображення. Замість того, щоб порівнювати зображення розміром 20x20 із зображенням зображення 20x20 кожної лапи "шаблону", ми порівнюємо 50-мірне перетворене зображення з кожною 50-мірною формою перетвореної шаблону. Це набагато менш чутливо до невеликих варіацій у тому, як саме розміщується кожен палець тощо, і в основному зменшує розмірність проблеми лише до відповідних розмірів.
Класифікація лап на основі Eigenpaw
Тепер ми можемо просто використовувати відстань між 50-мірними векторами та векторами "шаблону" для кожної ноги, щоб класифікувати, яка саме лапа:
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()]
Ось деякі результати:
Залишилися проблеми
Є ще деякі проблеми, особливо з собаками, занадто маленькими, щоб зробити чіткий відбиток фарби ... (Найкраще це працює з великими собаками, оскільки пальці ніг чіткіше відокремлюються в роздільній здатності датчика.) Також часткові відбитки лапи не розпізнаються при цьому системи, хоча вони можуть бути із системою, що базується на трапеції.
Однак, оскільки для аналізу власних лап по суті використовується метрика відстані, ми можемо класифікувати лапи обома способами і повернутися до системи на основі трапеції, коли найменша відстань аналізу власних лап від "кодової книги" перевищує деякий поріг. Я цього ще не втілив.
Фу ... Це було довго! Моя шапка знята Іво за таке цікаве запитання!