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