/*
* 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 "alpr_impl.h"
void plateAnalysisThread(void* arg);
using namespace std;
using namespace cv;
namespace alpr
{
AlprImpl::AlprImpl(const std::string country, const std::string configFile, const std::string runtimeDir)
{
timespec startTime;
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)
{
return;
}
plateDetector = createDetector(config);
ocr = new OCR(config);
setNumThreads(0);
setDetectRegion(DEFAULT_DETECT_REGION);
this->topN = DEFAULT_TOPN;
setDefaultRegion("");
prewarp = new PreWarp(config);
timespec endTime;
getTimeMonotonic(&endTime);
if (config->debugTiming)
cout << "OpenALPR Initialization Time: " << diffclock(startTime, endTime) << "ms." << endl;
}
AlprImpl::~AlprImpl()
{
delete config;
if (plateDetector != ALPR_NULL_PTR)
delete plateDetector;
if (stateDetector != ALPR_NULL_PTR)
delete stateDetector;
if (ocr != ALPR_NULL_PTR)
delete ocr;
if (prewarp != ALPR_NULL_PTR)
delete prewarp;
}
bool AlprImpl::isLoaded()
{
return config->loaded;
}
AlprFullDetails AlprImpl::recognizeFullDetails(cv::Mat img, std::vector regionsOfInterest)
{
timespec startTime;
getTimeMonotonic(&startTime);
AlprFullDetails response;
response.results.epoch_time = getEpochTimeMs();
response.results.img_width = img.cols;
response.results.img_height = img.rows;
// Fix regions of interest in case they extend beyond the bounds of the image
for (unsigned int i = 0; i < regionsOfInterest.size(); i++)
regionsOfInterest[i] = expandRect(regionsOfInterest[i], 0, 0, img.cols, img.rows);
for (unsigned int i = 0; i < regionsOfInterest.size(); i++)
{
response.results.regionsOfInterest.push_back(AlprRegionOfInterest(regionsOfInterest[i].x, regionsOfInterest[i].y,
regionsOfInterest[i].width, regionsOfInterest[i].height));
}
if (!img.data)
{
// Invalid image
if (this->config->debugGeneral)
std::cerr << "Invalid image" << std::endl;
return response;
}
// Convert image to grayscale if required
Mat grayImg = img;
if (img.channels() > 2)
cvtColor( img, grayImg, CV_BGR2GRAY );
// Prewarp the image and ROIs if configured]
std::vector warpedRegionsOfInterest = regionsOfInterest;
// 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)
{
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);
}
}
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;
}
if (config->debugGeneral && config->debugShowImages)
{
for (unsigned int i = 0; i < regionsOfInterest.size(); i++)
{
rectangle(img, regionsOfInterest[i], Scalar(0,255,0), 2);
}
for (unsigned int i = 0; i < response.plateRegions.size(); i++)
{
rectangle(img, response.plateRegions[i].rect, Scalar(0, 0, 255), 2);
}
for (unsigned int i = 0; i < response.results.plates.size(); i++)
{
// Draw a box around the license plate
for (int z = 0; z < 4; z++)
{
AlprCoordinate* coords = response.results.plates[i].plate_points;
Point p1(coords[z].x, coords[z].y);
Point p2(coords[(z + 1) % 4].x, coords[(z + 1) % 4].y);
line(img, p1, p2, Scalar(255,0,255), 2);
}
// Draw the individual character boxes
for (int q = 0; q < response.results.plates[i].bestPlate.character_details.size(); q++)
{
AlprChar details = response.results.plates[i].bestPlate.character_details[q];
line(img, Point(details.corners[0].x, details.corners[0].y), Point(details.corners[1].x, details.corners[1].y), Scalar(0,255,0), 1);
line(img, Point(details.corners[1].x, details.corners[1].y), Point(details.corners[2].x, details.corners[2].y), Scalar(0,255,0), 1);
line(img, Point(details.corners[2].x, details.corners[2].y), Point(details.corners[3].x, details.corners[3].y), Scalar(0,255,0), 1);
line(img, Point(details.corners[3].x, details.corners[3].y), Point(details.corners[0].x, details.corners[0].y), Scalar(0,255,0), 1);
}
}
displayImage(config, "Main Image", img);
// Sleep 1ms
sleep_ms(1);
}
if (config->debugPauseOnFrame)
{
// Pause indefinitely until they press a key
while ((char) cv::waitKey(50) == -1)
{}
}
return response;
}
AlprResults AlprImpl::recognize( std::vector imageBytes)
{
cv::Mat img = cv::imdecode(cv::Mat(imageBytes), 1);
return this->recognize(img);
}
AlprResults AlprImpl::recognize(std::vector imageBytes, std::vector regionsOfInterest)
{
cv::Mat img = cv::imdecode(cv::Mat(imageBytes), 1);
std::vector rois = convertRects(regionsOfInterest);
AlprFullDetails fullDetails = recognizeFullDetails(img, rois);
return fullDetails.results;
}
AlprResults AlprImpl::recognize( unsigned char* pixelData, int bytesPerPixel, int imgWidth, int imgHeight, std::vector regionsOfInterest)
{
int arraySize = imgWidth * imgHeight * bytesPerPixel;
cv::Mat imgData = cv::Mat(arraySize, 1, CV_8U, pixelData);
cv::Mat img = imgData.reshape(bytesPerPixel, imgHeight);
if (regionsOfInterest.size() == 0)
{
AlprRegionOfInterest fullFrame(0,0, img.cols, img.rows);
regionsOfInterest.push_back(fullFrame);
}
return this->recognize(img, this->convertRects(regionsOfInterest));
}
AlprResults AlprImpl::recognize(cv::Mat img)
{
std::vector regionsOfInterest;
regionsOfInterest.push_back(cv::Rect(0, 0, img.cols, img.rows));
return this->recognize(img, regionsOfInterest);
}
AlprResults AlprImpl::recognize(cv::Mat img, std::vector regionsOfInterest)
{
AlprFullDetails fullDetails = recognizeFullDetails(img, regionsOfInterest);
return fullDetails.results;
}
std::vector AlprImpl::convertRects(std::vector regionsOfInterest)
{
std::vector rectRegions;
for (unsigned int i = 0; i < regionsOfInterest.size(); i++)
{
rectRegions.push_back(cv::Rect(regionsOfInterest[i].x, regionsOfInterest[i].y, regionsOfInterest[i].width, regionsOfInterest[i].height));
}
return rectRegions;
}
string AlprImpl::toJson( const AlprResults results )
{
cJSON *root, *jsonResults;
root = cJSON_CreateObject();
cJSON_AddNumberToObject(root,"version", 2 );
cJSON_AddStringToObject(root,"data_type", "alpr_results" );
cJSON_AddNumberToObject(root,"epoch_time", results.epoch_time );
cJSON_AddNumberToObject(root,"img_width", results.img_width );
cJSON_AddNumberToObject(root,"img_height", results.img_height );
cJSON_AddNumberToObject(root,"processing_time_ms", results.total_processing_time_ms );
// Add the regions of interest to the JSON
cJSON *rois;
cJSON_AddItemToObject(root, "regions_of_interest", rois=cJSON_CreateArray());
for (unsigned int i=0;ibestPlate.characters.c_str());
cJSON_AddNumberToObject(root,"confidence", result->bestPlate.overall_confidence);
cJSON_AddNumberToObject(root,"matches_template", result->bestPlate.matches_template);
cJSON_AddNumberToObject(root,"plate_index", result->plate_index);
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_AddNumberToObject(root,"requested_topn", result->requested_topn);
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);
}
cJSON_AddItemToObject(root, "candidates", candidates=cJSON_CreateArray());
for (unsigned int i = 0; i < result->topNPlates.size(); i++)
{
cJSON *candidate_object;
candidate_object = cJSON_CreateObject();
cJSON_AddStringToObject(candidate_object, "plate", result->topNPlates[i].characters.c_str());
cJSON_AddNumberToObject(candidate_object, "confidence", result->topNPlates[i].overall_confidence);
cJSON_AddNumberToObject(candidate_object, "matches_template", result->topNPlates[i].matches_template);
cJSON_AddItemToArray(candidates, candidate_object);
}
return root;
}
AlprResults AlprImpl::fromJson(std::string json) {
AlprResults allResults;
cJSON* root = cJSON_Parse(json.c_str());
int version = cJSON_GetObjectItem(root, "version")->valueint;
allResults.epoch_time = (int64_t) cJSON_GetObjectItem(root, "epoch_time")->valuedouble;
allResults.img_width = cJSON_GetObjectItem(root, "img_width")->valueint;
allResults.img_height = cJSON_GetObjectItem(root, "img_height")->valueint;
allResults.total_processing_time_ms = cJSON_GetObjectItem(root, "processing_time_ms")->valueint;
cJSON* rois = cJSON_GetObjectItem(root,"regions_of_interest");
int numRois = cJSON_GetArraySize(rois);
for (int c = 0; c < numRois; c++)
{
cJSON* roi = cJSON_GetArrayItem(rois, c);
int x = cJSON_GetObjectItem(roi, "x")->valueint;
int y = cJSON_GetObjectItem(roi, "y")->valueint;
int width = cJSON_GetObjectItem(roi, "width")->valueint;
int height = cJSON_GetObjectItem(roi, "height")->valueint;
AlprRegionOfInterest alprRegion(x,y,width,height);
allResults.regionsOfInterest.push_back(alprRegion);
}
cJSON* resultsArray = cJSON_GetObjectItem(root,"results");
int resultsSize = cJSON_GetArraySize(resultsArray);
for (int i = 0; i < resultsSize; i++)
{
cJSON* item = cJSON_GetArrayItem(resultsArray, i);
AlprPlateResult plate;
//plate.bestPlate = cJSON_GetObjectItem(item, "plate")->valuestring;
plate.processing_time_ms = cJSON_GetObjectItem(item, "processing_time_ms")->valuedouble;
plate.plate_index = cJSON_GetObjectItem(item, "plate_index")->valueint;
plate.region = std::string(cJSON_GetObjectItem(item, "region")->valuestring);
plate.regionConfidence = cJSON_GetObjectItem(item, "region_confidence")->valueint;
plate.requested_topn = cJSON_GetObjectItem(item, "requested_topn")->valueint;
cJSON* coordinates = cJSON_GetObjectItem(item,"coordinates");
for (int c = 0; c < 4; c++)
{
cJSON* coordinate = cJSON_GetArrayItem(coordinates, c);
AlprCoordinate alprcoord;
alprcoord.x = cJSON_GetObjectItem(coordinate, "x")->valueint;
alprcoord.y = cJSON_GetObjectItem(coordinate, "y")->valueint;
plate.plate_points[c] = alprcoord;
}
cJSON* candidates = cJSON_GetObjectItem(item,"candidates");
int numCandidates = cJSON_GetArraySize(candidates);
for (int c = 0; c < numCandidates; c++)
{
cJSON* candidate = cJSON_GetArrayItem(candidates, c);
AlprPlate plateCandidate;
plateCandidate.characters = std::string(cJSON_GetObjectItem(candidate, "plate")->valuestring);
plateCandidate.overall_confidence = cJSON_GetObjectItem(candidate, "confidence")->valuedouble;
plateCandidate.matches_template = (cJSON_GetObjectItem(candidate, "matches_template")->valueint) != 0;
plate.topNPlates.push_back(plateCandidate);
if (c == 0)
{
plate.bestPlate = plateCandidate;
}
}
allResults.plates.push_back(plate);
}
cJSON_Delete(root);
return allResults;
}
void AlprImpl::setDetectRegion(bool detectRegion)
{
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;
}
}
void AlprImpl::setTopN(int topn)
{
this->topN = topn;
}
void AlprImpl::setDefaultRegion(string region)
{
this->defaultRegion = region;
}
std::string AlprImpl::getVersion()
{
std::stringstream ss;
ss << OPENALPR_MAJOR_VERSION << "." << OPENALPR_MINOR_VERSION << "." << OPENALPR_PATCH_VERSION;
return ss.str();
}
cv::Mat AlprImpl::getCharacterTransformMatrix(PipelineData* pipeline_data ) {
std::vector crop_corners;
crop_corners.push_back(Point2f(0,0));
crop_corners.push_back(Point2f(pipeline_data->crop_gray.cols,0));
crop_corners.push_back(Point2f(pipeline_data->crop_gray.cols,pipeline_data->crop_gray.rows));
crop_corners.push_back(Point2f(0,pipeline_data->crop_gray.rows));
// Transform the points from the cropped region (skew corrected license plate region) back to the original image
cv::Mat transmtx = cv::getPerspectiveTransform(crop_corners, pipeline_data->plate_corners);
return transmtx;
}
std::vector AlprImpl::getCharacterPoints(cv::Rect char_rect, cv::Mat transmtx ) {
std::vector points;
points.push_back(Point2f(char_rect.x, char_rect.y));
points.push_back(Point2f(char_rect.x + char_rect.width, char_rect.y));
points.push_back(Point2f(char_rect.x + char_rect.width, char_rect.y + char_rect.height));
points.push_back(Point2f(char_rect.x, char_rect.y + char_rect.height));
cv::perspectiveTransform(points, points, transmtx);
// If using prewarp, remap the points to the original image
points = prewarp->projectPoints(points, true);
std::vector cornersvector;
for (int i = 0; i < 4; i++)
{
AlprCoordinate coord;
coord.x = round(points[i].x);
coord.y = round(points[i].y);
cornersvector.push_back(coord);
}
return cornersvector;
}
}