326 lines
9.5 KiB
C++
326 lines
9.5 KiB
C++
#include<cmath>
|
|
#include<algorithm>
|
|
#include<unistd.h>
|
|
#include<fstream>
|
|
#include<iomanip>
|
|
|
|
#ifdef HAS_OPENCV3
|
|
#include <opencv2/core.hpp> //Any OPENCV3 code
|
|
#else
|
|
#include <opencv2/core/core.hpp> //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<cv::Point> > contours;
|
|
std::vector<cv::Vec4i> 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<cv::Point> 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<cv::Point> vertical_points[2];
|
|
cv::Vec<double,3> 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<double,3> 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<cv::Point> 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<double>(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<int> 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:
|