OpenCV C ++ / Obj-C: Phát hiện một tờ giấy / Phát hiện vuông


178

Tôi đã triển khai thành công ví dụ phát hiện vuông OpenCV trong ứng dụng thử nghiệm của mình, nhưng bây giờ cần lọc đầu ra, vì nó khá lộn xộn - hoặc mã của tôi sai?

Tôi đang quan tâm đến bốn điểm góc của giấy để giảm nghiêng (như rằng ) và tiếp tục xử lý ...

Đầu ra đầu vào: Đầu ra đầu vào

Ảnh gốc:

nhấp chuột

Mã số:

double angle( cv::Point pt1, cv::Point pt2, cv::Point pt0 ) {
    double dx1 = pt1.x - pt0.x;
    double dy1 = pt1.y - pt0.y;
    double dx2 = pt2.x - pt0.x;
    double dy2 = pt2.y - pt0.y;
    return (dx1*dx2 + dy1*dy2)/sqrt((dx1*dx1 + dy1*dy1)*(dx2*dx2 + dy2*dy2) + 1e-10);
}

- (std::vector<std::vector<cv::Point> >)findSquaresInImage:(cv::Mat)_image
{
    std::vector<std::vector<cv::Point> > squares;
    cv::Mat pyr, timg, gray0(_image.size(), CV_8U), gray;
    int thresh = 50, N = 11;
    cv::pyrDown(_image, pyr, cv::Size(_image.cols/2, _image.rows/2));
    cv::pyrUp(pyr, timg, _image.size());
    std::vector<std::vector<cv::Point> > contours;
    for( int c = 0; c < 3; c++ ) {
        int ch[] = {c, 0};
        mixChannels(&timg, 1, &gray0, 1, ch, 1);
        for( int l = 0; l < N; l++ ) {
            if( l == 0 ) {
                cv::Canny(gray0, gray, 0, thresh, 5);
                cv::dilate(gray, gray, cv::Mat(), cv::Point(-1,-1));
            }
            else {
                gray = gray0 >= (l+1)*255/N;
            }
            cv::findContours(gray, contours, CV_RETR_LIST, CV_CHAIN_APPROX_SIMPLE);
            std::vector<cv::Point> approx;
            for( size_t i = 0; i < contours.size(); i++ )
            {
                cv::approxPolyDP(cv::Mat(contours[i]), approx, arcLength(cv::Mat(contours[i]), true)*0.02, true);
                if( approx.size() == 4 && fabs(contourArea(cv::Mat(approx))) > 1000 && cv::isContourConvex(cv::Mat(approx))) {
                    double maxCosine = 0;

                    for( int j = 2; j < 5; j++ )
                    {
                        double cosine = fabs(angle(approx[j%4], approx[j-2], approx[j-1]));
                        maxCosine = MAX(maxCosine, cosine);
                    }

                    if( maxCosine < 0.3 ) {
                        squares.push_back(approx);
                    }
                }
            }
        }
    }
    return squares;
}

EDIT 17/08/2012:

Để vẽ các ô vuông được phát hiện trên hình ảnh, hãy sử dụng mã này:

cv::Mat debugSquares( std::vector<std::vector<cv::Point> > squares, cv::Mat image )
{
    for ( int i = 0; i< squares.size(); i++ ) {
        // draw contour
        cv::drawContours(image, squares, i, cv::Scalar(255,0,0), 1, 8, std::vector<cv::Vec4i>(), 0, cv::Point());

        // draw bounding rect
        cv::Rect rect = boundingRect(cv::Mat(squares[i]));
        cv::rectangle(image, rect.tl(), rect.br(), cv::Scalar(0,255,0), 2, 8, 0);

        // draw rotated rect
        cv::RotatedRect minRect = minAreaRect(cv::Mat(squares[i]));
        cv::Point2f rect_points[4];
        minRect.points( rect_points );
        for ( int j = 0; j < 4; j++ ) {
            cv::line( image, rect_points[j], rect_points[(j+1)%4], cv::Scalar(0,0,255), 1, 8 ); // blue
        }
    }

    return image;
}


