Files
openalpr-base/src/openalpr/textdetection/characteranalysis.cpp

683 lines
21 KiB
C++

/*
* 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 <http://www.gnu.org/licenses/>.
*/
#include <opencv2/imgproc/imgproc.hpp>
#include "characteranalysis.h"
#include "linefinder.h"
using namespace cv;
using namespace std;
namespace alpr
{
bool sort_text_line(TextLine i, TextLine j) { return (i.topLine.p1.y < j.topLine.p1.y); }
CharacterAnalysis::CharacterAnalysis(PipelineData* pipeline_data)
{
this->pipeline_data = pipeline_data;
this->config = pipeline_data->config;
if (this->config->debugCharAnalysis)
cout << "Starting CharacterAnalysis identification" << endl;
this->analyze();
}
CharacterAnalysis::~CharacterAnalysis()
{
}
void CharacterAnalysis::analyze()
{
timespec startTime;
getTimeMonotonic(&startTime);
if (config->always_invert)
bitwise_not(pipeline_data->crop_gray, pipeline_data->crop_gray);
pipeline_data->clearThresholds();
pipeline_data->thresholds = produceThresholds(pipeline_data->crop_gray, config);
timespec contoursStartTime;
getTimeMonotonic(&contoursStartTime);
pipeline_data->textLines.clear();
for (unsigned int i = 0; i < pipeline_data->thresholds.size(); i++)
{
TextContours tc(pipeline_data->thresholds[i]);
allTextContours.push_back(tc);
}
if (config->debugTiming)
{
timespec contoursEndTime;
getTimeMonotonic(&contoursEndTime);
cout << " -- Character Analysis Find Contours Time: " << diffclock(contoursStartTime, contoursEndTime) << "ms." << endl;
}
//Mat img_equalized = equalizeBrightness(img_gray);
timespec filterStartTime;
getTimeMonotonic(&filterStartTime);
for (unsigned int i = 0; i < pipeline_data->thresholds.size(); i++)
{
this->filter(pipeline_data->thresholds[i], allTextContours[i]);
if (config->debugCharAnalysis)
cout << "Threshold " << i << " had " << allTextContours[i].getGoodIndicesCount() << " good indices." << endl;
}
if (config->debugTiming)
{
timespec filterEndTime;
getTimeMonotonic(&filterEndTime);
cout << " -- Character Analysis Filter Time: " << diffclock(filterStartTime, filterEndTime) << "ms." << endl;
}
PlateMask plateMask(pipeline_data);
plateMask.findOuterBoxMask(allTextContours);
pipeline_data->hasPlateBorder = plateMask.hasPlateMask;
pipeline_data->plateBorderMask = plateMask.getMask();
if (plateMask.hasPlateMask)
{
// Filter out bad contours now that we have an outer box mask...
for (unsigned int i = 0; i < pipeline_data->thresholds.size(); i++)
{
filterByOuterMask(allTextContours[i]);
}
}
int bestFitScore = -1;
int bestFitIndex = -1;
for (unsigned int i = 0; i < pipeline_data->thresholds.size(); i++)
{
int segmentCount = allTextContours[i].getGoodIndicesCount();
if (segmentCount > bestFitScore)
{
bestFitScore = segmentCount;
bestFitIndex = i;
bestThreshold = pipeline_data->thresholds[i];
bestContours = allTextContours[i];
}
}
if (this->config->debugCharAnalysis)
cout << "Best fit score: " << bestFitScore << " Index: " << bestFitIndex << endl;
if (bestFitScore <= 1)
{
pipeline_data->disqualified = true;
pipeline_data->disqualify_reason = "Low best fit score in characteranalysis";
return;
}
//getColorMask(img, allContours, allHierarchy, charSegments);
if (this->config->debugCharAnalysis)
{
Mat img_contours = bestContours.drawDebugImage(bestThreshold);
displayImage(config, "Matching Contours", img_contours);
}
if (config->auto_invert)
pipeline_data->plate_inverted = isPlateInverted();
else
pipeline_data->plate_inverted = config->always_invert;
if (config->debugGeneral)
cout << "Plate inverted: " << pipeline_data->plate_inverted << endl;
// Invert multiline plates and redo the thresholds before finding the second line
if (config->multiline && config->auto_invert && pipeline_data->plate_inverted)
{
bitwise_not(pipeline_data->crop_gray, pipeline_data->crop_gray);
pipeline_data->thresholds = produceThresholds(pipeline_data->crop_gray, pipeline_data->config);
}
LineFinder lf(pipeline_data);
vector<vector<Point> > linePolygons = lf.findLines(pipeline_data->crop_gray, bestContours);
vector<TextLine> tempTextLines;
for (unsigned int i = 0; i < linePolygons.size(); i++)
{
vector<Point> linePolygon = linePolygons[i];
LineSegment topLine = LineSegment(linePolygon[0].x, linePolygon[0].y, linePolygon[1].x, linePolygon[1].y);
LineSegment bottomLine = LineSegment(linePolygon[3].x, linePolygon[3].y, linePolygon[2].x, linePolygon[2].y);
vector<Point> textArea = getCharArea(topLine, bottomLine);
TextLine textLine(textArea, linePolygon, pipeline_data->crop_gray.size());
tempTextLines.push_back(textLine);
}
filterBetweenLines(bestThreshold, bestContours, tempTextLines);
// Sort the lines from top to bottom.
std::sort(tempTextLines.begin(), tempTextLines.end(), sort_text_line);
// Now that we've filtered a few more contours, re-do the text area.
for (unsigned int i = 0; i < tempTextLines.size(); i++)
{
vector<Point> updatedTextArea = getCharArea(tempTextLines[i].topLine, tempTextLines[i].bottomLine);
vector<Point> linePolygon = tempTextLines[i].linePolygon;
if (updatedTextArea.size() > 0 && linePolygon.size() > 0)
{
pipeline_data->textLines.push_back(TextLine(updatedTextArea, linePolygon, pipeline_data->crop_gray.size()));
}
}
if (pipeline_data->textLines.size() > 0)
{
int confidenceDrainers = 0;
int charSegmentCount = this->bestContours.getGoodIndicesCount();
if (charSegmentCount == 1)
confidenceDrainers += 91;
else if (charSegmentCount < 5)
confidenceDrainers += (5 - charSegmentCount) * 10;
// Use the angle for the first line -- assume they'll always be parallel for multi-line plates
int absangle = abs(pipeline_data->textLines[0].topLine.angle);
if (absangle > config->maxPlateAngleDegrees)
confidenceDrainers += 91;
else if (absangle > 1)
confidenceDrainers += absangle ;
// If a multiline plate has only one line, disqualify
if (pipeline_data->isMultiline && pipeline_data->textLines.size() < 2)
{
if (config->debugCharAnalysis)
std::cout << "Did not detect multiple lines on multi-line plate" << std::endl;
confidenceDrainers += 95;
}
if (confidenceDrainers >= 90)
{
pipeline_data->disqualified = true;
pipeline_data->disqualify_reason = "Low confidence in characteranalysis";
}
else
{
float confidence = 100 - confidenceDrainers;
pipeline_data->confidence_weights.setScore("CHARACTER_ANALYSIS_SCORE", confidence, 1.0);
}
}
else
{
pipeline_data->disqualified = true;
pipeline_data->disqualify_reason = "No text lines found in characteranalysis";
}
if (config->debugTiming)
{
timespec endTime;
getTimeMonotonic(&endTime);
cout << "Character Analysis Time: " << diffclock(startTime, endTime) << "ms." << endl;
}
// Draw debug dashboard
if (this->pipeline_data->config->debugCharAnalysis && pipeline_data->textLines.size() > 0)
{
vector<Mat> tempDash;
for (unsigned int z = 0; z < pipeline_data->thresholds.size(); z++)
{
Mat tmp(pipeline_data->thresholds[z].size(), pipeline_data->thresholds[z].type());
pipeline_data->thresholds[z].copyTo(tmp);
cvtColor(tmp, tmp, COLOR_GRAY2BGR);
tempDash.push_back(tmp);
}
Mat bestVal(this->bestThreshold.size(), this->bestThreshold.type());
this->bestThreshold.copyTo(bestVal);
cvtColor(bestVal, bestVal, COLOR_GRAY2BGR);
for (unsigned int z = 0; z < this->bestContours.size(); z++)
{
Scalar dcolor(255,0,0);
if (this->bestContours.goodIndices[z])
dcolor = Scalar(0,255,0);
drawContours(bestVal, this->bestContours.contours, z, dcolor, 1);
}
tempDash.push_back(bestVal);
displayImage(config, "Character Region Step 1 Thresholds", drawImageDashboard(tempDash, bestVal.type(), 3));
}
}
Mat CharacterAnalysis::getCharacterMask()
{
Mat charMask = Mat::zeros(bestThreshold.size(), CV_8U);
for (unsigned int i = 0; i < bestContours.size(); i++)
{
if (bestContours.goodIndices[i] == false)
continue;
drawContours(charMask, bestContours.contours,
i, // draw this contour
cv::Scalar(255,255,255), // in
FILLED,
8,
bestContours.hierarchy,
1
);
}
return charMask;
}
void CharacterAnalysis::filter(Mat img, TextContours& textContours)
{
int STARTING_MIN_HEIGHT = round (((float) img.rows) * config->charAnalysisMinPercent);
int STARTING_MAX_HEIGHT = round (((float) img.rows) * (config->charAnalysisMinPercent + config->charAnalysisHeightRange));
int HEIGHT_STEP = round (((float) img.rows) * config->charAnalysisHeightStepSize);
int NUM_STEPS = config->charAnalysisNumSteps;
int bestFitScore = -1;
vector<bool> bestIndices;
for (int i = 0; i < NUM_STEPS; i++)
{
//vector<bool> goodIndices(contours.size());
for (unsigned int z = 0; z < textContours.size(); z++) textContours.goodIndices[z] = true;
this->filterByBoxSize(textContours, STARTING_MIN_HEIGHT + (i * HEIGHT_STEP), STARTING_MAX_HEIGHT + (i * HEIGHT_STEP));
int goodIndices = textContours.getGoodIndicesCount();
if ( goodIndices == 0 || goodIndices <= bestFitScore) // Don't bother doing more filtering if we already lost...
continue;
this->filterContourHoles(textContours);
goodIndices = textContours.getGoodIndicesCount();
if ( goodIndices == 0 || goodIndices <= bestFitScore) // Don't bother doing more filtering if we already lost...
continue;
int segmentCount = textContours.getGoodIndicesCount();
if (segmentCount > bestFitScore)
{
bestFitScore = segmentCount;
bestIndices = textContours.getIndicesCopy();
}
}
textContours.setIndices(bestIndices);
}
// Goes through the contours for the plate and picks out possible char segments based on min/max height
void CharacterAnalysis::filterByBoxSize(TextContours& textContours, int minHeightPx, int maxHeightPx)
{
// For multiline plates, we want to target the biggest line for character analysis, since it should be easier to spot.
float larger_char_height_mm = 0;
float larger_char_width_mm = 0;
for (unsigned int i = 0; i < config->charHeightMM.size(); i++)
{
if (config->charHeightMM[i] > larger_char_height_mm)
{
larger_char_height_mm = config->charHeightMM[i];
larger_char_width_mm = config->charWidthMM[i];
}
}
float idealAspect=larger_char_width_mm / larger_char_height_mm;
float aspecttolerance=0.25;
for (unsigned int i = 0; i < textContours.size(); i++)
{
if (textContours.goodIndices[i] == false)
continue;
textContours.goodIndices[i] = false; // Set it to not included unless it proves valid
//Create bounding rect of object
Rect mr= boundingRect(textContours.contours[i]);
float minWidth = mr.height * 0.2;
//Crop image
//cout << "Height: " << minHeightPx << " - " << mr.height << " - " << maxHeightPx << " ////// Width: " << mr.width << " - " << minWidth << endl;
if(mr.height >= minHeightPx && mr.height <= maxHeightPx && mr.width > minWidth)
{
float charAspect= (float)mr.width/(float)mr.height;
//cout << " -- stage 2 aspect: " << abs(charAspect) << " - " << aspecttolerance << endl;
if (abs(charAspect - idealAspect) < aspecttolerance)
textContours.goodIndices[i] = true;
}
}
}
void CharacterAnalysis::filterContourHoles(TextContours& textContours)
{
for (unsigned int i = 0; i < textContours.size(); i++)
{
if (textContours.goodIndices[i] == false)
continue;
textContours.goodIndices[i] = false; // Set it to not included unless it proves valid
int parentIndex = textContours.hierarchy[i][3];
if (parentIndex >= 0 && textContours.goodIndices[parentIndex])
{
// this contour is a child of an already identified contour. REMOVE it
if (this->config->debugCharAnalysis)
{
cout << "filterContourHoles: contour index: " << i << endl;
}
}
else
{
textContours.goodIndices[i] = true;
}
}
}
// 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
void CharacterAnalysis::filterByParentContour( TextContours& textContours)
{
vector<int> parentIDs;
vector<int> votes;
for (unsigned int i = 0; i < textContours.size(); i++)
{
if (textContours.goodIndices[i] == false)
continue;
textContours.goodIndices[i] = false; // Set it to not included unless it proves
int voteIndex = -1;
int parentID = textContours.hierarchy[i][3];
// check if parentID is already in the lsit
for (unsigned 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 (unsigned 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 (unsigned int i = 0; i < textContours.size(); i++)
{
if (textContours.goodIndices[i] == false)
continue;
if (totalVotes <= 2)
{
textContours.goodIndices[i] = true;
}
else if (textContours.hierarchy[i][3] == winningParentId)
{
textContours.goodIndices[i] = true;
}
}
}
void CharacterAnalysis::filterBetweenLines(Mat img, TextContours& textContours, vector<TextLine> textLines )
{
static float MIN_AREA_PERCENT_WITHIN_LINES = 0.88;
static float MAX_DISTANCE_PERCENT_FROM_LINES = 0.15;
if (textLines.size() == 0)
return;
vector<Point> validPoints;
// Create a white mask for the area inside the polygon
Mat outerMask = Mat::zeros(img.size(), CV_8U);
for (unsigned int i = 0; i < textLines.size(); i++)
fillConvexPoly(outerMask, textLines[i].linePolygon.data(), textLines[i].linePolygon.size(), Scalar(255,255,255));
// For each contour, determine if enough of it is between the lines to qualify
for (unsigned int i = 0; i < textContours.size(); i++)
{
if (textContours.goodIndices[i] == false)
continue;
float percentInsideMask = getContourAreaPercentInsideMask(outerMask,
textContours.contours,
textContours.hierarchy,
(int) i);
if (percentInsideMask < MIN_AREA_PERCENT_WITHIN_LINES)
{
// Not enough area is inside the lines.
if (config->debugCharAnalysis)
cout << "Rejecting due to insufficient area" << endl;
textContours.goodIndices[i] = false;
continue;
}
// now check to make sure that the top and bottom of the contour are near enough to the lines
// First get the high and low point for the contour
// Remember that origin is top-left, so the top Y values are actually closer to 0.
Rect brect = boundingRect(textContours.contours[i]);
int xmiddle = brect.x + (brect.width / 2);
Point topMiddle = Point(xmiddle, brect.y);
Point botMiddle = Point(xmiddle, brect.y+brect.height);
// Get the absolute distance from the top and bottom lines
for (unsigned int i = 0; i < textLines.size(); i++)
{
Point closestTopPoint = textLines[i].topLine.closestPointOnSegmentTo(topMiddle);
Point closestBottomPoint = textLines[i].bottomLine.closestPointOnSegmentTo(botMiddle);
float absTopDistance = distanceBetweenPoints(closestTopPoint, topMiddle);
float absBottomDistance = distanceBetweenPoints(closestBottomPoint, botMiddle);
float maxDistance = textLines[i].lineHeight * MAX_DISTANCE_PERCENT_FROM_LINES;
if (absTopDistance < maxDistance && absBottomDistance < maxDistance)
{
// It's ok, leave it as-is.
}
else
{
textContours.goodIndices[i] = false;
if (config->debugCharAnalysis)
cout << "Rejecting due to top/bottom points that are out of range" << endl;
}
}
}
}
void CharacterAnalysis::filterByOuterMask(TextContours& textContours)
{
float MINIMUM_PERCENT_LEFT_AFTER_MASK = 0.1;
float MINIMUM_PERCENT_OF_CHARS_INSIDE_PLATE_MASK = 0.6;
if (this->pipeline_data->hasPlateBorder == false)
return;
cv::Mat plateMask = pipeline_data->plateBorderMask;
Mat tempMaskedContour = Mat::zeros(plateMask.size(), CV_8U);
Mat tempFullContour = Mat::zeros(plateMask.size(), CV_8U);
int charsInsideMask = 0;
int totalChars = 0;
vector<bool> originalindices;
for (unsigned int i = 0; i < textContours.size(); i++)
originalindices.push_back(textContours.goodIndices[i]);
for (unsigned int i=0; i < textContours.size(); i++)
{
if (textContours.goodIndices[i] == false)
continue;
totalChars++;
tempFullContour = Mat::zeros(plateMask.size(), CV_8U);
drawContours(tempFullContour, textContours.contours, i, Scalar(255,255,255), FILLED, 8, textContours.hierarchy);
bitwise_and(tempFullContour, plateMask, tempMaskedContour);
textContours.goodIndices[i] = false;
float beforeMaskWhiteness = mean(tempFullContour)[0];
float afterMaskWhiteness = mean(tempMaskedContour)[0];
if (afterMaskWhiteness / beforeMaskWhiteness > MINIMUM_PERCENT_LEFT_AFTER_MASK)
{
charsInsideMask++;
textContours.goodIndices[i] = true;
}
}
if (totalChars == 0)
{
textContours.goodIndices = originalindices;
return;
}
// 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)
{
textContours.goodIndices = originalindices;
return;
}
}
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;
}
vector<Point> CharacterAnalysis::getCharArea(LineSegment topLine, LineSegment bottomLine)
{
const int MAX = 100000;
const int MIN= -1;
int leftX = MAX;
int rightX = MIN;
for (unsigned int i = 0; i < bestContours.size(); i++)
{
if (bestContours.goodIndices[i] == false)
continue;
for (unsigned int z = 0; z < bestContours.contours[i].size(); z++)
{
if (bestContours.contours[i][z].x < leftX)
leftX = bestContours.contours[i][z].x;
if (bestContours.contours[i][z].x > rightX)
rightX = bestContours.contours[i][z].x;
}
}
vector<Point> 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;
}
}