Как сортировать лапы?

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

alt text

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

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

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

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

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

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

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

alt text

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

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

К clarfiy: 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" ), которые представляют собой удаленные эффекты.

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

И для всех, кого интересует, Im, сохраняющий блог со всеми обновлениями относительно проекта!

Ответы

Ответ 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, чтобы сгруппировать их по лапе.

Normal Impact Sequence

Однако, даже если все "нормально", это не работает. Это связано с трапециевидной формой рисунка. Задняя лапа пространственно отстает от предыдущей передней лапы.

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

Missed Hind Paw

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

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

Несмотря на все это, он часто работает неправильно. Многие из собак в полном наборе данных, похоже, работают, а удары лапы не соответствуют тому же временному порядку, что и при ходьбе собаки. (Или, возможно, у собаки просто серьезные проблемы с бедрами...)

Abnormal Impact Sequence

К счастью, мы все же можем программно определить, влияет ли воздействие лапы на наш ожидаемый пространственный шаблон:

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 собакам очень разных размеров, и мы, кажется, получаем согласованные результаты!

Average Paws

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

Mean Paw

Теперь мы можем анализировать отличия от среднего, которые немного легче распознать:

Differential Paws

Распознавание 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 являются "собственными лапами".

Eigenpaws

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

Вот некоторые из результатов: alt textalt textalt text

Оставшиеся проблемы

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

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

Фу... Это было долго! Моя шляпа уходит в Иво за такой забавный вопрос!

Ответ 2

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

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

Ответ 3

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

  • Покажите технический порядок изображений шагов и попросите их аннотировать первую лапу.
  • Обозначьте другие лапы на основе первой лапы и позвольте технике вносить исправления или повторно запускать тест. Это позволяет использовать хромых или трехногих собак.