Это мой взгляд на проблему. Я дам вам общую суть, а затем мою реализацию в C++
. Основная идея заключается в том, что я хочу обрабатывать изображение слева направо, сверху вниз. Я буду обрабатывать каждую каплю (или контур), как только найду ее, однако мне потребуется несколько промежуточных шагов для достижения успешной (упорядоченной) сегментации.
Вертикальная сортировка по строкам
Первый шаг заключается в сортировке больших двоичных объектов по строкам. Это означает, что каждая строка содержит набор (неупорядоченных) горизонтальных больших двоичных объектов. Это нормально. первым шагом является вычисление некоторой вертикальной сортировки, и если мы будем обрабатывать каждую строку сверху вниз, мы добьемся именно этого.
После того как капли (вертикально) отсортированы по строкам, я могу проверить их центроиды (или центр масс) и отсортировать их по горизонтали. Идея состоит в том, что я буду обрабатывать строку за строкой и for
в каждой строке сортировать BLOB-объекты по центроидам. Давайте посмотрим на пример того, чего я пытаюсь достичь здесь.
Это ваше входное изображение:
Это то, что я называю маской строки:
Это последнее изображение содержит белые области, каждая из которых представляет строку. Каждая строка имеет номер (например, Row1
, Row2
и т. д.), а каждая row
содержит набор больших двоичных объектов (или символов, в данном случае). Обрабатывая каждый row
, сверху вниз, вы уже сортируете капли по вертикальной оси.
Если я пронумерую каждую строку сверху вниз, я получу это изображение:
Маска строк — это способ создания строк больших двоичных объектов, и эта маска может быть вычислена морфологически. Посмотрите на 2 изображения, наложенные друг на друга, чтобы лучше понять порядок обработки:
То, что мы пытаемся сделать здесь, это, во-первых, вертикальное упорядочение (синяя стрелка), а затем мы позаботимся о горизонтальном упорядочении (красная стрелка). Вы можете видеть, что, обрабатывая каждую строку, мы можем (возможно) решить проблему сортировки!
Горизонтальная сортировка с использованием центроидов
Давайте теперь посмотрим, как мы можем отсортировать капли horizontally
. Если мы создадим более простое изображение с width
, равным входному изображению, и height
, равным числам rows
в нашей маске строки, мы можем просто наложить каждую горизонтальную координату (координату x) каждого блоба. центроид. Посмотрите этот пример:
Это таблица строк. Каждая строка представляет собой количество строк, найденных в маске строк, и также читается сверху вниз. width
таблицы совпадает с width
входного изображения и пространственно соответствует горизонтальной оси. Каждый квадрат — это пиксель входного изображения, сопоставленный с таблицей строк с использованием только горизонтальной координаты (поскольку наше упрощение строк довольно простое). Фактическое значение каждого пикселя в таблице строк — это label
, помечающее каждую каплю на входном изображении. Обратите внимание, что этикетки не упорядочены!
Так, например, в этой таблице показано, что в строке 1 (вы уже знаете, что такое строка 1 — это первая белая область на маске строки) в позиции (1,4)
есть номер блоба 3
. В позиции (1,6)
находится большой двоичный объект с номером 2
и так далее. Что хорошего (я думаю) в этой таблице, так это то, что вы можете просмотреть ее, и for
каждое значение, отличное от 0
, горизонтальное упорядочение становится очень тривиальным. Это таблица строк, упорядоченная слева направо:
Сопоставление информации о больших двоичных объектах с центроидами
Мы собираемся использовать BLOB-объекты центроиды для map
информации между двумя нашими представлениями (маска строки/таблица строки). Предположим, у вас уже есть оба вспомогательных изображения, и вы одновременно обрабатываете каждую каплю (или контур) на входном изображении. Например, у вас есть это в качестве начала:
Хорошо, здесь есть капля. Как мы можем сопоставить его с маской строки и таблицей строк? Используя его центроиды. Если мы вычислим центроид (показанный на рисунке зеленой точкой), мы сможем построить dictionary
из центроидов и меток. Например, для этого BLOB-объекта centroid
находится по адресу (271,193)
. Хорошо, давайте назначим label = 1
. Итак, теперь у нас есть этот словарь:
Теперь мы находим row
, в котором находится этот блоб, используя тот же centroid
в маске строки. Что-то вроде этого:
rowNumber = rowMask.at( 271,193 )
Эта операция должна вернуть rownNumber = 3
. Ницца! Мы знаем, в какой строке находится наш большой двоичный объект, поэтому теперь он упорядочен вертикально. Теперь давайте сохраним его горизонтальную координату в таблице строк:
rowTable.at( 271, 193 ) = 1
Теперь rowTable
содержит (в своей строке и столбце) метку обработанного большого двоичного объекта. Таблица строк должна выглядеть примерно так:
Таблица намного шире, потому что ее размер по горизонтали должен совпадать с исходным изображением. На этом изображении label 1
помещено в Column 271, Row 3.
Если бы это было единственное пятно на вашем изображении, пятна были бы уже отсортированы. Но что произойдет, если вы добавите еще один блоб, скажем, Column 2
, Row 1
? Вот почему вам нужно снова пройтись по этой таблице после того, как вы обработали все блобы, чтобы правильно исправить их метку.
Реализация на C++
Хорошо, надеюсь, алгоритм должен быть немного ясен (если нет, просто спросите, мой друг). Попробую реализовать эти идеи в OpenCV
с помощью C++
. Во-первых, мне нужно binary image
вашего вклада. Вычисление тривиально с использованием метода Otsu’s thresholding
:
//Read the input image:
std::string imageName = "C://opencvImages//yFX3M.png";
cv::Mat testImage = cv::imread( imageName );
//Compute grayscale image
cv::Mat grayImage;
cv::cvtColor( testImage, grayImage, cv::COLOR_RGB2GRAY );
//Get binary image via Otsu:
cv::Mat binImage;
cv::threshold( grayImage, binImage, 0, 255, cv::THRESH_OTSU );
//Invert image:
binImage = 255 - binImage;
Это результирующий двоичный образ, ничего особенного, как раз то, что нам нужно для начала работы:
Первый шаг — получить Row Mask
. Этого можно добиться с помощью морфологии. Просто примените dilation + erosion
с ОЧЕНЬ большим горизонтальным structuring element
. Идея в том, что вы хотите превратить эти капли в прямоугольники, соединив их вместе по горизонтали:
//Create a hard copy of the binary mask:
cv::Mat rowMask = binImage.clone();
//horizontal dilation + erosion:
int horizontalSize = 100; // a very big horizontal structuring element
cv::Mat SE = cv::getStructuringElement( cv::MORPH_RECT, cv::Size(horizontalSize,1) );
cv::morphologyEx( rowMask, rowMask, cv::MORPH_DILATE, SE, cv::Point(-1,-1), 2 );
cv::morphologyEx( rowMask, rowMask, cv::MORPH_ERODE, SE, cv::Point(-1,-1), 1 );
Это приводит к следующему Row Mask
:
Это очень здорово, теперь, когда у нас есть Row Mask
, мы должны пронумеровать их ряды, хорошо? Есть много способов сделать это, но сейчас меня интересует более простой: пройтись по этому изображению и получить каждый пиксель. If
пиксель белый, используйте операцию Flood Fill
, чтобы пометить эту часть изображения как уникальный блоб (или строку, в данном случае). Это можно сделать следующим образом:
//Label the row mask:
int rowCount = 0; //This will count our rows
//Loop thru the mask:
for( int y = 0; y < rowMask.rows; y++ ){
for( int x = 0; x < rowMask.cols; x++ ){
//Get the current pixel:
uchar currentPixel = rowMask.at<uchar>( y, x );
//If the pixel is white, this is an unlabeled blob:
if ( currentPixel == 255 ) {
//Create new label (different from zero):
rowCount++;
//Flood fill on this point:
cv::floodFill( rowMask, cv::Point( x, y ), rowCount, (cv::Rect*)0, cv::Scalar(), 0 );
}
}
}
Этот процесс пометит все строки от 1
до r
. Это то, что мы хотели. Если вы посмотрите на изображение, вы увидите слабые строки, потому что наши метки соответствуют очень низким значениям интенсивности пикселей в градациях серого.
Хорошо, теперь давайте подготовим таблицу строк. Эта таблица на самом деле просто еще одно изображение, помните: та же ширина, что и у ввода, и высота, равная количеству строк, которые вы подсчитали в Row Mask
:
//create rows image:
cv::Mat rowTable = cv::Mat::zeros( cv::Size(binImage.cols, rowCount), CV_8UC1 );
//Just for convenience:
rowTable = 255 - rowTable;
Здесь я просто инвертировал финальное изображение для удобства. Потому что я хочу на самом деле увидеть, как таблица заполнена пикселями (очень низкой интенсивности), и убедиться, что все работает так, как задумано.
Теперь самое интересное. У нас есть оба изображения (или контейнеры данных). Нам нужно обрабатывать каждый блоб независимо. Идея состоит в том, что вы должны извлечь каждый блоб/контур/символ из бинарного изображения и вычислить его centroid
и присвоить новый label
. Опять же, есть много способов сделать это. Здесь я использую следующий подход:
Я пройдусь по binary mask
. Я получу current biggest blob
из этого бинарного ввода. Я вычислю его centroid
и сохраню его данные во всех необходимых контейнерах, а затем delete
извлеку этот блоб из маски. Я буду повторять процесс, пока не останется больше капель. Это мой способ сделать это, особенно потому, что у меня есть функции, которые я уже написал для этого. Это подход:
//Prepare a couple of dictionaries for data storing:
std::map< int, cv::Point > blobMap; //holds label, gives centroid
std::map< int, cv::Rect > boundingBoxMap; //holds label, gives bounding box
Сначала два dictionaries
. Один получает метку большого двоичного объекта и возвращает центроид. Другой получает ту же метку и возвращает ограничивающую рамку.
//Extract each individual blob:
cv::Mat bobFilterInput = binImage.clone();
//The new blob label:
int blobLabel = 0;
//Some control variables:
bool extractBlobs = true; //Controls loop
int currentBlob = 0; //Counter of blobs
while ( extractBlobs ){
//Get the biggest blob:
cv::Mat biggestBlob = findBiggestBlob( bobFilterInput );
//Compute the centroid/center of mass:
cv::Moments momentStructure = cv::moments( biggestBlob, true );
float cx = momentStructure.m10 / momentStructure.m00;
float cy = momentStructure.m01 / momentStructure.m00;
//Centroid point:
cv::Point blobCentroid;
blobCentroid.x = cx;
blobCentroid.y = cy;
//Compute bounding box:
boundingBox boxData;
computeBoundingBox( biggestBlob, boxData );
//Convert boundingBox data into opencv rect data:
cv::Rect cropBox = boundingBox2Rect( boxData );
//Label blob:
blobLabel++;
blobMap.emplace( blobLabel, blobCentroid );
boundingBoxMap.emplace( blobLabel, cropBox );
//Get the row for this centroid
int blobRow = rowMask.at<uchar>( cy, cx );
blobRow--;
//Place centroid on rowed image:
rowTable.at<uchar>( blobRow, cx ) = blobLabel;
//Resume blob flow control:
cv::Mat blobDifference = bobFilterInput - biggestBlob;
//How many pixels are left on the new mask?
int pixelsLeft = cv::countNonZero( blobDifference );
bobFilterInput = blobDifference;
//Done extracting blobs?
if ( pixelsLeft <= 0 ){
extractBlobs = false;
}
//Increment blob counter:
currentBlob++;
}
Посмотрите красивую анимацию того, как эта обработка проходит через каждый большой двоичный объект, обрабатывает его и удаляет до тех пор, пока ничего не останется:
Теперь несколько заметок к приведенному выше фрагменту. У меня есть несколько вспомогательных функций: biggestBlob и computeBoundingBox
. Эти функции вычисляют самый большой двоичный объект в двоичном изображении и преобразуют пользовательскую структуру ограничительной рамки в структуру OpenCV
Rect
соответственно. Это операции, которые выполняют эти функции.
Суть фрагмента заключается в следующем: если у вас есть изолированный большой двоичный объект, вычислите его centroid
(на самом деле я вычисляю center of mass
через central moments
). Создайте новый файл label
. Сохраните эти label
и centroid
в словаре dictionary
, в моем случае blobMap
. Дополнительно вычислите bounding box
и сохраните его в другом dictionary
, boundingBoxMap
:
//Label blob:
blobLabel++;
blobMap.emplace( blobLabel, blobCentroid );
boundingBoxMap.emplace( blobLabel, cropBox );
Теперь, используя данные centroid
, fetch
соответствующие row
этого блоба. Как только вы получите строку, сохраните это число в своей таблице строк:
//Get the row for this centroid
int blobRow = rowMask.at<uchar>( cy, cx );
blobRow--;
//Place centroid on rowed image:
rowTable.at<uchar>( blobRow, cx ) = blobLabel;
Превосходно. На этом этапе у вас есть готовая таблица строк. Давайте пройдемся по нему и, наконец, упорядочим эти чертовы капли:
int blobCounter = 1; //The ORDERED label, starting at 1
for( int y = 0; y < rowTable.rows; y++ ){
for( int x = 0; x < rowTable.cols; x++ ){
//Get current label:
uchar currentLabel = rowTable.at<uchar>( y, x );
//Is it a valid label?
if ( currentLabel != 255 ){
//Get the bounding box for this label:
cv::Rect currentBoundingBox = boundingBoxMap[ currentLabel ];
cv::rectangle( testImage, currentBoundingBox, cv::Scalar(0,255,0), 2, 8, 0 );
//The blob counter to string:
std::string counterString = std::to_string( blobCounter );
cv::putText( testImage, counterString, cv::Point( currentBoundingBox.x, currentBoundingBox.y-1 ),
cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(255,0,0), 1, cv::LINE_8, false );
blobCounter++; //Increment the blob/label
}
}
}
Ничего необычного, просто обычный вложенный цикл for
, перебирающий каждый пиксель на row table
. Если пиксель отличается от белого, используйте label
, чтобы получить как centroid
, так и bounding box
, и просто измените label
на возрастающее число. Для отображения результата я просто рисую ограничивающие рамки и новую метку на исходном изображении.
Посмотрите упорядоченную обработку в этой анимации:
Очень круто, вот бонусная анимация, таблица строк заполняется горизонтальными координатами:
person
stateMachine
schedule
31.08.2020
.
или-
? - person Jimit Vaghela   schedule 28.08.2020