diff --git a/src/src/core/chars_segment.cpp b/src/src/core/chars_segment.cpp index 3edc85a..ddd0e13 100644 --- a/src/src/core/chars_segment.cpp +++ b/src/src/core/chars_segment.cpp @@ -7,8 +7,8 @@ namespace easypr { -const float DEFAULT_BLUEPERCEMT = 0.3f; -const float DEFAULT_WHITEPERCEMT = 0.1f; +const float DEFAULT_BLUEPERCEMT = 0.3f;//定义蓝色百分比 +const float DEFAULT_WHITEPERCEMT = 0.1f;//定义白色百分比 CCharsSegment::CCharsSegment() { m_LiuDingSize = DEFAULT_LIUDING_SIZE; @@ -19,62 +19,75 @@ CCharsSegment::CCharsSegment() { m_WhitePercent = DEFAULT_WHITEPERCEMT; m_debug = DEFAULT_DEBUG; -} +}//设置几个类的成员变量,进行初始化 bool CCharsSegment::verifyCharSizes(Mat r) { // Char sizes 45x90 - float aspect = 45.0f / 90.0f; + //接收一个OpenCV的Mat对象作为参数,将其赋值给r + float aspect = 45.0f / 90.0f;//预期的字符宽高比 float charAspect = (float)r.cols / (float)r.rows; - float error = 0.7f; - float minHeight = 10.f; - float maxHeight = 35.f; + //输入图像的宽高比,通过计算输入图像的列数和行数得出。 + float error = 0.7f;//允许的宽高比误差 + float minHeight = 10.f;//字符最小高度 + float maxHeight = 35.f;//字符最大高度 // We have a different aspect ratio for number 1, and it can be ~0.2 - float minAspect = 0.05f; - float maxAspect = aspect + aspect * error; + float minAspect = 0.05f//最小允许宽高比 + float maxAspect = aspect + aspect * error + //最大允许宽高比,由预期的宽高比加上其误差得出 // area of pixels int area = cv::countNonZero(r); - // bb area + // 输入图像的非零像素数 int bbArea = r.cols * r.rows; //% of pixel in area + //输入图像的总面积,通过乘以其列数和行数得出 int percPixels = area / bbArea; - + //非零像素数在总面积中的比例 if (percPixels <= 1 && charAspect > minAspect && charAspect < maxAspect && r.rows >= minHeight && r.rows < maxHeight) return true; else return false; -} +}//判断输入图像是否可以被视为一个字符,满足特定的字符尺寸和形状要求 Mat CCharsSegment::preprocessChar(Mat in) { // Remap image + //接收一个Mat对象作为输入,它对输入的图像进行一些预处理操作。 int h = in.rows; int w = in.cols; - + //获取输入图像的高度和宽度 int charSize = CHAR_SIZE; Mat transformMat = Mat::eye(2, 3, CV_32F); + //定义一个2x3的单位矩阵作为变换矩阵,这个矩阵用于图像的几何变换。 int m = max(w, h); + transformMat.at(0, 2) = float(m / 2 - w / 2); transformMat.at(1, 2) = float(m / 2 - h / 2); - + //根据输入图像的最大尺寸,调整变换矩阵的中央值, + // 以便将图像中心置于新图像的中心。 Mat warpImage(m, m, in.type()); warpAffine(in, warpImage, transformMat, warpImage.size(), INTER_LINEAR, BORDER_CONSTANT, Scalar(0)); - + Mat out; + //warpAffine函数应用仿射变换,生成新的变换后的图像。 resize(warpImage, out, Size(charSize, charSize)); - + //resize函数调整新图像的尺寸至预设的字符大小。 return out; } //! choose the bese threshold method for chinese -void CCharsSegment::judgeChinese(Mat in, Mat& out, Color plateType) { +void CCharsSegment::judgeChinese(Mat in, Mat& out, Color plateType) +{ //接收一个Mat对象作为输入,并对其进行阈值处理以识别中文字符 Mat auxRoi = in; + //表示输入图像的一个子区域,其中可能包含要识别的中文字符 float valOstu = -1.f, valAdap = -1.f; Mat roiOstu, roiAdap; + //为Otsu阈值法和自适应阈值法分别定义了一个变量valOstu和valAdap, + // 用于存储阈值 bool isChinese = true; if (1) { if (BLUE == plateType) { @@ -89,17 +102,23 @@ void CCharsSegment::judgeChinese(Mat in, Mat& out, Color plateType) { else { threshold(auxRoi, roiOstu, 0, 255, CV_THRESH_OTSU + CV_THRESH_BINARY); } + //根据输入的颜色类型,使用不同的阈值处理方法对auxRoi进行阈值处理 + // 生成二值化的图像roiOstu roiOstu = preprocessChar(roiOstu); + //对二值化的图像进行字符识别预处理 + //调用preprocessChar(roiOstu)方法对图像进行进一步处理 if (0) { imshow("roiOstu", roiOstu); waitKey(0); destroyWindow("roiOstu"); } auto character = CharsIdentify::instance()->identifyChinese(roiOstu, valOstu, isChinese); + //对预处理后的图像进行字符识别,并返回识别的结果和阈值。 } if (1) { if (BLUE == plateType) { adaptiveThreshold(auxRoi, roiAdap, 255, ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY, 3, 0); + //调用图像处理函数对图像进行自适应阈值处理。 } else if (YELLOW == plateType) { adaptiveThreshold(auxRoi, roiAdap, 255, ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY_INV, 3, 0); @@ -111,6 +130,7 @@ void CCharsSegment::judgeChinese(Mat in, Mat& out, Color plateType) { adaptiveThreshold(auxRoi, roiAdap, 255, ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY, 3, 0); } roiAdap = preprocessChar(roiAdap); + //对二值化后的图像进行预处理字符操作。 auto character = CharsIdentify::instance()->identifyChinese(roiAdap, valAdap, isChinese); } @@ -126,20 +146,20 @@ void CCharsSegment::judgeChinese(Mat in, Mat& out, Color plateType) { } void CCharsSegment::judgeChineseGray(Mat in, Mat& out, Color plateType) { - out = in; + out = in;//复制输入图像 } bool slideChineseWindow(Mat& image, Rect mr, Mat& newRoi, Color plateType, float slideLengthRatio, bool useAdapThreshold) { std::vector charCandidateVec; - + //用于存储候选的中文字符。 Rect maxrect = mr; Point tlPoint = mr.tl(); - - bool isChinese = true; + //获取mr的左上角点,并赋值给tlPoint + bool isChinese = true;//标记图像中是否含有中文字符 int slideLength = int(slideLengthRatio * maxrect.width); - int slideStep = 1; - int fromX = 0; - fromX = tlPoint.x; + int slideStep = 1;//控制滑动窗口的步长 + int fromX = 0;//指定从哪个位置开始滑动窗口 + fromX = tlPoint.x;//实际的起始位置是左上角的x坐标 for (int slideX = -slideLength; slideX < slideLength; slideX += slideStep) { float x_slide = 0; @@ -147,20 +167,21 @@ bool slideChineseWindow(Mat& image, Rect mr, Mat& newRoi, Color plateType, float x_slide = float(fromX + slideX); float y_slide = (float)tlPoint.y; + //用来计算滑动窗口的位置。 Point2f p_slide(x_slide, y_slide); //cv::circle(image, p_slide, 2, Scalar(255), 1); int chineseWidth = int(maxrect.width); int chineseHeight = int(maxrect.height); - + //宽度和高度 Rect rect(Point2f(x_slide, y_slide), Size(chineseWidth, chineseHeight)); if (rect.tl().x < 0 || rect.tl().y < 0 || rect.br().x >= image.cols || rect.br().y >= image.rows) continue; - + //检查新定义的矩形区域是否在图像的边界内。如果超出了边界,就跳过这次循环。 Mat auxRoi = image(rect); - + //原始图像中提取出这个矩形区域的子图像auxRoi Mat roiOstu, roiAdap; if (1) { if (BLUE == plateType) { @@ -175,13 +196,15 @@ bool slideChineseWindow(Mat& image, Rect mr, Mat& newRoi, Color plateType, float else { threshold(auxRoi, roiOstu, 0, 255, CV_THRESH_OTSU + CV_THRESH_BINARY); } + //根据预设的颜色,使用不同的阈值处理方法对提取出的子图像进行阈值处理。 + // 处理后的图像分别保存到roiOstu和roiAdap。 roiOstu = preprocessChar(roiOstu, kChineseSize); - CCharacter charCandidateOstu; - charCandidateOstu.setCharacterPos(rect); - charCandidateOstu.setCharacterMat(roiOstu); - charCandidateOstu.setIsChinese(isChinese); - charCandidateVec.push_back(charCandidateOstu); + CCharacter charCandidateOstu; + charCandidateOstu.setCharacterPos(rect); // 设置字符的位置信息 + charCandidateOstu.setCharacterMat(roiOstu);// 设置字符的图像信息 + charCandidateOstu.setIsChinese(isChinese);// 设置字符是否为中文字符的信息 + charCandidateVec.push_back(charCandidateOstu);// 将字符信息添加到字符候选向量中以备后续处理使用。 } if (useAdapThreshold) { if (BLUE == plateType) { @@ -197,7 +220,7 @@ bool slideChineseWindow(Mat& image, Rect mr, Mat& newRoi, Color plateType, float adaptiveThreshold(auxRoi, roiAdap, 255, ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY, 3, 0); } roiAdap = preprocessChar(roiAdap, kChineseSize); - + //对图像进行阈值处理以获得二值化的图像。处理后的图像保存在roiAdap中。 CCharacter charCandidateAdap; charCandidateAdap.setCharacterPos(rect); charCandidateAdap.setCharacterMat(roiAdap); @@ -211,7 +234,8 @@ bool slideChineseWindow(Mat& image, Rect mr, Mat& newRoi, Color plateType, float double overlapThresh = 0.1; NMStoCharacter(charCandidateVec, overlapThresh); - + //使用了非极大值抑制(NMS)算法,它通过计算每个字符候选区域与其它所有字符候选区域的交并比 + // 将交并比低于某个阈值的字符候选区域去除 if (charCandidateVec.size() >= 1) { std::sort(charCandidateVec.begin(), charCandidateVec.end(), [](const CCharacter& r1, const CCharacter& r2) { @@ -221,53 +245,60 @@ bool slideChineseWindow(Mat& image, Rect mr, Mat& newRoi, Color plateType, float newRoi = charCandidateVec.at(0).getCharacterMat(); return true; } - + //如果生成的字符候选向量中至少有一个元素, + // 则将得分最高的字符候选区域的图像提取出来,并返回true;否则返回false。 return false; } bool slideChineseGrayWindow(const Mat& image, Rect& mr, Mat& newRoi, Color plateType, float slideLengthRatio) { std::vector charCandidateVec; - + // 定义一个向量来保存字符候选 Rect maxrect = mr; Point tlPoint = mr.tl(); - + // 获取给定的最大矩形区域的左上角点和宽度 bool isChinese = true; + // 默认假设它是中文字符 int slideLength = int(slideLengthRatio * maxrect.width); int slideStep = 1; int fromX = 0; fromX = tlPoint.x; - + // 根据最大矩形的宽度计算滑动窗口的长度和步长 for (int slideX = -slideLength; slideX < slideLength; slideX += slideStep) { + // 在指定的范围内进行滑动窗口操作 float x_slide = 0; x_slide = float(fromX + slideX); float y_slide = (float)tlPoint.y; - + // 计算当前滑动的x和y坐标 int chineseWidth = int(maxrect.width); int chineseHeight = int(maxrect.height); - + // 获取中国字符的宽度和高度 Rect rect(Point2f(x_slide, y_slide), Size(chineseWidth, chineseHeight)); - + // 根据当前滑动的坐标和中国字符的尺寸创建一个矩形区域 if (rect.tl().x < 0 || rect.tl().y < 0 || rect.br().x >= image.cols || rect.br().y >= image.rows) continue; - + // 检查这个矩形是否在图像内,如果不在,则跳过当前的循环迭代 Mat auxRoi = image(rect); Mat grayChinese; grayChinese.create(kGrayCharHeight, kGrayCharWidth, CV_8UC1); + // 创建一个灰度图像,尺寸为预设的中文字符的尺寸和通道数 resize(auxRoi, grayChinese, grayChinese.size(), 0, 0, INTER_LINEAR); - + // 将提取的子图像调整为预设的中文字符的尺寸,并将其保存到灰度图像中 + // 从图像中提取当前矩形区域的子图像 CCharacter charCandidateOstu; charCandidateOstu.setCharacterPos(rect); charCandidateOstu.setCharacterMat(grayChinese); charCandidateOstu.setIsChinese(isChinese); - charCandidateVec.push_back(charCandidateOstu); + charCandidateVec.push_back(charCandidateOstu);// 将字符候选对象添加到向量中以备后续处理使用 + // 创建一个字符候选对象,并设置其位置、图像和其他属性(这里指定为中文字符) } CharsIdentify::instance()->classifyChineseGray(charCandidateVec); - + // 对所有的字符候选进行分类(这里假设是中文字符分类) double overlapThresh = 0.1; NMStoCharacter(charCandidateVec, overlapThresh); - + // 对所有的字符候选进行非极大值抑制(NMS)以消除多余的字符候选区域, + // 这里的阈值设置为0.1(根据实际情况可能需要进行调整) if (charCandidateVec.size() >= 1) { std::sort(charCandidateVec.begin(), charCandidateVec.end(), [](const CCharacter& r1, const CCharacter& r2) { @@ -278,18 +309,23 @@ bool slideChineseGrayWindow(const Mat& image, Rect& mr, Mat& newRoi, Color plate mr = charCandidateVec.at(0).getCharacterPos(); return true; } + // 如果字符候选向量中至少有一个元素,则对它们进行排序, + // 并提取出得分最高的字符候选区域,将其图像保存到newRoi中, + // 并更新mr为最高得分字符候选的位置,然后返回true;否则返回false。 + // 注意这里使用了一个lambda表达式作为排序函数, + // 根据字符候选的得分进行降序排序。 return false; } int CCharsSegment::charsSegment(Mat input, vector& resultVec, Color color) { if (!input.data) return 0x01; - + //检查输入图像是否有数据 Color plateType = color; - Mat input_grey; + Mat input_grey; //存储灰度图像。 cvtColor(input, input_grey, CV_BGR2GRAY); - + //将输入图像转换为灰度图像,并保存到input_grey中。 if (0) { imshow("plate", input_grey); waitKey(0); @@ -300,6 +336,7 @@ int CCharsSegment::charsSegment(Mat input, vector& resultVec, Color color) img_threshold = input_grey.clone(); spatial_ostu(img_threshold, 8, 2, plateType); + //对图像进行Otsu阈值分割 if (0) { imshow("plate", img_threshold); @@ -310,23 +347,28 @@ int CCharsSegment::charsSegment(Mat input, vector& resultVec, Color color) // 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); - + //将img_threshold复制到img_contours中。 vector > contours; + //定义一个二维向量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 - + //使用OpenCV的findContours函数查找图像中的轮廓。 + // 这里指定了检索模式为外部轮廓,并且指定了轮廓的近似方法。 vector >::iterator itc = contours.begin(); + //定义一个迭代器itc,并初始化为轮廓向量的起始位置 vector vecRect; - - while (itc != contours.end()) { + //定义一个向量vecRect,用于保存每个轮廓的边界框。 + while (itc != contours.end()) {//遍历轮廓向量 Rect mr = boundingRect(Mat(*itc)); + //对于当前轮廓,使用OpenCV的boundingRect函数计算其边界框, + // 并保存到变量mr中。 Mat auxRoi(img_threshold, mr); - + //根据当前轮廓的边界框,从图像中提取一个子图像。 if (verifyCharSizes(auxRoi)) vecRect.push_back(mr); ++itc; } @@ -335,16 +377,18 @@ int CCharsSegment::charsSegment(Mat input, vector& resultVec, Color color) if (vecRect.size() == 0) return 0x03; vector sortedRect(vecRect); + //创建一个新的向量sortedRect,并将vecRect的内容复制到这个新的向量中。 std::sort(sortedRect.begin(), sortedRect.end(), [](const Rect& r1, const Rect& r2) { return r1.x < r2.x; }); - + //使用lambda函数将sortedRect按照x坐标从小到大进行排序。 size_t specIndex = 0; specIndex = GetSpecificRect(sortedRect); - + //获取特定的矩形 Rect chineseRect; if (specIndex < sortedRect.size()) chineseRect = GetChineseRect(sortedRect[specIndex]); + //获取特定索引的矩形并存储到chineseRect else return 0x04; @@ -354,11 +398,13 @@ int CCharsSegment::charsSegment(Mat input, vector& resultVec, Color color) waitKey(0); destroyWindow("plate"); } + //绘制矩形chineseRect在图像上并显示出来 + //这部分代码永远不会被执行 vector newSortedRect; newSortedRect.push_back(chineseRect); RebuildRect(sortedRect, newSortedRect, specIndex); - + //对图像中的矩形区域进行重新构建或处理的。 if (newSortedRect.size() == 0) return 0x05; bool useSlideWindow = true; @@ -366,12 +412,14 @@ int CCharsSegment::charsSegment(Mat input, vector& resultVec, Color color) //bool useAdapThreshold = CParams::instance()->getParam1b(); for (size_t i = 0; i < newSortedRect.size(); i++) { + //循环遍历新的排序矩形向量中的每一个矩形。 Rect mr = newSortedRect[i]; - + //获取当前矩形的坐标信息 // Mat auxRoi(img_threshold, mr); Mat auxRoi(input_grey, mr); + //根据给定的图像和矩形区域创建ROI Mat newRoi; - + //第二个是用来存储处理后的图像的。 if (i == 0) { if (useSlideWindow) { float slideLengthRatio = 0.1f; @@ -395,15 +443,24 @@ int CCharsSegment::charsSegment(Mat input, vector& resultVec, Color color) else { threshold(auxRoi, newRoi, 0, 255, CV_THRESH_OTSU + CV_THRESH_BINARY); } - + //根据不同的颜色来执行不同的阈值处理操作, + // 然后对处理后的图像进行预处 newRoi = preprocessChar(newRoi); } if (0) { if (i == 0) { imshow("input_grey", input_grey); + // 使用imshow函数显示名为"input_grey"的窗口,并在其中显示图像"input_grey"。 + // 用于创建和显示窗口。 + waitKey(0); + //这是调用OpenCV的waitKey函数,等待用户按键。 + // 该函数的参数是0,这意味着它将无限期地等待用户按键。 + // 一旦用户按下键,该函数将返回按下的键的ASCII值。 + destroyWindow("input_grey"); + //这个函数会销毁窗口并释放其内存。 } if (i == 0) { imshow("newRoi", newRoi); @@ -413,6 +470,9 @@ int CCharsSegment::charsSegment(Mat input, vector& resultVec, Color color) } resultVec.push_back(newRoi); + // 将新的Roi添加到名为resultVec的向量中。 + // 存储一系列元素。这里将新的Roi图像添加到该向量中。 + } return 0; @@ -421,44 +481,53 @@ int CCharsSegment::charsSegment(Mat input, vector& resultVec, Color color) int CCharsSegment::projectSegment(const Mat& input, Color color, vector& out_indexs) { if (!input.data) return 0x01; - Color plateType = color; - Mat input_grey; - cvtColor(input, input_grey, CV_BGR2GRAY); - SHOW_IMAGE(input_grey, 0); + Color plateType = color; // 将输入的颜色赋值给plateType + Mat input_grey; // 定义一个Mat对象用于存储灰度图像 + cvtColor(input, input_grey, CV_BGR2GRAY); // 将输入图像转换为灰度图像 + SHOW_IMAGE(input_grey, 0); // 显示灰度图像 - Mat img_threshold; - img_threshold = input_grey.clone(); - spatial_ostu(img_threshold, 8, 2, plateType); - SHOW_IMAGE(img_threshold, 0); + Mat img_threshold; // 定义一个Mat对象用于存储阈值化的图像 + img_threshold = input_grey.clone(); // 将灰度图像复制到img_threshold中 + spatial_ostu(img_threshold, 8, 2, plateType); // 对图像进行空间自适应阈值化处理, + SHOW_IMAGE(img_threshold, 0); // 显示阈值化后的图像 // remove liuding and hor lines // also judge weather is plate use jump count - if (!clearLiuDing(img_threshold)) return 0x02; - SHOW_IMAGE(img_threshold, 0); - - Mat vhist = ProjectedHistogram(img_threshold, VERTICAL, 0); - Mat showHist = showHistogram(vhist); - SHOW_IMAGE(showHist, 1); - - vector values; - vector indexs; - int size = vhist.cols; - for (int i = 0; i < size; i++) { - float val = vhist.at(i); - values.push_back(1.f - val); + // 进行一些形状的清理和判断是否为车牌的跳跃计数 + if (!clearLiuDing(img_threshold)) return 0x02; + // 使用clearLiuDing函数清理图像中的多余部分, + SHOW_IMAGE(img_threshold, 0); + // 显示清理后的图像 + + Mat vhist = ProjectedHistogram(img_threshold, VERTICAL, 0); + // 对图像进行垂直直方图投影,结果保存在vhist中 + Mat showHist = showHistogram(vhist); + // 显示直方图,但是此行代码在后续的代码中并没有被使用 + SHOW_IMAGE(showHist, 1); // 显示直方图 + + vector values; // 定义一个向量用于存储每个像素到直方图最大值的距离 + vector indexs; // 定义一个向量用于存储非最大值的索引 + int size = vhist.cols; // 获取直方图列的数量,作为后续循环的次数 + + for (int i = 0; i < size; i++) { // 遍历每个像素在直方图中的位置 + float val = vhist.at(i); // 获取当前像素在直方图中的值 + values.push_back(1.f - val); + // 计算当前像素到直方图最大值的距离,并添加到values向量中 } - Mat img_test = img_threshold.clone(); - NMSfor1D(values, indexs); - - out_indexs.resize(size); - for (int j = 0; j < size; j++) - out_indexs.at(j) = 0; - for (int i = 0; i < size; i++) { - float val = vhist.at(i); - if (indexs.at(i) && val < 0.1f) { - out_indexs.at(i) = 1; + Mat img_test = img_threshold.clone(); // 复制阈值化后的图像到一个新的Mat对象中 + NMSfor1D(values, indexs); // 对values向量进行非极大值抑制(NMS) + + out_indexs.resize(size); // 重新调整out_indexs向量的尺寸,为后续的赋值做准备 + for (int j = 0; j < size; j++) + out_indexs.at(j) = 0; // 将out_indexs向量中的所有值初始化为0 + + for (int i = 0; i < size; i++) { // 遍历每个像素在直方图中的位置 + float val = vhist.at(i); // 获取当前像素在直方图中的值 + if (indexs.at(i) && val < 0.1f) { // 如果当前像素是非极大值且其值小于0.1f + out_indexs.at(i) = 1; // 则将out_indexs向量中的相应位置设置为1 for (int j = 0; j < img_test.rows; j++) { img_test.at(j, i) = (char)255; + // 将该位置的像素值设置为255(白色) } } } @@ -470,72 +539,106 @@ int CCharsSegment::projectSegment(const Mat& input, Color color, vector& ou bool verifyCharRectSizes(Rect r) { // Char sizes 45x90 float aspect = 45.0f / 90.0f; + //定义一个变量aspect,其值为0.5,表示字符的预期宽高比。 float charAspect = (float)r.width / (float)r.height; + //计算输入矩形的宽高比,并将其存储在变量charAspect中。 float error = 0.5f; + //表示允许的宽高比误差。 float minHeight = kPlateResizeHeight * 0.5f; + //表示矩形的最小高度 float maxHeight = kPlateResizeHeight * 1.f; + //表示矩形的最大高度 // We have a different aspect ratio for number 1, and it can be ~0.2 float minAspect = 0.10f; //0.2f; - + //表示允许的最小宽高比 float maxAspect = 0.85f; // aspect + aspect * error; //0.8f; - + //表示允许的最大宽高比 int ch = r.tl().y + r.height / 2; + //计算矩形的中心点纵坐标 int min_ch = int(kPlateResizeHeight * 0.3f); + //表示字符中心点的最小纵坐标 int max_ch = int(kPlateResizeHeight * 0.7f); + //表示字符中心点的最大纵坐标 if (ch > max_ch || ch < min_ch) return false; - + //如果字符中心点的纵坐标超出允许的范围,则返回false。 float h = (float)r.height; + //将输入矩形的height转换为浮点数 if (h > maxHeight || h < minHeight) return false; + //矩形的height超出允许的范围,则返回false。 if (charAspect < minAspect || charAspect > maxAspect) return false; - + //如果矩形的宽高比超出允许的范围,则返回false。 return true; } Mat preprocessCharMat(Mat in, int char_size) { // Remap image - int h = in.rows; - int w = in.cols; + // 对图像进行映射变换 + int h = in.rows; // 获取输入图像的行数 + int w = in.cols; // 获取输入图像的列数 - int charSize = char_size; + int charSize = char_size; // 将传入的字符大小参数保存为charSize - Mat transformMat = Mat::eye(2, 3, CV_32F); - int m = max(w, h); - transformMat.at(0, 2) = float(m / 2 - w / 2); - transformMat.at(1, 2) = float(m / 2 - h / 2); + // 创建一个2x3的单位矩阵,作为图像变换的转换矩阵 + Mat transformMat = Mat::eye(2, 3, CV_32F); - Mat warpImage(m, m, in.type()); - warpAffine(in, warpImage, transformMat, warpImage.size(), INTER_LINEAR, - BORDER_CONSTANT, Scalar(0)); + // 找出输入图像的最大尺寸(高或宽) + int m = max(w, h); - Mat out; - cv::resize(warpImage, out, Size(charSize, charSize)); + // 根据最大尺寸,设置转换矩阵的最后两行(平移参数) + // 这样做的目的是将输入图像的中心移动到变换后的图像的中心 + transformMat.at(0, 2) = float(m / 2 - w / 2); + transformMat.at(1, 2) = float(m / 2 - h / 2); - return out; + // 创建一个与输入图像相同类型和大小的输出图像 + Mat warpImage(m, m, in.type()); + + // 使用上面定义的转换矩阵,对输入图像执行仿射变换(warpAffine) + warpAffine(in, warpImage, transformMat, warpImage.size(), INTER_LINEAR, + BORDER_CONSTANT, Scalar(0)); + + // 根据指定的字符大小,调整变换后的图像大小 + Mat out; + cv::resize(warpImage, out, Size(charSize, charSize)); + + // 返回处理后的图像 + return out; } Mat clearLiuDingAndBorder(const Mat& grayImage, Color color) { SHOW_IMAGE(grayImage, 0); + //显示输入的灰度图像 Mat img_threshold; + //定义一个Mat对象img_threshold,用于存放阈值化后的图像。 img_threshold = grayImage.clone(); + //将输入的灰度图像复制到img_threshold中。 spatial_ostu(img_threshold, 1, 1, color); + //对图像进行空间自适应阈值化(spatial OSTU), + // 这里的参数1和1可能表示的是高斯核的大小,而color则表示颜色空间。 clearLiuDing(img_threshold); + //为了去除刘定噪声 Rect cropRect; + //存放裁剪区域的坐标。 clearBorder(img_threshold, cropRect); + //为了清除图像的边框 Mat cropedGrayImage; + //存放裁剪后的灰度图像 resize(grayImage(cropRect), cropedGrayImage, Size(kPlateResizeWidth, kPlateResizeHeight)); + //对裁剪后的灰度图像进行大小调整。 SHOW_IMAGE(cropedGrayImage, 0); + //显示调整大小后的灰度图像。 return cropedGrayImage; + //返回处理后的灰度图像。 } void NMStoCharacterByRatio(std::vector &inVec, double overlap, const Rect groundRect) { // rechange the score - for (auto& character : inVec) { - double score = character.getCharacterScore(); + for (auto& character : inVec) {//对inVec中的每个字符进行操作 + double score = character.getCharacterScore();//获取当前字符的得分 //cout << "score:" << score << endl; - Rect rect = character.getCharacterPos(); + Rect rect = character.getCharacterPos();//获取当前字符的位置信息 int w = rect.width; int h = rect.height; int gw = groundRect.width; @@ -545,13 +648,14 @@ void NMStoCharacterByRatio(std::vector &inVec, double overlap, const int w_diff = abs(w - gw); int h_diff = abs(h - gh); - + //计算当前字符框与地面真实框的IOU(交并比) //float w_ratio = (float)w / (float)gw; //float h_ratio = (float)h / (float)gh; float w_ratio = 1 - (float)w_diff / (float)gw; float h_ratio = 1 - (float)h_diff / (float)gh; - + //分别表示字符框的宽度和高度与地面真实框的宽高比。 + // 这两个比例会影响最终的权重得分。 float a = 0.5f; float b = 0.5f; //cout << "str:" << character.getCharacterStr() << endl; @@ -559,17 +663,20 @@ void NMStoCharacterByRatio(std::vector &inVec, double overlap, const if ("1" == character.getCharacterStr()) { a = 0.3f; //0.2f; b = 0.7f; //0.8f; + //如果字符是'1',那么会对其IOU进行一个调整。 } float c = 0.1f; //float weighted_score = a * (float)score + b * w_ratio + c * h_ratio; float weighted_score = a * (float)score + b * w_ratio + c * h_ratio; + //计算权重得分 SHOW_IMAGE(character.getCharacterMat(), 0); character.setCharacterScore((double)weighted_score); + //设置新的权重得分。 //cout << "weighted_score:" << character.getCharacterScore() << endl; } std::sort(inVec.begin(), inVec.end()); - + //对vector进行排序,以便后续的NMS操作。 std::vector::iterator it = inVec.begin(); for (; it != inVec.end(); ++it) { CCharacter charSrc = *it; @@ -589,20 +696,26 @@ void NMStoCharacterByRatio(std::vector &inVec, double overlap, const ++itc; } } - } + }//如果它与后面的字符的IOU大于预设的重叠阈值,那么就将后面的字符从vector中删除。 + //这样可以保证最终vector中每一个字符都不与其它字符有大的重叠 } int getNearestIndex(Point center, const vector& groundCenters) { int gc_size = int(groundCenters.size()); + //获取 groundCenters 向量的大小,并将其转换为整数类型并存储在变量 gc_size 中 int index = 0; + //用于存储最接近 center 的地心坐标的索引 int min_length = INT_MAX; - for (int p = 0; p < gc_size; p++) { - Point gc_point = groundCenters.at(p); + //用于存储与 center 距离最短的地心坐标的距离的平方 + for (int p = 0; p < gc_size; p++) {//遍历所有地心坐标 + Point gc_point = groundCenters.at(p);//获取索引为 p 的地心坐标,并将其存储在变量 gc_point 中。 int length_square = (gc_point.x - center.x) * (gc_point.x - center.x) + (gc_point.y - center.y) * (gc_point.y - center.y); + //计算当前地心坐标 gc_point 与 center 之间的距离的平方。 //int length_square = abs(gc_point.x - center.x); if (length_square < min_length) { min_length = length_square; + //当前地心坐标与 center 的距离的平方设置为新的最小距离。 index = p; } } @@ -610,59 +723,58 @@ int getNearestIndex(Point center, const vector& groundCenters) { } int CCharsSegment::charsSegmentUsingMSER(Mat input, vector& resultVec, vector& grayChars, Color color) { - Mat grayImage; - cvtColor(input, grayImage, CV_BGR2GRAY); - std::vector bgrSplit; - split(input, bgrSplit); - - //Mat grayChannel = clearLiuDingAndBorder(grayImage, color); //clearLiuDingAndBorder(grayImage, color); - Mat grayChannel = grayImage; - - // Mat cropedGrayImage = grayImage; - // generate all channgel images; - vector channelImages; - bool useThreeChannel = false; - channelImages.push_back(grayChannel); - if (useThreeChannel) { - for (int i = 0; i < 3; i++) - channelImages.push_back(bgrSplit.at(i)); - } - int csize = channelImages.size(); - - //TODO three channels - std::vector> all_contours; - std::vector all_boxes; - all_contours.reserve(32); - all_boxes.reserve(32); - - const int imageArea = input.rows * input.cols; - const int delta = 1; - const int minArea = 30; - const double maxAreaRatio = 0.2; - - int type = -1; - if (Color::BLUE == color) type = 0; - if (Color::YELLOW == color) type = 1; - if (Color::WHITE == color) type = 1; - if (Color::UNKNOWN == color) type = 0; - - for (int c_index = 0; c_index < csize; c_index++) { - Mat cimage = channelImages.at(c_index); - Mat testImage = cimage.clone(); - cvtColor(testImage, testImage, CV_GRAY2BGR); - - const float plateMaxSymbolCount = kPlateMaxSymbolCount; - const int symbolIndex = kSymbolIndex; - float segmentRatio = plateMaxSymbolCount - int(plateMaxSymbolCount); - const int plateMaxCharCount = int(plateMaxSymbolCount); - - vector> charsVecVec; - charsVecVec.resize(plateMaxCharCount); - - vector groundCenters; - groundCenters.reserve(plateMaxCharCount); - vector groundRects; - groundRects.reserve(plateMaxCharCount); + Mat grayImage; // 定义灰度图像变量 + cvtColor(input, grayImage, CV_BGR2GRAY); // 转换为灰度图像 + std::vector bgrSplit; // 定义三通道图像变量 + split(input, bgrSplit); // 将BGR三通道分离存入变量 + + Mat grayChannel = grayImage; // 将灰度图像赋值给grayChannel + + vector channelImages; // 定义用于存储通道图像的变量 + bool useThreeChannel = false; // 标识是否使用三通道图像的布尔变量 + + channelImages.push_back(grayChannel); // 将灰度通道图像存入channelImages + if (useThreeChannel) { // 如果使用三通道 + for (int i = 0; i < 3; i++) + channelImages.push_back(bgrSplit.at(i)); // 将BGR三通道存入channelImages + } + + int csize = channelImages.size(); // 获取通道图像的数量 + + // TODO three channels + std::vector> all_contours; // 定义所有字符轮廓的变量 + std::vector all_boxes; // 定义所有字符框的变量 + all_contours.reserve(32); // 预分配空间 + all_boxes.reserve(32); // 预分配空间 + + const int imageArea = input.rows * input.cols; // 获取输入图像面积 + const int delta = 1; // 定义delta参数,控制灵敏度 + const int minArea = 30; // 定义minArea参数,控制灵敏度 + const double maxAreaRatio = 0.2; // 定义maxAreaRatio参数,控制灵敏度 + + int type = -1; // 定义表示颜色类型的参数 + if (Color::BLUE == color) type = 0; + if (Color::YELLOW == color) type = 1; + if (Color::WHITE == color) type = 1; + if (Color::UNKNOWN == color) type = 0; + + for (int c_index = 0; c_index < csize; c_index++) { // 对每个通道分别进行处理 + Mat cimage = channelImages.at(c_index); // 获取当前通道的图像 + Mat testImage = cimage.clone(); // 复制当前图像 + cvtColor(testImage, testImage, CV_GRAY2BGR); // 将testImage转换为RGB格式 + + const float plateMaxSymbolCount = kPlateMaxSymbolCount; // 最多字符数 + const int symbolIndex = kSymbolIndex; // 字符下标 + float segmentRatio = plateMaxSymbolCount - int(plateMaxSymbolCount); // 准备得到字符段的比例 + const int plateMaxCharCount = int(plateMaxSymbolCount); // 一个车牌中最多字符数 + + vector> charsVecVec; // 定义字符向量数组 + charsVecVec.resize(plateMaxCharCount); // 调整为车牌中最多字符数 + + vector groundCenters; // 定义字符的中心点坐标 + groundCenters.reserve(plateMaxCharCount); // 预分配空间 + vector groundRects; // 定义字符框 + groundRects.reserve(plateMaxCharCount); // 预分配空间 // compute the ground char rect int avg_char_width = int(kPlateResizeWidth * (1.f / plateMaxSymbolCount)); @@ -670,112 +782,122 @@ int CCharsSegment::charsSegmentUsingMSER(Mat input, vector& resultVec, vect int x_axis = 0; int y_axis = int((kPlateResizeHeight - avg_char_height) * 0.5f); - for (int j = 0; j < plateMaxSymbolCount; j++) { - int char_width = avg_char_width; - if (j != symbolIndex) char_width = avg_char_width; - else char_width = int(segmentRatio * avg_char_width); + for (int j = 0; j < plateMaxSymbolCount; j++) { // 遍历每个字符 + int char_width = avg_char_width; // 获取平均字符宽度 + if (j != symbolIndex) char_width = avg_char_width; + else char_width = int(segmentRatio * avg_char_width); // 计算当前字符宽度 - Rect avg_char_rect = Rect(x_axis, y_axis, char_width, avg_char_height); - rectangle(testImage, avg_char_rect, Scalar(0, 0, 255)); + Rect avg_char_rect = Rect(x_axis, y_axis, char_width, avg_char_height); // 确定字符坐标 + rectangle(testImage, avg_char_rect, Scalar(0, 0, 255)); // 绘制矩形框 - Point center = Point(x_axis + int(char_width * 0.5f), y_axis + int(avg_char_height * 0.5f)); - circle(testImage, center, 3, Scalar(0, 255, 0)); - x_axis += char_width; + Point center = Point(x_axis + int(char_width * 0.5f), y_axis + int(avg_char_height * 0.5f)); // 计算中心点坐标 + circle(testImage, center, 3, Scalar(0, 255, 0)); // 绘制中心点 + x_axis += char_width; // 取下一个字符的X轴起始位置 - if (j != symbolIndex) { - groundCenters.push_back(center); - groundRects.push_back(avg_char_rect); - } + if (j != symbolIndex) { // 如果当前字符不是分割字符 + groundCenters.push_back(center); // 将字符中心点存入groundCenters + groundRects.push_back(avg_char_rect); // 将字符框存入groundRects + } } - SHOW_IMAGE(testImage, 0); + SHOW_IMAGE(testImage, 0); // 显示测试图像 - Mat showImage = cimage.clone(); - cvtColor(showImage, showImage, CV_GRAY2BGR); - Mat mdoImage = cimage.clone(); - string candidateLicense; + Mat showImage = cimage.clone(); // 复制灰度图像 + cvtColor(showImage, showImage, CV_GRAY2BGR); // 转换为RGB格式 + Mat mdoImage = cimage.clone(); // 复制灰度图像 + string candidateLicense; // 候选车牌 - Ptr mser; + Ptr mser; // 定义MSER指针 // use origin mser to detect as many as possible characters - mser = MSER2::create(delta, minArea, int(maxAreaRatio * imageArea), false); - mser->detectRegions(cimage, all_contours, all_boxes, type); + mser = MSER2::create(delta, minArea, int(maxAreaRatio * imageArea), false); // 初始化MSER指针 + mser->detectRegions(cimage, all_contours, all_boxes, type); // 检测字符轮廓和字符框 - std::vector charVec; - charVec.reserve(16); - size_t size = all_contours.size(); + std::vector charVec; // 定义CCharacter向量 + charVec.reserve(16); // 预分配空间 + size_t size = all_contours.size(); // 获取字符轮廓的数量 - int char_index = 0; - int char_size = 20; + int char_index = 0; // 定义字符下标 + int char_size = 20; // 定义字符数量 + // 克隆原始彩色图像,准备进行MSER特征显示 Mat showMSERImage = cimage.clone(); + // 将显示图像转换为BGR格式,准备涂上彩色方框等 cvtColor(showMSERImage, showMSERImage, CV_GRAY2BGR); + // verify char size and output to rects; for (size_t index = 0; index < size; index++) { - Rect rect = all_boxes[index]; - vector &contour = all_contours[index]; - rectangle(showMSERImage, rect, Scalar(0,0,255)); + Rect rect = all_boxes[index]; // 获取一个候选框 + vector& contour = all_contours[index]; // 相应的边缘点 + rectangle(showMSERImage, rect, Scalar(0, 0, 255)); // 在图上画出红色的方框 - // find character - if (verifyCharRectSizes(rect)) { - Mat mserMat = adaptive_image_from_points(contour, rect, Size(char_size, char_size)); - Mat mserInput = preprocessCharMat(mserMat, char_size); + // 找到字符 + if (verifyCharRectSizes(rect)) { // 验证字符矩形尺寸是否合适 + Mat mserMat = adaptive_image_from_points(contour, rect, Size(char_size, char_size)); // 使用MSER的点重新组织图像块 + Mat mserInput = preprocessCharMat(mserMat, char_size); // 对MSER图像块进行预处理 Rect charRect = rect; - Point center(charRect.tl().x + charRect.width / 2, charRect.tl().y + charRect.height / 2); + Point center(charRect.tl().x + charRect.width / 2, charRect.tl().y + charRect.height / 2); // 计算矩形的中心点 Mat tmpMat; - double ostu_level = cv::threshold(cimage(charRect), tmpMat, 0, 255, CV_THRESH_BINARY | CV_THRESH_OTSU); - Mat grayCharMat = cimage(charRect); + double ostu_level = cv::threshold(cimage(charRect), tmpMat, 0, 255, CV_THRESH_BINARY | CV_THRESH_OTSU); // 使用Otsu算法计算二值化阈值 + Mat grayCharMat = cimage(charRect); // 获取字符区域的灰度图 Mat ostuMat; + // 根据字符颜色选择合适的二值化方法 switch (color) { - case BLUE: threshold(grayCharMat, ostuMat, 0, 255, CV_THRESH_BINARY + CV_THRESH_OTSU); break; - case YELLOW: threshold(grayCharMat, ostuMat, 0, 255, CV_THRESH_BINARY_INV + CV_THRESH_OTSU); break; - case WHITE: threshold(grayCharMat, ostuMat, 0, 255, CV_THRESH_OTSU + CV_THRESH_BINARY_INV); break; - default: threshold(grayCharMat, ostuMat, 0, 255, CV_THRESH_OTSU + CV_THRESH_BINARY); break; + case BLUE: threshold(grayCharMat, ostuMat, 0, 255, CV_THRESH_BINARY + CV_THRESH_OTSU); break; + case YELLOW: threshold(grayCharMat, ostuMat, 0, 255, CV_THRESH_BINARY_INV + CV_THRESH_OTSU); break; + case WHITE: threshold(grayCharMat, ostuMat, 0, 255, CV_THRESH_OTSU + CV_THRESH_BINARY_INV); break; + default: threshold(grayCharMat, ostuMat, 0, 255, CV_THRESH_OTSU + CV_THRESH_BINARY); break; } - Mat ostuInput = preprocessChar(ostuMat); - // use judegMDOratio2 function to - // remove the small lines in character like "zh-cuan" + Mat ostuInput = preprocessChar(ostuMat); // 对Otsu算法处理后的图像进行进一步预处理 + // 使用judegMDOratio2函数移除字符内像“川”字中的小横线 if (judegMDOratio2(cimage, rect, contour, mdoImage, 1.2f, true)) { - CCharacter charCandidate; - //cout << contour.size() << endl; - charCandidate.setCharacterPos(charRect); - charCandidate.setCharacterMat(ostuInput); //charInput or ostuInput - charCandidate.setOstuLevel(ostu_level); - charCandidate.setCenterPoint(center); - int pos = getNearestIndex(center, groundCenters); - charsVecVec.at(pos).push_back(charCandidate); - charCandidate.setIndex(pos); - charCandidate.setIsChinese(false); - charVec.push_back(charCandidate); + CCharacter charCandidate; + // 设置字符候选的各项属性 + charCandidate.setCharacterPos(charRect); + charCandidate.setCharacterMat(ostuInput); // 使用Otsu输入作为字符图像 + charCandidate.setOstuLevel(ostu_level); + charCandidate.setCenterPoint(center); + int pos = getNearestIndex(center, groundCenters); // 找到最接近的字符候选位置 + charsVecVec.at(pos).push_back(charCandidate); + charCandidate.setIndex(pos); + charCandidate.setIsChinese(false); + charVec.push_back(charCandidate); // 添加到字符向量中 + } + } + else { + SHOW_IMAGE(showMSERImage(rect), 0); // 不符合条件的显示出来 } - } - else { - SHOW_IMAGE(showMSERImage(rect), 0); - } } SHOW_IMAGE(showMSERImage, 0); - SHOW_IMAGE(mdoImage, 0); + SHOW_IMAGE(mdoImage, 0); // 显示经过MDOratio处理后的图像 - // classify all the images; + // 对所有候选字符图像进行分类 CharsIdentify::instance()->classify(charVec); Rect maxrect = groundRects.at(0); - // NMS to the seven groud truth rect + // 对7个真值区域进行NMS(非极大抑制) bool useGround = true; if (useGround) { - for (auto charCandidate : charVec) { - int pos = charCandidate.getIndex(); - charsVecVec.at(pos).push_back(charCandidate); - } - charVec.clear(); - for (size_t c = 0; c < charsVecVec.size(); c++) { - Mat testImage_2 = cimage.clone(); - cvtColor(testImage_2, testImage_2, CV_GRAY2BGR); - vector& charPosVec = charsVecVec.at(c); - for (auto character : charPosVec) { - rectangle(testImage_2, character.getCharacterPos(), Scalar(0, 255, 0)); + // 遍历候选字符 + for (auto charCandidate : charVec) { + int pos = charCandidate.getIndex(); + // 将字符添加到对应索引位置的向量中 + charsVecVec.at(pos).push_back(charCandidate); } - SHOW_IMAGE(testImage_2, 0); - + // 清空原始字符向量 + charVec.clear(); + for (size_t c = 0; c < charsVecVec.size(); c++) { + // 创建测试图像的副本,并转换为BGR颜色空间 + Mat testImage_2 = cimage.clone(); + cvtColor(testImage_2, testImage_2, CV_GRAY2BGR); + + // 在测试图像上为每个字符绘制矩形 + vector& charPosVec = charsVecVec.at(c); + for (auto character : charPosVec) { + rectangle(testImage_2, character.getCharacterPos(), Scalar(0, 255, 0)); + } + // 显示标记了字符的测试图像 + SHOW_IMAGE(testImage_2, 0); + // 使用非极大抑制(NMS)来合并重叠的字符 double overlapThresh = 0.; NMStoCharacterByRatio(charPosVec, overlapThresh, groundRects.at(c)); charPosVec.shrink_to_fit(); @@ -786,34 +908,40 @@ int CCharsSegment::charsSegmentUsingMSER(Mat input, vector& resultVec, vect rectangle(testImage_3, character.getCharacterPos(), Scalar(0, 255, 0)); } - // only the last group will contain more than one candidate character + // 对最后一组字符,所有候选字符都会被添加回charVec if (charsVecVec.size() - 1 == c) { - for (auto charPos : charPosVec) - charVec.push_back(charPos); + for (auto charPos : charPosVec) + charVec.push_back(charPos); } + // 对于其它组,如果有候选字符,只有第一个字符会被添加 else { - if (charPosVec.size() != 0) { - CCharacter& inputChar = charPosVec.at(0); - charVec.push_back(inputChar); - Mat charMat = inputChar.getCharacterMat(); - SHOW_IMAGE(charMat, 0); - } + if (charPosVec.size() != 0) { + CCharacter& inputChar = charPosVec.at(0); + charVec.push_back(inputChar); + // 提取字符的Mat对象并显示 + Mat charMat = inputChar.getCharacterMat(); + SHOW_IMAGE(charMat, 0); + } } + // 查找面积最大的矩形 for (auto charPos : charPosVec) { Rect r = charPos.getCharacterPos(); if (r.area() > maxrect.area()) maxrect = r; } + // 显示带有字符标记的测试图像 SHOW_IMAGE(testImage_3, 0); } } else { + // 如果useGround为假,则只用NMS处理charVec而不是charsVecVec NMStoCharacterByRatio(charVec, 0.2f, maxrect); } - + // 如果charVec中的字符数量少于一定阈值,函数将返回错误码 if (charVec.size() < kCharsCountInOnePlate) return 0x03; + // 对字符进行排序,基于它们在x轴上的位置 std::sort(charVec.begin(), charVec.end(),[](const CCharacter& r1, const CCharacter& r2) { return r1.getCharacterPos().x < r2.getCharacterPos().x; }); - + // 构建预测的许可证字符串并对矩形排序 string predictLicense = ""; vector sortedRect; for (auto charCandidate : charVec) { @@ -822,33 +950,35 @@ int CCharsSegment::charsSegmentUsingMSER(Mat input, vector& resultVec, vect } std::sort(sortedRect.begin(), sortedRect.end(), [](const Rect& r1, const Rect& r2) { return r1.x < r2.x; }); + // 输出预测的许可证字符串 cout << "predictLicense: " << predictLicense << endl; // find chinese rect - size_t specIndex = 0; - specIndex = GetSpecificRect(sortedRect); - SHOW_IMAGE(showImage(sortedRect[specIndex]), 0); + size_t specIndex = 0; // 初始化一个变量specIndex,用于存储特定矩形的索引 + specIndex = GetSpecificRect(sortedRect); // 获取有特定特征的矩形的索引,并存储于specIndex + SHOW_IMAGE(showImage(sortedRect[specIndex]), 0); // 显示经过排序的specIndex指定的矩形图像 - Rect chineseRect; + Rect chineseRect; // 创建一个矩形变量用于存储中文字符的矩形区域 if (specIndex < sortedRect.size()) - chineseRect = GetChineseRect(sortedRect[specIndex]); + chineseRect = GetChineseRect(sortedRect[specIndex]); // 如果specIndex小于矩形数组的大小,获取中文字符的矩形区域 else - return 0x04; + return 0x04; // specIndex越界,返回错误代码 - vector newSortedRect; - newSortedRect.push_back(chineseRect); - if (newSortedRect.size() == 0) return 0x05; - SHOW_IMAGE(showImage(chineseRect), 0); - RebuildRect(sortedRect, newSortedRect, specIndex); + vector newSortedRect; // 创建一个新的Rect向量,用于存储排序后的矩形区域 + newSortedRect.push_back(chineseRect); // 将中文字符的矩形区域添加到新的Rect向量中 + if (newSortedRect.size() == 0) return 0x05; // 如果新的矩形向量为空,返回错误代码 + + SHOW_IMAGE(showImage(chineseRect), 0); // 显示中文字符的矩形图像 + RebuildRect(sortedRect, newSortedRect, specIndex); // 根据特定矩形索引重建矩形数组 Mat theImage = channelImages.at(c_index); for (size_t i = 0; i < newSortedRect.size(); i++) { - Rect mr = newSortedRect[i]; - //mr = rectEnlarge(newSortedRect[i], cimage.cols, cimage.rows); - Mat auxRoi(theImage, mr); - Mat newRoi; - if (i == 0) { + Rect mr = newSortedRect[i]; // 获取当前矩形区域 + // mr = rectEnlarge(newSortedRect[i], cimage.cols, cimage.rows); // (未使用)可能是用以扩大矩形区域的函数 + Mat auxRoi(theImage, mr); // 从单通道图像中裁剪出当前矩形区域 + Mat newRoi; // 创建一个新Mat对象,用于存储处理后的图像 + if (i == 0) { // 如果是第一个矩形(通常是中文字符的矩形) //Rect large_mr = rectEnlarge(mr, theImage.cols, theImage.rows); Rect large_mr = mr; Mat grayChar(theImage, large_mr); @@ -865,6 +995,7 @@ int CCharsSegment::charsSegmentUsingMSER(Mat input, vector& resultVec, vect grayChars.push_back(newChineseRoi); } else { + //对不同的颜色做出对应的反应 switch (color) { case BLUE: threshold(auxRoi, newRoi, 0, 255, CV_THRESH_BINARY + CV_THRESH_OTSU); break; case YELLOW: threshold(auxRoi, newRoi, 0, 255, CV_THRESH_BINARY_INV + CV_THRESH_OTSU); break; @@ -888,169 +1019,196 @@ int CCharsSegment::charsSegmentUsingMSER(Mat input, vector& resultVec, vect int CCharsSegment::charsSegmentUsingOSTU(Mat input, vector& resultVec, vector& grayChars, 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 > 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 >::iterator itc = contours.begin(); - vector 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 sortedRect(vecRect); - std::sort(sortedRect.begin(), sortedRect.end(), - [](const Rect& r1, const Rect& r2) { return r1.x < r2.x; }); - - size_t specIndex = 0; - specIndex = GetSpecificRect(sortedRect); + // 判断输入图像是否为空 + if (!input.data) return 0x01; + + Color plateType = color; // 车牌颜色类型 + Mat input_grey; // 将要转换为灰度图的Mat + cvtColor(input, input_grey, CV_BGR2GRAY); // 将输入图像转换为灰度图 + + // 用于阈值分割的图像 + Mat img_threshold; + img_threshold = input_grey.clone(); // 克隆灰度图 + spatial_ostu(img_threshold, 8, 2, plateType); // 空间OSTU阈值分割 + + // 清除轮廓和水平线,并判断是否为车牌 + if (!clearLiuDing(img_threshold)) return 0x02; // 如果清理失败,返回错误码 + + // 用于查找轮廓的图像 + Mat img_contours; + img_threshold.copyTo(img_contours); // 将阈值图像复制给轮廓图 + + // 定义轮廓向量 + vector > contours; + // 查找轮廓 + findContours(img_contours, + contours, // 轮廓的向量 + CV_RETR_EXTERNAL, // 只检索外部轮廓 + CV_CHAIN_APPROX_NONE); // 存储所有轮廓点 + + // 迭代器遍历所有轮廓,寻找可能的字符轮廓 + vector >::iterator itc = contours.begin(); + vector 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; - Rect chineseRect; - if (specIndex < sortedRect.size()) - chineseRect = GetChineseRect(sortedRect[specIndex]); - else - return 0x04; + // 对字符矩形进行排序 + vector sortedRect(vecRect); + std::sort(sortedRect.begin(), sortedRect.end(), + [](const Rect& r1, const Rect& r2) { return r1.x < r2.x; }); - if (0) { - rectangle(img_threshold, chineseRect, Scalar(255)); - imshow("plate", img_threshold); - waitKey(0); - destroyWindow("plate"); - } + // 获取特定的字符矩形(例如车牌中的省份字符) + size_t specIndex = 0; + specIndex = GetSpecificRect(sortedRect); - vector newSortedRect; - newSortedRect.push_back(chineseRect); - RebuildRect(sortedRect, newSortedRect, specIndex); + // 定义中文字符矩形 + Rect chineseRect; + // 如果成功获取特定字符,计算中文字符的位置 + if (specIndex < sortedRect.size()) + chineseRect = GetChineseRect(sortedRect[specIndex]); + else + return 0x04; // 如果获取失败,返回错误码 - if (newSortedRect.size() == 0) return 0x05; + // 此处的if代码块用于调试,暂时被设置为不执行 + if (0) { + rectangle(img_threshold, chineseRect, Scalar(255)); // 在图像上画出中文字符的矩形 + imshow("plate", img_threshold); // 显示图像 + waitKey(0); // 等待按键 + destroyWindow("plate"); // 销毁窗口 + } - bool useSlideWindow = true; - bool useAdapThreshold = true; - //bool useAdapThreshold = CParams::instance()->getParam1b(); + // 利用中文字符的位置信息重建排序后的字符矩形 + vector newSortedRect; + newSortedRect.push_back(chineseRect); + RebuildRect(sortedRect, newSortedRect, specIndex); - for (size_t i = 0; i < newSortedRect.size(); i++) { - Rect mr = newSortedRect[i]; - Mat auxRoi(input_grey, mr); - Mat newRoi; + // 如果没有字符矩形,返回错误码 + if (newSortedRect.size() == 0) return 0x05; - if (i == 0) { - // genenrate gray chinese char - Rect large_mr = rectEnlarge(mr, input_grey.cols, input_grey.rows); - Mat grayChar(input_grey, large_mr); - Mat grayChinese; - grayChinese.create(kGrayCharHeight, kGrayCharWidth, CV_8UC1); - resize(grayChar, grayChinese, grayChinese.size(), 0, 0, INTER_LINEAR); - - Mat newChineseRoi; - if (useSlideWindow) { - float slideLengthRatio = 0.1f; - if (!slideChineseGrayWindow(input_grey, large_mr, newChineseRoi, plateType, slideLengthRatio)) - judgeChineseGray(grayChinese, newChineseRoi, plateType); - } - else { - judgeChinese(auxRoi, newRoi, plateType); - } - grayChars.push_back(newChineseRoi); - } - else { - switch (plateType) { - case BLUE: threshold(auxRoi, newRoi, 0, 255, CV_THRESH_BINARY + CV_THRESH_OTSU); break; - case YELLOW: threshold(auxRoi, newRoi, 0, 255, CV_THRESH_BINARY_INV + CV_THRESH_OTSU); break; - case WHITE: threshold(auxRoi, newRoi, 0, 255, CV_THRESH_OTSU + CV_THRESH_BINARY_INV); break; - default: threshold(auxRoi, newRoi, 0, 255, CV_THRESH_OTSU + CV_THRESH_BINARY); break; - } - newRoi = preprocessChar(newRoi); + // 初始化使用滑窗和自适应阈值的布尔变量 + bool useSlideWindow = true; + bool useAdapThreshold = true; + //bool useAdapThreshold = CParams::instance()->getParam1b(); // 可能的动态参数获取 - // genenrate gray chinese char - Rect fit_mr = rectFit(mr, input_grey.cols, input_grey.rows); - Mat grayChar(input_grey, fit_mr); - grayChars.push_back(grayChar); + // 遍历所有字符矩形,对其中的字符串进行预处理 + for (size_t i = 0; i < newSortedRect.size(); i++) { + Rect mr = newSortedRect[i]; + Mat auxRoi(input_grey, mr); // 从灰度图像中裁剪字符ROI + Mat newRoi; // 新的字符ROI + + // 如果是中文字符 + if (i == 0) { + // 扩大中文字符的矩形范围 + Rect large_mr = rectEnlarge(mr, input_grey.cols, input_grey.rows); + Mat grayChar(input_grey, large_mr); // 从灰度图像中裁剪扩大后的ROI + Mat grayChinese; + grayChinese.create(kGrayCharHeight, kGrayCharWidth, CV_8UC1); // 创建灰度中文字符的Mat + resize(grayChar, grayChinese, grayChinese.size(), 0, 0, INTER_LINEAR); // 调整大小 + + Mat newChineseRoi; + // 若使用滑动窗口 + if (useSlideWindow) { + float slideLengthRatio = 0.1f; // 滑动窗口的长度比例 + // 如果滑动窗口调整失败,尝试直接判断中文字符的灰度 + if (!slideChineseGrayWindow(input_grey, large_mr, newChineseRoi, plateType, slideLengthRatio)) + judgeChineseGray(grayChinese, newChineseRoi, plateType); + } + else { + // 如果不使用滑动窗口,直接判断中文字符 + judgeChinese(auxRoi, newRoi, plateType); + } + grayChars.push_back(newChineseRoi); // 添加到灰度字符向量中 + } + else { + // 对非中文字符进行二值化处理,根据车牌颜色,选择不同的二值化方法 + switch (plateType) { + case BLUE: threshold(auxRoi, newRoi, 0, 255, CV_THRESH_BINARY + CV_THRESH_OTSU); break; + case YELLOW: threshold(auxRoi, newRoi, 0, 255, CV_THRESH_BINARY_INV + CV_THRESH_OTSU); break; + case WHITE: threshold(auxRoi, newRoi, 0, 255, CV_THRESH_OTSU + CV_THRESH_BINARY_INV); break; + default: threshold(auxRoi, newRoi, 0, 255, CV_THRESH_OTSU + CV_THRESH_BINARY); break; + } + newRoi = preprocessChar(newRoi); // 对字符进行预处理 + + // 生成灰度字符 + Rect fit_mr = rectFit(mr, input_grey.cols, input_grey.rows); + Mat grayChar(input_grey, fit_mr); + grayChars.push_back(grayChar); // 添加到灰度字符向量中 + } + resultVec.push_back(newRoi); // 添加到结果字符向量中 } - resultVec.push_back(newRoi); - } - return 0; + return 0; // 处理成功,返回0 } Rect CCharsSegment::GetChineseRect(const Rect rectSpe) { - int height = rectSpe.height; - float newwidth = rectSpe.width * 1.15f; - int x = rectSpe.x; - int y = rectSpe.y; + int height = rectSpe.height; // 获取特定区域的高度 + float newwidth = rectSpe.width * 1.15f; // 将特定区域的宽度增加15% + int x = rectSpe.x; // 获取特定区域的x坐标 + int y = rectSpe.y; // 获取特定区域的y坐标 - int newx = x - int(newwidth * 1.15); - newx = newx > 0 ? newx : 0; + // 计算新区域的x坐标,同时确保其不小于0 + int newx = x - int(newwidth * 1.15); + newx = newx > 0 ? newx : 0; // 如果newx小于0,则将其设置为0 - Rect a(newx, y, int(newwidth), height); + // 创建一个新的Rect对象,新区域的宽度为原来的1.15倍 + Rect a(newx, y, int(newwidth), height); - return a; + return a; // 返回调整后的区域 } int CCharsSegment::GetSpecificRect(const vector& vecRect) { - vector xpositions; - int maxHeight = 0; - int maxWidth = 0; + vector xpositions; // 用来存储每个矩形的x坐标 + int maxHeight = 0; // 用来记录最大高度 + int maxWidth = 0; // 用来记录最大宽度 - for (size_t i = 0; i < vecRect.size(); i++) { - xpositions.push_back(vecRect[i].x); + // 遍历所有矩形以计算最大高度和宽度,同时记录x坐标 + for (size_t i = 0; i < vecRect.size(); i++) { + xpositions.push_back(vecRect[i].x); // 记录x坐标 - if (vecRect[i].height > maxHeight) { - maxHeight = vecRect[i].height; - } - if (vecRect[i].width > maxWidth) { - maxWidth = vecRect[i].width; + 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; - - // use prior knowledage to find the specific character - // position in 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; + int specIndex = 0; // 特定矩形的索引 + // 再次遍历所有矩形来找出特定的矩形 + for (size_t i = 0; i < vecRect.size(); i++) { + Rect mr = vecRect[i]; + // 计算中心x坐标 + int midx = mr.x + mr.width / 2; + + // 使用先前知识来找到宽度和高度都较大,并且在特定位置(1/7和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; + return specIndex; // 返回特定矩形的索引 } int CCharsSegment::RebuildRect(const vector& vecRect, - vector& outRect, int specIndex) { - int count = 6; - for (size_t i = specIndex; i < vecRect.size() && count; ++i, --count) { - outRect.push_back(vecRect[i]); - } + vector& outRect, int specIndex) { + int count = 6; // 需要从特定索引开始的矩形数量 + // 从特定索引开始,将后面的矩形加入到输出矩形数组中 + for (size_t i = specIndex; i < vecRect.size() && count; ++i, --count) { + outRect.push_back(vecRect[i]); // 将矩形加入输出数组 + } - return 0; + return 0; // 返回0,可能表示操作成功 } }