diff --git a/src/openalpr/CMakeLists.txt b/src/openalpr/CMakeLists.txt index 9ca6c59..fc5fe8b 100644 --- a/src/openalpr/CMakeLists.txt +++ b/src/openalpr/CMakeLists.txt @@ -24,6 +24,7 @@ set(lpr_source_files colorfilter.cpp textdetection/characteranalysis.cpp textdetection/characteranalysis2l.cpp + textdetection/platemask.cpp pipeline_data.cpp trex.c cjson.c diff --git a/src/openalpr/characterregion.cpp b/src/openalpr/characterregion.cpp index 0df17db..6321ffb 100644 --- a/src/openalpr/characterregion.cpp +++ b/src/openalpr/characterregion.cpp @@ -38,7 +38,6 @@ CharacterRegion::CharacterRegion(PipelineData* pipeline_data) charAnalysis = new CharacterAnalysis(pipeline_data); charAnalysis->analyze(); pipeline_data->plate_inverted = charAnalysis->thresholdsInverted; - pipeline_data->plate_mask = charAnalysis->plateMask; if (this->debug && charAnalysis->linePolygon.size() > 0) { diff --git a/src/openalpr/licenseplatecandidate.cpp b/src/openalpr/licenseplatecandidate.cpp index b7a6d60..b89183a 100644 --- a/src/openalpr/licenseplatecandidate.cpp +++ b/src/openalpr/licenseplatecandidate.cpp @@ -56,7 +56,9 @@ void LicensePlateCandidate::recognize() { PlateLines plateLines(config); - plateLines.processImage(pipeline_data->plate_mask, &charRegion, 1.10); + if (pipeline_data->hasPlateBorder) + plateLines.processImage(pipeline_data->plateBorderMask, &charRegion, 1.10); + plateLines.processImage(pipeline_data->crop_gray, &charRegion, 0.9); PlateCorners cornerFinder(pipeline_data->crop_gray, &plateLines, &charRegion, config); diff --git a/src/openalpr/pipeline_data.h b/src/openalpr/pipeline_data.h index 6e21438..14cd045 100644 --- a/src/openalpr/pipeline_data.h +++ b/src/openalpr/pipeline_data.h @@ -23,7 +23,10 @@ class PipelineData cv::Rect regionOfInterest; cv::Mat crop_gray; - cv::Mat plate_mask; + + bool hasPlateBorder; + cv::Mat plateBorderMask; + std::vector thresholds; std::vector plate_corners; diff --git a/src/openalpr/textdetection/characteranalysis.cpp b/src/openalpr/textdetection/characteranalysis.cpp index af02c58..9999ff5 100644 --- a/src/openalpr/textdetection/characteranalysis.cpp +++ b/src/openalpr/textdetection/characteranalysis.cpp @@ -18,6 +18,7 @@ */ #include "characteranalysis.h" +#include "platemask.h" using namespace cv; using namespace std; @@ -27,7 +28,7 @@ CharacterAnalysis::CharacterAnalysis(PipelineData* pipeline_data) this->pipeline_data = pipeline_data; this->config = pipeline_data->config; - this->hasPlateMask = false; + this->isTwoLine = true; if (this->config->debugCharAnalysis) cout << "Starting CharacterAnalysis identification" << endl; @@ -92,9 +93,13 @@ void CharacterAnalysis::analyze() cout << " -- Character Analysis Filter Time: " << diffclock(startTime, endTime) << "ms." << endl; } - this->plateMask = findOuterBoxMask(); + PlateMask plateMask(pipeline_data); + plateMask.findOuterBoxMask(charSegments, allContours, allHierarchy); + + pipeline_data->hasPlateBorder = plateMask.hasPlateMask; + pipeline_data->plateBorderMask = plateMask.getMask(); - if (hasPlateMask) + if (plateMask.hasPlateMask) { // Filter out bad contours now that we have an outer box mask... for (uint i = 0; i < pipeline_data->thresholds.size(); i++) @@ -195,166 +200,7 @@ int CharacterAnalysis::getGoodIndicesCount(vector goodIndices) 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() { @@ -827,13 +673,15 @@ std::vector< bool > CharacterAnalysis::filterByOuterMask(vector< vector< Point > float MINIMUM_PERCENT_LEFT_AFTER_MASK = 0.1; float MINIMUM_PERCENT_OF_CHARS_INSIDE_PLATE_MASK = 0.6; - if (hasPlateMask == false) + if (this->pipeline_data->hasPlateBorder == false) return goodIndices; vector passingIndices; for (uint i = 0; i < goodIndices.size(); i++) passingIndices.push_back(false); + cv::Mat plateMask = pipeline_data->plateBorderMask; + Mat tempMaskedContour = Mat::zeros(plateMask.size(), CV_8U); Mat tempFullContour = Mat::zeros(plateMask.size(), CV_8U); diff --git a/src/openalpr/textdetection/characteranalysis.h b/src/openalpr/textdetection/characteranalysis.h index 7940f93..e539720 100644 --- a/src/openalpr/textdetection/characteranalysis.h +++ b/src/openalpr/textdetection/characteranalysis.h @@ -32,8 +32,6 @@ class CharacterAnalysis CharacterAnalysis(PipelineData* pipeline_data); virtual ~CharacterAnalysis(); - bool hasPlateMask; - cv::Mat plateMask; cv::Mat bestThreshold; std::vector > bestContours; @@ -52,6 +50,7 @@ class CharacterAnalysis LineSegment charBoxRight; bool thresholdsInverted; + bool isTwoLine; std::vector > > allContours; std::vector > allHierarchy; diff --git a/src/openalpr/textdetection/platemask.cpp b/src/openalpr/textdetection/platemask.cpp new file mode 100644 index 0000000..7828e64 --- /dev/null +++ b/src/openalpr/textdetection/platemask.cpp @@ -0,0 +1,201 @@ +/* + * Copyright (c) 2014 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 "platemask.h" + +using namespace std; +using namespace cv; + +PlateMask::PlateMask(PipelineData* pipeline_data) { + this->pipeline_data = pipeline_data; + this->hasPlateMask = false; + +} + + +PlateMask::~PlateMask() { +} + +cv::Mat PlateMask::getMask() { + return this->plateMask; +} + +void PlateMask::findOuterBoxMask(vector > charSegments, + vector > > allContours, + vector > allHierarchy) +{ + double min_parent_area = pipeline_data->config->templateHeightPx * pipeline_data->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 (pipeline_data->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 (pipeline_data->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 (pipeline_data->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(pipeline_data->config, "Winning outer box", dashboard); + } + + hasPlateMask = true; + this->plateMask = mask; + } + + hasPlateMask = false; + Mat fullMask = Mat::zeros(pipeline_data->thresholds[0].size(), CV_8U); + bitwise_not(fullMask, fullMask); + + this->plateMask = fullMask; +} diff --git a/src/openalpr/textdetection/platemask.h b/src/openalpr/textdetection/platemask.h new file mode 100644 index 0000000..ef57fc3 --- /dev/null +++ b/src/openalpr/textdetection/platemask.h @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2014 New Designs Unlimited, LLC + * Opensource Automated License Plate Recognition [http://www.openalpr.com] + * + * This file is part of OpenAlpr. + * + * OpenAlpr is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License + * version 3 as published by the Free Software Foundation + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . +*/ + +#ifndef OPENALPR_PLATEMASK_H +#define OPENALPR_PLATEMASK_H + +#include "opencv2/imgproc/imgproc.hpp" +#include "pipeline_data.h" + +class PlateMask { +public: + PlateMask(PipelineData* pipeline_data); + virtual ~PlateMask(); + + bool hasPlateMask; + + cv::Mat getMask(); + + void findOuterBoxMask(std::vector > charSegments, std::vector > > allContours, std::vector > allHierarchy); + +private: + + PipelineData* pipeline_data; + cv::Mat plateMask; + + +}; + +#endif /* OPENALPR_PLATEMASK_H */ +