Как я могу улучшить обнаружение лапы?

198

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

Видите ли, мое доказательство концепции в основном измеряло максимальное давление каждого датчика с течением времени и начинало искать сумму в каждом ряду, пока не обнаружит на этом! = 0.0. Затем он делает то же самое для столбцов и, как только он находит более 2 строк с этим, снова становится равным нулю. Он хранит минимальные и максимальные значения строки и столбца в некотором индексе.

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

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

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

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

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

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

Примеры того, где это начинает идти не так, как надо:

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

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

Обновить:

Я пытался реализовать ответ Джо (потрясающе!), Но у меня возникают трудности с извлечением фактических данных лапы из моих файлов.

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

Coded_paws показывает мне все различные лапы при применении к изображению максимального давления (см. Выше). Однако решение распространяется на каждый кадр (для разделения перекрывающихся лап) и устанавливает четыре атрибута Rectangle, такие как координаты или высота / ширина.

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

Итак, как я могу использовать атрибуты Rectangles для извлечения этих значений для каждой лапы?

У меня есть измерения, которые я использовал в настройке вопроса в моей общедоступной папке Dropbox ( пример 1 , пример 2 , пример 3 ). Для всех, кто интересуется, я также создал блог, чтобы держать вас в курсе :-)

Иво Флипс
источник
Похоже, вам придется отказаться от алгоритма строки / столбца, если вы ограничиваете полезную информацию.
Тамара Вийсман
12
Вот Это Да! Программное обеспечение Cat для управления?
alxx
На самом деле это данные о собаках @alxx ;-) Но да, они будут использованы для их диагностики!
Иво Флипс
4
Зачем? (не важно, веселее не знать ...)
Бен Регенспан,

Ответы:

358

Если вы просто хотите (пол) смежные областей, есть уже простая реализация в Python: SciPy «S ndimage.morphology модуль. Это довольно распространенная операция морфологии изображения .


По сути, у вас есть 5 шагов:

def find_paws(data, smooth_radius=5, threshold=0.0001):
    data = sp.ndimage.uniform_filter(data, smooth_radius)
    thresh = data > threshold
    filled = sp.ndimage.morphology.binary_fill_holes(thresh)
    coded_paws, num_paws = sp.ndimage.label(filled)
    data_slices = sp.ndimage.find_objects(coded_paws)
    return object_slices
  1. Размыть входные данные немного, чтобы убедиться, что лапы имеют непрерывный след. (Было бы более эффективно просто использовать большее ядро ​​( structurekwarg для различных scipy.ndimage.morphologyфункций), но по некоторым причинам это не совсем работает ...)

  2. Пороговое значение массива, чтобы у вас был логический массив мест, где давление превышает какое-то пороговое значение (то есть thresh = data > value)

  3. Заполните все внутренние отверстия, чтобы у вас были более чистые области ( filled = sp.ndimage.morphology.binary_fill_holes(thresh))

  4. Найдите отдельные смежные области ( coded_paws, num_paws = sp.ndimage.label(filled)). Это возвращает массив с регионами, закодированными по номеру (каждый регион является непрерывной областью уникального целого числа (от 1 до количества лап) с нулями повсюду)).

  5. Изолируйте смежные области, используя data_slices = sp.ndimage.find_objects(coded_paws). Это возвращает список кортежей sliceобъектов, так что вы можете получить область данных для каждой лапы с помощью [data[x] for x in data_slices]. Вместо этого мы нарисуем прямоугольник, основанный на этих срезах, что требует немного больше работы.


Две приведенные ниже анимации показывают пример данных «Перекрывающиеся лапы» и «Сгруппированные лапы». Этот метод, кажется, работает отлично. (И сколько бы это ни стоило, это работает намного более гладко, чем изображения GIF ниже на моей машине, поэтому алгоритм обнаружения лапы довольно быстрый ...)

Перекрывающиеся лапы Сгруппированные лапы


Вот полный пример (теперь с гораздо более подробными объяснениями). Подавляющее большинство из них читает входные данные и создает анимацию. Фактическое обнаружение лапы составляет всего 5 строк кода.

import numpy as np
import scipy as sp
import scipy.ndimage

import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle

