mirror of
https://github.com/kerberos-io/openalpr-base.git
synced 2025-10-16 01:50:36 +08:00
Improved method for finding lines of text on multiline plates.
Uses histograms (parallel to first horizontal line) for second line rather than blob matching.
This commit is contained in:
@@ -18,6 +18,8 @@
|
||||
*/
|
||||
|
||||
#include <opencv2/core/core.hpp>
|
||||
#include <opencv2/core/core_c.h>
|
||||
#include <opencv2/imgproc/imgproc.hpp>
|
||||
|
||||
#include "linefinder.h"
|
||||
#include "utility.h"
|
||||
@@ -54,40 +56,237 @@ namespace alpr
|
||||
charPoints.push_back( CharPointInfo(contours.contours[i], i) );
|
||||
}
|
||||
|
||||
vector<Point> bestLine = getBestLine(contours, charPoints);
|
||||
|
||||
vector<Point> bestCharArea = getBestLine(contours, charPoints);
|
||||
vector<Point> bestLine = extendToEdges(Size(contours.width, contours.height), bestCharArea);
|
||||
|
||||
if (bestLine.size() > 0)
|
||||
linesFound.push_back(bestLine);
|
||||
|
||||
if (pipeline_data->isMultiline)
|
||||
{
|
||||
// we have a two-line plate. Find the next best line, removing the tops/bottoms from before.
|
||||
// Create a mask from the bestLine area, and remove all contours with tops that fall inside of it.
|
||||
|
||||
vector<CharPointInfo> remainingPoints;
|
||||
for (unsigned int i = 0; i < charPoints.size(); i++)
|
||||
vector<Point> next_best_line = findNextBestLine(Size(contours.width, contours.height), bestCharArea);
|
||||
|
||||
if (next_best_line.size() > 0)
|
||||
{
|
||||
Mat mask = Mat::zeros(Size(contours.width, contours.height), CV_8U);
|
||||
fillConvexPoly(mask, bestLine.data(), bestLine.size(), Scalar(255,255,255));
|
||||
|
||||
float percentInside = getContourAreaPercentInsideMask(mask, contours.contours, contours.hierarchy, charPoints[i].contourIndex);
|
||||
|
||||
if (percentInside < MIN_AREA_TO_IGNORE)
|
||||
{
|
||||
remainingPoints.push_back(charPoints[i]);
|
||||
}
|
||||
vector<Point> next_best_line_extended = extendToEdges(Size(contours.width, contours.height), next_best_line);
|
||||
linesFound.push_back(next_best_line_extended);
|
||||
}
|
||||
|
||||
|
||||
vector<Point> nextBestLine = getBestLine(contours, remainingPoints);
|
||||
|
||||
if (nextBestLine.size() > 0)
|
||||
linesFound.push_back(nextBestLine);
|
||||
}
|
||||
|
||||
|
||||
return linesFound;
|
||||
}
|
||||
|
||||
std::vector<cv::Point> LineFinder::calculateCroppedRegionForHistogram(cv::Size imageSize, std::vector<cv::Point> charArea) {
|
||||
|
||||
LineSegment topLine(charArea[0], charArea[1]);
|
||||
|
||||
|
||||
LineSegment new_top;
|
||||
LineSegment new_bottom;
|
||||
if (topLine.angle < 0)
|
||||
{
|
||||
float distance_from_top = topLine.p1.y;
|
||||
new_top = topLine.getParallelLine(distance_from_top);
|
||||
|
||||
float distance_from_bottom = imageSize.height - topLine.p2.y;
|
||||
new_bottom = topLine.getParallelLine(-1 * distance_from_bottom);
|
||||
}
|
||||
else
|
||||
{
|
||||
float distance_from_top = topLine.p2.y;
|
||||
new_top = topLine.getParallelLine(distance_from_top);
|
||||
|
||||
float distance_from_bottom = imageSize.height - topLine.p1.y;
|
||||
new_bottom = topLine.getParallelLine(-1 * distance_from_bottom);
|
||||
}
|
||||
|
||||
|
||||
vector<Point> points;
|
||||
points.push_back(new_top.p1);
|
||||
points.push_back(new_top.p2);
|
||||
points.push_back(new_bottom.p2);
|
||||
points.push_back(new_bottom.p1);
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
|
||||
std::vector<cv::Point> LineFinder::findNextBestLine(cv::Size imageSize, std::vector<cv::Point> bestLine) {
|
||||
|
||||
// Pull out a crop of the plate around the line we know about,
|
||||
// then do a horizontal histogram on all the thresholds. Find the other line based on that histogram
|
||||
|
||||
vector<Point> histogramArea = calculateCroppedRegionForHistogram(imageSize, bestLine);
|
||||
|
||||
Size cropped_quad_size(distanceBetweenPoints(histogramArea[0], histogramArea[1]), distanceBetweenPoints(histogramArea[0], histogramArea[3]));
|
||||
|
||||
Mat mask = Mat::zeros(cropped_quad_size, CV_8U);
|
||||
bitwise_not(mask, mask);
|
||||
|
||||
vector<Point2f> inputQuad;
|
||||
for (int i = 0; i < histogramArea.size(); i++)
|
||||
inputQuad.push_back(histogramArea[i]);
|
||||
vector<Point2f> outputQuad;
|
||||
outputQuad.push_back(Point2f(0,0));
|
||||
outputQuad.push_back(Point2f(cropped_quad_size.width,0));
|
||||
outputQuad.push_back(Point2f(cropped_quad_size.width,cropped_quad_size.height));
|
||||
outputQuad.push_back(Point2f(0,cropped_quad_size.height));
|
||||
|
||||
int pxLeniency = 2;
|
||||
|
||||
Mat trans_matrix = getPerspectiveTransform(inputQuad, outputQuad);
|
||||
vector<Point2f> orig_best_line;
|
||||
for (int i = 0; i < bestLine.size(); i++)
|
||||
orig_best_line.push_back(bestLine[i]);
|
||||
vector<Point2f> transformed_best_line;
|
||||
perspectiveTransform(orig_best_line, transformed_best_line, trans_matrix);
|
||||
|
||||
int transformed_best_line_start = round(transformed_best_line[0].y);
|
||||
int transformed_best_line_end = round(transformed_best_line[3].y);
|
||||
int transformed_best_line_width = transformed_best_line_end - transformed_best_line_start;
|
||||
int transformed_best_line_variance = (int) ((float) transformed_best_line_width) * 0.25;
|
||||
|
||||
|
||||
|
||||
float lowest_width_diff = 99999999999;
|
||||
int best_secondline_index = -1;
|
||||
int best_secondline_threshold = -1;
|
||||
int best_secondline_top_pixel_offset_from_bestline_top = 0;
|
||||
int best_secondline_bottom_pixel_offset_from_bestline_top = 0;
|
||||
|
||||
for (unsigned int i = 0; i < pipeline_data->thresholds.size(); i++)
|
||||
{
|
||||
Mat warpedImage = Mat::zeros(cropped_quad_size, CV_8U);
|
||||
warpPerspective(pipeline_data->thresholds[i], warpedImage,
|
||||
trans_matrix,
|
||||
cropped_quad_size);
|
||||
|
||||
|
||||
|
||||
HistogramHorizontal histogram(warpedImage, mask);
|
||||
|
||||
vector<pair<int, int> > histogram_hits = histogram.get1DHits(pxLeniency);
|
||||
|
||||
// First find the histogram blob for the "best line" that we already found
|
||||
// Do this by comparing the "transformed_best_line" points to the histogram_hits
|
||||
|
||||
|
||||
int best_line_index = -1;
|
||||
for (unsigned int hitidx = 0; hitidx < histogram_hits.size(); hitidx++)
|
||||
{
|
||||
pair<int,int> hit = histogram_hits[hitidx];
|
||||
|
||||
if ((hit.first >= transformed_best_line_start - transformed_best_line_variance) &&
|
||||
(hit.first <= transformed_best_line_start + transformed_best_line_variance) &&
|
||||
(hit.second >= transformed_best_line_end - transformed_best_line_variance) &&
|
||||
(hit.second <= transformed_best_line_end + transformed_best_line_variance))
|
||||
{
|
||||
best_line_index = hitidx;
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (best_line_index < 0) // Best line not found on this threshold...
|
||||
{
|
||||
if (pipeline_data->config->debugCharAnalysis)
|
||||
cout << "Could not find best line for multiline plate" << endl;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pipeline_data->config->debugCharAnalysis)
|
||||
cout << "Found a multiline best line " << histogram_hits[best_line_index].first << " -> " << histogram_hits[best_line_index].second << endl;
|
||||
|
||||
// Now look at all other hits and find one that is above or below our best line and has the correct text height ratio
|
||||
// I'm either looking for a bigger line above or a smaller line below (or vice versa, or two same sized lines depending on the plate config)
|
||||
|
||||
// Assume maximum of two lines per plate for now
|
||||
|
||||
// TODO: Use char_whitespace_between_lines_mm to score lines better
|
||||
|
||||
//float best_line_width = histogram_hits[best_line_index].second - histogram_hits[best_line_index].first;
|
||||
if (pipeline_data->config->debugCharAnalysis)
|
||||
cout << "Ideal calculation: " << pipeline_data->config->charHeightMM[0] << " : " << pipeline_data->config->charHeightMM[1] << " - " << transformed_best_line_width << endl;
|
||||
|
||||
float ideal_above_size = (pipeline_data->config->charHeightMM[0] / pipeline_data->config->charHeightMM[1]) * transformed_best_line_width;
|
||||
float ideal_below_size = (pipeline_data->config->charHeightMM[1] / pipeline_data->config->charHeightMM[0]) * transformed_best_line_width;
|
||||
|
||||
float max_deviation_percent = 0.30;
|
||||
|
||||
|
||||
for (unsigned int hitidx = 0; hitidx < histogram_hits.size(); hitidx++)
|
||||
{
|
||||
if (hitidx == best_line_index)
|
||||
continue;
|
||||
|
||||
float hit_width = histogram_hits[hitidx].second - histogram_hits[hitidx].first;
|
||||
|
||||
float ideal_width;
|
||||
if (histogram_hits[hitidx].second <= histogram_hits[best_line_index].first)
|
||||
ideal_width = ideal_above_size;
|
||||
else if (histogram_hits[hitidx].first >= histogram_hits[best_line_index].second)
|
||||
ideal_width = ideal_below_size;
|
||||
else
|
||||
assert(false);
|
||||
|
||||
if (pipeline_data->config->debugCharAnalysis)
|
||||
cout << "Hit Width: " << hit_width << " -- ideal width: " << ideal_width << endl;
|
||||
|
||||
if ((hit_width >= ideal_width * (1-max_deviation_percent)) &&
|
||||
(hit_width <= ideal_width * (1+max_deviation_percent)))
|
||||
{
|
||||
float diff_from_ideal = hit_width - ideal_width;
|
||||
if (diff_from_ideal < lowest_width_diff)
|
||||
{
|
||||
lowest_width_diff = diff_from_ideal;
|
||||
best_secondline_index = hitidx;
|
||||
best_secondline_threshold = i;
|
||||
best_secondline_top_pixel_offset_from_bestline_top = (histogram_hits[best_line_index].first - histogram_hits[hitidx].first);
|
||||
best_secondline_bottom_pixel_offset_from_bestline_top = (histogram_hits[best_line_index].first - histogram_hits[hitidx].second);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
Mat debugImg(pipeline_data->thresholds[1].size(), pipeline_data->thresholds[1].type());
|
||||
pipeline_data->thresholds[1].copyTo(debugImg);
|
||||
cvtColor(debugImg, debugImg, CV_GRAY2BGR);
|
||||
|
||||
LineSegment orig_top_line(bestLine[0], bestLine[1]);
|
||||
LineSegment secondline_top = orig_top_line.getParallelLine(best_secondline_top_pixel_offset_from_bestline_top + 1);
|
||||
LineSegment secondline_bottom = orig_top_line.getParallelLine(best_secondline_bottom_pixel_offset_from_bestline_top - 1);
|
||||
|
||||
line(debugImg, orig_top_line.p1, orig_top_line.p2, Scalar(0,0,255), 2);
|
||||
line(debugImg, secondline_top.p1, secondline_top.p2, Scalar(255,255,0), 2);
|
||||
line(debugImg, secondline_bottom.p1, secondline_bottom.p2, Scalar(0,255,0), 2);
|
||||
|
||||
if (pipeline_data->config->debugCharAnalysis)
|
||||
{
|
||||
cout << "Multiline = " << secondline_top.str() << " -- " << secondline_bottom.str() << endl;
|
||||
cout << "Multiline winner is: " << best_secondline_index << " on threshold " << best_secondline_threshold << endl;
|
||||
}
|
||||
|
||||
vector<cv::Point> response;
|
||||
|
||||
if (best_secondline_index >= 0)
|
||||
{
|
||||
response.push_back(secondline_top.p1);
|
||||
response.push_back(secondline_top.p2);
|
||||
response.push_back(secondline_bottom.p1);
|
||||
response.push_back(secondline_bottom.p2);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
// 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<Point> LineFinder::getBestLine(const TextContours contours, vector<CharPointInfo> charPoints)
|
||||
@@ -134,13 +333,6 @@ namespace alpr
|
||||
LineSegment bottom(charPoints[leftCPIndex].bottom, charPoints[rightCPIndex].bottom);
|
||||
|
||||
|
||||
// Only allow lines that have a sane angle
|
||||
// if (abs(top.angle) <= pipeline_data->config->maxPlateAngleDegrees &&
|
||||
// abs(bottom.angle) <= pipeline_data->config->maxPlateAngleDegrees)
|
||||
// {
|
||||
// topLines.push_back(top);
|
||||
// bottomLines.push_back(bottom);
|
||||
// }
|
||||
|
||||
LineSegment parallelBot = top.getParallelLine(medianCharHeight * -1);
|
||||
LineSegment parallelTop = bottom.getParallelLine(medianCharHeight);
|
||||
@@ -189,8 +381,6 @@ namespace alpr
|
||||
curScore++;
|
||||
}
|
||||
|
||||
//cout << "Slope: " << topslope << " yPos: " << topYPos << endl;
|
||||
//drawAndWait(&tempImg);
|
||||
}
|
||||
|
||||
// Tie goes to the one with longer line segments
|
||||
@@ -221,20 +411,38 @@ namespace alpr
|
||||
displayImage(pipeline_data->config, "Winning lines", tempImg);
|
||||
}
|
||||
|
||||
Point topLeft = Point(0, topLines[bestScoreIndex].getPointAt(0) );
|
||||
Point topRight = Point(contours.width, topLines[bestScoreIndex].getPointAt(contours.width));
|
||||
Point bottomRight = Point(contours.width, bottomLines[bestScoreIndex].getPointAt(contours.width));
|
||||
Point bottomLeft = Point(0, bottomLines[bestScoreIndex].getPointAt(0));
|
||||
|
||||
bestStripe.push_back(topLeft);
|
||||
bestStripe.push_back(topRight);
|
||||
bestStripe.push_back(bottomRight);
|
||||
bestStripe.push_back(bottomLeft);
|
||||
|
||||
bestStripe.push_back(topLines[bestScoreIndex].p1);
|
||||
bestStripe.push_back(topLines[bestScoreIndex].p2);
|
||||
bestStripe.push_back(bottomLines[bestScoreIndex].p2);
|
||||
bestStripe.push_back(bottomLines[bestScoreIndex].p1);
|
||||
|
||||
return bestStripe;
|
||||
}
|
||||
|
||||
std::vector<cv::Point> LineFinder::extendToEdges(cv::Size imageSize, std::vector<cv::Point> charArea) {
|
||||
|
||||
vector<Point> extended;
|
||||
|
||||
|
||||
if (charArea.size() < 4)
|
||||
return extended;
|
||||
|
||||
LineSegment top(charArea[0], charArea[1]);
|
||||
LineSegment bottom(charArea[3], charArea[2]);
|
||||
|
||||
Point topLeft = Point(0, top.getPointAt(0) );
|
||||
Point topRight = Point(imageSize.width, top.getPointAt(imageSize.width));
|
||||
Point bottomRight = Point(imageSize.width, bottom.getPointAt(imageSize.width));
|
||||
Point bottomLeft = Point(0, bottom.getPointAt(0));
|
||||
|
||||
extended.push_back(topLeft);
|
||||
extended.push_back(topRight);
|
||||
extended.push_back(bottomRight);
|
||||
extended.push_back(bottomLeft);
|
||||
|
||||
return extended;
|
||||
}
|
||||
|
||||
CharPointInfo::CharPointInfo(vector<Point> contour, int index) {
|
||||
|
||||
|
||||
@@ -254,5 +462,5 @@ namespace alpr
|
||||
this->bottom = Point(x,y);
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
@@ -26,6 +26,7 @@
|
||||
#include "textcontours.h"
|
||||
#include "textline.h"
|
||||
#include "pipeline_data.h"
|
||||
#include "segmentation/histogramhorizontal.h"
|
||||
|
||||
namespace alpr
|
||||
{
|
||||
@@ -51,9 +52,18 @@ namespace alpr
|
||||
private:
|
||||
PipelineData* pipeline_data;
|
||||
|
||||
// Returns 4 points, counter clockwise that bound the detected character area
|
||||
std::vector<cv::Point> getBestLine(const TextContours contours, std::vector<CharPointInfo> charPoints);
|
||||
};
|
||||
|
||||
// Extends the top and bottom lines to the left and right edge of the image. Returns 4 points, counter clockwise.
|
||||
std::vector<cv::Point> extendToEdges(cv::Size imageSize, std::vector<cv::Point> charArea);
|
||||
|
||||
|
||||
std::vector<cv::Point> findNextBestLine(cv::Size imageSize, std::vector<cv::Point> bestLine);
|
||||
// Gets a polygon that covers the entire area we wish to run a horizontal histogram over
|
||||
// This needs to be done to handle rotation/skew
|
||||
std::vector<cv::Point> calculateCroppedRegionForHistogram(cv::Size imageSize, std::vector<cv::Point> charArea);
|
||||
};
|
||||
}
|
||||
|
||||
#endif /* OPENALPR_LINEFINDER_H */
|
||||
|
Reference in New Issue
Block a user