#include #include #include #include #include #ifdef HAS_OPENCV3 #include //Any OPENCV3 code #else #include //Any Opencv2 code #endif #include"lines.h" #include "geometry.h" #include "cvutils.h" using namespace cv; #define likely(x) __builtin_expect(!!(x), 1) #define unlikely(x) __builtin_expect(!!(x), 0) // all those colors are still visible on b/w (the first component is not so low) auto WHITE = cv::Scalar(255,255,255); auto BLUE = cv::Scalar(200,50,50); auto MAGENTA = cv::Scalar(110,10,200); auto BROWN = cv::Scalar(90,100,60); auto BLACK = cv::Scalar(0,0,0); auto YELLOW = cv::Scalar(20,200,200); void usage(char* program, std::ostream &buf) { buf << "Usage: " << program << "[options] IMAGE-INPUT" << std::endl; buf << "Identify the image as a book in perspective, get deperspectivized content" << std::endl; buf << std::endl; buf << "Options:" << std::endl; buf << " -p PROFILE Save the transformation matrix in PROFILE" << std::endl; buf << " -l LEFTPAGE Save the left page in LEFTPAGE" << std::endl; buf << " -r RIGHTPAGE Save the right page in RIGHTPAGE" << std::endl; buf << " -h HELP Show this help and exit" << std::endl; } class Options { public: char *left; char *right; char *profile; char *input; Options(); Options(int argc, char*argv[]); bool parse(int argc, char*argv[]); ~Options(); }; Options::Options() { left = right = profile = input = NULL; } Options::~Options() { if(left == NULL) { free(left); } if(right == NULL) { free(right); } if(profile == NULL) { free(profile); } //input doesn't need to be free-d: it points to argv[argc-1] } Options::Options(int argc, char *argv[]) { left = right = profile = input = NULL; parse(argc, argv); } /* Options::parse parse arguments, return true if help is requested */ bool Options::parse(int argc, char *argv[]) { int c; while((c=getopt(argc, argv, "p:l:r:h")) != -1) { switch(c) { case 'l': left = strdup(optarg); break; case 'r': right = strdup(optarg); break; case 'p': profile = strdup(optarg); break; case 'h': return true; break; default: throw std::runtime_error("Parsing error: invalid argument"); } } if(optind >= argc) { std::cerr << "Error: " << argv[0] << " needs an argument" << std::endl; throw std::runtime_error("Parsing error: argument needed"); } input = argv[optind++]; if(optind < argc) { std::cerr << "Error: too many arguments supplied" << std::endl; throw std::runtime_error("Parsing error: too many arguments"); } return false; } int main(int argc, char *argv[]) { Options args; try{ if(args.parse(argc, argv)) { usage(argv[0], std::cout); return EXIT_SUCCESS; } } catch(std::runtime_error) { usage(argv[0], std::cerr); return EXIT_FAILURE; } cv::Mat img; img=cv::imread(args.input,CV_LOAD_IMAGE_GRAYSCALE); if( img.empty() ) { std::cerr << "Error opening image, aborting" << std::endl; return EXIT_FAILURE; } //dotwidth is just a simple size so that lines and dots are visible on big images //but not huge on small images #ifdef _DEBUG unsigned short dotwidth = img.cols >> 6; // divide by 64, so efficient #endif std::vector< std::vector > contours; std::vector hierarchy; //this is not really useful cv::findContours(img,contours,hierarchy,CV_RETR_EXTERNAL,CV_CHAIN_APPROX_SIMPLE); if( 0==contours.size() ) { std::cerr << "No contours found" << std::endl; return EXIT_FAILURE; } auto contour = contours[0]; if( 1!=contours.size() ) { // choosing the biggest contour in the image // you can test it with files/2contours.png for(auto cont: contours) { if(cont.size() > contour.size()) contour = cont; } } //hull: points forming an external convex border std::vector hull; cv::convexHull(cv::Mat(contour),hull,false); //because of the "butterfly" shape of a book, the most distant points in the hull //are the one between the corner. unsigned *maxdistances = max2_distance(hull); cv::Point corn_1, corn_2, corn_3, corn_4; corn_1 = hull[maxdistances[0]]; corn_2 = hull[(maxdistances[0]+1)%hull.size()]; corn_3 = hull[maxdistances[1]]; corn_4 = hull[(maxdistances[1]+1)%hull.size()]; #ifdef _DEBUG img = cv::imread(args.input,CV_LOAD_IMAGE_COLOR); // uncomment to this to display image with colors std::cout << "Maxdist1:" << maxdistances[0] << " " << corn_1 << corn_2 << std::endl; std::cout << "Maxdist2:" << maxdistances[1] << " " << corn_3 << corn_4 << std::endl; cv::namedWindow("win", CV_GUI_NORMAL); // cv::imshow("4 corners",img); // cv::waitKey(0); #endif std::vector verticals[2]; // Between the two corners on the same side, the longest line is the vertical border of the book verticals[0] = find_longest_line(hull, (maxdistances[0]+1)%hull.size(), maxdistances[1]); std::cout << maxdistances[1] << std::endl; std::cout << maxdistances[1]+1 << std::endl; std::cout << (maxdistances[1]+1)%hull.size() << std::endl; verticals[1] = find_longest_line(hull, (maxdistances[1]+1)%hull.size(), maxdistances[0]); free(maxdistances); // theta is the angle of the line connecting point 1 and 2; it will be the // rotation of our new coordinate system auto cs = CoordinateSystem(corn_1, corn_2); assert(cs.map(corn_1).x == 0); assert(cs.map(corn_1).y == 0); assert(cs.map(corn_2).x > 0); assert(cs.map(corn_2).y == 0); cv::Vec diag1, diag2; diag1 = get_line(cs.map(corn_1), cs.map(corn_3)); diag2 = get_line(cs.map(corn_4), cs.map(corn_2)); std::cout << "mapped diag1: " << diag1 << std::endl; std::cout << "mapped diag2: " << diag2 << std::endl; std::vector points1, points2; for(cv::Point p: contour) { // the point is interesting where it is above both lines or below both lines cv::Point mapped = cs.map(p); if(is_above_line(diag1, mapped)) { if(is_above_line(diag2, mapped)) { points1.push_back(p); } } else { if(!is_above_line(diag2, mapped)) { points2.push_back(p); } } } cv::Point middle1 = further_point_from_line(get_line(corn_1, corn_2), points2); cv::Point middle2 = further_point_from_line(get_line(corn_3, corn_4), points1); // we now have all the points of the two trapezoid (middle1 and middle2 are shared) cv::Mat trasf[2]; //transformation matrix double xsize = dist(corn_1, middle1), ysize=dist(middle1, middle2); cv::Point2f trapezoid[2][4] = { {corn_1, middle1, middle2, corn_4}, {corn_2, corn_3, middle2, middle1}}; cv::Point2f outsizes[4] = {cv::Point2f(0, 0), cv::Point(xsize, 0), cv::Point(xsize, ysize), cv::Point(0, ysize)}; cv::Mat rect[2]; //final pages, transformed in a nice rectangle //we are rereading in full color to get colored output img = cv::imread(args.input,CV_LOAD_IMAGE_COLOR); for(int i=0; i < 2; i++) { trasf[i] = cv::getPerspectiveTransform(trapezoid[i], outsizes); assert(9==trasf[i].total()); // 3x3 cv::warpPerspective(img, rect[i], trasf[i], cv::Size(xsize, ysize)); } if(args.profile) { // format: each line is a tab-delimited 3x3 transformation matrix (as get by getPerspectiveTransform) // first three elements is the first line, etc std::ofstream profilebuf; profilebuf.open(args.profile); for(int t=0; t<2; t++) { for(int i=0; i<3; i++) { double* row= trasf[t].ptr(i); for(int j=0; j < 3; j++) { profilebuf << std::fixed << std::setprecision(16); profilebuf << row[j] << "\t"; //yes, there's a trailing tab } } profilebuf << std::endl; } profilebuf.close(); } #ifdef _DEBUG cv::line(img, corn_1, middle1, MAGENTA, dotwidth>>3); cv::line(img, corn_2, middle1, MAGENTA, dotwidth>>3); cv::line(img, middle1, middle2, BLACK, dotwidth>>4); cv::line(img, corn_3, middle2, MAGENTA, dotwidth>>3); cv::line(img, corn_4, middle2, MAGENTA, dotwidth>>3); cv::line(img, corn_2, corn_3, BLUE, dotwidth>>4); cv::line(img, corn_1, corn_4, BLUE, dotwidth>>4); std::cout << middle1 << middle2 << std::endl; cv::imshow("win",img); while(1) { if( (char)cv::waitKey(0) == 113 ) break; } #endif //TODO: distinguish left and right std::vector params; if(args.left || args.right) { params.push_back(CV_IMWRITE_PNG_COMPRESSION); params.push_back(9); if(args.left) { cv::imwrite(args.left, rect[0], params); } if(args.right) { cv::imwrite(args.right, rect[1], params); } } return EXIT_SUCCESS; } // vim: set noet ts=4 sw=4: