IT Notes

OpenCV: HSV и поиск объектов по цвету

Рассмотрим еще один способ поиска объектов на изображениях с помощью OpenCV. Воспользуемся одним из самых простых, однако достаточно полезных на практике методов - распознавание по цвету.

Вот что у нас получится:

opencv-hsv-sample

Коротко о цветовой схеме HSV

Очень часто с изображениями работают как с массивом пикселей в формате RGB. Хотя такое представление оказывается относительно интуитивным, его нельзя назвать оптимальным для задачи, которую нам предстоит решить. В RGB цвет пикселя определяется насыщенностью красным, зеленым и голубым. Таким образом, выбор диапазона оттенков одного и того же цвета становится не самой простой задачей.

С форматом HSV дела обстоят иначе. Эта цветовая схема определяется тремя компонентами:

  1. Hue - цветовой тон;
  2. Saturation - насыщенность;
  3. Value - яркость.

hsv-gimp

В схеме HSV базовый цвет можно выбрать с помощью компоненты Hue (например, красный, оранжевый и т.д.). Две остальных компоненты позволяют регулировать насыщенность и яркость базового цвета, делая его более насыщенным или тусклым, более светлым или темным.

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

Реализация поиска объектов на изображении по цвету с помощью OpenCV

В интерфейс приложения включим область для просмотра изображений, кнопку загрузки другого изображения, переключатель режимов для удобства отладки и несколько ползунков для выбора фильтров по компонентам HSV:

opencv-hsv-sample-interface-thumbnail

Итак, мы хотим найти на изображении объекты некоторого цвета. Сделать это можно в несколько этапов:

  1. Отфильтровать все лишнее по диапазону HSV-цветов. Здесь мы получим битовую маску, где нужные нам участки изображения (в идеале - искомые объекты) окажутся помечены белым цветом, а все остальное станет черным;
  2. Немного "подчистить" шумы и сгладить битовую маску;
  3. Найти контуры получившихся белых областей;
  4. Определить прямоугольники, в которые вписываются найденные контуры.

Заголовочный файл mainwidget.h:

#ifndef MAINWIDGET_H
#define MAINWIDGET_H

#include <opencv2/imgproc.hpp>

#include <QWidget>
#include <QSettings>

namespace Ui {
class MainWidget;
}

class MainWidget : public QWidget {
    Q_OBJECT

public:
    explicit MainWidget( QWidget* parent = 0 );
    ~MainWidget();

private slots:
    void onLoad();
    void refreshHSV();

private:
    Ui::MainWidget* ui;
    cv::Mat m_mat;

    QString m_lastLoadPath;
    QSettings m_settings;

};

#endif // MAINWIDGET_H

Файл реализации mainwidget.cpp:

#include "mainwidget.h"
#include "ui_mainwidget.h"

#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>

#include <QFileDialog>
#include <QPixmap>
#include <QPainter>

static const char* CONFIG_FILE_NAME = "config.ini";

MainWidget::MainWidget( QWidget* parent ) :
    QWidget( parent ), ui( new Ui::MainWidget ), m_lastLoadPath( "." ), 
    m_settings( CONFIG_FILE_NAME, QSettings::IniFormat ) {
    ui->setupUi( this );

    foreach( QSlider* slider, findChildren< QSlider* >() ) {
        connect( slider, SIGNAL( sliderMoved( int ) ), SLOT( refreshHSV() ) );
        QString sliderName = slider->objectName();
        slider->setValue( m_settings.value( sliderName, slider->value() ).toInt() );
        if( QSpinBox* sp = findChild< QSpinBox* >( "sp" + sliderName.mid( 2 ) ) ) {
            sp->setMinimum( slider->minimum() );
            sp->setMaximum( slider->maximum() );
            sp->setValue( slider->value() );
            connect( sp, SIGNAL( valueChanged( int ) ), SLOT( refreshHSV() ) );
        }
    }

    foreach( const QRadioButton* rb, findChildren< QRadioButton* >() ) {
        connect( rb, SIGNAL( clicked( bool ) ), SLOT( refreshHSV() ) );
    }

    connect( ui->bnLoad, SIGNAL( clicked( bool ) ), SLOT( onLoad() ) );
}

MainWidget::~MainWidget() {
    foreach( const QSlider* slider, findChildren< QSlider* >() ) {
        QString sliderName = slider->objectName();
        m_settings.setValue( sliderName, slider->value() );
    }

    delete ui;
}

void MainWidget::onLoad() {
    QString imgName = QFileDialog::getOpenFileName( 
        this, 
        "Load image", 
        m_lastLoadPath, 
        "Images (*.jpg *.jpeg *.png *.bmp)" 
    );
    if( imgName.isEmpty() ) {
        return;
    }

    m_lastLoadPath = QFileInfo( imgName ).absolutePath();

    m_mat = cv::imread( imgName.toStdString() );
    if( !m_mat.empty() ) {
        cv::cvtColor( m_mat, m_mat, cv::COLOR_BGR2RGB );
    }

    refreshHSV();
}