1
Tôi nghĩ bạn có thể điều chỉnh tiêu đề của câu hỏi cho một cái gì đó như Phát hiện một tờ giấy , nếu bạn nghĩ nó phù hợp hơn.
karlphillip

1
@moosgummi Tôi đang tìm kiếm để có cùng chức năng mà bạn đã triển khai, tức là "Phát hiện các góc của hình ảnh / tài liệu đã chụp". Làm thế nào bạn đạt được điều này? Tôi có thể sử dụng OpenCV trong ứng dụng iPhone của mình không? Xin gợi ý cho tôi một số cách tốt hơn để có thứ này ..
Ajay Sharma

1
Bạn đã bao giờ làm điều gì đó với OpenCV chưa? Có ứng dụng nào không?
karlphillip

6
Điều đáng chú ý là cờ CV_RETR_EXTERNAL có thể được sử dụng khi tìm các đường đếm để từ chối tất cả các đường viền bên trong một hình dạng kín.
mehfoos yacoob

Câu trả lời:


162

Đây là một chủ đề định kỳ trong Stackoverflow và vì tôi không thể tìm thấy một triển khai có liên quan, tôi quyết định chấp nhận thử thách.

Tôi đã thực hiện một số sửa đổi cho bản demo hình vuông có trong OpenCV và kết quả là mã C ++ bên dưới có thể phát hiện một tờ giấy trong hình ảnh:

void find_squares(Mat& image, vector<vector<Point> >& squares)
{
    // blur will enhance edge detection
    Mat blurred(image);
    medianBlur(image, blurred, 9);

    Mat gray0(blurred.size(), CV_8U), gray;
    vector<vector<Point> > contours;

    // find squares in every color plane of the image
    for (int c = 0; c < 3; c++)
    {
        int ch[] = {c, 0};
        mixChannels(&blurred, 1, &gray0, 1, ch, 1);

        // try several threshold levels
        const int threshold_level = 2;
        for (int l = 0; l < threshold_level; l++)
        {
            // Use Canny instead of zero threshold level!
            // Canny helps to catch squares with gradient shading
            if (l == 0)
            {
                Canny(gray0, gray, 10, 20, 3); // 

                // Dilate helps to remove potential holes between edge segments
                dilate(gray, gray, Mat(), Point(-1,-1));
            }
            else
            {
                    gray = gray0 >= (l+1) * 255 / threshold_level;
            }

            // Find contours and store them in a list
            findContours(gray, contours, CV_RETR_LIST, CV_CHAIN_APPROX_SIMPLE);

            // Test contours
            vector<Point> approx;
            for (size_t i = 0; i < contours.size(); i++)
            {
                    // approximate contour with accuracy proportional
                    // to the contour perimeter
                    approxPolyDP(Mat(contours[i]), approx, arcLength(Mat(contours[i]), true)*0.02, true);

                    // Note: absolute value of an area is used because
                    // area may be positive or negative - in accordance with the
                    // contour orientation
                    if (approx.size() == 4 &&
                            fabs(contourArea(Mat(approx))) > 1000 &&
                            isContourConvex(Mat(approx)))
                    {
                            double maxCosine = 0;

                            for (int j = 2; j < 5; j++)
                            {
                                    double cosine = fabs(angle(approx[j%4], approx[j-2], approx[j-1]));
                                    maxCosine = MAX(maxCosine, cosine);
                            }

                            if (maxCosine < 0.3)
                                    squares.push_back(approx);
                    }
            }
        }
    }
}

Sau khi thủ tục này được thực thi, tờ giấy sẽ là hình vuông lớn nhất trong vector<vector<Point> >:

phát hiện tờ giấy opencv

Tôi cho phép bạn viết hàm để tìm hình vuông lớn nhất. ;)