def animate(input_filename):
    """Detects paws and animates the position and raw data of each frame
    in the input file"""
    # With matplotlib, it's much, much faster to just update the properties
    # of a display object than it is to create a new one, so we'll just update
    # the data and position of the same objects throughout this animation...

    infile = paw_file(input_filename)

    # Since we're making an animation with matplotlib, we need 
    # ion() instead of show()...
    plt.ion()
    fig = plt.figure()
    ax = fig.add_subplot(111)
    fig.suptitle(input_filename)

    # Make an image based on the first frame that we'll update later
    # (The first frame is never actually displayed)
    im = ax.imshow(infile.next()[1])

    # Make 4 rectangles that we can later move to the position of each paw
    rects = [Rectangle((0,0), 1,1, fc='none', ec='red') for i in range(4)]
    [ax.add_patch(rect) for rect in rects]

    title = ax.set_title('Time 0.0 ms')

    # Process and display each frame
    for time, frame in infile:
        paw_slices = find_paws(frame)

        # Hide any rectangles that might be visible
        [rect.set_visible(False) for rect in rects]

        # Set the position and size of a rectangle for each paw and display it
        for slice, rect in zip(paw_slices, rects):
            dy, dx = slice
            rect.set_xy((dx.start, dy.start))
            rect.set_width(dx.stop - dx.start + 1)
            rect.set_height(dy.stop - dy.start + 1)
            rect.set_visible(True)

        # Update the image data and title of the plot
        title.set_text('Time %0.2f ms' % time)
        im.set_data(frame)
        im.set_clim([frame.min(), frame.max()])
        fig.canvas.draw()

def find_paws(data, smooth_radius=5, threshold=0.0001):
    """Detects and isolates contiguous regions in the input array"""
    # Blur the input data a bit so the paws have a continous footprint 
    data = sp.ndimage.uniform_filter(data, smooth_radius)
    # Threshold the blurred data (this needs to be a bit > 0 due to the blur)
    thresh = data > threshold
    # Fill any interior holes in the paws to get cleaner regions...
    filled = sp.ndimage.morphology.binary_fill_holes(thresh)
    # Label each contiguous paw
    coded_paws, num_paws = sp.ndimage.label(filled)
    # Isolate the extent of each paw
    data_slices = sp.ndimage.find_objects(coded_paws)
    return data_slices

def paw_file(filename):
    """Returns a iterator that yields the time and data in each frame
    The infile is an ascii file of timesteps formatted similar to this:

    Frame 0 (0.00 ms)
    0.0 0.0 0.0
    0.0 0.0 0.0

    Frame 1 (0.53 ms)
    0.0 0.0 0.0
    0.0 0.0 0.0
    ...
    """
    with open(filename) as infile:
        while True:
            try:
                time, data = read_frame(infile)
                yield time, data
            except StopIteration:
                break

def read_frame(infile):
    """Reads a frame from the infile."""
    frame_header = infile.next().strip().split()
    time = float(frame_header[-2][1:])
    data = []
    while True:
        line = infile.next().strip().split()
        if line == []:
            break
        data.append(line)
    return time, np.array(data, dtype=np.float)

if __name__ == '__main__':
    animate('Overlapping paws.bin')
    animate('Grouped up paws.bin')
    animate('Normal measurement.bin')

Обновление: что касается определения того, какая лапа в какое время контактирует с датчиком, самое простое решение - просто выполнить тот же анализ, но использовать все данные одновременно. (т.е. складывать входные данные в трехмерный массив и работать с ним вместо отдельных временных периодов.) Поскольку функции SciPy ndimage предназначены для работы с n-мерными массивами, нам не нужно изменять исходную функцию поиска лапы вообще.

# This uses functions (and imports) in the previous code example!!
def paw_regions(infile):
    # Read in and stack all data together into a 3D array
    data, time = [], []
    for t, frame in paw_file(infile):
        time.append(t)
        data.append(frame)
    data = np.dstack(data)
    time = np.asarray(time)

    # Find and label the paw impacts
    data_slices, coded_paws = find_paws(data, smooth_radius=4)

    # Sort by time of initial paw impact... This way we can determine which
    # paws are which relative to the first paw with a simple modulo 4.
    # (Assuming a 4-legged dog, where all 4 paws contacted the sensor)
    data_slices.sort(key=lambda dat_slice: dat_slice[2].start)

    # Plot up a simple analysis
    fig = plt.figure()
    ax1 = fig.add_subplot(2,1,1)
    annotate_paw_prints(time, data, data_slices, ax=ax1)
    ax2 = fig.add_subplot(2,1,2)
    plot_paw_impacts(time, data_slices, ax=ax2)
    fig.suptitle(infile)

