/*M/////////////////////////////////////////////////////////////////////////////////////// // // IMPORTANT: READ BEFORE DOWNLOADING, COPYING, INSTALLING OR USING. // // By downloading, copying, installing or using the software you agree to this license. // If you do not agree to this license, do not download, install, // copy or use the software. // // // License Agreement // For Open Source Computer Vision Library // // Copyright (C) 2000-2008, Intel Corporation, all rights reserved. // Copyright (C) 2009, Willow Garage Inc., all rights reserved. // Third party copyrights are property of their respective owners. // // Redistribution and use in source and binary forms, with or without modification, // are permitted provided that the following conditions are met: // // * Redistribution's of source code must retain the above copyright notice, // this list of conditions and the following disclaimer. // // * Redistribution's in binary form must reproduce the above copyright notice, // this list of conditions and the following disclaimer in the documentation // and/or other materials provided with the distribution. // // * The name of the copyright holders may not be used to endorse or promote products // derived from this software without specific prior written permission. // // This software is provided by the copyright holders and contributors "as is" and // any express or implied warranties, including, but not limited to, the implied // warranties of merchantability and fitness for a particular purpose are disclaimed. // In no event shall the Intel Corporation or contributors be liable for any direct, // indirect, incidental, special, exemplary, or consequential damages // (including, but not limited to, procurement of substitute goods or services; // loss of use, data, or profits; or business interruption) however caused // and on any theory of liability, whether in contract, strict liability, // or tort (including negligence or otherwise) arising in any way out of // the use of this software, even if advised of the possibility of such damage. // //M*/ #include "precomp.hpp" #include "opencv2/imgproc.hpp" #include "opencv2/ml.hpp" #include #include #include namespace cv { namespace text { using namespace std; using namespace cv::ml; /* OCR BeamSearch Decoder */ void OCRBeamSearchDecoder::run(Mat& image, string& output_text, vector* component_rects, vector* component_texts, vector* component_confidences, int component_level) { CV_Assert( (image.type() == CV_8UC1) || (image.type() == CV_8UC3) ); CV_Assert( (component_level == OCR_LEVEL_TEXTLINE) || (component_level == OCR_LEVEL_WORD) ); output_text.clear(); if (component_rects != NULL) component_rects->clear(); if (component_texts != NULL) component_texts->clear(); if (component_confidences != NULL) component_confidences->clear(); } void OCRBeamSearchDecoder::run(Mat& image, Mat& mask, string& output_text, vector* component_rects, vector* component_texts, vector* component_confidences, int component_level) { CV_Assert(mask.type() == CV_8UC1); CV_Assert( (image.type() == CV_8UC1) || (image.type() == CV_8UC3) ); CV_Assert( (component_level == OCR_LEVEL_TEXTLINE) || (component_level == OCR_LEVEL_WORD) ); output_text.clear(); if (component_rects != NULL) component_rects->clear(); if (component_texts != NULL) component_texts->clear(); if (component_confidences != NULL) component_confidences->clear(); } CV_WRAP String OCRBeamSearchDecoder::run(InputArray image, int min_confidence, int component_level) { std::string output1; std::string output2; vector component_texts; vector component_confidences; Mat image_m = image.getMat(); run(image_m, output1, NULL, &component_texts, &component_confidences, component_level); for(unsigned int i = 0; i < component_texts.size(); i++) { //cout << "confidence: " << component_confidences[i] << " text:" << component_texts[i] << endl; if(component_confidences[i] > min_confidence) { output2 += component_texts[i]; } } return String(output2); } CV_WRAP String OCRBeamSearchDecoder::run(InputArray image, InputArray mask, int min_confidence, int component_level) { std::string output1; std::string output2; vector component_texts; vector component_confidences; Mat image_m = image.getMat(); Mat mask_m = mask.getMat(); run(image_m, mask_m, output1, NULL, &component_texts, &component_confidences, component_level); for(unsigned int i = 0; i < component_texts.size(); i++) { //cout << "confidence: " << component_confidences[i] << " text:" << component_texts[i] << endl; if(component_confidences[i] > min_confidence) { output2 += component_texts[i]; } } return String(output2); } void OCRBeamSearchDecoder::ClassifierCallback::eval( InputArray image, vector< vector >& recognition_probabilities, vector& oversegmentation) { CV_Assert(( image.getMat().type() == CV_8UC3 ) || ( image.getMat().type() == CV_8UC1 )); if (!recognition_probabilities.empty()) { for (size_t i=0; i segmentation; bool expanded; // TODO calculating score of its child would be much faster if we store the last column // of their "root" path. }; bool beam_sort_function ( beamSearch_node a, beamSearch_node b ); bool beam_sort_function ( beamSearch_node a, beamSearch_node b ) { return (a.score > b.score); } class OCRBeamSearchDecoderImpl CV_FINAL : public OCRBeamSearchDecoder { public: //Default constructor OCRBeamSearchDecoderImpl( Ptr _classifier, const string& _vocabulary, InputArray transition_probabilities_table, InputArray emission_probabilities_table, decoder_mode _mode, int _beam_size) { classifier = _classifier; step_size = classifier->getStepSize(); win_size = classifier->getWindowSize(); emission_p = emission_probabilities_table.getMat(); vocabulary = _vocabulary; mode = _mode; beam_size = _beam_size; transition_probabilities_table.getMat().copyTo(transition_p); for (int i=0; i(i,j) == 0) transition_p.at(i,j) = -DBL_MAX; else transition_p.at(i,j) = log(transition_p.at(i,j)); } } } ~OCRBeamSearchDecoderImpl() CV_OVERRIDE { } void run( Mat& src, Mat& mask, string& out_sequence, vector* component_rects, vector* component_texts, vector* component_confidences, int component_level) CV_OVERRIDE { CV_Assert(mask.type() == CV_8UC1); //nothing to do with a mask here run( src, out_sequence, component_rects, component_texts, component_confidences, component_level); } void run( Mat& src, string& out_sequence, vector* component_rects, vector* component_texts, vector* component_confidences, int component_level) CV_OVERRIDE { CV_Assert( (src.type() == CV_8UC1) || (src.type() == CV_8UC3) ); CV_Assert( (src.cols > 0) && (src.rows > 0) ); CV_Assert( component_level == OCR_LEVEL_WORD ); out_sequence.clear(); if (component_rects != NULL) component_rects->clear(); if (component_texts != NULL) component_texts->clear(); if (component_confidences != NULL) component_confidences->clear(); if(src.type() == CV_8UC3) { cvtColor(src,src,COLOR_RGB2GRAY); } // TODO if input is a text line (not a word) we may need to split into words here! // do sliding window classification along a cropped word image classifier->eval(src, recognition_probabilities, oversegmentation); // if the number of oversegmentation points found is less than 2 we can not do nothing!! if (oversegmentation.size() < 2) return; //NMS of recognitions double last_best_p = 0; int last_best_idx = -1; for (size_t i=0; i best_p) { best_p = recognition_probabilities[i][j]; best_idx = (int)j; } } if ((i>0) && (best_idx == last_best_idx) && (oversegmentation[i]*step_size < oversegmentation[i-1]*step_size + win_size) ) { if (last_best_p > best_p) { //remove i'th elements and do not increment i recognition_probabilities.erase (recognition_probabilities.begin()+i); oversegmentation.erase (oversegmentation.begin()+i); continue; } else { //remove (i-1)'th elements and do not increment i recognition_probabilities.erase (recognition_probabilities.begin()+i-1); oversegmentation.erase (oversegmentation.begin()+i-1); last_best_idx = best_idx; last_best_p = best_p; continue; } } last_best_idx = best_idx; last_best_p = best_p; i++; } /*Now we go with the beam search algorithm to optimize the recognition score*/ //convert probabilities to log probabilities for (size_t i=0; i > childs = generate_childs( node.segmentation ); node.expanded = true; beam.push_back( node ); if (!childs.empty()) update_beam( childs ); generated_chids += (int)childs.size(); } } while (generated_chids != 0) { generated_chids = 0; for (size_t i=0; i > childs; if (!beam[i].expanded) { childs = generate_childs( beam[i].segmentation ); beam[i].expanded = true; } if (!childs.empty()) update_beam( childs ); generated_chids += (int)childs.size(); } } // Done! Get the best prediction found into out_sequence double lp = score_segmentation( beam[0].segmentation, out_sequence ); // fill other (dummy) output parameters if (component_rects != NULL) component_rects->push_back(Rect(0,0,src.cols,src.rows)); if (component_texts != NULL) component_texts->push_back(out_sequence); if (component_confidences != NULL) component_confidences->push_back((float)exp(lp)); return; } private: int win_size; int step_size; vector< beamSearch_node > beam; vector< vector > recognition_probabilities; vector oversegmentation; vector< vector > generate_childs( vector &segmentation ) { vector< vector > childs; for (size_t i=segmentation[segmentation.size()-1]+1; i child = segmentation; child.push_back(seg_point); childs.push_back(child); } } return childs; } void update_beam ( vector< vector > &childs ) { string out_sequence; double min_score = -DBL_MAX; //min score value to be part of the beam if ((int)beam.size() >= beam_size) min_score = beam[beam_size-1].score; //last element has the lowest score for (size_t i=0; i min_score) { beamSearch_node node; node.score = score; node.segmentation = childs[i]; node.expanded = false; beam.push_back(node); sort(beam.begin(),beam.end(),beam_sort_function); if ((int)beam.size() > beam_size) { beam.erase(beam.begin()+beam_size,beam.end()); min_score = beam[beam.size()-1].score; } } } } double score_segmentation( vector &segmentation, string& outstring ) { // Score Heuristics: // No need to use Viterbi to know a given segmentation is bad // e.g.: in some cases we discard a segmentation because it includes a very large character // in other cases we do it because the overlapping between two chars is too large // TODO Add more heuristics (e.g. penalize large inter-character variance) Mat interdist ((int)segmentation.size()-1, 1, CV_32F, 1); for (size_t i=0; i((int)i,0) = (float)oversegmentation[segmentation[(int)i+1]]*step_size - (float)oversegmentation[segmentation[(int)i]]*step_size; if ((float)interdist.at((int)i,0)/win_size > 2.25) // TODO explain how did you set this thrs { return -DBL_MAX; } if ((float)interdist.at((int)i,0)/win_size < 0.15) // TODO explain how did you set this thrs { return -DBL_MAX; } } Scalar m, std; meanStdDev(interdist, m, std); //double interdist_std = std[0]; //TODO Extracting start probs from lexicon (if we have it) may boost accuracy! vector start_p(vocabulary.size()); for (int i=0; i<(int)vocabulary.size(); i++) start_p[i] = log(1.0/vocabulary.size()); Mat V = Mat::ones((int)segmentation.size(),(int)vocabulary.size(),CV_64FC1); V = V * -DBL_MAX; vector path(vocabulary.size()); // Initialize base cases (t == 0) for (int i=0; i<(int)vocabulary.size(); i++) { V.at(0,i) = start_p[i] + recognition_probabilities[segmentation[0]][i]; path[i] = vocabulary.at(i); } // Run Viterbi for t > 0 for (int t=1; t<(int)segmentation.size(); t++) { vector newpath(vocabulary.size()); for (int i=0; i<(int)vocabulary.size(); i++) { double max_prob = -DBL_MAX; int best_idx = 0; for (int j=0; j<(int)vocabulary.size(); j++) { double prob = V.at(t-1,j) + transition_p.at(j,i) + recognition_probabilities[segmentation[t]][i]; if ( prob > max_prob) { max_prob = prob; best_idx = j; } } V.at(t,i) = max_prob; newpath[i] = path[best_idx] + vocabulary.at(i); } // Don't need to remember the old paths path.swap(newpath); } double max_prob = -DBL_MAX; int best_idx = 0; for (int i=0; i<(int)vocabulary.size(); i++) { double prob = V.at((int)segmentation.size()-1,i); if ( prob > max_prob) { max_prob = prob; best_idx = i; } } outstring = path[best_idx]; return (max_prob / (segmentation.size()-1)); } }; Ptr OCRBeamSearchDecoder::create( Ptr _classifier, const string& _vocabulary, InputArray transition_p, InputArray emission_p, decoder_mode _mode, int _beam_size) { return makePtr(_classifier, _vocabulary, transition_p, emission_p, _mode, _beam_size); } Ptr OCRBeamSearchDecoder::create(const String& _filename, const String& _vocabulary, InputArray transition_p, InputArray emission_p, decoder_mode _mode, int _beam_size) { return makePtr(loadOCRBeamSearchClassifierCNN(_filename), _vocabulary, transition_p, emission_p, (decoder_mode)_mode, _beam_size); } class OCRBeamSearchClassifierCNN CV_FINAL : public OCRBeamSearchDecoder::ClassifierCallback { public: //constructor OCRBeamSearchClassifierCNN(const std::string& filename); // Destructor ~OCRBeamSearchClassifierCNN() CV_OVERRIDE {} void eval( InputArray src, vector< vector >& recognition_probabilities, vector& oversegmentation ) CV_OVERRIDE; int getWindowSize() {return window_size;} int getStepSize() {return step_size;} void setStepSize(int _step_size) {step_size = _step_size;} protected: void normalizeAndZCA(Mat& patches); double eval_feature(Mat& feature, double* prob_estimates); private: int window_size; // window size int step_size; // sliding window step int nr_class; // number of classes int nr_feature; // number of features Mat feature_min; // scale range Mat feature_max; Mat weights; // Logistic Regression weights Mat kernels; // CNN kernels Mat M, P; // ZCA Whitening parameters int quad_size; int patch_size; int num_quads; // extract 25 quads (12x12) from each image int num_tiles; // extract 25 patches (8x8) from each quad double alpha; // used in non-linear activation function z = max(0, |D*a| - alpha) }; OCRBeamSearchClassifierCNN::OCRBeamSearchClassifierCNN (const string& filename) { if (ifstream(filename.c_str())) { FileStorage fs(filename, FileStorage::READ); // Load kernels bank and withenning params fs["kernels"] >> kernels; fs["M"] >> M; fs["P"] >> P; // Load Logistic Regression weights fs["weights"] >> weights; // Load feature scaling ranges fs["feature_min"] >> feature_min; fs["feature_max"] >> feature_max; fs.release(); } else CV_Error(Error::StsBadArg, "Default classifier data file not found!"); nr_feature = weights.rows; nr_class = weights.cols; patch_size = cvRound(sqrt((float)kernels.cols)); window_size = 4*patch_size; step_size = 4; quad_size = 12; num_quads = 25; num_tiles = 25; alpha = 0.5; // used in non-linear activation function z = max(0, |D*a| - alpha) } void OCRBeamSearchClassifierCNN::eval( InputArray _src, vector< vector >& recognition_probabilities, vector& oversegmentation) { CV_Assert(( _src.getMat().type() == CV_8UC3 ) || ( _src.getMat().type() == CV_8UC1 )); if (!recognition_probabilities.empty()) { for (size_t i=0; i > data_pool(9); int quad_id = 1; for (int q_x = 0; q_x <= sz_window_quad; q_x += sz_half_quad) { for (int q_y = 0; q_y <= sz_window_quad; q_y += sz_half_quad) { Rect quad_rect = Rect(q_x,q_y,quad_size,quad_size); quad = img(quad_rect); //start sliding window (8x8) in each tile and store the patch as row in data_pool for (int w_x = 0; w_x <= sz_quad_patch; w_x++) { for (int w_y = 0; w_y <= sz_quad_patch; w_y++) { quad(Rect(w_x,w_y,patch_size,patch_size)).convertTo(tmp, CV_64F); tmp = tmp.reshape(0,1); normalizeAndZCA(tmp); vector patch; tmp.copyTo(patch); if ((quad_id == 1)||(quad_id == 2)||(quad_id == 6)||(quad_id == 7)) data_pool[0].insert(data_pool[0].end(),patch.begin(),patch.end()); if ((quad_id == 2)||(quad_id == 7)||(quad_id == 3)||(quad_id == 8)||(quad_id == 4)||(quad_id == 9)) data_pool[1].insert(data_pool[1].end(),patch.begin(),patch.end()); if ((quad_id == 4)||(quad_id == 9)||(quad_id == 5)||(quad_id == 10)) data_pool[2].insert(data_pool[2].end(),patch.begin(),patch.end()); if ((quad_id == 6)||(quad_id == 11)||(quad_id == 16)||(quad_id == 7)||(quad_id == 12)||(quad_id == 17)) data_pool[3].insert(data_pool[3].end(),patch.begin(),patch.end()); if ((quad_id == 7)||(quad_id == 12)||(quad_id == 17)||(quad_id == 8)||(quad_id == 13)||(quad_id == 18)||(quad_id == 9)||(quad_id == 14)||(quad_id == 19)) data_pool[4].insert(data_pool[4].end(),patch.begin(),patch.end()); if ((quad_id == 9)||(quad_id == 14)||(quad_id == 19)||(quad_id == 10)||(quad_id == 15)||(quad_id == 20)) data_pool[5].insert(data_pool[5].end(),patch.begin(),patch.end()); if ((quad_id == 16)||(quad_id == 21)||(quad_id == 17)||(quad_id == 22)) data_pool[6].insert(data_pool[6].end(),patch.begin(),patch.end()); if ((quad_id == 17)||(quad_id == 22)||(quad_id == 18)||(quad_id == 23)||(quad_id == 19)||(quad_id == 24)) data_pool[7].insert(data_pool[7].end(),patch.begin(),patch.end()); if ((quad_id == 19)||(quad_id == 24)||(quad_id == 20)||(quad_id == 25)) data_pool[8].insert(data_pool[8].end(),patch.begin(),patch.end()); } } quad_id++; } } //do dot product of each normalized and whitened patch //each pool is averaged and this yields a representation of 9xD Mat feature = Mat::zeros(9,kernels.rows,CV_64FC1); for (int i=0; i<9; i++) { Mat pool = Mat(data_pool[i]); pool = pool.reshape(0,(int)data_pool[i].size()/kernels.cols); for (int p=0; p(0,f) = feature.row(i).at(0,f) + max(0.0,std::abs(pool.row(p).dot(kernels.row(f)))-alpha); } } } feature = feature.reshape(0,1); // data must be normalized within the range obtained during training double lower = -1.0; double upper = 1.0; for (int k=0; k(0,k) = lower + (upper-lower) * (feature.at(0,k)-feature_min.at(0,k))/ (feature_max.at(0,k)-feature_min.at(0,k)); } double *p = new double[nr_class]; double predict_label = eval_feature(feature,p); if ( (predict_label < 0) || (predict_label > nr_class) ) CV_Error(Error::StsOutOfRange, "OCRBeamSearchClassifierCNN::eval Error: unexpected prediction in eval_feature()"); vector recognition_p(p, p+nr_class); recognition_probabilities.push_back(recognition_p); oversegmentation.push_back(seg_points); seg_points++; } } // normalize for contrast and apply ZCA whitening to a set of image patches void OCRBeamSearchClassifierCNN::normalizeAndZCA(Mat& patches) { //Normalize for contrast for (int i=0; i(0,e_val.rows-i-1); } P = V * D * V.t(); } for (int i=0; i(idx,i)*feature.at(0,idx); //TODO use vectorized dot product int dec_max_idx = 0; for(int i=1;i prob_estimates[dec_max_idx]) dec_max_idx = i; } for(int i=0;i loadOCRBeamSearchClassifierCNN(const String& filename) { return makePtr(std::string(filename)); } } }