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 charRegions, float avgCharWidth, float avgCharHeight); + + int getLongestBlobLengthBetweenLines(Mat img, int col); + + int isSkinnyLineInsideBox(Mat threshold, Rect box, vector > contours, vector hierarchy, float avgCharWidth, float avgCharHeight); + + vector getEncapsulatingLines(Mat img, vector > contours, vector goodIndices); +}; + +#endif // CHARACTERSEGMENTER_H + + diff --git a/src/openalpr/cjson.c b/src/openalpr/cjson.c new file mode 100644 index 0000000..7331258 --- /dev/null +++ b/src/openalpr/cjson.c @@ -0,0 +1,596 @@ +/* + Copyright (c) 2009 Dave Gamble + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +/* cJSON */ +/* JSON parser in C. */ + +#include +#include +#include +#include +#include +#include +#include +#include "cjson.h" + +static const char *ep; + +const char *cJSON_GetErrorPtr(void) {return ep;} + +static int cJSON_strcasecmp(const char *s1,const char *s2) +{ + if (!s1) return (s1==s2)?0:1;if (!s2) return 1; + for(; tolower(*s1) == tolower(*s2); ++s1, ++s2) if(*s1 == 0) return 0; + return tolower(*(const unsigned char *)s1) - tolower(*(const unsigned char *)s2); +} + +static void *(*cJSON_malloc)(size_t sz) = malloc; +static void (*cJSON_free)(void *ptr) = free; + +static char* cJSON_strdup(const char* str) +{ + size_t len; + char* copy; + + len = strlen(str) + 1; + if (!(copy = (char*)cJSON_malloc(len))) return 0; + memcpy(copy,str,len); + return copy; +} + +void cJSON_InitHooks(cJSON_Hooks* hooks) +{ + if (!hooks) { /* Reset hooks */ + cJSON_malloc = malloc; + cJSON_free = free; + return; + } + + cJSON_malloc = (hooks->malloc_fn)?hooks->malloc_fn:malloc; + cJSON_free = (hooks->free_fn)?hooks->free_fn:free; +} + +/* Internal constructor. */ +static cJSON *cJSON_New_Item(void) +{ + cJSON* node = (cJSON*)cJSON_malloc(sizeof(cJSON)); + if (node) memset(node,0,sizeof(cJSON)); + return node; +} + +/* Delete a cJSON structure. */ +void cJSON_Delete(cJSON *c) +{ + cJSON *next; + while (c) + { + next=c->next; + if (!(c->type&cJSON_IsReference) && c->child) cJSON_Delete(c->child); + if (!(c->type&cJSON_IsReference) && c->valuestring) cJSON_free(c->valuestring); + if (c->string) cJSON_free(c->string); + cJSON_free(c); + c=next; + } +} + +/* Parse the input text to generate a number, and populate the result into item. */ +static const char *parse_number(cJSON *item,const char *num) +{ + double n=0,sign=1,scale=0;int subscale=0,signsubscale=1; + + if (*num=='-') sign=-1,num++; /* Has sign? */ + if (*num=='0') num++; /* is zero */ + if (*num>='1' && *num<='9') do n=(n*10.0)+(*num++ -'0'); while (*num>='0' && *num<='9'); /* Number? */ + if (*num=='.' && num[1]>='0' && num[1]<='9') {num++; do n=(n*10.0)+(*num++ -'0'),scale--; while (*num>='0' && *num<='9');} /* Fractional part? */ + if (*num=='e' || *num=='E') /* Exponent? */ + { num++;if (*num=='+') num++; else if (*num=='-') signsubscale=-1,num++; /* With sign? */ + while (*num>='0' && *num<='9') subscale=(subscale*10)+(*num++ - '0'); /* Number? */ + } + + n=sign*n*pow(10.0,(scale+subscale*signsubscale)); /* number = +/- number.fraction * 10^+/- exponent */ + + item->valuedouble=n; + item->valueint=(int)n; + item->type=cJSON_Number; + return num; +} + +/* Render the number nicely from the given item into a string. */ +static char *print_number(cJSON *item) +{ + char *str; + double d=item->valuedouble; + if (fabs(((double)item->valueint)-d)<=DBL_EPSILON && d<=INT_MAX && d>=INT_MIN) + { + str=(char*)cJSON_malloc(21); /* 2^64+1 can be represented in 21 chars. */ + if (str) sprintf(str,"%d",item->valueint); + } + else + { + str=(char*)cJSON_malloc(64); /* This is a nice tradeoff. */ + if (str) + { + if (fabs(floor(d)-d)<=DBL_EPSILON && fabs(d)<1.0e60)sprintf(str,"%.0f",d); + else if (fabs(d)<1.0e-6 || fabs(d)>1.0e9) sprintf(str,"%e",d); + else sprintf(str,"%f",d); + } + } + return str; +} + +static unsigned parse_hex4(const char *str) +{ + unsigned h=0; + if (*str>='0' && *str<='9') h+=(*str)-'0'; else if (*str>='A' && *str<='F') h+=10+(*str)-'A'; else if (*str>='a' && *str<='f') h+=10+(*str)-'a'; else return 0; + h=h<<4;str++; + if (*str>='0' && *str<='9') h+=(*str)-'0'; else if (*str>='A' && *str<='F') h+=10+(*str)-'A'; else if (*str>='a' && *str<='f') h+=10+(*str)-'a'; else return 0; + h=h<<4;str++; + if (*str>='0' && *str<='9') h+=(*str)-'0'; else if (*str>='A' && *str<='F') h+=10+(*str)-'A'; else if (*str>='a' && *str<='f') h+=10+(*str)-'a'; else return 0; + h=h<<4;str++; + if (*str>='0' && *str<='9') h+=(*str)-'0'; else if (*str>='A' && *str<='F') h+=10+(*str)-'A'; else if (*str>='a' && *str<='f') h+=10+(*str)-'a'; else return 0; + return h; +} + +/* Parse the input text into an unescaped cstring, and populate item. */ +static const unsigned char firstByteMark[7] = { 0x00, 0x00, 0xC0, 0xE0, 0xF0, 0xF8, 0xFC }; +static const char *parse_string(cJSON *item,const char *str) +{ + const char *ptr=str+1;char *ptr2;char *out;int len=0;unsigned uc,uc2; + if (*str!='\"') {ep=str;return 0;} /* not a string! */ + + while (*ptr!='\"' && *ptr && ++len) if (*ptr++ == '\\') ptr++; /* Skip escaped quotes. */ + + out=(char*)cJSON_malloc(len+1); /* This is how long we need for the string, roughly. */ + if (!out) return 0; + + ptr=str+1;ptr2=out; + while (*ptr!='\"' && *ptr) + { + if (*ptr!='\\') *ptr2++=*ptr++; + else + { + ptr++; + switch (*ptr) + { + case 'b': *ptr2++='\b'; break; + case 'f': *ptr2++='\f'; break; + case 'n': *ptr2++='\n'; break; + case 'r': *ptr2++='\r'; break; + case 't': *ptr2++='\t'; break; + case 'u': /* transcode utf16 to utf8. */ + uc=parse_hex4(ptr+1);ptr+=4; /* get the unicode char. */ + + if ((uc>=0xDC00 && uc<=0xDFFF) || uc==0) break; /* check for invalid. */ + + if (uc>=0xD800 && uc<=0xDBFF) /* UTF16 surrogate pairs. */ + { + if (ptr[1]!='\\' || ptr[2]!='u') break; /* missing second-half of surrogate. */ + uc2=parse_hex4(ptr+3);ptr+=6; + if (uc2<0xDC00 || uc2>0xDFFF) break; /* invalid second-half of surrogate. */ + uc=0x10000 + (((uc&0x3FF)<<10) | (uc2&0x3FF)); + } + + len=4;if (uc<0x80) len=1;else if (uc<0x800) len=2;else if (uc<0x10000) len=3; ptr2+=len; + + switch (len) { + case 4: *--ptr2 =((uc | 0x80) & 0xBF); uc >>= 6; + case 3: *--ptr2 =((uc | 0x80) & 0xBF); uc >>= 6; + case 2: *--ptr2 =((uc | 0x80) & 0xBF); uc >>= 6; + case 1: *--ptr2 =(uc | firstByteMark[len]); + } + ptr2+=len; + break; + default: *ptr2++=*ptr; break; + } + ptr++; + } + } + *ptr2=0; + if (*ptr=='\"') ptr++; + item->valuestring=out; + item->type=cJSON_String; + return ptr; +} + +/* Render the cstring provided to an escaped version that can be printed. */ +static char *print_string_ptr(const char *str) +{ + const char *ptr;char *ptr2,*out;int len=0;unsigned char token; + + if (!str) return cJSON_strdup(""); + ptr=str;while ((token=*ptr) && ++len) {if (strchr("\"\\\b\f\n\r\t",token)) len++; else if (token<32) len+=5;ptr++;} + + out=(char*)cJSON_malloc(len+3); + if (!out) return 0; + + ptr2=out;ptr=str; + *ptr2++='\"'; + while (*ptr) + { + if ((unsigned char)*ptr>31 && *ptr!='\"' && *ptr!='\\') *ptr2++=*ptr++; + else + { + *ptr2++='\\'; + switch (token=*ptr++) + { + case '\\': *ptr2++='\\'; break; + case '\"': *ptr2++='\"'; break; + case '\b': *ptr2++='b'; break; + case '\f': *ptr2++='f'; break; + case '\n': *ptr2++='n'; break; + case '\r': *ptr2++='r'; break; + case '\t': *ptr2++='t'; break; + default: sprintf(ptr2,"u%04x",token);ptr2+=5; break; /* escape and print */ + } + } + } + *ptr2++='\"';*ptr2++=0; + return out; +} +/* Invote print_string_ptr (which is useful) on an item. */ +static char *print_string(cJSON *item) {return print_string_ptr(item->valuestring);} + +/* Predeclare these prototypes. */ +static const char *parse_value(cJSON *item,const char *value); +static char *print_value(cJSON *item,int depth,int fmt); +static const char *parse_array(cJSON *item,const char *value); +static char *print_array(cJSON *item,int depth,int fmt); +static const char *parse_object(cJSON *item,const char *value); +static char *print_object(cJSON *item,int depth,int fmt); + +/* Utility to jump whitespace and cr/lf */ +static const char *skip(const char *in) {while (in && *in && (unsigned char)*in<=32) in++; return in;} + +/* Parse an object - create a new root, and populate. */ +cJSON *cJSON_ParseWithOpts(const char *value,const char **return_parse_end,int require_null_terminated) +{ + const char *end=0; + cJSON *c=cJSON_New_Item(); + ep=0; + if (!c) return 0; /* memory fail */ + + end=parse_value(c,skip(value)); + if (!end) {cJSON_Delete(c);return 0;} /* parse failure. ep is set. */ + + /* if we require null-terminated JSON without appended garbage, skip and then check for a null terminator */ + if (require_null_terminated) {end=skip(end);if (*end) {cJSON_Delete(c);ep=end;return 0;}} + if (return_parse_end) *return_parse_end=end; + return c; +} +/* Default options for cJSON_Parse */ +cJSON *cJSON_Parse(const char *value) {return cJSON_ParseWithOpts(value,0,0);} + +/* Render a cJSON item/entity/structure to text. */ +char *cJSON_Print(cJSON *item) {return print_value(item,0,1);} +char *cJSON_PrintUnformatted(cJSON *item) {return print_value(item,0,0);} + +/* Parser core - when encountering text, process appropriately. */ +static const char *parse_value(cJSON *item,const char *value) +{ + if (!value) return 0; /* Fail on null. */ + if (!strncmp(value,"null",4)) { item->type=cJSON_NULL; return value+4; } + if (!strncmp(value,"false",5)) { item->type=cJSON_False; return value+5; } + if (!strncmp(value,"true",4)) { item->type=cJSON_True; item->valueint=1; return value+4; } + if (*value=='\"') { return parse_string(item,value); } + if (*value=='-' || (*value>='0' && *value<='9')) { return parse_number(item,value); } + if (*value=='[') { return parse_array(item,value); } + if (*value=='{') { return parse_object(item,value); } + + ep=value;return 0; /* failure. */ +} + +/* Render a value to text. */ +static char *print_value(cJSON *item,int depth,int fmt) +{ + char *out=0; + if (!item) return 0; + switch ((item->type)&255) + { + case cJSON_NULL: out=cJSON_strdup("null"); break; + case cJSON_False: out=cJSON_strdup("false");break; + case cJSON_True: out=cJSON_strdup("true"); break; + case cJSON_Number: out=print_number(item);break; + case cJSON_String: out=print_string(item);break; + case cJSON_Array: out=print_array(item,depth,fmt);break; + case cJSON_Object: out=print_object(item,depth,fmt);break; + } + return out; +} + +/* Build an array from input text. */ +static const char *parse_array(cJSON *item,const char *value) +{ + cJSON *child; + if (*value!='[') {ep=value;return 0;} /* not an array! */ + + item->type=cJSON_Array; + value=skip(value+1); + if (*value==']') return value+1; /* empty array. */ + + item->child=child=cJSON_New_Item(); + if (!item->child) return 0; /* memory fail */ + value=skip(parse_value(child,skip(value))); /* skip any spacing, get the value. */ + if (!value) return 0; + + while (*value==',') + { + cJSON *new_item; + if (!(new_item=cJSON_New_Item())) return 0; /* memory fail */ + child->next=new_item;new_item->prev=child;child=new_item; + value=skip(parse_value(child,skip(value+1))); + if (!value) return 0; /* memory fail */ + } + + if (*value==']') return value+1; /* end of array */ + ep=value;return 0; /* malformed. */ +} + +/* Render an array to text */ +static char *print_array(cJSON *item,int depth,int fmt) +{ + char **entries; + char *out=0,*ptr,*ret;int len=5; + cJSON *child=item->child; + int numentries=0,i=0,fail=0; + + /* How many entries in the array? */ + while (child) numentries++,child=child->next; + /* Explicitly handle numentries==0 */ + if (!numentries) + { + out=(char*)cJSON_malloc(3); + if (out) strcpy(out,"[]"); + return out; + } + /* Allocate an array to hold the values for each */ + entries=(char**)cJSON_malloc(numentries*sizeof(char*)); + if (!entries) return 0; + memset(entries,0,numentries*sizeof(char*)); + /* Retrieve all the results: */ + child=item->child; + while (child && !fail) + { + ret=print_value(child,depth+1,fmt); + entries[i++]=ret; + if (ret) len+=strlen(ret)+2+(fmt?1:0); else fail=1; + child=child->next; + } + + /* If we didn't fail, try to malloc the output string */ + if (!fail) out=(char*)cJSON_malloc(len); + /* If that fails, we fail. */ + if (!out) fail=1; + + /* Handle failure. */ + if (fail) + { + for (i=0;itype=cJSON_Object; + value=skip(value+1); + if (*value=='}') return value+1; /* empty array. */ + + item->child=child=cJSON_New_Item(); + if (!item->child) return 0; + value=skip(parse_string(child,skip(value))); + if (!value) return 0; + child->string=child->valuestring;child->valuestring=0; + if (*value!=':') {ep=value;return 0;} /* fail! */ + value=skip(parse_value(child,skip(value+1))); /* skip any spacing, get the value. */ + if (!value) return 0; + + while (*value==',') + { + cJSON *new_item; + if (!(new_item=cJSON_New_Item())) return 0; /* memory fail */ + child->next=new_item;new_item->prev=child;child=new_item; + value=skip(parse_string(child,skip(value+1))); + if (!value) return 0; + child->string=child->valuestring;child->valuestring=0; + if (*value!=':') {ep=value;return 0;} /* fail! */ + value=skip(parse_value(child,skip(value+1))); /* skip any spacing, get the value. */ + if (!value) return 0; + } + + if (*value=='}') return value+1; /* end of array */ + ep=value;return 0; /* malformed. */ +} + +/* Render an object to text. */ +static char *print_object(cJSON *item,int depth,int fmt) +{ + char **entries=0,**names=0; + char *out=0,*ptr,*ret,*str;int len=7,i=0,j; + cJSON *child=item->child; + int numentries=0,fail=0; + /* Count the number of entries. */ + while (child) numentries++,child=child->next; + /* Explicitly handle empty object case */ + if (!numentries) + { + out=(char*)cJSON_malloc(fmt?depth+4:3); + if (!out) return 0; + ptr=out;*ptr++='{'; + if (fmt) {*ptr++='\n';for (i=0;ichild;depth++;if (fmt) len+=depth; + while (child) + { + names[i]=str=print_string_ptr(child->string); + entries[i++]=ret=print_value(child,depth,fmt); + if (str && ret) len+=strlen(ret)+strlen(str)+2+(fmt?2+depth:0); else fail=1; + child=child->next; + } + + /* Try to allocate the output string */ + if (!fail) out=(char*)cJSON_malloc(len); + if (!out) fail=1; + + /* Handle failure */ + if (fail) + { + for (i=0;ichild;int i=0;while(c)i++,c=c->next;return i;} +cJSON *cJSON_GetArrayItem(cJSON *array,int item) {cJSON *c=array->child; while (c && item>0) item--,c=c->next; return c;} +cJSON *cJSON_GetObjectItem(cJSON *object,const char *string) {cJSON *c=object->child; while (c && cJSON_strcasecmp(c->string,string)) c=c->next; return c;} + +/* Utility for array list handling. */ +static void suffix_object(cJSON *prev,cJSON *item) {prev->next=item;item->prev=prev;} +/* Utility for handling references. */ +static cJSON *create_reference(cJSON *item) {cJSON *ref=cJSON_New_Item();if (!ref) return 0;memcpy(ref,item,sizeof(cJSON));ref->string=0;ref->type|=cJSON_IsReference;ref->next=ref->prev=0;return ref;} + +/* Add item to array/object. */ +void cJSON_AddItemToArray(cJSON *array, cJSON *item) {cJSON *c=array->child;if (!item) return; if (!c) {array->child=item;} else {while (c && c->next) c=c->next; suffix_object(c,item);}} +void cJSON_AddItemToObject(cJSON *object,const char *string,cJSON *item) {if (!item) return; if (item->string) cJSON_free(item->string);item->string=cJSON_strdup(string);cJSON_AddItemToArray(object,item);} +void cJSON_AddItemReferenceToArray(cJSON *array, cJSON *item) {cJSON_AddItemToArray(array,create_reference(item));} +void cJSON_AddItemReferenceToObject(cJSON *object,const char *string,cJSON *item) {cJSON_AddItemToObject(object,string,create_reference(item));} + +cJSON *cJSON_DetachItemFromArray(cJSON *array,int which) {cJSON *c=array->child;while (c && which>0) c=c->next,which--;if (!c) return 0; + if (c->prev) c->prev->next=c->next;if (c->next) c->next->prev=c->prev;if (c==array->child) array->child=c->next;c->prev=c->next=0;return c;} +void cJSON_DeleteItemFromArray(cJSON *array,int which) {cJSON_Delete(cJSON_DetachItemFromArray(array,which));} +cJSON *cJSON_DetachItemFromObject(cJSON *object,const char *string) {int i=0;cJSON *c=object->child;while (c && cJSON_strcasecmp(c->string,string)) i++,c=c->next;if (c) return cJSON_DetachItemFromArray(object,i);return 0;} +void cJSON_DeleteItemFromObject(cJSON *object,const char *string) {cJSON_Delete(cJSON_DetachItemFromObject(object,string));} + +/* Replace array/object items with new ones. */ +void cJSON_ReplaceItemInArray(cJSON *array,int which,cJSON *newitem) {cJSON *c=array->child;while (c && which>0) c=c->next,which--;if (!c) return; + newitem->next=c->next;newitem->prev=c->prev;if (newitem->next) newitem->next->prev=newitem; + if (c==array->child) array->child=newitem; else newitem->prev->next=newitem;c->next=c->prev=0;cJSON_Delete(c);} +void cJSON_ReplaceItemInObject(cJSON *object,const char *string,cJSON *newitem){int i=0;cJSON *c=object->child;while(c && cJSON_strcasecmp(c->string,string))i++,c=c->next;if(c){newitem->string=cJSON_strdup(string);cJSON_ReplaceItemInArray(object,i,newitem);}} + +/* Create basic types: */ +cJSON *cJSON_CreateNull(void) {cJSON *item=cJSON_New_Item();if(item)item->type=cJSON_NULL;return item;} +cJSON *cJSON_CreateTrue(void) {cJSON *item=cJSON_New_Item();if(item)item->type=cJSON_True;return item;} +cJSON *cJSON_CreateFalse(void) {cJSON *item=cJSON_New_Item();if(item)item->type=cJSON_False;return item;} +cJSON *cJSON_CreateBool(int b) {cJSON *item=cJSON_New_Item();if(item)item->type=b?cJSON_True:cJSON_False;return item;} +cJSON *cJSON_CreateNumber(double num) {cJSON *item=cJSON_New_Item();if(item){item->type=cJSON_Number;item->valuedouble=num;item->valueint=(int)num;}return item;} +cJSON *cJSON_CreateString(const char *string) {cJSON *item=cJSON_New_Item();if(item){item->type=cJSON_String;item->valuestring=cJSON_strdup(string);}return item;} +cJSON *cJSON_CreateArray(void) {cJSON *item=cJSON_New_Item();if(item)item->type=cJSON_Array;return item;} +cJSON *cJSON_CreateObject(void) {cJSON *item=cJSON_New_Item();if(item)item->type=cJSON_Object;return item;} + +/* Create Arrays: */ +cJSON *cJSON_CreateIntArray(const int *numbers,int count) {int i;cJSON *n=0,*p=0,*a=cJSON_CreateArray();for(i=0;a && ichild=n;else suffix_object(p,n);p=n;}return a;} +cJSON *cJSON_CreateFloatArray(const float *numbers,int count) {int i;cJSON *n=0,*p=0,*a=cJSON_CreateArray();for(i=0;a && ichild=n;else suffix_object(p,n);p=n;}return a;} +cJSON *cJSON_CreateDoubleArray(const double *numbers,int count) {int i;cJSON *n=0,*p=0,*a=cJSON_CreateArray();for(i=0;a && ichild=n;else suffix_object(p,n);p=n;}return a;} +cJSON *cJSON_CreateStringArray(const char **strings,int count) {int i;cJSON *n=0,*p=0,*a=cJSON_CreateArray();for(i=0;a && ichild=n;else suffix_object(p,n);p=n;}return a;} + +/* Duplication */ +cJSON *cJSON_Duplicate(cJSON *item,int recurse) +{ + cJSON *newitem,*cptr,*nptr=0,*newchild; + /* Bail on bad ptr */ + if (!item) return 0; + /* Create new item */ + newitem=cJSON_New_Item(); + if (!newitem) return 0; + /* Copy over all vars */ + newitem->type=item->type&(~cJSON_IsReference),newitem->valueint=item->valueint,newitem->valuedouble=item->valuedouble; + if (item->valuestring) {newitem->valuestring=cJSON_strdup(item->valuestring); if (!newitem->valuestring) {cJSON_Delete(newitem);return 0;}} + if (item->string) {newitem->string=cJSON_strdup(item->string); if (!newitem->string) {cJSON_Delete(newitem);return 0;}} + /* If non-recursive, then we're done! */ + if (!recurse) return newitem; + /* Walk the ->next chain for the child. */ + cptr=item->child; + while (cptr) + { + newchild=cJSON_Duplicate(cptr,1); /* Duplicate (with recurse) each item in the ->next chain */ + if (!newchild) {cJSON_Delete(newitem);return 0;} + if (nptr) {nptr->next=newchild,newchild->prev=nptr;nptr=newchild;} /* If newitem->child already set, then crosswire ->prev and ->next and move on */ + else {newitem->child=newchild;nptr=newchild;} /* Set newitem->child and move to it */ + cptr=cptr->next; + } + return newitem; +} + +void cJSON_Minify(char *json) +{ + char *into=json; + while (*json) + { + if (*json==' ') json++; + else if (*json=='\t') json++; // Whitespace characters. + else if (*json=='\r') json++; + else if (*json=='\n') json++; + else if (*json=='/' && json[1]=='/') while (*json && *json!='\n') json++; // double-slash comments, to end of line. + else if (*json=='/' && json[1]=='*') {while (*json && !(*json=='*' && json[1]=='/')) json++;json+=2;} // multiline comments. + else if (*json=='\"'){*into++=*json++;while (*json && *json!='\"'){if (*json=='\\') *into++=*json++;*into++=*json++;}*into++=*json++;} // string literals, which are \" sensitive. + else *into++=*json++; // All other characters. + } + *into=0; // and null-terminate. +} \ No newline at end of file diff --git a/src/openalpr/cjson.h b/src/openalpr/cjson.h new file mode 100644 index 0000000..867b7c3 --- /dev/null +++ b/src/openalpr/cjson.h @@ -0,0 +1,143 @@ +/* + Copyright (c) 2009 Dave Gamble + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +#ifndef cJSON__h +#define cJSON__h + +#ifdef __cplusplus +extern "C" +{ +#endif + +/* cJSON Types: */ +#define cJSON_False 0 +#define cJSON_True 1 +#define cJSON_NULL 2 +#define cJSON_Number 3 +#define cJSON_String 4 +#define cJSON_Array 5 +#define cJSON_Object 6 + +#define cJSON_IsReference 256 + +/* The cJSON structure: */ +typedef struct cJSON { + struct cJSON *next,*prev; /* next/prev allow you to walk array/object chains. Alternatively, use GetArraySize/GetArrayItem/GetObjectItem */ + struct cJSON *child; /* An array or object item will have a child pointer pointing to a chain of the items in the array/object. */ + + int type; /* The type of the item, as above. */ + + char *valuestring; /* The item's string, if type==cJSON_String */ + int valueint; /* The item's number, if type==cJSON_Number */ + double valuedouble; /* The item's number, if type==cJSON_Number */ + + char *string; /* The item's name string, if this item is the child of, or is in the list of subitems of an object. */ +} cJSON; + +typedef struct cJSON_Hooks { + void *(*malloc_fn)(size_t sz); + void (*free_fn)(void *ptr); +} cJSON_Hooks; + +/* Supply malloc, realloc and free functions to cJSON */ +extern void cJSON_InitHooks(cJSON_Hooks* hooks); + + +/* Supply a block of JSON, and this returns a cJSON object you can interrogate. Call cJSON_Delete when finished. */ +extern cJSON *cJSON_Parse(const char *value); +/* Render a cJSON entity to text for transfer/storage. Free the char* when finished. */ +extern char *cJSON_Print(cJSON *item); +/* Render a cJSON entity to text for transfer/storage without any formatting. Free the char* when finished. */ +extern char *cJSON_PrintUnformatted(cJSON *item); +/* Delete a cJSON entity and all subentities. */ +extern void cJSON_Delete(cJSON *c); + +/* Returns the number of items in an array (or object). */ +extern int cJSON_GetArraySize(cJSON *array); +/* Retrieve item number "item" from array "array". Returns NULL if unsuccessful. */ +extern cJSON *cJSON_GetArrayItem(cJSON *array,int item); +/* Get item "string" from object. Case insensitive. */ +extern cJSON *cJSON_GetObjectItem(cJSON *object,const char *string); + +/* For analysing failed parses. This returns a pointer to the parse error. You'll probably need to look a few chars back to make sense of it. Defined when cJSON_Parse() returns 0. 0 when cJSON_Parse() succeeds. */ +extern const char *cJSON_GetErrorPtr(void); + +/* These calls create a cJSON item of the appropriate type. */ +extern cJSON *cJSON_CreateNull(void); +extern cJSON *cJSON_CreateTrue(void); +extern cJSON *cJSON_CreateFalse(void); +extern cJSON *cJSON_CreateBool(int b); +extern cJSON *cJSON_CreateNumber(double num); +extern cJSON *cJSON_CreateString(const char *string); +extern cJSON *cJSON_CreateArray(void); +extern cJSON *cJSON_CreateObject(void); + +/* These utilities create an Array of count items. */ +extern cJSON *cJSON_CreateIntArray(const int *numbers,int count); +extern cJSON *cJSON_CreateFloatArray(const float *numbers,int count); +extern cJSON *cJSON_CreateDoubleArray(const double *numbers,int count); +extern cJSON *cJSON_CreateStringArray(const char **strings,int count); + +/* Append item to the specified array/object. */ +extern void cJSON_AddItemToArray(cJSON *array, cJSON *item); +extern void cJSON_AddItemToObject(cJSON *object,const char *string,cJSON *item); +/* Append reference to item to the specified array/object. Use this when you want to add an existing cJSON to a new cJSON, but don't want to corrupt your existing cJSON. */ +extern void cJSON_AddItemReferenceToArray(cJSON *array, cJSON *item); +extern void cJSON_AddItemReferenceToObject(cJSON *object,const char *string,cJSON *item); + +/* Remove/Detatch items from Arrays/Objects. */ +extern cJSON *cJSON_DetachItemFromArray(cJSON *array,int which); +extern void cJSON_DeleteItemFromArray(cJSON *array,int which); +extern cJSON *cJSON_DetachItemFromObject(cJSON *object,const char *string); +extern void cJSON_DeleteItemFromObject(cJSON *object,const char *string); + +/* Update array items. */ +extern void cJSON_ReplaceItemInArray(cJSON *array,int which,cJSON *newitem); +extern void cJSON_ReplaceItemInObject(cJSON *object,const char *string,cJSON *newitem); + +/* Duplicate a cJSON item */ +extern cJSON *cJSON_Duplicate(cJSON *item,int recurse); +/* Duplicate will create a new, identical cJSON item to the one you pass, in new memory that will +need to be released. With recurse!=0, it will duplicate any children connected to the item. +The item->next and ->prev pointers are always zero on return from Duplicate. */ + +/* ParseWithOpts allows you to require (and check) that the JSON is null terminated, and to retrieve the pointer to the final byte parsed. */ +extern cJSON *cJSON_ParseWithOpts(const char *value,const char **return_parse_end,int require_null_terminated); + +extern void cJSON_Minify(char *json); + +/* Macros for creating things quickly. */ +#define cJSON_AddNullToObject(object,name) cJSON_AddItemToObject(object, name, cJSON_CreateNull()) +#define cJSON_AddTrueToObject(object,name) cJSON_AddItemToObject(object, name, cJSON_CreateTrue()) +#define cJSON_AddFalseToObject(object,name) cJSON_AddItemToObject(object, name, cJSON_CreateFalse()) +#define cJSON_AddBoolToObject(object,name,b) cJSON_AddItemToObject(object, name, cJSON_CreateBool(b)) +#define cJSON_AddNumberToObject(object,name,n) cJSON_AddItemToObject(object, name, cJSON_CreateNumber(n)) +#define cJSON_AddStringToObject(object,name,s) cJSON_AddItemToObject(object, name, cJSON_CreateString(s)) + +/* When assigning an integer value, it needs to be propagated to valuedouble too. */ +#define cJSON_SetIntValue(object,val) ((object)?(object)->valueint=(object)->valuedouble=(val):(val)) + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/openalpr/colorfilter.cpp b/src/openalpr/colorfilter.cpp new file mode 100644 index 0000000..71e938f --- /dev/null +++ b/src/openalpr/colorfilter.cpp @@ -0,0 +1,421 @@ +/* + * 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 "colorfilter.h" + + + +ColorFilter::ColorFilter(Mat image, Mat characterMask, Config* config) +{ + + timespec startTime; + getTime(&startTime); + + this->config = config; + + this->debug = config->debugColorFiler; + + + this->grayscale = imageIsGrayscale(image); + + if (this->debug) + cout << "ColorFilter: isGrayscale = " << grayscale << endl; + + this->hsv = Mat(image.size(), image.type()); + cvtColor( image, this->hsv, CV_BGR2HSV ); + preprocessImage(); + + this->charMask = characterMask; + + this->colorMask = Mat(image.size(), CV_8U); + + findCharColors(); + + + if (config->debugTiming) + { + timespec endTime; + getTime(&endTime); + cout << " -- ColorFilter Time: " << diffclock(startTime, endTime) << "ms." << endl; + } +} + +ColorFilter::~ColorFilter() +{ + +} + +bool ColorFilter::imageIsGrayscale(Mat image) +{ + // Check whether the original image is grayscale. If it is, we shouldn't attempt any color filter + for (int row = 0; row < image.rows; row++) + { + for (int col = 0; col < image.cols; col++) + { + int r = (int) image.at(row, col)[0]; + int g = (int) image.at(row, col)[1]; + int b = (int) image.at(row, col)[2]; + + if (r == g == b) + { + // So far so good + } + else + { + // Image is color. + return false; + } + } + } + + return true; +} + +void ColorFilter::preprocessImage() +{ + // Equalize the brightness on the HSV channel "V" + vector channels; + split(this->hsv,channels); + Mat img_equalized = equalizeBrightness(channels[2]); + merge(channels,this->hsv); +} + +// Gets the hue/sat/val for areas that we believe are license plate characters +// Then uses that to filter the whole image and provide a mask. +void ColorFilter::findCharColors() +{ + int MINIMUM_SATURATION = 45; + + if (this->debug) + cout << "ColorFilter::findCharColors" << endl; + + //charMask.copyTo(this->colorMask); + this->colorMask = Mat::zeros(charMask.size(), CV_8U); + bitwise_not(this->colorMask, this->colorMask); + + Mat erodedCharMask(charMask.size(), CV_8U); + Mat element = getStructuringElement( 1, + Size( 2 + 1, 2+1 ), + Point( 1, 1 ) ); + erode(charMask, erodedCharMask, element); + + vector > contours; + vector hierarchy; + findContours(erodedCharMask, contours, hierarchy, CV_RETR_TREE, CV_CHAIN_APPROX_SIMPLE); + + + + vector hMeans, sMeans, vMeans; + vector hStdDevs, sStdDevs, vStdDevs; + + for (int i = 0; i < contours.size(); i++) + { + if (hierarchy[i][3] != -1) + continue; + + Mat singleCharMask = Mat::zeros(hsv.size(), CV_8U); + + drawContours(singleCharMask, contours, + i, // draw this contour + cv::Scalar(255,255,255), // in + CV_FILLED, + 8, + hierarchy + ); + + // get rid of the outline by drawing a 1 pixel width black line + drawContours(singleCharMask, contours, + i, // draw this contour + cv::Scalar(0,0,0), // in + 1, + 8, + hierarchy + ); + + + + + //drawAndWait(&singleCharMask); + + Scalar mean; + Scalar stddev; + meanStdDev(hsv, mean, stddev, singleCharMask); + + if (this->debug) + { + cout << "ColorFilter " << setw(3) << i << ". Mean: h: " << setw(7) << mean[0] << " s: " << setw(7) <getMajorityOpinion(hMeans, .65, 30); + int bestSatIndex = this->getMajorityOpinion(sMeans, .65, 35); + int bestValIndex = this->getMajorityOpinion(vMeans, .65, 30); + + + if (sMeans[bestSatIndex] < MINIMUM_SATURATION) + return; + + + bool doHueFilter = false, doSatFilter = false, doValFilter = false; + float hueMin, hueMax; + float satMin, satMax; + float valMin, valMax; + + if (this->debug) + cout << "ColorFilter Winning indices:" << endl; + if (bestHueIndex != -1) + { + doHueFilter = true; + hueMin = hMeans[bestHueIndex] - (2 * hStdDevs[bestHueIndex]); + hueMax = hMeans[bestHueIndex] + (2 * hStdDevs[bestHueIndex]); + + if (abs(hueMin - hueMax) < 20) + { + hueMin = hMeans[bestHueIndex] - 20; + hueMax = hMeans[bestHueIndex] + 20; + } + + if (hueMin < 0) + hueMin = 0; + if (hueMax > 180) + hueMax = 180; + + if (this->debug) + cout << "ColorFilter Hue: " << bestHueIndex << " : " << setw(7) << hMeans[bestHueIndex] << " -- " << hueMin << "-" << hueMax << endl; + } + if (bestSatIndex != -1) + { + doSatFilter = true; + + satMin = sMeans[bestSatIndex] - (2 * sStdDevs[bestSatIndex]); + satMax = sMeans[bestSatIndex] + (2 * sStdDevs[bestSatIndex]); + + if (abs(satMin - satMax) < 20) + { + satMin = sMeans[bestSatIndex] - 20; + satMax = sMeans[bestSatIndex] + 20; + } + + if (satMin < 0) + satMin = 0; + if (satMax > 255) + satMax = 255; + + if (this->debug) + cout << "ColorFilter Sat: " << bestSatIndex << " : " << setw(7) << sMeans[bestSatIndex] << " -- " << satMin << "-" << satMax << endl; + } + if (bestValIndex != -1) + { + doValFilter = true; + + valMin = vMeans[bestValIndex] - (1.5 * vStdDevs[bestValIndex]); + valMax = vMeans[bestValIndex] + (1.5 * vStdDevs[bestValIndex]); + + if (abs(valMin - valMax) < 20) + { + valMin = vMeans[bestValIndex] - 20; + valMax = vMeans[bestValIndex] + 20; + } + + if (valMin < 0) + valMin = 0; + if (valMax > 255) + valMax = 255; + + if (this->debug) + cout << "ColorFilter Val: " << bestValIndex << " : " << setw(7) << vMeans[bestValIndex] << " -- " << valMin << "-" << valMax << endl; + } + + + + Mat imgDebugHueOnly = Mat::zeros(hsv.size(), hsv.type()); + Mat imgDebug = Mat::zeros(hsv.size(), hsv.type()); + Mat imgDistanceFromCenter = Mat::zeros(hsv.size(), CV_8U); + Mat debugMask = Mat::zeros(hsv.size(), CV_8U); + bitwise_not(debugMask, debugMask); + + for (int row = 0; row < charMask.rows; row++) + { + for (int col = 0; col < charMask.cols; col++) + { + int h = (int) hsv.at(row, col)[0]; + int s = (int) hsv.at(row, col)[1]; + int v = (int) hsv.at(row, col)[2]; + + bool hPasses = true; + bool sPasses = true; + bool vPasses = true; + + int vDistance = abs(v - vMeans[bestValIndex]); + + imgDebugHueOnly.at(row, col)[0] = h; + imgDebugHueOnly.at(row, col)[1] = 255; + imgDebugHueOnly.at(row, col)[2] = 255; + + imgDebug.at(row, col)[0] = 255; + imgDebug.at(row, col)[1] = 255; + imgDebug.at(row, col)[2] = 255; + + if (doHueFilter && (h < hueMin || h > hueMax)) + { + hPasses = false; + imgDebug.at(row, col)[0] = 0; + debugMask.at(row, col) = 0; + } + if (doSatFilter && (s < satMin || s > satMax)) + { + sPasses = false; + imgDebug.at(row, col)[1] = 0; + } + if (doValFilter && (v < valMin || v > valMax)) + { + vPasses = false; + imgDebug.at(row, col)[2] = 0; + } + + //if (pixelPasses) + // colorMask.at(row, col) = 255; + //else + //imgDebug.at(row, col)[0] = hPasses & 255; + //imgDebug.at(row, col)[1] = sPasses & 255; + //imgDebug.at(row, col)[2] = vPasses & 255; + + if ((hPasses) || (hPasses && sPasses))//(hPasses && vPasses) || (sPasses && vPasses) || + this->colorMask.at(row, col) = 255; + else + this->colorMask.at(row, col) = 0; + + + if ((hPasses && sPasses) || (hPasses && vPasses) || (sPasses && vPasses)) + { + vDistance = pow(vDistance, 0.9); + } + else + { + vDistance = pow(vDistance, 1.1); + } + if (vDistance > 255) + vDistance = 255; + imgDistanceFromCenter.at(row, col) = vDistance; + } + } + + + + vector debugImagesSet; + + if (this->debug) + { + debugImagesSet.push_back(addLabel(charMask, "Charecter mask")); + //debugImagesSet1.push_back(erodedCharMask); + Mat maskCopy(colorMask.size(), colorMask.type()); + colorMask.copyTo(maskCopy); + debugImagesSet.push_back(addLabel(maskCopy, "color Mask Before")); + } + + + Mat bigElement = getStructuringElement( 1, + Size( 3 + 1, 3+1 ), + Point( 1, 1 ) ); + + Mat smallElement = getStructuringElement( 1, + Size( 1 + 1, 1+1 ), + Point( 1, 1 ) ); + + morphologyEx(this->colorMask, this->colorMask, MORPH_CLOSE, bigElement); + //dilate(this->colorMask, this->colorMask, bigElement); + + Mat combined(charMask.size(), charMask.type()); + bitwise_and(charMask, colorMask, combined); + + if (this->debug) + { + debugImagesSet.push_back(addLabel(colorMask, "Color Mask After")); + + debugImagesSet.push_back(addLabel(combined, "Combined")); + + //displayImage(config, "COLOR filter Mask", colorMask); + debugImagesSet.push_back(addLabel(imgDebug, "Color filter Debug")); + + cvtColor(imgDebugHueOnly, imgDebugHueOnly, CV_HSV2BGR); + debugImagesSet.push_back(addLabel(imgDebugHueOnly, "Color Filter Hue")); + + equalizeHist(imgDistanceFromCenter, imgDistanceFromCenter); + debugImagesSet.push_back(addLabel(imgDistanceFromCenter, "COLOR filter Distance")); + + debugImagesSet.push_back(addLabel(debugMask, "COLOR Hues off")); + + + Mat dashboard = drawImageDashboard(debugImagesSet, imgDebugHueOnly.type(), 3); + displayImage(config, "Color Filter Images", dashboard); + } + +} + + + +// Goes through an array of values, picks the winner based on the highest percentage of other values that are within the maxValDifference +// Return -1 if it fails. +int ColorFilter::getMajorityOpinion(vector values, float minPercentAgreement, float maxValDifference) +{ + float bestPercentAgreement = 0; + float lowestOverallDiff = 1000000000; + int bestPercentAgreementIndex = -1; + + for (int i = 0; i < values.size(); i++) + { + int valuesInRange = 0; + float overallDiff = 0; + for (int j = 0; j < values.size(); j++) + { + float diff = abs(values[i] - values[j]); + if (diff < maxValDifference) + valuesInRange++; + + overallDiff += diff; + } + + float percentAgreement = ((float) valuesInRange) / ((float) values.size()); + if (overallDiff < lowestOverallDiff && percentAgreement >= bestPercentAgreement && percentAgreement >= minPercentAgreement) + { + bestPercentAgreement = percentAgreement; + bestPercentAgreementIndex = i; + lowestOverallDiff = overallDiff; + } + } + + return bestPercentAgreementIndex; +} diff --git a/src/openalpr/colorfilter.h b/src/openalpr/colorfilter.h new file mode 100644 index 0000000..d4075ac --- /dev/null +++ b/src/openalpr/colorfilter.h @@ -0,0 +1,65 @@ +/* + * 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 COLORFILTER_H +#define COLORFILTER_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 ColorFilter +{ + + public: + ColorFilter(Mat image, Mat characterMask, Config* config); + virtual ~ColorFilter(); + + Mat colorMask; + + + + private: + + Config* config; + bool debug; + + Mat hsv; + Mat charMask; + + + bool grayscale; + + void preprocessImage(); + void findCharColors(); + + bool imageIsGrayscale(Mat image); + int getMajorityOpinion(vector values, float minPercentAgreement, float maxValDifference); +}; + +#endif // COLORFILTER_H \ No newline at end of file diff --git a/src/openalpr/config.cpp b/src/openalpr/config.cpp new file mode 100644 index 0000000..50a1720 --- /dev/null +++ b/src/openalpr/config.cpp @@ -0,0 +1,219 @@ +/* + * 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 "config.h" + + +Config::Config(const std::string country, const std::string runtimeBaseDir) +{ + this->runtimeBaseDir = runtimeBaseDir; + + ini = new CSimpleIniA(); + + char* envRuntimeDir; + envRuntimeDir = getenv (ENV_VARIABLE_RUNTIME_DIR); + if (runtimeBaseDir.compare("") != 0) + { + // User has supplied a runtime directory. Use that. + + } + else if (envRuntimeDir!=NULL) + { + // Environment variable is non-empty. Use that. + this->runtimeBaseDir = envRuntimeDir; + } + else + { + // Use the default + this->runtimeBaseDir = DEFAULT_RUNTIME_DIR; + } + + string configFile = (this->runtimeBaseDir + CONFIG_FILE); + + if (DirectoryExists(this->runtimeBaseDir.c_str()) == false) + { + std::cerr << "--(!)Runtime directory '" << this->runtimeBaseDir << "' does not exist!" << endl; + return; + } + else if (fileExists(configFile.c_str()) == false) + { + std::cerr << "--(!)Runtime directory '" << this->runtimeBaseDir << "' does not contain a config file '" << CONFIG_FILE << "'!" << endl; + return; + } + + ini->LoadFile(configFile.c_str()); + + this->country = country; + + loadValues(country); +} +Config::~Config() +{ + delete ini; +} + +void Config::loadValues(string country) +{ + maxPlateWidthPercent = getFloat("common", "max_plate_width_percent", 100); + maxPlateHeightPercent = getFloat("common", "max_plate_height_percent", 100); + + minPlateSizeWidthPx = getInt(country, "min_plate_size_width_px", 100); + minPlateSizeHeightPx = getInt(country, "min_plate_size_height_px", 100); + + plateWidthMM = getFloat(country, "plate_width_mm", 100); + plateHeightMM = getFloat(country, "plate_height_mm", 100); + + charHeightMM = getFloat(country, "char_height_mm", 100); + charWidthMM = getFloat(country, "char_width_mm", 100); + charWhitespaceTopMM = getFloat(country, "char_whitespace_top_mm", 100); + charWhitespaceBotMM = getFloat(country, "char_whitespace_bot_mm", 100); + + templateWidthPx = getInt(country, "template_max_width_px", 100); + templateHeightPx = getInt(country, "template_max_height_px", 100); + + float ocrImagePercent = getFloat("common", "ocr_img_size_percent", 100); + ocrImageWidthPx = round(((float) templateWidthPx) * ocrImagePercent); + ocrImageHeightPx = round(((float)templateHeightPx) * ocrImagePercent); + + + float stateIdImagePercent = getFloat("common", "state_id_img_size_percent", 100); + stateIdImageWidthPx = round(((float)templateWidthPx) * ocrImagePercent); + stateIdimageHeightPx = round(((float)templateHeightPx) * ocrImagePercent); + + + charAnalysisMinPercent = getFloat(country, "char_analysis_min_pct", 0); + charAnalysisHeightRange = getFloat(country, "char_analysis_height_range", 0); + charAnalysisHeightStepSize = getFloat(country, "char_analysis_height_step_size", 0); + charAnalysisNumSteps = getInt(country, "char_analysis_height_num_steps", 0); + + segmentationMinBoxWidthPx = getInt(country, "segmentation_min_box_width_px", 0); + segmentationMinCharHeightPercent = getFloat(country, "segmentation_min_charheight_percent", 0); + segmentationMaxCharWidthvsAverage = getFloat(country, "segmentation_max_segment_width_percent_vs_average", 0); + + plateLinesSensitivityVertical = getFloat(country, "plateline_sensitivity_vertical", 0); + plateLinesSensitivityHorizontal = getFloat(country, "plateline_sensitivity_horizontal", 0); + + ocrLanguage = getString(country, "ocr_language", "none"); + ocrMinFontSize = getInt("common", "ocr_min_font_point", 100); + + postProcessMinConfidence = getFloat("common", "postprocess_min_confidence", 100); + postProcessConfidenceSkipLevel = getFloat("common", "postprocess_confidence_skip_level", 100); + postProcessMaxSubstitutions = getInt("common", "postprocess_max_substitutions", 100); + postProcessMinCharacters = getInt("common", "postprocess_min_characers", 100); + postProcessMaxCharacters = getInt("common", "postprocess_max_characers", 100); + + debugGeneral = getBoolean("debug", "general", false); + debugTiming = getBoolean("debug", "timing", false); + debugStateId = getBoolean("debug", "state_id", false); + debugPlateLines = getBoolean("debug", "plate_lines", false); + debugPlateCorners = getBoolean("debug", "plate_corners", false); + debugCharRegions = getBoolean("debug", "char_regions", false); + debugCharSegmenter = getBoolean("debug", "char_segment", false); + debugCharAnalysis = getBoolean("debug", "char_analysis", false); + debugColorFiler = getBoolean("debug", "color_filter", false); + debugOcr = getBoolean("debug", "ocr", false); + debugPostProcess = getBoolean("debug", "postprocess", false); + debugShowImages = getBoolean("debug", "show_images", false); + +} + +void Config::debugOff() +{ + debugGeneral = false; + debugTiming = false; + debugStateId = false; + debugPlateLines = false; + debugPlateCorners = false; + debugCharRegions = false; + debugCharSegmenter = false; + debugCharAnalysis = false; + debugColorFiler = false; + debugOcr = false; + debugPostProcess = false; +} + + +string Config::getCascadeRuntimeDir() +{ + return this->runtimeBaseDir + CASCADE_DIR; +} +string Config::getKeypointsRuntimeDir() +{ + return this->runtimeBaseDir + KEYPOINTS_DIR; +} +string Config::getPostProcessRuntimeDir() +{ + return this->runtimeBaseDir + POSTPROCESS_DIR; +} +string Config::getTessdataPrefix() +{ + return "TESSDATA_PREFIX=" + this->runtimeBaseDir + "/ocr/"; +} + + + + +float Config::getFloat(string section, string key, float defaultValue) +{ + const char * pszValue = ini->GetValue(section.c_str(), key.c_str(), NULL /*default*/); + if (pszValue == NULL) + { + std::cout << "Error: missing configuration entry for: " << section << "->" << key << endl; + return defaultValue; + } + + float val = atof(pszValue); + return val; +} +int Config::getInt(string section, string key, int defaultValue) +{ + const char * pszValue = ini->GetValue(section.c_str(), key.c_str(), NULL /*default*/); + if (pszValue == NULL) + { + std::cout << "Error: missing configuration entry for: " << section << "->" << key << endl; + return defaultValue; + } + + int val = atoi(pszValue); + return val; +} +bool Config::getBoolean(string section, string key, bool defaultValue) +{ + const char * pszValue = ini->GetValue(section.c_str(), key.c_str(), NULL /*default*/); + if (pszValue == NULL) + { + std::cout << "Error: missing configuration entry for: " << section << "->" << key << endl; + return defaultValue; + } + + int val = atoi(pszValue); + return val != 0; +} +string Config::getString(string section, string key, string defaultValue) +{ + const char * pszValue = ini->GetValue(section.c_str(), key.c_str(), NULL /*default*/); + if (pszValue == NULL) + { + std::cout << "Error: missing configuration entry for: " << section << "->" << key << endl; + return defaultValue; + } + + string val = string(pszValue); + return val; +} diff --git a/src/openalpr/config.h b/src/openalpr/config.h new file mode 100644 index 0000000..b18dc07 --- /dev/null +++ b/src/openalpr/config.h @@ -0,0 +1,125 @@ +/* + * 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 CONFIG_H +#define CONFIG_H + + +#include "simpleini/simpleini.h" +#include "support/filesystem.h" + +#include "linux_dev.h" + +#include +#include +#include /* getenv */ +#include + +using namespace std; + +class Config +{ + + public: + Config(const std::string country, const std::string runtimeDir = ""); + virtual ~Config(); + + string country; + + float maxPlateWidthPercent; + float maxPlateHeightPercent; + + float minPlateSizeWidthPx; + float minPlateSizeHeightPx; + + float plateWidthMM; + float plateHeightMM; + + float charHeightMM; + float charWidthMM; + float charWhitespaceTopMM; + float charWhitespaceBotMM; + + int templateWidthPx; + int templateHeightPx; + + int ocrImageWidthPx; + int ocrImageHeightPx; + + int stateIdImageWidthPx; + int stateIdimageHeightPx; + + float charAnalysisMinPercent; + float charAnalysisHeightRange; + float charAnalysisHeightStepSize; + int charAnalysisNumSteps; + + float plateLinesSensitivityVertical; + float plateLinesSensitivityHorizontal; + + int segmentationMinBoxWidthPx; + float segmentationMinCharHeightPercent; + float segmentationMaxCharWidthvsAverage; + + string ocrLanguage; + int ocrMinFontSize; + + float postProcessMinConfidence; + float postProcessConfidenceSkipLevel; + int postProcessMaxSubstitutions; + int postProcessMinCharacters; + int postProcessMaxCharacters; + + + bool debugGeneral; + bool debugTiming; + bool debugStateId; + bool debugPlateLines; + bool debugPlateCorners; + bool debugCharRegions; + bool debugCharSegmenter; + bool debugCharAnalysis; + bool debugColorFiler; + bool debugOcr; + bool debugPostProcess; + bool debugShowImages; + + void debugOff(); + + string getKeypointsRuntimeDir(); + string getCascadeRuntimeDir(); + string getPostProcessRuntimeDir(); + string getTessdataPrefix(); + +private: + CSimpleIniA* ini; + + string runtimeBaseDir; + + void loadValues(string country); + + int getInt(string section, string key, int defaultValue); + float getFloat(string section, string key, float defaultValue); + string getString(string section, string key, string defaultValue); + bool getBoolean(string section, string key, bool defaultValue); +}; + + +#endif // CONFIG_H \ No newline at end of file diff --git a/src/openalpr/constants.h b/src/openalpr/constants.h new file mode 100644 index 0000000..b014f7b --- /dev/null +++ b/src/openalpr/constants.h @@ -0,0 +1,21 @@ +/* + * 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 "linux_dev.h" + diff --git a/src/openalpr/featurematcher.cpp b/src/openalpr/featurematcher.cpp new file mode 100644 index 0000000..b21a289 --- /dev/null +++ b/src/openalpr/featurematcher.cpp @@ -0,0 +1,432 @@ +/* + * 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 "featurematcher.h" + + +//const int DEFAULT_QUERY_FEATURES = 305; +//const int DEFAULT_TRAINING_FEATURES = 305; +const float MAX_DISTANCE_TO_MATCH = 100.0f; + + +FeatureMatcher::FeatureMatcher(Config* config) +{ + this->config = config; + + //this->descriptorMatcher = DescriptorMatcher::create( "BruteForce-HammingLUT" ); + this->descriptorMatcher = new BFMatcher(NORM_HAMMING, false); + + //this->descriptorMatcher = DescriptorMatcher::create( "FlannBased" ); + + + this->detector = new FastFeatureDetector(10, true); + this->extractor = new BRISK(10, 1, 0.9); +} + +FeatureMatcher::~FeatureMatcher() +{ + for (int i = 0; i < trainingImgKeypoints.size(); i++) + trainingImgKeypoints[i].clear(); + trainingImgKeypoints.clear(); + + descriptorMatcher.release(); + detector.release(); + extractor.release(); + +} + + +bool FeatureMatcher::isLoaded() +{ + if( detector.empty() || extractor.empty() || descriptorMatcher.empty() ) + { + return false; + } + + return true; +} + +int FeatureMatcher::numTrainingElements() +{ + return billMapping.size(); +} + + + + +void FeatureMatcher::surfStyleMatching( const Mat& queryDescriptors, vector queryKeypoints, + vector& matches12 ) +{ + vector > matchesKnn; + + this->descriptorMatcher->radiusMatch(queryDescriptors, matchesKnn, MAX_DISTANCE_TO_MATCH); + + + vector tempMatches; + _surfStyleMatching(queryDescriptors, matchesKnn, tempMatches); + + crisscrossFiltering(queryKeypoints, tempMatches, matches12); +} + + + +void FeatureMatcher::_surfStyleMatching(const Mat& queryDescriptors, vector > matchesKnn, vector& matches12) +{ + + //objectMatches.clear(); + //objectMatches.resize(objectIds.size()); + //cout << "starting matcher" << matchesKnn.size() << endl; + for (int descInd = 0; descInd < queryDescriptors.rows; descInd++) + { + const std::vector & matches = matchesKnn[descInd]; + //cout << "two: " << descInd << ":" << matches.size() << endl; + + // Check to make sure we have 2 matches. I think this is always the case, but it doesn't hurt to be sure + if (matchesKnn[descInd].size() > 1) + { + + // Next throw out matches with a crappy score + // Ignore... already handled by the radiusMatch + //if (matchesKnn[descInd][0].distance < MAX_DISTANCE_TO_MATCH) + //{ + float ratioThreshold = 0.75; + + // Check if both matches came from the same image. If they both came from the same image, score them slightly less harshly + if (matchesKnn[descInd][0].imgIdx == matchesKnn[descInd][1].imgIdx) + { + ratioThreshold = 0.85; + } + + if ((matchesKnn[descInd][0].distance / matchesKnn[descInd][1].distance) < ratioThreshold) + { + bool already_exists = false; + // Quickly run through the matches we've already added and make sure it's not a duplicate... + for (int q = 0; q < matches12.size(); q++) + { + if (matchesKnn[descInd][0].queryIdx == matches12[q].queryIdx) + { + already_exists = true; + break; + } + else if ((matchesKnn[descInd][0].trainIdx == matches12[q].trainIdx) && + (matchesKnn[descInd][0].imgIdx == matches12[q].imgIdx)) + { + already_exists = true; + break; + } + } + + // Good match. + if (already_exists == false) + matches12.push_back(matchesKnn[descInd][0]); + } + + + //} + } + else if (matchesKnn[descInd].size() == 1) + { + // Only match? Does this ever happen? + matches12.push_back(matchesKnn[descInd][0]); + } + // In the ratio test, we will compare the quality of a match with the next match that is not from the same object: + // we can accept several matches with similar scores as long as they are for the same object. Those should not be + // part of the model anyway as they are not discriminative enough + + //for (unsigned int first_index = 0; first_index < matches.size(); ++first_index) + //{ + + //matches12.push_back(match); + //} + + + } + + + +} + +// Compares the matches keypoints for parallel lines. Removes matches that are criss-crossing too much +// We assume that license plates won't be upside-down or backwards. So expect lines to be closely parallel +void FeatureMatcher::crisscrossFiltering(const vector queryKeypoints, const vector inputMatches, vector &outputMatches) +{ + + Rect crissCrossAreaVertical(0, 0, config->stateIdImageWidthPx, config->stateIdimageHeightPx * 2); + Rect crissCrossAreaHorizontal(0, 0, config->stateIdImageWidthPx * 2, config->stateIdimageHeightPx); + + for (int i = 0; i < billMapping.size(); i++) + { + vector matchesForOnePlate; + for (int j = 0; j < inputMatches.size(); j++) + { + if (inputMatches[j].imgIdx == i) + matchesForOnePlate.push_back(inputMatches[j]); + } + + // For each plate, compare the lines for the keypoints (training image and query image) + // go through each line between keypoints and filter out matches that are criss-crossing + vector vlines; + vector hlines; + vector matchIdx; + + for (int j = 0; j < matchesForOnePlate.size(); j++) + { + KeyPoint tkp = trainingImgKeypoints[i][matchesForOnePlate[j].trainIdx]; + KeyPoint qkp = queryKeypoints[matchesForOnePlate[j].queryIdx]; + + vlines.push_back(LineSegment(tkp.pt.x, tkp.pt.y + config->stateIdimageHeightPx, qkp.pt.x, qkp.pt.y)); + hlines.push_back(LineSegment(tkp.pt.x, tkp.pt.y, qkp.pt.x + config->stateIdImageWidthPx, qkp.pt.y)); + matchIdx.push_back(j); + } + + + + // Iterate through each line (n^2) removing the one with the most criss-crosses until there are none left. + int mostIntersections = 1; + while (mostIntersections > 0 && vlines.size() > 0) + { + int mostIntersectionsIndex = -1; + mostIntersections = 0; + + for (int j = 0; j < vlines.size(); j++) + { + int intrCount = 0; + for (int q = 0; q < vlines.size(); q++) + { + Point vintr = vlines[j].intersection(vlines[q]); + Point hintr = hlines[j].intersection(hlines[q]); + float vangleDiff = abs(vlines[j].angle - vlines[q].angle); + float hangleDiff = abs(hlines[j].angle - hlines[q].angle); + if (vintr.inside(crissCrossAreaVertical) && vangleDiff > 10) + { + intrCount++; + } + else if (hintr.inside(crissCrossAreaHorizontal) && hangleDiff > 10) + { + intrCount++; + } + } + + if (intrCount > mostIntersections) + { + mostIntersections = intrCount; + mostIntersectionsIndex = j; + } + } + + if (mostIntersectionsIndex >= 0) + { + if (this->config->debugStateId) + cout << "Filtered intersection! " << billMapping[i] << endl; + vlines.erase(vlines.begin() + mostIntersectionsIndex); + hlines.erase(hlines.begin() + mostIntersectionsIndex); + matchIdx.erase(matchIdx.begin() + mostIntersectionsIndex); + } + + } + + // Push the non-crisscrosses back on the list + for (int j = 0; j < matchIdx.size(); j++) + { + outputMatches.push_back(matchesForOnePlate[matchIdx[j]]); + } + } + +} + + +// Returns true if successful, false otherwise +bool FeatureMatcher::loadRecognitionSet(string country) +{ + std::ostringstream out; + out << config->getKeypointsRuntimeDir() << "/" << country << "/"; + string country_dir = out.str(); + + + if (DirectoryExists(country_dir.c_str())) + { + vector trainImages; + vector plateFiles = getFilesInDir(country_dir.c_str()); + + for (int i = 0; i < plateFiles.size(); i++) + { + if (hasEnding(plateFiles[i], ".jpg") == false) + continue; + + string fullpath = country_dir + plateFiles[i]; + Mat img = imread( fullpath ); + + // convert to gray and resize to the size of the templates + cvtColor(img, img, CV_BGR2GRAY); + resize(img, img, getSizeMaintainingAspect(img, config->stateIdImageWidthPx, config->stateIdimageHeightPx)); + + if( img.empty() ) + { + cout << "Can not read images" << endl; + return -1; + } + + + Mat descriptors; + + vector keypoints; + detector->detect( img, keypoints ); + extractor->compute(img, keypoints, descriptors); + + if (descriptors.cols > 0) + { + billMapping.push_back(plateFiles[i].substr(0, 2)); + trainImages.push_back(descriptors); + trainingImgKeypoints.push_back(keypoints); + } + + } + + + this->descriptorMatcher->add(trainImages); + this->descriptorMatcher->train(); + + return true; + } + + return false; + +} + + + +RecognitionResult FeatureMatcher::recognize( const Mat& queryImg, bool drawOnImage, Mat* outputImage, + bool debug_on, vector debug_matches_array + ) +{ + RecognitionResult result; + + result.haswinner = false; + + Mat queryDescriptors; + vector queryKeypoints; + + detector->detect( queryImg, queryKeypoints ); + extractor->compute(queryImg, queryKeypoints, queryDescriptors); + + + + if (queryKeypoints.size() <= 5) + { + // Cut it loose if there's less than 5 keypoints... nothing would ever match anyway and it could crash the matcher. + if (drawOnImage) + { + drawKeypoints( queryImg, queryKeypoints, *outputImage, CV_RGB(0, 255, 0), DrawMatchesFlags::DEFAULT ); + } + return result; + } + + + + vector filteredMatches; + + surfStyleMatching( queryDescriptors, queryKeypoints, filteredMatches ); + + + // Create and initialize the counts to 0 + std::vector bill_match_counts( billMapping.size() ); + + for (int i = 0; i < billMapping.size(); i++) { bill_match_counts[i] = 0; } + + for (int i = 0; i < filteredMatches.size(); i++) + { + bill_match_counts[filteredMatches[i].imgIdx]++; + //if (filteredMatches[i].imgIdx + } + + + + float max_count = 0; // represented as a percent (0 to 100) + int secondmost_count = 0; + int maxcount_index = -1; + for (int i = 0; i < billMapping.size(); i++) + { + if (bill_match_counts[i] > max_count && bill_match_counts[i] >= 4) + { + secondmost_count = max_count; + if (secondmost_count <= 2) // A value of 1 or 2 is effectively 0 + secondmost_count = 0; + + max_count = bill_match_counts[i]; + maxcount_index = i; + } + } + + float score = ((max_count - secondmost_count - 3) / 10) * 100; + if (score < 0) + score = 0; + else if (score > 100) + score = 100; + + + if (score > 0) + { + result.haswinner = true; + result.winner = billMapping[maxcount_index]; + result.confidence = score; + + if (drawOnImage) + { + vector positiveMatches; + for (int i = 0; i < filteredMatches.size(); i++) + { + if (filteredMatches[i].imgIdx == maxcount_index) + { + positiveMatches.push_back( queryKeypoints[filteredMatches[i].queryIdx] ); + } + } + + Mat tmpImg; + drawKeypoints( queryImg, queryKeypoints, tmpImg, CV_RGB(185, 0, 0), DrawMatchesFlags::DEFAULT ); + drawKeypoints( tmpImg, positiveMatches, *outputImage, CV_RGB(0, 255, 0), DrawMatchesFlags::DEFAULT ); + + if (result.haswinner == true) + { + + std::ostringstream out; + out << result.winner << " (" << result.confidence << "%)"; + + // we detected a bill, let the people know! + //putText(*outputImage, out.str(), Point(15, 27), FONT_HERSHEY_DUPLEX, 1.1, CV_RGB(0, 0, 0), 2); + } + } + + } + + if (this->config->debugStateId) + { + + for (int i = 0; i < billMapping.size(); i++) + { + cout << billMapping[i] << " : " << bill_match_counts[i] << endl; + } + + } + + return result; + + +} + diff --git a/src/openalpr/featurematcher.h b/src/openalpr/featurematcher.h new file mode 100644 index 0000000..733d75a --- /dev/null +++ b/src/openalpr/featurematcher.h @@ -0,0 +1,93 @@ +/* + * 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 FEATUREMATCHER_H +#define FEATUREMATCHER_H + +#include "opencv2/highgui/highgui.hpp" +#include "opencv2/calib3d/calib3d.hpp" +#include "opencv2/imgproc/imgproc.hpp" +#include "opencv2/features2d/features2d.hpp" +#include "opencv2/video/tracking.hpp" + +#include "support/filesystem.h" +#include "constants.h" +#include "utility.h" +#include "config.h" + +using namespace cv; +using namespace std; + + + +struct RecognitionResult { + bool haswinner; + string winner; + int confidence; +} ; + +class FeatureMatcher +{ + + public: + FeatureMatcher(Config* config); + virtual ~FeatureMatcher(); + + + + RecognitionResult recognize( const Mat& queryImg, bool drawOnImage, Mat* outputImage, + bool debug_on, vector debug_matches_array ); + + + bool loadRecognitionSet(string country); + + bool isLoaded(); + + int numTrainingElements(); + + private: + Config* config; + + Ptr descriptorMatcher; + Ptr detector; + Ptr extractor; + + + vector > trainingImgKeypoints; + + + void _surfStyleMatching(const Mat& queryDescriptors, vector > matchesKnn, vector& matches12); + + void crisscrossFiltering(const vector queryKeypoints, const vector inputMatches, vector &outputMatches); + + vector billMapping; + + + + void surfStyleMatching( const Mat& queryDescriptors, vector queryKeypoints, + vector& matches12 ); + +}; + +#endif // FEATUREMATCHER_H + + diff --git a/src/openalpr/licenseplatecandidate.cpp b/src/openalpr/licenseplatecandidate.cpp new file mode 100644 index 0000000..2de4c6d --- /dev/null +++ b/src/openalpr/licenseplatecandidate.cpp @@ -0,0 +1,202 @@ +/* + * 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 "licenseplatecandidate.h" + + +LicensePlateCandidate::LicensePlateCandidate(Mat frame, Rect regionOfInterest, Config* config) +{ + this->config = config; + + this->frame = frame; + this->plateRegion = regionOfInterest; +} + +LicensePlateCandidate::~LicensePlateCandidate() +{ + delete charSegmenter; +} + +// Must delete this pointer in parent class +void LicensePlateCandidate::recognize() +{ + charSegmenter = NULL; + + + this->confidence = 0; + + int expandX = round(this->plateRegion.width * 0.15); + int expandY = round(this->plateRegion.height * 0.10); + // expand box by 15% in all directions + Rect expandedRegion = expandRect( this->plateRegion, expandX, expandY, frame.cols, frame.rows) ; + + + + + Mat plate_bgr = Mat(frame, expandedRegion); + resize(plate_bgr, plate_bgr, Size(config->templateWidthPx, config->templateHeightPx)); + + Mat plate_bgr_cleaned = Mat(plate_bgr.size(), plate_bgr.type()); + this->cleanupColors(plate_bgr, plate_bgr_cleaned); + + + CharacterRegion charRegion(plate_bgr, config); + + + if (charRegion.confidence > 10) + { + + PlateLines plateLines(config); + //Mat boogedy = charRegion.getPlateMask(); + + plateLines.processImage(charRegion.getPlateMask(), 1.15); + plateLines.processImage(plate_bgr_cleaned, 0.9); + + PlateCorners cornerFinder(plate_bgr, &plateLines, &charRegion, config); + vector smallPlateCorners = cornerFinder.findPlateCorners(); + + if (cornerFinder.confidence > 0) + { + this->plateCorners = transformPointsToOriginalImage(frame, plate_bgr, expandedRegion, smallPlateCorners); + + + this->deskewed = deSkewPlate(frame, this->plateCorners); + + + charSegmenter = new CharacterSegmenter(deskewed, charRegion.thresholdsInverted(), config); + + + //this->recognizedText = ocr->recognizedText; + //strcpy(this->recognizedText, ocr.recognizedText); + + this->confidence = 100; + + } + charRegion.confidence = 0; + } + + +} + + + + + +// Re-maps the coordinates from the smallImage to the coordinate space of the bigImage. +vector LicensePlateCandidate::transformPointsToOriginalImage(Mat bigImage, Mat smallImage, Rect region, vector corners) +{ + vector cornerPoints; + for (int i = 0; i < corners.size(); i++) + { + float bigX = (corners[i].x * ((float) region.width / smallImage.cols)); + float bigY = (corners[i].y * ((float) region.height / smallImage.rows)); + + bigX = bigX + region.x; + bigY = bigY + region.y; + + cornerPoints.push_back(Point2f(bigX, bigY)); + } + + return cornerPoints; +} + + +Mat LicensePlateCandidate::deSkewPlate(Mat inputImage, vector corners) +{ + + // Figure out the appoximate width/height of the license plate region, so we can maintain the aspect ratio. + LineSegment leftEdge(round(corners[3].x), round(corners[3].y), round(corners[0].x), round(corners[0].y)); + LineSegment rightEdge(round(corners[2].x), round(corners[2].y), round(corners[1].x), round(corners[1].y)); + LineSegment topEdge(round(corners[0].x), round(corners[0].y), round(corners[1].x), round(corners[1].y)); + LineSegment bottomEdge(round(corners[3].x), round(corners[3].y), round(corners[2].x), round(corners[2].y)); + + float w = distanceBetweenPoints(leftEdge.midpoint(), rightEdge.midpoint()); + float h = distanceBetweenPoints(bottomEdge.midpoint(), topEdge.midpoint()); + float aspect = w/h; + + int width = config->ocrImageWidthPx; + int height = round(((float) width) / aspect); + if (height > config->ocrImageHeightPx) + { + height = config->ocrImageHeightPx; + width = round(((float) height) * aspect); + } + + Mat deskewed(height, width, frame.type()); + + // Corners of the destination image + vector quad_pts; + quad_pts.push_back(Point2f(0, 0)); + quad_pts.push_back(Point2f(deskewed.cols, 0)); + quad_pts.push_back(Point2f(deskewed.cols, deskewed.rows)); + quad_pts.push_back(Point2f(0, deskewed.rows)); + + // Get transformation matrix + Mat transmtx = getPerspectiveTransform(corners, quad_pts); + + // Apply perspective transformation + warpPerspective(inputImage, deskewed, transmtx, deskewed.size()); + + if (this->config->debugGeneral) + displayImage(config, "quadrilateral", deskewed); + + return deskewed; +} + + +void LicensePlateCandidate::cleanupColors(Mat inputImage, Mat outputImage) +{ + if (this->config->debugGeneral) + cout << "LicensePlate::cleanupColors" << endl; + + //Mat normalized(inputImage.size(), inputImage.type()); + + Mat intermediate(inputImage.size(), inputImage.type()); + + normalize(inputImage, intermediate, 0, 255, CV_MINMAX ); + + // Equalize intensity: + if(intermediate.channels() >= 3) + { + Mat ycrcb; + + cvtColor(intermediate,ycrcb,CV_BGR2YCrCb); + + vector channels; + split(ycrcb,channels); + + equalizeHist(channels[0], channels[0]); + + merge(channels,ycrcb); + + cvtColor(ycrcb,intermediate,CV_YCrCb2BGR); + + //ycrcb.release(); + } + + + bilateralFilter(intermediate, outputImage, 3, 25, 35); + + + if (this->config->debugGeneral) + { + displayImage(config, "After cleanup", outputImage); + } + +} diff --git a/src/openalpr/licenseplatecandidate.h b/src/openalpr/licenseplatecandidate.h new file mode 100644 index 0000000..a34a799 --- /dev/null +++ b/src/openalpr/licenseplatecandidate.h @@ -0,0 +1,83 @@ +/* + * 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 STAGE2_H +#define STAGE2_H + +#include +#include +#include +//#include + +#include "opencv2/imgproc/imgproc.hpp" +#include "opencv2/core/core.hpp" +#include "opencv2/highgui/highgui.hpp" + +#include "utility.h" +#include "constants.h" +#include "platelines.h" +#include "characterregion.h" +#include "charactersegmenter.h" +#include "platecorners.h" +#include "config.h" + +using namespace std; +using namespace cv; + + + +//vector getCharacterRegions(Mat frame, vector regionsOfInterest); +//vector getCharSegmentsBetweenLines(Mat img, vector > contours, LineSegment top, LineSegment bottom); + +class LicensePlateCandidate +{ + + public: + LicensePlateCandidate(Mat frame, Rect regionOfInterest, Config* config); + virtual ~LicensePlateCandidate(); + + float confidence; // 0-100 + //vector points; // top-left, top-right, bottom-right, bottom-left + vector plateCorners; + + void recognize(); + + Mat deskewed; + CharacterSegmenter* charSegmenter; + + private: + + Config* config; + + + Mat frame; + Rect plateRegion; + + void cleanupColors(Mat inputImage, Mat outputImage); + Mat filterByCharacterHue(vector > charRegionContours); + vector findPlateCorners(Mat inputImage, PlateLines plateLines, CharacterRegion charRegion); // top-left, top-right, bottom-right, bottom-left + + vector transformPointsToOriginalImage(Mat bigImage, Mat smallImage, Rect region, vector corners); + Mat deSkewPlate(Mat inputImage, vector corners); + +}; + + +#endif // STAGE2_H \ No newline at end of file diff --git a/src/openalpr/linux_dev.h b/src/openalpr/linux_dev.h new file mode 100644 index 0000000..8468d96 --- /dev/null +++ b/src/openalpr/linux_dev.h @@ -0,0 +1,28 @@ +/* + * 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 . +*/ + +#define CONFIG_FILE "/openalpr.conf" +#define KEYPOINTS_DIR "/keypoints" +#define CASCADE_DIR "/region/" +#define POSTPROCESS_DIR "/postprocess" + +#define DEFAULT_RUNTIME_DIR "/home/mhill/projects/alpr/runtime_data" +#define ENV_VARIABLE_RUNTIME_DIR "OPENALPR_RUNTIME_DIR" + + diff --git a/src/openalpr/ocr.cpp b/src/openalpr/ocr.cpp new file mode 100644 index 0000000..b7d5976 --- /dev/null +++ b/src/openalpr/ocr.cpp @@ -0,0 +1,145 @@ +/* + * 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 "ocr.h" + +OCR::OCR(Config* config) +{ + this->config = config; + + this->postProcessor = new PostProcess(config); + + tesseract=new TessBaseAPI(); + + // Tesseract requires the prefix directory to be set as an env variable + vector tessdataPrefix(config->getTessdataPrefix().size()); + + strcpy(tessdataPrefix.data(), config->getTessdataPrefix().c_str()); + putenv(tessdataPrefix.data()); + + + tesseract->Init("", config->ocrLanguage.c_str() ); + tesseract->SetVariable("save_blob_choices", "T"); + //tesseract->SetVariable("tessedit_char_whitelist", "ABCDEFGHIJKLMNPQRSTUVWXYZ1234567890"); + tesseract->SetPageSegMode(PSM_SINGLE_CHAR); +} + +OCR::~OCR() +{ + tesseract->Clear(); + delete postProcessor; + delete tesseract; +} + + +void OCR::performOCR(vector thresholds, vector charRegions) +{ + + + timespec startTime; + getTime(&startTime); + + + postProcessor->clear(); + + + for (int i = 0; i < thresholds.size(); i++) + { + + // Make it black text on white background + bitwise_not(thresholds[i], thresholds[i]); + tesseract->SetImage((uchar*) thresholds[i].data, thresholds[i].size().width, thresholds[i].size().height, thresholds[i].channels(), thresholds[i].step1()); + + + for (int j = 0; j < charRegions.size(); j++) + { + Rect expandedRegion = expandRect( charRegions[j], 2, 2, thresholds[i].cols, thresholds[i].rows) ; + + tesseract->SetRectangle(expandedRegion.x, expandedRegion.y, expandedRegion.width, expandedRegion.height); + tesseract->Recognize(NULL); + + tesseract::ResultIterator* ri = tesseract->GetIterator(); + tesseract::PageIteratorLevel level = tesseract::RIL_SYMBOL; + do { + const char* symbol = ri->GetUTF8Text(level); + float conf = ri->Confidence(level); + + bool dontcare; + int fontindex = 0; + int pointsize = 0; + const char* fontName = ri->WordFontAttributes(&dontcare, &dontcare, &dontcare, &dontcare, &dontcare, &dontcare, &pointsize, &fontindex); + + if(symbol != 0 && pointsize >= config->ocrMinFontSize) { + postProcessor->addLetter(*symbol, j, conf); + + + if (this->config->debugOcr) + printf("charpos%d: threshold %d: symbol %s, conf: %f font: %s (index %d) size %dpx", j, i, symbol, conf, fontName, fontindex, pointsize); + + bool indent = false; + tesseract::ChoiceIterator ci(*ri); + do { + const char* choice = ci.GetUTF8Text(); + + postProcessor->addLetter(*choice, j, ci.Confidence()); + + + //letterScores.addScore(*choice, j, ci.Confidence() - MIN_CONFIDENCE); + if (this->config->debugOcr) + { + if (indent) printf("\t\t "); + printf("\t- "); + printf("%s conf: %f\n", choice, ci.Confidence()); + } + + indent = true; + } while(ci.Next()); + } + + if (this->config->debugOcr) + printf("---------------------------------------------\n"); + + delete[] symbol; + } while((ri->Next(level))); + + delete ri; + } + + + } + + if (config->debugTiming) + { + timespec endTime; + getTime(&endTime); + cout << "OCR Time: " << diffclock(startTime, endTime) << "ms." << endl; + } + + + + +} + + + + + diff --git a/src/openalpr/ocr.h b/src/openalpr/ocr.h new file mode 100644 index 0000000..7bf6d0f --- /dev/null +++ b/src/openalpr/ocr.h @@ -0,0 +1,69 @@ +/* + * 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 OCR_H +#define OCR_H + + #include + #include + +#include "utility.h" +#include "postprocess.h" +#include "config.h" + +#include "constants.h" +#include "opencv2/highgui/highgui.hpp" +#include "opencv2/imgproc/imgproc.hpp" + +#include "baseapi.h" +using namespace tesseract; +using namespace std; +using namespace cv; + + +class OCR +{ + + public: + OCR(Config* config); + virtual ~OCR(); + + void performOCR(vector thresholds, vector charRegions); + + PostProcess* postProcessor; + //string recognizedText; + //float confidence; + //float overallConfidence; + + + private: + Config* config; + + TessBaseAPI *tesseract; + + + +}; + + + +#endif // OCR_H diff --git a/src/openalpr/platecorners.cpp b/src/openalpr/platecorners.cpp new file mode 100644 index 0000000..bd298b5 --- /dev/null +++ b/src/openalpr/platecorners.cpp @@ -0,0 +1,447 @@ +/* + * 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 "platecorners.h" + +PlateCorners::PlateCorners(Mat inputImage, PlateLines* plateLines, CharacterRegion* charRegion, Config* config) +{ + this->config = config; + + if (this->config->debugPlateCorners) + cout << "PlateCorners constructor" << endl; + + + + this->inputImage = inputImage; + this->plateLines = plateLines; + this->charRegion = charRegion; + + this->bestHorizontalScore = 9999999999999; + this->bestVerticalScore = 9999999999999; + + + Point topPoint = charRegion->getTopLine().midpoint(); + Point bottomPoint = charRegion->getBottomLine().closestPointOnSegmentTo(topPoint); + this->charHeight = distanceBetweenPoints(topPoint, bottomPoint); + + //this->charHeight = distanceBetweenPoints(charRegion->getCharArea()[0], charRegion->getCharArea()[3]); + //this->charHeight = this->charHeight - 2; // Adjust since this height is a box around our char. + // Adjust the char height for the difference in size... + //this->charHeight = ((float) inputImage.size().height / (float) TEMPLATE_PLATE_HEIGHT) * this->charHeight; + + this->charAngle = angleBetweenPoints(charRegion->getCharArea()[0], charRegion->getCharArea()[1]); +} + +PlateCorners::~PlateCorners() +{ + +} + +vector PlateCorners::findPlateCorners() +{ + if (this->config->debugPlateCorners) + cout << "PlateCorners::findPlateCorners" << endl; + + timespec startTime; + getTime(&startTime); + + int horizontalLines = this->plateLines->horizontalLines.size(); + int verticalLines = this->plateLines->verticalLines.size(); + + + // layout horizontal lines + for (int h1 = NO_LINE; h1 < horizontalLines; h1++) + { + for (int h2 = NO_LINE; h2 < horizontalLines; h2++) + { + if (h1 == h2 && h1 != NO_LINE) continue; + + this->scoreHorizontals(h1, h2); + + } + } + + // layout vertical lines + for (int v1 = NO_LINE; v1 < verticalLines; v1++) + { + for (int v2 = NO_LINE; v2 < verticalLines; v2++) + { + if (v1 == v2 && v1 != NO_LINE) continue; + + this->scoreVerticals(v1, v2); + } + } + + + if (this->config->debugPlateCorners) + { + + cout << "Drawing debug stuff..." << endl; + + Mat imgCorners = Mat(inputImage.size(), inputImage.type()); + inputImage.copyTo(imgCorners); + for (int i = 0; i < 4; i++) + circle(imgCorners, charRegion->getCharArea()[i], 2, Scalar(0, 0, 0)); + + + line(imgCorners, this->bestTop.p1, this->bestTop.p2, Scalar(255, 0, 0), 1, CV_AA); + line(imgCorners, this->bestRight.p1, this->bestRight.p2, Scalar(0, 0, 255), 1, CV_AA); + line(imgCorners, this->bestBottom.p1, this->bestBottom.p2, Scalar(0, 0, 255), 1, CV_AA); + line(imgCorners, this->bestLeft.p1, this->bestLeft.p2, Scalar(255, 0, 0), 1, CV_AA); + + + + + displayImage(config, "Winning top/bottom Boundaries", imgCorners); + + } + + // Check if a left/right edge has been established. + if (bestLeft.p1.x == 0 && bestLeft.p1.y == 0 && bestLeft.p2.x == 0 && bestLeft.p2.y == 0) + confidence = 0; + else if (bestTop.p1.x == 0 && bestTop.p1.y == 0 && bestTop.p2.x == 0 && bestTop.p2.y == 0) + confidence = 0; + else + confidence = 100; + + vector corners; + corners.push_back(bestTop.intersection(bestLeft)); + corners.push_back(bestTop.intersection(bestRight)); + corners.push_back(bestBottom.intersection(bestRight)); + corners.push_back(bestBottom.intersection(bestLeft)); + + + + if (config->debugTiming) + { + timespec endTime; + getTime(&endTime); + cout << "Plate Corners Time: " << diffclock(startTime, endTime) << "ms." << endl; + } + + return corners; +} + + +void PlateCorners::scoreVerticals(int v1, int v2) +{ + + float score = 0; // Lower is better + + LineSegment left; + LineSegment right; + + + + float charHeightToPlateWidthRatio = config->plateWidthMM / config->charHeightMM; + float idealPixelWidth = this->charHeight * (charHeightToPlateWidthRatio * 1.05); // Add 10% so we don't clip any characters + + if (v1 == NO_LINE && v2 == NO_LINE) + { + //return; + Point centerTop = charRegion->getCharBoxTop().midpoint(); + Point centerBottom = charRegion->getCharBoxBottom().midpoint(); + LineSegment centerLine = LineSegment(centerBottom.x, centerBottom.y, centerTop.x, centerTop.y); + + left = centerLine.getParallelLine(idealPixelWidth / 2); + right = centerLine.getParallelLine(-1 * idealPixelWidth / 2 ); + + score += SCORING_MISSING_SEGMENT_PENALTY_VERTICAL * 2; + } + else if (v1 != NO_LINE && v2 != NO_LINE) + { + left = this->plateLines->verticalLines[v1]; + right = this->plateLines->verticalLines[v2]; + } + else if (v1 == NO_LINE && v2 != NO_LINE) + { + right = this->plateLines->verticalLines[v2]; + left = right.getParallelLine(idealPixelWidth); + score += SCORING_MISSING_SEGMENT_PENALTY_VERTICAL; + } + else if (v1 != NO_LINE && v2 == NO_LINE) + { + left = this->plateLines->verticalLines[v1]; + right = left.getParallelLine(-1 * idealPixelWidth); + score += SCORING_MISSING_SEGMENT_PENALTY_VERTICAL; + } + + + + // Make sure this line is to the left of our license plate letters + if (left.isPointBelowLine(charRegion->getCharBoxLeft().midpoint()) == false) + return; + + // Make sure this line is to the right of our license plate letters + if (right.isPointBelowLine(charRegion->getCharBoxRight().midpoint())) + return; + + + ///////////////////////////////////////////////////////////////////////// + // Score "Distance from the edge... + ///////////////////////////////////////////////////////////////////////// + + float leftDistanceFromEdge = abs((float) (left.p1.x + left.p2.x) / 2); + float rightDistanceFromEdge = abs(this->inputImage.cols - ((float) (right.p1.x + right.p2.x) / 2)); + + float distanceFromEdge = leftDistanceFromEdge + rightDistanceFromEdge; + score += distanceFromEdge * SCORING_VERTICALDISTANCE_FROMEDGE_WEIGHT; + + + ///////////////////////////////////////////////////////////////////////// + // Score "Boxiness" of the 4 lines. How close is it to a parallelogram? + ///////////////////////////////////////////////////////////////////////// + + float verticalAngleDiff = abs(left.angle - right.angle); + + score += (verticalAngleDiff) * SCORING_BOXINESS_WEIGHT; + + + ////////////////////////////////////////////////////////////////////////// + // SCORE the shape wrt character position and height relative to position + ////////////////////////////////////////////////////////////////////////// + + + Point leftMidLinePoint = left.closestPointOnSegmentTo(charRegion->getCharBoxLeft().midpoint()); + Point rightMidLinePoint = right.closestPointOnSegmentTo(charRegion->getCharBoxRight().midpoint()); + + float plateDistance = abs(idealPixelWidth - distanceBetweenPoints(leftMidLinePoint, rightMidLinePoint)); + + score += plateDistance * SCORING_VERTICALDISTANCE_WEIGHT; + + if (score < this->bestVerticalScore) + { + float scorecomponent; + + + if (this->config->debugPlateCorners) + { + cout << "xx xx Score: charHeight " << this->charHeight << endl; + cout << "xx xx Score: idealwidth " << idealPixelWidth << endl; + cout << "xx xx Score: v1,v2= " << v1 << "," << v2 << endl; + cout << "xx xx Score: Left= " << left.str() << endl; + cout << "xx xx Score: Right= " << right.str() << endl; + + cout << "Vertical breakdown Score:" << endl; + cout << " -- Boxiness Score: " << verticalAngleDiff << " -- Weight (" << SCORING_BOXINESS_WEIGHT << ")" << endl; + scorecomponent = verticalAngleDiff * SCORING_BOXINESS_WEIGHT; + cout << " -- -- Score: " << scorecomponent << " = " << scorecomponent / score * 100 << "% of score" << endl; + + cout << " -- Distance From Edge Score: " << distanceFromEdge << " -- Weight (" << SCORING_VERTICALDISTANCE_FROMEDGE_WEIGHT << ")" << endl; + scorecomponent = distanceFromEdge * SCORING_VERTICALDISTANCE_FROMEDGE_WEIGHT; + cout << " -- -- Score: " << scorecomponent << " = " << scorecomponent / score * 100 << "% of score" << endl; + + cout << " -- Distance Score: " << plateDistance << " -- Weight (" << SCORING_VERTICALDISTANCE_WEIGHT << ")" << endl; + scorecomponent = plateDistance * SCORING_VERTICALDISTANCE_WEIGHT; + cout << " -- -- Score: " << scorecomponent << " = " << scorecomponent / score * 100 << "% of score" << endl; + + cout << " -- Score: " << score << endl; + } + + this->bestVerticalScore = score; + bestLeft = LineSegment(left.p1.x, left.p1.y, left.p2.x, left.p2.y); + bestRight = LineSegment(right.p1.x, right.p1.y, right.p2.x, right.p2.y); + } + +} +// Score a collection of lines as a possible license plate region. +// If any segments are missing, extrapolate the missing pieces +void PlateCorners::scoreHorizontals(int h1, int h2) +{ + + //if (this->debug) + // cout << "PlateCorners::scorePlate" << endl; + + float score = 0; // Lower is better + + LineSegment top; + LineSegment bottom; + + float charHeightToPlateHeightRatio = config->plateHeightMM / config->charHeightMM; + float idealPixelHeight = this->charHeight * charHeightToPlateHeightRatio; + + + if (h1 == NO_LINE && h2 == NO_LINE) + { +// return; + Point centerLeft = charRegion->getCharBoxLeft().midpoint(); + Point centerRight = charRegion->getCharBoxRight().midpoint(); + LineSegment centerLine = LineSegment(centerLeft.x, centerLeft.y, centerRight.x, centerRight.y); + + top = centerLine.getParallelLine(idealPixelHeight / 2); + bottom = centerLine.getParallelLine(-1 * idealPixelHeight / 2 ); + + score += SCORING_MISSING_SEGMENT_PENALTY_HORIZONTAL * 2; + } + else if (h1 != NO_LINE && h2 != NO_LINE) + { + top = this->plateLines->horizontalLines[h1]; + bottom = this->plateLines->horizontalLines[h2]; + } + else if (h1 == NO_LINE && h2 != NO_LINE) + { + bottom = this->plateLines->horizontalLines[h2]; + top = bottom.getParallelLine(idealPixelHeight); + score += SCORING_MISSING_SEGMENT_PENALTY_HORIZONTAL; + } + else if (h1 != NO_LINE && h2 == NO_LINE) + { + top = this->plateLines->horizontalLines[h1]; + bottom = top.getParallelLine(-1 * idealPixelHeight); + score += SCORING_MISSING_SEGMENT_PENALTY_HORIZONTAL; + } + + + + // Make sure this line is above our license plate letters + if (top.isPointBelowLine(charRegion->getCharBoxTop().midpoint()) == false) + return; + + + // Make sure this line is below our license plate letters + if (bottom.isPointBelowLine(charRegion->getCharBoxBottom().midpoint())) + return; + + + + + + + // We now have 4 possible lines. Let's put them to the test and score them... + + + + + ///////////////////////////////////////////////////////////////////////// + // Score "Boxiness" of the 4 lines. How close is it to a parallelogram? + ///////////////////////////////////////////////////////////////////////// + + float horizontalAngleDiff = abs(top.angle - bottom.angle); + + + score += (horizontalAngleDiff) * SCORING_BOXINESS_WEIGHT; +// if (this->debug) +// cout << "PlateCorners boxiness score: " << (horizontalAngleDiff + verticalAngleDiff) * SCORING_BOXINESS_WEIGHT << endl; + + + ////////////////////////////////////////////////////////////////////////// + // SCORE the shape wrt character position and height relative to position + ////////////////////////////////////////////////////////////////////////// + + Point topPoint = top.midpoint(); + Point botPoint = bottom.closestPointOnSegmentTo(topPoint); + float plateHeightPx = distanceBetweenPoints(topPoint, botPoint); + + // Get the height difference + + + float heightRatio = charHeight / plateHeightPx; + float idealHeightRatio = (config->charHeightMM / config->plateHeightMM); + //if (leftRatio < MIN_CHAR_HEIGHT_RATIO || leftRatio > MAX_CHAR_HEIGHT_RATIO || rightRatio < MIN_CHAR_HEIGHT_RATIO || rightRatio > MAX_CHAR_HEIGHT_RATIO) + float heightRatioDiff = abs(heightRatio - idealHeightRatio); + // Ideal ratio == ~.45 + + // Get the distance from the top and the distance from the bottom + // Take the average distances from the corners of the character region to the top/bottom lines +// float topDistance = distanceBetweenPoints(topMidLinePoint, charRegion->getCharBoxTop().midpoint()); +// float bottomDistance = distanceBetweenPoints(bottomMidLinePoint, charRegion->getCharBoxBottom().midpoint()); + +// float idealTopDistance = charHeight * (TOP_WHITESPACE_HEIGHT_MM / CHARACTER_HEIGHT_MM); +// float idealBottomDistance = charHeight * (BOTTOM_WHITESPACE_HEIGHT_MM / CHARACTER_HEIGHT_MM); +// float distScore = abs(topDistance - idealTopDistance) + abs(bottomDistance - idealBottomDistance); + + + score += heightRatioDiff * SCORING_PLATEHEIGHT_WEIGHT; + + ////////////////////////////////////////////////////////////////////////// + // SCORE the middliness of the stuff. We want our top and bottom line to have the characters right towards the middle + ////////////////////////////////////////////////////////////////////////// + + Point charAreaMidPoint = charRegion->getCharBoxLeft().midpoint(); + Point topLineSpot = top.closestPointOnSegmentTo(charAreaMidPoint); + Point botLineSpot = bottom.closestPointOnSegmentTo(charAreaMidPoint); + + float topDistanceFromMiddle = distanceBetweenPoints(topLineSpot, charAreaMidPoint); + float bottomDistanceFromMiddle = distanceBetweenPoints(topLineSpot, charAreaMidPoint); + + float idealDistanceFromMiddle = idealPixelHeight / 2; + + float middleScore = abs(topDistanceFromMiddle - idealDistanceFromMiddle) + abs(bottomDistanceFromMiddle - idealDistanceFromMiddle); + + score += middleScore * SCORING_TOP_BOTTOM_SPACE_VS_CHARHEIGHT_WEIGHT; + +// if (this->debug) +// { +// cout << "PlateCorners boxiness score: " << avgRatio * SCORING_TOP_BOTTOM_SPACE_VS_CHARHEIGHT_WEIGHT << endl; +// cout << "PlateCorners boxiness score: " << distScore * SCORING_PLATEHEIGHT_WEIGHT << endl; +// } + ////////////////////////////////////////////////////////////// + // SCORE: the shape for angles matching the character region + ////////////////////////////////////////////////////////////// + + float charanglediff = abs(charAngle - top.angle) + abs(charAngle - bottom.angle); + + + score += charanglediff * SCORING_ANGLE_MATCHES_LPCHARS_WEIGHT; + +// if (this->debug) +// cout << "PlateCorners boxiness score: " << charanglediff * SCORING_ANGLE_MATCHES_LPCHARS_WEIGHT << endl; + + + + if (score < this->bestHorizontalScore) + { + float scorecomponent; + + if (this->config->debugPlateCorners) + { + cout << "xx xx Score: charHeight " << this->charHeight << endl; + cout << "xx xx Score: idealHeight " << idealPixelHeight << endl; + cout << "xx xx Score: h1,h2= " << h1 << "," << h2 << endl; + cout << "xx xx Score: Top= " << top.str() << endl; + cout << "xx xx Score: Bottom= " << bottom.str() << endl; + + cout << "Horizontal breakdown Score:" << endl; + cout << " -- Boxiness Score: " << horizontalAngleDiff << " -- Weight (" << SCORING_BOXINESS_WEIGHT << ")" << endl; + scorecomponent = horizontalAngleDiff * SCORING_BOXINESS_WEIGHT; + cout << " -- -- Score: " << scorecomponent << " = " << scorecomponent / score * 100 << "% of score" << endl; + + cout << " -- Height Ratio Diff Score: " << heightRatioDiff << " -- Weight (" << SCORING_PLATEHEIGHT_WEIGHT << ")" << endl; + scorecomponent = heightRatioDiff * SCORING_PLATEHEIGHT_WEIGHT; + cout << " -- -- " << scorecomponent << " = " << scorecomponent / score * 100 << "% of score" << endl; + + cout << " -- Distance Score: " << middleScore << " -- Weight (" << SCORING_TOP_BOTTOM_SPACE_VS_CHARHEIGHT_WEIGHT << ")" << endl; + scorecomponent = middleScore * SCORING_TOP_BOTTOM_SPACE_VS_CHARHEIGHT_WEIGHT; + cout << " -- -- Score: " << scorecomponent << " = " << scorecomponent / score * 100 << "% of score" << endl; + + cout << " -- Char angle Score: " << charanglediff << " -- Weight (" << SCORING_ANGLE_MATCHES_LPCHARS_WEIGHT << ")" << endl; + scorecomponent = charanglediff * SCORING_ANGLE_MATCHES_LPCHARS_WEIGHT; + cout << " -- -- Score: " << scorecomponent << " = " << scorecomponent / score * 100 << "% of score" << endl; + + cout << " -- Score: " << score << endl; + } + this->bestHorizontalScore = score; + bestTop = LineSegment(top.p1.x, top.p1.y, top.p2.x, top.p2.y); + bestBottom = LineSegment(bottom.p1.x, bottom.p1.y, bottom.p2.x, bottom.p2.y); + } + + + +} diff --git a/src/openalpr/platecorners.h b/src/openalpr/platecorners.h new file mode 100644 index 0000000..6c2aecc --- /dev/null +++ b/src/openalpr/platecorners.h @@ -0,0 +1,83 @@ +/* + * 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 PLATECORNERS_H +#define PLATECORNERS_H + +#include "opencv2/highgui/highgui.hpp" +#include "opencv2/imgproc/imgproc.hpp" +#include "characterregion.h" +#include "platelines.h" +#include "utility.h" +#include "config.h" + +using namespace cv; +using namespace std; + +#define NO_LINE -1 + + +#define SCORING_MISSING_SEGMENT_PENALTY_VERTICAL 10 +#define SCORING_MISSING_SEGMENT_PENALTY_HORIZONTAL 15 + +#define SCORING_BOXINESS_WEIGHT 0.8 +#define SCORING_PLATEHEIGHT_WEIGHT 2.2 +#define SCORING_TOP_BOTTOM_SPACE_VS_CHARHEIGHT_WEIGHT 0.05 +#define SCORING_ANGLE_MATCHES_LPCHARS_WEIGHT 1.1 +#define SCORING_VERTICALDISTANCE_WEIGHT 0.1 + +#define SCORING_VERTICALDISTANCE_FROMEDGE_WEIGHT 0.05 + +class PlateCorners +{ + + public: + PlateCorners(Mat inputImage, PlateLines* plateLines, CharacterRegion* charRegion, Config* config); + virtual ~PlateCorners(); + + vector findPlateCorners(); + + float confidence; + + private: + + Config* config; + Mat inputImage; + float charHeight; + float charAngle; + + float bestHorizontalScore; + float bestVerticalScore; + LineSegment bestTop; + LineSegment bestBottom; + LineSegment bestLeft; + LineSegment bestRight; + + PlateLines* plateLines; + CharacterRegion* charRegion; + + void scoreHorizontals( int h1, int h2 ); + void scoreVerticals( int v1, int v2 ); + +}; + +#endif // PLATELINES_H + + diff --git a/src/openalpr/platelines.cpp b/src/openalpr/platelines.cpp new file mode 100644 index 0000000..c07d297 --- /dev/null +++ b/src/openalpr/platelines.cpp @@ -0,0 +1,366 @@ +/* + * 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 "platelines.h" + + +PlateLines::PlateLines(Config* config) +{ + this->config = config; + this->debug = config->debugPlateLines; + + if (debug) + cout << "PlateLines constructor" << endl; + + +} + +PlateLines::~PlateLines() +{ + +} + + + +void PlateLines::processImage(Mat inputImage, float sensitivity) +{ + if (this->debug) + cout << "PlateLines findLines" << endl; + + + timespec startTime; + getTime(&startTime); + + + Mat smoothed(inputImage.size(), inputImage.type()); + inputImage.copyTo(smoothed); + int morph_elem = 2; + int morph_size = 2; + Mat element = getStructuringElement( morph_elem, Size( 2*morph_size + 1, 2*morph_size+1 ), Point( morph_size, morph_size ) ); + + morphologyEx( smoothed, smoothed, MORPH_CLOSE, element ); + + morph_size = 1; + element = getStructuringElement( morph_elem, Size( 2*morph_size + 1, 2*morph_size+1 ), Point( morph_size, morph_size ) ); + + //morphologyEx( thresholded, thresholded, MORPH_GRADIENT, element ); + + morph_size = 1; + element = getStructuringElement( morph_elem, Size( 2*morph_size + 1, 2*morph_size+1 ), Point( morph_size, morph_size ) ); + morphologyEx( smoothed, smoothed, MORPH_OPEN, element ); + + + + Mat edges(inputImage.size(), inputImage.type()); + Canny(smoothed, edges, 66, 133); + + + vector hlines = this->getLines(edges, sensitivity, false); + vector vlines = this->getLines(edges, sensitivity, true); + for (int i = 0; i < hlines.size(); i++) + this->horizontalLines.push_back(hlines[i]); + for (int i = 0; i < vlines.size(); i++) + this->verticalLines.push_back(vlines[i]); + + + + + // if debug is enabled, draw the image + if (this->debug) + { + Mat debugImgHoriz(edges.size(), edges.type()); + Mat debugImgVert(edges.size(), edges.type()); + edges.copyTo(debugImgHoriz); + edges.copyTo(debugImgVert); + cvtColor(debugImgHoriz,debugImgHoriz,CV_GRAY2BGR); + cvtColor(debugImgVert,debugImgVert,CV_GRAY2BGR); + + for( size_t i = 0; i < this->horizontalLines.size(); i++ ) + { + line( debugImgHoriz, this->horizontalLines[i].p1, this->horizontalLines[i].p2, Scalar(0,0,255), 1, CV_AA); + } + + for( size_t i = 0; i < this->verticalLines.size(); i++ ) + { + line( debugImgVert, this->verticalLines[i].p1, this->verticalLines[i].p2, Scalar(0,0,255), 1, CV_AA); + } + + vector images; + images.push_back(debugImgHoriz); + images.push_back(debugImgVert); + + Mat dashboard = drawImageDashboard(images, debugImgVert.type(), 1); + displayImage(config, "Hough Lines", dashboard); + } + + + + if (config->debugTiming) + { + timespec endTime; + getTime(&endTime); + cout << "Plate Lines Time: " << diffclock(startTime, endTime) << "ms." << endl; + } + //smoothed.release(); + + + //////////////// METHOD2!!!!!!!//////////////////// + + /* + Mat imgBlur; + Mat imgCanny; + GaussianBlur(inputImage, imgBlur, Size(9, 9), 1, 1); + + + + Canny(imgBlur, imgCanny, 10, 30, 3); + + + + //int morph_elem = 2; + //int morph_size = 1; + //Mat element = getStructuringElement( morph_elem, Size( 2*morph_size + 1, 2*morph_size+1 ), Point( morph_size, morph_size ) ); + morphologyEx( imgCanny, imgCanny, MORPH_CLOSE, element ); + + + Mat imgShaped; + imgCanny.copyTo(imgShaped); + //Find contours of possibles characters + vector< vector< Point> > biggestShapes; + findContours(imgShaped, + biggestShapes, // a vector of contours + CV_RETR_EXTERNAL, // retrieve the external contours + CV_CHAIN_APPROX_SIMPLE ); // all pixels of each contours + + // Draw blue contours on a white image + //cvtColor(imgShaped, imgShaped, CV_GRAY2RGB); + cv::drawContours(imgShaped,biggestShapes, + -1, // draw all contours + cv::Scalar(255,255,255), // in blue + 1); // with a thickness of 1 + + displayImage(config, "Blurred", imgCanny); + displayImage(config, "Blurred Contours", imgShaped); + + vector shapeRects( biggestShapes.size() ); + + vector >hull( biggestShapes.size() ); + for( int i = 0; i < biggestShapes.size(); i++ ) + { + //approxPolyDP( Mat(biggestShapes[i]), shapeRects[i], 3, true ); + convexHull( biggestShapes[i], hull[i], false ); + //approxPolyDP( biggestShapes[i], hull[i], 10, true ); + + //minEnclosingCircle( (Mat)contours_poly[i], center[i], radius[i] ); + } + */ + +} + +/* +vector PlateLines::getLines(Mat edges, bool vertical) +{ + + vector filteredLines; + + int sensitivity; + + LSWMS lswms(Size(edges.cols, edges.rows), 3, 155, false); + + vector lsegs; + vector errors; + lswms.run(edges, lsegs, errors); + + + for( size_t i = 0; i < lsegs.size(); i++ ) + { + + if (vertical) + { + LineSegment candidate; + if (lsegs[i][0].y <= lsegs[i][1].y) + candidate = LineSegment(lsegs[i][0].x, lsegs[i][0].y, lsegs[i][1].x, lsegs[i][1].y); + else + candidate = LineSegment(lsegs[i][1].x, lsegs[i][1].y, lsegs[i][0].x, lsegs[i][0].y); + + cout << "VERT Angle: " << candidate.angle << endl; + //if ((candidate.angle > 70 && candidate.angle < 110) || (candidate.angle > 250 && candidate.angle < 290)) + //{ + // good vertical + filteredLines.push_back(candidate); + + //} + } + else + { + LineSegment candidate; + if (lsegs[i][0].x <= lsegs[i][1].x) + candidate = LineSegment(lsegs[i][0].x, lsegs[i][0].y, lsegs[i][1].x, lsegs[i][1].y); + else + candidate = LineSegment(lsegs[i][1].x, lsegs[i][1].y, lsegs[i][0].x, lsegs[i][0].y); + cout << "HORIZAngle: " << candidate.angle << endl; + + //if ( (candidate.angle > -20 && candidate.angle < 20) || (candidate.angle > 160 && candidate.angle < 200)) + //{ + // good horizontal + filteredLines.push_back(candidate); + + //} + } + } + + // if debug is enabled, draw the image + if (this->debug) + { + Mat debugImg(edges.size(), edges.type()); + edges.copyTo(debugImg); + cvtColor(debugImg,debugImg,CV_GRAY2BGR); + + for( size_t i = 0; i < filteredLines.size(); i++ ) + { + + line( debugImg, filteredLines[i].p1, filteredLines[i].p2, Scalar(0,0,255), 1, CV_AA); + } + if (vertical) + displayImage(config, "Lines Vertical", debugImg); + else + displayImage(config, "Lines Horizontal", debugImg); + } + + return filteredLines; +} +*/ + + +vector PlateLines::getLines(Mat edges, float sensitivityMultiplier, bool vertical) +{ + if (this->debug) + cout << "PlateLines::getLines" << endl; + + static int HORIZONTAL_SENSITIVITY = config->plateLinesSensitivityHorizontal; + static int VERTICAL_SENSITIVITY = config->plateLinesSensitivityVertical; + + vector allLines; + vector filteredLines; + + int sensitivity; + if (vertical) + sensitivity = VERTICAL_SENSITIVITY * (1.0 / sensitivityMultiplier); + else + sensitivity = HORIZONTAL_SENSITIVITY * (1.0 / sensitivityMultiplier); + + HoughLines( edges, allLines, 1, CV_PI/180, sensitivity, 0, 0 ); + + + for( size_t i = 0; i < allLines.size(); i++ ) + { + float rho = allLines[i][0], theta = allLines[i][1]; + Point pt1, pt2; + double a = cos(theta), b = sin(theta); + double x0 = a*rho, y0 = b*rho; + + double angle = theta * (180 / CV_PI); + pt1.x = cvRound(x0 + 1000*(-b)); + pt1.y = cvRound(y0 + 1000*(a)); + pt2.x = cvRound(x0 - 1000*(-b)); + pt2.y = cvRound(y0 - 1000*(a)); + + if (vertical) + { + if (angle < 20 || angle > 340 || (angle > 160 && angle < 210)) + { + // good vertical + + LineSegment line; + if (pt1.y <= pt2.y) + line = LineSegment(pt2.x, pt2.y, pt1.x, pt1.y); + else + line = LineSegment(pt1.x, pt1.y, pt2.x, pt2.y); + + // Get rid of the -1000, 1000 stuff. Terminate at the edges of the image + // Helps with debugging/rounding issues later + LineSegment top(0, 0, edges.cols, 0); + LineSegment bottom(0, edges.rows, edges.cols, edges.rows); + Point p1 = line.intersection(bottom); + Point p2 = line.intersection(top); + filteredLines.push_back(LineSegment(p1.x, p1.y, p2.x, p2.y)); + } + } + else + { + + if ( (angle > 70 && angle < 110) || (angle > 250 && angle < 290)) + { + // good horizontal + + LineSegment line; + if (pt1.x <= pt2.x) + line = LineSegment(pt1.x, pt1.y, pt2.x, pt2.y); + else + line =LineSegment(pt2.x, pt2.y, pt1.x, pt1.y); + + // Get rid of the -1000, 1000 stuff. Terminate at the edges of the image + // Helps with debugging/ rounding issues later + int newY1 = line.getPointAt(0); + int newY2 = line.getPointAt(edges.cols); + + filteredLines.push_back(LineSegment(0, newY1, edges.cols, newY2)); + } + } + } + + + return filteredLines; +} + + + + + +Mat PlateLines::customGrayscaleConversion(Mat src) +{ + Mat img_hsv; + cvtColor(src,img_hsv,CV_BGR2HSV); + + + Mat grayscale = Mat(img_hsv.size(), CV_8U ); + Mat hue(img_hsv.size(), CV_8U ); + + for (int row = 0; row < img_hsv.rows; row++) + { + for (int col = 0; col < img_hsv.cols; col++) + { + int h = (int) img_hsv.at(row, col)[0]; + int s = (int) img_hsv.at(row, col)[1]; + int v = (int) img_hsv.at(row, col)[2]; + + int pixval = pow(v, 1.05); + + + if (pixval > 255) + pixval = 255; + grayscale.at(row, col) = pixval; + + hue.at(row, col) = h * (255.0 / 180.0); + } + } + + //displayImage(config, "Hue", hue); + return grayscale; +} \ No newline at end of file diff --git a/src/openalpr/platelines.h b/src/openalpr/platelines.h new file mode 100644 index 0000000..59e05f7 --- /dev/null +++ b/src/openalpr/platelines.h @@ -0,0 +1,62 @@ +/* + * 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 PLATELINES_H +#define PLATELINES_H + +#include "opencv2/highgui/highgui.hpp" +#include "opencv2/imgproc/imgproc.hpp" +#include "utility.h" +#include "binarize_wolf.h" +//#include "lswms.h" +#include "config.h" + +using namespace cv; +using namespace std; + + +class PlateLines +{ + + public: + PlateLines(Config* config); + virtual ~PlateLines(); + + void processImage(Mat img, float sensitivity=1.0); + + vector horizontalLines; + vector verticalLines; + + vector winningCorners; + + private: + Config* config; + bool debug; + + + Mat customGrayscaleConversion(Mat src); + void findLines(Mat inputImage); + vector getLines(Mat edges, float sensitivityMultiplier, bool vertical); +}; + +#endif // PLATELINES_H + + diff --git a/src/openalpr/postprocess.cpp b/src/openalpr/postprocess.cpp new file mode 100644 index 0000000..7e28694 --- /dev/null +++ b/src/openalpr/postprocess.cpp @@ -0,0 +1,478 @@ +/* + * 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 "postprocess.h" + + +PostProcess::PostProcess(Config* config) +{ + this->config = config; + + stringstream filename; + filename << config->getPostProcessRuntimeDir() << "/" << config->country << ".patterns"; + + std::ifstream infile(filename.str().c_str()); + + + string region, pattern; + while (infile >> region >> pattern) + { + RegexRule* rule = new RegexRule(region, pattern); + //cout << "REGION: " << region << " PATTERN: " << pattern << endl; + + if (rules.find(region) == rules.end()) + { + vector newRule; + newRule.push_back(rule); + rules[region] = newRule; + } + else + { + vector oldRule = rules[region]; + oldRule.push_back(rule); + rules[region] = oldRule; + } + } + + //vector test = rules["base"]; + //for (int i = 0; i < test.size(); i++) + // cout << "Rule: " << test[i].regex << endl; + +} + +PostProcess::~PostProcess() +{ + // TODO: Delete all entries in rules vector + map >::iterator iter; + + for (iter = rules.begin(); iter != rules.end(); ++iter) { + for (int i = 0; i < iter->second.size(); i++) + { + delete iter->second[i]; + } + + } +} + + +void PostProcess::addLetter(char letter, int charposition, float score) +{ + if (score < config->postProcessMinConfidence) + return; + + insertLetter(letter, charposition, score); + + if (score < config->postProcessConfidenceSkipLevel) + { + float adjustedScore = abs(config->postProcessConfidenceSkipLevel - score) + config->postProcessMinConfidence; + insertLetter(SKIP_CHAR, charposition, adjustedScore ); + } + + //if (letter == '0') + //{ + // insertLetter('O', charposition, score - 0.5); + //} + +} + +void PostProcess::insertLetter(char letter, int charposition, float score) +{ + + score = score - config->postProcessMinConfidence; + + + int existingIndex = -1; + if (letters.size() < charposition + 1) + { + for (int i = letters.size(); i < charposition + 1; i++) + { + vector tmp; + letters.push_back(tmp); + } + } + + for (int i = 0; i < letters[charposition].size(); i++) + { + if (letters[charposition][i].letter == letter && + letters[charposition][i].charposition == charposition) + { + existingIndex = i; + break; + } + } + + if (existingIndex == -1) + { + Letter newLetter; + newLetter.charposition = charposition; + newLetter.letter = letter; + newLetter.occurences = 1; + newLetter.totalscore = score; + letters[charposition].push_back(newLetter); + } + else + { + letters[charposition][existingIndex].occurences = letters[charposition][existingIndex].occurences + 1; + letters[charposition][existingIndex].totalscore = letters[charposition][existingIndex].totalscore + score; + } + +} + + +void PostProcess::clear() +{ + for (int i = 0; i < letters.size(); i++) + { + letters[i].clear(); + } + letters.resize(0); + + unknownCharPositions.clear(); + unknownCharPositions.resize(0); + allPossibilities.clear(); + //allPossibilities.resize(0); + + bestChars = ""; + matchesTemplate = false; +} +void PostProcess::analyze(string templateregion, int topn) +{ + + timespec startTime; + getTime(&startTime); + + + + // Get a list of missing positions + for (int i = letters.size() -1; i >= 0; i--) + { + if (letters[i].size() == 0) + { + unknownCharPositions.push_back(i); + } + } + + + if (letters.size() == 0) + return; + + + // Sort the letters as they are + for (int i = 0; i < letters.size(); i++) + { + if (letters[i].size() > 0) + sort(letters[i].begin(), letters[i].end(), letterCompare); + } + + + + //getTopN(); + vector tmp; + findAllPermutations(tmp, 0, config->postProcessMaxSubstitutions); + + + timespec sortStartTime; + getTime(&sortStartTime); + + int numelements = topn; + if (allPossibilities.size() < topn) + numelements = allPossibilities.size() - 1; + + partial_sort( allPossibilities.begin(), allPossibilities.begin() + numelements, allPossibilities.end() - 1, wordCompare ); + + if (config->debugTiming) + { + timespec sortEndTime; + getTime(&sortEndTime); + cout << " -- PostProcess Sort Time: " << diffclock(sortStartTime, sortEndTime) << "ms." << endl; + } + + + + matchesTemplate = false; + + + if (templateregion != "") + { + vector regionRules = rules[templateregion]; + + for (int i = 0; i < allPossibilities.size(); i++) + { + for (int j = 0; j < regionRules.size(); j++) + { + allPossibilities[i].matchesTemplate = regionRules[j]->match(allPossibilities[i].letters); + if (allPossibilities[i].matchesTemplate) + { + allPossibilities[i].letters = regionRules[j]->filterSkips(allPossibilities[i].letters); + //bestChars = regionRules[j]->filterSkips(allPossibilities[i].letters); + matchesTemplate = true; + break; + } + } + + + + if (i >= topn - 1) + break; + //if (matchesTemplate || i >= TOP_N - 1) + //break; + } + } + + if (matchesTemplate) + { + for (int z = 0; z < allPossibilities.size(); z++) + { + if (allPossibilities[z].matchesTemplate) + { + bestChars = allPossibilities[z].letters; + break; + } + } + } + else + { + bestChars = allPossibilities[0].letters; + } + + // Now adjust the confidence scores to a percentage value + if (allPossibilities.size() > 0) + { + float maxPercentScore = calculateMaxConfidenceScore(); + float highestRelativeScore = (float) allPossibilities[0].totalscore; + + for (int i = 0; i < allPossibilities.size(); i++) + { + allPossibilities[i].totalscore = maxPercentScore * (allPossibilities[i].totalscore / highestRelativeScore); + } + + } + + + + if (this->config->debugPostProcess) + { + + // Print all letters + for (int i = 0; i < letters.size(); i++) + { + for (int j = 0; j < letters[i].size(); j++) + cout << "PostProcess Letter: " << letters[i][j].charposition << " " << letters[i][j].letter << " -- score: " << letters[i][j].totalscore << " -- occurences: " << letters[i][j].occurences << endl; + } + + // Print top words + for (int i = 0; i < allPossibilities.size(); i++) + { + cout << "Top " << topn << " Possibilities: " << allPossibilities[i].letters << " :\t" << allPossibilities[i].totalscore; + if (allPossibilities[i].letters == bestChars) + cout << " <--- "; + cout << endl; + + if (i >= topn - 1) + break; + } + cout << allPossibilities.size() << " total permutations" << endl; + } + + + + if (config->debugTiming) + { + timespec endTime; + getTime(&endTime); + cout << "PostProcess Time: " << diffclock(startTime, endTime) << "ms." << endl; + } + + if (this->config->debugPostProcess) + cout << "PostProcess Analysis Complete: " << bestChars << " -- MATCH: " << matchesTemplate << endl; +} + +float PostProcess::calculateMaxConfidenceScore() +{ + // Take the best score for each char position and average it. + + float totalScore = 0; + int numScores = 0; + // Get a list of missing positions + for (int i = 0; i < letters.size(); i++) + { + if (letters[i].size() > 0) + { + totalScore += (letters[i][0].totalscore / letters[i][0].occurences) + config->postProcessMinConfidence; + numScores++; + } + } + + if (numScores == 0) + return 0; + + return totalScore / ((float) numScores); +} + + +const vector PostProcess::getResults() +{ + return this->allPossibilities; +} + +void PostProcess::findAllPermutations(vector prevletters, int charPos, int substitutionsLeft) +{ + + if (substitutionsLeft < 0) + return; + + // Add my letter to the chain and recurse + for (int i = 0; i < letters[charPos].size(); i++) + { + + if (charPos == letters.size() - 1) + { + // Last letter, add the word + PPResult possibility; + possibility.letters = ""; + possibility.totalscore = 0; + possibility.matchesTemplate = false; + for (int z = 0; z < prevletters.size(); z++) + { + if (prevletters[z].letter != SKIP_CHAR) + possibility.letters = possibility.letters + prevletters[z].letter; + possibility.totalscore = possibility.totalscore + prevletters[z].totalscore; + } + + if (letters[charPos][i].letter != SKIP_CHAR) + possibility.letters = possibility.letters + letters[charPos][i].letter; + possibility.totalscore = possibility.totalscore +letters[charPos][i].totalscore; + + allPossibilities.push_back(possibility); + } + else + { + prevletters.push_back(letters[charPos][i]); + + float scorePercentDiff = abs( letters[charPos][0].totalscore - letters[charPos][i].totalscore ) / letters[charPos][0].totalscore; + if (i != 0 && letters[charPos][i].letter != SKIP_CHAR && scorePercentDiff > 0.10f ) + findAllPermutations(prevletters, charPos + 1, substitutionsLeft - 1); + else + findAllPermutations(prevletters, charPos + 1, substitutionsLeft); + + prevletters.pop_back(); + } + } + + if (letters[charPos].size() == 0) + { + // No letters for this char position... + // Just pass it along + findAllPermutations(prevletters, charPos + 1, substitutionsLeft); + } + + + +} + + + + +bool wordCompare( const PPResult &left, const PPResult &right ){ + if (left.totalscore < right.totalscore) + return false; + return true; + +} + +bool letterCompare( const Letter &left, const Letter &right ) +{ + if (left.totalscore < right.totalscore) + return false; + return true; +} + + +RegexRule::RegexRule(string region, string pattern) +{ + this->original = pattern; + this->region = region; + + numchars = 0; + for (int i = 0; i < pattern.size(); i++) + { + if (pattern.at(i) == '[') + { + while (pattern.at(i) != ']' ) + { + this->regex = this->regex + pattern.at(i); + i++; + } + this->regex = this->regex + ']'; + + } + else if (pattern.at(i) == '?') + { + this->regex = this->regex + '.'; + this->skipPositions.push_back(numchars); + } + else if (pattern.at(i) == '@') + { + this->regex = this->regex + "\\a"; + } + else if (pattern.at(i) == '#') + { + this->regex = this->regex + "\\d"; + } + + numchars++; + } + + trexp.Compile(this->regex.c_str()); + + //cout << "AA " << this->region << ": " << original << " regex: " << regex << endl; + //for (int z = 0; z < this->skipPositions.size(); z++) + // cout << "AA Skip position: " << skipPositions[z] << endl; +} + + +bool RegexRule::match(string text) +{ + if (text.length() != numchars) + return false; + + return trexp.Match(text.c_str()); +} + +string RegexRule::filterSkips(string text) +{ + string response = ""; + for (int i = 0; i < text.size(); i++) + { + bool skip = false; + for (int j = 0; j < skipPositions.size(); j++) + { + if (skipPositions[j] == i) + { + skip = true; + break; + } + } + + if (skip == false) + response = response + text[i]; + } + + return response; +} diff --git a/src/openalpr/postprocess.h b/src/openalpr/postprocess.h new file mode 100644 index 0000000..f13d4e5 --- /dev/null +++ b/src/openalpr/postprocess.h @@ -0,0 +1,130 @@ +/* + * 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 POSTPROCESS_H +#define POSTPROCESS_H + + #include "TRexpp.h" + #include "constants.h" + #include "utility.h" + #include + #include + #include + #include +#include "config.h" + +using namespace std; + + +#define SKIP_CHAR '~' + +struct Letter +{ + char letter; + int charposition; + float totalscore; + int occurences; +}; + +struct PPResult +{ + string letters; + float totalscore; + bool matchesTemplate; +}; + + +bool wordCompare( const PPResult &left, const PPResult &right ); +bool letterCompare( const Letter &left, const Letter &right ); + + +class RegexRule +{ + public: + RegexRule(string region, string pattern); + + + bool match(string text); + string filterSkips(string text); + + private: + int numchars; + TRexpp trexp; + string original; + string regex; + string region; + vector skipPositions; +}; + + +class PostProcess +{ + public: + PostProcess(Config* config); + ~PostProcess(); + + void addLetter(char letter, int charposition, float score); + + void clear(); + void analyze(string templateregion, int topn); + + string bestChars; + bool matchesTemplate; + + const vector getResults(); + + private: + Config* config; + //void getTopN(); + void findAllPermutations(vector prevletters, int charPos, int substitutionsLeft); + + void insertLetter(char letter, int charPosition, float score); + + map > rules; + + float calculateMaxConfidenceScore(); + + vector > letters; + vector unknownCharPositions; + + + vector allPossibilities; +}; + +/* +class LetterScores +{ + public: + LetterScores(int numCharPositions); + + void addScore(char letter, int charposition, float score); + + vector getBestScore(); + float getConfidence(); + + private: + int numCharPositions; + + vector letters; + vector charpositions; + vector scores; +}; +*/ +#endif // POSTPROCESS_H \ No newline at end of file diff --git a/src/openalpr/regiondetector.cpp b/src/openalpr/regiondetector.cpp new file mode 100644 index 0000000..cd64969 --- /dev/null +++ b/src/openalpr/regiondetector.cpp @@ -0,0 +1,115 @@ +/* + * 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 "regiondetector.h" + + +RegionDetector::RegionDetector(Config* config) +{ + this->config = config; + // Don't scale. Can change this in the future (i.e., maximum resolution preference, or some such). + this->scale_factor = 1.0f; + + + // Load the cascade classifier + if( this->plate_cascade.load( config->getCascadeRuntimeDir() + config->country + ".xml" ) ) + { + this->loaded = true; + } + else + { + printf("--(!)Error loading classifier\n"); + this->loaded = false; + } + +} + +RegionDetector::~RegionDetector() +{ + +} + + + +bool RegionDetector::isLoaded() +{ + return this->loaded; +} + + +vector RegionDetector::detect(Mat frame) +{ + + Mat frame_gray; + cvtColor( frame, frame_gray, CV_BGR2GRAY ); + + vector regionsOfInterest = doCascade(frame_gray); + + return regionsOfInterest; +} + + +/** @function detectAndDisplay */ +vector RegionDetector::doCascade(Mat frame) +{ + //float scale_factor = 1; + int w = frame.size().width; + int h = frame.size().height; + + vector plates; + + equalizeHist( frame, frame ); + resize(frame, frame, Size(w * this->scale_factor, h * this->scale_factor)); + + //-- Detect plates + timespec startTime; + getTime(&startTime); + + Size minSize(config->minPlateSizeWidthPx * this->scale_factor, config->minPlateSizeHeightPx * this->scale_factor); + Size maxSize(w * config->maxPlateWidthPercent * this->scale_factor, h * config->maxPlateHeightPercent * this->scale_factor); + + + plate_cascade.detectMultiScale( frame, plates, 1.1, 3, + 0, + //0|CV_HAAR_SCALE_IMAGE, + minSize, maxSize ); + + + if (config->debugTiming) + { + timespec endTime; + getTime(&endTime); + cout << "LBP Time: " << diffclock(startTime, endTime) << "ms." << endl; + } + + + + for( int i = 0; i < plates.size(); i++ ) + { + plates[i].x = plates[i].x / scale_factor; + plates[i].y = plates[i].y / scale_factor; + plates[i].width = plates[i].width / scale_factor; + plates[i].height = plates[i].height / scale_factor; + } + + return plates; + +} diff --git a/src/openalpr/regiondetector.h b/src/openalpr/regiondetector.h new file mode 100644 index 0000000..0d53a0d --- /dev/null +++ b/src/openalpr/regiondetector.h @@ -0,0 +1,68 @@ +/* + * 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 REGIONDETECTOR_H +#define REGIONDETECTOR_H + +#include +#include + +#include "opencv2/objdetect/objdetect.hpp" +#include "opencv2/imgproc/imgproc.hpp" +#include "opencv2/core/core.hpp" +#include "opencv2/ml/ml.hpp" + #include "opencv2/highgui/highgui.hpp" + +#include "utility.h" +#include "support/timing.h" +#include "constants.h" + + + + + +class RegionDetector +{ + + public: + RegionDetector(Config* config); + virtual ~RegionDetector(); + + bool isLoaded(); + vector detect(Mat frame); + + private: + Config* config; + + float scale_factor; + CascadeClassifier plate_cascade; + CvSVM* svmClassifier; + bool loaded; + + vector doCascade(Mat frame); + +}; + + + + +#endif // REGIONDETECTOR_H diff --git a/src/openalpr/stateidentifier.cpp b/src/openalpr/stateidentifier.cpp new file mode 100644 index 0000000..4c1afe0 --- /dev/null +++ b/src/openalpr/stateidentifier.cpp @@ -0,0 +1,98 @@ +/* + * 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 "stateidentifier.h" + + +StateIdentifier::StateIdentifier(Config* config) +{ + this->config = config; + + featureMatcher = new FeatureMatcher(config); + + if (featureMatcher->isLoaded() == false) + { + cout << "Can not create detector or descriptor extractor or descriptor matcher of given types" << endl; + return; + } + + featureMatcher->loadRecognitionSet(config->country); +} + +StateIdentifier::~StateIdentifier() +{ + delete featureMatcher; +} + +int StateIdentifier::recognize(Mat img, Rect frame, char* stateCode) +{ + Mat croppedImage = Mat(img, frame); + + return this->recognize(croppedImage, stateCode); +} +// Attempts to recognize the plate. Returns a confidence level. Updates teh "stateCode" variable +// with the value of the country/state +int StateIdentifier::recognize(Mat img, char* stateCode) +{ + + timespec startTime; + getTime(&startTime); + + cvtColor(img, img, CV_BGR2GRAY); + + resize(img, img, getSizeMaintainingAspect(img, config->stateIdImageWidthPx, config->stateIdimageHeightPx)); + + Mat plateImg(img.size(), img.type()); + //plateImg = equalizeBrightness(img); + img.copyTo(plateImg); + + Mat debugImg(plateImg.size(), plateImg.type()); + plateImg.copyTo(debugImg); + vector matchesArray(featureMatcher->numTrainingElements()); + + + RecognitionResult result = featureMatcher->recognize(plateImg, true, &debugImg, true, matchesArray ); + + if (this->config->debugStateId) + { + + + displayImage(config, "State Identifier1", plateImg); + displayImage(config, "State Identifier", debugImg); + cout << result.haswinner << " : " << result.confidence << " : " << result.winner << endl; + } + + + if (config->debugTiming) + { + timespec endTime; + getTime(&endTime); + cout << "State Identification Time: " << diffclock(startTime, endTime) << "ms." << endl; + } + + + if (result.haswinner == false) + return 0; + + strcpy(stateCode, result.winner.c_str()); + + + return result.confidence; +} + diff --git a/src/openalpr/stateidentifier.h b/src/openalpr/stateidentifier.h new file mode 100644 index 0000000..3161541 --- /dev/null +++ b/src/openalpr/stateidentifier.h @@ -0,0 +1,59 @@ +/* + * 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 STATEIDENTIFIER_H +#define STATEIDENTIFIER_H + +#include "opencv2/highgui/highgui.hpp" +#include "opencv2/imgproc/imgproc.hpp" +#include "constants.h" +#include "featurematcher.h" +#include "utility.h" +#include "config.h" + + + +class StateIdentifier +{ + + public: + StateIdentifier(Config* config); + virtual ~StateIdentifier(); + + int recognize(Mat img, Rect frame, char* stateCode); + int recognize(Mat img, char* stateCode); + + //int confidence; + + protected: + Config* config; + + + private: + + + FeatureMatcher* featureMatcher; + +}; + +#endif // STATEIDENTIFIER_H + + diff --git a/src/openalpr/trex.c b/src/openalpr/trex.c new file mode 100644 index 0000000..b9e0690 --- /dev/null +++ b/src/openalpr/trex.c @@ -0,0 +1,643 @@ +/* see copyright notice in trex.h */ +#include +#include +#include +#include +#include "trex.h" + +#ifdef _UINCODE +#define scisprint iswprint +#define scstrlen wcslen +#define scprintf wprintf +#define _SC(x) L(x) +#else +#define scisprint isprint +#define scstrlen strlen +#define scprintf printf +#define _SC(x) (x) +#endif + +#ifdef _DEBUG +#include + +static const TRexChar *g_nnames[] = +{ + _SC("NONE"),_SC("OP_GREEDY"), _SC("OP_OR"), + _SC("OP_EXPR"),_SC("OP_NOCAPEXPR"),_SC("OP_DOT"), _SC("OP_CLASS"), + _SC("OP_CCLASS"),_SC("OP_NCLASS"),_SC("OP_RANGE"),_SC("OP_CHAR"), + _SC("OP_EOL"),_SC("OP_BOL"),_SC("OP_WB") +}; + +#endif +#define OP_GREEDY (MAX_CHAR+1) // * + ? {n} +#define OP_OR (MAX_CHAR+2) +#define OP_EXPR (MAX_CHAR+3) //parentesis () +#define OP_NOCAPEXPR (MAX_CHAR+4) //parentesis (?:) +#define OP_DOT (MAX_CHAR+5) +#define OP_CLASS (MAX_CHAR+6) +#define OP_CCLASS (MAX_CHAR+7) +#define OP_NCLASS (MAX_CHAR+8) //negates class the [^ +#define OP_RANGE (MAX_CHAR+9) +#define OP_CHAR (MAX_CHAR+10) +#define OP_EOL (MAX_CHAR+11) +#define OP_BOL (MAX_CHAR+12) +#define OP_WB (MAX_CHAR+13) + +#define TREX_SYMBOL_ANY_CHAR ('.') +#define TREX_SYMBOL_GREEDY_ONE_OR_MORE ('+') +#define TREX_SYMBOL_GREEDY_ZERO_OR_MORE ('*') +#define TREX_SYMBOL_GREEDY_ZERO_OR_ONE ('?') +#define TREX_SYMBOL_BRANCH ('|') +#define TREX_SYMBOL_END_OF_STRING ('$') +#define TREX_SYMBOL_BEGINNING_OF_STRING ('^') +#define TREX_SYMBOL_ESCAPE_CHAR ('\\') + + +typedef int TRexNodeType; + +typedef struct tagTRexNode{ + TRexNodeType type; + int left; + int right; + int next; +}TRexNode; + +struct TRex{ + const TRexChar *_eol; + const TRexChar *_bol; + const TRexChar *_p; + int _first; + int _op; + TRexNode *_nodes; + int _nallocated; + int _nsize; + int _nsubexpr; + TRexMatch *_matches; + int _currsubexp; + void *_jmpbuf; + const TRexChar **_error; +}; + +static int trex_list(TRex *exp); + +static int trex_newnode(TRex *exp, TRexNodeType type) +{ + TRexNode n; + int newid; + n.type = type; + n.next = n.right = n.left = -1; + if(type == OP_EXPR) + n.right = exp->_nsubexpr++; + if(exp->_nallocated < (exp->_nsize + 1)) { + int oldsize = exp->_nallocated; + exp->_nallocated *= 2; + exp->_nodes = (TRexNode *)realloc(exp->_nodes, exp->_nallocated * sizeof(TRexNode)); + } + exp->_nodes[exp->_nsize++] = n; + newid = exp->_nsize - 1; + return (int)newid; +} + +static void trex_error(TRex *exp,const TRexChar *error) +{ + if(exp->_error) *exp->_error = error; + longjmp(*((jmp_buf*)exp->_jmpbuf),-1); +} + +static void trex_expect(TRex *exp, int n){ + if((*exp->_p) != n) + trex_error(exp, _SC("expected paren")); + exp->_p++; +} + +static TRexChar trex_escapechar(TRex *exp) +{ + if(*exp->_p == TREX_SYMBOL_ESCAPE_CHAR){ + exp->_p++; + switch(*exp->_p) { + case 'v': exp->_p++; return '\v'; + case 'n': exp->_p++; return '\n'; + case 't': exp->_p++; return '\t'; + case 'r': exp->_p++; return '\r'; + case 'f': exp->_p++; return '\f'; + default: return (*exp->_p++); + } + } else if(!scisprint(*exp->_p)) trex_error(exp,_SC("letter expected")); + return (*exp->_p++); +} + +static int trex_charclass(TRex *exp,int classid) +{ + int n = trex_newnode(exp,OP_CCLASS); + exp->_nodes[n].left = classid; + return n; +} + +static int trex_charnode(TRex *exp,TRexBool isclass) +{ + TRexChar t; + if(*exp->_p == TREX_SYMBOL_ESCAPE_CHAR) { + exp->_p++; + switch(*exp->_p) { + case 'n': exp->_p++; return trex_newnode(exp,'\n'); + case 't': exp->_p++; return trex_newnode(exp,'\t'); + case 'r': exp->_p++; return trex_newnode(exp,'\r'); + case 'f': exp->_p++; return trex_newnode(exp,'\f'); + case 'v': exp->_p++; return trex_newnode(exp,'\v'); + case 'a': case 'A': case 'w': case 'W': case 's': case 'S': + case 'd': case 'D': case 'x': case 'X': case 'c': case 'C': + case 'p': case 'P': case 'l': case 'u': + { + t = *exp->_p; exp->_p++; + return trex_charclass(exp,t); + } + case 'b': + case 'B': + if(!isclass) { + int node = trex_newnode(exp,OP_WB); + exp->_nodes[node].left = *exp->_p; + exp->_p++; + return node; + } //else default + default: + t = *exp->_p; exp->_p++; + return trex_newnode(exp,t); + } + } + else if(!scisprint(*exp->_p)) { + + trex_error(exp,_SC("letter expected")); + } + t = *exp->_p; exp->_p++; + return trex_newnode(exp,t); +} +static int trex_class(TRex *exp) +{ + int ret = -1; + int first = -1,chain; + if(*exp->_p == TREX_SYMBOL_BEGINNING_OF_STRING){ + ret = trex_newnode(exp,OP_NCLASS); + exp->_p++; + }else ret = trex_newnode(exp,OP_CLASS); + + if(*exp->_p == ']') trex_error(exp,_SC("empty class")); + chain = ret; + while(*exp->_p != ']' && exp->_p != exp->_eol) { + if(*exp->_p == '-' && first != -1){ + int r,t; + if(*exp->_p++ == ']') trex_error(exp,_SC("unfinished range")); + r = trex_newnode(exp,OP_RANGE); + if(first>*exp->_p) trex_error(exp,_SC("invalid range")); + if(exp->_nodes[first].type == OP_CCLASS) trex_error(exp,_SC("cannot use character classes in ranges")); + exp->_nodes[r].left = exp->_nodes[first].type; + t = trex_escapechar(exp); + exp->_nodes[r].right = t; + exp->_nodes[chain].next = r; + chain = r; + first = -1; + } + else{ + if(first!=-1){ + int c = first; + exp->_nodes[chain].next = c; + chain = c; + first = trex_charnode(exp,TRex_True); + } + else{ + first = trex_charnode(exp,TRex_True); + } + } + } + if(first!=-1){ + int c = first; + exp->_nodes[chain].next = c; + chain = c; + first = -1; + } + /* hack? */ + exp->_nodes[ret].left = exp->_nodes[ret].next; + exp->_nodes[ret].next = -1; + return ret; +} + +static int trex_parsenumber(TRex *exp) +{ + int ret = *exp->_p-'0'; + int positions = 10; + exp->_p++; + while(isdigit(*exp->_p)) { + ret = ret*10+(*exp->_p++-'0'); + if(positions==1000000000) trex_error(exp,_SC("overflow in numeric constant")); + positions *= 10; + }; + return ret; +} + +static int trex_element(TRex *exp) +{ + int ret = -1; + switch(*exp->_p) + { + case '(': { + int expr,newn; + exp->_p++; + + + if(*exp->_p =='?') { + exp->_p++; + trex_expect(exp,':'); + expr = trex_newnode(exp,OP_NOCAPEXPR); + } + else + expr = trex_newnode(exp,OP_EXPR); + newn = trex_list(exp); + exp->_nodes[expr].left = newn; + ret = expr; + trex_expect(exp,')'); + } + break; + case '[': + exp->_p++; + ret = trex_class(exp); + trex_expect(exp,']'); + break; + case TREX_SYMBOL_END_OF_STRING: exp->_p++; ret = trex_newnode(exp,OP_EOL);break; + case TREX_SYMBOL_ANY_CHAR: exp->_p++; ret = trex_newnode(exp,OP_DOT);break; + default: + ret = trex_charnode(exp,TRex_False); + break; + } + + { + int op; + TRexBool isgreedy = TRex_False; + unsigned short p0 = 0, p1 = 0; + switch(*exp->_p){ + case TREX_SYMBOL_GREEDY_ZERO_OR_MORE: p0 = 0; p1 = 0xFFFF; exp->_p++; isgreedy = TRex_True; break; + case TREX_SYMBOL_GREEDY_ONE_OR_MORE: p0 = 1; p1 = 0xFFFF; exp->_p++; isgreedy = TRex_True; break; + case TREX_SYMBOL_GREEDY_ZERO_OR_ONE: p0 = 0; p1 = 1; exp->_p++; isgreedy = TRex_True; break; + case '{': + exp->_p++; + if(!isdigit(*exp->_p)) trex_error(exp,_SC("number expected")); + p0 = (unsigned short)trex_parsenumber(exp); + /*******************************/ + switch(*exp->_p) { + case '}': + p1 = p0; exp->_p++; + break; + case ',': + exp->_p++; + p1 = 0xFFFF; + if(isdigit(*exp->_p)){ + p1 = (unsigned short)trex_parsenumber(exp); + } + trex_expect(exp,'}'); + break; + default: + trex_error(exp,_SC(", or } expected")); + } + /*******************************/ + isgreedy = TRex_True; + break; + + } + if(isgreedy) { + int nnode = trex_newnode(exp,OP_GREEDY); + op = OP_GREEDY; + exp->_nodes[nnode].left = ret; + exp->_nodes[nnode].right = ((p0)<<16)|p1; + ret = nnode; + } + } + if((*exp->_p != TREX_SYMBOL_BRANCH) && (*exp->_p != ')') && (*exp->_p != TREX_SYMBOL_GREEDY_ZERO_OR_MORE) && (*exp->_p != TREX_SYMBOL_GREEDY_ONE_OR_MORE) && (*exp->_p != '\0')) { + int nnode = trex_element(exp); + exp->_nodes[ret].next = nnode; + } + + return ret; +} + +static int trex_list(TRex *exp) +{ + int ret=-1,e; + if(*exp->_p == TREX_SYMBOL_BEGINNING_OF_STRING) { + exp->_p++; + ret = trex_newnode(exp,OP_BOL); + } + e = trex_element(exp); + if(ret != -1) { + exp->_nodes[ret].next = e; + } + else ret = e; + + if(*exp->_p == TREX_SYMBOL_BRANCH) { + int temp,tright; + exp->_p++; + temp = trex_newnode(exp,OP_OR); + exp->_nodes[temp].left = ret; + tright = trex_list(exp); + exp->_nodes[temp].right = tright; + ret = temp; + } + return ret; +} + +static TRexBool trex_matchcclass(int cclass,TRexChar c) +{ + switch(cclass) { + case 'a': return isalpha(c)?TRex_True:TRex_False; + case 'A': return !isalpha(c)?TRex_True:TRex_False; + case 'w': return (isalnum(c) || c == '_')?TRex_True:TRex_False; + case 'W': return (!isalnum(c) && c != '_')?TRex_True:TRex_False; + case 's': return isspace(c)?TRex_True:TRex_False; + case 'S': return !isspace(c)?TRex_True:TRex_False; + case 'd': return isdigit(c)?TRex_True:TRex_False; + case 'D': return !isdigit(c)?TRex_True:TRex_False; + case 'x': return isxdigit(c)?TRex_True:TRex_False; + case 'X': return !isxdigit(c)?TRex_True:TRex_False; + case 'c': return iscntrl(c)?TRex_True:TRex_False; + case 'C': return !iscntrl(c)?TRex_True:TRex_False; + case 'p': return ispunct(c)?TRex_True:TRex_False; + case 'P': return !ispunct(c)?TRex_True:TRex_False; + case 'l': return islower(c)?TRex_True:TRex_False; + case 'u': return isupper(c)?TRex_True:TRex_False; + } + return TRex_False; /*cannot happen*/ +} + +static TRexBool trex_matchclass(TRex* exp,TRexNode *node,TRexChar c) +{ + do { + switch(node->type) { + case OP_RANGE: + if(c >= node->left && c <= node->right) return TRex_True; + break; + case OP_CCLASS: + if(trex_matchcclass(node->left,c)) return TRex_True; + break; + default: + if(c == node->type)return TRex_True; + } + } while((node->next != -1) && (node = &exp->_nodes[node->next])); + return TRex_False; +} + +static const TRexChar *trex_matchnode(TRex* exp,TRexNode *node,const TRexChar *str,TRexNode *next) +{ + + TRexNodeType type = node->type; + switch(type) { + case OP_GREEDY: { + //TRexNode *greedystop = (node->next != -1) ? &exp->_nodes[node->next] : NULL; + TRexNode *greedystop = NULL; + int p0 = (node->right >> 16)&0x0000FFFF, p1 = node->right&0x0000FFFF, nmaches = 0; + const TRexChar *s=str, *good = str; + + if(node->next != -1) { + greedystop = &exp->_nodes[node->next]; + } + else { + greedystop = next; + } + + while((nmaches == 0xFFFF || nmaches < p1)) { + + const TRexChar *stop; + if(!(s = trex_matchnode(exp,&exp->_nodes[node->left],s,greedystop))) + break; + nmaches++; + good=s; + if(greedystop) { + //checks that 0 matches satisfy the expression(if so skips) + //if not would always stop(for instance if is a '?') + if(greedystop->type != OP_GREEDY || + (greedystop->type == OP_GREEDY && ((greedystop->right >> 16)&0x0000FFFF) != 0)) + { + TRexNode *gnext = NULL; + if(greedystop->next != -1) { + gnext = &exp->_nodes[greedystop->next]; + }else if(next && next->next != -1){ + gnext = &exp->_nodes[next->next]; + } + stop = trex_matchnode(exp,greedystop,s,gnext); + if(stop) { + //if satisfied stop it + if(p0 == p1 && p0 == nmaches) break; + else if(nmaches >= p0 && p1 == 0xFFFF) break; + else if(nmaches >= p0 && nmaches <= p1) break; + } + } + } + + if(s >= exp->_eol) + break; + } + if(p0 == p1 && p0 == nmaches) return good; + else if(nmaches >= p0 && p1 == 0xFFFF) return good; + else if(nmaches >= p0 && nmaches <= p1) return good; + return NULL; + } + case OP_OR: { + const TRexChar *asd = str; + TRexNode *temp=&exp->_nodes[node->left]; + while( (asd = trex_matchnode(exp,temp,asd,NULL)) ) { + if(temp->next != -1) + temp = &exp->_nodes[temp->next]; + else + return asd; + } + asd = str; + temp = &exp->_nodes[node->right]; + while( (asd = trex_matchnode(exp,temp,asd,NULL)) ) { + if(temp->next != -1) + temp = &exp->_nodes[temp->next]; + else + return asd; + } + return NULL; + break; + } + case OP_EXPR: + case OP_NOCAPEXPR:{ + TRexNode *n = &exp->_nodes[node->left]; + const TRexChar *cur = str; + int capture = -1; + if(node->type != OP_NOCAPEXPR && node->right == exp->_currsubexp) { + capture = exp->_currsubexp; + exp->_matches[capture].begin = cur; + exp->_currsubexp++; + } + + do { + TRexNode *subnext = NULL; + if(n->next != -1) { + subnext = &exp->_nodes[n->next]; + }else { + subnext = next; + } + if(!(cur = trex_matchnode(exp,n,cur,subnext))) { + if(capture != -1){ + exp->_matches[capture].begin = 0; + exp->_matches[capture].len = 0; + } + return NULL; + } + } while((n->next != -1) && (n = &exp->_nodes[n->next])); + + if(capture != -1) + exp->_matches[capture].len = cur - exp->_matches[capture].begin; + return cur; + } + case OP_WB: + if(str == exp->_bol && !isspace(*str) + || (str == exp->_eol && !isspace(*(str-1))) + || (!isspace(*str) && isspace(*(str+1))) + || (isspace(*str) && !isspace(*(str+1))) ) { + return (node->left == 'b')?str:NULL; + } + return (node->left == 'b')?NULL:str; + case OP_BOL: + if(str == exp->_bol) return str; + return NULL; + case OP_EOL: + if(str == exp->_eol) return str; + return NULL; + case OP_DOT:{ + *str++; + } + return str; + case OP_NCLASS: + case OP_CLASS: + if(trex_matchclass(exp,&exp->_nodes[node->left],*str)?(type == OP_CLASS?TRex_True:TRex_False):(type == OP_NCLASS?TRex_True:TRex_False)) { + *str++; + return str; + } + return NULL; + case OP_CCLASS: + if(trex_matchcclass(node->left,*str)) { + *str++; + return str; + } + return NULL; + default: /* char */ + if(*str != node->type) return NULL; + *str++; + return str; + } + return NULL; +} + +/* public api */ +TRex *trex_compile(const TRexChar *pattern,const TRexChar **error) +{ + TRex *exp = (TRex *)malloc(sizeof(TRex)); + exp->_eol = exp->_bol = NULL; + exp->_p = pattern; + exp->_nallocated = (int)scstrlen(pattern) * sizeof(TRexChar); + exp->_nodes = (TRexNode *)malloc(exp->_nallocated * sizeof(TRexNode)); + exp->_nsize = 0; + exp->_matches = 0; + exp->_nsubexpr = 0; + exp->_first = trex_newnode(exp,OP_EXPR); + exp->_error = error; + exp->_jmpbuf = malloc(sizeof(jmp_buf)); + if(setjmp(*((jmp_buf*)exp->_jmpbuf)) == 0) { + int res = trex_list(exp); + exp->_nodes[exp->_first].left = res; + if(*exp->_p!='\0') + trex_error(exp,_SC("unexpected character")); +#ifdef _DEBUG + { + int nsize,i; + TRexNode *t; + nsize = exp->_nsize; + t = &exp->_nodes[0]; + scprintf(_SC("\n")); + for(i = 0;i < nsize; i++) { + if(exp->_nodes[i].type>MAX_CHAR) + scprintf(_SC("[%02d] %10s "),i,g_nnames[exp->_nodes[i].type-MAX_CHAR]); + else + scprintf(_SC("[%02d] %10c "),i,exp->_nodes[i].type); + scprintf(_SC("left %02d right %02d next %02d\n"),exp->_nodes[i].left,exp->_nodes[i].right,exp->_nodes[i].next); + } + scprintf(_SC("\n")); + } +#endif + exp->_matches = (TRexMatch *) malloc(exp->_nsubexpr * sizeof(TRexMatch)); + memset(exp->_matches,0,exp->_nsubexpr * sizeof(TRexMatch)); + } + else{ + trex_free(exp); + return NULL; + } + return exp; +} + +void trex_free(TRex *exp) +{ + if(exp) { + if(exp->_nodes) free(exp->_nodes); + if(exp->_jmpbuf) free(exp->_jmpbuf); + if(exp->_matches) free(exp->_matches); + free(exp); + } +} + +TRexBool trex_match(TRex* exp,const TRexChar* text) +{ + const TRexChar* res = NULL; + exp->_bol = text; + exp->_eol = text + scstrlen(text); + exp->_currsubexp = 0; + res = trex_matchnode(exp,exp->_nodes,text,NULL); + if(res == NULL || res != exp->_eol) + return TRex_False; + return TRex_True; +} + +TRexBool trex_searchrange(TRex* exp,const TRexChar* text_begin,const TRexChar* text_end,const TRexChar** out_begin, const TRexChar** out_end) +{ + const TRexChar *cur = NULL; + int node = exp->_first; + if(text_begin >= text_end) return TRex_False; + exp->_bol = text_begin; + exp->_eol = text_end; + do { + cur = text_begin; + while(node != -1) { + exp->_currsubexp = 0; + cur = trex_matchnode(exp,&exp->_nodes[node],cur,NULL); + if(!cur) + break; + node = exp->_nodes[node].next; + } + *text_begin++; + } while(cur == NULL && text_begin != text_end); + + if(cur == NULL) + return TRex_False; + + --text_begin; + + if(out_begin) *out_begin = text_begin; + if(out_end) *out_end = cur; + return TRex_True; +} + +TRexBool trex_search(TRex* exp,const TRexChar* text, const TRexChar** out_begin, const TRexChar** out_end) +{ + return trex_searchrange(exp,text,text + scstrlen(text),out_begin,out_end); +} + +int trex_getsubexpcount(TRex* exp) +{ + return exp->_nsubexpr; +} + +TRexBool trex_getsubexp(TRex* exp, int n, TRexMatch *subexp) +{ + if( n<0 || n >= exp->_nsubexpr) return TRex_False; + *subexp = exp->_matches[n]; + return TRex_True; +} + diff --git a/src/openalpr/trex.h b/src/openalpr/trex.h new file mode 100644 index 0000000..bf268b1 --- /dev/null +++ b/src/openalpr/trex.h @@ -0,0 +1,67 @@ +#ifndef _TREX_H_ +#define _TREX_H_ +/*************************************************************** + T-Rex a tiny regular expression library + + Copyright (C) 2003-2006 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. + +****************************************************************/ + +#ifdef _UNICODE +#define TRexChar unsigned short +#define MAX_CHAR 0xFFFF +#define _TREXC(c) L##c +#define trex_strlen wcslen +#define trex_printf wprintf +#else +#define TRexChar char +#define MAX_CHAR 0xFF +#define _TREXC(c) (c) +#define trex_strlen strlen +#define trex_printf printf +#endif + +#ifndef TREX_API +#define TREX_API extern +#endif + +#define TRex_True 1 +#define TRex_False 0 + +typedef unsigned int TRexBool; +typedef struct TRex TRex; + +typedef struct { + const TRexChar *begin; + int len; +} TRexMatch; + +TREX_API TRex *trex_compile(const TRexChar *pattern,const TRexChar **error); +TREX_API void trex_free(TRex *exp); +TREX_API TRexBool trex_match(TRex* exp,const TRexChar* text); +TREX_API TRexBool trex_search(TRex* exp,const TRexChar* text, const TRexChar** out_begin, const TRexChar** out_end); +TREX_API TRexBool trex_searchrange(TRex* exp,const TRexChar* text_begin,const TRexChar* text_end,const TRexChar** out_begin, const TRexChar** out_end); +TREX_API int trex_getsubexpcount(TRex* exp); +TREX_API TRexBool trex_getsubexp(TRex* exp, int n, TRexMatch *subexp); + +#endif diff --git a/src/openalpr/utility.cpp b/src/openalpr/utility.cpp new file mode 100644 index 0000000..a605aa8 --- /dev/null +++ b/src/openalpr/utility.cpp @@ -0,0 +1,400 @@ +/* + * 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 "utility.h" + + #include + +Rect expandRect(Rect original, int expandXPixels, int expandYPixels, int maxX, int maxY) +{ + Rect expandedRegion = Rect(original); + + float halfX = round((float) expandXPixels / 2.0); + float halfY = round((float) expandYPixels / 2.0); + expandedRegion.x = expandedRegion.x - halfX; + expandedRegion.width = expandedRegion.width + expandXPixels; + expandedRegion.y = expandedRegion.y - halfY; + expandedRegion.height = expandedRegion.height + expandYPixels; + + if (expandedRegion.x < 0) + expandedRegion.x = 0; + if (expandedRegion.y < 0) + expandedRegion.y = 0; + if (expandedRegion.x + expandedRegion.width > maxX) + expandedRegion.width = maxX - expandedRegion.x; + if (expandedRegion.y + expandedRegion.height > maxY) + expandedRegion.height = maxY - expandedRegion.y; + + return expandedRegion; +} + +Mat drawImageDashboard(vector images, int imageType, int numColumns) +{ + int numRows = ceil((float) images.size() / (float) numColumns); + + Mat dashboard(Size(images[0].cols * numColumns, images[0].rows * numRows), imageType); + + for (int i = 0; i < numColumns * numRows; i++) + { + if (i < images.size()) + images[i].copyTo(dashboard(Rect((i%numColumns) * images[i].cols, floor((float) i/numColumns) * images[i].rows, images[i].cols, images[i].rows))); + else + { + Mat black = Mat::zeros(images[0].size(), imageType); + black.copyTo(dashboard(Rect((i%numColumns) * images[0].cols, floor((float) i/numColumns) * images[0].rows, images[0].cols, images[0].rows))); + } + } + + return dashboard; +} + +Mat addLabel(Mat input, string label) +{ + const int border_size = 1; + const Scalar border_color(0,0,255); + const int extraHeight = 20; + const Scalar bg(222,222,222); + const Scalar fg(0,0,0); + + Rect destinationRect(border_size, extraHeight, input.cols, input.rows); + Mat newImage(Size(input.cols + (border_size), input.rows + extraHeight + (border_size )), input.type()); + input.copyTo(newImage(destinationRect)); + + cout << " Adding label " << label << endl; + if (input.type() == CV_8U) + cvtColor(newImage, newImage, CV_GRAY2BGR); + + rectangle(newImage, Point(0,0), Point(input.cols, extraHeight), bg, CV_FILLED); + putText(newImage, label, Point(5, extraHeight - 5), CV_FONT_HERSHEY_PLAIN , 0.7, fg); + + rectangle(newImage, Point(0,0), Point(newImage.cols - 1, newImage.rows -1), border_color, border_size); + + return newImage; +} + + + +void drawAndWait(cv::Mat* frame) +{ + cv::imshow("Temp Window", *frame); + + while (cv::waitKey(50) == -1) + { + // loop + } + + cv::destroyWindow("Temp Window"); +} + +void displayImage(Config* config, string windowName, cv::Mat frame) +{ + if (config->debugShowImages) + imshow(windowName, frame); +} + +vector produceThresholds(const Mat img_gray, Config* config) +{ + const int THRESHOLD_COUNT = 10; + //Mat img_equalized = equalizeBrightness(img_gray); + + timespec startTime; + getTime(&startTime); + + vector thresholds; + + //#pragma omp parallel for + for (int i = 0; i < THRESHOLD_COUNT; i++) + thresholds.push_back(Mat(img_gray.size(), CV_8U)); + + + for (int i = 0; i < THRESHOLD_COUNT; i++) + { + + + if (i <= 2) //0-2 + { + int k = ((i%3) * 5) + 7; // 7, 12, 17 + if (k==12) k = 13; // change 12 to 13 + //#pragma omp ordered + adaptiveThreshold(img_gray, thresholds[i], 255, ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY_INV , k, 3); + } + else if (i <= 6) //3-6 + { + int k = i%2; // 0 or 1 + int win = 18 + (k * 4); // 18 or 22 + //#pragma omp ordered + NiblackSauvolaWolfJolion (img_gray, thresholds[i], WOLFJOLION, win, win, 0.05 + (k * 0.35)); + bitwise_not(thresholds[i], thresholds[i]); + + } + else if (i <= 9) //7-9 + { + int k = (i%3) + 1; // 1,2,3 + //#pragma omp ordered + NiblackSauvolaWolfJolion (img_gray, thresholds[i], SAUVOLA, 12, 12, 0.18 * k); + bitwise_not(thresholds[i], thresholds[i]); + + } + + + + + } + + + + + + if (config->debugTiming) + { + timespec endTime; + getTime(&endTime); + cout << " -- Produce Threshold Time: " << diffclock(startTime, endTime) << "ms." << endl; + } + + return thresholds; + //threshold(img_equalized, img_threshold, 100, 255, THRESH_BINARY); + +} + + + + +Mat equalizeBrightness(Mat img) +{ + + // Divide the image by its morphologically closed counterpart + Mat kernel = getStructuringElement(MORPH_ELLIPSE, Size(19,19)); + Mat closed; + morphologyEx(img, closed, MORPH_CLOSE, kernel); + + img.convertTo(img, CV_32FC1); // divide requires floating-point + divide(img, closed, img, 1, CV_32FC1); + normalize(img, img, 0, 255, NORM_MINMAX); + img.convertTo(img, CV_8U); // convert back to unsigned int + + + return img; +} + +void drawRotatedRect(Mat* img, RotatedRect rect, Scalar color, int thickness) +{ + + Point2f rect_points[4]; + rect.points( rect_points ); + for( int j = 0; j < 4; j++ ) + line( *img, rect_points[j], rect_points[(j+1)%4], color, thickness, 8 ); + +} + +void fillMask(Mat img, const Mat mask, Scalar color) +{ + for (int row = 0; row < img.rows; row++) + { + for (int col = 0; col < img.cols; col++) + { + int m = (int) mask.at(row, col); + + if (m) + { + for (int z = 0; z < 3; z++) + { + int prevVal = img.at(row, col)[z]; + img.at(row, col)[z] = ((int) color[z]) | prevVal; + } + } + + } + } +} + + +void drawX(Mat img, Rect rect, Scalar color, int thickness) +{ + Point tl(rect.x, rect.y); + Point tr(rect.x + rect.width, rect.y); + Point bl(rect.x, rect.y + rect.height); + Point br(rect.x + rect.width, rect.y + rect.height); + + line(img, tl, br, color, thickness); + line(img, bl, tr, color, thickness); +} + +double distanceBetweenPoints(Point p1, Point p2) +{ + float asquared = (p2.x - p1.x)*(p2.x - p1.x); + float bsquared = (p2.y - p1.y)*(p2.y - p1.y); + + return sqrt(asquared + bsquared); +} + +float angleBetweenPoints(Point p1, Point p2) +{ + int deltaY = p2.y - p1.y; + int deltaX = p2.x - p1.x; + + return atan2((float) deltaY, (float) deltaX) * (180 / CV_PI); +} + + +Size getSizeMaintainingAspect(Mat inputImg, int maxWidth, int maxHeight) +{ + float aspect = ((float) inputImg.cols) / ((float) inputImg.rows); + + if (maxWidth / aspect > maxHeight) + { + return Size(maxHeight * aspect, maxHeight); + } + else + { + return Size(maxWidth, maxWidth / aspect); + } +} + + +LineSegment::LineSegment() +{ + init(0, 0, 0, 0); +} + +LineSegment::LineSegment(Point p1, Point p2) +{ + init(p1.x, p1.y, p2.x, p2.y); +} +LineSegment::LineSegment(int x1, int y1, int x2, int y2) +{ + init(x1, y1, x2, y2); +} + +void LineSegment::init(int x1, int y1, int x2, int y2) +{ + this->p1 = Point(x1, y1); + this->p2 = Point(x2, y2); + + if (p2.x - p1.x == 0) + this->slope = 0.00000000001; + else + this->slope = (float) (p2.y - p1.y) / (float) (p2.x - p1.x); + + this->length = distanceBetweenPoints(p1, p2); + + this->angle = angleBetweenPoints(p1, p2); +} + +bool LineSegment::isPointBelowLine( Point tp ){ + return ((p2.x - p1.x)*(tp.y - p1.y) - (p2.y - p1.y)*(tp.x - p1.x)) > 0; +} + +float LineSegment::getPointAt(float x) +{ + return slope * (x - p2.x) + p2.y; +} + +Point LineSegment::closestPointOnSegmentTo(Point p) +{ + float top = (p.x - p1.x) * (p2.x - p1.x) + (p.y - p1.y)*(p2.y - p1.y); + + float bottom = distanceBetweenPoints(p2, p1); + bottom = bottom * bottom; + + float u = top / bottom; + + float x = p1.x + u * (p2.x - p1.x); + float y = p1.y + u * (p2.y - p1.y); + + return Point(x, y); +} + +Point LineSegment::intersection(LineSegment line) +{ + float c1, c2; + float intersection_X = -1, intersection_Y= -1; + + + c1 = p1.y - slope * p1.x; // which is same as y2 - slope * x2 + + c2 = line.p2.y - line.slope * line.p2.x; // which is same as y2 - slope * x2 + + if( (slope - line.slope) == 0) + { + //std::cout << "No Intersection between the lines" << endl; + } + else if (p1.x == p2.x) + { + // Line1 is vertical + return Point(p1.x, line.getPointAt(p1.x)); + } + else if (line.p1.x == line.p2.x) + { + // Line2 is vertical + return Point(line.p1.x, getPointAt(line.p1.x)); + } + else + { + intersection_X = (c2 - c1) / (slope - line.slope); + intersection_Y = slope * intersection_X + c1; + + } + + return Point(intersection_X, intersection_Y); + +} + + + +Point LineSegment::midpoint() +{ + // Handle the case where the line is vertical + if (p1.x == p2.x) + { + float ydiff = p2.y-p1.y; + float y = p1.y + (ydiff/2); + return Point(p1.x, y); + } + float diff = p2.x - p1.x; + float midX = ((float) p1.x) + (diff / 2); + int midY = getPointAt(midX); + + return Point(midX, midY); +} + +LineSegment LineSegment::getParallelLine(float distance) +{ + float diff_x = p2.x - p1.x; + float diff_y = p2.y - p1.y; + float angle = atan2( diff_x, diff_y); + float dist_x = distance * cos(angle); + float dist_y = -distance * sin(angle); + + int offsetX = (int)round(dist_x); + int offsetY = (int)round(dist_y); + + LineSegment result(p1.x + offsetX, p1.y + offsetY, + p2.x + offsetX, p2.y + offsetY); + + return result; +} + + + + + diff --git a/src/openalpr/utility.h b/src/openalpr/utility.h new file mode 100644 index 0000000..26f6add --- /dev/null +++ b/src/openalpr/utility.h @@ -0,0 +1,119 @@ +/* + * 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 UTILITY_H +#define UTILITY_H + + + + +#include +#include +#include + +#include "constants.h" +#include "support/timing.h" + #include "opencv2/highgui/highgui.hpp" + #include "opencv2/imgproc/imgproc.hpp" +#include "opencv2/core/core.hpp" +#include "binarize_wolf.h" +#include +#include "config.h" + + + + /* +struct LineSegment +{ + float x1; + float y1; + float x2; + float y2; +}; +*/ + + +class LineSegment +{ + + public: + Point p1, p2; + float slope; + float length; + float angle; + + // LineSegment(Point point1, Point point2); + LineSegment(); + LineSegment(int x1, int y1, int x2, int y2); + LineSegment(Point p1, Point p2); + + void init(int x1, int y1, int x2, int y2); + + bool isPointBelowLine(Point tp); + + float getPointAt(float x); + + Point closestPointOnSegmentTo(Point p); + + Point intersection(LineSegment line); + + LineSegment getParallelLine(float distance); + + Point midpoint(); + + inline std::string str() + { + std::stringstream ss; + ss << "(" << p1.x << ", " << p1.y << ") : (" << p2.x << ", " << p2.y << ")"; + return ss.str() ; + } + + +}; + + + + vector produceThresholds(const Mat img_gray, Config* config); + + Mat drawImageDashboard(vector images, int imageType, int numColumns); + + void displayImage(Config* config, string windowName, cv::Mat frame); + void drawAndWait(cv::Mat* frame); + + double distanceBetweenPoints(Point p1, Point p2); + + void drawRotatedRect(Mat* img, RotatedRect rect, Scalar color, int thickness); + + void drawX(Mat img, Rect rect, Scalar color, int thickness); + void fillMask(Mat img, const Mat mask, Scalar color); + + float angleBetweenPoints(Point p1, Point p2); + + Size getSizeMaintainingAspect(Mat inputImg, int maxWidth, int maxHeight); + + Mat equalizeBrightness(Mat img); + + Rect expandRect(Rect original, int expandXPixels, int expandYPixels, int maxX, int maxY); + + Mat addLabel(Mat input, string label); + +#endif // UTILITY_H diff --git a/src/openalpr/verticalhistogram.cpp b/src/openalpr/verticalhistogram.cpp new file mode 100644 index 0000000..65ae68e --- /dev/null +++ b/src/openalpr/verticalhistogram.cpp @@ -0,0 +1,155 @@ +/* + * 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 "verticalhistogram.h" + +VerticalHistogram::VerticalHistogram(Mat inputImage, Mat mask) +{ + analyzeImage(inputImage, mask); + + + +} + +VerticalHistogram::~VerticalHistogram() +{ + debugImg.release(); + colHeights.clear(); +} + + +void VerticalHistogram::analyzeImage(Mat inputImage, Mat mask) +{ + highestPeak = 0; + lowestValley = inputImage.rows; + + + debugImg = Mat::zeros(inputImage.size(), CV_8U); + + int columnCount; + + for (int col = 0; col < inputImage.cols; col++) + { + columnCount = 0; + + for (int row = 0; row < inputImage.rows; row++) + { + + if (inputImage.at(row, col) > 0 && mask.at(row, col) > 0) + columnCount++; + } + + + for (; columnCount > 0; columnCount--) + debugImg.at(inputImage.rows - columnCount, col) = 255; + + this->colHeights.push_back(columnCount); + + if (columnCount < lowestValley) + lowestValley = columnCount; + if (columnCount > highestPeak) + highestPeak = columnCount; + } + +} + +void VerticalHistogram::findValleys() +{ + int MINIMUM_PEAK_HEIGHT = (int) (((float) highestPeak) * 0.75); + + int totalWidth = colHeights.size(); + + int midpoint = ((highestPeak - lowestValley) / 2) + lowestValley; + + HistogramDirection prevDirection = FALLING; + + int relativePeakHeight = 0; + int valleyStart = 0; + + for (int i = 0; i < totalWidth; i++) + { + bool aboveMidpoint = (colHeights[i] >= midpoint); + + if (aboveMidpoint) + { + if (colHeights[i] > relativePeakHeight) + relativePeakHeight = colHeights[i]; + + + prevDirection = FLAT; + + } + else + { + relativePeakHeight = 0; + + HistogramDirection direction = getHistogramDirection(i); + + if ((prevDirection == FALLING || prevDirection == FLAT) && direction == RISING) + { + + } + else if ((prevDirection == FALLING || prevDirection == FLAT) && direction == RISING) + { + + } + } + + } +} + +HistogramDirection VerticalHistogram::getHistogramDirection(int index) +{ + int EXTRA_WIDTH_TO_AVERAGE = 2; + + float trailingAverage = 0; + float forwardAverage = 0; + + int trailStartIndex = index - EXTRA_WIDTH_TO_AVERAGE; + if (trailStartIndex < 0) + trailStartIndex = 0; + int forwardEndIndex = index + EXTRA_WIDTH_TO_AVERAGE; + if (forwardEndIndex >= colHeights.size()) + forwardEndIndex = colHeights.size() - 1; + + for (int i = index; i >= trailStartIndex; i--) + { + trailingAverage += colHeights[i]; + } + trailingAverage = trailingAverage / ((float) (1 + index - trailStartIndex)); + + for (int i = index; i <= forwardEndIndex; i++) + { + forwardAverage += colHeights[i]; + } + forwardAverage = forwardAverage / ((float) (1 + forwardEndIndex - index)); + + + float diff = forwardAverage - trailingAverage; + float minDiff = ((float) (highestPeak - lowestValley)) * 0.10; + + if (diff > minDiff) + return RISING; + else if (diff < minDiff) + return FALLING; + else + return FLAT; +} + + diff --git a/src/openalpr/verticalhistogram.h b/src/openalpr/verticalhistogram.h new file mode 100644 index 0000000..cae304d --- /dev/null +++ b/src/openalpr/verticalhistogram.h @@ -0,0 +1,62 @@ +/* + * 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 VERTICALHISTOGRAM_H +#define VERTICALHISTOGRAM_H + +#include "opencv2/imgproc/imgproc.hpp" + +using namespace cv; +using namespace std; + + +struct Valley +{ + int startIndex; + int endIndex; + int width; + int pixelsWithin; +}; + +enum HistogramDirection { RISING, FALLING, FLAT }; + +class VerticalHistogram +{ + + public: + VerticalHistogram(Mat inputImage, Mat mask); + virtual ~VerticalHistogram(); + + Mat debugImg; + + + private: + vector colHeights; + int highestPeak; + int lowestValley; + vector valleys; + + void analyzeImage(Mat inputImage, Mat mask); + void findValleys(); + + HistogramDirection getHistogramDirection(int index); +}; + +#endif // VERTICALHISTOGRAM_H \ No newline at end of file