4
Đó là lý do tại sao tôi sử dụng kiểm soát nguồn. Việc sửa đổi ngẫu nhiên nhỏ nhất cho mã có thể dễ dàng được phát hiện. Nếu bạn không thay đổi bất cứ điều gì, hãy thử kiểm tra với các hình ảnh khác và cuối cùng biên dịch lại / cài đặt lại opencv.
karlphillip

2
OpenCV khá giống nhau cho tất cả các nền tảng (Win / Linux / Mac / iPhone / ...). Sự khác biệt là một số không hỗ trợ mô-đun GPU của OpenCV. Bạn đã xây dựng OpenCV cho iOS chưa ? Bạn có thể kiểm tra nó không? Tôi nghĩ đây là những câu hỏi bạn cần trả lời trước khi thử bất cứ điều gì cao cấp hơn. Bước chân em bé!
karlphillip

1
@karlphillip Tôi đã kiểm tra mã này và tôi đã có thể phát hiện ra giấy rõ ràng, nhưng nó tốn rất nhiều thời gian. Là mã thực sự nặng? có một ứng dụng tên là SayText nơi phát hiện này xảy ra trong thời gian thực từ luồng video. Mã này sẽ không thực tế cho thời gian thực, phải không?
alandalusi

1
Có lẽ. Đây là một câu trả lời học thuật, không thực tế cho ngành công nghiệp. Có tất cả các loại tối ưu hóa bạn có thể thử, bắt đầu với định nghĩa của bộ đếm được đặt tại for (int c = 0; c < 3; c++), có trách nhiệm lặp lại trên mọi kênh của hình ảnh. Chẳng hạn, bạn có thể đặt nó thành lặp chỉ trên một kênh :) Đừng quên bỏ phiếu.
karlphillip

3
@SilentPro angle()là một hàm trợ giúp . Như đã nêu trong câu trả lời, mã này dựa trên các mẫu / cpp / squares.cpp có trong OpenCV.
karlphillip

40

Trừ khi có một số yêu cầu khác không được chỉ định, tôi chỉ đơn giản là chuyển đổi hình ảnh màu của bạn sang thang độ xám và chỉ hoạt động với điều đó (không cần phải làm việc trên 3 kênh, hiện tại độ tương phản đã quá cao). Ngoài ra, trừ khi có một số vấn đề cụ thể liên quan đến thay đổi kích thước, tôi sẽ làm việc với một phiên bản thu nhỏ của hình ảnh của bạn, vì chúng tương đối lớn và kích thước không thêm gì cho vấn đề đang được giải quyết. Sau đó, cuối cùng, vấn đề của bạn được giải quyết với bộ lọc trung vị, một số công cụ hình thái cơ bản và thống kê (chủ yếu là cho ngưỡng Otsu, đã được thực hiện cho bạn).

Đây là những gì tôi có được với hình ảnh mẫu của bạn và một số hình ảnh khác với một tờ giấy tôi tìm thấy xung quanh:

nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây

Bộ lọc trung vị được sử dụng để xóa các chi tiết nhỏ khỏi hình ảnh, giờ là thang độ xám. Nó có thể sẽ loại bỏ các đường mỏng bên trong tờ giấy trắng, điều này là tốt bởi vì sau đó bạn sẽ kết thúc với các thành phần nhỏ được kết nối dễ dàng loại bỏ. Sau trung vị, áp dụng một gradient hình thái (đơn giản dilation- erosion) và nhị phân kết quả bằng Otsu. Độ dốc hình thái là một phương pháp tốt để giữ các cạnh mạnh, nó nên được sử dụng nhiều hơn. Sau đó, vì độ dốc này sẽ tăng chiều rộng đường viền, áp dụng độ mỏng hình thái. Bây giờ bạn có thể loại bỏ các thành phần nhỏ.

Tại thời điểm này, đây là những gì chúng ta có với hình ảnh bên phải (trước khi vẽ đa giác màu xanh), phần bên trái không được hiển thị vì thành phần duy nhất còn lại là phần mô tả bài báo:

