Решил поделиться простым, но эффективным способом поворота изображения регистрационного номера автомобиля. Для реализации идеи использую кроссплатформенную обертку .NET над OpenCV — EMGU.



Постобработка


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

public Bitmap SomeMethod(Image<Bgr, byte> img)
        {
            using (Image<Gray, byte> gray = img.Convert<Gray, Byte>())
            using (Image<Gray, float> sobel = new Image<Gray, float>(img.Size))
            {
                CvInvoke.cvSmooth(gray, gray, Emgu.CV.CvEnum.SMOOTH_TYPE.CV_GAUSSIAN, 5, 5, 25, 25);
                CvInvoke.cvSobel(gray, sobel, 0, 1, 3); 
                CvInvoke.cvConvert(sobel, gray); // Image<Gray, float> --> Image<Gray, Byte>
            }
            return null;
         }



Выделение признаков


Признаками для данной задачи являются прямые линии, при этом они должны быть больше, чем половина ширины изображения. Для выделения линии используем метод:
public LineSegment2D[][] HoughLinesBinary(
	double rhoResolution,
	double thetaResolution,
	int threshold,
	double minLineWidth,
	double gapBetweenLines
)

Первые два аргумента rhoResolution и thetaResolution устанавливают желаемое разрешение для линии в бинарном изображении. Линии можно рассматривать как 2D гистограммы с пересечением и углом наклона, таким образом rhoResolution назначается в пикселях, а thetaResolution в радианах. Threshold является минимальным количеством пикселей в отрезках. Когда счетчик пикселей больше чем порог, отрезок записывается в список найденных. Следующие два параметра minLineWidth и gapBetweenLines, являются минимальной длинной отрезка и разрывом между линиями, назначаются в пикселях.

Таким образом для получения признаков используем следующий код:
public Bitmap SomeMethod(Image<Bgr, byte> img)
        {
            LineSegment2D[] lines = null;
            using (Image<Gray, byte> gray = img.Convert<Gray, Byte>())
            using (Image<Gray, float> sobel = new Image<Gray, float>(img.Size))
            {
                CvInvoke.cvSmooth(gray, gray, Emgu.CV.CvEnum.SMOOTH_TYPE.CV_GAUSSIAN, 5, 5, 20, 20);
                CvInvoke.cvSobel(gray, sobel, 0, 1, 3);
                CvInvoke.cvConvert(sobel, gray); // Image<Gray, float> --> Image<Gray, Byte>
                lines = gray.HoughLinesBinary(1, Math.PI / 45, 50, img.Width / 2, 0)[0];
            }
            return null;
         }



Вычисляем угол наклона


Для того чтобы вычислить угол наклона сформируем среднее значение для точек полученных линий (так как это дешевле, чем просчет угла для каждого):
                LineSegment2D avr = new LineSegment2D();
                foreach (LineSegment2D seg in lines)
                {
                    avr.P1 = new Point(avr.P1.X + seg.P1.X, avr.P1.Y + seg.P1.Y);
                    avr.P2 = new Point(avr.P2.X + seg.P2.X, avr.P2.Y + seg.P2.Y);
                }
                avr.P1 = new Point(avr.P1.X / lines.Length, avr.P1.Y / lines.Length);
                avr.P2 = new Point(avr.P2.X / lines.Length, avr.P2.Y / lines.Length);

И затем достроим угол с помощью горизонтальной линии:
                LineSegment2D horizontal = new LineSegment2D(avr.P1, new Point(avr.P2.X, avr.P1.Y));

Получили результирующий угол:



Где C (horizontal), A — катеты, B (avr) — гипотенуза.
Для вычисления сторон треугольник и угла CB воспользуемся школьными формулами:
                double c = horizontal.P2.X - horizontal.P1.X;
                double a = Math.Abs(horizontal.P2.Y - avr.P2.Y);
                double b = Math.Sqrt(c * c + a * a);
                angle = (a / b * (180 / Math.PI)) * (horizontal.P2.Y > avr.P2.Y ? 1 : -1);

