Алгоритм обнаружения углов бумажного листа на фотографии

Каков наилучший способ определения углов счета-фактуры/квитанции/листа бумаги на фотографии? Это должно использоваться для последующей коррекции перспективы до OCR.

Мой текущий подход:

RGB > Серый > Обнаружение Canny Edge с пороговым значением > Dilate (1) > Удалить мелкие объекты (6) > clear boarder objects > выбрать крупный блог на основе Convex Area. > [обнаружение угла - не реализовано]

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

Более широкий контекст:

Я использую matlab для прототипа и планирую реализовать систему в OpenCV и Tesserect-OCR. Это первый из многих проблем обработки изображений, которые мне нужно решить для этого конкретного приложения. Поэтому я ищу, чтобы сверлить собственное решение и переосмыслить себя алгоритмами обработки изображений.

Вот пример изображения, с которым мне бы хотелось обработать алгоритм: если вы хотите, чтобы большие изображения находились на http://madteckhead.com/tmp

case 1 http://madteckhead.com/tmp/IMG_0773_sml.jpg case 2 http://madteckhead.com/tmp/IMG_0774_sml.jpg case 3 http://madteckhead.com/tmp/IMG_0775_sml.jpg case 4 http://madteckhead.com/tmp/IMG_0776_sml.jpg

В лучшем случае это дает:

case 1 - canny http://madteckhead.com/tmp/IMG_0773_canny.jpg case 1 - post canny http://madteckhead.com/tmp/IMG_0773_postcanny.jpg case 1 - крупнейший блог http://madteckhead.com/tmp/IMG_0773_blob.jpg

Однако в других случаях он легко справляется:

case 2 - canny http://madteckhead.com/tmp/IMG_0774_canny.jpg case 2 - post canny http://madteckhead.com/tmp/IMG_0774_postcanny.jpg case 2 - крупнейший блог http://madteckhead.com/tmp/IMG_0774_blob.jpg

Спасибо за все замечательные идеи! Я люблю SO!

РЕДАКТИРОВАТЬ: Ход трансформации прогресса

Q: Какой алгоритм будет класть строки hough, чтобы найти углы? Следуя советам ответов, я смог использовать Преобразование Хафа, выбрать линии и отфильтровать их. Мой нынешний подход довольно груб. Я сделал предположение, что счет-фактура всегда будет меньше, чем 15deg из выравнивания с изображением. В итоге я получаю разумные результаты для строк, если это так (см. Ниже). Но я не совсем уверен в подходящем алгоритме, чтобы скопировать строки (или проголосовать), чтобы экстраполировать их по углам. Линии Hough не являются непрерывными. И в шумовых изображениях могут быть параллельные линии, поэтому требуется некоторая форма или расстояние от метрик начала линии. Любые идеи?

case 1 http://madteckhead.com/tmp/IMG_0773_hough.jpg case 2 http://madteckhead.com/tmp/IMG_0774_hough.jpg case 3 http://madteckhead.com/tmp/IMG_0775_hough.jpg case 4 http://madteckhead.com/tmp/IMG_0776_hough.jpg

Ответ 1

Я друг Мартина, который работал над этим в начале этого года. Это был мой первый проект по кодированию, и вроде бы закончилось немного поспешным, поэтому код нуждается в некотором декодировании... Я дам несколько советов из того, что я видел, что вы делаете уже, а затем соберите мой код завтра в выходной день.

Первый совет, OpenCV и python являются удивительными, перейдите к ним как можно скорее.: D

Вместо того, чтобы удалять мелкие объекты и/или шум, опустите канительные ограничения, поэтому он принимает больше ребер, а затем найдет самый большой замкнутый контур (в OpenCV используйте findcontour() с некоторыми простыми параметрами, я думаю, что использовал CV_RETR_LIST), может по-прежнему бороться, когда он на белом листе бумаги, но определенно обеспечивает наилучшие результаты.

Для Houghline2() Transform, попробуйте с CV_HOUGH_STANDARD в отличие от CV_HOUGH_PROBABILISTIC, он даст значения rho и theta, определяя линию в полярных координатах, а затем вы можете группировать строки в пределах определенного толерантность к ним.

Моя группировка работала как таблица поиска, для каждой строки, полученной из преобразования hough, она давала бы пару rho и theta. Если эти значения находились внутри, скажем, 5% пары значений в таблице, они были отброшены, если они были вне этого 5%, в таблицу была добавлена ​​новая запись.

Вы можете сделать анализ параллельных линий или расстояние между линиями намного легче.

Надеюсь, что это поможет.

Ответ 2

Студенческая группа в моем университете недавно продемонстрировала приложение для iPhone (и приложение OpenCV для python), которое они написали, чтобы сделать именно это. Как я помню, шаги были примерно такими:

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

Это выглядело довольно неплохо, и они смогли сделать снимок листа или книги, выполнить обнаружение углов, а затем отобразить документ на изображении на плоскую плоскость почти в реальном времени (был один OpenCV функция для выполнения отображения). Не было OCR, когда я увидел, что он работает.

Ответ 3

Вот что я придумал после нескольких экспериментов:

import cv, cv2, numpy as np
import sys

def get_new(old):
    new = np.ones(old.shape, np.uint8)
    cv2.bitwise_not(new,new)
    return new

if __name__ == '__main__':
    orig = cv2.imread(sys.argv[1])

    # these constants are carefully picked
    MORPH = 9
    CANNY = 84
    HOUGH = 25

    img = cv2.cvtColor(orig, cv2.COLOR_BGR2GRAY)
    cv2.GaussianBlur(img, (3,3), 0, img)


    # this is to recognize white on white
    kernel = cv2.getStructuringElement(cv2.MORPH_RECT,(MORPH,MORPH))
    dilated = cv2.dilate(img, kernel)

    edges = cv2.Canny(dilated, 0, CANNY, apertureSize=3)

    lines = cv2.HoughLinesP(edges, 1,  3.14/180, HOUGH)
    for line in lines[0]:
         cv2.line(edges, (line[0], line[1]), (line[2], line[3]),
                         (255,0,0), 2, 8)

    # finding contours
    contours, _ = cv2.findContours(edges.copy(), cv.CV_RETR_EXTERNAL,
                                   cv.CV_CHAIN_APPROX_TC89_KCOS)
    contours = filter(lambda cont: cv2.arcLength(cont, False) > 100, contours)
    contours = filter(lambda cont: cv2.contourArea(cont) > 10000, contours)

    # simplify contours down to polygons
    rects = []
    for cont in contours:
        rect = cv2.approxPolyDP(cont, 40, True).copy().reshape(-1, 2)
        rects.append(rect)

    # that basically it
    cv2.drawContours(orig, rects,-1,(0,255,0),1)

    # show only contours
    new = get_new(img)
    cv2.drawContours(new, rects,-1,(0,255,0),1)
    cv2.GaussianBlur(new, (9,9), 0, new)
    new = cv2.Canny(new, 0, CANNY, apertureSize=3)

    cv2.namedWindow('result', cv2.WINDOW_NORMAL)
    cv2.imshow('result', orig)
    cv2.waitKey(0)
    cv2.imshow('result', dilated)
    cv2.waitKey(0)
    cv2.imshow('result', edges)
    cv2.waitKey(0)
    cv2.imshow('result', new)
    cv2.waitKey(0)

    cv2.destroyAllWindows()

Не совершенен, но по крайней мере работает для всех образцов:

1234

Ответ 4

Вместо начала обнаружения края вы можете использовать обнаружение углов.

Marvin Framework обеспечивает реализацию алгоритма Moravec для этой цели. Вы можете найти углы газет в качестве отправной точки. Ниже выхода алгоритма Moravec:

enter image description here

Ответ 5

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

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

result

Ответ 6

После обнаружения края используйте Hough Transform. Затем поместите эти точки в SVM (поддерживающий векторный механизм) с их метками, если в примерах есть гладкие линии на них, SVM не будет иметь никаких трудностей для разделения необходимых частей примера и других частей. Мой совет по SVM, поместите параметр, такой как связь и длина. То есть, если точки связаны и длинны, они, вероятно, будут линией квитанции. Затем вы можете устранить все остальные моменты.

Ответ 7

Здесь у вас есть код @Vanuan с использованием С++:

cv::cvtColor(mat, mat, CV_BGR2GRAY);
cv::GaussianBlur(mat, mat, cv::Size(3,3), 0);
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Point(9,9));
cv::Mat dilated;
cv::dilate(mat, dilated, kernel);

cv::Mat edges;
cv::Canny(dilated, edges, 84, 3);

