diff --git a/src/openalpr/TRexpp.h b/src/openalpr/TRexpp.h
new file mode 100644
index 0000000..8391e47
--- /dev/null
+++ b/src/openalpr/TRexpp.h
@@ -0,0 +1,75 @@
+#ifndef _TREXPP_H_
+#define _TREXPP_H_
+/***************************************************************
+ T-Rex a tiny regular expression library
+
+ Copyright (C) 2003-2004 Alberto Demichelis
+
+ This software is provided 'as-is', without any express
+ or implied warranty. In no event will the authors be held
+ liable for any damages arising from the use of this software.
+
+ Permission is granted to anyone to use this software for
+ any purpose, including commercial applications, and to alter
+ it and redistribute it freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented;
+ you must not claim that you wrote the original software.
+ If you use this software in a product, an acknowledgment
+ in the product documentation would be appreciated but
+ is not required.
+
+ 2. Altered source versions must be plainly marked as such,
+ and must not be misrepresented as being the original software.
+
+ 3. This notice may not be removed or altered from any
+ source distribution.
+
+****************************************************************/
+
+extern "C" {
+#include "trex.h"
+}
+
+struct TRexParseException{TRexParseException(const TRexChar *c):desc(c){}const TRexChar *desc;};
+
+class TRexpp {
+public:
+ TRexpp() { _exp = (TRex *)0; }
+ ~TRexpp() { CleanUp(); }
+ // compiles a regular expression
+ void Compile(const TRexChar *pattern) {
+ const TRexChar *error;
+ CleanUp();
+ if(!(_exp = trex_compile(pattern,&error)))
+ throw TRexParseException(error);
+ }
+ // return true if the given text match the expression
+ bool Match(const TRexChar* text) {
+ return _exp?(trex_match(_exp,text) != 0):false;
+ }
+ // Searches for the first match of the expression in a zero terminated string
+ bool Search(const TRexChar* text, const TRexChar** out_begin, const TRexChar** out_end) {
+ return _exp?(trex_search(_exp,text,out_begin,out_end) != 0):false;
+ }
+ // Searches for the first match of the expression in a string sarting at text_begin and ending at text_end
+ bool SearchRange(const TRexChar* text_begin,const TRexChar* text_end,const TRexChar** out_begin, const TRexChar** out_end) {
+ return _exp?(trex_searchrange(_exp,text_begin,text_end,out_begin,out_end) != 0):false;
+ }
+ bool GetSubExp(int n, const TRexChar** out_begin, int *out_len)
+ {
+ TRexMatch match;
+ TRexBool res = _exp?(trex_getsubexp(_exp,n,&match)):TRex_False;
+ if(res) {
+ *out_begin = match.begin;
+ *out_len = match.len;
+ return true;
+ }
+ return false;
+ }
+ int GetSubExpCount() { return _exp?trex_getsubexpcount(_exp):0; }
+private:
+ void CleanUp() { if(_exp) trex_free(_exp); _exp = (TRex *)0; }
+ TRex *_exp;
+};
+#endif //_TREXPP_H_
\ No newline at end of file
diff --git a/src/openalpr/alpr.cpp b/src/openalpr/alpr.cpp
new file mode 100644
index 0000000..074d992
--- /dev/null
+++ b/src/openalpr/alpr.cpp
@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2013 New Designs Unlimited, LLC
+ * Opensource Automated License Plate Recognition [http://www.openalpr.com]
+ *
+ * This file is part of OpenAlpr.
+ *
+ * OpenAlpr is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License
+ * version 3 as published by the Free Software Foundation
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+*/
+
+#include "alpr.h"
+#include "alpr_impl.h"
+
+
+
+
+// ALPR code
+
+Alpr::Alpr(const std::string country, const std::string runtimeDir)
+{
+ impl = new AlprImpl(country, runtimeDir);
+}
+Alpr::~Alpr()
+{
+ delete impl;
+}
+std::vector Alpr::recognize(std::string filepath)
+{
+ cv::Mat img = cv::imread(filepath, CV_LOAD_IMAGE_COLOR);
+ return impl->recognize(img);
+
+}
+
+
+std::vector Alpr::recognize(std::vector imageBuffer)
+{
+ // Not sure if this actually works
+ cv::Mat img = cv::imdecode(Mat(imageBuffer), 1);
+
+ return impl->recognize(img);
+}
+
+
+string Alpr::toJson(const vector< AlprResult > results)
+{
+ return impl->toJson(results);
+}
+
+
+void Alpr::setDetectRegion(bool detectRegion)
+{
+ impl->setDetectRegion(detectRegion);
+}
+void Alpr::setTopN(int topN)
+{
+ impl->setTopN(topN);
+}
+void Alpr::setDefaultRegion(std::string region)
+{
+ impl->setDefaultRegion(region);
+}
+
+bool Alpr::isLoaded()
+{
+ return true;
+}
+
+
+
+// Results code
+
+
+AlprResult::AlprResult()
+{
+
+}
+AlprResult::~AlprResult()
+{
+
+}
diff --git a/src/openalpr/alpr.h b/src/openalpr/alpr.h
new file mode 100644
index 0000000..7aeb890
--- /dev/null
+++ b/src/openalpr/alpr.h
@@ -0,0 +1,88 @@
+/*
+ * Copyright (c) 2013 New Designs Unlimited, LLC
+ * Opensource Automated License Plate Recognition [http://www.openalpr.com]
+ *
+ * This file is part of OpenAlpr.
+ *
+ * OpenAlpr is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License
+ * version 3 as published by the Free Software Foundation
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+*/
+
+
+#ifndef ALPR_H
+#define ALPR_H
+
+#include
+#include
+
+#define OPENALPR_VERSION "0.3"
+
+struct AlprPlate
+{
+ std::string characters;
+ float overall_confidence;
+
+ bool matches_template;
+ //int char_confidence[];
+};
+
+struct AlprCoordinate
+{
+ int x;
+ int y;
+};
+
+class AlprResult
+{
+ public:
+ AlprResult();
+ virtual ~AlprResult();
+
+ int requested_topn;
+ int result_count;
+
+ AlprPlate bestPlate;
+ std::vector topNPlates;
+
+ float processing_time_ms;
+ AlprCoordinate plate_points[4];
+
+ int regionConfidence;
+ std::string region;
+};
+
+
+
+class AlprImpl;
+class Alpr
+{
+
+ public:
+ Alpr(const std::string country, const std::string runtimeDir = "");
+ virtual ~Alpr();
+
+ void setDetectRegion(bool detectRegion);
+ void setTopN(int topN);
+ void setDefaultRegion(std::string region);
+
+ std::vector recognize(std::string filepath);
+ std::vector recognize(std::vector imageBuffer);
+
+ std::string toJson(const std::vector results);
+
+ bool isLoaded();
+
+ private:
+ AlprImpl* impl;
+};
+
+#endif // APLR_H
\ No newline at end of file
diff --git a/src/openalpr/alpr_impl.cpp b/src/openalpr/alpr_impl.cpp
new file mode 100644
index 0000000..621a1ae
--- /dev/null
+++ b/src/openalpr/alpr_impl.cpp
@@ -0,0 +1,229 @@
+/*
+ * Copyright (c) 2013 New Designs Unlimited, LLC
+ * Opensource Automated License Plate Recognition [http://www.openalpr.com]
+ *
+ * This file is part of OpenAlpr.
+ *
+ * OpenAlpr is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License
+ * version 3 as published by the Free Software Foundation
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+*/
+
+#include "alpr_impl.h"
+
+
+AlprImpl::AlprImpl(const std::string country, const std::string runtimeDir)
+{
+ config = new Config(country, runtimeDir);
+ plateDetector = new RegionDetector(config);
+ stateIdentifier = new StateIdentifier(config);
+ ocr = new OCR(config);
+
+ this->detectRegion = DEFAULT_DETECT_REGION;
+ this->topN = DEFAULT_TOPN;
+ this->defaultRegion = "";
+
+}
+AlprImpl::~AlprImpl()
+{
+ delete config;
+ delete plateDetector;
+ delete stateIdentifier;
+ delete ocr;
+}
+
+
+std::vector AlprImpl::recognize(cv::Mat img)
+{
+ timespec startTime;
+ getTime(&startTime);
+
+ vector response;
+
+
+ vector plateRegions = plateDetector->detect(img);
+
+
+ // Recognize.
+
+ for (int i = 0; i < plateRegions.size(); i++)
+ {
+ timespec platestarttime;
+ getTime(&platestarttime);
+
+ LicensePlateCandidate lp(img, plateRegions[i], config);
+
+ lp.recognize();
+
+
+ if (lp.confidence > 10)
+ {
+ AlprResult plateResult;
+ plateResult.region = defaultRegion;
+ plateResult.regionConfidence = 0;
+
+ for (int pointidx = 0; pointidx < 4; pointidx++)
+ {
+ plateResult.plate_points[pointidx].x = (int) lp.plateCorners[pointidx].x;
+ plateResult.plate_points[pointidx].y = (int) lp.plateCorners[pointidx].y;
+ }
+
+ if (detectRegion)
+ {
+ char statecode[4];
+ plateResult.regionConfidence = stateIdentifier->recognize(img, plateRegions[i], statecode);
+ if (plateResult.regionConfidence > 0)
+ {
+ plateResult.region = statecode;
+ }
+ }
+
+
+ ocr->performOCR(lp.charSegmenter->getThresholds(), lp.charSegmenter->characters);
+
+ ocr->postProcessor->analyze(plateResult.region, topN);
+
+ //plateResult.characters = ocr->postProcessor->bestChars;
+ const vector ppResults = ocr->postProcessor->getResults();
+
+ int bestPlateIndex = 0;
+
+ for (int pp = 0; pp < ppResults.size(); pp++)
+ {
+ if (pp >= topN)
+ break;
+
+ // Set our "best plate" match to either the first entry, or the first entry with a postprocessor template match
+ if (bestPlateIndex == 0 && ppResults[pp].matchesTemplate)
+ bestPlateIndex = pp;
+
+ if (ppResults[pp].letters.size() >= config->postProcessMinCharacters &&
+ ppResults[pp].letters.size() <= config->postProcessMaxCharacters)
+ {
+ AlprPlate aplate;
+ aplate.characters = ppResults[pp].letters;
+ aplate.overall_confidence = ppResults[pp].totalscore;
+ aplate.matches_template = ppResults[pp].matchesTemplate;
+ plateResult.topNPlates.push_back(aplate);
+ }
+ }
+ plateResult.result_count = plateResult.topNPlates.size();
+
+ if (plateResult.topNPlates.size() > 0)
+ plateResult.bestPlate = plateResult.topNPlates[bestPlateIndex];
+
+ timespec plateEndTime;
+ getTime(&plateEndTime);
+ plateResult.processing_time_ms = diffclock(platestarttime, plateEndTime);
+
+ if (plateResult.result_count > 0)
+ response.push_back(plateResult);
+
+ if (config->debugGeneral)
+ {
+ rectangle(img, plateRegions[i], Scalar(0, 255, 0), 2);
+ for (int z = 0; z < 4; z++)
+ line(img, lp.plateCorners[z], lp.plateCorners[(z + 1) % 4], Scalar(255,0,255), 2);
+ }
+
+
+ }
+ else
+ {
+ if (config->debugGeneral)
+ rectangle(img, plateRegions[i], Scalar(0, 0, 255), 2);
+ }
+
+ }
+
+ if (config->debugTiming)
+ {
+ timespec endTime;
+ getTime(&endTime);
+ cout << "Total Time to process image: " << diffclock(startTime, endTime) << "ms." << endl;
+ }
+
+ if (config->debugGeneral && config->debugShowImages)
+ {
+ displayImage(config, "Main Image", img);
+ // Pause indefinitely until they press a key
+ while ((char) cv::waitKey(50) == -1)
+ {}
+ }
+ return response;
+}
+
+string AlprImpl::toJson(const vector< AlprResult > results)
+{
+ cJSON *root = cJSON_CreateArray();
+
+ for (int i = 0; i < results.size(); i++)
+ {
+ cJSON *resultObj = createJsonObj( &results[i] );
+ cJSON_AddItemToArray(root, resultObj);
+ }
+
+ // Print the JSON object to a string and return
+ char *out;
+ out=cJSON_PrintUnformatted(root);
+ cJSON_Delete(root);
+
+ string response(out);
+
+ free(out);
+ return response;
+}
+
+
+
+cJSON* AlprImpl::createJsonObj(const AlprResult* result)
+{
+ cJSON *root, *coords;
+
+ root=cJSON_CreateObject();
+
+ cJSON_AddStringToObject(root,"plate", result->bestPlate.characters.c_str());
+ cJSON_AddNumberToObject(root,"confidence", result->bestPlate.overall_confidence);
+ cJSON_AddNumberToObject(root,"matches_template", result->bestPlate.matches_template);
+
+ cJSON_AddStringToObject(root,"region", result->region.c_str());
+ cJSON_AddNumberToObject(root,"region_confidence", result->regionConfidence);
+
+ cJSON_AddNumberToObject(root,"processing_time_ms", result->processing_time_ms);
+
+ cJSON_AddItemToObject(root, "coordinates", coords=cJSON_CreateArray());
+ for (int i=0;i<4;i++)
+ {
+ cJSON *coords_object;
+ coords_object = cJSON_CreateObject();
+ cJSON_AddNumberToObject(coords_object, "x", result->plate_points[i].x);
+ cJSON_AddNumberToObject(coords_object, "y", result->plate_points[i].y);
+
+ cJSON_AddItemToArray(coords, coords_object);
+ }
+
+ return root;
+}
+
+
+void AlprImpl::setDetectRegion(bool detectRegion)
+{
+ this->detectRegion = detectRegion;
+}
+void AlprImpl::setTopN(int topn)
+{
+ this->topN = topn;
+}
+void AlprImpl::setDefaultRegion(string region)
+{
+ this->defaultRegion = region;
+}
+
diff --git a/src/openalpr/alpr_impl.h b/src/openalpr/alpr_impl.h
new file mode 100644
index 0000000..31f1e4f
--- /dev/null
+++ b/src/openalpr/alpr_impl.h
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2013 New Designs Unlimited, LLC
+ * Opensource Automated License Plate Recognition [http://www.openalpr.com]
+ *
+ * This file is part of OpenAlpr.
+ *
+ * OpenAlpr is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License
+ * version 3 as published by the Free Software Foundation
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+*/
+
+
+#ifndef ALPRIMPL_H
+#define ALPRIMPL_H
+
+#include "alpr.h"
+#include "config.h"
+
+#include "regiondetector.h"
+#include "licenseplatecandidate.h"
+#include "stateidentifier.h"
+#include "charactersegmenter.h"
+#include "ocr.h"
+
+#include "cjson.h"
+
+#include
+
+
+#define DEFAULT_TOPN 25
+#define DEFAULT_DETECT_REGION false
+
+class AlprImpl
+{
+
+ public:
+ AlprImpl(const std::string country, const std::string runtimeDir = "");
+ virtual ~AlprImpl();
+
+ std::vector recognize(cv::Mat img);
+
+ void applyRegionTemplate(AlprResult* result, std::string region);
+
+ void setDetectRegion(bool detectRegion);
+ void setTopN(int topn);
+ void setDefaultRegion(string region);
+
+ std::string toJson(const vector results);
+
+ Config* config;
+
+ private:
+
+ RegionDetector* plateDetector;
+ StateIdentifier* stateIdentifier;
+ OCR* ocr;
+
+ int topN;
+ bool detectRegion;
+ std::string defaultRegion;
+
+ cJSON* createJsonObj(const AlprResult* result);
+};
+
+#endif // ALPRIMPL_H
\ No newline at end of file
diff --git a/src/openalpr/binarize_wolf.cpp b/src/openalpr/binarize_wolf.cpp
new file mode 100644
index 0000000..198aad1
--- /dev/null
+++ b/src/openalpr/binarize_wolf.cpp
@@ -0,0 +1,372 @@
+/*
+ * Copyright (c) 2013 New Designs Unlimited, LLC
+ * Opensource Automated License Plate Recognition [http://www.openalpr.com]
+ *
+ * This file is part of OpenAlpr.
+ *
+ * OpenAlpr is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License
+ * version 3 as published by the Free Software Foundation
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+*/
+
+/**************************************************************
+ * Binarization with several methods
+ * (0) Niblacks method
+ * (1) Sauvola & Co.
+ * ICDAR 1997, pp 147-152
+ * (2) by myself - Christian Wolf
+ * Research notebook 19.4.2001, page 129
+ * (3) by myself - Christian Wolf
+ * 20.4.2007
+ *
+ * See also:
+ * Research notebook 24.4.2001, page 132 (Calculation of s)
+ **************************************************************/
+
+#include "binarize_wolf.h"
+
+
+// *************************************************************
+// glide a window across the image and
+// create two maps: mean and standard deviation.
+// *************************************************************
+
+
+float calcLocalStats (Mat &im, Mat &map_m, Mat &map_s, int winx, int winy) {
+
+ float m,s,max_s, sum, sum_sq, foo;
+ int wxh = winx/2;
+ int wyh = winy/2;
+ int x_firstth= wxh;
+ int y_lastth = im.rows-wyh-1;
+ int y_firstth= wyh;
+ float winarea = winx*winy;
+
+ max_s = 0;
+ for (int j = y_firstth ; j<=y_lastth; j++)
+ {
+ // Calculate the initial window at the beginning of the line
+ sum = sum_sq = 0;
+ for (int wy=0 ; wy max_s)
+ max_s = s;
+ map_m.fset(x_firstth, j, m);
+ map_s.fset(x_firstth, j, s);
+
+ // Shift the window, add and remove new/old values to the histogram
+ for (int i=1 ; i <= im.cols-winx; i++) {
+
+ // Remove the left old column and add the right new column
+ for (int wy=0; wy max_s)
+ max_s = s;
+ map_m.fset(i+wxh, j, m);
+ map_s.fset(i+wxh, j, s);
+ }
+ }
+
+ return max_s;
+}
+
+
+/**********************************************************
+ * The binarization routine
+ **********************************************************/
+
+
+void NiblackSauvolaWolfJolion (Mat im, Mat output, NiblackVersion version,
+ int winx, int winy, float k) {
+
+ float dR = BINARIZEWOLF_DEFAULTDR;
+
+ float m, s, max_s;
+ float 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 float 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++) {
+
+ // NORMAL, NON-BORDER AREA IN THE MIDDLE OF THE WINDOW:
+ for (int i=0 ; i <= im.cols-winx; i++) {
+
+ m = map_m.fget(i+wxh, j);
+ s = map_s.fget(i+wxh, j);
+
+ // 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);
+
+ if (i==0) {
+ // LEFT BORDER
+ for (int i=0; i<=x_firstth; ++i)
+ thsurf.fset(i,j,th);
+
+ // LEFT-UPPER CORNER
+ if (j==y_firstth)
+ for (int u=0; u= thsurf.fget(x,y))
+ {
+ output.uset(x,y,255);
+ }
+ else
+ {
+ output.uset(x,y,0);
+ }
+ }
+}
+
+/**********************************************************
+ * The main function
+ **********************************************************/
+
+
+/**********************************************************
+ * 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";
+}
+
+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);
+ }
+
+
+ // 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";
+ }
+
+ // Threshold
+ Mat output (input.rows, input.cols, CV_8U);
+ NiblackSauvolaWolfJolion (input, output, versionCode, winx, winy, optK);
+
+ // Write the tresholded file
+ cerr << "Writing binarized image to file '" << outputname << "'.\n";
+ imwrite (outputname, output);
+
+ return 0;
+}
+*/
\ No newline at end of file
diff --git a/src/openalpr/binarize_wolf.h b/src/openalpr/binarize_wolf.h
new file mode 100644
index 0000000..93604f4
--- /dev/null
+++ b/src/openalpr/binarize_wolf.h
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2013 New Designs Unlimited, LLC
+ * Opensource Automated License Plate Recognition [http://www.openalpr.com]
+ *
+ * This file is part of OpenAlpr.
+ *
+ * OpenAlpr is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License
+ * version 3 as published by the Free Software Foundation
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+*/
+
+
+#ifndef BINARIZEWOLF_H
+#define BINARIZEWOLF_H
+
+#include "support/filesystem.h"
+
+#include
+#include
+#include
+#include
+
+using namespace std;
+using namespace cv;
+
+enum NiblackVersion
+{
+ NIBLACK=0,
+ SAUVOLA,
+ WOLFJOLION,
+};
+
+#define BINARIZEWOLF_VERSION "2.3 (February 26th, 2013)"
+#define BINARIZEWOLF_DEFAULTDR 128
+
+
+#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;
+
+
+
+void NiblackSauvolaWolfJolion (Mat im, Mat output, NiblackVersion version,
+ int winx, int winy, float k);
+
+
+
+#endif // BINARIZEWOLF_H
\ No newline at end of file
diff --git a/src/openalpr/characteranalysis.cpp b/src/openalpr/characteranalysis.cpp
new file mode 100644
index 0000000..60106ab
--- /dev/null
+++ b/src/openalpr/characteranalysis.cpp
@@ -0,0 +1,1040 @@
+/*
+ * Copyright (c) 2013 New Designs Unlimited, LLC
+ * Opensource Automated License Plate Recognition [http://www.openalpr.com]
+ *
+ * This file is part of OpenAlpr.
+ *
+ * OpenAlpr is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License
+ * version 3 as published by the Free Software Foundation
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+*/
+
+#include "characteranalysis.h"
+#include
+
+CharacterAnalysis::CharacterAnalysis(Mat img, Config* config)
+{
+
+ this->config = config;
+
+ this->hasPlateMask = false;
+
+ if (this->config->debugCharAnalysis)
+ cout << "Starting CharacterAnalysis identification" << endl;
+
+
+ if (img.type() != CV_8U)
+ cvtColor( img, this->img_gray, CV_BGR2GRAY );
+ else
+ {
+ img_gray = Mat(img.size(), img.type());
+ img.copyTo(img_gray);
+ }
+
+}
+
+CharacterAnalysis::~CharacterAnalysis()
+{
+ for (int i = 0; i < thresholds.size(); i++)
+ {
+ thresholds[i].release();
+ }
+ thresholds.clear();
+}
+
+
+
+void CharacterAnalysis::analyze()
+{
+
+
+ thresholds = produceThresholds(img_gray, config);
+
+
+ /*
+ // Morph Close the gray image to make it easier to detect blobs
+ int morph_elem = 1;
+ int morph_size = 1;
+ Mat element = getStructuringElement( morph_elem, Size( 2*morph_size + 1, 2*morph_size+1 ), Point( morph_size, morph_size ) );
+
+ for (int i = 0; i < thresholds.size(); i++)
+ {
+ //morphologyEx( mask, mask, MORPH_CLOSE, element );
+ morphologyEx( thresholds[i], thresholds[i], MORPH_OPEN, element );
+ //dilate( thresholds[i], thresholds[i], element );
+
+ }
+ */
+
+
+ timespec startTime;
+ getTime(&startTime);
+
+
+ for (int i = 0; i < thresholds.size(); i++)
+ {
+ vector > contours;
+ vector hierarchy;
+
+ Mat tempThreshold(thresholds[i].size(), CV_8U);
+ thresholds[i].copyTo(tempThreshold);
+ findContours(tempThreshold,
+ contours, // a vector of contours
+ hierarchy,
+ CV_RETR_TREE, // retrieve all contours
+ CV_CHAIN_APPROX_SIMPLE ); // all pixels of each contours
+
+ allContours.push_back(contours);
+ allHierarchy.push_back(hierarchy);
+ }
+
+
+
+
+ if (config->debugTiming)
+ {
+ timespec endTime;
+ getTime(&endTime);
+ cout << " -- Character Analysis Find Contours Time: " << diffclock(startTime, endTime) << "ms." << endl;
+ }
+ //Mat img_equalized = equalizeBrightness(img_gray);
+
+
+ getTime(&startTime);
+
+ for (int i = 0; i < thresholds.size(); i++)
+ {
+ vector goodIndices = this->filter(thresholds[i], allContours[i], allHierarchy[i]);
+ charSegments.push_back(goodIndices);
+
+ if (config->debugCharAnalysis)
+ cout << "Threshold " << i << " had " << getGoodIndicesCount(goodIndices) << " good indices." << endl;
+ }
+
+ if (config->debugTiming)
+ {
+ timespec endTime;
+ getTime(&endTime);
+ cout << " -- Character Analysis Filter Time: " << diffclock(startTime, endTime) << "ms." << endl;
+ }
+
+
+
+ this->plateMask = findOuterBoxMask();
+
+ if (hasPlateMask)
+ {
+ // Filter out bad contours now that we have an outer box mask...
+ for (int i = 0; i < thresholds.size(); i++)
+ {
+ charSegments[i] = filterByOuterMask(allContours[i], allHierarchy[i], charSegments[i]);
+ }
+ }
+
+
+ int bestFitScore = -1;
+ int bestFitIndex = -1;
+ for (int i = 0; i < thresholds.size(); i++)
+ {
+
+ //vector goodIndices = this->filter(thresholds[i], allContours[i], allHierarchy[i]);
+ //charSegments.push_back(goodIndices);
+
+ int segmentCount = getGoodIndicesCount(charSegments[i]);
+
+
+ if (segmentCount > bestFitScore)
+ {
+ bestFitScore = segmentCount;
+ bestFitIndex = i;
+ bestCharSegments = charSegments[i];
+ bestThreshold = thresholds[i];
+ bestContours = allContours[i];
+ bestHierarchy = allHierarchy[i];
+ bestCharSegmentsCount = segmentCount;
+ }
+ }
+
+ if (this->config->debugCharAnalysis)
+ cout << "Best fit score: " << bestFitScore << " Index: " << bestFitIndex << endl;
+
+
+ if (bestFitScore <= 1)
+ return;
+
+
+ //getColorMask(img, allContours, allHierarchy, charSegments);
+
+ if (this->config->debugCharAnalysis)
+ {
+
+ Mat img_contours(bestThreshold.size(), CV_8U);
+ bestThreshold.copyTo(img_contours);
+ cvtColor(img_contours, img_contours, CV_GRAY2RGB);
+
+ vector > allowedContours;
+ for (int i = 0; i < bestContours.size(); i++)
+ {
+ if (bestCharSegments[i])
+ allowedContours.push_back(bestContours[i]);
+ }
+
+ drawContours(img_contours, bestContours,
+ -1, // draw all contours
+ cv::Scalar(255,0,0), // in blue
+ 1); // with a thickness of 1
+
+ drawContours(img_contours, allowedContours,
+ -1, // draw all contours
+ cv::Scalar(0,255,0), // in green
+ 1); // with a thickness of 1
+
+
+ displayImage(config, "Matching Contours", img_contours);
+ }
+
+
+ //charsegments = this->getPossibleCharRegions(img_threshold, allContours, allHierarchy, STARTING_MIN_HEIGHT + (bestFitIndex * HEIGHT_STEP), STARTING_MAX_HEIGHT + (bestFitIndex * HEIGHT_STEP));
+
+
+
+ this->linePolygon = getBestVotedLines(img_gray, bestContours, bestCharSegments);
+
+ if (this->linePolygon.size() > 0)
+ {
+ this->topLine = LineSegment(this->linePolygon[0].x, this->linePolygon[0].y, this->linePolygon[1].x, this->linePolygon[1].y);
+ this->bottomLine = LineSegment(this->linePolygon[3].x, this->linePolygon[3].y, this->linePolygon[2].x, this->linePolygon[2].y);
+ //this->charArea = getCharSegmentsBetweenLines(bestThreshold, bestContours, this->linePolygon);
+ filterBetweenLines(bestThreshold, bestContours, bestHierarchy, linePolygon, bestCharSegments);
+
+ this->charArea = getCharArea();
+
+ if (this->charArea.size() > 0)
+ {
+ this->charBoxTop = LineSegment(this->charArea[0].x, this->charArea[0].y, this->charArea[1].x, this->charArea[1].y);
+ this->charBoxBottom = LineSegment(this->charArea[3].x, this->charArea[3].y, this->charArea[2].x, this->charArea[2].y);
+ this->charBoxLeft = LineSegment(this->charArea[3].x, this->charArea[3].y, this->charArea[0].x, this->charArea[0].y);
+ this->charBoxRight = LineSegment(this->charArea[2].x, this->charArea[2].y, this->charArea[1].x, this->charArea[1].y);
+
+
+ }
+ }
+
+ this->thresholdsInverted = isPlateInverted();
+
+
+
+}
+
+int CharacterAnalysis::getGoodIndicesCount(vector goodIndices)
+{
+ int count = 0;
+ for (int i = 0; i < goodIndices.size(); i++)
+ {
+ if (goodIndices[i])
+ count++;
+ }
+
+ return count;
+}
+
+
+
+Mat CharacterAnalysis::findOuterBoxMask()
+{
+ double min_parent_area = config->templateHeightPx * config->templateWidthPx * 0.10; // Needs to be at least 10% of the plate area to be considered.
+
+ int winningIndex = -1;
+ int winningParentId = -1;
+ int bestCharCount = 0;
+ double lowestArea = 99999999999999;
+
+
+ if (this->config->debugCharAnalysis)
+ cout << "CharacterAnalysis::findOuterBoxMask" << endl;
+
+ for (int imgIndex = 0; imgIndex < allContours.size(); imgIndex++)
+ {
+ //vector charContours = filter(thresholds[imgIndex], allContours[imgIndex], allHierarchy[imgIndex]);
+
+ int charsRecognized = 0;
+ int parentId = -1;
+ bool hasParent = false;
+ for (int i = 0; i < charSegments[imgIndex].size(); i++)
+ {
+ if (charSegments[imgIndex][i]) charsRecognized++;
+ if (charSegments[imgIndex][i] && allHierarchy[imgIndex][i][3] != -1)
+ {
+ parentId = allHierarchy[imgIndex][i][3];
+ hasParent = true;
+ }
+ }
+
+ if (charsRecognized == 0)
+ continue;
+
+ if (hasParent)
+ {
+ double boxArea = contourArea(allContours[imgIndex][parentId]);
+ if (boxArea < min_parent_area)
+ continue;
+
+ if ((charsRecognized > bestCharCount) ||
+ (charsRecognized == bestCharCount && boxArea < lowestArea))
+ //(boxArea < lowestArea)
+ {
+ bestCharCount = charsRecognized;
+ winningIndex = imgIndex;
+ winningParentId = parentId;
+ lowestArea = boxArea;
+ }
+ }
+
+
+ }
+
+ if (this->config->debugCharAnalysis)
+ cout << "Winning image index is: " << winningIndex << endl;
+
+
+
+
+ if (winningIndex != -1 && bestCharCount >= 3)
+ {
+ int longestChildIndex = -1;
+ double longestChildLength = 0;
+ // Find the child with the longest permiter/arc length ( just for kicks)
+ for (int i = 0; i < allContours[winningIndex].size(); i++)
+ {
+ for (int j = 0; j < allContours[winningIndex].size(); j++)
+ {
+ if (allHierarchy[winningIndex][j][3] == winningParentId)
+ {
+ double arclength = arcLength(allContours[winningIndex][j], false);
+ if (arclength > longestChildLength)
+ {
+ longestChildIndex = j;
+ longestChildLength = arclength;
+ }
+ }
+ }
+ }
+
+
+
+
+
+ Mat mask = Mat::zeros(thresholds[winningIndex].size(), CV_8U);
+
+ // get rid of the outline by drawing a 1 pixel width black line
+ drawContours(mask, allContours[winningIndex],
+ winningParentId, // draw this contour
+ cv::Scalar(255,255,255), // in
+ CV_FILLED,
+ 8,
+ allHierarchy[winningIndex],
+ 0
+ );
+
+
+ // Morph Open the mask to get rid of any little connectors to non-plate portions
+ int morph_elem = 2;
+ int morph_size = 3;
+ Mat element = getStructuringElement( morph_elem, Size( 2*morph_size + 1, 2*morph_size+1 ), Point( morph_size, morph_size ) );
+
+ //morphologyEx( mask, mask, MORPH_CLOSE, element );
+ morphologyEx( mask, mask, MORPH_OPEN, element );
+
+ //morph_size = 1;
+ //element = getStructuringElement( morph_elem, Size( 2*morph_size + 1, 2*morph_size+1 ), Point( morph_size, morph_size ) );
+ //dilate(mask, mask, element);
+
+
+ // Drawing the edge black effectively erodes the image. This may clip off some extra junk from the edges.
+ // We'll want to do the contour again and find the larges one so that we remove the clipped portion.
+
+ vector > contoursSecondRound;
+
+ findContours(mask, contoursSecondRound, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE);
+ int biggestContourIndex = -1;
+ double largestArea = 0;
+ for (int c = 0; c < contoursSecondRound.size(); c++)
+ {
+ double area = contourArea(contoursSecondRound[c]);
+ if (area > largestArea)
+ {
+ biggestContourIndex = c;
+ largestArea = area;
+ }
+ }
+
+ if (biggestContourIndex != -1)
+ {
+ mask = Mat::zeros(thresholds[winningIndex].size(), CV_8U);
+
+ vector smoothedMaskPoints;
+ approxPolyDP(contoursSecondRound[biggestContourIndex], smoothedMaskPoints, 2, true);
+
+ vector > tempvec;
+ tempvec.push_back(smoothedMaskPoints);
+ //fillPoly(mask, smoothedMaskPoints.data(), smoothedMaskPoints, Scalar(255,255,255));
+ drawContours(mask, tempvec,
+ 0, // draw this contour
+ cv::Scalar(255,255,255), // in
+ CV_FILLED,
+ 8,
+ allHierarchy[winningIndex],
+ 0
+ );
+
+
+
+ }
+
+ if (this->config->debugCharAnalysis)
+ {
+ vector debugImgs;
+ Mat debugImgMasked = Mat::zeros(thresholds[winningIndex].size(), CV_8U);
+
+ thresholds[winningIndex].copyTo(debugImgMasked, mask);
+
+ debugImgs.push_back(mask);
+ debugImgs.push_back(thresholds[winningIndex]);
+ debugImgs.push_back(debugImgMasked);
+
+ Mat dashboard = drawImageDashboard(debugImgs, CV_8U, 1);
+ displayImage(config, "Winning outer box", dashboard);
+ }
+
+ hasPlateMask = true;
+ return mask;
+ }
+
+ hasPlateMask = false;
+ Mat fullMask = Mat::zeros(thresholds[0].size(), CV_8U);
+ bitwise_not(fullMask, fullMask);
+ return fullMask;
+
+
+
+}
+
+
+Mat CharacterAnalysis::getCharacterMask()
+{
+
+ Mat charMask = Mat::zeros(bestThreshold.size(), CV_8U);
+
+ for (int i = 0; i < bestContours.size(); i++)
+ {
+ if (bestCharSegments[i] == false)
+ continue;
+
+
+ drawContours(charMask, bestContours,
+ i, // draw this contour
+ cv::Scalar(255,255,255), // in
+ CV_FILLED,
+ 8,
+ bestHierarchy,
+ 1
+ );
+
+ // get rid of the outline by drawing a 1 pixel width black line
+ drawContours(charMask, bestContours,
+ i, // draw this contour
+ cv::Scalar(0,0,0), // in
+ 1,
+ 8,
+ bestHierarchy,
+ 1
+ );
+ }
+
+
+ return charMask;
+}
+
+
+
+// Returns a polygon "stripe" across the width of the character region. The lines are voted and the polygon starts at 0 and extends to image width
+vector CharacterAnalysis::getBestVotedLines(Mat img, vector > contours, vector goodIndices)
+{
+
+ //if (this->debug)
+ // cout << "CharacterAnalysis::getBestVotedLines" << endl;
+
+ vector bestStripe;
+
+ vector charRegions;
+
+ for (int i = 0; i < contours.size(); i++)
+ {
+ if (goodIndices[i])
+ charRegions.push_back(boundingRect(contours[i]));
+ }
+
+
+ // Find the best fit line segment that is parallel with the most char segments
+ if (charRegions.size() <= 1)
+ {
+ // Maybe do something about this later, for now let's just ignore
+ }
+ else
+ {
+ vector topLines;
+ vector bottomLines;
+ // Iterate through each possible char and find all possible lines for the top and bottom of each char segment
+ for (int i = 0; i < charRegions.size() - 1; i++)
+ {
+ for (int k = i+1; k < charRegions.size(); k++)
+ {
+ //Mat tempImg;
+ //result.copyTo(tempImg);
+
+
+ Rect* leftRect;
+ Rect* rightRect;
+ if (charRegions[i].x < charRegions[k].x)
+ {
+ leftRect = &charRegions[i];
+ rightRect = &charRegions[k];
+ }
+ else
+ {
+ leftRect = &charRegions[k];
+ rightRect = &charRegions[i];
+ }
+
+ //rectangle(tempImg, *leftRect, Scalar(0, 255, 0), 2);
+ //rectangle(tempImg, *rightRect, Scalar(255, 255, 255), 2);
+
+ int x1, y1, x2, y2;
+
+ if (leftRect->y > rightRect->y) // Rising line, use the top left corner of the rect
+ {
+ x1 = leftRect->x;
+ x2 = rightRect->x;
+ }
+ else // falling line, use the top right corner of the rect
+ {
+ x1 = leftRect->x + leftRect->width;
+ x2 = rightRect->x + rightRect->width;
+ }
+ y1 = leftRect->y;
+ y2 = rightRect->y;
+
+ //cv::line(tempImg, Point(x1, y1), Point(x2, y2), Scalar(0, 0, 255));
+ topLines.push_back(LineSegment(x1, y1, x2, y2));
+
+
+ if (leftRect->y > rightRect->y) // Rising line, use the bottom right corner of the rect
+ {
+ x1 = leftRect->x + leftRect->width;
+ x2 = rightRect->x + rightRect->width;
+ }
+ else // falling line, use the bottom left corner of the rect
+ {
+ x1 = leftRect->x;
+ x2 = rightRect->x;
+ }
+ y1 = leftRect->y + leftRect->height;
+ y2 = rightRect->y + leftRect->height;
+
+ //cv::line(tempImg, Point(x1, y1), Point(x2, y2), Scalar(0, 0, 255));
+ bottomLines.push_back(LineSegment(x1, y1, x2, y2));
+
+ //drawAndWait(&tempImg);
+ }
+ }
+
+ int bestScoreIndex = 0;
+ int bestScore = -1;
+ int bestScoreDistance = -1; // Line segment distance is used as a tie breaker
+
+ // Now, among all possible lines, find the one that is the best fit
+ for (int i = 0; i < topLines.size(); i++)
+ {
+ float SCORING_MIN_THRESHOLD = 0.97;
+ float SCORING_MAX_THRESHOLD = 1.03;
+
+
+ int curScore = 0;
+ for (int charidx = 0; charidx < charRegions.size(); charidx++)
+ {
+
+ float topYPos = topLines[i].getPointAt(charRegions[charidx].x);
+ float botYPos = bottomLines[i].getPointAt(charRegions[charidx].x);
+
+ float minTop = charRegions[charidx].y * SCORING_MIN_THRESHOLD;
+ float maxTop = charRegions[charidx].y * SCORING_MAX_THRESHOLD;
+ float minBot = (charRegions[charidx].y + charRegions[charidx].height) * SCORING_MIN_THRESHOLD;
+ float maxBot = (charRegions[charidx].y + charRegions[charidx].height) * SCORING_MAX_THRESHOLD;
+ if ( (topYPos >= minTop && topYPos <= maxTop) &&
+ (botYPos >= minBot && botYPos <= maxBot))
+ {
+ curScore++;
+ }
+
+ //cout << "Slope: " << topslope << " yPos: " << topYPos << endl;
+ //drawAndWait(&tempImg);
+
+ }
+
+
+ // Tie goes to the one with longer line segments
+ if ((curScore > bestScore) ||
+ (curScore == bestScore && topLines[i].length > bestScoreDistance))
+ {
+ bestScore = curScore;
+ bestScoreIndex = i;
+ // Just use x distance for now
+ bestScoreDistance = topLines[i].length;
+ }
+ }
+
+
+ if (this->config->debugCharAnalysis)
+ {
+ cout << "The winning score is: " << bestScore << endl;
+ // Draw the winning line segment
+ //Mat tempImg;
+ //result.copyTo(tempImg);
+ //cv::line(tempImg, topLines[bestScoreIndex].p1, topLines[bestScoreIndex].p2, Scalar(0, 0, 255), 2);
+ //cv::line(tempImg, bottomLines[bestScoreIndex].p1, bottomLines[bestScoreIndex].p2, Scalar(0, 0, 255), 2);
+
+ //displayImage(config, "Lines", tempImg);
+ }
+
+ //winningLines.push_back(topLines[bestScoreIndex]);
+ //winningLines.push_back(bottomLines[bestScoreIndex]);
+
+ Point topLeft = Point(0, topLines[bestScoreIndex].getPointAt(0) );
+ Point topRight = Point(img.cols, topLines[bestScoreIndex].getPointAt(img.cols));
+ Point bottomRight = Point(img.cols, bottomLines[bestScoreIndex].getPointAt(img.cols));
+ Point bottomLeft = Point(0, bottomLines[bestScoreIndex].getPointAt(0));
+
+
+ bestStripe.push_back(topLeft);
+ bestStripe.push_back(topRight);
+ bestStripe.push_back(bottomRight);
+ bestStripe.push_back(bottomLeft);
+
+
+ }
+
+
+ return bestStripe;
+}
+
+
+
+vector CharacterAnalysis::filter(Mat img, vector > contours, vector hierarchy)
+{
+ static int STARTING_MIN_HEIGHT = round (((float) img.rows) * config->charAnalysisMinPercent);
+ static int STARTING_MAX_HEIGHT = round (((float) img.rows) * (config->charAnalysisMinPercent + config->charAnalysisHeightRange));
+ static int HEIGHT_STEP = round (((float) img.rows) * config->charAnalysisHeightStepSize);
+ static int NUM_STEPS = config->charAnalysisNumSteps;
+
+
+ vector charSegments;
+ int bestFitScore = -1;
+ for (int i = 0; i < NUM_STEPS; i++)
+ {
+ int goodIndicesCount;
+
+ vector goodIndices(contours.size());
+ for (int z = 0; z < goodIndices.size(); z++) goodIndices[z] = true;
+
+ goodIndices = this->filterByBoxSize(contours, goodIndices, STARTING_MIN_HEIGHT + (i * HEIGHT_STEP), STARTING_MAX_HEIGHT + (i * HEIGHT_STEP));
+
+
+ goodIndicesCount = getGoodIndicesCount(goodIndices);
+ if ( goodIndicesCount > 0 && goodIndicesCount <= bestFitScore) // Don't bother doing more filtering if we already lost...
+ continue;
+ goodIndices = this->filterContourHoles(contours, hierarchy, goodIndices);
+
+ goodIndicesCount = getGoodIndicesCount(goodIndices);
+ if ( goodIndicesCount > 0 && goodIndicesCount <= bestFitScore) // Don't bother doing more filtering if we already lost...
+ continue;
+ //goodIndices = this->filterByParentContour( contours, hierarchy, goodIndices);
+ vector lines = getBestVotedLines(img, contours, goodIndices);
+ goodIndices = this->filterBetweenLines(img, contours, hierarchy, lines, goodIndices);
+
+
+ int segmentCount = getGoodIndicesCount(goodIndices);
+
+ if (segmentCount > bestFitScore)
+ {
+ bestFitScore = segmentCount;
+ charSegments = goodIndices;
+ }
+ }
+
+
+ return charSegments;
+}
+
+
+// Goes through the contours for the plate and picks out possible char segments based on min/max height
+vector CharacterAnalysis::filterByBoxSize(vector< vector< Point> > contours, vector goodIndices, int minHeightPx, int maxHeightPx)
+{
+
+ float idealAspect=config->charWidthMM / config->charHeightMM;
+ float aspecttolerance=0.25;
+
+
+ vector includedIndices(contours.size());
+ for (int j = 0; j < contours.size(); j++)
+ includedIndices.push_back(false);
+
+ for (int i = 0; i < contours.size(); i++)
+ {
+ if (goodIndices[i] == false)
+ continue;
+
+ //Create bounding rect of object
+ Rect mr= boundingRect(contours[i]);
+
+ float minWidth = mr.height * 0.2;
+ //Crop image
+ //Mat auxRoi(img, mr);
+ if(mr.height >= minHeightPx && mr.height <= maxHeightPx && mr.width > minWidth){
+
+ float charAspect= (float)mr.width/(float)mr.height;
+
+ if (abs(charAspect - idealAspect) < aspecttolerance)
+ includedIndices[i] = true;
+ }
+ }
+
+ return includedIndices;
+
+}
+
+
+
+vector< bool > CharacterAnalysis::filterContourHoles(vector< vector< Point > > contours, vector< Vec4i > hierarchy, vector< bool > goodIndices)
+{
+
+ vector includedIndices(contours.size());
+ for (int j = 0; j < contours.size(); j++)
+ includedIndices.push_back(false);
+
+ for (int i = 0; i < contours.size(); i++)
+ {
+ if (goodIndices[i] == false)
+ continue;
+
+ int parentIndex = hierarchy[i][3];
+
+ if (parentIndex >= 0 && goodIndices[parentIndex] == true)
+ {
+ // this contour is a child of an already identified contour. REMOVE it
+ if (this->config->debugCharAnalysis)
+ {
+ cout << "filterContourHoles: contour index: " << i << endl;
+ }
+ }
+ else
+ {
+ includedIndices[i] = true;
+ }
+ }
+
+ return includedIndices;
+}
+
+
+// Goes through the contours for the plate and picks out possible char segments based on min/max height
+// returns a vector of indices corresponding to valid contours
+vector CharacterAnalysis::filterByParentContour( vector< vector< Point> > contours, vector hierarchy, vector goodIndices)
+{
+
+ vector includedIndices(contours.size());
+ for (int j = 0; j < contours.size(); j++)
+ includedIndices[j] = false;
+
+ vector parentIDs;
+ vector votes;
+
+ for (int i = 0; i < contours.size(); i++)
+ {
+ if (goodIndices[i] == false)
+ continue;
+
+ int voteIndex = -1;
+ int parentID = hierarchy[i][3];
+ // check if parentID is already in the lsit
+ for (int j = 0; j < parentIDs.size(); j++)
+ {
+ if (parentIDs[j] == parentID)
+ {
+ voteIndex = j;
+ break;
+ }
+ }
+ if (voteIndex == -1)
+ {
+ parentIDs.push_back(parentID);
+ votes.push_back(1);
+ }
+ else
+ {
+ votes[voteIndex] = votes[voteIndex] + 1;
+ }
+
+ }
+
+ // Tally up the votes, pick the winner
+ int totalVotes = 0;
+ int winningParentId = 0;
+ int highestVotes = 0;
+ for (int i = 0; i < parentIDs.size(); i++)
+ {
+ if (votes[i] > highestVotes)
+ {
+ winningParentId = parentIDs[i];
+ highestVotes = votes[i];
+ }
+ totalVotes += votes[i];
+ }
+
+ // Now filter out all the contours with a different parent ID (assuming the totalVotes > 2)
+ for (int i = 0; i < contours.size(); i++)
+ {
+ if (goodIndices[i] == false)
+ continue;
+
+ if (totalVotes <= 2)
+ {
+ includedIndices[i] = true;
+ }
+ else if (hierarchy[i][3] == winningParentId)
+ {
+ includedIndices[i] = true;
+ }
+ }
+
+ return includedIndices;
+}
+
+
+vector CharacterAnalysis::filterBetweenLines(Mat img, vector > contours, vector hierarchy, vector outerPolygon, vector goodIndices)
+{
+ static float MIN_AREA_PERCENT_WITHIN_LINES = 0.88;
+
+ vector includedIndices(contours.size());
+ for (int j = 0; j < contours.size(); j++)
+ includedIndices[j] = false;
+
+
+ if (outerPolygon.size() == 0)
+ return includedIndices;
+
+ vector validPoints;
+
+ // Figure out the line height
+ LineSegment topLine(outerPolygon[0].x, outerPolygon[0].y, outerPolygon[1].x, outerPolygon[1].y);
+ LineSegment bottomLine(outerPolygon[3].x, outerPolygon[3].y, outerPolygon[2].x, outerPolygon[2].y);
+
+ float x = ((float) img.cols) / 2;
+ Point midpoint = Point(x, bottomLine.getPointAt(x));
+ Point acrossFromMidpoint = topLine.closestPointOnSegmentTo(midpoint);
+ float lineHeight = distanceBetweenPoints(midpoint, acrossFromMidpoint);
+
+ // Create a white mask for the area inside the polygon
+ Mat outerMask = Mat::zeros(img.size(), CV_8U);
+ Mat innerArea = Mat::zeros(img.size(), CV_8U);
+ fillConvexPoly(outerMask, outerPolygon.data(), outerPolygon.size(), Scalar(255,255,255));
+
+
+ for (int i = 0; i < contours.size(); i++)
+ {
+ if (goodIndices[i] == false)
+ continue;
+
+ // get rid of the outline by drawing a 1 pixel width black line
+ drawContours(innerArea, contours,
+ i, // draw this contour
+ cv::Scalar(255,255,255), // in
+ CV_FILLED,
+ 8,
+ hierarchy,
+ 0
+ );
+
+
+ bitwise_and(innerArea, outerMask, innerArea);
+
+
+ vector > tempContours;
+ findContours(innerArea, tempContours,
+ CV_RETR_EXTERNAL, // retrieve the external contours
+ CV_CHAIN_APPROX_SIMPLE ); // all pixels of each contours );
+
+ double totalArea = contourArea(contours[i]);
+ double areaBetweenLines = 0;
+
+ for (int tempContourIdx = 0; tempContourIdx < tempContours.size(); tempContourIdx++)
+ {
+ areaBetweenLines += contourArea(tempContours[tempContourIdx]);
+
+ }
+
+
+ if (areaBetweenLines / totalArea >= MIN_AREA_PERCENT_WITHIN_LINES)
+ {
+ includedIndices[i] = true;
+ }
+
+ innerArea.setTo(Scalar(0,0,0));
+ }
+
+ return includedIndices;
+}
+
+std::vector< bool > CharacterAnalysis::filterByOuterMask(vector< vector< Point > > contours, vector< Vec4i > hierarchy, std::vector< bool > goodIndices)
+{
+ float MINIMUM_PERCENT_LEFT_AFTER_MASK = 0.1;
+ float MINIMUM_PERCENT_OF_CHARS_INSIDE_PLATE_MASK = 0.6;
+
+ if (hasPlateMask == false)
+ return goodIndices;
+
+
+ vector passingIndices;
+ for (int i = 0; i < goodIndices.size(); i++)
+ passingIndices.push_back(false);
+
+ Mat tempMaskedContour = Mat::zeros(plateMask.size(), CV_8U);
+ Mat tempFullContour = Mat::zeros(plateMask.size(), CV_8U);
+
+ int charsInsideMask = 0;
+ int totalChars = 0;
+
+ for (int i=0; i < goodIndices.size(); i++)
+ {
+ if (goodIndices[i] == false)
+ continue;
+
+ totalChars++;
+
+ drawContours(tempFullContour, contours, i, Scalar(255,255,255), CV_FILLED, 8, hierarchy);
+ bitwise_and(tempFullContour, plateMask, tempMaskedContour);
+
+ float beforeMaskWhiteness = mean(tempFullContour)[0];
+ float afterMaskWhiteness = mean(tempMaskedContour)[0];
+
+ if (afterMaskWhiteness / beforeMaskWhiteness > MINIMUM_PERCENT_LEFT_AFTER_MASK)
+ {
+ charsInsideMask++;
+ passingIndices[i] = true;
+ }
+ }
+
+ if (totalChars == 0)
+ return goodIndices;
+
+ // Check to make sure that this is a valid box. If the box is too small (e.g., 1 char is inside, and 3 are outside)
+ // then don't use this to filter.
+ float percentCharsInsideMask = ((float) charsInsideMask) / ((float) totalChars);
+ if (percentCharsInsideMask < MINIMUM_PERCENT_OF_CHARS_INSIDE_PLATE_MASK)
+ return goodIndices;
+
+ return passingIndices;
+}
+
+
+
+bool CharacterAnalysis::isPlateInverted()
+{
+
+ Mat charMask = getCharacterMask();
+
+ Scalar meanVal = mean(bestThreshold, charMask)[0];
+
+ if (this->config->debugCharAnalysis)
+ cout << "CharacterAnalysis, plate inverted: MEAN: " << meanVal << " : " << bestThreshold.type() << endl;
+
+
+ if (meanVal[0] < 100) // Half would be 122.5. Give it a little extra oomf before saying it needs inversion. Most states aren't inverted.
+ return true;
+
+ return false;
+}
+
+
+bool CharacterAnalysis::verifySize(Mat r, float minHeightPx, float maxHeightPx)
+{
+ //Char sizes 45x90
+ float aspect=config->charWidthMM / config->charHeightMM;
+ float charAspect= (float)r.cols/(float)r.rows;
+ float error=0.35;
+ //float minHeight=TEMPLATE_PLATE_HEIGHT * .35;
+ //float maxHeight=TEMPLATE_PLATE_HEIGHT * .65;
+ //We have a different aspect ratio for number 1, and it can be ~0.2
+ float minAspect=0.2;
+ float maxAspect=aspect+aspect*error;
+ //area of pixels
+ float area=countNonZero(r);
+ //bb area
+ float bbArea=r.cols*r.rows;
+ //% of pixel in area
+ float percPixels=area/bbArea;
+
+ //if(DEBUG)
+ //cout << "Aspect: "<< aspect << " ["<< minAspect << "," << maxAspect << "] " << "Area "<< percPixels <<" Char aspect " << charAspect << " Height char "<< r.rows << "\n";
+ if(percPixels < 0.8 && charAspect > minAspect && charAspect < maxAspect && r.rows >= minHeightPx && r.rows < maxHeightPx)
+ return true;
+ else
+ return false;
+
+}
+
+
+vector CharacterAnalysis::getCharArea()
+{
+ const int MAX = 100000;
+ const int MIN= -1;
+
+ int leftX = MAX;
+ int rightX = MIN;
+
+ for (int i = 0; i < bestContours.size(); i++)
+ {
+ if (bestCharSegments[i] == false)
+ continue;
+
+ for (int z = 0; z < bestContours[i].size(); z++)
+ {
+ if (bestContours[i][z].x < leftX)
+ leftX = bestContours[i][z].x;
+ if (bestContours[i][z].x > rightX)
+ rightX = bestContours[i][z].x;
+ }
+ }
+
+ vector charArea;
+ if (leftX != MAX && rightX != MIN)
+ {
+ Point tl(leftX, topLine.getPointAt(leftX));
+ Point tr(rightX, topLine.getPointAt(rightX));
+ Point br(rightX, bottomLine.getPointAt(rightX));
+ Point bl(leftX, bottomLine.getPointAt(leftX));
+ charArea.push_back(tl);
+ charArea.push_back(tr);
+ charArea.push_back(br);
+ charArea.push_back(bl);
+ }
+
+ return charArea;
+}
diff --git a/src/openalpr/characteranalysis.h b/src/openalpr/characteranalysis.h
new file mode 100644
index 0000000..5a0343d
--- /dev/null
+++ b/src/openalpr/characteranalysis.h
@@ -0,0 +1,104 @@
+/*
+ * Copyright (c) 2013 New Designs Unlimited, LLC
+ * Opensource Automated License Plate Recognition [http://www.openalpr.com]
+ *
+ * This file is part of OpenAlpr.
+ *
+ * OpenAlpr is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License
+ * version 3 as published by the Free Software Foundation
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+*/
+
+
+
+#ifndef CHARACTERANALYSIS_H
+#define CHARACTERANALYSIS_H
+
+#include "opencv2/highgui/highgui.hpp"
+#include "opencv2/imgproc/imgproc.hpp"
+#include "constants.h"
+#include "utility.h"
+#include "config.h"
+
+using namespace cv;
+using namespace std;
+
+
+class CharacterAnalysis
+{
+
+ public:
+ CharacterAnalysis(Mat img, Config* config);
+ virtual ~CharacterAnalysis();
+
+ bool hasPlateMask;
+ Mat plateMask;
+
+ Mat bestThreshold;
+ vector > bestContours;
+ vector bestHierarchy;
+ vector bestCharSegments;
+ int bestCharSegmentsCount;
+
+ LineSegment topLine;
+ LineSegment bottomLine;
+ vector linePolygon;
+ vector charArea;
+
+ LineSegment charBoxTop;
+ LineSegment charBoxBottom;
+ LineSegment charBoxLeft;
+ LineSegment charBoxRight;
+
+ bool thresholdsInverted;
+
+ vector thresholds;
+ vector > > allContours;
+ vector > allHierarchy;
+ vector > charSegments;
+
+ void analyze();
+
+ Mat getCharacterMask();
+
+
+
+ private:
+ Config* config;
+
+ Mat img_gray;
+
+ Mat findOuterBoxMask( );
+
+
+ bool isPlateInverted();
+ vector filter(Mat img, vector > contours, vector hierarchy);
+
+ vector filterByBoxSize(vector > contours, vector goodIndices, int minHeightPx, int maxHeightPx);
+ vector filterByParentContour( vector< vector< Point> > contours, vector hierarchy, vector goodIndices);
+ vector filterContourHoles(vector > contours, vector hierarchy, vector goodIndices);
+ vector filterByOuterMask(vector > contours, vector hierarchy, vector goodIndices);
+
+ vector getCharArea();
+ vector getBestVotedLines(Mat img, vector > contours, vector goodIndices);
+ //vector getCharSegmentsBetweenLines(Mat img, vector > contours, vector outerPolygon);
+ vector filterBetweenLines(Mat img, vector > contours, vector hierarchy, vector outerPolygon, vector goodIndices);
+
+ bool verifySize(Mat r, float minHeightPx, float maxHeightPx);
+
+ int getGoodIndicesCount(vector goodIndices);
+
+
+};
+
+#endif // CHARACTERANALYSIS_H
+
+
diff --git a/src/openalpr/characterregion.cpp b/src/openalpr/characterregion.cpp
new file mode 100644
index 0000000..70515c4
--- /dev/null
+++ b/src/openalpr/characterregion.cpp
@@ -0,0 +1,194 @@
+/*
+ * Copyright (c) 2013 New Designs Unlimited, LLC
+ * Opensource Automated License Plate Recognition [http://www.openalpr.com]
+ *
+ * This file is part of OpenAlpr.
+ *
+ * OpenAlpr is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License
+ * version 3 as published by the Free Software Foundation
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+*/
+
+#include "characterregion.h"
+//#include
+#include
+
+CharacterRegion::CharacterRegion(Mat img, Config* config)
+{
+ this->config = config;
+ this->debug = config->debugCharRegions;
+
+ this->confidence = 0;
+
+
+
+ if (this->debug)
+ cout << "Starting CharacterRegion identification" << endl;
+
+ timespec startTime;
+ getTime(&startTime);
+
+
+ charAnalysis = new CharacterAnalysis(img, config);
+ charAnalysis->analyze();
+
+
+ if (this->debug)
+ {
+ vector tempDash;
+ for (int z = 0; z < charAnalysis->thresholds.size(); z++)
+ {
+ Mat tmp(charAnalysis->thresholds[z].size(), charAnalysis->thresholds[z].type());
+ charAnalysis->thresholds[z].copyTo(tmp);
+ cvtColor(tmp, tmp, CV_GRAY2BGR);
+
+ tempDash.push_back(tmp);
+ }
+
+
+ Mat bestVal(charAnalysis->bestThreshold.size(), charAnalysis->bestThreshold.type());
+ charAnalysis->bestThreshold.copyTo(bestVal);
+ cvtColor(bestVal, bestVal, CV_GRAY2BGR);
+
+ for (int z = 0; z < charAnalysis->bestContours.size(); z++)
+ {
+ Scalar dcolor(255,0,0);
+ if (charAnalysis->bestCharSegments[z])
+ dcolor = Scalar(0,255,0);
+ drawContours(bestVal, charAnalysis->bestContours, z, dcolor, 1);
+ }
+ tempDash.push_back(bestVal);
+ displayImage(config, "Character Region Step 1 Thresholds", drawImageDashboard(tempDash, bestVal.type(), 3));
+ }
+
+
+
+ if (this->debug)
+ {
+ /*
+ Mat img_contours(img_threshold.size(), CV_8U);
+ img_threshold.copyTo(img_contours);
+ cvtColor(img_contours, img_contours, CV_GRAY2RGB);
+
+ vector > allowedContours;
+ for (int i = 0; i < contours.size(); i++)
+ {
+ if (charSegments[i])
+ allowedContours.push_back(contours[i]);
+ }
+
+ drawContours(img_contours, contours,
+ -1, // draw all contours
+ cv::Scalar(255,0,0), // in blue
+ 1); // with a thickness of 1
+
+ drawContours(img_contours, allowedContours,
+ -1, // draw all contours
+ cv::Scalar(0,255,0), // in green
+ 1); // with a thickness of 1
+
+
+ displayImage(config, "Matching Contours", img_contours);
+ */
+ }
+
+
+ //charsegments = this->getPossibleCharRegions(img_threshold, allContours, allHierarchy, STARTING_MIN_HEIGHT + (bestFitIndex * HEIGHT_STEP), STARTING_MAX_HEIGHT + (bestFitIndex * HEIGHT_STEP));
+
+
+ if (charAnalysis->linePolygon.size() > 0)
+ {
+
+ int confidenceDrainers = 0;
+ int charSegmentCount = charAnalysis->bestCharSegmentsCount;
+ if (charSegmentCount == 1)
+ confidenceDrainers += 91;
+ else if (charSegmentCount < 5)
+ confidenceDrainers += (5 - charSegmentCount) * 10;
+
+ int absangle = abs(charAnalysis->topLine.angle);
+ if (absangle > 10)
+ confidenceDrainers += 91;
+ else if (absangle > 1)
+ confidenceDrainers += (10 - absangle) * 5;
+
+
+ if (confidenceDrainers >= 100)
+ this->confidence=1;
+ else
+ this->confidence = 100 - confidenceDrainers;
+
+ }
+
+
+
+ if (config->debugTiming)
+ {
+ timespec endTime;
+ getTime(&endTime);
+ cout << "Character Region Time: " << diffclock(startTime, endTime) << "ms." << endl;
+ }
+
+}
+
+CharacterRegion::~CharacterRegion()
+{
+ delete(charAnalysis);
+}
+
+
+
+Mat CharacterRegion::getPlateMask()
+{
+ return charAnalysis->plateMask;
+}
+
+LineSegment CharacterRegion::getTopLine()
+{
+ return charAnalysis->topLine;
+}
+
+LineSegment CharacterRegion::getBottomLine()
+{
+ return charAnalysis->bottomLine;
+}
+
+vector CharacterRegion::getCharArea()
+{
+ return charAnalysis->charArea;
+}
+
+LineSegment CharacterRegion::getCharBoxTop()
+{
+ return charAnalysis->charBoxTop;
+}
+
+LineSegment CharacterRegion::getCharBoxBottom()
+{
+ return charAnalysis->charBoxBottom;
+}
+
+LineSegment CharacterRegion::getCharBoxLeft()
+{
+ return charAnalysis->charBoxLeft;
+}
+
+LineSegment CharacterRegion::getCharBoxRight()
+{
+ return charAnalysis->charBoxRight;
+}
+
+bool CharacterRegion::thresholdsInverted()
+{
+ return charAnalysis->thresholdsInverted;
+}
+
+
diff --git a/src/openalpr/characterregion.h b/src/openalpr/characterregion.h
new file mode 100644
index 0000000..450108d
--- /dev/null
+++ b/src/openalpr/characterregion.h
@@ -0,0 +1,87 @@
+/*
+ * Copyright (c) 2013 New Designs Unlimited, LLC
+ * Opensource Automated License Plate Recognition [http://www.openalpr.com]
+ *
+ * This file is part of OpenAlpr.
+ *
+ * OpenAlpr is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License
+ * version 3 as published by the Free Software Foundation
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+*/
+
+
+
+#ifndef CHARACTERREGION_H
+#define CHARACTERREGION_H
+
+#include "opencv2/highgui/highgui.hpp"
+#include "opencv2/imgproc/imgproc.hpp"
+#include "constants.h"
+#include "utility.h"
+#include "characteranalysis.h"
+#include "config.h"
+
+using namespace cv;
+using namespace std;
+
+
+class CharacterRegion
+{
+
+ public:
+ CharacterRegion(Mat img, Config* config);
+ virtual ~CharacterRegion();
+
+ CharacterAnalysis *charAnalysis;
+
+ int confidence;
+ Mat getPlateMask();
+
+ LineSegment getTopLine();
+ LineSegment getBottomLine();
+ //vector getLinePolygon();
+ vector getCharArea();
+
+ LineSegment getCharBoxTop();
+ LineSegment getCharBoxBottom();
+ LineSegment getCharBoxLeft();
+ LineSegment getCharBoxRight();
+
+ bool thresholdsInverted();
+
+ protected:
+ Config* config;
+ bool debug;
+
+ Mat findOuterBoxMask(vector thresholds, vector > > allContours, vector > allHierarchy);
+
+ vector filter(Mat img, vector > contours, vector hierarchy);
+ vector filterByBoxSize(Mat img, vector > contours, vector goodIndices, float minHeightPx, float maxHeightPx);
+ vector filterByParentContour( vector< vector< Point> > contours, vector hierarchy, vector goodIndices);
+ vector filterContourHoles(vector > contours, vector hierarchy, vector goodIndices);
+
+ vector getBestVotedLines(Mat img, vector > contours, vector goodIndices);
+ //vector getCharSegmentsBetweenLines(Mat img, vector > contours, vector outerPolygon);
+ vector filterBetweenLines(Mat img, vector > contours, vector hierarchy, vector outerPolygon, vector goodIndices);
+ Mat getCharacterMask(Mat img, vector > contours, vector hierarchy, vector goodIndices);
+
+ vector wrapContours(vector > contours);
+ bool verifySize(Mat r, float minHeightPx, float maxHeightPx);
+
+ int getGoodIndicesCount(vector goodIndices);
+
+ bool isPlateInverted(Mat threshold, vector > contours, vector hierarchy, vector goodIndices);
+
+};
+
+#endif // CHARACTERREGION_H
+
+
diff --git a/src/openalpr/charactersegmenter.cpp b/src/openalpr/charactersegmenter.cpp
new file mode 100644
index 0000000..30292ed
--- /dev/null
+++ b/src/openalpr/charactersegmenter.cpp
@@ -0,0 +1,1188 @@
+/*
+ * Copyright (c) 2013 New Designs Unlimited, LLC
+ * Opensource Automated License Plate Recognition [http://www.openalpr.com]
+ *
+ * This file is part of OpenAlpr.
+ *
+ * OpenAlpr is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License
+ * version 3 as published by the Free Software Foundation
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+*/
+
+#include "charactersegmenter.h"
+
+CharacterSegmenter::CharacterSegmenter(Mat img, bool invertedColors, Config* config)
+{
+
+ this->config = config;
+
+ this->confidence = 0;
+
+ if (this->config->debugCharSegmenter)
+ cout << "Starting CharacterSegmenter" << endl;
+
+ //CharacterRegion charRegion(img, debug);
+
+ timespec startTime;
+ getTime(&startTime);
+
+
+ Mat img_gray(img.size(), CV_8U);
+ cvtColor( img, img_gray, CV_BGR2GRAY );
+ //normalize(img_gray, img_gray, 0, 255, CV_MINMAX );
+ medianBlur(img_gray, img_gray, 3);
+
+ if (invertedColors)
+ bitwise_not(img_gray, img_gray);
+
+
+ charAnalysis = new CharacterAnalysis(img_gray, config);
+ charAnalysis->analyze();
+
+
+
+ if (this->config->debugCharSegmenter)
+ {
+ displayImage(config, "CharacterSegmenter Thresholds", drawImageDashboard(charAnalysis->thresholds, CV_8U, 3));
+ }
+
+
+
+
+
+
+ if (this->config->debugCharSegmenter)
+ {
+
+ Mat img_contours(charAnalysis->bestThreshold.size(), CV_8U);
+ charAnalysis->bestThreshold.copyTo(img_contours);
+ cvtColor(img_contours, img_contours, CV_GRAY2RGB);
+
+ vector > allowedContours;
+ for (int i = 0; i < charAnalysis->bestContours.size(); i++)
+ {
+ if (charAnalysis->bestCharSegments[i])
+ allowedContours.push_back(charAnalysis->bestContours[i]);
+ }
+
+ drawContours(img_contours, charAnalysis->bestContours,
+ -1, // draw all contours
+ cv::Scalar(255,0,0), // in blue
+ 1); // with a thickness of 1
+
+ drawContours(img_contours, allowedContours,
+ -1, // draw all contours
+ cv::Scalar(0,255,0), // in green
+ 1); // with a thickness of 1
+
+ if (charAnalysis->linePolygon.size() > 0)
+ {
+ line(img_contours, charAnalysis->linePolygon[0], charAnalysis->linePolygon[1], Scalar(255, 0, 255), 1);
+ line(img_contours, charAnalysis->linePolygon[3], charAnalysis->linePolygon[2], Scalar(255, 0, 255), 1);
+ }
+
+ Mat bordered = addLabel(img_contours, "Best Contours");
+ imgDbgGeneral.push_back(bordered);
+ }
+
+ // Figure out the average character width
+ float totalCharWidth = 0;
+ float totalCharHeight = 0;
+
+ if (charAnalysis->linePolygon.size() > 0)
+ {
+ this->top = LineSegment(charAnalysis->linePolygon[0].x, charAnalysis->linePolygon[0].y, charAnalysis->linePolygon[1].x, charAnalysis->linePolygon[1].y);
+ this->bottom = LineSegment(charAnalysis->linePolygon[3].x, charAnalysis->linePolygon[3].y, charAnalysis->linePolygon[2].x, charAnalysis->linePolygon[2].y);
+
+ for (int i = 0; i < charAnalysis->bestContours.size(); i++)
+ {
+ if (charAnalysis->bestCharSegments[i] == false)
+ continue;
+
+ Rect mr = boundingRect(charAnalysis->bestContours[i]);
+ totalCharWidth += mr.width;
+ totalCharHeight += mr.height;
+ }
+
+ int numSamples = charAnalysis->bestCharSegmentsCount;
+ float avgCharWidth = totalCharWidth / numSamples;
+ float avgCharHeight = totalCharHeight / numSamples;
+
+ removeSmallContours(charAnalysis->thresholds, charAnalysis->allContours, avgCharWidth, avgCharHeight);
+
+ // Do the histogram analysis to figure out char regions
+
+
+ timespec startTime;
+ getTime(&startTime);
+
+
+ vector allHistograms;
+
+ vector allBoxes;
+ for (int i = 0; i < charAnalysis->allContours.size(); i++)
+ {
+
+
+ Mat histogramMask = Mat::zeros(charAnalysis->thresholds[i].size(), CV_8U);
+
+ fillConvexPoly(histogramMask, charAnalysis->linePolygon.data(), charAnalysis->linePolygon.size(), Scalar(255,255,255));
+
+
+ VerticalHistogram vertHistogram(charAnalysis->thresholds[i], histogramMask);
+
+
+ if (this->config->debugCharSegmenter)
+ {
+ Mat histoCopy(vertHistogram.debugImg.size(), vertHistogram.debugImg.type());
+ //vertHistogram.copyTo(histoCopy);
+ cvtColor(vertHistogram.debugImg, histoCopy, CV_GRAY2RGB);
+ allHistograms.push_back(histoCopy);
+ }
+
+//
+ float score = 0;
+ vector charBoxes = getHistogramBoxes(vertHistogram.debugImg, avgCharWidth, avgCharHeight, &score);
+
+
+ if (this->config->debugCharSegmenter)
+ {
+ for (int cboxIdx = 0; cboxIdx < charBoxes.size(); cboxIdx++)
+ {
+ rectangle(allHistograms[i], charBoxes[cboxIdx], Scalar(0, 255, 0));
+ }
+
+ Mat histDashboard = drawImageDashboard(allHistograms, allHistograms[0].type(), 3);
+ displayImage(config, "Char seg histograms", histDashboard);
+ }
+
+ for (int z = 0; z < charBoxes.size(); z++)
+ allBoxes.push_back(charBoxes[z]);
+ //drawAndWait(&histogramMask);
+ }
+
+
+ float biggestCharWidth = avgCharWidth;
+ // Compute largest char width
+ for (int i = 0; i < allBoxes.size(); i++)
+ {
+ if (allBoxes[i].width > biggestCharWidth)
+ biggestCharWidth = allBoxes[i].width;
+ }
+
+
+ if (config->debugTiming)
+ {
+ timespec endTime;
+ getTime(&endTime);
+ cout << " -- Character Segmentation Create and Score Histograms Time: " << diffclock(startTime, endTime) << "ms." << endl;
+ }
+
+ //ColorFilter colorFilter(img, charAnalysis->getCharacterMask());
+ vector candidateBoxes = getBestCharBoxes(charAnalysis->thresholds[0], allBoxes, biggestCharWidth);
+
+
+ if (this->config->debugCharSegmenter)
+ {
+ // Setup the dashboard images to show the cleaning filters
+ for (int i = 0; i < charAnalysis->thresholds.size(); i++)
+ {
+ Mat cleanImg = Mat::zeros(charAnalysis->thresholds[i].size(), charAnalysis->thresholds[i].type());
+ Mat boxMask = getCharBoxMask(charAnalysis->thresholds[i], candidateBoxes);
+ charAnalysis->thresholds[i].copyTo(cleanImg);
+ bitwise_and(cleanImg, boxMask, cleanImg);
+ cvtColor(cleanImg, cleanImg, CV_GRAY2BGR);
+
+ for (int c = 0; c < candidateBoxes.size(); c++)
+ rectangle(cleanImg, candidateBoxes[c], Scalar(0, 255, 0), 1);
+ imgDbgCleanStages.push_back(cleanImg);
+ }
+ }
+
+
+ getTime(&startTime);
+
+ filterEdgeBoxes(charAnalysis->thresholds, candidateBoxes, biggestCharWidth, avgCharHeight);
+
+ candidateBoxes = filterMostlyEmptyBoxes(charAnalysis->thresholds, candidateBoxes);
+
+ candidateBoxes = combineCloseBoxes(candidateBoxes, biggestCharWidth);
+
+ cleanCharRegions(charAnalysis->thresholds, candidateBoxes);
+ cleanMostlyFullBoxes(charAnalysis->thresholds, candidateBoxes);
+
+ //cleanBasedOnColor(thresholds, colorFilter.colorMask, candidateBoxes);
+
+ candidateBoxes = filterMostlyEmptyBoxes(charAnalysis->thresholds, candidateBoxes);
+ this->characters = candidateBoxes;
+
+ if (config->debugTiming)
+ {
+ timespec endTime;
+ getTime(&endTime);
+ cout << " -- Character Segmentation Box cleaning/filtering Time: " << diffclock(startTime, endTime) << "ms." << endl;
+ }
+
+ if (this->config->debugCharSegmenter)
+ {
+
+
+ Mat imgDash = drawImageDashboard(charAnalysis->thresholds, CV_8U, 3);
+ displayImage(config, "Segmentation after cleaning", imgDash);
+
+ Mat generalDash = drawImageDashboard(this->imgDbgGeneral, this->imgDbgGeneral[0].type(), 2);
+ displayImage(config, "Segmentation General", generalDash);
+
+ Mat cleanImgDash = drawImageDashboard(this->imgDbgCleanStages, this->imgDbgCleanStages[0].type(), 3);
+ displayImage(config, "Segmentation Clean Filters", cleanImgDash);
+ }
+ }
+
+
+
+
+
+
+ if (config->debugTiming)
+ {
+ timespec endTime;
+ getTime(&endTime);
+ cout << "Character Segmenter Time: " << diffclock(startTime, endTime) << "ms." << endl;
+ }
+}
+
+CharacterSegmenter::~CharacterSegmenter()
+{
+ delete charAnalysis;
+}
+
+
+
+
+
+
+// Given a histogram and the horizontal line boundaries, respond with an array of boxes where the characters are
+// Scores the histogram quality as well based on num chars, char volume, and even separation
+vector CharacterSegmenter::getHistogramBoxes(Mat histogram, float avgCharWidth, float avgCharHeight, float* score)
+{
+ float MIN_HISTOGRAM_HEIGHT = avgCharHeight * config->segmentationMinCharHeightPercent;
+
+ float MAX_SEGMENT_WIDTH = avgCharWidth * config->segmentationMaxCharWidthvsAverage;
+
+ //float MIN_BOX_AREA = (avgCharWidth * avgCharHeight) * 0.25;
+
+ int pxLeniency = 2;
+
+ vector charBoxes;
+ vector allBoxes = get1DHits(histogram, pxLeniency);
+
+ for (int i = 0; i < allBoxes.size(); i++)
+ {
+
+ if (allBoxes[i].width >= config->segmentationMinBoxWidthPx && allBoxes[i].width <= MAX_SEGMENT_WIDTH &&
+ allBoxes[i].area() > MIN_HISTOGRAM_HEIGHT )
+ {
+ charBoxes.push_back(allBoxes[i]);
+ }
+
+ }
+
+
+
+ return charBoxes;
+}
+
+vector CharacterSegmenter::getBestCharBoxes(Mat img, vector charBoxes, float avgCharWidth)
+{
+
+
+ float MAX_SEGMENT_WIDTH = avgCharWidth * 1.55;
+
+ // This histogram is based on how many char boxes (from ALL of the many thresholded images) are covering each column
+ // Makes a sort of histogram from all the previous char boxes. Figures out the best fit from that.
+
+ Mat histoImg = Mat::zeros(Size(img.cols, img.rows), CV_8U);
+
+
+ int columnCount;
+
+ for (int col = 0; col < img.cols; col++)
+ {
+ columnCount = 0;
+
+ for (int i = 0; i < charBoxes.size(); i++)
+ {
+ if (col >= charBoxes[i].x && col < (charBoxes[i].x + charBoxes[i].width))
+ columnCount++;
+ }
+
+ // Fill the line of the histogram
+ for (; columnCount > 0; columnCount--)
+ histoImg.at(histoImg.rows - columnCount, col) = 255;
+ }
+
+
+ // Go through each row in the histoImg and score it. Try to find the single line that gives me the most right-sized character regions (based on avgCharWidth)
+
+ int bestRowIndex = 0;
+ float bestRowScore = 0;
+ vector bestBoxes;
+
+
+ for (int row = 0; row < histoImg.rows; row++)
+ {
+ vector validBoxes;
+ vector allBoxes = get1DHits(histoImg, row);
+
+ if (this->config->debugCharSegmenter)
+ cout << "All Boxes size " << allBoxes.size() << endl;
+
+ if (allBoxes.size() == 0)
+ break;
+
+ float rowScore = 0;
+
+ for (int boxidx = 0; boxidx < allBoxes.size(); boxidx++)
+ {
+ int w = allBoxes[boxidx].width;
+ if (w >= config->segmentationMinBoxWidthPx && w <= MAX_SEGMENT_WIDTH)
+ {
+ float widthDiffPixels = abs(w - avgCharWidth);
+ float widthDiffPercent = widthDiffPixels / avgCharWidth;
+ rowScore += 10 * (1 - widthDiffPercent);
+
+ if (widthDiffPercent < 0.25) // Bonus points when it's close to the average character width
+ rowScore += 8;
+
+ validBoxes.push_back(allBoxes[boxidx]);
+ }
+ }
+
+
+ if (rowScore > bestRowScore)
+ {
+ bestRowScore = rowScore;
+ bestRowIndex = row;
+ bestBoxes = validBoxes;
+ }
+ }
+
+
+
+ if (this->config->debugCharSegmenter)
+ {
+ cvtColor(histoImg, histoImg, CV_GRAY2BGR);
+ line(histoImg, Point(0, histoImg.rows - 1 - bestRowIndex), Point(histoImg.cols, histoImg.rows - 1 - bestRowIndex), Scalar(0, 255, 0));
+
+ Mat imgBestBoxes(img.size(), img.type());
+ img.copyTo(imgBestBoxes);
+ cvtColor(imgBestBoxes, imgBestBoxes, CV_GRAY2BGR);
+ for (int i = 0; i < bestBoxes.size(); i++)
+ rectangle(imgBestBoxes, bestBoxes[i], Scalar(0, 255, 0));
+
+ this->imgDbgGeneral.push_back(addLabel(histoImg, "All Histograms"));
+ this->imgDbgGeneral.push_back(addLabel(imgBestBoxes, "Best Boxes"));
+
+ }
+
+
+ return bestBoxes;
+}
+
+vector CharacterSegmenter::get1DHits(Mat img, int yOffset)
+{
+ vector hits;
+
+ bool onSegment = false;
+ int curSegmentLength = 0;
+ for (int col = 0; col < img.cols; col++)
+ {
+ bool isOn = img.at(img.rows - 1 - yOffset, col);
+ if (isOn)
+ {
+ // We're on a segment. Increment the length
+ onSegment = true;
+ curSegmentLength++;
+ }
+
+ if ((isOn == false && onSegment == true) ||
+ (col == img.cols - 1 && onSegment == true))
+ {
+ // A segment just ended or we're at the very end of the row and we're on a segment
+ Point topLeft = Point(col - curSegmentLength, top.getPointAt(col - curSegmentLength) - 1);
+ Point botRight = Point(col, bottom.getPointAt(col) + 1);
+ hits.push_back(Rect(topLeft, botRight));
+
+ onSegment = false;
+ curSegmentLength = 0;
+ }
+
+ }
+
+
+ return hits;
+}
+
+
+void CharacterSegmenter::removeSmallContours(vector thresholds, vector > > allContours, float avgCharWidth, float avgCharHeight)
+{
+ //const float MIN_CHAR_AREA = 0.02 * avgCharWidth * avgCharHeight; // To clear out the tiny specks
+ const float MIN_CONTOUR_HEIGHT = 0.3 * avgCharHeight;
+
+ for (int i = 0; i < thresholds.size(); i++)
+ {
+
+ for (int c = 0; c < allContours[i].size(); c++)
+ {
+ if (allContours[i][c].size() == 0)
+ continue;
+
+ Rect mr = boundingRect(allContours[i][c]);
+ if (mr.height < MIN_CONTOUR_HEIGHT)
+ {
+ // Erase it
+ drawContours(thresholds[i], allContours[i], c, Scalar(0, 0, 0), -1);
+ continue;
+ }
+
+ }
+ }
+}
+
+vector CharacterSegmenter::combineCloseBoxes( vector charBoxes, float biggestCharWidth)
+{
+ vector newCharBoxes;
+
+ for (int i = 0; i < charBoxes.size(); i++)
+ {
+ if (i == charBoxes.size() - 1)
+ {
+ newCharBoxes.push_back(charBoxes[i]);
+ break;
+ }
+ float bigWidth = (charBoxes[i + 1].x + charBoxes[i + 1].width - charBoxes[i].x);
+
+ float w1Diff = abs(charBoxes[i].width - biggestCharWidth);
+ float w2Diff = abs(charBoxes[i + 1].width - biggestCharWidth);
+ float bigDiff = abs(bigWidth - biggestCharWidth);
+ bigDiff *= 1.3; // Make it a little harder to merge boxes.
+
+ if (bigDiff < w1Diff && bigDiff < w2Diff)
+ {
+ Rect bigRect(charBoxes[i].x, charBoxes[i].y, bigWidth, charBoxes[i].height);
+ newCharBoxes.push_back(bigRect);
+ if (this->config->debugCharSegmenter)
+ {
+ for (int z = 0; z < charAnalysis->thresholds.size(); z++)
+ {
+ Point center(bigRect.x + bigRect.width / 2, bigRect.y + bigRect.height / 2);
+ RotatedRect rrect(center, Size2f(bigRect.width, bigRect.height + (bigRect.height / 2)), 0);
+ ellipse(imgDbgCleanStages[z], rrect, Scalar(0,255,0), 1);
+ }
+ cout << "Merging 2 boxes -- " << i << " and " << i + 1 << endl;
+ }
+
+ i++;
+ }
+ else
+ {
+ newCharBoxes.push_back(charBoxes[i]);
+ }
+
+
+
+ }
+
+ return newCharBoxes;
+}
+
+void CharacterSegmenter::cleanCharRegions(vector thresholds, vector charRegions)
+{
+ const float MIN_SPECKLE_HEIGHT_PERCENT = 0.13;
+ const float MIN_SPECKLE_WIDTH_PX = 3;
+ const float MIN_CONTOUR_AREA_PERCENT = 0.1;
+ const float MIN_CONTOUR_HEIGHT_PERCENT = 0.60;
+
+ Mat mask = getCharBoxMask(thresholds[0], charRegions);
+
+
+ for (int i = 0; i < thresholds.size(); i++)
+ {
+ bitwise_and(thresholds[i], mask, thresholds[i]);
+ vector > contours;
+
+ Mat tempImg(thresholds[i].size(), thresholds[i].type());
+ thresholds[i].copyTo(tempImg);
+
+ //Mat element = getStructuringElement( 1,
+// Size( 2 + 1, 2+1 ),
+// Point( 1, 1 ) );
+ //dilate(thresholds[i], tempImg, element);
+ //morphologyEx(thresholds[i], tempImg, MORPH_CLOSE, element);
+ //drawAndWait(&tempImg);
+
+ findContours(tempImg, contours, RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE);
+
+ for (int j = 0; j < charRegions.size(); j++)
+ {
+ const float MIN_SPECKLE_HEIGHT = ((float)charRegions[j].height) * MIN_SPECKLE_HEIGHT_PERCENT;
+ const float MIN_CONTOUR_AREA = ((float)charRegions[j].area()) * MIN_CONTOUR_AREA_PERCENT;
+
+
+ int tallestContourHeight = 0;
+ float totalArea = 0;
+ for (int c = 0; c < contours.size(); c++)
+ {
+ if (contours[c].size() == 0)
+ continue;
+ if (charRegions[j].contains(contours[c][0]) == false)
+ continue;
+
+
+
+ Rect r = boundingRect(contours[c]);
+
+ if (r.height <= MIN_SPECKLE_HEIGHT || r.width <= MIN_SPECKLE_WIDTH_PX)
+ {
+ // Erase this speckle
+ drawContours(thresholds[i], contours, c, Scalar(0,0,0), CV_FILLED);
+
+ if (this->config->debugCharSegmenter)
+ {
+ drawContours(imgDbgCleanStages[i], contours, c, COLOR_DEBUG_SPECKLES, CV_FILLED);
+ }
+ }
+ else
+ {
+ if (r.height > tallestContourHeight)
+ tallestContourHeight = r.height;
+
+ totalArea += contourArea(contours[c]);
+
+
+ }
+ //else if (r.height > tallestContourHeight)
+ //{
+ // tallestContourIndex = c;
+ // tallestContourHeight = h;
+ //}
+
+ }
+
+
+
+ if (totalArea < MIN_CONTOUR_AREA)
+ {
+ // Character is not voluminous enough. Erase it.
+ if (this->config->debugCharSegmenter)
+ {
+ cout << "Character CLEAN: (area) removing box " << j << " in threshold " << i << " -- Area " << totalArea << " < " << MIN_CONTOUR_AREA << endl;
+
+ Rect boxTop(charRegions[j].x, charRegions[j].y - 10, charRegions[j].width, 10);
+ rectangle(imgDbgCleanStages[i], boxTop, COLOR_DEBUG_MIN_AREA, -1);
+ }
+
+
+ rectangle(thresholds[i], charRegions[j], Scalar(0, 0, 0), -1);
+ }
+ else if (tallestContourHeight < ((float) charRegions[j].height * MIN_CONTOUR_HEIGHT_PERCENT))
+ {
+ // This character is too short. Black the whole thing out
+ if (this->config->debugCharSegmenter)
+ {
+ cout << "Character CLEAN: (height) removing box " << j << " in threshold " << i << " -- Height " << tallestContourHeight << " < " << ((float) charRegions[j].height * MIN_CONTOUR_HEIGHT_PERCENT) << endl;
+
+ Rect boxBottom(charRegions[j].x, charRegions[j].y + charRegions[j].height, charRegions[j].width, 10);
+ rectangle(imgDbgCleanStages[i], boxBottom, COLOR_DEBUG_MIN_HEIGHT, -1);
+ }
+ rectangle(thresholds[i], charRegions[j], Scalar(0, 0, 0), -1);
+ }
+
+ }
+
+
+ Mat closureElement = getStructuringElement( 1,
+ Size( 2 + 1, 2+1 ),
+ Point( 1, 1 ) );
+
+ //morphologyEx(thresholds[i], thresholds[i], MORPH_OPEN, element);
+
+ //dilate(thresholds[i], thresholds[i], element);
+ //erode(thresholds[i], thresholds[i], element);
+
+ morphologyEx(thresholds[i], thresholds[i], MORPH_CLOSE, closureElement);
+
+ // Lastly, draw a clipping line between each character boxes
+ for (int j = 0; j < charRegions.size(); j++)
+ {
+ line(thresholds[i], Point(charRegions[j].x - 1, charRegions[j].y), Point(charRegions[j].x - 1, charRegions[j].y + charRegions[j].height), Scalar(0, 0, 0));
+ line(thresholds[i], Point(charRegions[j].x + charRegions[j].width + 1, charRegions[j].y), Point(charRegions[j].x + charRegions[j].width + 1, charRegions[j].y + charRegions[j].height), Scalar(0, 0, 0));
+ }
+ }
+
+}
+
+
+void CharacterSegmenter::cleanBasedOnColor(vector thresholds, Mat colorMask, vector charRegions)
+{
+ // If I knock out x% of the contour area from this thing (after applying the color filter)
+ // Consider it a bad news bear. REmove the whole area.
+ const float MIN_PERCENT_CHUNK_REMOVED = 0.6;
+
+ for (int i = 0; i < thresholds.size(); i++)
+ {
+
+ for (int j = 0; j < charRegions.size(); j++)
+ {
+
+ Mat boxChar = Mat::zeros(thresholds[i].size(), CV_8U);
+ rectangle(boxChar, charRegions[j], Scalar(255,255,255), CV_FILLED);
+
+ bitwise_and(thresholds[i], boxChar, boxChar);
+
+
+
+ float meanBefore = mean(boxChar, boxChar)[0];
+
+ Mat thresholdCopy(thresholds[i].size(), CV_8U);
+ bitwise_and(colorMask, boxChar, thresholdCopy);
+
+ float meanAfter = mean(thresholdCopy, boxChar)[0];
+
+ if (meanAfter < meanBefore * (1-MIN_PERCENT_CHUNK_REMOVED))
+ {
+ rectangle(thresholds[i], charRegions[j], Scalar(0,0,0), CV_FILLED);
+
+ if (this->config->debugCharSegmenter)
+ {
+ //vector tmpDebug;
+ //Mat thresholdCopy2 = Mat::zeros(thresholds[i].size(), CV_8U);
+ //rectangle(thresholdCopy2, charRegions[j], Scalar(255,255,255), CV_FILLED);
+ //tmpDebug.push_back(addLabel(thresholdCopy2, "box Mask"));
+ //bitwise_and(thresholds[i], thresholdCopy2, thresholdCopy2);
+ //tmpDebug.push_back(addLabel(thresholdCopy2, "box Mask + Thresh"));
+ //bitwise_and(colorMask, thresholdCopy2, thresholdCopy2);
+ //tmpDebug.push_back(addLabel(thresholdCopy2, "box Mask + Thresh + Color"));
+//
+// Mat tmpytmp = addLabel(thresholdCopy2, "box Mask + Thresh + Color");
+// Mat tmpx = drawImageDashboard(tmpDebug, tmpytmp.type(), 1);
+ //drawAndWait( &tmpx );
+
+
+ cout << "Segmentation Filter Clean by Color: Removed Threshold " << i << " charregion " << j << endl;
+ cout << "Segmentation Filter Clean by Color: before=" << meanBefore << " after=" << meanAfter << endl;
+
+ Point topLeft(charRegions[j].x, charRegions[j].y);
+ circle(imgDbgCleanStages[i], topLeft, 5, COLOR_DEBUG_COLORFILTER, CV_FILLED);
+ }
+ }
+ }
+
+ }
+}
+
+
+void CharacterSegmenter::cleanMostlyFullBoxes(vector thresholds, const vector charRegions)
+{
+ float MAX_FILLED = 0.95 * 255;
+
+ for (int i = 0; i < charRegions.size(); i++)
+ {
+ Mat mask = Mat::zeros(thresholds[0].size(), CV_8U);
+ rectangle(mask, charRegions[i], Scalar(255,255,255), -1);
+
+ for (int j = 0; j < thresholds.size(); j++)
+ {
+ if (mean(thresholds[j], mask)[0] > MAX_FILLED)
+ {
+ // Empty the rect
+ rectangle(thresholds[j], charRegions[i], Scalar(0,0,0), -1);
+
+ if (this->config->debugCharSegmenter)
+ {
+ Rect inset(charRegions[i].x + 2, charRegions[i].y - 2, charRegions[i].width - 4, charRegions[i].height - 4);
+ rectangle(imgDbgCleanStages[j], inset, COLOR_DEBUG_FULLBOX, 1);
+ }
+ }
+ }
+ }
+}
+vector CharacterSegmenter::filterMostlyEmptyBoxes(vector thresholds, const vector charRegions)
+{
+ // Of the n thresholded images, if box 3 (for example) is empty in half (for example) of the thresholded images,
+ // clear all data for every box #3.
+
+ //const float MIN_AREA_PERCENT = 0.1;
+ const float MIN_CONTOUR_HEIGHT_PERCENT = 0.65;
+
+ Mat mask = getCharBoxMask(thresholds[0], charRegions);
+
+ vector boxScores(charRegions.size());
+
+ for (int i = 0; i < charRegions.size(); i++)
+ boxScores[i] = 0;
+
+ for (int i = 0; i < thresholds.size(); i++)
+ {
+
+ for (int j = 0; j < charRegions.size(); j++)
+ {
+ //float minArea = charRegions[j].area() * MIN_AREA_PERCENT;
+
+ Mat tempImg = Mat::zeros(thresholds[i].size(), thresholds[i].type());
+ rectangle(tempImg, charRegions[j], Scalar(255,255,255), CV_FILLED);
+ bitwise_and(thresholds[i], tempImg, tempImg);
+
+ vector > contours;
+ findContours(tempImg, contours, RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE);
+
+ float biggestContourHeight = 0;
+
+ vector allPointsInBox;
+ for (int c = 0; c < contours.size(); c++)
+ {
+ if (contours[c].size() == 0)
+ continue;
+
+ for (int z = 0; z < contours[c].size(); z++)
+ allPointsInBox.push_back(contours[c][z]);
+
+ }
+
+ float height = 0;
+ if (allPointsInBox.size() > 0)
+ {
+ height = boundingRect(allPointsInBox).height;
+ }
+
+
+ if (height >= ((float) charRegions[j].height * MIN_CONTOUR_HEIGHT_PERCENT))
+ {
+ boxScores[j] = boxScores[j] + 1;
+ }
+ else if (this->config->debugCharSegmenter)
+ {
+ drawX(imgDbgCleanStages[i], charRegions[j], COLOR_DEBUG_EMPTYFILTER, 3);
+ }
+
+
+ }
+
+ }
+
+ vector newCharRegions;
+
+ int maxBoxScore = 0;
+ for (int i = 0; i < charRegions.size(); i++)
+ {
+ if (boxScores[i] > maxBoxScore)
+ maxBoxScore = boxScores[i];
+ }
+
+ // Need a good char sample in at least 50% of the boxes for it to be valid.
+ int MIN_FULL_BOXES = maxBoxScore * 0.49;
+
+ // Now check each score. If it's below the minimum, remove the charRegion
+ for (int i = 0; i < charRegions.size(); i++)
+ {
+ if (boxScores[i] > MIN_FULL_BOXES)
+ newCharRegions.push_back(charRegions[i]);
+ else
+ {
+ // Erase the box from the Mat... mainly for debug purposes
+ if (this->config->debugCharSegmenter)
+ {
+ cout << "Mostly Empty Filter: box index: " << i;
+ cout << " this box had a score of : " << boxScores[i];;
+ cout << " MIN_FULL_BOXES: " << MIN_FULL_BOXES << endl;;
+
+ for (int z = 0; z < thresholds.size(); z++)
+ {
+ rectangle(thresholds[z], charRegions[i], Scalar(0,0,0), -1);
+
+ drawX(imgDbgCleanStages[z], charRegions[i], COLOR_DEBUG_EMPTYFILTER, 1);
+ }
+
+ }
+ }
+
+ if (this->config->debugCharSegmenter)
+ cout << " Box Score: " << boxScores[i] << endl;
+ }
+
+ return newCharRegions;
+}
+
+void CharacterSegmenter::filterEdgeBoxes(vector thresholds, const vector charRegions, float avgCharWidth, float avgCharHeight)
+{
+ const float MIN_ANGLE_FOR_ROTATION = 0.4;
+ int MIN_CONNECTED_EDGE_PIXELS = (avgCharHeight * 1.5);
+
+ // Sometimes the rectangle won't be very tall, making it impossible to detect an edge
+ // Adjust for this here.
+ int alternate = thresholds[0].rows * 0.92;
+ if (alternate < MIN_CONNECTED_EDGE_PIXELS && alternate > avgCharHeight)
+ MIN_CONNECTED_EDGE_PIXELS = alternate;
+
+ //
+ // Pay special attention to the edge boxes. If it's a skinny box, and the vertical height extends above our bounds... remove it.
+ //while (charBoxes.size() > 0 && charBoxes[charBoxes.size() - 1].width < MIN_SEGMENT_WIDTH_EDGES)
+ // charBoxes.erase(charBoxes.begin() + charBoxes.size() - 1);
+ // Now filter the "edge" boxes. We don't want to include skinny boxes on the edges, since these could be plate boundaries
+ //while (charBoxes.size() > 0 && charBoxes[0].width < MIN_SEGMENT_WIDTH_EDGES)
+ // charBoxes.erase(charBoxes.begin() + 0);
+
+
+ // TECHNIQUE #1
+ // Check for long vertical lines. Once the line is too long, mask the whole region
+
+ if (charRegions.size() <= 1)
+ return;
+
+ // Check both sides to see where the edges are
+ // The first starts at the right edge of the leftmost char region and works its way left
+ // The second starts at the left edge of the rightmost char region and works its way right.
+ // We start by rotating the threshold image to the correct angle
+ // then check each column 1 by 1.
+
+ vector leftEdges;
+ vector rightEdges;
+
+ for (int i = 0; i < thresholds.size(); i++)
+ {
+ Mat rotated;
+
+ if (top.angle > MIN_ANGLE_FOR_ROTATION)
+ {
+ // Rotate image:
+ rotated = Mat(thresholds[i].size(), thresholds[i].type());
+ Mat rot_mat( 2, 3, CV_32FC1 );
+ Point center = Point( thresholds[i].cols/2, thresholds[i].rows/2 );
+
+ rot_mat = getRotationMatrix2D( center, top.angle, 1.0 );
+ warpAffine( thresholds[i], rotated, rot_mat, thresholds[i].size() );
+ }
+ else
+ {
+ rotated = thresholds[i];
+ }
+
+ int leftEdgeX = 0;
+ int rightEdgeX = rotated.cols;
+ // Do the left side
+ int col = charRegions[0].x + charRegions[0].width;
+ while (col >= 0)
+ {
+
+ int rowLength = getLongestBlobLengthBetweenLines(rotated, col);
+
+ if (rowLength > MIN_CONNECTED_EDGE_PIXELS)
+ {
+ leftEdgeX = col;
+ break;
+ }
+
+ col--;
+ }
+
+ col = charRegions[charRegions.size() - 1].x;
+ while (col < rotated.cols)
+ {
+
+ int rowLength = getLongestBlobLengthBetweenLines(rotated, col);
+
+ if (rowLength > MIN_CONNECTED_EDGE_PIXELS)
+ {
+ rightEdgeX = col;
+ break;
+ }
+ col++;
+ }
+
+
+ if (leftEdgeX != 0)
+ leftEdges.push_back(leftEdgeX);
+ if (rightEdgeX != thresholds[i].cols)
+ rightEdges.push_back(rightEdgeX);
+
+ }
+
+ int leftEdge = 0;
+ int rightEdge = thresholds[0].cols;
+
+ // Assign the edge values to the SECOND closest value
+ if (leftEdges.size() > 1)
+ {
+ sort (leftEdges.begin(), leftEdges.begin()+leftEdges.size());
+ leftEdge = leftEdges[leftEdges.size() - 2] + 1;
+ }
+ if (rightEdges.size() > 1)
+ {
+ sort (rightEdges.begin(), rightEdges.begin()+rightEdges.size());
+ rightEdge = rightEdges[1] - 1;
+ }
+
+ if (leftEdge != 0 || rightEdge != thresholds[0].cols)
+ {
+ Mat mask = Mat::zeros(thresholds[0].size(), CV_8U);
+ rectangle(mask, Point(leftEdge, 0), Point(rightEdge, thresholds[0].rows), Scalar(255,255,255), -1);
+
+ if (top.angle > MIN_ANGLE_FOR_ROTATION)
+ {
+ // Rotate mask:
+ Mat rot_mat( 2, 3, CV_32FC1 );
+ Point center = Point( mask.cols/2, mask.rows/2 );
+
+ rot_mat = getRotationMatrix2D( center, top.angle * -1, 1.0 );
+ warpAffine( mask, mask, rot_mat, mask.size() );
+ }
+
+
+ // If our edge mask covers more than x% of the char region, mask the whole thing...
+ const float MAX_COVERAGE_PERCENT = 0.6;
+ int leftCoveragePx = leftEdge - charRegions[0].x;
+ float leftCoveragePercent = ((float) leftCoveragePx) / ((float) charRegions[0].width);
+ float rightCoveragePx = (charRegions[charRegions.size() -1].x + charRegions[charRegions.size() -1].width) - rightEdge;
+ float rightCoveragePercent = ((float) rightCoveragePx) / ((float) charRegions[charRegions.size() -1].width);
+ if ((leftCoveragePercent > MAX_COVERAGE_PERCENT) ||
+ (charRegions[0].width - leftCoveragePx < config->segmentationMinBoxWidthPx))
+ {
+ rectangle(mask, charRegions[0], Scalar(0,0,0), -1); // Mask the whole region
+ if (this->config->debugCharSegmenter)
+ cout << "Edge Filter: Entire left region is erased" << endl;
+ }
+ if ((rightCoveragePercent > MAX_COVERAGE_PERCENT) ||
+ (charRegions[charRegions.size() -1].width - rightCoveragePx < config->segmentationMinBoxWidthPx))
+ {
+ rectangle(mask, charRegions[charRegions.size() -1], Scalar(0,0,0), -1);
+ if (this->config->debugCharSegmenter)
+ cout << "Edge Filter: Entire right region is erased" << endl;
+ }
+
+ for (int i = 0; i < thresholds.size(); i++)
+ {
+ bitwise_and(thresholds[i], mask, thresholds[i]);
+ }
+
+
+ if (this->config->debugCharSegmenter)
+ {
+ cout << "Edge Filter: left=" << leftEdge << " right=" << rightEdge << endl;
+ Mat bordered = addLabel(mask, "Edge Filter #1");
+ imgDbgGeneral.push_back(bordered);
+
+ Mat invertedMask(mask.size(), mask.type());
+ bitwise_not(mask, invertedMask);
+ for (int z = 0; z < imgDbgCleanStages.size(); z++)
+ fillMask(imgDbgCleanStages[z], invertedMask, Scalar(0,0,255));
+ }
+ }
+
+ // TECHNIQUE #2
+ // Check for tall skinny blobs on the edge boxes. If they're too long and skinny, maks the whole char region
+ /*
+ *
+ float MIN_EDGE_CONTOUR_HEIGHT = avgCharHeight * 0.7;
+ float MIN_EDGE_CONTOUR_AREA_PCT = avgCharHeight * 0.1;
+
+ for (int i = 0; i < thresholds.size(); i++)
+ {
+ // Just check the first and last char box. If the contour extends too far above/below the line. Drop it.
+
+ for (int boxidx = 0; boxidx < charRegions.size(); boxidx++)
+ {
+ if (boxidx != 0 || boxidx != charRegions.size() -1)
+ {
+ // This is a middle box. we never want to filter these here.
+ continue;
+ }
+
+ vector > contours;
+ Mat mask = Mat::zeros(thresholds[i].size(),CV_8U);
+ rectangle(mask, charRegions[boxidx], Scalar(255,255,255), CV_FILLED);
+
+ bitwise_and(thresholds[i], mask, mask);
+ findContours(mask, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE);
+ //int tallContourIndex = isSkinnyLineInsideBox(thresholds[i], charRegions[boxidx], allContours[i], hierarchy[i], avgCharWidth, avgCharHeight);
+ float tallestContourHeight = 0;
+ float fattestContourWidth = 0;
+ float biggestContourArea = 0;
+ for (int c = 0; c < contours.size(); c++)
+ {
+ Rect r = boundingRect(contours[c]);
+ if (r.height > tallestContourHeight)
+ tallestContourHeight = r.height;
+ if (r.width > fattestContourWidth)
+ fattestContourWidth = r.width;
+ float a = r.area();
+ if (a > biggestContourArea)
+ biggestContourArea = a;
+ }
+
+ float minArea = charRegions[boxidx].area() * MIN_EDGE_CONTOUR_AREA_PCT;
+ if ((fattestContourWidth < MIN_BOX_WIDTH_PX) ||
+ (tallestContourHeight < MIN_EDGE_CONTOUR_HEIGHT) ||
+ (biggestContourArea < minArea)
+ )
+ {
+ // Find a good place to MASK this contour.
+ // for now, just mask the whole thing
+ if (this->debug)
+ {
+
+ rectangle(imgDbgCleanStages[i], charRegions[boxidx], COLOR_DEBUG_EDGE, 2);
+ cout << "Edge Filter: threshold " << i << " box " << boxidx << endl;
+ }
+ rectangle(thresholds[i], charRegions[boxidx], Scalar(0,0,0), -1);
+ }
+ else
+ {
+ filteredCharRegions.push_back(charRegions[boxidx]);
+ }
+ }
+ }
+ */
+
+}
+
+int CharacterSegmenter::getLongestBlobLengthBetweenLines(Mat img, int col)
+{
+
+ int longestBlobLength = 0;
+
+ bool onSegment = false;
+ bool wasbetweenLines = false;
+ float curSegmentLength = 0;
+ for (int row = 0; row < img.rows; row++)
+ {
+ bool isbetweenLines = false;
+
+ bool isOn = img.at(row, col);
+ // check two rows at a time.
+ if (!isOn && col < img.cols)
+ isOn = img.at(row, col);
+
+ if (isOn)
+ {
+ // We're on a segment. Increment the length
+ isbetweenLines = top.isPointBelowLine(Point(col, row)) && !bottom.isPointBelowLine(Point(col, row));
+ float incrementBy = 1;
+
+ // Add a little extra to the score if this is outside of the lines
+ if (!isbetweenLines)
+ incrementBy = 1.1;
+
+ onSegment = true;
+ curSegmentLength += incrementBy;
+ }
+ if (isOn && isbetweenLines)
+ {
+ wasbetweenLines = true;
+ }
+
+ if ((isOn == false && onSegment == true) ||
+ (row == img.rows - 1 && onSegment == true))
+ {
+ if (wasbetweenLines && curSegmentLength > longestBlobLength)
+ longestBlobLength = curSegmentLength;
+
+ onSegment = false;
+ isbetweenLines = false;
+ curSegmentLength = 0;
+ }
+
+ }
+
+
+ return longestBlobLength;
+}
+
+// Checks to see if a skinny, tall line (extending above or below the char Height) is inside the given box.
+// Returns the contour index if true. -1 otherwise
+int CharacterSegmenter::isSkinnyLineInsideBox(Mat threshold, Rect box, vector > contours, vector hierarchy, float avgCharWidth, float avgCharHeight)
+{
+ float MIN_EDGE_CONTOUR_HEIGHT = avgCharHeight * 1.25;
+
+ // Sometimes the threshold is smaller than the MIN_EDGE_CONTOUR_HEIGHT.
+ // In that case, adjust to be smaller
+ int alternate = threshold.rows * 0.92;
+ if (alternate < MIN_EDGE_CONTOUR_HEIGHT && alternate > avgCharHeight)
+ MIN_EDGE_CONTOUR_HEIGHT = alternate;
+
+ Rect slightlySmallerBox(box.x, box.y, box.width, box.height);
+ Mat boxMask = Mat::zeros(threshold.size(), CV_8U);
+ rectangle(boxMask, slightlySmallerBox, Scalar(255, 255, 255), -1);
+
+
+ for (int i = 0; i < contours.size(); i++)
+ {
+ // Only bother with the big boxes
+ if (boundingRect(contours[i]).height < MIN_EDGE_CONTOUR_HEIGHT)
+ continue;
+
+ Mat tempImg = Mat::zeros(threshold.size(), CV_8U);
+ drawContours(tempImg, contours, i, Scalar(255,255,255), -1, 8, hierarchy, 1);
+ bitwise_and(tempImg, boxMask, tempImg);
+
+ vector > subContours;
+ findContours(tempImg, subContours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE);
+ int tallestContourIdx = -1;
+ int tallestContourHeight = 0;
+ int tallestContourWidth = 0;
+ float tallestContourArea = 0;
+ for (int s = 0; s < subContours.size(); s++)
+ {
+ Rect r = boundingRect(subContours[s]);
+ if (r.height > tallestContourHeight)
+ {
+ tallestContourIdx = s;
+ tallestContourHeight = r.height;
+ tallestContourWidth = r.width;
+ tallestContourArea = contourArea(subContours[s]);
+ }
+ }
+
+ if (tallestContourIdx != -1)
+ {
+
+
+ //cout << "Edge Filter: " << tallestContourHeight << " -- " << avgCharHeight << endl;
+ if (tallestContourHeight >= avgCharHeight * 0.9 &&
+ ((tallestContourWidth < config->segmentationMinBoxWidthPx) || (tallestContourArea < avgCharWidth * avgCharHeight * 0.1)))
+ {
+ cout << "Edge Filter: Avg contour width: " << avgCharWidth << " This guy is: " << tallestContourWidth << endl;
+ cout << "Edge Filter: tallestContourArea: " << tallestContourArea << " Minimum: " << avgCharWidth * avgCharHeight * 0.1 << endl;
+ return i;
+ }
+ }
+ }
+
+ return -1;
+}
+
+
+
+Mat CharacterSegmenter::getCharBoxMask(Mat img_threshold, vector charBoxes)
+{
+
+ Mat mask = Mat::zeros(img_threshold.size(), CV_8U);
+ for (int i = 0; i < charBoxes.size(); i++)
+ rectangle(mask, charBoxes[i], Scalar(255, 255, 255), -1);
+
+ return mask;
+}
+
+vector CharacterSegmenter::getThresholds()
+{
+ return charAnalysis->thresholds;
+}
+
+
diff --git a/src/openalpr/charactersegmenter.h b/src/openalpr/charactersegmenter.h
new file mode 100644
index 0000000..d5074e6
--- /dev/null
+++ b/src/openalpr/charactersegmenter.h
@@ -0,0 +1,106 @@
+/*
+ * Copyright (c) 2013 New Designs Unlimited, LLC
+ * Opensource Automated License Plate Recognition [http://www.openalpr.com]
+ *
+ * This file is part of OpenAlpr.
+ *
+ * OpenAlpr is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License
+ * version 3 as published by the Free Software Foundation
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+*/
+
+
+
+#ifndef CHARACTERSEGMENTER_H
+#define CHARACTERSEGMENTER_H
+
+#include "opencv2/highgui/highgui.hpp"
+#include "opencv2/imgproc/imgproc.hpp"
+#include "constants.h"
+#include "binarize_wolf.h"
+#include "utility.h"
+#include "characterregion.h"
+#include "colorfilter.h"
+#include "verticalhistogram.h"
+#include "config.h"
+
+using namespace cv;
+using namespace std;
+
+//const float MIN_BOX_WIDTH_PX = 4; // 4 pixels
+
+const Scalar COLOR_DEBUG_EDGE(0,0,255); // Red
+const Scalar COLOR_DEBUG_SPECKLES(0,0,255); // Red
+const Scalar COLOR_DEBUG_MIN_HEIGHT(255,0,0); // Blue
+const Scalar COLOR_DEBUG_MIN_AREA(255,0,0); // Blue
+const Scalar COLOR_DEBUG_FULLBOX(255,255,0); // Blue-green
+const Scalar COLOR_DEBUG_COLORFILTER(255,0,255); // Magenta
+const Scalar COLOR_DEBUG_EMPTYFILTER(0,255,255); // Yellow
+
+class CharacterSegmenter
+{
+
+ public:
+ CharacterSegmenter(Mat img, bool invertedColors, Config* config);
+ virtual ~CharacterSegmenter();
+
+ vector characters;
+ int confidence;
+
+ vector getThresholds();
+
+ private:
+ Config* config;
+
+ CharacterAnalysis* charAnalysis;
+
+ LineSegment top;
+ LineSegment bottom;
+
+ vector imgDbgGeneral;
+ vector imgDbgCleanStages;
+
+ vector filter(Mat img, vector > contours, vector hierarchy);
+ vector filterByBoxSize(vector< vector< Point> > contours, vector goodIndices, float minHeightPx, float maxHeightPx);
+ vector filterBetweenLines(Mat img, vector > contours, vector hierarchy, vector outerPolygon, vector goodIndices);
+ vector filterContourHoles(vector > contours, vector hierarchy, vector goodIndices);
+
+ vector getBestVotedLines(Mat img, vector > contours, vector goodIndices);
+ int getGoodIndicesCount(vector goodIndices);
+
+ Mat getCharacterMask(Mat img_threshold, vector > contours, vector hierarchy, vector goodIndices);
+ Mat getCharBoxMask(Mat img_threshold, vector charBoxes);
+
+ void removeSmallContours(vector thresholds, vector > > allContours, float avgCharWidth, float avgCharHeight);
+
+ Mat getVerticalHistogram(Mat img, Mat mask);
+ vector getHistogramBoxes(Mat histogram, float avgCharWidth, float avgCharHeight, float* score);
+ vector getBestCharBoxes(Mat img, vector charBoxes, float avgCharWidth);
+ vector combineCloseBoxes( vector charBoxes, float avgCharWidth);
+
+ vector get1DHits(Mat img, int yOffset);
+
+ void cleanCharRegions(vector thresholds, vector charRegions);
+ void cleanBasedOnColor(vector thresholds, Mat colorMask, vector charRegions);
+ void cleanMostlyFullBoxes(vector thresholds, const vector charRegions);
+ vector filterMostlyEmptyBoxes(vector thresholds, const vector charRegions);
+ void filterEdgeBoxes(vector thresholds, const vector