nhập mô tả hình ảnh ở đây

Cho các ví dụ, bây giờ vấn đề duy nhất còn lại là phân biệt giữa các thành phần trông giống như hình chữ nhật và các thành phần khác thì không. Đây là vấn đề xác định tỷ lệ giữa diện tích vỏ lồi chứa hình dạng và diện tích của hộp giới hạn của nó; tỷ lệ 0,7 hoạt động tốt cho các ví dụ này. Đây có thể là trường hợp bạn cũng cần loại bỏ các thành phần bên trong tờ giấy, nhưng không phải trong các ví dụ này bằng cách sử dụng phương pháp này (tuy nhiên, thực hiện bước này sẽ rất dễ dàng đặc biệt vì có thể được thực hiện trực tiếp qua OpenCV).

Để tham khảo, đây là một mã mẫu trong Mathematica:

f = Import["http://thwartedglamour.files.wordpress.com/2010/06/my-coffee-table-1-sa.jpg"]
f = ImageResize[f, ImageDimensions[f][[1]]/4]
g = MedianFilter[ColorConvert[f, "Grayscale"], 2]
h = DeleteSmallComponents[Thinning[
     Binarize[ImageSubtract[Dilation[g, 1], Erosion[g, 1]]]]]
convexvert = ComponentMeasurements[SelectComponents[
     h, {"ConvexArea", "BoundingBoxArea"}, #1 / #2 > 0.7 &], 
     "ConvexVertices"][[All, 2]]
(* To visualize the blue polygons above: *)
Show[f, Graphics[{EdgeForm[{Blue, Thick}], RGBColor[0, 0, 1, 0.5], 
     Polygon @@ convexvert}]]

Nếu có nhiều tình huống khác nhau trong đó hình chữ nhật của giấy không được xác định rõ hoặc cách tiếp cận nhầm lẫn nó với các hình dạng khác - những tình huống này có thể xảy ra do nhiều lý do khác nhau, nhưng nguyên nhân phổ biến là thu nhận hình ảnh xấu - sau đó thử kết hợp trước - các bước xử lý với công việc được mô tả trong bài báo "Phát hiện hình chữ nhật dựa trên biến đổi Hough Windowed".


1
Có sự khác biệt lớn nào trong việc triển khai của bạn và của bạn ở trên không (ví dụ: câu trả lời của @karlphilip)? Tôi xin lỗi, tôi không thể tìm thấy bất kỳ cái gì trong một cái nhìn nhanh (ngoại trừ 3 kênh-1 kênh và Mathicala-OpenCV).
Abid Rahman K

2
@AbidRahmanK vâng, có .. Tôi không sử dụng canny cũng không "vài ngưỡng" để bắt đầu. Có những khác biệt khác, nhưng bằng giọng điệu bình luận của bạn, có vẻ như vô nghĩa khi đặt bất kỳ nỗ lực nào vào bình luận của riêng tôi.
mmgp

1
Tôi thấy cả hai bạn lần đầu tiên tìm thấy các cạnh và xác định cạnh nào là hình vuông. Để tìm các cạnh, bạn mọi người sử dụng các phương pháp khác nhau. Ông sử dụng canny, bạn sử dụng một số xói mòn-xói mòn. Và "một vài ngưỡng", có thể anh ta đã nhận được từ các mẫu OpenCV, được sử dụng để tìm hình vuông. Điều chính là, tôi cảm thấy khái niệm tổng thể là như nhau. "Tìm các cạnh và phát hiện hình vuông". Và tôi đã hỏi nó một cách chân thành, tôi không biết "giai điệu" nào bạn nhận được từ nhận xét của tôi, hoặc những gì bạn (hiểu / hiểu sai). Vì vậy, nếu bạn cảm thấy câu hỏi này là chân thành, tôi muốn biết sự khác biệt khác. Nếu không thì bỏ ý kiến ​​của tôi.
Abid Rahman K

1
@AbidRahmanK tất nhiên khái niệm là như nhau, nhiệm vụ là như nhau. Lọc trung bình đang được sử dụng, pha loãng đang được sử dụng, tôi không quan tâm đến việc anh ấy đã lấy ý tưởng ở ngưỡng nào - nó chỉ không được sử dụng ở đây (vì vậy làm thế nào nó không thể là một sự khác biệt?), Hình ảnh được thay đổi kích thước ở đây, đo lường thành phần là khác nhau. "Một số sự xói mòn-xói mòn" không cho các cạnh nhị phân, otsu được sử dụng cho điều đó. Thật vô nghĩa khi đề cập đến điều này, mã ở đó.
mmgp

1
K. Cảm ơn bạn. Có câu trả lời. Concept is the same. (Tôi chưa bao giờ sử dụng Mathicala, vì vậy tôi không thể hiểu được mã.) Và sự khác biệt bạn đã đề cập là sự khác biệt, nhưng không phải là cách tiếp cận khác hoặc cách tiếp cận chính. Nếu bạn vẫn chưa làm Ví dụ, hãy kiểm tra điều này:
Abid Rahman K

14

Chà, tôi đến muộn.


Trong hình ảnh của bạn, giấy là white, trong khi nền là colored. Vì vậy, tốt hơn là phát hiện giấy là Saturation(饱和度)kênh HSV color space. Trước tiên hãy tham khảo wiki HSL_and_HSV . Sau đó, tôi sẽ sao chép hầu hết ý tưởng từ câu trả lời của mình trong Phân đoạn phát hiện màu này trong một hình ảnh .


Các bước chính:

  1. Đọc vào BGR
  2. Chuyển đổi hình ảnh từ bgrđể hsvkhông gian
  3. Ngưỡng kênh S
  4. Sau đó tìm đường viền ngoài tối đa (hoặc làm Canny, hoặc HoughLinestùy thích, tôi chọn findContours), xấp xỉ để có được các góc.

Đây là kết quả của tôi:

nhập mô tả hình ảnh ở đây


Mã Python (Python 3.5 + OpenCV 3.3):

#!/usr/bin/python3
# 2017.12.20 10:47:28 CST
# 2017.12.20 11:29:30 CST

import cv2
import numpy as np

##(1) read into  bgr-space
img = cv2.imread("test2.jpg")

##(2) convert to hsv-space, then split the channels
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
h,s,v = cv2.split(hsv)

##(3) threshold the S channel using adaptive method(`THRESH_OTSU`) or fixed thresh
th, threshed = cv2.threshold(s, 50, 255, cv2.THRESH_BINARY_INV)

##(4) find all the external contours on the threshed S
#_, cnts, _ = cv2.findContours(threshed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cv2.findContours(threshed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]

canvas  = img.copy()
#cv2.drawContours(canvas, cnts, -1, (0,255,0), 1)

## sort and choose the largest contour
cnts = sorted(cnts, key = cv2.contourArea)
cnt = cnts[-1]

## approx the contour, so the get the corner points
arclen = cv2.arcLength(cnt, True)
approx = cv2.approxPolyDP(cnt, 0.02* arclen, True)
cv2.drawContours(canvas, [cnt], -1, (255,0,0), 1, cv2.LINE_AA)
cv2.drawContours(canvas, [approx], -1, (0, 0, 255), 1, cv2.LINE_AA)

## Ok, you can see the result as tag(6)
cv2.imwrite("detected.png", canvas)

Câu trả lời liên quan:

  1. Làm cách nào để phát hiện các mảng màu trong ảnh bằng OpenCV?
  2. Phát hiện cạnh trên nền màu bằng OpenCV
  3. OpenCV C ++ / Obj-C: Phát hiện một tờ giấy / Phát hiện vuông
  4. Làm cách nào để sử dụng `cv2.findContours` trong các phiên bản OpenCV khác nhau?

Tôi đã thử sử dụng không gian S nhưng vẫn không thể thành công. Xem điều này: stackoverflow.com/questions/50699893/ Cách
hchouhan02

3

Những gì bạn cần là một hình tứ giác thay vì một hình chữ nhật xoay. RotatedRectsẽ cho bạn kết quả không chính xác. Ngoài ra, bạn sẽ cần một hình chiếu phối cảnh.

Về cơ bản những gì phải được thực hiện là:

  • Lặp lại tất cả các phân đoạn đa giác và kết nối những đoạn gần như bằng nhau.
  • Sắp xếp chúng để bạn có 4 phân khúc dòng lớn nhất.
  • Giao cắt các đường đó và bạn có 4 điểm góc rất có thể.
  • Chuyển đổi ma trận qua phối cảnh được thu thập từ các điểm góc và tỷ lệ khung hình của đối tượng đã biết.

Tôi đã triển khai một lớp học Quadranglechăm sóc đường viền để chuyển đổi tứ giác và cũng sẽ chuyển đổi nó theo quan điểm đúng.

Xem một triển khai làm việc ở đây: Java OpenCV bỏ qua một đường viền


1

Khi bạn đã phát hiện hộp giới hạn của tài liệu, bạn có thể thực hiện chuyển đổi phối cảnh bốn điểm để có được chế độ xem mắt chim từ trên xuống của hình ảnh. Điều này sẽ sửa lỗi và chỉ cách ly đối tượng mong muốn.


Hình ảnh đầu vào:

Đối tượng văn bản được phát hiện

Chế độ xem từ trên xuống của tài liệu văn bản

from imutils.perspective import four_point_transform
import cv2
import numpy

# Load image, grayscale, Gaussian blur, Otsu's threshold
image = cv2.imread("1.png")
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray, (7,7), 0)
thresh = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)[1]

