Шаблон соответствия для монет с OpenCV

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

Мой вопрос: как мне действовать дальше? Мне нужно сделать несколько шаблонов соответствия на сегментированных изображениях на основе некоторых ранее сохраненных функций. Как я могу сделать это?

Я также читал о том, что называется K-Ближайшие соседи и я чувствую, что это то, что я должен использовать. Но я не совсем уверен, как его использовать.

Исследовательские статьи, за которыми я следил:

7

Решение

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

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

Создатель шаблонов

#define TEMPLATE_IMG "50c.jpg"#define ANGLE_STEP 30
int main()
{
cv::Mat image = loadImage(TEMPLATE_IMG);
cv::Mat mask = createMask( image );
cv::Mat loc = locate( mask );
cv::Mat imageCS;
cv::Mat maskCS;
centerAndScale( image, mask, loc, imageCS, maskCS);
saveRotatedTemplates( imageCS, maskCS, ANGLE_STEP );
return 0;
}

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

cv::Mat loadImage(const char* name)
{
cv::Mat image;
image = cv::imread(name);
if ( image.data==NULL || image.channels()!=3 )
{
std::cout << name << " could not be read or is not correct." << std::endl;
exit(1);
}
return image;
}

loadImage использования cv::imread читать изображение. Проверяет, что данные были прочитаны, и изображение имеет три канала, и возвращает прочитанное изображение.

#define THRESHOLD_BLUE  130
#define THRESHOLD_TYPE_BLUE  cv::THRESH_BINARY_INV
#define THRESHOLD_GREEN 230
#define THRESHOLD_TYPE_GREEN cv::THRESH_BINARY_INV
#define THRESHOLD_RED   140
#define THRESHOLD_TYPE_RED   cv::THRESH_BINARY
#define CLOSE_ITERATIONS 5
cv::Mat createMask(const cv::Mat& image)
{
cv::Mat channels[3];
cv::split( image, channels);
cv::Mat mask[3];
cv::threshold( channels[0], mask[0], THRESHOLD_BLUE , 255, THRESHOLD_TYPE_BLUE );
cv::threshold( channels[1], mask[1], THRESHOLD_GREEN, 255, THRESHOLD_TYPE_GREEN );
cv::threshold( channels[2], mask[2], THRESHOLD_RED  , 255, THRESHOLD_TYPE_RED );
cv::Mat compositeMask;
cv::bitwise_and( mask[0], mask[1], compositeMask);
cv::bitwise_and( compositeMask, mask[2], compositeMask);
cv::morphologyEx(compositeMask, compositeMask, cv::MORPH_CLOSE,
cv::Mat(), cv::Point(-1, -1), CLOSE_ITERATIONS );

/// Next three lines only for debugging, may be removed
cv::Mat filtered;
image.copyTo( filtered, compositeMask );
cv::imwrite( "filtered.jpg", filtered);

return compositeMask;
}

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

Здесь мы можем видеть изображение 50с, отфильтрованное маской, созданной в createMask

50c изображение фильтруется по маске

cv::Mat locate( const cv::Mat& mask )
{
// Compute center and radius.
cv::Moments moments = cv::moments( mask, true);
float area = moments.m00;
float radius = sqrt( area/M_PI );
float xCentroid = moments.m10/moments.m00;
float yCentroid = moments.m01/moments.m00;
float m[1][3] = {{ xCentroid, yCentroid, radius}};
return cv::Mat(1, 3, CV_32F, m);
}

locate вычисляет центр массы маски и ее радиус. Возврат этих 3 значений в одну строку mat в виде {x, y, radius}.
Оно использует cv::moments который вычисляет все моменты вплоть до третьего порядка многоугольника или растровой формы. Растеризованная форма в нашем случае. Нас не интересуют все эти моменты. Но три из них полезны здесь. M00 это площадь маски. А центроид можно рассчитать по m00, m10 и m01.