void MainWidget::refreshHSV() {
    if( m_mat.empty() ) {
        return;
    }

    QImage resultImg;

    if( ui->rbOriginal->isChecked() ) {
        resultImg = QImage( m_mat.data, m_mat.cols, m_mat.rows, m_mat.step, QImage::Format_RGB888 ).copy();
    } else {
        int hueFrom = ui->slHueFrom->value();
        int hueTo = std::max( hueFrom, ui->slHueTo->value() );

        int saturationFrom = ui->slSaturationFrom->value();
        int saturationTo = std::max( saturationFrom, ui->slSaturationTo->value() );

        int valueFrom = ui->slValueFrom->value();
        int valueTo = std::max( valueFrom, ui->slValueTo->value() );

        cv::Mat thresholdedMat;
        cv::cvtColor( m_mat, thresholdedMat, cv::COLOR_RGB2HSV );
        // Отфильтровываем только то, что нужно, по диапазону цветов
        cv::inRange(
            thresholdedMat,
            cv::Scalar( hueFrom, saturationFrom, valueFrom ),
            cv::Scalar( hueTo, saturationTo, valueTo ),
            thresholdedMat
        );

        // Убираем шум
        cv::erode(
            thresholdedMat,
            thresholdedMat,
            cv::getStructuringElement( cv::MORPH_ELLIPSE, cv::Size( 5, 5 ) )
        );
        cv::dilate(
            thresholdedMat,
            thresholdedMat,
            cv::getStructuringElement( cv::MORPH_ELLIPSE, cv::Size( 5, 5 ) )
        );

        // Замыкаем оставшиеся крупные объекты
        cv::dilate(
            thresholdedMat,
            thresholdedMat,
            cv::getStructuringElement( cv::MORPH_ELLIPSE, cv::Size( 5, 5 ) )
        );
        cv::erode(
            thresholdedMat,
            thresholdedMat,
            cv::getStructuringElement( cv::MORPH_ELLIPSE, cv::Size( 5, 5 ) )
        );

        if( ui->rbCanny->isChecked() ) {
            // Визуально выделяем границы
            cv::Canny( thresholdedMat, thresholdedMat, 100, 50, 5 );
        }

        if( ui->rbResult->isChecked() ) {
            // Находим контуры
            std::vector< std::vector< cv::Point > > countours;
            std::vector< cv::Vec4i > hierarchy;
            cv::findContours(
                thresholdedMat,
                countours,
                hierarchy,
                CV_RETR_TREE,
                CV_CHAIN_APPROX_SIMPLE,
                cv::Point( 0, 0 )
            );

            std::vector< cv::Rect > rects;
            for( uint i = 0; i < countours.size(); ++i ) {
                // Пропускаем внутренние контуры
                if( 0 <= hierarchy[ i ][ 3 ] ) {
                    continue;
                }
                rects.push_back( cv::boundingRect( countours[ i ] ) );
            }

            resultImg = QImage(
                            m_mat.data,
                            m_mat.cols,
                            m_mat.rows,
                            m_mat.step,
                            QImage::Format_RGB888
                        ).copy();

            QPainter p;
            p.begin( &resultImg );
            p.setPen( QPen( Qt::green, 2 ) );
            foreach( const cv::Rect& r, rects ) {
                p.drawRect( r.x, r.y, r.width, r.height );
            }
            p.end();

        } else {
            resultImg = QImage(
                            thresholdedMat.data,
                            thresholdedMat.cols,
                            thresholdedMat.rows,
                            thresholdedMat.step,
                            QImage::Format_Indexed8
                        ).copy();
        }
    }

    ui->lbView->setPixmap(
        QPixmap::fromImage( resultImg ).scaled(
            ui->lbView->size(),
            Qt::KeepAspectRatio,
            Qt::SmoothTransformation
        )
    );
}

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

Исходники

 Скачать пример поиска объектов по цвету на изображении с помощью OpenCV

Еще пара более реалистичных примеров использования

opencv-hsv-sample-1-thumbnail

opencv-hsv-sample-2-thumbnail

opencv-hsv-sample-3-thumbnail

Понравилась статья?
Не забудь поделиться ей с друзьями!

Похожие публикации

Комментарии

Доброго времени суток!

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

При таком раскладе, OpenCV лишний совсем. Можно просто, оперевшись на белый фон, найти границы знака простым перебором.

Гораздо интереснее было бы разобрать примеры с оттенками цвета. Допустим, из такой картинки https://valvetimes.com/wp-content/uploads/2015/03/boomboxscout.png выделить персонажа. Причем, в данной картинке цвета(оттенки) искусственные - это ведь не фотография. С реальными фотографиями все будет еще сложнее. Имхо.

P.S. Сайт не мой. Игра TF2

Anonymous:

Доброго времени суток!

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

При таком раскладе, OpenCV лишний совсем. Можно просто, оперевшись на белый фон, найти границы знака простым перебором.

Здравствуйте, спасибо за комментарий.

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

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

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

P.S. Добавил несколько более реалистичных примеров на поиск объектов по цвету в конец статьи.

Доброго времени суток!

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

При распознавании объектов по цвету с помощью OpenCV, есть проблема как раз в подстройке параметров под каждое новое изображение. Особенно, если нет "контрастных объектов нужного цвета". Было бы неплохо, попытаться прикрутить к этому нейронные сети.

Хотя, это наверняка не так просто...

Здравствуйте можете сделать следующею статью про файловый менеджер на QT .

Anonymous:

Доброго времени суток!

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

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

Anonymous:

Здравствуйте можете сделать следующею статью про файловый менеджер на QT .

Здравствуйте. А какие функции Вы бы хотели видеть в подобном проекте?

Доброго времени суток!

Жаль. Очень. Понятно, что проще описать функции файлового менеджера, коих как грязи. Опять же - какой из начинающих программеров не мечтал написать свой ФМ с поэтессами и шахматами?

P.S. Это так - крик души! Без перехода на личности...

А когда будет новая статья ?

Anonymous:

А когда будет новая статья ?

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

Как работать с Ping в Node-red ?

Anonymous:

Как работать с Ping в Node-red ?

Здравствуйте. К сожалению, это вопрос не по моей специализации.

Блог - всё?

Блог - всё?

Здравствуйте. На данный момент выпуск новых статей приостановлен. К сожалению, не знаю, когда смогу возобновить публикацию материалов.

RSS RSS-рассылка

Популярное

Дешевый хостинг