def plot_paw_impacts(time, data_slices, ax=None):
    if ax is None:
        ax = plt.gca()

    # Group impacts by paw...
    for i, dat_slice in enumerate(data_slices):
        dx, dy, dt = dat_slice
        paw = i%4 + 1
        # Draw a bar over the time interval where each paw is in contact
        ax.barh(bottom=paw, width=time[dt].ptp(), height=0.2, 
                left=time[dt].min(), align='center', color='red')
    ax.set_yticks(range(1, 5))
    ax.set_yticklabels(['Paw 1', 'Paw 2', 'Paw 3', 'Paw 4'])
    ax.set_xlabel('Time (ms) Since Beginning of Experiment')
    ax.yaxis.grid(True)
    ax.set_title('Periods of Paw Contact')

def annotate_paw_prints(time, data, data_slices, ax=None):
    if ax is None:
        ax = plt.gca()

    # Display all paw impacts (sum over time)
    ax.imshow(data.sum(axis=2).T)

    # Annotate each impact with which paw it is
    # (Relative to the first paw to hit the sensor)
    x, y = [], []
    for i, region in enumerate(data_slices):
        dx, dy, dz = region
        # Get x,y center of slice...
        x0 = 0.5 * (dx.start + dx.stop)
        y0 = 0.5 * (dy.start + dy.stop)
        x.append(x0); y.append(y0)

        # Annotate the paw impacts         
        ax.annotate('Paw %i' % (i%4 +1), (x0, y0),  
            color='red', ha='center', va='bottom')

    # Plot line connecting paw impacts
    ax.plot(x,y, '-wo')
    ax.axis('image')
    ax.set_title('Order of Steps')

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


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


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

Джо Кингтон
источник
82
Я даже не могу начать объяснять, какой ты классный ответ!
Иво Флипс
1
@Ivo: Да, я бы тоже с удовольствием проголосовал за Джо :) но должен ли я начать новый вопрос, или, может быть, @Joe, ответь здесь? stackoverflow.com/questions/2546780/…
unutbu
2
Я на самом деле просто выбросил .png и сделал convert *.png output.gif. Я, конечно, раньше заставил imagemagick поставить свою машину на колени, хотя для этого примера она работала нормально. В прошлом я использовал этот сценарий: svn.effbot.python-hosting.com/pil/Scripts/gifmaker.py, чтобы напрямую писать анимированный GIF из Python без сохранения отдельных кадров. Надеюсь, это поможет! Я выложу пример на упомянутый вопрос @unutbu.
Джо Кингтон
1
Спасибо за информацию, @Joe. Часть моей проблемы пренебрегает использовать bbox_inches='tight'в plt.savefig, другой нетерпение :)
unutbu
4
Святая корова, я просто должен сказать, вау, насколько хорош этот ответ.
Андерсой
4

Я не эксперт в обнаружении изображений, и я не знаю Python, но я сделаю это ...

Чтобы обнаружить отдельные лапы, вы должны сначала выбрать все с давлением, превышающим некоторый небольшой порог, очень близким к отсутствию давления вообще. Каждый пиксель / точка, которая выше этого, должна быть помечена. Затем каждый пиксель, смежный со всеми «помеченными» пикселями, становится маркированным, и этот процесс повторяется несколько раз. Массы, которые полностью связаны, будут сформированы, поэтому у вас есть различные объекты. Затем каждый «объект» имеет минимальное и максимальное значения x и y, поэтому ограничивающие рамки могут быть аккуратно упакованы вокруг них.

псевдокод:

(MARK) ALL PIXELS ABOVE (0.5)

(MARK) ALL PIXELS (ADJACENT) TO (MARK) PIXELS

REPEAT (STEP 2) (5) TIMES

SEPARATE EACH TOTALLY CONNECTED MASS INTO A SINGLE OBJECT

MARK THE EDGES OF EACH OBJECT, AND CUT APART TO FORM SLICES.

Это должно сделать.

TaslemGuy
источник
0

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

Похоже, вам нужно проанализировать функцию (давление во времени) для каждого пикселя и определить, где функция поворачивается (когда она изменяется> X в другом направлении, это считается поворотом к ошибкам счетчика).

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

после чего я подойду к решению, какая это лапа!

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

Тамара Вийсман
источник