void centerAndScale(const cv::Mat& image, const cv::Mat& mask,
const cv::Mat& characteristics,
cv::Mat& imageCS, cv::Mat& maskCS)
{
float radius = characteristics.at<float>(0,2);
float xCenter = characteristics.at<float>(0,0);
float yCenter = characteristics.at<float>(0,1);
int diameter = round(radius*2);
int xOrg = round(xCenter-radius);
int yOrg = round(yCenter-radius);
cv::Rect roiOrg = cv::Rect( xOrg, yOrg, diameter, diameter );
cv::Mat roiImg = image(roiOrg);
cv::Mat roiMask = mask(roiOrg);
cv::Mat centered = cv::Mat::zeros( diameter, diameter, CV_8UC3);
roiImg.copyTo( centered, roiMask);
cv::imwrite( "centered.bmp", centered); // debug
imageCS.create( TEMPLATE_SIZE, TEMPLATE_SIZE, CV_8UC3);
cv::resize( centered, imageCS, cv::Size(TEMPLATE_SIZE,TEMPLATE_SIZE), 0, 0 );
cv::imwrite( "scaled.bmp", imageCS); // debug

roiMask.copyTo(centered);
cv::resize( centered, maskCS, cv::Size(TEMPLATE_SIZE,TEMPLATE_SIZE), 0, 0 );
}

centerAndScale использует центроид и радиус, вычисленный locate чтобы получить интересующую область входного изображения и интересующую область маски так, чтобы центр таких областей был также центром монеты и маски, а длина сторон областей была равна диаметру монеты / маска.
Эти регионы позже масштабируются до фиксированного значения TEMPLATE_SIZE. Этот масштабированный регион будет нашим справочным шаблоном. Когда позже в программе сопоставления мы хотим проверить, является ли обнаруженная монета-кандидат этой монетой, мы также возьмем область монеты-кандидата, отцентрируем и отредактируем эту монету-кандидата таким же образом перед выполнением сопоставления с шаблоном. Таким образом, мы достигаем масштабной инвариантности.

void saveRotatedTemplates( const cv::Mat& image, const cv::Mat& mask, int stepAngle )
{
char name[1000];
cv::Mat rotated( TEMPLATE_SIZE, TEMPLATE_SIZE, CV_8UC3 );
for ( int angle=0; angle<360; angle+=stepAngle )
{
cv::Point2f center( TEMPLATE_SIZE/2, TEMPLATE_SIZE/2);
cv::Mat r = cv::getRotationMatrix2D(center, angle, 1.0);

cv::warpAffine(image, rotated, r, cv::Size(TEMPLATE_SIZE, TEMPLATE_SIZE));
sprintf( name, "template-%03d.bmp", angle);
cv::imwrite( name, rotated );

cv::warpAffine(mask, rotated, r, cv::Size(TEMPLATE_SIZE, TEMPLATE_SIZE));
sprintf( name, "templateMask-%03d.bmp", angle);
cv::imwrite( name, rotated );
}
}

saveRotatedTemplates сохраняет предыдущий вычисленный шаблон.
Но он сохраняет несколько его копий, каждая из которых повернута на угол, определенный в ANGLE_STEP, Цель этого — обеспечить неизменность ориентации. Чем ниже мы определяем stepAngle, тем лучше получаем неизменность ориентации, но это также подразумевает более высокие вычислительные затраты.

Вы можете скачать всю программу создания шаблонов Вот.
При запуске с ANGLE_STEP как 30 я получаю следующие 12 шаблонов:
шаблон 0шаблон 30шаблон 60шаблон 90шаблон 120шаблон 150шаблон 180шаблон 210шаблон 240шаблон 270шаблон 300шаблон 330

Шаблон соответствия.

#define INPUT_IMAGE "coins.jpg"#define LABELED_IMAGE "coins_with50cLabeled.bmp"#define LABEL "50c"#define MATCH_THRESHOLD 0.065
#define ANGLE_STEP 30
int main()
{
vector<cv::Mat> templates;
loadTemplates( templates, ANGLE_STEP );
cv::Mat image = loadImage( INPUT_IMAGE );
cv::Mat mask = createMask( image );
vector<Candidate> candidates;
getCandidates( image, mask, candidates );
saveCandidates( candidates ); // debug
matchCandidates( templates, candidates );
for (int n = 0; n < candidates.size( ); ++n)
std::cout << candidates[n].score << std::endl;
cv::Mat labeledImg = labelCoins( image, candidates, MATCH_THRESHOLD, false, LABEL );
cv::imwrite( LABELED_IMAGE, labeledImg );
return 0;
}

Цель здесь — прочитать шаблоны и изображение, которое нужно исследовать, и определить местоположение монет, которые соответствуют нашему шаблону.

Сначала мы читаем в векторе изображений все шаблоны изображений, которые мы создали в предыдущей программе.
Затем мы читаем изображение для изучения.
Затем мы оцифровываем изображение, которое нужно исследовать, используя ту же функцию, что и в шаблонизаторе.
getCandidates находит группы точек, которые вместе образуют многоугольник. Каждый из этих полигонов является кандидатом в монеты. И все они масштабируются и центрируются в квадрате размера, равного размеру наших шаблонов, чтобы мы могли выполнять сопоставление способом, инвариантным к масштабу.
Мы сохраняем изображения-кандидаты, полученные для отладки и настройки.
matchCandidates сопоставляет каждого кандидата со всеми шаблонами, хранящими для каждого результата лучшего совпадения. Поскольку у нас есть шаблоны для нескольких ориентаций, это обеспечивает неизменность ориентации.
Результаты каждого кандидата печатаются, поэтому мы можем выбрать порог, чтобы отделить монеты 50 центов от монет не 50 центов.
labelCoins копирует исходное изображение и рисует метку над теми, у которых оценка выше (или меньше, чем для некоторых методов) порога, определенного в MATCH_THRESHOLD,
И, наконец, мы сохраняем результат в .BMP

void loadTemplates(vector<cv::Mat>& templates, int angleStep)
{
templates.clear( );
for (int angle = 0; angle < 360; angle += angleStep)
{
char name[1000];
sprintf( name, "template-%03d.bmp", angle );
cv::Mat templateImg = cv::imread( name );
if (templateImg.data == NULL)
{
std::cout << "Could not read " << name << std::endl;
exit( 1 );
}
templates.push_back( templateImg );
}
}

loadTemplates похож на loadImage, Но он загружает несколько изображений вместо одного и сохраняет их в std::vector,

loadImage точно так же, как в шаблоне производителя.

createMask также точно так же, как в tempate Maker. На этот раз мы применим его к изображению с несколькими монетами. Следует отметить, что пороги бинаризации были выбраны для бинаризации 50с, и они не будут работать должным образом для бинаризации всех монет на изображении. Но это не имеет значения, поскольку целью программы является только идентификация монет 50c. Пока они правильно сегментированы, мы в порядке. Это действительно работает в нашу пользу, если в этой сегментации будут потеряны некоторые монеты, так как мы сэкономим время, оценивая их (до тех пор, пока мы теряем только монеты не 50c).

typedef struct Candidate
{
cv::Mat image;
float x;
float y;
float radius;
float score;
} Candidate;

void getCandidates(const cv::Mat& image, const cv::Mat& mask,
vector<Candidate>& candidates)
{
vector<vector<cv::Point> > contours;
vector<cv::Vec4i> hierarchy;
/// Find contours
cv::Mat maskCopy;
mask.copyTo( maskCopy );
cv::findContours( maskCopy, contours, hierarchy, CV_RETR_TREE, CV_CHAIN_APPROX_SIMPLE, cv::Point( 0, 0 ) );
cv::Mat maskCS;
cv::Mat imageCS;
cv::Scalar white = cv::Scalar( 255 );
for (int nContour = 0; nContour < contours.size( ); ++nContour)
{
/// Draw contour
cv::Mat drawing = cv::Mat::zeros( mask.size( ), CV_8UC1 );
cv::drawContours( drawing, contours, nContour, white, -1, 8, hierarchy, 0, cv::Point( ) );

// Compute center and radius and area.
// Discard small areas.
cv::Moments moments = cv::moments( drawing, true );
float area = moments.m00;
if (area < CANDIDATES_MIN_AREA)
continue;
Candidate candidate;
candidate.radius = sqrt( area / M_PI );
candidate.x = moments.m10 / moments.m00;
candidate.y = moments.m01 / moments.m00;
float m[1][3] = {
{ candidate.x, candidate.y, candidate.radius}
};
cv::Mat characteristics( 1, 3, CV_32F, m );
centerAndScale( image, drawing, characteristics, imageCS, maskCS );
imageCS.copyTo( candidate.image );
candidates.push_back( candidate );
}
}

Сердце getCandidates является cv::findContours который находит контуры областей, присутствующих в его входном изображении. Который здесь является маской, ранее вычисленной.
findContours возвращает вектор контуров. Каждый контур сам является вектором точек, которые образуют внешнюю линию обнаруженного многоугольника.
Каждый многоугольник ограничивает область каждой монеты-кандидата.
Для каждого контура мы используем cv::drawContours нарисовать закрашенный многоугольник на черном изображении.
С этим нарисованным изображением мы используем ту же процедуру, которая была объяснена ранее для вычисления центроида и радиуса многоугольника.
И мы используем centerAndScaleта же функция, используемая в шаблонизаторе, для центрирования и масштабирования изображения, содержащегося в этом полигоне, в изображении, которое будет иметь тот же размер, что и наши шаблоны. Таким образом, позже мы сможем выполнить правильное сопоставление даже для монет с фотографий разных масштабов.
Каждая из этих монет-кандидатов копируется в структуру кандидата, которая содержит:

  • Изображение кандидата
  • х и у для центроида
  • радиус
  • Гол

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

