Как разобрать лапы?

121

В моем предыдущем вопросе я получил отличный ответ, который помог мне определить, где лапа попадает в нажимную пластину, но теперь я изо всех сил пытаюсь связать эти результаты с соответствующими лапами:

альтернативный текст

Я вручную аннотировал лапы (RF = правая передняя, ​​RH = правая задняя, ​​LF = левая передняя, ​​LH = левая задняя).

Как вы можете видеть, здесь явно присутствует повторяющийся узор, который проявляется почти в каждом измерении. Вот ссылка на презентацию 6 испытаний, аннотированных вручную.

Моя первоначальная мысль заключалась в том, чтобы использовать эвристику для сортировки, например:

  • Соотношение нагрузки между передней и задней лапами составляет ~ 60-40%;
  • Задние лапы обычно меньше по площади;
  • Лапы (часто) пространственно разделены на левую и правую.

Однако я немного скептически отношусь к своим эвристикам, поскольку они откажутся от меня, как только я столкнусь с вариантом, о котором не подумал. Они также не смогут справиться с измерениями с хромых собак, у которых, вероятно, есть свои правила.

Кроме того, аннотация, предложенная Джо, иногда бывает ошибочной и не учитывает, как на самом деле выглядит лапа.

Основываясь на ответах, которые я получил на свой вопрос об обнаружении пиков в лапе , я надеюсь, что есть более продвинутые решения для сортировки лап. Тем более, что распределение давления и его прогрессия различны для каждой отдельной лапы, почти как отпечаток пальца. Я надеюсь, что есть метод, который может использовать это для кластеризации моих лап, а не просто сортировки их в порядке появления.

альтернативный текст

Поэтому я ищу лучший способ сортировки результатов с помощью соответствующей лапы.

Для всех, кто справится с этой задачей, я собрал словарь со всеми нарезанными массивами, которые содержат данные о давлении каждой лапы (объединенные по измерениям) и срез, который описывает их местоположение (положение на пластине и во времени).

Для пояснения: walk_sliced_data - это словарь, который содержит ['ser_3', 'ser_2', 'sel_1', 'sel_2', 'ser_1', 'sel_3'], которые являются названиями измерений. Каждое измерение содержит другой словарь, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] (пример из «sel_1»), которые представляют собой извлеченные воздействия.

Также обратите внимание, что «ложные» воздействия, например, когда лапа частично измеряется (в пространстве или времени), можно игнорировать. Они полезны только потому, что помогают распознать закономерность, но не будут анализироваться.

А всем, кому интересно, я веду блог со всеми обновлениями проекта!

Иво Флипс
источник
1
Да, подход, который я использовал, не совсем работает. Чтобы уточнить, подход, который я использовал, состоял в том, чтобы просто упорядочить удары и предположить, что первая лапа, которую нужно коснуться, такая же, как пятая лапа, которую нужно коснуться, и так далее. (т.е. упорядочить удары и использовать по модулю 4). Проблема заключается в том, что иногда задние лапы отрываются от сенсорной площадки после того, как первая лапа коснется земли. В этом случае первая лапа, которая ударится, соответствует четвертой или третьей лапе, которая столкнется с ним. Надеюсь, в этом есть смысл.
Джо Кингтон
1
Могу ли я правильно интерпретировать изображения в том, что один палец каждой задней лапы оказывает значительно меньшее давление, чем остальные? Также кажется, что зацеп всегда направлен «внутрь», то есть к центру масс собаки. Не могли бы вы использовать это как эвристику?
Томас Лэнгстон
1
Я признаю, что мои ограниченные навыки обработки изображений несколько ржавые, но легко ли взять наименее крутой градиент большой средней подушечки каждой лапы? Кажется, что угол наименьшей крутизны очень поможет (нарисованный от руки пример лап размещен: imgur.com/y2wBC imgur.com/yVqVU imgur.com/yehOc imgur.com/q0tcD )
user470379
Не могли бы вы пояснить, как структурированы данные walk_sliced_data? Вижу словарь словарей трехмерных массивов. Если я исправлю третье измерение и нарисую первые два как изображение, я думаю, что увижу лапы.
Стив Тьоа,
@ Томас, да, каждая лапа явно загружена по-своему. Я знаю, что мне нужно, чтобы программа делала, но я понятия не имею, как ее программировать ... @Steve, я добавил пояснение внизу :-)
Ivo Flipse

Ответы:

123

Хорошо! Наконец-то мне удалось заставить что-то работать стабильно! Эта проблема тянула меня на несколько дней ... Приколы! Извините за длину этого ответа, но мне нужно немного рассказать о некоторых вещах ... (Хотя я могу установить рекорд для самого длинного ответа, не связанного со спамом!)

В качестве примечания я использую полный набор данных, на который Иво дал ссылку в своем исходном вопросе . Это серия файлов rar (по одному на собаку), каждый из которых содержит несколько различных запусков экспериментов, хранящихся в виде массивов ascii. Вместо того, чтобы пытаться скопировать примеры автономного кода в этот вопрос, вот меркуриальный репозиторий bitbucket с полным автономным кодом. Вы можете клонировать его с помощью

