From b5a5b7dd0d477cc7a916d2031717b2c5b7529227 Mon Sep 17 00:00:00 2001 From: heureux <1739570939@qq.com> Date: Mon, 4 Dec 2023 20:38:42 +0800 Subject: [PATCH 1/2] zhaohaoyi --- src/util/kv.cpp | 52 ++++++++++++++++++++++++------------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/src/util/kv.cpp b/src/util/kv.cpp index 84ee6c9..edacd3e 100644 --- a/src/util/kv.cpp +++ b/src/util/kv.cpp @@ -3,11 +3,11 @@ namespace easypr { -Kv::Kv() { } +Kv::Kv() { } //Kv类的构造函数 -void Kv::load(const std::string &file) { - this->clear(); - std::ifstream reader(file); +void Kv::load(const std::string &file) { //一个成员函数,用于从指定的文件中加载键值对 + this->clear();//清空当前的键值对 + std::ifstream reader(file);//打开文件 assert(reader); if (reader.is_open()) { @@ -15,17 +15,17 @@ void Kv::load(const std::string &file) { std::string line; std::getline(reader, line); if (line.empty()) continue; - - const auto parse = [](const std::string &str) { +//打开文件并读取每一行 + const auto parse = [](const std::string &str) {//解析键和值 std::string tmp, key, value; for (size_t i = 0, len = str.length(); i < len; ++i) { const char ch = str[i]; - if (ch == ' ') { + if (ch == ' ') {//遇到空格时 if (i > 0 && str[i - 1] != ' ' && key.empty()) { key = tmp; tmp.clear(); } - } + }//将之前的字符串作为键,空格后的字符串作为值 else { tmp.push_back(ch); } @@ -37,45 +37,45 @@ void Kv::load(const std::string &file) { }; auto kv = parse(line); - this->add(kv.first, kv.second); + this->add(kv.first, kv.second);//解析出的键值对添加到存储中 } - reader.close(); + reader.close();//关闭文件 } } -std::string Kv::get(const std::string &key) { +std::string Kv::get(const std::string &key) {//是一个成员函数,用于获取给定键的值 if (data_.find(key) == data_.end()) { - std::cerr << "[Kv] cannot find " << key << std::endl; - return ""; + std::cerr << "[Kv] cannot find " << key << std::endl;//如果键不存在,它会打印一个错误消息 + return "";//返回一个空字符串。 } - return data_.at(key); + return data_.at(key);//获取给定键的值 } -void Kv::add(const std::string &key, const std::string &value) { - if (data_.find(key) != data_.end()) { +void Kv::add(const std::string &key, const std::string &value) {//是一个成员函数,用于添加一个键值对 + if (data_.find(key) != data_.end()) {//如果键已经存在 fprintf(stderr, "[Kv] find duplicate: %s = %s , ignore\n", key.c_str(), - value.c_str()); + value.c_str());//会打印一个错误消息并忽略这个添加操作 } else { std::string v(value); -#ifdef OS_WINDOWS - v = utils::utf8_to_gbk(value.c_str()); +#ifdef OS_WINDOWS//如果在Windows操作系统上 + v = utils::utf8_to_gbk(value.c_str()); //将值从UTF-8编码转换为GBK编码。 #endif - data_[key] = v; + data_[key] = v;//添加一个键值对 } } -void Kv::remove(const std::string &key) { - if (data_.find(key) == data_.end()) { - std::cerr << "[Kv] cannot find " << key << std::endl; +void Kv::remove(const std::string &key) {//是一个成员函数,用于删除一个键值对。 + if (data_.find(key) == data_.end()) {//如果键不存在 + std::cerr << "[Kv] cannot find " << key << std::endl;//打印一个错误消息。 return; } - data_.erase(key); + data_.erase(key);//删除一个键值对。 } -void Kv::clear() { - data_.clear(); +void Kv::clear() {//是一个成员函数,用于清空所有的键值对。 + data_.clear();//用于清空所有的键值对。 } } From c9acbeb31a88dd7a62a5093c352a2d577a5df723 Mon Sep 17 00:00:00 2001 From: golden <865566449@qq.com> Date: Mon, 4 Dec 2023 21:11:47 +0800 Subject: [PATCH 2/2] chars_segment --- src/core/chars_segment.cpp | 375 ++++++++++++++++++++++--------------- 1 file changed, 226 insertions(+), 149 deletions(-) diff --git a/src/core/chars_segment.cpp b/src/core/chars_segment.cpp index 5265585..c40d38e 100644 --- a/src/core/chars_segment.cpp +++ b/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,75 +19,75 @@ CCharsSegment::CCharsSegment() { m_WhitePercent = DEFAULT_WHITEPERCEMT; m_debug = DEFAULT_DEBUG; -}//üijԱгʼ +}//设置几个类的成员变量,进行初始化 bool CCharsSegment::verifyCharSizes(Mat r) { // Char sizes 45x90 - //һOpenCVMatΪ丳ֵr - 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 minAspect = 0.05f//最小允许宽高比 float maxAspect = aspect + aspect * error - //߱ȣԤڵĿ߱ȼó + //最大允许宽高比,由预期的宽高比加上其误差得出 // area of pixels int area = cv::countNonZero(r); - // ͼķ + // 输入图像的非零像素数 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Ϊ룬ͼһЩԤ + //接收一个Mat对象作为输入,它对输入的图像进行一些预处理操作。 int h = in.rows; int w = in.cols; - //ȡͼĸ߶ȺͿ + //获取输入图像的高度和宽度 int charSize = CHAR_SIZE; Mat transformMat = Mat::eye(2, 3, CV_32F); - //һ2x3ĵλΪ任ͼļα任 + //定义一个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Ӧ÷任µı任ͼ + //warpAffine函数应用仿射变换,生成新的变换后的图像。 resize(warpImage, out, Size(charSize, charSize)); - //resizeͼijߴԤַС + //resize函数调整新图像的尺寸至预设的字符大小。 return out; } //! choose the bese threshold method for chinese void CCharsSegment::judgeChinese(Mat in, Mat& out, Color plateType) -{ //һMatΪ룬ֵʶַ +{ //接收一个Mat对象作为输入,并对其进行阈值处理以识别中文字符 Mat auxRoi = in; - //ʾͼһпܰҪʶַ + //表示输入图像的一个子区域,其中可能包含要识别的中文字符 float valOstu = -1.f, valAdap = -1.f; Mat roiOstu, roiAdap; - //ΪOtsuֵӦֱֵһvalOstuvalAdap - // ڴ洢ֵ + //为Otsu阈值法和自适应阈值法分别定义了一个变量valOstu和valAdap, + // 用于存储阈值 bool isChinese = true; if (1) { if (BLUE == plateType) { @@ -102,23 +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 + //根据输入的颜色类型,使用不同的阈值处理方法对auxRoi进行阈值处理 + // 生成二值化的图像roiOstu roiOstu = preprocessChar(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); @@ -130,7 +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); } @@ -146,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(); - //ȡmrϽǵ㣬ֵtlPoint - bool isChinese = true;//ͼǷַ + //获取mr的左上角点,并赋值给tlPoint + bool isChinese = true;//标记图像中是否含有中文字符 int slideLength = int(slideLengthRatio * maxrect.width); - int slideStep = 1;//ƻڵIJ - int fromX = 0;//ָĸλÿʼ - fromX = tlPoint.x;//ʵʵʼλϽǵx + int slideStep = 1;//控制滑动窗口的步长 + int fromX = 0;//指定从哪个位置开始滑动窗口 + fromX = tlPoint.x;//实际的起始位置是左上角的x坐标 for (int slideX = -slideLength; slideX < slideLength; slideX += slideStep) { float x_slide = 0; @@ -167,21 +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 + //原始图像中提取出这个矩形区域的子图像auxRoi Mat roiOstu, roiAdap; if (1) { if (BLUE == plateType) { @@ -196,15 +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); } - //Ԥɫʹòֵͬȡͼֵ - // ͼֱ𱣴浽roiOsturoiAdap + //根据预设的颜色,使用不同的阈值处理方法对提取出的子图像进行阈值处理。 + // 处理后的图像分别保存到roiOstu和roiAdap。 roiOstu = preprocessChar(roiOstu, kChineseSize); CCharacter charCandidateOstu; - charCandidateOstu.setCharacterPos(rect); // ַλϢ - charCandidateOstu.setCharacterMat(roiOstu);// ַͼϢ - charCandidateOstu.setIsChinese(isChinese);// ַǷΪַϢ - charCandidateVec.push_back(charCandidateOstu);// ַϢӵַѡԱʹá + charCandidateOstu.setCharacterPos(rect); // 设置字符的位置信息 + charCandidateOstu.setCharacterMat(roiOstu);// 设置字符的图像信息 + charCandidateOstu.setIsChinese(isChinese);// 设置字符是否为中文字符的信息 + charCandidateVec.push_back(charCandidateOstu);// 将字符信息添加到字符候选向量中以备后续处理使用。 } if (useAdapThreshold) { if (BLUE == plateType) { @@ -220,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С + //对图像进行阈值处理以获得二值化的图像。处理后的图像保存在roiAdap中。 CCharacter charCandidateAdap; charCandidateAdap.setCharacterPos(rect); charCandidateAdap.setCharacterMat(roiAdap); @@ -234,8 +234,8 @@ bool slideChineseWindow(Mat& image, Rect mr, Mat& newRoi, Color plateType, float double overlapThresh = 0.1; NMStoCharacter(charCandidateVec, overlapThresh); - //ʹ˷ǼֵƣNMS㷨ͨÿַѡַѡĽ - // ȵijֵַѡȥ + //使用了非极大值抑制(NMS)算法,它通过计算每个字符候选区域与其它所有字符候选区域的交并比 + // 将交并比低于某个阈值的字符候选区域去除 if (charCandidateVec.size() >= 1) { std::sort(charCandidateVec.begin(), charCandidateVec.end(), [](const CCharacter& r1, const CCharacter& r2) { @@ -245,60 +245,60 @@ bool slideChineseWindow(Mat& image, Rect mr, Mat& newRoi, Color plateType, float newRoi = charCandidateVec.at(0).getCharacterMat(); return true; } - //ɵַѡһԪأ - // 򽫵÷ߵַѡͼȡtrue򷵻false + //如果生成的字符候选向量中至少有一个元素, + // 则将得分最高的字符候选区域的图像提取出来,并返回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; - // εĿȼ㻬ڵijȺͲ + // 根据最大矩形的宽度计算滑动窗口的长度和步长 for (int slideX = -slideLength; slideX < slideLength; slideX += slideStep) { - // ָķΧڽлڲ + // 在指定的范围内进行滑动窗口操作 float x_slide = 0; x_slide = float(fromX + slideX); float y_slide = (float)tlPoint.y; - // 㵱ǰxy + // 计算当前滑动的x和y坐标 int chineseWidth = int(maxrect.width); int chineseHeight = int(maxrect.height); - // ȡйַĿȺ͸߶ + // 获取中国字符的宽度和高度 Rect rect(Point2f(x_slide, y_slide), Size(chineseWidth, chineseHeight)); - // ݵǰйַijߴ紴һ + // 根据当前滑动的坐标和中国字符的尺寸创建一个矩形区域 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); - // һҶͼ񣬳ߴΪԤַijߴͨ + // 创建一个灰度图像,尺寸为预设的中文字符的尺寸和通道数 resize(auxRoi, grayChinese, grayChinese.size(), 0, 0, INTER_LINEAR); - // ȡͼΪԤַijߴ磬䱣浽Ҷͼ - // ͼȡǰͼ + // 将提取的子图像调整为预设的中文字符的尺寸,并将其保存到灰度图像中 + // 从图像中提取当前矩形区域的子图像 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ʵҪе + // 对所有的字符候选进行非极大值抑制(NMS)以消除多余的字符候选区域, + // 这里的阈值设置为0.1(根据实际情况可能需要进行调整) if (charCandidateVec.size() >= 1) { std::sort(charCandidateVec.begin(), charCandidateVec.end(), [](const CCharacter& r1, const CCharacter& r2) { @@ -309,23 +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ʽΪ - // ַѡĵ÷ֽн + // 如果字符候选向量中至少有一个元素,则对它们进行排序, + // 并提取出得分最高的字符候选区域,将其图像保存到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); @@ -336,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); @@ -346,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; } @@ -371,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; @@ -390,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; @@ -402,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; @@ -431,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); @@ -449,6 +470,9 @@ int CCharsSegment::charsSegment(Mat input, vector& resultVec, Color color) } resultVec.push_back(newRoi); + // 将新的Roi添加到名为resultVec的向量中。 + // 存储一系列元素。这里将新的Roi图像添加到该向量中。 + } return 0; @@ -457,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(白色) } } } @@ -506,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; @@ -581,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; @@ -595,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; @@ -625,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; } }