Ответ 1
Хорошо! Мне, наконец, удалось добиться чего-то, работающего последовательно! Эта проблема втянула меня в течение нескольких дней... Веселый материал! Извините за длину этого ответа, но мне нужно немного разобраться в некоторых вещах... (Хотя я могу установить запись для самого длинного ответа, не относящегося к спаму stackoverflow)!
В качестве побочного примечания я использую полный набор данных, который Ivo предоставил ссылку на в своем оригинальный вопрос. Это серия файлов rar (по одному для каждой собаки), каждая из которых содержит несколько разных экспериментов, хранящихся в виде массивов ascii. Вместо того, чтобы пытаться копировать отдельные примеры кода в этот вопрос, здесь bitbucket mercurial repository с полным автономным кодом. Вы можете клонировать его с помощью
hg clone https://[email protected]/joferkington/paw-analysis
Обзор
Есть два способа подхода к проблеме, как вы отметили в своем вопросе. Я фактически буду использовать оба по-разному.
- Используйте (временный и пространственный) порядок воздействия лапы, чтобы определить, какая лапа является той.
- Попробуйте определить "pawprint", основанный исключительно на его форме.
В принципе, первый метод работает с собачьими лапами, следуя трапецеидально-подобному образцу, показанному выше в вопросе Ivo, но терпит неудачу, когда лапы не следуют этому образцу. Это довольно просто, чтобы программно определить, когда он не работает.
Поэтому мы можем использовать измерения, в которых он работал, чтобы создать учебный набор данных (от 2000 лап от 30 разных собак), чтобы узнать, какая лапа это, и проблема сводится к контролируемой классификации (с некоторыми дополнительные морщины... Распознавание изображений немного сложнее, чем "нормальная" проблема классификации под контролем).
Анализ шаблонов
Чтобы разработать первый метод, когда собака ходит (не работает!) нормально (что некоторые из этих собак не могут быть), мы ожидаем, что лапы будут воздействовать в следующем порядке: передний левый, задний правый, передний правый, Hind Left, Front Left и т.д. Шаблон может начинаться либо с передней левой, либо с передней правой лапы.
Если бы это всегда было так, мы могли бы просто отсортировать удары с помощью первоначального времени контакта и использовать модуль 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 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 разных собак!).
Теперь мы можем начать смотреть на то, что "средний" фронт слева и т.д., выглядит лапа.
Для этого нам нужна какая-то "метка лапы", которая является той же размерностью для любой собаки. (В полном наборе данных есть как очень большие, так и очень маленькие собаки!). Отпечаток лапы у ирландского эльхонда будет намного шире и "тяжелее", чем лапу от игрушечного пуделя. Нам нужно перемасштабировать каждую печать лапы, чтобы а) они имели одинаковое количество пикселей, и б) значения давления стандартизированы. Для этого я распечатал каждую лапкую печать на сетке 20x20 и изменил значения давления на основе максимального, минимального и среднего значения давления для воздействия лапы.
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 собакам очень разных размеров, и мы, кажется, получаем согласованные результаты!
Однако, прежде чем делать какой-либо анализ по этим вопросам, нам нужно вычесть среднее (средняя лапа для всех ног всех собак).
Теперь мы можем анализировать отличия от среднего, которые немного легче распознать:
Распознавание Paw на основе изображений
Хорошо... У нас наконец есть набор шаблонов, с которыми мы можем начать пытаться сопоставить лапы. Каждая лапа может рассматриваться как 400-мерный вектор (возвращаемый функцией paw_image
), который можно сравнить с этими четырьмя 400-мерными векторами.
К сожалению, если мы просто используем "нормальный" контролируемый алгоритм классификации (т.е. находим, какой из четырех моделей наиболее близок к конкретной печати лапы с использованием простого расстояния), он не работает последовательно. Фактически, это не намного лучше, чем случайные шансы в наборе учебных материалов.
Это обычная проблема при распознавании изображений. Из-за высокой размерности входных данных и несколько "нечеткой" природы изображений (т.е. Соседние пиксели имеют высокую ковариацию), просто просмотр различий изображения из шаблонного изображения не дает очень хорошей оценки сходство их форм.
Eigenpaws
Чтобы обойти это, нам нужно построить набор "собственных глаз" (так же, как "собственные" в распознавании лица) и описать каждую лапу печать как комбинацию этих собственных лап. Это идентично анализу основных компонентов и в основном обеспечивает способ уменьшить размерность наших данных, так что расстояние является хорошей мерой формы.
Поскольку у нас больше обучающих образов, чем размеры (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 arbirtrary...
num_basis_vecs = 50
basis_vecs = sorted_eigvecs[:,-num_basis_vecs:]
return basis_vecs
Эти basis_vecs
являются "собственными лапами".
Чтобы использовать их, мы просто усекаем (т.е. матричное умножение) каждое изображение лапы (как 400-мерный вектор, а не изображение 20х20) с базисными векторами. Это дает нам 50-мерный вектор (один элемент на базовый вектор), который мы можем использовать для классификации изображения. Вместо сравнения изображения 20х20 с изображением 20х20 каждой "шаблонной" лапы мы сравниваем 50-мерное преобразованное изображение с каждой 50-мерной трансформированной лапой. Это гораздо менее чувствительно к небольшим изменениям в точности расположения каждого носка и т.д. И в основном уменьшает размерность проблемы только до соответствующих размеров.
Классификация Paw на основе собственной чести
Теперь мы можем просто использовать расстояние между 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()]
Вот некоторые из результатов:
Оставшиеся проблемы
Есть еще некоторые проблемы, особенно с собаками, слишком маленькими, чтобы сделать ясный pawprint... (Он лучше всего работает с крупными собаками, поскольку пальцы ног более четко разделены при разрешении датчика.) Кроме того, частичные отпечатки не являются признанные в этой системе, в то время как они могут быть с системой на основе трапецеидальных шаблонов.
Однако, поскольку собственный анализ по умолчанию использует метрику расстояния, мы можем классифицировать лапы в обоих направлениях и возвращаться к системе с трапециевидным шаблоном, когда наименьшее расстояние от "кодовой книги" на самом собственном анализе занимает некоторый порог. Я еще не реализовал это.
Фу... Это было долго! Моя шляпа уходит в Иво за такой забавный вопрос!