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

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

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

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

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

4 ответов


один из способов сопоставления шаблонов-использование cv:: matchTemplate.

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

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

Шаблон Создатель

#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, делает и из этих трех бинаризованные изображения и выполняет близкую морфологическую операцию по созданию маски.
Три строки отладки копируют исходное изображение в Черное, используя вычисленную маску в качестве маски для операции копирования. Это помогло выбрать правильные значения для порога.

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

50c image filtered by mask

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 значений в одну строку в виде {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, тем лучше инвариантность ориентации мы получаем, но это также подразумевает более высокую вычислительную стоимость.

вы можете скачать всю программу template maker здесь.
При запуске с ANGLE_STEP как 30 я получаю следующие 12 шаблонов :
template 0template 30template 60template 90template 120template 150template 180template 210template 240template 270template 300template 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 соответствует каждому кандидату со всеми шаблонами, хранящими для каждого результата лучшего матча. Поскольку у нас есть шаблоны для нескольких ориентаций это обеспечивает инвариантность к ориентации.
Баллы каждого кандидата печатаются, поэтому мы можем принять решение о порог для отделения монет 50c от монет non 50c.
labelCoins копирует исходное изображение и рисует метку поверх тех, у которых оценка больше (или меньше, чем для некоторых методов) порога, определенного в MATCH_THRESHOLD.
И, наконец, мы сохраняем результат в a .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. На этот раз мы применяем его к изображению с несколькими монетами. Следует отметить, что пороги бинаризации были выбраны для бинаризации 50c, и они не будут работать должным образом для бинаризации всех монет на изображении. Но это не имеет значения, так как цель программы состоит только в том, чтобы идентифицировать монеты 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 is cv::findContours который находит контуры областей, присутствующих в его входном изображении. Которая здесь-маска, вычисленная ранее.
findContours возвращает вектор контуры. Каждый контур сам по себе является вектором точек, которые образуют внешнюю линию обнаруженного многоугольника.
Каждый полигон ограничивает область каждой монеты-кандидата.
Для каждый контур мы используем cv::drawContours чтобы нарисовать заполненный многоугольник над Черным изображением.
С этим нарисованным изображением мы используем ту же процедуру, что и ранее, для вычисления центроида и радиуса многоугольника.
И мы используем centerAndScale, та же функция, используемая в шаблоне maker, для центрирования и масштабирования изображения, содержащегося в этом полигоне, в изображении, которое будет иметь тот же размер, что и наши шаблоны. Таким образом, мы позже сможем выполнить правильное сопоставление даже для монет из фотографий разных весы.
Каждая из этих монет-кандидатов копируется в структуру кандидата, которая содержит:

  • кандидат изображения
  • x и y для центроида
  • радиус
  • результат

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

это 4 кандидата получено:
Candidate 0Candidate 1Candidate 2Candidate 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 сохраняет вычисленные кандидаты для отладки purpouses. А также, чтобы я мог разместить эти изображения здесь.

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 peforms соответствия.
cv::matchTemplate использует два изображения imput, второй меньше или равен по размеру первому.
Общий случай использования для небольшого шаблона (2-й параметр) для сопоставления с большим изображением (1-й параметр), и результатом является двумерный мат поплавков с соответствием шаблона вдоль изображения. Найдя максимум (или минимум в зависимости от метода) этого коврика поплавков, мы получаем наилучшую позицию кандидата для нашего шаблона на изображении 1-го параметра.
Но мы не заинтересованы в размещении нашего шаблона на изображении, у нас уже есть координаты наших кандидатов.
Мы хотим получить меру. сходства между нашим кандидатом и шаблоном. Вот почему мы используем cv::matchTemplate менее обычным способом; мы делаем это с изображением 1-го параметра размером, равным шаблону 2-го параметра. В этой ситуации результатом является мат размером 1x1. И единственное значение в этом коврике-наша оценка сходства (или несходства).

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

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

enter image description here

CCORR и CCOEFF дают неправильный результат, поэтому эти два отбрасываются. Из оставшихся 4 методов два метода SQDIFF являются теми, которые имеют более высокую относительную разницу между лучшим совпадением (которое составляет 50c) и 2-м лучшим (который не является 50c). Вот почему я выбрал их.
Я выбрал SQDIFF_NORMED, но для этого нет веских причин. Для того, чтобы действительно выбрали метод мы должны испытать с более высоким ammount образцов, не как раз одним.
Для этого метода рабочий порог может быть 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 );

результат :
Input image with 50c labeled

весь код для сопоставления монет можно загрузить здесь.

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

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

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


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

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

  • тогда у вас должно быть несколько шаблонов в каждом размере и использовать сопоставление шаблонов для распознавания его значения.(все шаблоны и обнаруженные монеты должны быть изменены до определенного размера. например, (100,100) ) для шаблона соответствие вы можете использовать matchtemplate


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


вы можете попробовать и настроить учебный набор изображений монет и генерировать просеивание / серфинг и т. д. дескрипторы этого. (EDIT:детекторы характеристик OpenCV Используя эти данные, вы можете настроить классификатор kNN, используя значения монет в качестве обучающих меток.

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