hg clone https://joferkington@bitbucket.org/joferkington/paw-analysis


обзор

Как вы отметили в своем вопросе, есть два основных подхода к решению проблемы. Я собираюсь использовать и то, и другое по-разному.

  1. Используйте (временной и пространственный) порядок ударов лапы, чтобы определить, какая лапа какая.
  2. Постарайтесь определить «след лапы» исключительно по его форме.

По сути, первый метод работает, когда лапы собаки следуют трапециевидной схеме, показанной в вопросе Иво выше, но терпит неудачу, когда лапы не следуют этому шаблону. Когда это не работает, довольно легко обнаружить программно.

Следовательно, мы можем использовать измерения, в которых это действительно сработало, для создания набора тренировочных данных (~ 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«собственные лапы».

Eigenpaws

Чтобы использовать их, мы просто ставим точки (т.е. матричное умножение) каждое изображение лапы (как 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()]

Вот некоторые из результатов: альтернативный текст альтернативный текст альтернативный текст

Остающиеся проблемы

По-прежнему существуют некоторые проблемы, особенно с собаками, слишком маленькими, чтобы оставлять четкие отпечатки лапы ... (Это лучше всего работает с большими собаками, поскольку пальцы ног более четко разделены при разрешении сенсора.) Кроме того, при этом не распознаются частичные отпечатки лапы. система, в то время как они могут быть с системой на основе трапеции.

Однако, поскольку анализ собственной лапы по своей сути использует метрику расстояния, мы можем классифицировать лапы в обоих направлениях и вернуться к системе на основе трапециевидного шаблона, когда наименьшее расстояние анализа собственной лапы от «кодовой книги» превышает некоторый порог. Однако я еще не реализовал это.

Уф ... Это было давно! Снимаю шляпу перед Иво за такой забавный вопрос!

Джо Кингтон
источник
2
Отличный ответ. Я тоже попробовал метод собственной лапы, но был не так настойчив, как вы. Одна проблема, которую я вижу, - это регистрация лапы, т. Е. Регистрация лица - это распознавание лица. Сталкивались ли вы с проблемами при нормализации положения и поворота каждой лапы? Если это так, то, возможно, лапа может быть предварительно обработана в некоторую инвариантную функцию сдвига-вращения перед выполнением PCA.
Steve Tjoa
2
@Steve, я не пробовал их вращать, хотя у меня были обсуждения с Джо о том, как это улучшить. Однако, чтобы закончить мой проект на данный момент, я вручную аннотировал все лапы, чтобы завершить его. К счастью, это также позволяет нам создавать различные обучающие наборы, чтобы сделать распознавание более чувствительным. Для вращения лап я планировал использовать пальцы ног, но, как вы можете прочитать в моем блоге, это не так просто, поскольку мой первый вопрос выглядел так ...
Иво Флипс
@Basic, да, я перешел на хостинг своего сайта и переместил весь контент Wordpress, но я больше не мог редактировать здесь свой комментарий. Вы сможете найти их здесь: flipserd.com/blog/ivoflipse/post/improving-the-paw-detection
Иво
4

Используя информацию исключительно на основе продолжительности, я думаю, вы могли бы применить методы моделирования кинематики; а именно обратная кинематика . В сочетании с ориентацией, длиной, продолжительностью и общим весом это дает некоторый уровень периодичности, который, я надеюсь, может стать первым шагом в попытке решить вашу проблему «сортировки лап».

Все эти данные можно использовать для создания списка ограниченных многоугольников (или кортежей), который вы можете использовать для сортировки по размеру шага, а затем по лапу [индекс].

Лам Чау
источник
2

Можете ли вы попросить специалиста, выполняющего тест, вручную ввести первую лапу (или первые две)? Процесс может быть таким:

  • Покажите технику порядок шагов изображения и потребуйте, чтобы они аннотировали первую лапу.
  • Обозначьте остальные лапы на основе первой лапы и позвольте технику внести исправления или повторно запустить тест. Это позволяет использовать хромых или трехногих собак.
Джейми Иде
источник
У меня есть аннотации к первым лапам, хотя и небезупречные. Однако первая лапа - это всегда передняя, ​​и я не могу отделить задние лапы. Кроме того, порядок не идеален, как сказал Джо, потому что для этого требуется, чтобы обе передние части касались пластины в начале.
Иво Флипс
Аннотации были бы полезны при использовании распознавания изображений, поскольку у меня есть 24 измерения, по крайней мере, 24 лапы уже будут аннотированы. Если затем они будут сгруппированы в 4 группы, две из них должны содержать разумное количество каждой передней лапы, достаточное для того, чтобы алгоритм был достаточно уверен в кластеризации.
Иво Флипс
Если я не читаю их неправильно, в связанных испытаниях с аннотациями показано, что задняя лапа касается первой в 4 из 6 испытаний.
Джейми Айд
Ах, я имел в виду по времени. Если вы пропустите напильник, передняя лапа всегда должна касаться пластины первой.
Иво Флипс