Обнаружение кластеров круговых объектов путем итеративного адаптивного определения порога и формы

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

Что облегчает тот факт, что объекты обычно отлично от фона.

Однако несколько трудностей затрудняют анализ:

  • Фон будет представлять собой постепенное, а также быстрое изменение интенсивности.
  • В краях контейнера объект будет скорее эллиптическим, чем круговым.
  • Края объектов иногда довольно нечеткие.
  • Объекты будут группироваться.
  • Объект может быть очень маленьким (6 пикселей диаметра)
  • В конечном итоге алгоритмы будут использоваться (через графический интерфейс) людьми, которые не имеют глубокого понимания анализа изображений, поэтому параметры должны быть интуитивно понятными и очень немногими.

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

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

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

Поэтому я искал новый метод для извлечения переднего плана.

Поэтому я исследовал другие методы обнаружения порога /blob. Я пробовал MSER, но узнал, что они не очень надежны и довольно медленны в моем случае.

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

  • Я разделил три канала своего изображения и уменьшил их шум (размытие/медианное размытие). Для каждого канала:
  • Я применяю ручную реализацию первого шага адаптивного порога, вычисляя абсолютную разницу между исходным каналом и свернутым (большим размытием ядра). Затем для всех соответствующих значений порога:
  • Я применяю порог к результату 2)
  • найти контуры
  • проверить или аннулировать контуры при предоставлении формы (размер, площадь, выпуклость...)
  • только действительные непрерывные области (т.е. ограниченные контурами) затем перерисовываются в аккумуляторе (1 аккумулятор на канал).
  • После накопления непрерывных областей над значениями порога я получаю карту "десятков регионов". Наиболее интенсивными областями являются те, которые наиболее часто выполняли критерии фильтра морфологии.
  • Три карты (по одному на канал) затем преобразуются в шкалу серого и пороговое значение (порог контролируется пользователем)

Просто чтобы показать вам образ, с которым я должен работать: enter image description here Это изображение представляет собой часть трех образцовых изображений в верхней части и результат моего алгоритма (синий = передний план) соответствующих частей внизу.

Вот моя реализация на С++: 3-7

/*
 * cv::Mat dst[3] is the result of the absolute difference between original and convolved channel.
 * MCF(std::vector<cv::Point>, int, int) is a filter function that returns an positive int only if the input contour is valid.
 */

/* Allocate 3 matrices (1 per channel)*/
cv::Mat accu[3];

/* We define the maximal threshold to be tried as half of the absolute maximal value in each channel*/
int maxBGR[3];
for(unsigned int i=0; i<3;i++){
    double min, max;
    cv::minMaxLoc(dst[i],&min,&max);
    maxBGR[i] = max/2;
    /* In addition, we fill accumulators by zeros*/
    accu[i]=cv::Mat(compos[0].rows,compos[0].cols,CV_8U,cv::Scalar(0));
}
/* This loops are intended to be multithreaded using
#pragma omp parallel for collapse(2) schedule(dynamic)
For each channel */
for(unsigned int i=0; i<3;i++){
    /* For each value of threshold (m_step can be > 1 in order to save time)*/
    for(int j=0;j<maxBGR[i] ;j += m_step ){
            /* Temporary matrix*/
            cv::Mat tmp;
            std::vector<std::vector<cv::Point> > contours;
            /* Thresholds dst by j*/
            cv::threshold(dst[i],tmp, j, 255, cv::THRESH_BINARY);
            /* Finds continous regions*/
            cv::findContours(tmp, contours, CV_RETR_LIST, CV_CHAIN_APPROX_TC89_L1);
            if(contours.size() > 0){
                /* Tests each contours*/
                for(unsigned int k=0;k<contours.size();k++){
                    int valid = MCF(contours[k],m_minRad,m_maxRad);
                    if(valid>0){
                        /* I found that redrawing was very much faster if the given contour was copied in a smaller container.
                         * I do not really understand why though. For instance,
                         cv::drawContours(miniTmp,contours,k,cv::Scalar(1),-1,8,cv::noArray(), INT_MAX, cv::Point(-rect.x,-rect.y));
                         is slower especially if contours is very long.
                         */
                        std::vector<std::vector<cv::Point> > tpv(1);
                        std::copy(contours.begin()+k, contours.begin()+k+1, tpv.begin());
                        /* We make a Roi here*/
                        cv::Rect rect = cv::boundingRect(tpv[0]);
                        cv::Mat miniTmp(rect.height,rect.width,CV_8U,cv::Scalar(0));
                        cv::drawContours(miniTmp,tpv,0,cv::Scalar(1),-1,8,cv::noArray(), INT_MAX, cv::Point(-rect.x,-rect.y));
                        accu[i](rect)    = miniTmp + accu[i](rect);
                    }
                }
            }
        }
    }
/* Make the global scoreMap*/
cv::merge(accu,3,scoreMap);
/* Conditional noise removal*/
if(m_minRad>2)
    cv::medianBlur(scoreMap,scoreMap,3);
cvtColor(scoreMap,scoreMap,CV_BGR2GRAY);

У меня есть два вопроса:

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

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

Большое вам спасибо за помощь,

Ответы

Ответ 1

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

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

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

Некоторые результаты приведены ниже:

Cells 2Cells