chars_segment.h用于从已经通过SVM判别得到的车牌区域中将车牌的字符分割开,用于下一步的ANN字符识别。
namespace easypr {
class CCharsSegment {
public:
CCharsSegment();
// using ostu algotithm the segment chars in plate字符分割,步骤为:灰度化,阈值,找轮廓,外接矩形,从左到右排序,找城市字符,找汉字字符。详情见文末3,原理:https://blog.csdn.net/qq_30815237/article/details/88763027
int charsSegment(Mat input, std::vector<Mat>& resultVec, Color color = BLUE);
//! using methods to segment chars in plate,charsSegmentUsingOSTU与charsSegment相比增添了一个去除铆钉的操作
//charsSegmentUsingMSER使用MSER方法,代码较长,日后再详述。
int charsSegmentUsingOSTU(Mat input, std::vector<Mat>& resultVec, std::vector<Mat>& grayChars, Color color = BLUE);
int charsSegmentUsingMSER(Mat input, vector<Mat>& resultVec, vector<Mat>& grayChars, Color color = BLUE);
//大律阈值二值化,去铆钉clearLiuDing函数,调用ProjectedHistogram函数,都在core_func.cpp文件中,参见文末4,5,投影原理:参见 https://blog.csdn.net/qq_30815237/article/details/88665822
int projectSegment(const Mat& input, Color color, vector<int>& out_indexs);
//参见文末1
bool verifyCharSizes(Mat r);
// find the best chinese binaranzation method,之后详述,在CharsIdentify头文件中
void judgeChinese(Mat in, Mat& out, Color plateType);
void judgeChineseGray(Mat in, Mat& out, Color plateType);//还未定义,函数体:out=in
//首先进行仿射变换,将字符统一大小,并归一化到中间,并resize为 20*20,如下图所示:
转化为
Mat preprocessChar(Mat in);
//找到汉字字符,传入参数是上一步操作中找的的城市字符的矩形框,返回汉字字符的矩形框
Rect GetChineseRect(const Rect rectSpe);
//找到代表城市的字符, like "苏A" 的A,目的是为了下一步找到汉字字符,一般为特殊字符左移字符宽度的1.15倍;参见文末2
int GetSpecificRect(const std::vector<Rect>& vecRect);
// 从城市字符开始,从左到右取前5个字符,排除右边边界会出现误判的 I,得到:
int RebuildRect(const std::vector<Rect>& vecRect, std::vector<Rect>& outRect,int specIndex);
//没有这个函数的定义,这个函数可能是想实现字符矩形框的排列,但实际使用sort函数就可以实现,不需要使用该函数
int SortRect(const std::vector<Rect>& vecRect, std::vector<Rect>& out);
inline void setLiuDingSize(int param) { m_LiuDingSize = param; }
inline void setColorThreshold(int param) { m_ColorThreshold = param; }
inline void setBluePercent(float param) { m_BluePercent = param; }
inline float getBluePercent() const { return m_BluePercent; }
inline void setWhitePercent(float param) { m_WhitePercent = param; }
inline float getWhitePercent() const { return m_WhitePercent; }
static const int DEFAULT_DEBUG = 1;
static const int CHAR_SIZE = 20;
static const int HORIZONTAL = 1;
static const int VERTICAL = 0;
static const int DEFAULT_LIUDING_SIZE = 7;
static const int DEFAULT_MAT_WIDTH = 136;
static const int DEFAULT_COLORTHRESHOLD = 150;
inline void setDebug(int param) { m_debug = param; }
inline int getDebug() { return m_debug; }
private:
int m_LiuDingSize;
int m_theMatWidth;
int m_ColorThreshold;
float m_BluePercent;
float m_WhitePercent;
int m_debug;
};
}
1、判别分割得到的字符尺寸大小,从面积,长宽比和字符的宽度高度等角度进行字符校验。如果不满足尺寸,则可能不是字符或不是完整的字符(汉字),返回flase.
bool CCharsSegment::verifyCharSizes(Mat r) {
// Char sizes 45x90
float aspect = 45.0f / 90.0f; //宽高比
float charAspect = (float)r.cols / (float)r.rows;
float error = 0.7f;
float minHeight = 10.f;//车牌区域Mat的尺寸
float maxHeight = 35.f;
// We have a different aspect ratio for number 1, and it can be ~0.2字符“1”比较特殊
float minAspect = 0.05f;
float maxAspect = aspect + aspect * error;//最大最小范围
// area of pixels
int area = cv::countNonZero(r);//对二值化图像执行countNonZero。可得到非零像素点数(字符像素).
int bbArea = r.cols * r.rows;
int percPixels = area / bbArea;//百分比
if (percPixels <= 1 && charAspect > minAspect && charAspect < maxAspect &&
r.rows >= minHeight && r.rows < maxHeight)
return true;
else
return false;
}
2、寻找汉字字符后面的一个字符,根据它的位置先验信息(在整个车牌的1/7和2/7范围内)
int CCharsSegment::GetSpecificRect(const vector<Rect>& vecRect) {
vector<int> xpositions;
int maxHeight = 0;
int maxWidth = 0;
for (size_t i = 0; i < vecRect.size(); i++) {
xpositions.push_back(vecRect[i].x);//将矩形框的左上角坐标存放在一起
if (vecRect[i].height > maxHeight) {
maxHeight = vecRect[i].height;
}
if (vecRect[i].width > maxWidth) {
maxWidth = vecRect[i].width;
}
}
int specIndex = 0;
for (size_t i = 0; i < vecRect.size(); i++) {
Rect mr = vecRect[i];
int midx = mr.x + mr.width / 2; //矩形框的中心点x坐标
// find the specific character汉字后面紧跟着的字符,位置在整个车牌宽的 1/7 and 2/7之间
if ((mr.width > maxWidth * 0.6 || mr.height > maxHeight * 0.6) &&
(midx < int(m_theMatWidth / kPlateMaxSymbolCount) * kSymbolIndex &&
midx > int(m_theMatWidth / kPlateMaxSymbolCount) * (kSymbolIndex - 1))) {
specIndex = i;
}
}
return specIndex;//返回特殊字符的索引
}
3、
int CCharsSegment::charsSegment(Mat input, vector<Mat>& resultVec, Color color) {
if (!input.data) return 0x01;
Color plateType = color;
Mat input_grey;
cvtColor(input, input_grey, CV_BGR2GRAY);//转为灰度图
Mat img_threshold;
img_threshold = input_grey.clone();
spatial_ostu(img_threshold, 8, 2, plateType);//自定义的空间大律法阈值
// remove liuding and hor lines
// also judge weather is plate use jump count
if (!clearLiuDing(img_threshold)) return 0x02;
Mat img_contours;
img_threshold.copyTo(img_contours);
vector<vector<Point> > contours;//找轮廓
findContours(img_contours,
contours, // a vector of contours
CV_RETR_EXTERNAL, // retrieve the external contours
CV_CHAIN_APPROX_NONE); // all pixels of each contours
vector<vector<Point> >::iterator itc = contours.begin();
vector<Rect> vecRect;
while (itc != contours.end()) {
Rect mr = boundingRect(Mat(*itc));
Mat auxRoi(img_threshold, mr);
if (verifyCharSizes(auxRoi)) vecRect.push_back(mr);//求单个字符轮廓的外接矩形,根据大小判断
++itc;
}
if (vecRect.size() == 0) return 0x03;
vector<Rect> sortedRect(vecRect);
std::sort(sortedRect.begin(), sortedRect.end(),
[](const Rect& r1, const Rect& r2) { return r1.x < r2.x; });//自定义排序方法,按x坐标升序排列,就是按车牌从左到右排列字符
size_t specIndex = 0;
specIndex = GetSpecificRect(sortedRect);//得到汉字字符后面的字符,主要是为了把汉字字符分开
Rect chineseRect;
if (specIndex < sortedRect.size())
chineseRect = GetChineseRect(sortedRect[specIndex]);//得到汉字字符
else
return 0x04;
vector<Rect> newSortedRect;
newSortedRect.push_back(chineseRect);
RebuildRect(sortedRect, newSortedRect, specIndex);//除汉字字符外的字符按顺序排列存放在newSortedRect中
if (newSortedRect.size() == 0) return 0x05;
bool useSlideWindow = true;
bool useAdapThreshold = true;
for (size_t i = 0; i < newSortedRect.size(); i++) {
Rect mr = newSortedRect[i];
//使用拷贝构造函数Mat(constMat& m, const Rect& roi ),矩形roi指定了兴趣区
Mat auxRoi(input_grey, mr);
Mat newRoi;
if (i == 0) {//i=0表示汉字字符,使用滑动框
if (useSlideWindow) {
float slideLengthRatio = 0.1f;
//float slideLengthRatio = CParams::instance()->getParam1f();
//改进中文字符的识别,在识别中文时,增加一个小型的滑动窗口slideChineseWindow,以此弥补通过省份字符直接查找中文字符时的定位不精等现象;
if (!slideChineseWindow(input_grey, mr, newRoi, plateType, slideLengthRatio, useAdapThreshold))
judgeChinese(auxRoi, newRoi, plateType);
}
else
judgeChinese(auxRoi, newRoi, plateType);
}
else {
if (BLUE == plateType) {
threshold(auxRoi, newRoi, 0, 255, CV_THRESH_BINARY + CV_THRESH_OTSU);
}
else if (YELLOW == plateType) {
threshold(auxRoi, newRoi, 0, 255, CV_THRESH_BINARY_INV + CV_THRESH_OTSU);
}
else if (WHITE == plateType) {
threshold(auxRoi, newRoi, 0, 255, CV_THRESH_OTSU + CV_THRESH_BINARY_INV);
}
else {
threshold(auxRoi, newRoi, 0, 255, CV_THRESH_OTSU + CV_THRESH_BINARY);
}
newRoi = preprocessChar(newRoi);
}
if (i == 0) {
imshow("newRoi", newRoi);
waitKey(0);
destroyWindow("newRoi");
}
}
resultVec.push_back(newRoi);//最终分割完成的结果
}
return 0;
}
4、水平投影
Mat ProjectedHistogram(Mat img, int t, int threshold) {//参数t为0,对图像进行水平投影
int sz = (t) ? img.rows : img.cols;
Mat mhist = Mat::zeros(1, sz, CV_32F);
for (int j = 0; j < sz; j++) {
Mat data = (t) ? img.row(j) : img.col(j);
mhist.at<float>(j) = countOfBigValue(data, threshold);//统计像素个数
}
double min, max;
minMaxLoc(mhist, &min, &max);//归一化
if (max > 0)
mhist.convertTo(mhist, -1, 1.0f / max, 0);
return mhist;
}
5、去处铆钉
处理车牌上铆钉和水平线,因为铆钉和字符连在一起,会影响后面识别的精度。此处有一个特别的乌龙事件,就是铆钉的读音应该是maoding,不读liuding;基本思路是:依次扫描各行,判断跳变的次数,字符所在行跳变次数会很多,但是铆钉所在行则偏少,将每行中跳变次数少于7的行判定为铆钉,清除影响。返回false表示这个Mat不是车牌。
bool clearLiuDing(Mat &img) {
std::vector<float> fJump;
int whiteCount = 0;
const int x = 7;//跳变次数小于7的是铆钉
Mat jump = Mat::zeros(1, img.rows, CV_32F);
for (int i = 0; i < img.rows; i++) {
int jumpCount = 0;
for (int j = 0; j < img.cols - 1; j++) {
if (img.at<char>(i, j) != img.at<char>(i, j + 1)) jumpCount++;//统计相邻像素的跳变次数
if (img.at<uchar>(i, j) == 255) {
whiteCount++;
}
}
jump.at<float>(i) = (float) jumpCount;//jump存放每行像素的跳变次数
}
int iCount = 0;
for (int i = 0; i < img.rows; i++) {
fJump.push_back(jump.at<float>(i));
if (jump.at<float>(i) >= 16 && jump.at<float>(i) <= 45) {
// jump condition
iCount++;
}
}
// if not is not plate
if (iCount * 1.0 / img.rows <= 0.40) {//跳变次数满足条件的行数如果少于40%,可能这个区域都不是车牌
return false;
}
if (whiteCount * 1.0 / (img.rows * img.cols) < 0.15 ||
whiteCount * 1.0 / (img.rows * img.cols) > 0.50) {//白色像素数量不满足条件的也不是车牌
return false;
}
for (int i = 0; i < img.rows; i++) {
if (jump.at<float>(i) <= x) {
for (int j = 0; j < img.cols; j++) {
img.at<char>(i, j) = 0;
}
}
}
return true;
}
6、 spatial_ostu
空间otsu算法,主要用于处理光照不均匀的图像,对于当前图像,分块分别进行二值化;主要是为了应对左右光照不一致的情况,譬如车牌的左边部分光照比右边部分要强烈的多,通过图像分块处理,提高otsu分割的鲁棒性;
void spatial_ostu(InputArray _src, int grid_x, int grid_y, Color type) {
Mat src = _src.getMat();
int width = src.cols / grid_x;//分成多少块
int height = src.rows / grid_y;
// iterate through grid
for (int i = 0; i < grid_y; i++) {
for (int j = 0; j < grid_x; j++) {
Mat src_cell = Mat(src, Range(i * height, (i + 1) * height), Range(j * width, (j + 1) * width));
if (type == BLUE) {
cv::threshold(src_cell, src_cell, 0, 255, CV_THRESH_OTSU + CV_THRESH_BINARY);
} else if (type == YELLOW) {
cv::threshold(src_cell, src_cell, 0, 255, CV_THRESH_OTSU + CV_THRESH_BINARY_INV);
} else if (type == WHITE) {
cv::threshold(src_cell, src_cell, 0, 255, CV_THRESH_OTSU + CV_THRESH_BINARY_INV);
} else {
cv::threshold(src_cell, src_cell, 0, 255, CV_THRESH_OTSU + CV_THRESH_BINARY);
}
}
}
}
推荐一个博客:EasyPR源码剖析(8):字符分割