Я предпринимаю проект, который будет автоматически считать значения монет из входного изображения. До сих пор я сегментировал монеты, используя некоторую предварительную обработку с обнаружением краев и используя преобразование Хафа.
Мой вопрос: как мне действовать дальше? Мне нужно сделать несколько шаблонов соответствия на сегментированных изображениях на основе некоторых ранее сохраненных функций. Как я могу сделать это?
Я также читал о том, что называется K-Ближайшие соседи и я чувствую, что это то, что я должен использовать. Но я не совсем уверен, как его использовать.
Исследовательские статьи, за которыми я следил:
Это берет входное изображение и меньшее изображение, которое действует как шаблон. Он сравнивает шаблон с перекрывающимися областями изображения, вычисляя сходство шаблона с перекрывающейся областью. Доступно несколько методов для вычисления сравнения.
Этот метод не поддерживает напрямую масштабную или ориентационную инвариантность. Но это можно преодолеть, масштабируя кандидатов до эталонного размера и проверяя несколько повернутых шаблонов.
Подробный пример этой техники показан для обнаружения присутствия и местоположения монет 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
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 шаблонов:
#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
,
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 );
Весь код для монетоприемника можно скачать Вот.
Если метод работает, один и тот же метод может быть повторен для каждого типа монет, что приводит к полному обнаружению всех монет.
Если вы правильно обнаружите все монеты, лучше использовать размер (радиальная) а также Особенности RGB признать его ценность. Не стоит объединять эти функции, потому что их число не одинаково (размер равен одному числу, а число функций RGB намного больше единицы). Я рекомендую вам использовать два классификатора для этой цели. Один для размера и другой для функций RGB.
Вы должны классифицировать все монеты, например, 3 (зависит от типа
из ваших монет) размер класса. Вы можете сделать это с помощью простого 1NN
классификатор (просто вычислите радиус тестовой монеты и классифицируйте его как
ближайший предопределенный радиус)
Затем вы должны иметь несколько шаблонов каждого размера и использовать сопоставление с шаблоном, чтобы распознать его значение (все шаблоны и обнаруженные монеты должны быть изменены до определенного размера, например (100 100)) Для шаблона
соответствие вы можете использовать matchtemplate функция. Я думаю, что CV_TM_CCOEFF метод может быть лучшим, но вы можете проверить все методы
чтобы получить хороший результат. (Обратите внимание, что вам не нужно искать на изображении монеты, потому что вы обнаружили монету ранее, как вы упоминали в
вопрос. Вам просто нужно использовать эту функцию, чтобы получить одно число как сходство / различие между двумя изображениями и классифицировать тестовую монету для класса, в котором сходство максимально или различие минимизировано)
EDIT1: У вас должны быть все вращения в ваших шаблонах в каждом классе, чтобы компенсировать вращение тестовой монеты.
EDIT2: Если все монеты имеют разные размеры, достаточно сделать первый шаг. В противном случае вы должны соединить одинаковые размеры с одним классом и классифицировать тестовую монету, используя второй шаг (функции RGB).
(1) Найдите край монет, используя Hough Transform Algorithm
,
(2) Определите начальную точку монет. Я не знаю, как ты это сделаешь.
(3) Вы можете использовать k
от KNN Algorithm
для сравнения диаметра или монет. Не забудьте установить значение смещения.
Вы можете попробовать установить обучающий набор изображений монет и сгенерировать его SIFT / SURF и т. Д. (РЕДАКТИРОВАТЬ: Детекторы функций OpenCV
Используя эти данные, вы можете настроить классификатор kNN, используя значения монет в качестве обучающих меток.
После того, как вы выполните классификацию kNN для ваших изображений сегментированных монет, результат вашей классификации даст ценность монет.