Вот 4 полученных кандидата:
Кандидат 0Кандидат 1Кандидат 2Кандидат 3

void saveCandidates(const vector<Candidate>& candidates)
{
for (int n = 0; n < candidates.size( ); ++n)
{
char name[1000];
sprintf( name, "Candidate-%03d.bmp", n );
cv::imwrite( name, candidates[n].image );
}
}

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

void matchCandidates(const vector<cv::Mat>& templates,
vector<Candidate>& candidates)
{
for (auto it = candidates.begin( ); it != candidates.end( ); ++it)
matchCandidate( templates, *it );
}

matchCandidates просто звонки matchCandidate для каждого кандидата. После завершения у нас будет подсчитано количество баллов для всех кандидатов.

void matchCandidate(const vector<cv::Mat>& templates, Candidate& candidate)
{
/// For SQDIFF and SQDIFF_NORMED, the best matches are lower values. For all the other methods, the higher the better
candidate.score;
if (MATCH_METHOD == CV_TM_SQDIFF || MATCH_METHOD == CV_TM_SQDIFF_NORMED)
candidate.score = FLT_MAX;
else
candidate.score = 0;
for (auto it = templates.begin( ); it != templates.end( ); ++it)
{
float score = singleTemplateMatch( *it, candidate.image );
if (MATCH_METHOD == CV_TM_SQDIFF || MATCH_METHOD == CV_TM_SQDIFF_NORMED)
{
if (score < candidate.score)
candidate.score = score;
}
else
{
if (score > candidate.score)
candidate.score = score;
}
}
}

matchCandidate имеет в качестве входных данных одного кандидата и все шаблоны. Его цель — сопоставить каждый шаблон с кандидатом. Эта работа делегирована singleTemplateMatch,
Мы храним лучший результат, который для CV_TM_SQDIFF а также CV_TM_SQDIFF_NORMED самый маленький, а для других подходящих методов самый большой.

float singleTemplateMatch(const cv::Mat& templateImg, const cv::Mat& candidateImg)
{
cv::Mat result( 1, 1, CV_8UC1 );
cv::matchTemplate( candidateImg, templateImg, result, MATCH_METHOD );
return result.at<float>( 0, 0 );
}

singleTemplateMatch выполняет сопоставление.
cv::matchTemplate использует два импульсных изображения, второе меньше или равно по размеру первому.
Обычный вариант использования — сопоставление небольшого шаблона (2-й параметр) с большим изображением (1-й параметр), и в результате получается двумерный мат чисел с плавающей точкой с сопоставлением шаблона вдоль изображения. Размещая максимальное значение (или минимальное значение в зависимости от метода) этого Mat of float, мы получаем лучшую позицию кандидата для нашего шаблона на изображении 1-го параметра.
Но мы не заинтересованы в размещении нашего шаблона на изображении, у нас уже есть координаты наших кандидатов.
То, что мы хотим, чтобы получить меру сходства между нашим кандидатом и шаблоном. Вот почему мы используем cv::matchTemplate менее привычным способом; мы делаем это с 1-м параметром изображения размером, равным 2-му параметру шаблона. В этой ситуации результатом является мат размером 1х1. И единственное значение в этом мате — наша оценка подобия (или разнородности).

for (int n = 0; n < candidates.size( ); ++n)
std::cout << candidates[n].score << std::endl;

Мы печатаем оценки, полученные для каждого из наших кандидатов.
В этой таблице мы видим оценки для каждого из методов, доступных для cv :: matchTemplate. Лучший результат — зеленый.

введите описание изображения здесь

CCORR и CCOEFF дают неверный результат, поэтому эти два отбрасываются. Из оставшихся 4 методов два метода SQDIFF — это те, которые имеют более высокую относительную разницу между лучшим соответствием (которое составляет 50c) и вторым лучшим (которое не является 50c). Вот почему я выбрал их.
Я выбрал SQDIFF_NORMED, но для этого нет веских причин. Чтобы действительно выбрать метод, мы должны тестировать с большим количеством образцов, а не с одним.
Для этого метода рабочий порог может быть 0,065. Выбор правильного порога также требует много образцов.