std::vector<cv::Vec4i> lines;
lines.clear();
cv::HoughLinesP(edges, lines, 1, CV_PI/180, 25);
std::vector<cv::Vec4i>::iterator it = lines.begin();
for(; it!=lines.end(); ++it) {
    cv::Vec4i l = *it;
    cv::line(edges, cv::Point(l[0], l[1]), cv::Point(l[2], l[3]), cv::Scalar(255,0,0), 2, 8);
}
std::vector< std::vector<cv::Point> > contours;
cv::findContours(edges, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_TC89_KCOS);
std::vector< std::vector<cv::Point> > contoursCleaned;
for (int i=0; i < contours.size(); i++) {
    if (cv::arcLength(contours[i], false) > 100)
        contoursCleaned.push_back(contours[i]);
}
std::vector<std::vector<cv::Point> > contoursArea;

for (int i=0; i < contoursCleaned.size(); i++) {
    if (cv::contourArea(contoursCleaned[i]) > 10000){
        contoursArea.push_back(contoursCleaned[i]);
    }
}
std::vector<std::vector<cv::Point> > contoursDraw (contoursCleaned.size());
for (int i=0; i < contoursArea.size(); i++){
    cv::approxPolyDP(Mat(contoursArea[i]), contoursDraw[i], 40, true);
}
Mat drawing = Mat::zeros( mat.size(), CV_8UC3 );
cv::drawContours(drawing, contoursDraw, -1, cv::Scalar(0,255,0),1);

Ответ 8

  • Преобразование в лабораторное пространство

  • Использовать кластер сегментов 2 kmeans

  • Затем используйте контуры или hough на одном из кластеров (intenral)

Ответ 9

Найдите версию Java, используя opencv ниже

