/*
* 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"
using namespace cv;
using namespace std;
CharacterAnalysis::CharacterAnalysis(PipelineData* pipeline_data)
{
this->pipeline_data = pipeline_data;
this->config = pipeline_data->config;
this->hasPlateMask = false;
if (this->config->debugCharAnalysis)
cout << "Starting CharacterAnalysis identification" << endl;
}
CharacterAnalysis::~CharacterAnalysis()
{
}
void CharacterAnalysis::analyze()
{
pipeline_data->clearThresholds();
pipeline_data->thresholds = produceThresholds(pipeline_data->crop_gray, config);
timespec startTime;
getTime(&startTime);
for (uint i = 0; i < pipeline_data->thresholds.size(); i++)
{
vector > contours;
vector hierarchy;
Mat tempThreshold(pipeline_data->thresholds[i].size(), CV_8U);
pipeline_data->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 (uint i = 0; i < pipeline_data->thresholds.size(); i++)
{
vector goodIndices = this->filter(pipeline_data->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 (uint i = 0; i < pipeline_data->thresholds.size(); i++)
{
charSegments[i] = filterByOuterMask(allContours[i], allHierarchy[i], charSegments[i]);
}
}
int bestFitScore = -1;
int bestFitIndex = -1;
for (uint i = 0; i < pipeline_data->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 = pipeline_data->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 (uint 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(pipeline_data->crop_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 (uint 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 (uint 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 (uint 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 (findOuterBoxMask) 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 (uint i = 0; i < allContours[winningIndex].size(); i++)
{
for (uint 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(pipeline_data->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 (uint c = 0; c < contoursSecondRound.size(); c++)
{
double area = contourArea(contoursSecondRound[c]);
if (area > largestArea)
{
biggestContourIndex = c;
largestArea = area;
}
}
if (biggestContourIndex != -1)
{
mask = Mat::zeros(pipeline_data->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(pipeline_data->thresholds[winningIndex].size(), CV_8U);
pipeline_data->thresholds[winningIndex].copyTo(debugImgMasked, mask);
debugImgs.push_back(mask);
debugImgs.push_back(pipeline_data->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(pipeline_data->thresholds[0].size(), CV_8U);
bitwise_not(fullMask, fullMask);
return fullMask;
}
Mat CharacterAnalysis::getCharacterMask()
{
Mat charMask = Mat::zeros(bestThreshold.size(), CV_8U);
for (uint 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
);
}
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 (uint 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 (uint i = 0; i < charRegions.size() - 1; i++)
{
for (uint 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 (uint i = 0; i < topLines.size(); i++)
{
float SCORING_MIN_THRESHOLD = 0.97;
float SCORING_MAX_THRESHOLD = 1.03;
int curScore = 0;
for (uint 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 (uint 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 (uint j = 0; j < contours.size(); j++)
includedIndices.push_back(false);
for (uint 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 (uint j = 0; j < contours.size(); j++)
includedIndices.push_back(false);
for (uint i = 0; i < contours.size(); i++)
{
if (goodIndices[i] == false)
continue;
int parentIndex = hierarchy[i][3];
if (parentIndex >= 0 && 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
{
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 (uint j = 0; j < contours.size(); j++)
includedIndices[j] = false;
vector parentIDs;
vector votes;
for (uint 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 (uint 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 (uint 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 (uint 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;
static float MAX_DISTANCE_PERCENT_FROM_LINES = 0.15;
vector includedIndices(contours.size());
for (uint 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(img.size(), CV_8U);
fillConvexPoly(outerMask, outerPolygon.data(), outerPolygon.size(), Scalar(255,255,255));
// For each contour, determine if enough of it is between the lines to qualify
for (uint i = 0; i < contours.size(); i++)
{
if (goodIndices[i] == false)
continue;
innerArea.setTo(Scalar(0,0,0));
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 (uint tempContourIdx = 0; tempContourIdx < tempContours.size(); tempContourIdx++)
{
areaBetweenLines += contourArea(tempContours[tempContourIdx]);
}
if (areaBetweenLines / totalArea < MIN_AREA_PERCENT_WITHIN_LINES)
{
// Not enough area is inside the lines.
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.
int highPointIndex = 0;
int highPointValue = 999999999;
int lowPointIndex = 0;
int lowPointValue = 0;
for (uint cidx = 0; cidx < contours[i].size(); cidx++)
{
if (contours[i][cidx].y < highPointValue)
{
highPointIndex = cidx;
highPointValue = contours[i][cidx].y;
}
if (contours[i][cidx].y > lowPointValue)
{
lowPointIndex = cidx;
lowPointValue = contours[i][cidx].y;
}
}
// Get the absolute distance from the top and bottom lines
Point closestTopPoint = topLine.closestPointOnSegmentTo(contours[i][highPointIndex]);
Point closestBottomPoint = bottomLine.closestPointOnSegmentTo(contours[i][lowPointIndex]);
float absTopDistance = distanceBetweenPoints(closestTopPoint, contours[i][highPointIndex]);
float absBottomDistance = distanceBetweenPoints(closestBottomPoint, contours[i][lowPointIndex]);
float maxDistance = lineHeight * MAX_DISTANCE_PERCENT_FROM_LINES;
if (absTopDistance < maxDistance && absBottomDistance < maxDistance)
{
includedIndices[i] = true;
}
}
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 (uint 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 (uint 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 (uint i = 0; i < bestContours.size(); i++)
{
if (bestCharSegments[i] == false)
continue;
for (uint 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;
}