我想展示一种快速而肮脏的方法来隔离板中的字母/数字,因为字符的实际分割不是问题。当这些是输入图像时:
这是你在我的算法结束时得到的:
因此,我在此答案中讨论的内容将为您提供一些想法,并帮助您摆脱当前分割过程结束时出现的伪影。请记住,这种方法应该只适用于这些类型的图像,如果您需要更强大的东西,您需要调整一些东西或想出全新的方法来做这些事情。
- 二值化的结果和你实现的差不多,所以我想出了一个方法,使用
findContours()来去除更小和更大的片段:
- 结果似乎好一点,但它破坏了盘子上字符的重要部分。但是,现在这并不是真正的问题,因为我们并不担心识别字符:我们只想隔离它们所在的区域。所以下一步是继续擦除段,更具体地说,是那些未与数字的同一 Y 轴对齐的段。在此切割过程中幸存下来的轮廓是:
从现在开始,您可以使用裁剪后的图像来执行您自己的技术并轻松分割车牌的字符。
这是 C++ 代码:
#include <iostream>
#include <vector>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/imgproc/imgproc_c.h>
/* The code has an outter loop where every iteration processes one of the four input images */
std::string files[] = { "plate1.jpg", "plate2.jpg", "plate3.jpg", "plate4.jpg" };
cv::Mat imgs[4];
for (int a = 0; a < 4; a++)
{
/* Load input image */
imgs[a] = cv::imread(files[a]);
if (imgs[a].empty())
{
std::cout << "!!! Failed to open image: " << imgs[a] << std::endl;
return -1;
}
/* Convert to grayscale */
cv::Mat gray;
cv::cvtColor(imgs[a], gray, cv::COLOR_BGR2GRAY);
/* Histogram equalization improves the contrast between dark/bright areas */
cv::Mat equalized;
cv::equalizeHist(gray, equalized);
cv::imwrite(std::string("eq_" + std::to_string(a) + ".jpg"), equalized);
cv::imshow("Hist. Eq.", equalized);
/* Bilateral filter helps to improve the segmentation process */
cv::Mat blur;
cv::bilateralFilter(equalized, blur, 9, 75, 75);
cv::imwrite(std::string("filter_" + std::to_string(a) + ".jpg"), blur);
cv::imshow("Filter", blur);
/* Threshold to binarize the image */
cv::Mat thres;
cv::adaptiveThreshold(blur, thres, 255, cv::ADAPTIVE_THRESH_GAUSSIAN_C, cv::THRESH_BINARY, 15, 2); //15, 2
cv::imwrite(std::string("thres_" + std::to_string(a) + ".jpg"), thres);
cv::imshow("Threshold", thres);
/* Remove small segments and the extremelly large ones as well */
std::vector<std::vector<cv::Point> > contours;
cv::findContours(thres, contours, cv::RETR_LIST, cv::CHAIN_APPROX_SIMPLE);
double min_area = 50;
double max_area = 2000;
std::vector<std::vector<cv::Point> > good_contours;
for (size_t i = 0; i < contours.size(); i++)
{
double area = cv::contourArea(contours[i]);
if (area > min_area && area < max_area)
good_contours.push_back(contours[i]);
}
cv::Mat segments(gray.size(), CV_8U, cv::Scalar(255));
cv::drawContours(segments, good_contours, -1, cv::Scalar(0), cv::FILLED, 4);
cv::imwrite(std::string("segments_" + std::to_string(a) + ".jpg"), segments);
cv::imshow("Segments", segments);
/* Examine the segments that survived the previous lame filtering process
* to figure out the top and bottom heights of the largest segments.
* This info will be used to remove segments that are not aligned with
* the letters/numbers of the plate.
* This technique is super flawed for other types of input images.
*/
// Figure out the average of the top/bottom heights of the largest segments
int min_average_y = 0, max_average_y = 0, count = 0;
for (size_t i = 0; i < good_contours.size(); i++)
{
std::vector<cv::Point> c = good_contours[i];
double area = cv::contourArea(c);
if (area > 200)
{
int min_y = segments.rows, max_y = 0;
for (size_t j = 0; j < c.size(); j++)
{
if (c[j].y < min_y)
min_y = c[j].y;
if (c[j].y > max_y)
max_y = c[j].y;
}
min_average_y += min_y;
max_average_y += max_y;
count++;
}
}
min_average_y /= count;
max_average_y /= count;
//std::cout << "Average min: " << min_average_y << " max: " << max_average_y << std::endl;
// Create a new vector of contours with just the ones that fall within the min/max Y
std::vector<std::vector<cv::Point> > final_contours;
for (size_t i = 0; i < good_contours.size(); i++)
{
std::vector<cv::Point> c = good_contours[i];
int min_y = segments.rows, max_y = 0;
for (size_t j = 0; j < c.size(); j++)
{
if (c[j].y < min_y)
min_y = c[j].y;
if (c[j].y > max_y)
max_y = c[j].y;
}
// 5 is to add a little tolerance from the average Y coordinate
if (min_y >= (min_average_y-5) && (max_y <= max_average_y+5))
final_contours.push_back(c);
}
cv::Mat final(gray.size(), CV_8U, cv::Scalar(255));
cv::drawContours(final, final_contours, -1, cv::Scalar(0), cv::FILLED, 4);
cv::imwrite(std::string("final_" + std::to_string(a) + ".jpg"), final);
cv::imshow("Final", final);
// Create a single vector with all the points that make the segments
std::vector<cv::Point> points;
for (size_t x = 0; x < final_contours.size(); x++)
{
std::vector<cv::Point> c = final_contours[x];
for (size_t y = 0; y < c.size(); y++)
points.push_back(c[y]);
}
// Compute a single bounding box for the points
cv::RotatedRect box = cv::minAreaRect(cv::Mat(points));
cv::Rect roi;
roi.x = box.center.x - (box.size.width / 2);
roi.y = box.center.y - (box.size.height / 2);
roi.width = box.size.width;
roi.height = box.size.height;
// Draw the box at on equalized image
cv::Point2f vertices[4];
box.points(vertices);
for(int i = 0; i < 4; ++i)
cv::line(imgs[a], vertices[i], vertices[(i + 1) % 4], cv::Scalar(255, 0, 0), 1, CV_AA);
cv::imwrite(std::string("box_" + std::to_string(a) + ".jpg"), imgs[a]);
cv::imshow("Box", imgs[a]);
// Crop the equalized image with the area defined by the ROI
cv::Mat crop = equalized(roi);
cv::imwrite(std::string("crop_" + std::to_string(a) + ".jpg"), crop);
cv::imshow("crop", crop);
/* The cropped image should contain only the plate's letters and numbers.
* From here on you can use your own techniques to segment the characters properly.
*/
cv::waitKey(0);
}
有关使用 OpenCV 进行车牌识别的更完整、更强大的方法,请查看 Mastering OpenCV with Practical Computer Vision Projects,第 5 章。 Source code is available on Github!