package testOpenCV;
    import org.opencv.core.Core;
    import org.opencv.core.CvType;
    import org.opencv.core.Mat;
    import java.awt.BorderLayout;
    import java.awt.Container;
    import java.awt.Image;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Random;
    import javax.swing.BoxLayout;
    import javax.swing.ImageIcon;
    import javax.swing.JFrame;
    import javax.swing.JLabel;
    import javax.swing.JPanel;
    import javax.swing.JSlider;
    import javax.swing.event.ChangeEvent;
    import javax.swing.event.ChangeListener;
    import org.opencv.core.MatOfPoint;
    import org.opencv.core.Point;
    import org.opencv.core.Scalar;
    import org.opencv.core.Size;
    import org.opencv.highgui.HighGui;
    import org.opencv.imgcodecs.Imgcodecs;
    import org.opencv.imgproc.Imgproc;

    class FindContours {
        private Mat srcGray = new Mat();
        private Mat src_orig_ref = new Mat();
        private Mat srcGray_after = new Mat();
        private JFrame frame;
        private JLabel imgSrcLabel;
        private JLabel imgContoursLabel;
        private static final int MAX_THRESHOLD = 255;
        private int threshold = 40;
        private Random rng = new Random(12345);
        private Mat src = new Mat();
        public FindContours(String[] args) {
            //OpenCVNativeLoader
            String filename =  "C:\\Desktop\\opencv\\4.PNG";
            src = Imgcodecs.imread(filename);
            src_orig_ref = Imgcodecs.imread(filename);
            if (src.empty()) {
                System.err.println("Cannot read image: " + filename);
                System.exit(0);
            }
            Imgproc.cvtColor(src, srcGray, Imgproc.COLOR_BGR2GRAY);
            Imgproc.blur(srcGray, srcGray, new Size(3, 3));
            Mat kernel = new Mat(new Size(3, 3), CvType.CV_8UC1, new Scalar(255));
            Imgproc.morphologyEx(srcGray, srcGray, Imgproc.MORPH_OPEN, kernel);
            Imgproc.morphologyEx(srcGray, srcGray, Imgproc.MORPH_CLOSE, kernel);
            // Create and set up the window.
            frame = new JFrame("Finding contours in your image demo");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            // Set up the content pane.
            Image img = HighGui.toBufferedImage(src);
            addComponentsToPane(frame.getContentPane(), img);
            // Use the content pane default BorderLayout. No need for
            // setLayout(new BorderLayout());
            // Display the window.
            frame.pack();
            frame.setVisible(true);
            update();
        }
        private void addComponentsToPane(Container pane, Image img) {
            if (!(pane.getLayout() instanceof BorderLayout)) {
                pane.add(new JLabel("Container doesn't use BorderLayout!"));
                return;
            }
            JPanel sliderPanel = new JPanel();
            sliderPanel.setLayout(new BoxLayout(sliderPanel, BoxLayout.PAGE_AXIS));
            sliderPanel.add(new JLabel("Canny threshold: "));
            JSlider slider = new JSlider(0, MAX_THRESHOLD, threshold);
            slider.setMajorTickSpacing(20);
            slider.setMinorTickSpacing(10);
            slider.setPaintTicks(true);
            slider.setPaintLabels(true);
            slider.addChangeListener(new ChangeListener() {
                @Override
                public void stateChanged(ChangeEvent e) {
                    JSlider source = (JSlider) e.getSource();
                    threshold = source.getValue();
                    update();
                }
            });
            sliderPanel.add(slider);
            pane.add(sliderPanel, BorderLayout.PAGE_START);
            JPanel imgPanel = new JPanel();
            imgSrcLabel = new JLabel(new ImageIcon(img));
            imgPanel.add(imgSrcLabel);
            Mat blackImg = Mat.zeros(srcGray.size(), CvType.CV_8U);
            imgContoursLabel = new JLabel(new ImageIcon(HighGui.toBufferedImage(blackImg)));
            imgPanel.add(imgContoursLabel);
            pane.add(imgPanel, BorderLayout.CENTER);
        }
        private void update() {
            Mat cannyOutput = new Mat();
            Imgproc.Canny(srcGray, cannyOutput, threshold, threshold * 2);
            List<MatOfPoint> contours = new ArrayList<MatOfPoint>();
            Mat hierarchy = new Mat();

            Imgproc.findContours(cannyOutput, contours, hierarchy, Imgproc.RETR_LIST, Imgproc.CHAIN_APPROX_SIMPLE);
            Mat drawing = Mat.zeros(cannyOutput.size(), CvType.CV_8UC3);
            for (int i = 0; i < contours.size(); i++) {
                //rng.nextInt(256)
                Scalar color = new Scalar(256, 256, 256);
                List<MatOfPoint> contours_ele = new ArrayList<MatOfPoint>();
                contours_ele.add(contours.get(i));
                if(Imgproc.contourArea(contours.get(i))>10) {
                Imgproc.drawContours(src, contours_ele , -1, color, 2, Imgproc.LINE_8, hierarchy, 0, new Point());
                }
            }

            Mat cannyOutput_after = new Mat();
            Imgproc.cvtColor(src, srcGray_after, Imgproc.COLOR_BGR2GRAY);
            Imgproc.blur(srcGray_after, srcGray_after, new Size(3, 3));
            Mat kernel = new Mat(new Size(3, 3), CvType.CV_8UC1, new Scalar(255));
            Imgproc.morphologyEx(srcGray_after, srcGray_after, Imgproc.MORPH_OPEN, kernel);
            Imgproc.morphologyEx(srcGray_after, srcGray_after, Imgproc.MORPH_CLOSE, kernel);
            Imgproc.Canny(srcGray_after, cannyOutput_after, threshold, threshold * 2);


            List<MatOfPoint> contours_dw = new ArrayList<MatOfPoint>();
            Mat hierarchy_dw = new Mat();
            Mat drawing_dw = Mat.zeros(cannyOutput.size(), CvType.CV_8UC3);
            Imgproc.findContours(cannyOutput_after, contours_dw, hierarchy_dw, Imgproc.RETR_TREE, Imgproc.CHAIN_APPROX_SIMPLE);
            for (int i = 0; i < contours_dw.size(); i++) {
                Scalar color = new Scalar(rng.nextInt(256), rng.nextInt(256), rng.nextInt(256));
                if(Imgproc.contourArea(contours_dw.get(i))>100000) {
                Imgproc.drawContours(src_orig_ref, contours_dw, i, color, 2, Imgproc.LINE_8, hierarchy, 0, new Point());
                }
            }


            imgContoursLabel.setIcon(new ImageIcon(HighGui.toBufferedImage(src_orig_ref)));
            frame.repaint();
        }
    }


    public class hello
    {
       public static void main( String[] args )
       {
          System.loadLibrary( Core.NATIVE_LIBRARY_NAME );
          Mat mat = Mat.eye( 3, 3, CvType.CV_8UC1 );
          System.out.println( "mat = " + mat.dump() );
          javax.swing.SwingUtilities.invokeLater(new Runnable() {
              @Override
              public void run() {
                  new FindContours(null);
              }
          });
       }
    }