[Из песочницы] Автоматический поворот изображения номера для систем LPR

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

4668e859697a4eb1b1e565b643c95402.jpg

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


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

public Bitmap SomeMethod(Image img)
        {
            using (Image gray = img.Convert())
            using (Image sobel = new Image(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 --> Image
            }
            return null;
         }


56647117489645cca5dcb28ac8026ac7.jpg

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


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

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 img)
        {
            LineSegment2D[] lines = null;
            using (Image gray = img.Convert())
            using (Image sobel = new Image(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 --> Image
                lines = gray.HoughLinesBinary(1, Math.PI / 45, 50, img.Width / 2, 0)[0];
            }
            return null;
         }


b5bb7e6542f54763881eaa3c6617e63d.jpg

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


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

                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));


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

722a3062dd174068bdcc2938f93a4b31.jpg

Где 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 img)
        {
            LineSegment2D[] lines = null;
            using (Image gray = img.Convert())
            using (Image sobel = new Image(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 --> Image
                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();
        }


956fd773910c459c8be8e643f84eb6d3.jpg

© Habrahabr.ru