Ask Your Question

Revision history [back]

Detecting Documents On White Surface

I am trying to reproduce a real time document scanner like the one Scannable evernote has created. With some of the methods I have found and tweaked, I was able to create a reliable outline of a document, but against surfaces that are not glossy and the color must be darker than the document. I need help finding a way to capture the document on a glass desk or on a surface that looks white, but the human eye can still make a clear distinction that between the document and surface color. In addition, these methods can be very slow.

Method One: (Cannot Detect Documents On White Surface)

cv::Mat downScale, upScale, hsv, whiteScale, grayScale, threshold, smooth;
 cv::Mat grayContours;

 cv::pyrDown(image, downScale, cv::Size(image.cols/2, image.rows/2));
 cv::pyrUp(downScale, upScale, image.size());

 if(!detected){
      cv::cvtColor(upScale, hsv, COLOR_RGB2HSV);
      cv::inRange(hsv, cv::Scalar(0, 0, 0), cv::Scalar(0, 0, 255), whiteScale);
      cv::cvtColor(whiteScale, grayScale, CV_RGB2GRAY);
 }else{
      cv::cvtColor(upScale, grayScale, CV_RGB2GRAY);
 }

 //cv::medianBlur(grayScale, smooth, 3);
 //cv::GaussianBlur(grayScale, smooth, cv::Size(3, 3), 0, 0);
 cv::blur(grayScale, smooth, cv::Size(3,3));
 // 128, 255
 cv::threshold(smooth, threshold, 128, 255, THRESH_BINARY | THRESH_OTSU);

 cv::Canny(threshold, grayContours, 0, 50, 5);
 cv::dilate(grayContours, grayContours, cv::Mat(), cv::Point(-1,-1));

 vector<vector <cv::Point>> contours;
 findContours(grayContours, contours, RETR_LIST, CHAIN_APPROX_SIMPLE);

 vector<vector<cv::Point> > squares;
 vector<cv::Point> approx;
 for( size_t i = 0; i < contours.size(); i++ )
 {
      approxPolyDP(Mat(contours[i]), approx, arcLength(Mat(contours[i]), true)*0.02, true);

      if( approx.size() == 4 &&
           fabs(contourArea(Mat(approx))) > 10000 &&
           isContourConvex(Mat(approx)) )
      {
           double maxCosine = 0;

           for( int j = 2; j < 5; j++ )
           {
                cv::Point pt1 = approx[j%4], pt2 = approx[j-2], pt0 = approx[j-1];
                double dx1 = pt1.x - pt0.x;
                double dy1 = pt1.y - pt0.y;
                double dx2 = pt2.x - pt0.x;
                double dy2 = pt2.y - pt0.y;

                double cosine = fabs((dx1*dx2 + dy1*dy2)/sqrt((dx1*dx1 + dy1*dy1)*(dx2*dx2 + dy2*dy2) + 1e-10));
                maxCosine = MAX(maxCosine, cosine);
           }

           if( maxCosine < 0.3 ){
                squares.push_back(approx);
                detected = true;
           }
      }
 }

 if(squares.empty())
      detected = false;

 for( size_t i = 0; i < squares.size(); i++ )
 {
      const cv::Point* p = &squares[i][0];
      int n = (int)squares[i].size();
      cv::polylines(image, &p, &n, 1, true, Scalar(0,255,0), 3, CV_AA);
 }

Method Two(Cannot Detect Document on White Surface):

 cv::RNG rng(12345);
 cv::Mat oResult, oToShow;
 cv::cvtColor(image, oToShow, CV_RGB2GRAY);

 //cv::threshold(image, oResult, 127, 255, 0);
 cv::threshold(oToShow, oResult, 128, 255, THRESH_BINARY | THRESH_OTSU);
 std::vector<std::vector<cv::Point>> contours;
 std::vector<cv::Vec4i> hierarchy;

 //CV_RETR_LIST
 cv::findContours(oResult, contours, hierarchy, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE);

 std::vector<std::vector<cv::Point> > oHull(contours.size());
 //vector<cv::Rect> boundRect( contours.size() );

 double largestArea = 0.0;
 if(contours.size() > 0)
 largestArea = cv::contourArea(contours[0]);
 int index = 0;
 for( size_t i = 0; i != contours.size(); i++)
 {
      double area = cv::contourArea(contours[i]);
      if(area > largestArea && area > 50000){
           cv::convexHull(contours[i], oHull[i]);
           cv::approxPolyDP(oHull[i], oHull[i], 0.1*cv::arcLength(oHull[i], true), true);
           if(oHull[i].size() == 4){
                largestArea = area;
                //boundRect[i] = cv::boundingRect( Mat(contours[i]) );
                index = i;
           }
      }
 }

 //cv::Scalar color = cv::Scalar( rng.uniform(0, 255), rng.uniform(0,255), rng.uniform(0,255) );
 //rectangle(image, boundRect[index].tl(), boundRect[index].br(), color, 2, 8, 0 );
 drawContours(image, contours,index, cv::Scalar( 255, 255, 0 ), 2 );

Method Three(Will Not Detect Any Document):

 cv::RNG rng(12345);
 cv::Mat downScale, upScale, grayScale, threshold, smooth;
 cv::Mat grayContours;

 cv::pyrDown(image, downScale, cv::Size(image.cols/2, image.rows/2));
 cv::pyrUp(downScale, upScale, image.size());
 cv::cvtColor(upScale, grayScale, CV_RGB2GRAY);
 cv::medianBlur(grayScale, smooth, 3);
 //cv::GaussianBlur(grayScale, smooth, cv::Size(3, 3), 0, 0);
 //cv::blur(grayScale, smooth, cv::Size(3,3));
 cv::threshold(smooth, threshold, 128, 255, THRESH_BINARY | THRESH_OTSU);

 //cv::Canny(threshold, grayContours, 0, 50, 5);
 //cv::dilate(grayContours, grayContours, cv::Mat(), cv::Point(-1,-1));

 vector<Point2f> corners;
 double qualityLevel = 0.01;
 double minDistance = 50;
 double maxCorners = 2;
 int blockSize = 3;
 bool useHarrisDetector = true;
 double k = 0.04;

 /// Copy the source image
 /// Apply corner detection
 cv::goodFeaturesToTrack(threshold,
      corners,
      maxCorners,
      qualityLevel,
      minDistance,
      Mat(),
      blockSize,
      useHarrisDetector,
      k );

 int r = 4;
 for( int i = 0; i < corners.size(); i++ )
 {
      cv::circle(image, corners[i], r, cv::Scalar(rng.uniform(0,255), rng.uniform(0,255),
           rng.uniform(0,255)), -1, 8, 0 );
 }

Method Four(Will Detect Images On White Surfaces, But Will Go Out Of Control On Patterns Or Dark Surfaces):

 cv::Mat grayScale;

 cv::cvtColor(image, grayScale, CV_BGR2GRAY);
 cv::GaussianBlur(grayScale, grayScale, cv::Size(3,3), 0);
 cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Point(9,9));
 cv::Mat dilated;
 cv::dilate(grayScale, 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);
 }
 cv::drawContours(image, contoursDraw, -1, cv::Scalar(0,255,0),1);