From 8bb3264072e40d8e06eed1adae7fb125a52f5402 Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Tue, 15 Sep 2015 23:10:41 -0400 Subject: [PATCH] Support analysis for multiple training data sets in a single pass. Iterate over each training data set and choose the best scoring plate when there are overlaps This is useful when countries may have multiple different plate styles (e.g., EU single line and EU multi-line) --- src/openalpr/CMakeLists.txt | 1 + src/openalpr/alpr_impl.cpp | 409 ++++++++++++++++------------- src/openalpr/alpr_impl.h | 14 +- src/openalpr/config.cpp | 64 ++++- src/openalpr/config.h | 6 + src/openalpr/result_aggregator.cpp | 190 ++++++++++++++ src/openalpr/result_aggregator.h | 62 +++++ src/openalpr/utility.cpp | 19 +- src/openalpr/utility.h | 3 + src/tests/CMakeLists.txt | 3 +- src/tests/test_config.cpp | 53 ++++ 11 files changed, 618 insertions(+), 206 deletions(-) create mode 100644 src/openalpr/result_aggregator.cpp create mode 100644 src/openalpr/result_aggregator.h create mode 100644 src/tests/test_config.cpp diff --git a/src/openalpr/CMakeLists.txt b/src/openalpr/CMakeLists.txt index 1424ad7..2219465 100644 --- a/src/openalpr/CMakeLists.txt +++ b/src/openalpr/CMakeLists.txt @@ -35,6 +35,7 @@ set(lpr_source_files pipeline_data.cpp cjson.c motiondetector.cpp + result_aggregator.cpp ) diff --git a/src/openalpr/alpr_impl.cpp b/src/openalpr/alpr_impl.cpp index e065f67..a0ef23f 100644 --- a/src/openalpr/alpr_impl.cpp +++ b/src/openalpr/alpr_impl.cpp @@ -18,6 +18,7 @@ */ #include "alpr_impl.h" +#include "result_aggregator.h" void plateAnalysisThread(void* arg); @@ -34,11 +35,9 @@ namespace alpr getTimeMonotonic(&startTime); config = new Config(country, configFile, runtimeDir); - - plateDetector = ALPR_NULL_PTR; - stateDetector = ALPR_NULL_PTR; - ocr = ALPR_NULL_PTR; + prewarp = ALPR_NULL_PTR; + // Config file or runtime dir not found. Don't process any further. if (config->loaded == false) @@ -46,8 +45,24 @@ namespace alpr return; } - plateDetector = createDetector(config); - ocr = new OCR(config); + for (unsigned int i = 0; i < config->loaded_countries.size(); i++) + { + config->setCountry(config->loaded_countries[i]); + + AlprRecognizers recognizer; + recognizer.plateDetector = createDetector(config); + recognizer.ocr = new OCR(config); + + recognizer.stateDetector = new StateDetector(this->config->country, this->config->runtimeBaseDir); + + + + + recognizers[config->country] = recognizer; + + + } + setNumThreads(0); setDetectRegion(DEFAULT_DETECT_REGION); @@ -62,18 +77,24 @@ namespace alpr cout << "OpenALPR Initialization Time: " << diffclock(startTime, endTime) << "ms." << endl; } + AlprImpl::~AlprImpl() { delete config; - if (plateDetector != ALPR_NULL_PTR) - delete plateDetector; + typedef std::map::iterator it_type; + for(it_type iterator = recognizers.begin(); iterator != recognizers.end(); iterator++) { - if (stateDetector != ALPR_NULL_PTR) - delete stateDetector; + if (iterator->second.plateDetector != ALPR_NULL_PTR) + delete iterator->second.plateDetector; + + if (iterator->second.stateDetector != ALPR_NULL_PTR) + delete iterator->second.stateDetector; + + if (iterator->second.ocr != ALPR_NULL_PTR) + delete iterator->second.ocr; + } - if (ocr != ALPR_NULL_PTR) - delete ocr; if (prewarp != ALPR_NULL_PTR) delete prewarp; @@ -126,174 +147,24 @@ namespace alpr // Warp the image if prewarp is provided grayImg = prewarp->warpImage(grayImg); warpedRegionsOfInterest = prewarp->projectRects(regionsOfInterest, grayImg.cols, grayImg.rows, false); - - vector warpedPlateRegions; - // Find all the candidate regions - if (config->skipDetection == false) + + // Iterate through each country provided (typically just one) + // and aggregate the results if necessary + ResultAggregator aggregator; + for (unsigned int i = 0; i < config->loaded_countries.size(); i++) { - warpedPlateRegions = plateDetector->detect(grayImg, warpedRegionsOfInterest); - } - else - { - // They have elected to skip plate detection. Instead, return a list of plate regions - // based on their regions of interest - for (unsigned int i = 0; i < warpedRegionsOfInterest.size(); i++) - { - PlateRegion pr; - pr.rect = cv::Rect(warpedRegionsOfInterest[i]); - warpedPlateRegions.push_back(pr); - } + if (config->debugGeneral) + cout << "Analyzing: " << config->loaded_countries[i] << endl; + + config->setCountry(config->loaded_countries[i]); + AlprFullDetails sub_results = analyzeSingleCountry(img, grayImg, warpedRegionsOfInterest); + + aggregator.addResults(sub_results); } + response = aggregator.getAggregateResults(); - queue plateQueue; - for (unsigned int i = 0; i < warpedPlateRegions.size(); i++) - plateQueue.push(warpedPlateRegions[i]); - - int platecount = 0; - while(!plateQueue.empty()) - { - PlateRegion plateRegion = plateQueue.front(); - plateQueue.pop(); - - PipelineData pipeline_data(img, grayImg, plateRegion.rect, config); - pipeline_data.prewarp = prewarp; - - timespec platestarttime; - getTimeMonotonic(&platestarttime); - - LicensePlateCandidate lp(&pipeline_data); - - lp.recognize(); - - bool plateDetected = false; - if (pipeline_data.disqualified && config->debugGeneral) - { - cout << "Disqualify reason: " << pipeline_data.disqualify_reason << endl; - } - if (!pipeline_data.disqualified) - { - AlprPlateResult plateResult; - plateResult.region = defaultRegion; - plateResult.regionConfidence = 0; - plateResult.plate_index = platecount++; - - // If using prewarp, remap the plate corners to the original image - vector cornerPoints = pipeline_data.plate_corners; - cornerPoints = prewarp->projectPoints(cornerPoints, true); - - for (int pointidx = 0; pointidx < 4; pointidx++) - { - plateResult.plate_points[pointidx].x = (int) cornerPoints[pointidx].x; - plateResult.plate_points[pointidx].y = (int) cornerPoints[pointidx].y; - } - - if (detectRegion) - { - std::vector state_candidates = stateDetector->detect(pipeline_data.color_deskewed.data, - pipeline_data.color_deskewed.elemSize(), - pipeline_data.color_deskewed.cols, - pipeline_data.color_deskewed.rows); - - if (state_candidates.size() > 0) - { - plateResult.region = state_candidates[0].state_code; - plateResult.regionConfidence = (int) state_candidates[0].confidence; - } - } - - if (plateResult.region.length() > 0 && ocr->postProcessor.regionIsValid(plateResult.region) == false) - { - std::cerr << "Invalid pattern provided: " << plateResult.region << std::endl; - std::cerr << "Valid patterns are located in the " << config->country << ".patterns file" << std::endl; - } - - ocr->performOCR(&pipeline_data); - ocr->postProcessor.analyze(plateResult.region, topN); - - timespec resultsStartTime; - getTimeMonotonic(&resultsStartTime); - - const vector ppResults = ocr->postProcessor.getResults(); - - int bestPlateIndex = 0; - - cv::Mat charTransformMatrix = getCharacterTransformMatrix(&pipeline_data); - bool isBestPlateSelected = false; - for (unsigned int pp = 0; pp < ppResults.size(); pp++) - { - - // Set our "best plate" match to either the first entry, or the first entry with a postprocessor template match - if (isBestPlateSelected == false && ppResults[pp].matchesTemplate){ - bestPlateIndex = plateResult.topNPlates.size(); - isBestPlateSelected = true; - } - - AlprPlate aplate; - aplate.characters = ppResults[pp].letters; - aplate.overall_confidence = ppResults[pp].totalscore; - aplate.matches_template = ppResults[pp].matchesTemplate; - - // Grab detailed results for each character - for (unsigned int c_idx = 0; c_idx < ppResults[pp].letter_details.size(); c_idx++) - { - AlprChar character_details; - character_details.character = ppResults[pp].letter_details[c_idx].letter; - character_details.confidence = ppResults[pp].letter_details[c_idx].totalscore; - cv::Rect char_rect = pipeline_data.charRegions[ppResults[pp].letter_details[c_idx].charposition]; - std::vector charpoints = getCharacterPoints(char_rect, charTransformMatrix ); - for (int cpt = 0; cpt < 4; cpt++) - character_details.corners[cpt] = charpoints[cpt]; - aplate.character_details.push_back(character_details); - } - plateResult.topNPlates.push_back(aplate); - } - - if (plateResult.topNPlates.size() > bestPlateIndex) - { - AlprPlate bestPlate; - bestPlate.characters = plateResult.topNPlates[bestPlateIndex].characters; - bestPlate.matches_template = plateResult.topNPlates[bestPlateIndex].matches_template; - bestPlate.overall_confidence = plateResult.topNPlates[bestPlateIndex].overall_confidence; - bestPlate.character_details = plateResult.topNPlates[bestPlateIndex].character_details; - - plateResult.bestPlate = bestPlate; - } - - timespec plateEndTime; - getTimeMonotonic(&plateEndTime); - plateResult.processing_time_ms = diffclock(platestarttime, plateEndTime); - if (config->debugTiming) - { - cout << "Result Generation Time: " << diffclock(resultsStartTime, plateEndTime) << "ms." << endl; - } - - if (plateResult.topNPlates.size() > 0) - { - plateDetected = true; - response.results.plates.push_back(plateResult); - } - } - - if (!plateDetected) - { - // Not a valid plate - // Check if this plate has any children, if so, send them back up for processing - for (unsigned int childidx = 0; childidx < plateRegion.children.size(); childidx++) - { - plateQueue.push(plateRegion.children[childidx]); - } - } - - } - - // Unwarp plate regions if necessary - prewarp->projectPlateRegions(warpedPlateRegions, grayImg.cols, grayImg.rows, true); - response.plateRegions = warpedPlateRegions; - timespec endTime; getTimeMonotonic(&endTime); - response.results.total_processing_time_ms = diffclock(startTime, endTime); - if (config->debugTiming) { cout << "Total Time to process image: " << diffclock(startTime, endTime) << "ms." << endl; @@ -352,7 +223,183 @@ namespace alpr return response; } + AlprFullDetails AlprImpl::analyzeSingleCountry(cv::Mat colorImg, cv::Mat grayImg, std::vector warpedRegionsOfInterest) + { + AlprFullDetails response; + AlprRecognizers country_recognizers = recognizers[config->country]; + timespec startTime; + getTimeMonotonic(&startTime); + + vector warpedPlateRegions; + // Find all the candidate regions + if (config->skipDetection == false) + { + warpedPlateRegions = country_recognizers.plateDetector->detect(grayImg, warpedRegionsOfInterest); + } + else + { + // They have elected to skip plate detection. Instead, return a list of plate regions + // based on their regions of interest + for (unsigned int i = 0; i < warpedRegionsOfInterest.size(); i++) + { + PlateRegion pr; + pr.rect = cv::Rect(warpedRegionsOfInterest[i]); + warpedPlateRegions.push_back(pr); + } + } + + queue plateQueue; + for (unsigned int i = 0; i < warpedPlateRegions.size(); i++) + plateQueue.push(warpedPlateRegions[i]); + + int platecount = 0; + while(!plateQueue.empty()) + { + PlateRegion plateRegion = plateQueue.front(); + plateQueue.pop(); + + PipelineData pipeline_data(colorImg, grayImg, plateRegion.rect, config); + pipeline_data.prewarp = prewarp; + + timespec platestarttime; + getTimeMonotonic(&platestarttime); + + LicensePlateCandidate lp(&pipeline_data); + + lp.recognize(); + + bool plateDetected = false; + if (pipeline_data.disqualified && config->debugGeneral) + { + cout << "Disqualify reason: " << pipeline_data.disqualify_reason << endl; + } + if (!pipeline_data.disqualified) + { + AlprPlateResult plateResult; + plateResult.region = defaultRegion; + plateResult.regionConfidence = 0; + plateResult.plate_index = platecount++; + + // If using prewarp, remap the plate corners to the original image + vector cornerPoints = pipeline_data.plate_corners; + cornerPoints = prewarp->projectPoints(cornerPoints, true); + + for (int pointidx = 0; pointidx < 4; pointidx++) + { + plateResult.plate_points[pointidx].x = (int) cornerPoints[pointidx].x; + plateResult.plate_points[pointidx].y = (int) cornerPoints[pointidx].y; + } + + if (detectRegion) + { + std::vector state_candidates = country_recognizers.stateDetector->detect(pipeline_data.color_deskewed.data, + pipeline_data.color_deskewed.elemSize(), + pipeline_data.color_deskewed.cols, + pipeline_data.color_deskewed.rows); + + if (state_candidates.size() > 0) + { + plateResult.region = state_candidates[0].state_code; + plateResult.regionConfidence = (int) state_candidates[0].confidence; + } + } + + if (plateResult.region.length() > 0 && country_recognizers.ocr->postProcessor.regionIsValid(plateResult.region) == false) + { + std::cerr << "Invalid pattern provided: " << plateResult.region << std::endl; + std::cerr << "Valid patterns are located in the " << config->country << ".patterns file" << std::endl; + } + + country_recognizers.ocr->performOCR(&pipeline_data); + country_recognizers.ocr->postProcessor.analyze(plateResult.region, topN); + + timespec resultsStartTime; + getTimeMonotonic(&resultsStartTime); + + const vector ppResults = country_recognizers.ocr->postProcessor.getResults(); + + int bestPlateIndex = 0; + + cv::Mat charTransformMatrix = getCharacterTransformMatrix(&pipeline_data); + bool isBestPlateSelected = false; + for (unsigned int pp = 0; pp < ppResults.size(); pp++) + { + + // Set our "best plate" match to either the first entry, or the first entry with a postprocessor template match + if (isBestPlateSelected == false && ppResults[pp].matchesTemplate){ + bestPlateIndex = plateResult.topNPlates.size(); + isBestPlateSelected = true; + } + + AlprPlate aplate; + aplate.characters = ppResults[pp].letters; + aplate.overall_confidence = ppResults[pp].totalscore; + aplate.matches_template = ppResults[pp].matchesTemplate; + + // Grab detailed results for each character + for (unsigned int c_idx = 0; c_idx < ppResults[pp].letter_details.size(); c_idx++) + { + AlprChar character_details; + character_details.character = ppResults[pp].letter_details[c_idx].letter; + character_details.confidence = ppResults[pp].letter_details[c_idx].totalscore; + cv::Rect char_rect = pipeline_data.charRegions[ppResults[pp].letter_details[c_idx].charposition]; + std::vector charpoints = getCharacterPoints(char_rect, charTransformMatrix ); + for (int cpt = 0; cpt < 4; cpt++) + character_details.corners[cpt] = charpoints[cpt]; + aplate.character_details.push_back(character_details); + } + plateResult.topNPlates.push_back(aplate); + } + + if (plateResult.topNPlates.size() > bestPlateIndex) + { + AlprPlate bestPlate; + bestPlate.characters = plateResult.topNPlates[bestPlateIndex].characters; + bestPlate.matches_template = plateResult.topNPlates[bestPlateIndex].matches_template; + bestPlate.overall_confidence = plateResult.topNPlates[bestPlateIndex].overall_confidence; + bestPlate.character_details = plateResult.topNPlates[bestPlateIndex].character_details; + + plateResult.bestPlate = bestPlate; + } + + timespec plateEndTime; + getTimeMonotonic(&plateEndTime); + plateResult.processing_time_ms = diffclock(platestarttime, plateEndTime); + if (config->debugTiming) + { + cout << "Result Generation Time: " << diffclock(resultsStartTime, plateEndTime) << "ms." << endl; + } + + if (plateResult.topNPlates.size() > 0) + { + plateDetected = true; + response.results.plates.push_back(plateResult); + } + } + + if (!plateDetected) + { + // Not a valid plate + // Check if this plate has any children, if so, send them back up for processing + for (unsigned int childidx = 0; childidx < plateRegion.children.size(); childidx++) + { + plateQueue.push(plateRegion.children[childidx]); + } + } + + } + + // Unwarp plate regions if necessary + prewarp->projectPlateRegions(warpedPlateRegions, grayImg.cols, grayImg.rows, true); + response.plateRegions = warpedPlateRegions; + + timespec endTime; + getTimeMonotonic(&endTime); + response.results.total_processing_time_ms = diffclock(startTime, endTime); + + return response; + } AlprResults AlprImpl::recognize( std::vector imageBytes) { @@ -596,18 +643,6 @@ namespace alpr { this->detectRegion = detectRegion; - if (detectRegion && this->stateDetector == NULL) - { - timespec startTime; - getTimeMonotonic(&startTime); - - this->stateDetector = new StateDetector(this->config->country, this->config->runtimeBaseDir); - - timespec endTime; - getTimeMonotonic(&endTime); - if (config->debugTiming) - cout << "State Identification Initialization Time: " << diffclock(startTime, endTime) << "ms." << endl; - } } diff --git a/src/openalpr/alpr_impl.h b/src/openalpr/alpr_impl.h index 6941fd1..cc22fd4 100644 --- a/src/openalpr/alpr_impl.h +++ b/src/openalpr/alpr_impl.h @@ -66,6 +66,13 @@ namespace alpr AlprResults results; }; + struct AlprRecognizers + { + Detector* plateDetector; + StateDetector* stateDetector; + OCR* ocr; + }; + class AlprImpl { @@ -83,6 +90,8 @@ namespace alpr void applyRegionTemplate(AlprPlateResult* result, std::string region); + AlprFullDetails analyzeSingleCountry(cv::Mat colorImg, cv::Mat grayImg, std::vector regionsOfInterest); + void setDetectRegion(bool detectRegion); void setTopN(int topn); void setDefaultRegion(std::string region); @@ -99,9 +108,8 @@ namespace alpr private: - Detector* plateDetector; - StateDetector* stateDetector; - OCR* ocr; + std::map recognizers; + PreWarp* prewarp; int topN; diff --git a/src/openalpr/config.cpp b/src/openalpr/config.cpp index 3cc3049..8c93106 100644 --- a/src/openalpr/config.cpp +++ b/src/openalpr/config.cpp @@ -21,6 +21,7 @@ #include "support/filesystem.h" #include "support/platform.h" #include "simpleini/simpleini.h" +#include "utility.h" using namespace std; @@ -88,9 +89,6 @@ namespace alpr } - this->country = country; - - loadCommonValues(configFile); if (runtime_dir.compare("") != 0) @@ -115,21 +113,24 @@ namespace alpr return; } - std::string country_config_file = this->runtimeBaseDir + "/config/" + country + ".conf"; - if (fileExists(country_config_file.c_str()) == false) - { - std::cerr << "--(!) Country config file '" << country_config_file << "' does not exist. Missing config for the country: '" << country<< "'!" << endl; - return; - } - - loadCountryValues(country_config_file, country); + this->loaded_countries = this->parse_country_string(country); - if (fileExists((this->runtimeBaseDir + "/ocr/tessdata/" + this->ocrLanguage + ".traineddata").c_str()) == false) + if (this->loaded_countries.size() == 0) { - std::cerr << "--(!) Runtime directory '" << this->runtimeBaseDir << "' is invalid. Missing OCR data for the country: '" << country<< "'!" << endl; + std::cerr << "--(!) Country not specified." << endl; return; } - + for (unsigned int i = 0; i < loaded_countries.size(); i++) + { + bool country_loaded = setCountry(this->loaded_countries[i]); + if (!country_loaded) + { + return; + } + } + setCountry(this->loaded_countries[0]); + + if (this->debugGeneral) { std::cout << debug_message << endl; @@ -309,7 +310,42 @@ namespace alpr } + std::vector Config::parse_country_string(std::string countries) + { + std::istringstream ss(countries); + std::string token; + std::vector parsed_countries; + while(std::getline(ss, token, ',')) { + std::string trimmed_token = trim(token); + if (trimmed_token.size() > 0) + parsed_countries.push_back(trimmed_token); + } + + return parsed_countries; + } + + bool Config::setCountry(std::string country) + { + this->country = country; + + std::string country_config_file = this->runtimeBaseDir + "/config/" + country + ".conf"; + if (fileExists(country_config_file.c_str()) == false) + { + std::cerr << "--(!) Country config file '" << country_config_file << "' does not exist. Missing config for the country: '" << country<< "'!" << endl; + return false; + } + + loadCountryValues(country_config_file, country); + + if (fileExists((this->runtimeBaseDir + "/ocr/tessdata/" + this->ocrLanguage + ".traineddata").c_str()) == false) + { + std::cerr << "--(!) Runtime directory '" << this->runtimeBaseDir << "' is invalid. Missing OCR data for the country: '" << country<< "'!" << endl; + return false; + } + + return true; + } float getFloat(CSimpleIniA* ini, string section, string key, float defaultValue) { diff --git a/src/openalpr/config.h b/src/openalpr/config.h index 3475f33..268ffde 100644 --- a/src/openalpr/config.h +++ b/src/openalpr/config.h @@ -26,6 +26,7 @@ #include #include +#include #include /* getenv */ #include @@ -130,11 +131,16 @@ namespace alpr std::string runtimeBaseDir; + std::vector loaded_countries; + + bool setCountry(std::string country); + private: float ocrImagePercent; float stateIdImagePercent; + std::vector parse_country_string(std::string countries); void loadCommonValues(std::string configFile); void loadCountryValues(std::string configFile, std::string country); diff --git a/src/openalpr/result_aggregator.cpp b/src/openalpr/result_aggregator.cpp new file mode 100644 index 0000000..38fe800 --- /dev/null +++ b/src/openalpr/result_aggregator.cpp @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2015 OpenALPR Technology, Inc. + * Open source 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 "result_aggregator.h" + +using namespace std; +using namespace cv; + +namespace alpr +{ + + ResultAggregator::ResultAggregator() + { + + } + + ResultAggregator::~ResultAggregator() { + + } + + + void ResultAggregator::addResults(AlprFullDetails full_results) + { + all_results.push_back(full_results); + } + + AlprFullDetails ResultAggregator::getAggregateResults() + { + assert(all_results.size() > 0); + + if (all_results.size() == 1) + return all_results[0]; + + + AlprFullDetails response; + + // Plate regions are needed for benchmarking + // Copy all detected boxes across all results + for (unsigned int i = 0; i < all_results.size(); i++) + { + for (unsigned int k = 0; k < all_results[i].plateRegions.size(); k++) + response.plateRegions.push_back(all_results[i].plateRegions[k]); + } + + + response.results.epoch_time = all_results[0].results.epoch_time; + response.results.img_height = all_results[0].results.img_height; + response.results.img_width = all_results[0].results.img_width; + response.results.total_processing_time_ms = all_results[0].results.total_processing_time_ms; + response.results.regionsOfInterest = all_results[0].results.regionsOfInterest; + + + vector > clusters = findClusters(); + + // Assume we have multiple results, one cluster for each unique train data (e.g., eu, eu2) + + // Now for each cluster of plates, pick the best one + for (unsigned int i = 0; i < clusters.size(); i++) + { + float best_confidence = 0; + int best_index = 0; + for (unsigned int k = 0; k < clusters[i].size(); k++) + { + if (clusters[i][k].bestPlate.overall_confidence > best_confidence) + { + best_confidence = clusters[i][k].bestPlate.overall_confidence; + best_index = k; + } + } + + response.results.plates.push_back(clusters[i][best_index]); + } + + return response; + } + + // Searches all_plates to find overlapping plates + // Returns an array containing "clusters" (overlapping plates) + std::vector > ResultAggregator::findClusters() + { + std::vector > clusters; + + for (unsigned int i = 0; i < all_results.size(); i++) + { + for (unsigned int plate_id = 0; plate_id < all_results[i].results.plates.size(); plate_id++) + { + AlprPlateResult plate = all_results[i].results.plates[plate_id]; + + int cluster_index = overlaps(plate, clusters); + if (cluster_index < 0) + { + vector new_cluster; + new_cluster.push_back(plate); + clusters.push_back(new_cluster); + } + else + { + clusters[cluster_index].push_back(plate); + } + } + } + + return clusters; + } + + PlateShapeInfo ResultAggregator::getShapeInfo(AlprPlateResult plate) + { + int NUM_POINTS = 4; + Moments mu; + + PlateShapeInfo response; + + vector points; + for (int i = 0; i < NUM_POINTS; i++ ) + { + cv::Point p(plate.plate_points[i].x, plate.plate_points[i].y); + points.push_back(p); + } + + mu = moments( points, false ); + response.center = cv::Point2f( mu.m10/mu.m00 , mu.m01/mu.m00 ); + response.area = mu.m00; + + Rect r = cv::boundingRect(points); + response.max_width = r.width; + response.max_height = r.height; + + return response; + } + + // Returns the cluster ID if the plate overlaps. Otherwise returns -1 + int ResultAggregator::overlaps(AlprPlateResult plate, + std::vector > clusters) + { + // Check the center positions to see how close they are to each other + // Also compare the size. If it's much much larger/smaller, treat it as a separate cluster + + PlateShapeInfo psi = getShapeInfo(plate); + + for (unsigned int i = 0; i < clusters.size(); i++) + { + for (unsigned int k = 0; k < clusters[i].size(); k++) + { + PlateShapeInfo cluster_shapeinfo = getShapeInfo(clusters[i][k]); + + int diffx = abs(psi.center.x - cluster_shapeinfo.center.x); + int diffy = abs(psi.center.y - cluster_shapeinfo.center.y); + + // divide the larger plate area by the smaller plate area to determine a match + float area_diff; + if (psi.area > cluster_shapeinfo.area) + area_diff = psi.area / cluster_shapeinfo.area; + else + area_diff = cluster_shapeinfo.area / psi.area; + + int max_x_diff = (psi.max_width + cluster_shapeinfo.max_width) / 2; + int max_y_diff = (psi.max_height + cluster_shapeinfo.max_height) / 2; + + float max_area_diff = 4.0; + // Consider it a match if center diffx/diffy are less than the average height + // the area is not more than 4x different + + if (diffx <= max_x_diff && diffy <= max_y_diff && area_diff <= max_area_diff) + { + return i; + } + } + + } + + + return -1; + } +} \ No newline at end of file diff --git a/src/openalpr/result_aggregator.h b/src/openalpr/result_aggregator.h new file mode 100644 index 0000000..837cd1f --- /dev/null +++ b/src/openalpr/result_aggregator.h @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2015 OpenALPR Technology, Inc. + * Open source 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 OPENALPR_RESULTAGGREGATOR_H +#define OPENALPR_RESULTAGGREGATOR_H + + +#include "alpr_impl.h" + + +// Runs the analysis for multiple training sets, and aggregates the results into the best matches + +struct PlateShapeInfo +{ + cv::Point2f center; + float area; + int max_width; + int max_height; +}; + +namespace alpr +{ + + class ResultAggregator + { + public: + ResultAggregator(); + + virtual ~ResultAggregator(); + + void addResults(AlprFullDetails full_results); + + AlprFullDetails getAggregateResults(); + + private: + std::vector all_results; + + PlateShapeInfo getShapeInfo(AlprPlateResult plate); + + std::vector > findClusters(); + int overlaps(AlprPlateResult plate, std::vector > clusters); + }; + +} + +#endif //OPENALPR_RESULTAGGREGATOR_H diff --git a/src/openalpr/utility.cpp b/src/openalpr/utility.cpp index bd1c1be..ac370cf 100644 --- a/src/openalpr/utility.cpp +++ b/src/openalpr/utility.cpp @@ -578,5 +578,22 @@ int levenshteinDistance (const std::string &s1, const std::string &s2, int max) ss << value; return ss.str(); } - + +// trim from start + std::string <rim(std::string &s) { + s.erase(s.begin(), std::find_if(s.begin(), s.end(), std::not1(std::ptr_fun(std::isspace)))); + return s; + } + +// trim from end + std::string &rtrim(std::string &s) { + s.erase(std::find_if(s.rbegin(), s.rend(), std::not1(std::ptr_fun(std::isspace))).base(), s.end()); + return s; + } + +// trim from both ends + std::string &trim(std::string &s) { + return ltrim(rtrim(s)); + } + } \ No newline at end of file diff --git a/src/openalpr/utility.h b/src/openalpr/utility.h index 642fce2..b2743b7 100644 --- a/src/openalpr/utility.h +++ b/src/openalpr/utility.h @@ -109,6 +109,9 @@ namespace alpr std::string toString(float value); std::string toString(double value); + std::string <rim(std::string &s); + std::string &rtrim(std::string &s); + std::string &trim(std::string &s); } #endif // OPENALPR_UTILITY_H diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index 739553b..818df57 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -2,7 +2,8 @@ enable_testing() ADD_EXECUTABLE( unittests test_api.cpp - test_utility.cpp + test_utility.cpp + test_config.cpp test_regex.cpp ) diff --git a/src/tests/test_config.cpp b/src/tests/test_config.cpp new file mode 100644 index 0000000..c207796 --- /dev/null +++ b/src/tests/test_config.cpp @@ -0,0 +1,53 @@ +/* + * File: utility_tests.cpp + * Author: mhill + * + * Created on October 23, 2014, 10:16 PM + */ + +#include +#include "catch.hpp" +#include "config.h" + +using namespace std; +using namespace alpr; + +Config get_config(std::string countries) +{ + Config config(countries, "", ""); + return config; +} + +TEST_CASE( "Loading Countries", "[Config]" ) { + + REQUIRE( get_config("us,eu").loaded_countries.size() == 2 ); + + REQUIRE( get_config("us").loaded_countries.size() == 1 ); + + REQUIRE( get_config("eu").loaded_countries.size() == 1 ); + + REQUIRE( get_config("us, eu").loaded_countries.size() == 2 ); + + REQUIRE( get_config("us, eu, au, kr").loaded_countries.size() == 4 ); + + REQUIRE( get_config("us , eu").loaded_countries.size() == 2 ); + + REQUIRE( get_config("us,eu,").loaded_countries.size() == 2 ); + + REQUIRE( get_config(",,us,eu,").loaded_countries.size() == 2 ); + +} + + +//TEST_CASE( "Modifying Countries", "[Config]" ) +//{ +// Config config("us,eu", "/etc/openalpr/openalpr.conf", "/usr/share/openalpr/runtime_data/"); +// +// REQUIRE(config.ocrLanguage == "lus"); +// +// config.setCountry("eu"); +// REQUIRE(config.ocrLanguage == "leu"); +// +// config.setCountry("us"); +// REQUIRE(config.ocrLanguage == "lus"); +//} \ No newline at end of file