После чего просто применяем метод Rotate для изображения с полученным углом.
public Bitmap SomeMethod(Image<Bgr, byte> img)
        {
            LineSegment2D[] lines = null;
            using (Image<Gray, byte> gray = img.Convert<Gray, Byte>())
            using (Image<Gray, float> sobel = new Image<Gray, float>(img.Size))
            {
                CvInvoke.cvSmooth(gray, gray, Emgu.CV.CvEnum.SMOOTH_TYPE.CV_GAUSSIAN, 5, 5, 20, 20);
                CvInvoke.cvSobel(gray, sobel, 0, 1, 3);
                CvInvoke.cvConvert(sobel, gray); // Image<Gray, float> --> Image<Gray, Byte>
                lines = gray.HoughLinesBinary(1, Math.PI / 45, 50, img.Width / 2, 0)[0];
            }

            if (lines != null && lines.Length > 0)
            {
                double angle = 0;
                LineSegment2D avr = new LineSegment2D();
                foreach (LineSegment2D seg in lines)
                {
                    avr.P1 = new Point(avr.P1.X + seg.P1.X, avr.P1.Y + seg.P1.Y);
                    avr.P2 = new Point(avr.P2.X + seg.P2.X, avr.P2.Y + seg.P2.Y);
                    img.Draw(seg, new Bgr(255, 0, 0), 1);
                }
                avr.P1 = new Point(avr.P1.X / lines.Length, avr.P1.Y / lines.Length);
                avr.P2 = new Point(avr.P2.X / lines.Length, avr.P2.Y / lines.Length);
                LineSegment2D horizontal = new LineSegment2D(avr.P1, new Point(avr.P2.X, avr.P1.Y));

                img.Draw(new LineSegment2D(avr.P1, new Point(avr.P2.X, avr.P1.Y)), new Bgr(0, 255, 0), 2);
                img.Draw(avr, new Bgr(0, 255, 0), 2);

                double c = horizontal.P2.X - horizontal.P1.X;
                double a = Math.Abs(horizontal.P2.Y - avr.P2.Y);
                double b = Math.Sqrt(c * c + a * a);
                angle = (a / b * (180 / Math.PI)) * (horizontal.P2.Y > avr.P2.Y ? 1 : -1);
                img = img.Rotate(angle, new Bgr(0, 0, 0));
            }
            return img.ToBitmap();
        }