bool selected(const Candidate& candidate, float threshold)
{
/// For SQDIFF and SQDIFF_NORMED, the best matches are lower values. For all the other methods, the higher the better
if (MATCH_METHOD == CV_TM_SQDIFF || MATCH_METHOD == CV_TM_SQDIFF_NORMED)
return candidate.score <= threshold;
else
return candidate.score>threshold;
}

void drawLabel(const Candidate& candidate, const char* label, cv::Mat image)
{
int x = candidate.x - candidate.radius;
int y = candidate.y;
cv::Point point( x, y );
cv::Scalar blue( 255, 128, 128 );
cv::putText( image, label, point, CV_FONT_HERSHEY_SIMPLEX, 1.5f, blue, 2 );
}

cv::Mat labelCoins(const cv::Mat& image, const vector<Candidate>& candidates,
float threshold, bool inverseThreshold, const char* label)
{
cv::Mat imageLabeled;
image.copyTo( imageLabeled );

for (auto it = candidates.begin( ); it != candidates.end( ); ++it)
{
if (selected( *it, threshold ))
drawLabel( *it, label, imageLabeled );
}

return imageLabeled;
}

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

cv::imwrite( LABELED_IMAGE, labeledImg );

Результат:
Входное изображение с маркировкой 50c

Весь код для монетоприемника можно скачать Вот.

Это хороший метод?


Сложно сказать.
Метод последовательный. Он правильно обнаруживает монету 50 с для образца и входного изображения.
Но мы понятия не имеем, является ли метод надежным, потому что он не был протестирован с правильным размером выборки. И еще более важно проверить его на выборках, которые не были доступны во время кодирования программы, что является истинной мерой надежности, когда выполняется с достаточно большим размером выборки.
Я довольно уверен в методе, не имеющем ложных срабатываний от серебряных монет. Но я не уверен в других медных монетах, таких как 20с. Как видно из полученных оценок, монета 20с получает оценку, очень похожую на 50с.
Также вполне возможно, что ложные негативы будут происходить при различных условиях освещения. Этого можно и нужно избегать, если мы контролируем условия освещения, например, когда мы проектируем машину для фотографирования монет и их подсчета.

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

7

Другие решения

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

  • Вы должны классифицировать все монеты, например, 3 (зависит от типа
    из ваших монет) размер класса. Вы можете сделать это с помощью простого 1NN
    классификатор
    (просто вычислите радиус тестовой монеты и классифицируйте его как
    ближайший предопределенный радиус)

  • Затем вы должны иметь несколько шаблонов каждого размера и использовать сопоставление с шаблоном, чтобы распознать его значение (все шаблоны и обнаруженные монеты должны быть изменены до определенного размера, например (100 100)) Для шаблона
    соответствие вы можете использовать matchtemplate функция. Я думаю, что CV_TM_CCOEFF метод может быть лучшим, но вы можете проверить все методы
    чтобы получить хороший результат. (Обратите внимание, что вам не нужно искать на изображении монеты, потому что вы обнаружили монету ранее, как вы упоминали в
    вопрос. Вам просто нужно использовать эту функцию, чтобы получить одно число как сходство / различие между двумя изображениями и классифицировать тестовую монету для класса, в котором сходство максимально или различие минимизировано)

EDIT1: У вас должны быть все вращения в ваших шаблонах в каждом классе, чтобы компенсировать вращение тестовой монеты.

EDIT2: Если все монеты имеют разные размеры, достаточно сделать первый шаг. В противном случае вы должны соединить одинаковые размеры с одним классом и классифицировать тестовую монету, используя второй шаг (функции RGB).

1

(1) Найдите край монет, используя Hough Transform Algorithm,
(2) Определите начальную точку монет. Я не знаю, как ты это сделаешь.
(3) Вы можете использовать k от KNN Algorithm для сравнения диаметра или монет. Не забудьте установить значение смещения.

-1

Вы можете попробовать установить обучающий набор изображений монет и сгенерировать его SIFT / SURF и т. Д. (РЕДАКТИРОВАТЬ: Детекторы функций OpenCV
Используя эти данные, вы можете настроить классификатор kNN, используя значения монет в качестве обучающих меток.

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

-1
По вопросам рекламы [email protected]