# Find contours and sort for largest contour
cnts = cv2.findContours(thresh, cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
cnts = sorted(cnts, key=cv2.contourArea, reverse=True)
displayCnt = None

for c in cnts:
    # Perform contour approximation
    peri = cv2.arcLength(c, True)
    approx = cv2.approxPolyDP(c, 0.02 * peri, True)
    if len(approx) == 4:
        displayCnt = approx
        break

# Obtain birds' eye view of image
warped = four_point_transform(image, displayCnt.reshape(4, 2))

cv2.imshow("thresh", thresh)
cv2.imshow("warped", warped)
cv2.imshow("image", image)
cv2.waitKey()

-1

Phát hiện tờ giấy là trường học cũ. Nếu bạn muốn giải quyết phát hiện lệch thì tốt hơn nếu bạn nhắm thẳng để phát hiện dòng văn bản. Với điều này, bạn sẽ có được các cực bên trái, phải, trên và dưới. Hủy bỏ bất kỳ đồ họa trong hình ảnh nếu bạn không muốn và sau đó thực hiện một số thống kê trên các phân đoạn dòng văn bản để tìm phạm vi góc xảy ra nhiều nhất hoặc thay vì góc. Đây là cách bạn sẽ thu hẹp đến một góc nghiêng tốt. Bây giờ sau đó, bạn đặt các tham số này góc nghiêng và các cực ngoài để khử và cắt hình ảnh theo yêu cầu.

Đối với yêu cầu hình ảnh hiện tại, sẽ tốt hơn nếu bạn thử CV_RETR_EXTERNAL thay vì CV_RETR_LIST.

Một phương pháp khác để phát hiện các cạnh là huấn luyện một trình phân loại rừng ngẫu nhiên trên các cạnh giấy và sau đó sử dụng trình phân loại để lấy Bản đồ cạnh. Đây là một phương pháp mạnh mẽ nhưng đòi hỏi đào tạo và thời gian.

Các khu rừng ngẫu nhiên sẽ hoạt động với các kịch bản khác biệt tương phản thấp, ví dụ như giấy trắng trên nền trắng.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.