Хорошо! Наконец-то мне удалось заставить что-то работать стабильно! Эта проблема тянула меня на несколько дней ... Приколы! Извините за длину этого ответа, но мне нужно немного рассказать о некоторых вещах ... (Хотя я могу установить рекорд для самого длинного ответа, не связанного со спамом!)
В качестве примечания я использую полный набор данных, на который Иво дал ссылку в своем исходном вопросе . Это серия файлов rar (по одному на собаку), каждый из которых содержит несколько различных запусков экспериментов, хранящихся в виде массивов ascii. Вместо того, чтобы пытаться скопировать примеры автономного кода в этот вопрос, вот меркуриальный репозиторий bitbucket с полным автономным кодом. Вы можете клонировать его с помощью
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 разных собак!).
Теперь мы можем начать смотреть, как выглядит "средняя" передняя левая лапа и т. Д.
Для этого нам нужна некая «лапа-метрика», размерность которой одинакова для любой собаки. (В полном наборе данных есть как очень большие, так и очень маленькие собаки!) Отпечаток лапы ирландского лосося будет намного шире и намного «тяжелее», чем след лапы игрушечного пуделя. Нам нужно изменить масштаб каждого отпечатка лапы так, чтобы а) они имели одинаковое количество пикселей и б) значения давления были стандартизированы. Для этого я пересчитал каждый отпечаток лапы на сетку 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 собак самых разных размеров, и мы, кажется, получаем стабильные результаты!
Однако, прежде чем мы проведем какой-либо анализ по ним, нам нужно вычесть среднее значение (средняя лапа для всех ног всех собак).
Теперь мы можем проанализировать отличия от среднего, которые немного легче распознать:
Распознавание лапы на основе изображений
Хорошо ... Наконец-то у нас есть набор шаблонов, по которым мы можем начать пытаться сопоставить лапы. Каждую лапу можно рассматривать как 400-мерный вектор (возвращаемый функциейpaw_image
функцией), который можно сравнить с этими четырьмя 400-мерными векторами.
К сожалению, если мы просто воспользуемся «обычным» алгоритмом контролируемой классификации (т.е. найдем, какой из 4 шаблонов ближе всего к конкретному отпечатку лапы, используя простое расстояние), он не будет работать последовательно. Фактически, это не намного лучше, чем случайная случайность в наборе обучающих данных.
Это обычная проблема при распознавании изображений. Из-за высокой размерности входных данных и несколько «нечеткой» природы изображений (т. Е. Соседние пиксели имеют высокую ковариацию), простой просмотр отличия изображения от изображения шаблона не дает очень хорошей оценки сходство их форм.
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's arbirtrary...
num_basis_vecs = 50
basis_vecs = sorted_eigvecs[:,-num_basis_vecs:]
return basis_vecs
Это basis_vecs
«собственные лапы».
Чтобы использовать их, мы просто ставим точки (т.е. матричное умножение) каждое изображение лапы (как 400-мерный вектор, а не изображение 20x20) с базисными векторами. Это дает нам 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()]
Вот некоторые из результатов:
Остающиеся проблемы
По-прежнему существуют некоторые проблемы, особенно с собаками, слишком маленькими, чтобы оставлять четкие отпечатки лапы ... (Это лучше всего работает с большими собаками, поскольку пальцы ног более четко разделены при разрешении сенсора.) Кроме того, при этом не распознаются частичные отпечатки лапы. система, в то время как они могут быть с системой на основе трапеции.
Однако, поскольку анализ собственной лапы по своей сути использует метрику расстояния, мы можем классифицировать лапы в обоих направлениях и вернуться к системе на основе трапециевидного шаблона, когда наименьшее расстояние анализа собственной лапы от «кодовой книги» превышает некоторый порог. Однако я еще не реализовал это.
Уф ... Это было давно! Снимаю шляпу перед Иво за такой забавный вопрос!