Комментарии (17)


  1. samodum
    22.07.2015 16:38
    +3

    А как вы находите на изображении области с номерами? Эта задача посложнее поворота, и было бы интересно узнать ваше решение.


    1. cyberspace Автор
      22.07.2015 16:50
      +2

      Я делал это с помощью проекций. К изображению просто применялся оператор Собеля с некоторыми фильтрами, и дальше обрабатываем проекцию. Этот принцип описан тут. Не знаю, имеет ли смысл делать по этому поводу статью=)


      1. samodum
        22.07.2015 17:06

        Указанный метод работает, если на фото несколько автомобильных номеров? Все найдёт? Какова погрешность нахождения? Достаточна ли скорость для поиска в режиме реального времени и в движении?


        1. cyberspace Автор
          22.07.2015 17:22

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


          1. samodum
            22.07.2015 18:28

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


            1. cyberspace Автор
              22.07.2015 18:29

              Если сама камера в движении, то каскад Хаара лучше решение, я считаю.


              1. samodum
                23.07.2015 12:35

                Я тоже так думаю, но, возможно, что есть другие методы.


                1. netmaxed
                  23.07.2015 13:10

                  а что насчет HOG?
                  dlib лица с помощью хогов выделяет на порядок лучше чем OpenCV c каскадом Хаара, например.


  1. ZlodeiBaal
    22.07.2015 17:46
    +2

    Вот тут мы выкладывали крупную базу с номерами — habrahabr.ru/company/recognitor/blog/243919
    Попробуйте этим методом. К сожалению, где-то каждый 15-20 номер будет обработан некорректно. Прямая криво выделиться, прямая частично не видна, и.т.д. и.т.п.
    Вот тут вот мы писали про дроугой метод — habrahabr.ru/company/recognitor/blog/221891
    Он работает чуть лучше, но всё равно неидеально.
    Лучше всего у нас заработал следующий метод: перебор углов и для каждого угла построение гистограммы интенсивности по y. Там где будет максимальный градиент — был корректный поворот. Проилюстрирую:
    image
    Сразу тут же отвечу на прошлый вопрос по поводу поиска номера.
    Самый простой вариант — использовать каскад Хаара ( habrahabr.ru/company/recognitor/blog/223441 ). В принципе, наш каскад для российских номеров замерджен в текущую версию OpenCV. Так что качаете OpenCV и распознаёте номера в кадре.
    Метод, конечно, не идеальный, но из тех, что просто реализовать и запустить — наилучший.
    В реальный системах используется очень много различных детекторов и критериев. Многие из которых опираются на подсветку.
    Сделать выделение на произвольных картинках лучше чем каскадом Хаара мы не смогли никак.


    1. cyberspace Автор
      22.07.2015 18:13

      А как насчет идеи о выделении машины в отдельное изображение? При условии если это, конечно, непроизвольное изображение, а снятое со стационарной камеры. Хочу реализовать эту идею, все же интересно Ваше мнение, как более опытного человека.


      1. ZlodeiBaal
        22.07.2015 18:19

        Со стационарной камеры всё проще — можно изначально определить углы и размеры. Если на такой камере есть подсветка — то лучше всего искать номер, его будет хорошо видно.
        Выделять машину — это можно, но работать будет менее точно чем выделение номера напрямую. Машины разные, номер — одинаковый.
        Обучить хаара проще чем нейросеть.

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


        1. cyberspace Автор
          22.07.2015 18:28

          Я задумывал с самого начала, грубо говоря — копить базу найденных номеров и каждый месяц переобучать каскад Хаара на более большом количестве примеров. Но показалась слишком простой идея. Что же, буду экспериментировать дальше, спасибо=)


          1. ZlodeiBaal
            22.07.2015 18:35

            А чем не понравилась та база, что у нас выложена?:)
            Если её руками разметить — можно неплохо каскад обучить.
            Наш каскад выложенный где-то 90%-95% номеров выделяет первично. Он не по всей базе обучен, если по всей — можно ещё пару процентов набрать. Для промышленной задачи, естественно такого не достаточно, но для многих применений достаточно.
            Но для промышленной, боюсь, либо какую-то хитрою нейронную сеть свёрточную обучать, либо что-то большое многоступенчатое городить.


            1. cyberspace Автор
              22.07.2015 18:44

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

              HaarCascade haar = new HaarCascade("haarcascade_russian_plate_number.xml");
              Просто крашится на этой строчке. Естественно, xml — документ размешен в той же директории, что и исполняемый файл, да и прямой путь ситуацию не поправляет.


              1. ZlodeiBaal
                22.07.2015 19:14

                У вас старый Emgu? У меня 2.9.0. (это была какая-то бэта версия годичной давности).
                Просто в нём вызов каскада осуществляется как:

                CascadeClassifier PL = new CascadeClassifier(«haarcascade_russian_plate_number.xml»)


                И потом:

                Rectangle[] PlateDetected = PL.DetectMultiScale(
                gray,
                1.1,
                3,
                new Size(160, 60),
                new Size(900, 250));


                В текущем OpenCV каскад подцепляется.

                В принципе, где-то 2 года назад OpenCV менял алгоритм формирования каскада. Может «HaarCascade» оттуда остался для старого формата.


                1. cyberspace Автор
                  22.07.2015 19:54

                  Огромное спасибо=) Да, действительно, проблема была в версии.

                  using (Image<Gray, byte> gray = img.Convert<Gray, Byte>())
                  plateDetected = haar.DetectMultiScale(gray, 1.1, 3, Size.Empty, Size.Empty);

                  Достаточно эффективная вещь, небольшая эвристика и алгоритм вполне годен=)


    1. ZlodeiBaal
      22.07.2015 18:13
      +1

      О, нашёл. Ещё есть такой мощный алгоритм — habrahabr.ru/post/245497
      Но его не тестили особо.