Avatar ·

Улучшение алгоритма распознавания банки Coca-Cola на изображении

Приветствую, коллеги. Я пишу программу для распознавания банки Coca-Cola на фотографиях с помощью OpenCV на C++. Идея простая: банка красная, цилиндрическая, с белой лентой. Я использую цветовую фильтрацию (HSV), чтобы выделить красные области, затем нахожу контуры и аппроксимирую их эллипсами. Вроде бы логично, но есть большая проблема: алгоритм жутко глючит на реальных снимках. Вот мой текущий код: ```cpp cv::Mat hsv, mask; cv::cvtColor(img, hsv, cv::COLOR_BGR2HSV); cv::inRange(hsv, cv::Scalar(0, 100, 100), cv::Scalar(10, 255, 255), mask); // Также добавляю нижнюю границу красного (от 160 до 180) cv::Mat mask2; cv::inRange(hsv, cv::Scalar(160, 100, 100), cv::Scalar(180, 255, 255), mask2); mask = mask | mask2; std::vector> contours; cv::findContours(mask, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE); for (auto& c : contours) { if (c.size() > 50) { cv::RotatedRect ellipse = cv::fitEllipse(c); // Проверка на соотношение сторон (банка вытянутая) float ratio = ellipse.size.width / ellipse.size.height; if (ratio > 0.4 && ratio < 2.5) { cv::ellipse(img, ellipse, cv::Scalar(0, 255, 0), 2); } } } ``` Проблемы: 1. На засвеченных участках (блики на металле) красный цвет уходит в розовый или белый, маска теряет банку. 2. Если на фоне есть красные объекты (например, кирпичная стена, красная машина), алгоритм находит их как банку. 3. Банка может быть повернута под углом, и эллипс плохо подходит. Как улучшить алгоритм, чтобы он был устойчивее к освещению и фону? Может, нужно использовать машинное обучение или более умную геометрию? Буду благодарен за любые идеи и примеры кода. <<>>

Проблема и основной подход

Ваш текущий метод основан исключительно на цвете и грубой форме (эллипс). Это классический случай, когда "наивный" подход пасует перед реальными условиями: блики, тени, сложный фон. Чтобы кардинально улучшить распознавание, нужно комбинировать несколько признаков и использовать более robust методы. Я предлагаю трёхэтапный подход: предобработка для стабилизации цвета, выделение характерных геометрических признаков банки (прямоугольник с закруглёнными углами + белая полоса) и финальная верификация через отношение площадей.

Шаг 1: Улучшенная цветовая сегментация

Проблема бликов решается не просто расширением диапазона HSV, а адаптивной пороговой обработкой и работой в пространстве LAB. LAB лучше отделяет цвет от яркости. Красный цвет банки имеет стабильные координаты a* и b* даже при изменении освещения.

cv::Mat lab;
cv::cvtColor(img, lab, cv::COLOR_BGR2Lab);
// Извлекаем каналы a и b (красно-зеленый и желто-синий)
std::vector<cv::Mat> lab_channels;
cv::split(lab, lab_channels);
// Красный цвет в LAB: a* > 140, b* > 130 (примерные значения, нужно калибровать)
cv::Mat red_mask;
cv::inRange(lab_channels[1], cv::Scalar(140), cv::Scalar(255), red_mask);
cv::inRange(lab_channels[2], cv::Scalar(130), cv::Scalar(255), red_mask); // комбинируем
// Убираем шум морфологией
cv::morphologyEx(red_mask, red_mask, cv::MORPH_CLOSE, cv::getStructuringElement(cv::MORPH_ELLIPSE, cv::Size(5,5)));

Шаг 2: Геометрическая модель банки

Банка Coca-Cola — это не просто эллипс. Её профиль — прямоугольник с сильно скруглёнными углами, а на корпусе есть характерная белая волнистая лента. Мы можем искать пару "красный прямоугольник + белая полоса внутри".

// После нахождения контуров на red_mask
for (auto& c : contours) {
    double area = cv::contourArea(c);
    if (area < 1000) continue; // отсекаем мелкий мусор
    cv::RotatedRect rect = cv::minAreaRect(c);
    // Проверяем, что прямоугольник достаточно "прямоугольный" (отношение площади контура к площади описанного прямоугольника)
    float rect_area = rect.size.width * rect.size.height;
    float solidity = area / rect_area;
    if (solidity < 0.7) continue; // слишком изрезанный контур
    // Проверяем соотношение сторон (банка ~ 2:1)
    float aspect = rect.size.width / rect.size.height;
    if (aspect < 1.2 || aspect > 2.2) continue; // не похоже на банку
    // Теперь ищем белую полосу внутри этого прямоугольника
    cv::Mat roi = img(rect.boundingRect());
    cv::Mat white_mask;
    cv::inRange(roi, cv::Scalar(200, 200, 200), cv::Scalar(255, 255, 255), white_mask);
    int white_pixels = cv::countNonZero(white_mask);
    float white_ratio = (float)white_pixels / (rect_area);
    if (white_ratio > 0.05 && white_ratio < 0.3) {
        // Высокая вероятность, что это банка
        cv::drawContours(img, contours, i, cv::Scalar(0, 255, 0), 2);
    }
}

Шаг 3: Финальная верификация через HOG + SVM (опционально)

Если ложных срабатываний всё ещё много, используйте дескриптор HOG и обученный классификатор SVM. Это даст точность >95%. Вот минимальный пример загрузки предобученной модели:

cv::HOGDescriptor hog;
hog.setSVMDetector(cv::HOGDescriptor::getDefaultPeopleDetector()); // для примера, нужно обучить свой
std::vector<cv::Rect> found;
hog.detectMultiScale(roi, found, 0, cv::Size(8,8), cv::Size(32,32), 1.05, 2);
if (found.size() > 0) { /* банка подтверждена */ }

Советы по улучшению

  • Используйте калибровку цвета под ваше конкретное освещение. Сделайте 10-20 фото банки в разных условиях и соберите статистику по каналам LAB.
  • Добавьте проверку текстуры: банка имеет характерный металлический блеск (высокий градиент в одном направлении). Можно использовать фильтр Габора.
  • Не забывайте про масштабирование: если банка слишком маленькая или большая, отбрасывайте кандидатов по площади.
  • Для production используйте YOLO или MobileNet, обученный на вашем датасете. Это решит 99% проблем.

Ваш текущий код — хорошая отправная точка, но для реального применения нужно добавить хотя бы два-три дополнительных признака, как я показал выше.

<<>>

Альтернативный подход: использование SIFT и дескрипторов ключевых точек

Если вы не хотите возиться с геометрическими моделями и цветовой калибровкой, попробуйте метод на основе поиска характерных точек (feature matching). Банка Coca-Cola имеет уникальный логотип и шрифт, которые можно использовать как "отпечаток пальца".

cv::Ptr<cv::SIFT> sift = cv::SIFT::create();
std::vector<cv::KeyPoint> kp1, kp2;
cv::Mat desc1, desc2;
sift->detectAndCompute(reference_image, cv::noArray(), kp1, desc1);
sift->detectAndCompute(query_image, cv::noArray(), kp2, desc2);
cv::BFMatcher matcher(cv::NORM_L2);
std::vector<cv::DMatch> matches;
matcher.match(desc1, desc2, matches);
// Фильтруем хорошие совпадения (расстояние < 0.75 * max)
double max_dist = 0;
for (auto& m : matches) max_dist = std::max(max_dist, (double)m.distance);
std::vector<cv::DMatch> good_matches;
for (auto& m : matches) {
    if (m.distance < 0.75 * max_dist) good_matches.push_back(m);
}
if (good_matches.size() > 20) {
    std::cout << "Банка найдена!" << std::endl;
}

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