#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; } class BookShape { private: cv::Mat trasf[2]; void precompute(); cv::Point2f trapezoids[2][4]; //transformation matrix public: BookShape(cv::Point tl, cv::Point tm, cv::Point tf, cv::Point bl, cv::Point bm, cv::Point br); double xsize(); double ysize(); cv::Mat* getTrasfs(); }; //topleft topmiddle topright //botleft botmiddle botright BookShape::BookShape(cv::Point tl, cv::Point tm, cv::Point tr, cv::Point bl, cv::Point bm, cv::Point br) { trapezoids[0][0] = tl; trapezoids[0][1] = tm; trapezoids[0][2] = bm; trapezoids[0][3] = bl; trapezoids[1][0] = tm; trapezoids[1][1] = tr; trapezoids[1][2] = br; trapezoids[1][3] = bm; precompute(); } void BookShape::precompute(void) { cv::Point2f outsizes[4] = {cv::Point2f(0, 0), cv::Point(xsize(), 0), cv::Point(xsize(), ysize()), cv::Point(0, ysize())}; for(int i=0; i < 2; i++) { trasf[i] = cv::getPerspectiveTransform(trapezoids[i], outsizes); assert(9==trasf[i].total()); // 3x3 } } cv::Mat* BookShape::getTrasfs() { return trasf; } double BookShape::xsize() { return dist(trapezoids[0][0], trapezoids[0][1]); } double BookShape::ysize() { return dist(trapezoids[0][1], trapezoids[0][2]); } BookShape get_book_shape(cv::Mat img) { //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; throw "Error getting contours"; } 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()]; std::vector vertical_points[2]; cv::Vec verticals[2]; // Between the two corners on the same side, the longest line is the vertical border of the book vertical_points[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; vertical_points[1] = find_longest_line(hull, (maxdistances[1]+1)%hull.size(), maxdistances[0]); free(maxdistances); #ifdef _DEBUG img = cv::imread("files/masckera.png", CV_LOAD_IMAGE_COLOR); cv::circle(img, vertical_points[0][0], img.cols>>7, cv::Scalar(200, 0, 0), -1); cv::circle(img, vertical_points[0][vertical_points[0].size()-1], img.cols>>7, cv::Scalar(200, 0, 0), -1); cv::circle(img, vertical_points[1][0], img.cols>>7, cv::Scalar(200, 200, 0), -1); cv::circle(img, vertical_points[1][vertical_points[1].size()-1], img.cols>>7, cv::Scalar(200, 0, 0), -1); cv::circle(img, corn_1, img.cols>>7, cv::Scalar(200, 0, 200), -1); cv::namedWindow("aaa", CV_GUI_NORMAL); cv::imshow("aaa", img); cv::waitKey(0); #endif verticals[0] = get_line(vertical_points[0][0], vertical_points[0][vertical_points[0].size()-1]); std::cout << verticals[0] << std::endl; std::cout << line_value(verticals[0], corn_1) << std::endl; verticals[0][2] -= line_value(verticals[0], corn_1); std::cout << line_value(verticals[0], corn_1) << "=0?" << std::endl; verticals[1] = get_line(vertical_points[1][0], vertical_points[1][vertical_points[1].size()-1]); // 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); BookShape bs = BookShape( vertical_points[1][vertical_points[1].size()-1], middle1, vertical_points[0][0], vertical_points[1][0], middle2, vertical_points[0][vertical_points[0].size()-1] ); return bs; } 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; } BookShape bs = get_book_shape(img); 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= bs.getTrasfs()[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(); } //TODO: distinguish left and right std::vector params; if(args.left || args.right) { //we are rereading in full color to get colored output img = cv::imread(args.input,CV_LOAD_IMAGE_COLOR); cv::Mat rect[2]; //final pages, transformed in a nice rectangle params.push_back(CV_IMWRITE_PNG_COMPRESSION); params.push_back(9); if(args.left) { cv::warpPerspective(img, rect[0], bs.getTrasfs()[0], cv::Size(bs.xsize(), bs.ysize())); cv::imwrite(args.left, rect[0], params); } if(args.right) { cv::warpPerspective(img, rect[1], bs.getTrasfs()[1], cv::Size(bs.xsize(), bs.ysize())); cv::imwrite(args.right, rect[1], params); } } return EXIT_SUCCESS; } // vim: set noet ts=4 sw=4: