diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fcebaf1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +binarizewolfjolion +result_*.jpg diff --git a/Makefile b/Makefile index 882eabf..e0b7546 100644 --- a/Makefile +++ b/Makefile @@ -1,16 +1,14 @@ - - all: - g++ -I/usr/include/opencv binarizewolfjolion.cpp -o binarizewolfjolion `pkg-config opencv --libs` -lstdc++ + c++ -O2 -I/usr/include/opencv4 binarizewolfjolion.cpp -o binarizewolfjolion `pkg-config opencv4 --libs` -lstdc++ + +test: all + ./binarizewolfjolion -k '-0.2' -m 'n' sample.jpg result_n.jpg + ./binarizewolfjolion -k '0.6' -m 's' sample.jpg result_s.jpg + ./binarizewolfjolion -k '0.6' -m 'w' sample.jpg result_w.jpg clean: rm -f binarizewolfjolion + rm -f result_*.jpg -test: - ./binarizewolfjolion -k 0.6 sample.jpg _result.jpg - - -package: clean - rm -f x.jpg - tar cvfz binarizewolfjolionopencv.tgz * - +package: clean + tar -cvfz binarizewolfjolionopencv.tar.gz * diff --git a/README.md b/README.md index daf7c43..e3b4cd5 100644 --- a/README.md +++ b/README.md @@ -14,22 +14,17 @@ This code uses an improved contrast maximization version of Niblack/Sauvola et a ## Usage: -The executable is called on the command line and only reads and writes PGM files. Under Linux you can use for instance "convert" (part of the ImageMagick package) to convert to and from the PGM format. The first argument chooses between one of several methods, the first and the second argument specify, respectively, the input and the output file: - ``` -usage: binarize [ -x -y -k ] [ version ] - -version: n Niblack (1986) needs white text on black background - s Sauvola et al. (1997) needs black text on white background - w Wolf et al. (2001) needs black text on white background +Usage: ./binarizewolfjolion [ -x -y -k -m ] -Default version: w (Wolf et al. 2001) -Default value for "k": 0.5 +method: n Niblack (1986) needs white text on black background + s Sauvola et al. (1997) needs black text on white background + w (deafult) Wolf et al. (2001) needs black text on white background -example: - binarize w in.pgm out.pgm - binarize in.pgm out.pgm - binarize s -x 50 -y 50 -k 0.6 in.pgm out.pgm +Example: + ./binarizewolfjolion -m w in.pgm out.pgm + ./binarizewolfjolion in.pgm out.pgm + ./binarizewolfjolion -m s -x 50 -y 50 -k 0.6 in.pgm out.pgm ``` The best working method is 'w', the one which performed 5th in the [DIBCO 2009 competition](http://www.cvc.uab.es/icdar2009/papers/3725b375.pdf). diff --git a/binarizewolfjolion.cpp b/binarizewolfjolion.cpp index e92d124..f6acecc 100644 --- a/binarizewolfjolion.cpp +++ b/binarizewolfjolion.cpp @@ -12,401 +12,361 @@ * Research notebook 24.4.2001, page 132 (Calculation of s) **************************************************************/ -#include -#include +#include #include #include -// #include -// #include #include -using namespace std; -using namespace cv; +#define BINARIZEWOLF_VERSION "2.4 (August 1st, 2014)" +#define DEFAULT_K 0.5 +#define MAX_WINX 100 +#define DEFAULT_WINX 40 +#define DEFAULT_WINY 40 -enum NiblackVersion -{ - NIBLACK=0, - SAUVOLA, - WOLFJOLION, +enum ThreshMethod { + NIBLACK = 0, + SAUVOLA, + WOLFJOLION, }; -#define BINARIZEWOLF_VERSION "2.4 (August 1st, 2014)" - -#define uget(x,y) at(y,x) -#define uset(x,y,v) at(y,x)=v; -#define fget(x,y) at(y,x) -#define fset(x,y,v) at(y,x)=v; - /********************************************************** * Usage **********************************************************/ -static void usage (char *com) { - cerr << "usage: " << com << " [ -x -y -k ] [ version ] \n\n" - << "version: n Niblack (1986) needs white text on black background\n" - << " s Sauvola et al. (1997) needs black text on white background\n" - << " w Wolf et al. (2001) needs black text on white background\n" - << "\n" - << "Default version: w (Wolf et al. 2001)\n" - << "\n" - << "example:\n" - << " " << com << " w in.pgm out.pgm\n" - << " " << com << " in.pgm out.pgm\n" - << " " << com << " s -x 50 -y 50 -k 0.6 in.pgm out.pgm\n"; +static void usage(char *com) { + std::cerr << "Usage: " << com + << " [ -x -y -k -m ] \n" + << "\n" + << "method: n Niblack (1986) needs white text on black background\n" + << " s Sauvola et al. (1997) needs black text on white background\n" + << " w (deafult) Wolf et al. (2001) needs black text on white background\n" + << "\n" + << "Example:\n" + << " " << com << " -m w in.pgm out.pgm\n" + << " " << com << " in.pgm out.pgm\n" + << " " << com << " -m s -x 50 -y 50 -k 0.6 in.pgm out.pgm\n"; } // ************************************************************* // glide a window across the image and -// ************************************************************* // create two maps: mean and standard deviation. +// ************************************************************* // // Version patched by Thibault Yohan (using opencv integral images) +double calcLocalStats(cv::Mat &im, cv::Mat &map_m, cv::Mat &map_s, int winx, int winy) { + cv::Mat im_sum; + cv::Mat im_sum_sq; + cv::integral(im, im_sum, im_sum_sq, CV_64F); + + int wxh = winx / 2; + int wyh = winy / 2; + int x_firstth = wxh; + int y_firstth = wyh; + int y_lastth = im.rows - wyh - 1; + double winarea = winx * winy; + + double max_s = 0; + for (int j = y_firstth; j <= y_lastth; j++) { + // for sum array iterator pointer + double *sum_top_left = im_sum.ptr(j - wyh); + double *sum_top_right = sum_top_left + winx; + double *sum_bottom_left = im_sum.ptr(j - wyh + winy); + double *sum_bottom_right = sum_bottom_left + winx; + + // for sum_sq array iterator pointer + double *sum_eq_top_left = im_sum_sq.ptr(j - wyh); + double *sum_eq_top_right = sum_eq_top_left + winx; + double *sum_eq_bottom_left = im_sum_sq.ptr(j - wyh + winy); + double *sum_eq_bottom_right = sum_eq_bottom_left + winx; + + double sum = (*sum_bottom_right + *sum_top_left) - + (*sum_top_right + *sum_bottom_left); + double sum_sq = (*sum_eq_bottom_right + *sum_eq_top_left) - + (*sum_eq_top_right + *sum_eq_bottom_left); + + double m = sum / winarea; + double s = std::sqrt((sum_sq - m * sum) / winarea); + if (s > max_s) { + max_s = s; + } -double calcLocalStats (Mat &im, Mat &map_m, Mat &map_s, int winx, int winy) { - Mat im_sum, im_sum_sq; - cv::integral(im,im_sum,im_sum_sq,CV_64F); - - double m,s,max_s,sum,sum_sq; - int wxh = winx/2; - int wyh = winy/2; - int x_firstth= wxh; - int y_firstth= wyh; - int y_lastth = im.rows-wyh-1; - double winarea = winx*winy; - - max_s = 0; - for (int j = y_firstth ; j<=y_lastth; j++){ - sum = sum_sq = 0; - - // for sum array iterator pointer - double *sum_top_left = im_sum.ptr(j - wyh); - double *sum_top_right = sum_top_left + winx; - double *sum_bottom_left = im_sum.ptr(j - wyh + winy); - double *sum_bottom_right = sum_bottom_left + winx; - - // for sum_sq array iterator pointer - double *sum_eq_top_left = im_sum_sq.ptr(j - wyh); - double *sum_eq_top_right = sum_eq_top_left + winx; - double *sum_eq_bottom_left = im_sum_sq.ptr(j - wyh + winy); - double *sum_eq_bottom_right = sum_eq_bottom_left + winx; - - sum = (*sum_bottom_right + *sum_top_left) - (*sum_top_right + *sum_bottom_left); - sum_sq = (*sum_eq_bottom_right + *sum_eq_top_left) - (*sum_eq_top_right + *sum_eq_bottom_left); - - m = sum / winarea; - s = sqrt ((sum_sq - m*sum)/winarea); - if (s > max_s) max_s = s; - - float *map_m_data = map_m.ptr(j) + x_firstth; - float *map_s_data = map_s.ptr(j) + x_firstth; - *map_m_data++ = m; - *map_s_data++ = s; - - // Shift the window, add and remove new/old values to the histogram - for (int i=1 ; i <= im.cols-winx; i++) { - sum_top_left++, sum_top_right++, sum_bottom_left++, sum_bottom_right++; - - sum_eq_top_left++, sum_eq_top_right++, sum_eq_bottom_left++, sum_eq_bottom_right++; - - sum = (*sum_bottom_right + *sum_top_left) - (*sum_top_right + *sum_bottom_left); - sum_sq = (*sum_eq_bottom_right + *sum_eq_top_left) - (*sum_eq_top_right + *sum_eq_bottom_left); - - m = sum / winarea; - s = sqrt ((sum_sq - m*sum)/winarea); - if (s > max_s) max_s = s; - - *map_m_data++ = m; - *map_s_data++ = s; - } - } + float *map_m_data = map_m.ptr(j) + x_firstth; + float *map_s_data = map_s.ptr(j) + x_firstth; + *map_m_data++ = m; + *map_s_data++ = s; + + // Shift the window, add and remove new/old values to the histogram + for (int i = 1; i <= im.cols - winx; i++) { + sum_top_left++; + sum_top_right++; + sum_bottom_left++; + sum_bottom_right++; + + sum_eq_top_left++; + sum_eq_top_right++; + sum_eq_bottom_left++; + sum_eq_bottom_right++; + + sum = (*sum_bottom_right + *sum_top_left) - + (*sum_top_right + *sum_bottom_left); + sum_sq = (*sum_eq_bottom_right + *sum_eq_top_left) - + (*sum_eq_top_right + *sum_eq_bottom_left); + + m = sum / winarea; + s = std::sqrt((sum_sq - m * sum) / winarea); + if (s > max_s) { + max_s = s; + } + + *map_m_data++ = m; + *map_s_data++ = s; + } + } - return max_s; + return max_s; } - - /********************************************************** * The binarization routine **********************************************************/ +void NiblackSauvolaWolfJolion(cv::Mat im, cv::Mat output, ThreshMethod method, + int winx, int winy, double k, double dR = 128) { + + int wxh = winx / 2; + int wyh = winy / 2; + int x_firstth = wxh; + int y_firstth = wyh; + int x_lastth = im.cols - wxh - 1; + int y_lastth = im.rows - wyh - 1; + + // Create local statistics and store them in a double matrices + cv::Mat map_m = cv::Mat::zeros(im.rows, im.cols, CV_32F); + cv::Mat map_s = cv::Mat::zeros(im.rows, im.cols, CV_32F); + double max_s = calcLocalStats(im, map_m, map_s, winx, winy); + + double min_I; + double max_I; + cv::minMaxLoc(im, &min_I, &max_I); + + cv::Mat thsurf(im.rows, im.cols, CV_32F); + + double th = 0.0; + + // Create the threshold surface, including border processing + // ---------------------------------------------------- + for (int j = y_firstth; j <= y_lastth; j++) { + + float *th_surf_ptr = thsurf.ptr(j) + wxh; + float *map_m_ptr = map_m.ptr(j) + wxh; + float *map_s_ptr = map_s.ptr(j) + wxh; + + // NORMAL, NON-BORDER AREA IN THE MIDDLE OF THE WINDOW: + for (int i = 0; i <= im.cols - winx; i++) { + double m = *map_m_ptr++; + double s = *map_s_ptr++; + + // Calculate the threshold + switch (method) { + + case NIBLACK: + th = m + k * s; + break; + + case SAUVOLA: + th = m * (1 + k * (s / dR - 1)); + break; + + case WOLFJOLION: + th = m + k * (s / max_s - 1) * (m - min_I); + break; + + default: + std::cerr << "Unknown thresholding method: " << method << std::endl; + std::exit(EXIT_FAILURE); + } + + *th_surf_ptr++ = th; + + if (i == 0) { + // LEFT BORDER + float *th_surf_ptr = thsurf.ptr(j); + for (int i = 0; i <= x_firstth; ++i) + *th_surf_ptr++ = th; + + // LEFT-UPPER CORNER + if (j == y_firstth) { + for (int u = 0; u < y_firstth; ++u) { + float *th_surf_ptr = thsurf.ptr(u); + for (int i = 0; i <= x_firstth; ++i) + *th_surf_ptr++ = th; + } + } + + // LEFT-LOWER CORNER + if (j == y_lastth) { + for (int u = y_lastth + 1; u < im.rows; ++u) { + float *th_surf_ptr = thsurf.ptr(u); + for (int i = 0; i <= x_firstth; ++i) + *th_surf_ptr++ = th; + } + } + } + + // LEFT-UPPER BORDER + if (j == y_firstth) + for (int u = 0; u < y_firstth; ++u) + thsurf.at(u, i+wxh) = th; + + // LEFT-LOWER BORDER + if (j == y_lastth) + for (int u = y_lastth + 1; u < im.rows; ++u) + thsurf.at(u, i+wxh) = th; + } + + // RIGHT BORDER + th_surf_ptr = thsurf.ptr(j) + x_lastth; + for (int i = x_lastth; i < im.cols; ++i) + *th_surf_ptr++ = th; + + // RIGHT-UPPER CORNER + if (j == y_firstth) { + for (int u = 0; u < y_firstth; ++u) { + th_surf_ptr = thsurf.ptr(u) + x_lastth; + for (int i = x_lastth; i < im.cols; ++i) + *th_surf_ptr++ = th; + } + } -void NiblackSauvolaWolfJolion (Mat im, Mat output, NiblackVersion version, - int winx, int winy, double k, double dR) { - - - double m, s, max_s; - double th=0; - double min_I, max_I; - int wxh = winx/2; - int wyh = winy/2; - int x_firstth= wxh; - int x_lastth = im.cols-wxh-1; - int y_lastth = im.rows-wyh-1; - int y_firstth= wyh; - // int mx, my; - - // Create local statistics and store them in a double matrices - Mat map_m = Mat::zeros (im.rows, im.cols, CV_32F); - Mat map_s = Mat::zeros (im.rows, im.cols, CV_32F); - max_s = calcLocalStats (im, map_m, map_s, winx, winy); - - minMaxLoc(im, &min_I, &max_I); - - Mat thsurf (im.rows, im.cols, CV_32F); - - // Create the threshold surface, including border processing - // ---------------------------------------------------- - for (int j = y_firstth ; j<=y_lastth; j++) { - - float *th_surf_data = thsurf.ptr(j) + wxh; - float *map_m_data = map_m.ptr(j) + wxh; - float *map_s_data = map_s.ptr(j) + wxh; - - // NORMAL, NON-BORDER AREA IN THE MIDDLE OF THE WINDOW: - for (int i=0 ; i <= im.cols-winx; i++) { - m = *map_m_data++; - s = *map_s_data++; - - // Calculate the threshold - switch (version) { - - case NIBLACK: - th = m + k*s; - break; - - case SAUVOLA: - th = m * (1 + k*(s/dR-1)); - break; - - case WOLFJOLION: - th = m + k * (s/max_s-1) * (m-min_I); - break; - - default: - cerr << "Unknown threshold type in ImageThresholder::surfaceNiblackImproved()\n"; - exit (1); - } - - // thsurf.fset(i+wxh,j,th); - *th_surf_data++ = th; - - - if (i==0) { - // LEFT BORDER - float *th_surf_ptr = thsurf.ptr(j); - for (int i=0; i<=x_firstth; ++i) - *th_surf_ptr++ = th; - - // LEFT-UPPER CORNER - if (j==y_firstth) - { - for (int u=0; u(u); - for (int i=0; i<=x_firstth; ++i) - *th_surf_ptr++ = th; - } - - } - - // LEFT-LOWER CORNER - if (j==y_lastth) - { - for (int u=y_lastth+1; u(u); - for (int i=0; i<=x_firstth; ++i) - *th_surf_ptr++ = th; - } - } - } - - // UPPER BORDER - if (j==y_firstth) - for (int u=0; u(j) + x_lastth; - for (int i=x_lastth; i(u) + x_lastth; - for (int i=x_lastth; i(u) + x_lastth; - for (int i=x_lastth; i(y); - float *th_surf_data = thsurf.ptr(y); - unsigned char *output_data = output.ptr(y); - for (int x=0; x= *th_surf_data ? 255 : 0; - im_data++; - th_surf_data++; - output_data++; - } - } + // RIGHT-LOWER CORNER + if (j == y_lastth) { + for (int u = y_lastth + 1; u < im.rows; ++u) { + th_surf_ptr = thsurf.ptr(u) + x_lastth; + for (int i = x_lastth; i < im.cols; ++i) + *th_surf_ptr++ = th; + } + } + } + std::cerr << "surface created\n"; + + for (int y = 0; y < im.rows; ++y) { + unsigned char *im_data = im.ptr(y); + float *th_surf_data = thsurf.ptr(y); + unsigned char *output_data = output.ptr(y); + for (int x = 0; x < im.cols; ++x) { + *output_data = *im_data >= *th_surf_data ? 255 : 0; + im_data++; + th_surf_data++; + output_data++; + } + } +} + +ThreshMethod parseMethodString(char *methodString) { + char method = methodString[0]; + switch (method) { + case 'n': + std::cerr << "Niblack (1986)\n"; + return NIBLACK; + case 's': + std::cerr << "Sauvola et al. (1997)\n"; + return SAUVOLA; + case 'w': + std::cerr << "Wolf and Jolion (2001)\n"; + return WOLFJOLION; + default: + std::cerr << "\nInvalid method: '" << method << "'!"; + std::exit(EXIT_FAILURE); + } } /********************************************************** * The main function **********************************************************/ -int main (int argc, char **argv) -{ - char version; - int c; - int winx=0, winy=0; - float optK=0.5; - bool didSpecifyK=false; - NiblackVersion versionCode; - char *inputname, *outputname, *versionstring; - - cerr << "===========================================================\n" - << "Christian Wolf, LIRIS Laboratory, Lyon, France.\n" - << "christian.wolf@liris.cnrs.fr\n" - << "Version " << BINARIZEWOLF_VERSION << endl - << "===========================================================\n"; - - // Argument processing - while ((c = getopt (argc, argv, "x:y:k:")) != EOF) { - - switch (c) { - - case 'x': - winx = atof(optarg); - break; - - case 'y': - winy = atof(optarg); - break; - - case 'k': - optK = atof(optarg); - didSpecifyK = true; - break; - - case '?': - usage (*argv); - cerr << "\nProblem parsing the options!\n\n"; - exit (1); - } - } - - switch(argc-optind) - { - case 3: - versionstring=argv[optind]; - inputname=argv[optind+1]; - outputname=argv[optind+2]; - break; - - case 2: - versionstring=(char *) "w"; - inputname=argv[optind]; - outputname=argv[optind+1]; - break; - - default: - usage (*argv); - exit (1); - } - - cerr << "Adaptive binarization\n" - << "Threshold calculation: "; - - // Determine the method - version = versionstring[0]; - switch (version) - { - case 'n': - versionCode = NIBLACK; - cerr << "Niblack (1986)\n"; - break; - - case 's': - versionCode = SAUVOLA; - cerr << "Sauvola et al. (1997)\n"; - break; - - case 'w': - versionCode = WOLFJOLION; - cerr << "Wolf and Jolion (2001)\n"; - break; - - default: - usage (*argv); - cerr << "\nInvalid version: '" << version << "'!"; - } - - - cerr << "parameter k=" << optK << endl; - - if (!didSpecifyK) - cerr << "Setting k to default value " << optK << endl; - - - // Load the image in grayscale mode - Mat input = imread(inputname,CV_LOAD_IMAGE_GRAYSCALE); - - - if ((input.rows<=0) || (input.cols<=0)) { - cerr << "*** ERROR: Couldn't read input image " << inputname << endl; - exit(1); +int main(int argc, char **argv) { + int winx = 0; + int winy = 0; + float optK = DEFAULT_K; + ThreshMethod methodCode = WOLFJOLION; + char *inputname; + char *outputname; + + std::cerr << "===========================================================\n" + << "Christian Wolf, LIRIS Laboratory, Lyon, France.\n" + << "christian.wolf@liris.cnrs.fr\n" + << "Version " << BINARIZEWOLF_VERSION << std::endl + << "===========================================================\n"; + + // Argument processing + char c; + while ((c = getopt(argc, argv, "x:y:k:m:")) != EOF) { + switch (c) { + case 'x': + winx = std::atoi(optarg); + break; + case 'y': + winy = std::atoi(optarg); + break; + case 'k': + optK = std::atof(optarg); + break; + case 'm': + methodCode = parseMethodString(optarg); + break; + case '?': + usage(*argv); + std::cerr << "\nProblem parsing the options!\n\n"; + std::exit(EXIT_FAILURE); } - - - // Treat the window size - if (winx==0||winy==0) { - cerr << "Input size: " << input.cols << "x" << input.rows << endl; - winy = (int) (2.0 * input.rows-1)/3; - winx = (int) input.cols-1 < winy ? input.cols-1 : winy; - // if the window is too big, than we asume that the image - // is not a single text box, but a document page: set - // the window size to a fixed constant. - if (winx > 100) - winx = winy = 40; - cerr << "Setting window size to [" << winx - << "," << winy << "].\n"; + } + + switch (argc - optind) { + case 2: + inputname = argv[optind]; + outputname = argv[optind + 1]; + break; + default: + usage(*argv); + std::exit(EXIT_FAILURE); + } + + std::cerr << "Adaptive binarization\n" + << "Threshold calculation: "; + + std::cerr << "parameter k=" << optK << std::endl; + + // Load the image in grayscale mode + cv::Mat input = imread(inputname, cv::ImreadModes::IMREAD_GRAYSCALE); + + if ((input.rows <= 0) || (input.cols <= 0)) { + std::cerr << "*** ERROR: Couldn't read input image " << inputname << std::endl; + std::exit(EXIT_FAILURE); + } + + // Treat the window size + if (winx <= 0 || winy <= 0) { + std::cerr << "Input size: " << input.cols << "x" << input.rows << std::endl; + winy = (2 * input.rows - 1) / 3; + winx = input.cols - 1 < winy ? input.cols - 1 : winy; + // if the window is too big, then we asume that the image + // is not a single text box, but a document page: set + // the window size to a fixed constant. + if (winx > MAX_WINX) { + winx = DEFAULT_WINX; + winy = DEFAULT_WINY; } + std::cerr << "Setting window size to [" << winx << "," << winy << "].\n"; + } - // Threshold - Mat output (input.rows, input.cols, CV_8U); - NiblackSauvolaWolfJolion (input, output, versionCode, winx, winy, optK, 128); + // Threshold + cv::Mat output(input.rows, input.cols, CV_8U); + NiblackSauvolaWolfJolion(input, output, methodCode, winx, winy, optK); - // Write the tresholded file - cerr << "Writing binarized image to file '" << outputname << "'.\n"; - imwrite (outputname, output); + // Write the tresholded file + std::cerr << "Writing binarized image to file '" << outputname << "'.\n"; + imwrite(outputname, output); - return 0; + return EXIT_SUCCESS; } diff --git a/sample.jpg b/sample.jpg old mode 100755 new mode 100644