Pull to refresh

Пример сегментации изображений средствами PHP

Reading time 4 min
Views 11K
Добрый день,
довольно редко, но все же встает вопрос о необходимости в автоматическом режиме делить изображение на логические фрагменты. Если вы ограничены только средствами PHP, то задача становится немного трудней, но все же решаема.
В данной статье я рассмотрю частный случай распознавания образов, ориентированный на не слишком изощренную публику.
В статье используются примеры с одного из сайтов с явным указанием ссылки, сайт не мой, изначально не было мыслей писать статью.

Итак, рассмотрим исходные данные



image

Прямоугольные картинки разного размера и пропорций, с белым фоном, на которых изображены фигурки киндер сюрприза и water mark, который иногда касается фигурок. Задача — получить картинки фигурок без water mark'a

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

Интерация 1. Выбор не белых областей


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

image

PHP код
$path = '../images/series/9/1.jpg';
$size = getimagesize($path);
$img = imagecreatefromjpeg($path);

$x = 1;
$y = 1;
$color = array();
for ($i = 1; $i < $size[0] * $size[1]; $i++) {

    $color_index = imagecolorat($img, $x, $y); //16777215
// делаем его удобочитаемым
    $color_tran = imagecolorsforindex($img, $color_index);

    if (($color_tran['red'] == 0 && $color_tran['green'] == 0 && $color_tran['blue'] == 0)
            or
            ($color_tran['red'] <= 255 && $color_tran['red'] >= 235 &&
            $color_tran['green'] <= 255 && $color_tran['green'] >= 235 &&
            $color_tran['blue'] <= 255 && $color_tran['blue'] >= 235)
            or
            ($color_tran['red'] >= 190 && $color_tran['red'] <= 230 &&
            $color_tran['green'] >= 225 && $color_tran['green'] <= 245 &&
            $color_tran['blue'] >= 245 && $color_tran['blue'] <= 255)
    ) {
          $color[$x][$y] = 0;
    } else {
        $color[$x][$y] = 1;
    }

    $x++;
    if ($x > $size[0]) {
        $x = 1;
        $y++;
    }
}


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

Закрасим теперь полученные области на исходном изображении.

image

Как видно сейчас наш алгоритм цепляет кусочки мусора (поэтому в вышеприведенном коде и есть фильтрация по цвету)

Ошибочный подход, попытка выделения граничных областей сегментов


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

image

Шлифуем

image

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

image

Теперь если мы закрасим все цветные точки внутри данных ограничений, то получим в результате картинку, на которой уже можно понять, что это смурфы)

image

Со стороны человеческого взгляда эти сегменты уже можно однозначно разделить, но со стороны нашего алгоритма это все еще массив единичек и ноликов, а так же массив ноликов и единиц – только границ сегментов (но сейчас это все еще 1 массив и основной вопрос в том, как разделить его в соответствии с сегментами).
На исходном изображении можно видеть явные белые пробелы между сегментами, попробуем разделить сегменты по принципу: «Если у нас есть большие разрывы (больше чем N) между закрашенными сегментами, то делим пиксели в основном массиве по левую и правую сторону от разрыва».

image

image

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

Метод разрастания областей


Краткое описание дано выше (в начале статьи), поэтому приступим сразу к делу.

image

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

image

PHP код
function nburn($ret, $color, $img, $col = false) {
    $ret_arr = array();
    foreach ($ret as $k => $v) {
        foreach ($v as $k2 => $v2) {
            $ret_arr['numb'] = get_nearleast($color, $k, $k2);
            $stop = false;
            if (count($ret_arr['numb']) > 0) {

                if (count($ret_arr['numb']) == 1) // 1- потому что только точка сама возвращается
                    $stop = true;
                else
                    $toys[] = array();
            }
            if (!$stop) {
                while (count($ret_arr['numb']) > 0) {
                    $x = key($ret_arr['numb']);
                    $y = key($ret_arr['numb'][$x]);
                    $toys[key($toys)][$x][$y] = 1;

                    unset($ret_arr['numb'][$x][$y]);
                    if (count($ret_arr['numb'][$x]) == 0)
                        unset($ret_arr['numb'][$x]);
                    unset($color[$x][$y]);


                    // Берем следующий элемент
                    $x = key($ret_arr['numb']);
                    if ($x == "")
                        break;
                    $y = key($ret_arr['numb'][$x]);
                    $near = get_nearleast($color, $x, $y);
                    foreach ($near as $f => $g) {
                        foreach ($g as $f2 => $g2) {

                            $ret_arr['numb'][$f][$f2] = 1;
                        }
                    }
                }
                next($toys);
            }
        }
    }

    return $toys;
}


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

image

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

image

Размещенный код писался для себя, поэтому, скорее всего, есть более элегантные решения.
Ссылки по теме:


UPD
Спасибо всем, кто плюсует.
Tags:
Hubs:
+35
Comments 3
Comments Comments 3

Articles