diff --git a/doc/EasyPR泛读报告.docx b/doc/EasyPR泛读报告.docx new file mode 100644 index 0000000..5e53785 Binary files /dev/null and b/doc/EasyPR泛读报告.docx differ diff --git a/doc/项目名称+泛读报告.docx b/doc/项目名称+泛读报告.docx deleted file mode 100644 index e69de29..0000000 diff --git a/src/src/core/chars_identify.cpp b/src/src/core/chars_identify.cpp index a88d727..306cae1 100644 --- a/src/src/core/chars_identify.cpp +++ b/src/src/core/chars_identify.cpp @@ -7,448 +7,595 @@ using namespace cv; -namespace easypr { +namespace easypr +{ -CharsIdentify* CharsIdentify::instance_ = nullptr; + CharsIdentify *CharsIdentify::instance_ = nullptr; -CharsIdentify* CharsIdentify::instance() { - if (!instance_) { - instance_ = new CharsIdentify; + CharsIdentify *CharsIdentify::instance() + { + if (!instance_) + { + instance_ = new CharsIdentify; + } + return instance_; } - return instance_; -} -CharsIdentify::CharsIdentify() { - LOAD_ANN_MODEL(ann_, kDefaultAnnPath); - LOAD_ANN_MODEL(annChinese_, kChineseAnnPath); - LOAD_ANN_MODEL(annGray_, kGrayAnnPath); + // 主要用于加载和管理预训练的神经网络模型,用于字符识别 + CharsIdentify::CharsIdentify() + { + LOAD_ANN_MODEL(ann_, kDefaultAnnPath); + LOAD_ANN_MODEL(annChinese_, kChineseAnnPath); + LOAD_ANN_MODEL(annGray_, kGrayAnnPath); - kv_ = std::shared_ptr(new Kv); - kv_->load(kChineseMappingPath); + kv_ = std::shared_ptr(new Kv); + kv_->load(kChineseMappingPath); - extractFeature = getGrayPlusProject; -} + extractFeature = getGrayPlusProject; + } -void CharsIdentify::LoadModel(std::string path) { - if (path != std::string(kDefaultAnnPath)) { - if (!ann_->empty()) - ann_->clear(); - LOAD_ANN_MODEL(ann_, path); + void CharsIdentify::LoadModel(std::string path) + { + if (path != std::string(kDefaultAnnPath)) + { + if (!ann_->empty()) + ann_->clear(); + LOAD_ANN_MODEL(ann_, path); + } } -} -void CharsIdentify::LoadChineseModel(std::string path) { - if (path != std::string(kChineseAnnPath)) { - if (!annChinese_->empty()) - annChinese_->clear(); - LOAD_ANN_MODEL(annChinese_, path); + void CharsIdentify::LoadChineseModel(std::string path) + { + if (path != std::string(kChineseAnnPath)) + { + if (!annChinese_->empty()) + annChinese_->clear(); + LOAD_ANN_MODEL(annChinese_, path); + } } -} -void CharsIdentify::LoadGrayChANN(std::string path) { - if (path != std::string(kGrayAnnPath)) { - if (!annGray_->empty()) - annGray_->clear(); - LOAD_ANN_MODEL(annGray_, path); + void CharsIdentify::LoadGrayChANN(std::string path) + { + if (path != std::string(kGrayAnnPath)) + { + if (!annGray_->empty()) + annGray_->clear(); + LOAD_ANN_MODEL(annGray_, path); + } } -} -void CharsIdentify::LoadChineseMapping(std::string path) { - kv_->clear(); - kv_->load(path); -} + void CharsIdentify::LoadChineseMapping(std::string path) + { + kv_->clear(); + kv_->load(path); + } -void CharsIdentify::classify(cv::Mat featureRows, std::vector& out_maxIndexs, - std::vector& out_maxVals, std::vector isChineseVec){ - int rowNum = featureRows.rows; + // 对输入的特征行进行预测,并识别出最可能的字符。 + void CharsIdentify::classify(cv::Mat featureRows, std::vector &out_maxIndexs, + std::vector &out_maxVals, std::vector isChineseVec) + { + // 获取特征行的行数。 + int rowNum = featureRows.rows; + // 创建一个新的矩阵output,大小为特征行的行数(rowNum)乘以总的字符数量 + cv::Mat output(rowNum, kCharsTotalNumber, CV_32FC1); + // 使用预先训练好的模型(ann_)对输入的特征行进行预测,结果保存在output矩阵中 + ann_->predict(featureRows, output); + // 循环遍历每一行输出: + // 对于每一行,首先获取该行的预测结果 + for (int output_index = 0; output_index < rowNum; output_index++) + { + Mat output_row = output.row(output_index); + int result = 0; + float maxVal = -2.f; + bool isChinses = isChineseVec[output_index]; + // 如果该行不是中文字符(由isChineseVec向量确定), + // 则遍历前kCharactersNumber个预测结果,找出值最大的那个,并记录其索引和值。 + if (!isChinses) + { + result = 0; + for (int j = 0; j < kCharactersNumber; j++) + { + float val = output_row.at(j); + // std::cout << "j:" << j << "val:" << val << std::endl; + if (val > maxVal) + { + maxVal = val; + result = j; + } + } + } + // 如果该行是中文字符, + // 则从kCharactersNumber开始遍历后面的预测结果,找出值最大的那个,并记录其索引和值。 + else + { + result = kCharactersNumber; + for (int j = kCharactersNumber; j < kCharsTotalNumber; j++) + { + float val = output_row.at(j); + // std::cout << "j:" << j << "val:" << val << std::endl; + if (val > maxVal) + { + maxVal = val; + result = j; + } + } + } + // 将记录的最大索引和最大值分别赋值给out_maxIndexs和out_maxVals的相应位置 + out_maxIndexs[output_index] = result; + out_maxVals[output_index] = maxVal; + } + } - cv::Mat output(rowNum, kCharsTotalNumber, CV_32FC1); - ann_->predict(featureRows, output); + // 接受一个CCharacter类型的向量(charVec),并对每个字符进行分类。 + void CharsIdentify::classify(std::vector &charVec) + { + size_t charVecSize = charVec.size(); + + if (charVecSize == 0) + return; + + // 创建一个名为featureRows的Mat对象,并通过循环将每个字符的特征提取出来并添加到featureRows中。 + Mat featureRows; + for (size_t index = 0; index < charVecSize; index++) + { + Mat charInput = charVec[index].getCharacterMat(); + Mat feature = charFeatures(charInput, kPredictSize); + featureRows.push_back(feature); + } - for (int output_index = 0; output_index < rowNum; output_index++) { - Mat output_row = output.row(output_index); - int result = 0; - float maxVal = -2.f; - bool isChinses = isChineseVec[output_index]; - if (!isChinses) { - result = 0; - for (int j = 0; j < kCharactersNumber; j++) { - float val = output_row.at(j); - // std::cout << "j:" << j << "val:" << val << std::endl; - if (val > maxVal) { - maxVal = val; - result = j; + // 创建一个输出矩阵output,并使用预先训练好的模型(ann_)对特征进行预测。 + cv::Mat output(charVecSize, kCharsTotalNumber, CV_32FC1); + ann_->predict(featureRows, output); + + // 遍历每个输出,对于每个输出,首先获取对应的字符(通过索引), + // 然后获取该字符的预测结果行(通过索引)。然后,函数检查该字符是否为中文字符, + // 如果不是,它就在循环中找出值最大的预测结果,并记录其索引和值。 + // 最后,函数根据这个最大值和索引确定预测的字符,并将其作为标签。 + for (size_t output_index = 0; output_index < charVecSize; output_index++) + { + CCharacter &character = charVec[output_index]; + Mat output_row = output.row(output_index); + + int result = 0; + float maxVal = -2.f; + std::string label = ""; + + bool isChinses = character.getIsChinese(); + if (!isChinses) + { + result = 0; + for (int j = 0; j < kCharactersNumber; j++) + { + float val = output_row.at(j); + // std::cout << "j:" << j << "val:" << val << std::endl; + if (val > maxVal) + { + maxVal = val; + result = j; + } + } + label = std::make_pair(kChars[result], kChars[result]).second; + } + // 如果字符是中文字符,函数则从预测结果的后面部分开始查找最大值,并记录其索引和值。 + // 然后,函数根据这个最大值和索引确定预测的字符,并通过键值对(kv_)查找对应的省份,将字符和省份作为标签。 + else + { + result = kCharactersNumber; + for (int j = kCharactersNumber; j < kCharsTotalNumber; j++) + { + float val = output_row.at(j); + // std::cout << "j:" << j << "val:" << val << std::endl; + if (val > maxVal) + { + maxVal = val; + result = j; + } } + const char *key = kChars[result]; + std::string s = key; + std::string province = kv_->get(s); + label = std::make_pair(s, province).second; } + /*std::cout << "result:" << result << std::endl; + std::cout << "maxVal:" << maxVal << std::endl;*/ + + // 函数将预测的最大值和标签分别设置到对应字符对象的得分和字符串属性中。 + character.setCharacterScore(maxVal); + character.setCharacterStr(label); } - else { - result = kCharactersNumber; - for (int j = kCharactersNumber; j < kCharsTotalNumber; j++) { + } + + // 对输入的中文字符进行分类 + void CharsIdentify::classifyChineseGray(std::vector &charVec) + { + size_t charVecSize = charVec.size(); + if (charVecSize == 0) + return; + + Mat featureRows; + // 通过循环提取每个字符的特征,并存储在featureRows中 + for (size_t index = 0; index < charVecSize; index++) + { + Mat charInput = charVec[index].getCharacterMat(); + cv::Mat feature; + extractFeature(charInput, feature); + featureRows.push_back(feature); + } + // 创建一个输出矩阵(output),然后使用预先训练好的模型(annGray_)对特征进行预测,并将结果存储在output中 + cv::Mat output(charVecSize, kChineseNumber, CV_32FC1); + annGray_->predict(featureRows, output); + + // 对于输出矩阵中的每一行(每个字符的预测结果), + // 如果该字符是中文字符,函数会从预测结果的后面部分开始查找最大值,并记录其索引和值。 + for (size_t output_index = 0; output_index < charVecSize; output_index++) + { + CCharacter &character = charVec[output_index]; + Mat output_row = output.row(output_index); + bool isChinese = true; + + float maxVal = -2; + int result = 0; + + for (int j = 0; j < kChineseNumber; j++) + { float val = output_row.at(j); - //std::cout << "j:" << j << "val:" << val << std::endl; - if (val > maxVal) { + // std::cout << "j:" << j << "val:" << val << std::endl; + if (val > maxVal) + { maxVal = val; result = j; } } - } - out_maxIndexs[output_index] = result; - out_maxVals[output_index] = maxVal; - } -} + // no match + if (-1 == result) + { + result = 0; + maxVal = 0; + isChinese = false; + } -void CharsIdentify::classify(std::vector& charVec){ - size_t charVecSize = charVec.size(); + // 根据这个最大值和索引确定预测的字符。 + // 这是通过查找kChars数组实现的,其中kChars可能是一个预定义的字符集。 + auto index = result + kCharsTotalNumber - kChineseNumber; + const char *key = kChars[index]; + std::string s = key; + std::string province = kv_->get(s); - if (charVecSize == 0) - return; + /*std::cout << "result:" << result << std::endl; + std::cout << "maxVal:" << maxVal << std::endl;*/ - Mat featureRows; - for (size_t index = 0; index < charVecSize; index++) { - Mat charInput = charVec[index].getCharacterMat(); - Mat feature = charFeatures(charInput, kPredictSize); - featureRows.push_back(feature); + // 将预测的最大值、预测的字符以及对应的省份作为标签, + // 分别设置到对应字符对象的得分、字符串属性和是否为中文字符属性中 + character.setCharacterScore(maxVal); + character.setCharacterStr(province); + character.setIsChinese(isChinese); + } } - cv::Mat output(charVecSize, kCharsTotalNumber, CV_32FC1); - ann_->predict(featureRows, output); + // 使用OpenCV库和神经网络进行中文字符识别 + void CharsIdentify::classifyChinese(std::vector &charVec) + { + size_t charVecSize = charVec.size(); + + if (charVecSize == 0) + return; + + Mat featureRows; + // 通过循环遍历每个字符,提取其特征并将其存储在featureRows中。 + // 这里,charFeatures函数被用于提取每个字符的特性,kChineseSize可能是一个预定义的特性大小。 + for (size_t index = 0; index < charVecSize; index++) + { + Mat charInput = charVec[index].getCharacterMat(); + Mat feature = charFeatures(charInput, kChineseSize); + featureRows.push_back(feature); + } - for (size_t output_index = 0; output_index < charVecSize; output_index++) { - CCharacter& character = charVec[output_index]; - Mat output_row = output.row(output_index); + // 创建一个输出矩阵(output),并使用预先训练好的模型(annChinese_)对特征进行预测。预测结果存储在output中。 + cv::Mat output(charVecSize, kChineseNumber, CV_32FC1); + annChinese_->predict(featureRows, output); - int result = 0; - float maxVal = -2.f; - std::string label = ""; + // 遍历每个预测结果,并对每个结果进行处理。对于每个预测结果,函数查找最大值及其索引。 + // 如果最大值小于或等于-1,则将最大值设置为0,并将result设置为0,同时将isChinese设置为false。 + for (size_t output_index = 0; output_index < charVecSize; output_index++) + { + CCharacter &character = charVec[output_index]; + Mat output_row = output.row(output_index); + bool isChinese = true; - bool isChinses = character.getIsChinese(); - if (!isChinses) { - result = 0; - for (int j = 0; j < kCharactersNumber; j++) { + float maxVal = -2; + int result = 0; + + for (int j = 0; j < kChineseNumber; j++) + { float val = output_row.at(j); - //std::cout << "j:" << j << "val:" << val << std::endl; - if (val > maxVal) { + // std::cout << "j:" << j << "val:" << val << std::endl; + if (val > maxVal) + { maxVal = val; result = j; } } - label = std::make_pair(kChars[result], kChars[result]).second; + + // no match + if (-1 == result) + { + result = 0; + maxVal = 0; + isChinese = false; + } + + // 计算索引值,并使用该索引从kChars数组中获取对应的字符。 + // 同时,通过键值对(kv_)查找与该字符对应的省份。 + auto index = result + kCharsTotalNumber - kChineseNumber; + const char *key = kChars[index]; + std::string s = key; + std::string province = kv_->get(s); + + /*std::cout << "result:" << result << std::endl; + std::cout << "maxVal:" << maxVal << std::endl;*/ + + // 将最大值、省份和isChinese作为标签,分别设置到对应字符对象的得分、字符串属性和是否为中文字符属性中。 + character.setCharacterScore(maxVal); + character.setCharacterStr(province); + character.setIsChinese(isChinese); } - else { + } + + // 对输入的图像数据进行分类 + int CharsIdentify::classify(cv::Mat f, float &maxVal, bool isChinses, bool isAlphabet) + { + int result = 0; + // 调用预先训练好的模型(ann_)进行预测,并将预测结果存储在output变量中。 + cv::Mat output(1, kCharsTotalNumber, CV_32FC1); + ann_->predict(f, output); + + // 查找最大值及其索引。如果图像数据不是中文,则会检查它是否是字母。 + // 如果它是字母,那么函数将只查找字母范围内的值(从10开始,对应于'A')。 + // 否则,它将查找所有字符范围内的值。如果图像数据是中文,则函数将查找中文字符范围内的值 + maxVal = -2.f; + if (!isChinses) + { + if (!isAlphabet) + { + result = 0; + for (int j = 0; j < kCharactersNumber; j++) + { + float val = output.at(j); + // std::cout << "j:" << j << "val:" << val << std::endl; + if (val > maxVal) + { + maxVal = val; + result = j; + } + } + } + else + { + result = 0; + // begin with 11th char, which is 'A' + for (int j = 10; j < kCharactersNumber; j++) + { + float val = output.at(j); + // std::cout << "j:" << j << "val:" << val << std::endl; + if (val > maxVal) + { + maxVal = val; + result = j; + } + } + } + } + else + { result = kCharactersNumber; - for (int j = kCharactersNumber; j < kCharsTotalNumber; j++) { - float val = output_row.at(j); - //std::cout << "j:" << j << "val:" << val << std::endl; - if (val > maxVal) { + for (int j = kCharactersNumber; j < kCharsTotalNumber; j++) + { + float val = output.at(j); + // std::cout << "j:" << j << "val:" << val << std::endl; + if (val > maxVal) + { maxVal = val; result = j; } } - const char* key = kChars[result]; - std::string s = key; - std::string province = kv_->get(s); - label = std::make_pair(s, province).second; } - /*std::cout << "result:" << result << std::endl; - std::cout << "maxVal:" << maxVal << std::endl;*/ - character.setCharacterScore(maxVal); - character.setCharacterStr(label); + // 返回索引值result,该值是预测的字符在预先定义的字符集kChars中的索引。 + // 同时,它也将最大值maxVal和对应的索引result设置到输入的float引用maxVal中,以便调用者可以访问这些值。 + // std::cout << "maxVal:" << maxVal << std::endl; + return result; } -} - + // 根据输入的图像数据判断它是否是一个字符(特别是中文字符) + bool CharsIdentify::isCharacter(cv::Mat input, std::string &label, float &maxVal, bool isChinese) + { + // 调用charFeatures函数提取输入图像的特征,并存储在feature变量中。 + // 然后,它调用classify函数对特征进行分类,得到一个索引值index + cv::Mat feature = charFeatures(input, kPredictSize); + auto index = static_cast(classify(feature, maxVal, isChinese)); -void CharsIdentify::classifyChineseGray(std::vector& charVec){ - size_t charVecSize = charVec.size(); - if (charVecSize == 0) - return; + if (isChinese) + { + // std::cout << "maxVal:" << maxVal << std::endl; + } - Mat featureRows; - for (size_t index = 0; index < charVecSize; index++) { - Mat charInput = charVec[index].getCharacterMat(); - cv::Mat feature; - extractFeature(charInput, feature); - featureRows.push_back(feature); + float chineseMaxThresh = 0.2f; + // 检查预测的最大值maxVal是否大于等于0.9,或者如果输入的字符是中文且最大值大于等于chineseMaxThresh(这个阈值被设置为0.2)。 + // 如果满足这些条件之一,函数将检查索引index是否小于kCharactersNumber(这可能是一个预定义的字符集大小)。 + // 如果是,则将索引对应的字符作为标签;否则,使用键值对kv_查找索引对应的省份,并将该索引对应的字符和省份作为标签。 + // 最后,函数返回true表示输入的图像是一个字符,否则返回false + if (maxVal >= 0.9 || (isChinese && maxVal >= chineseMaxThresh)) + { + if (index < kCharactersNumber) + { + label = std::make_pair(kChars[index], kChars[index]).second; + } + else + { + const char *key = kChars[index]; + std::string s = key; + std::string province = kv_->get(s); + label = std::make_pair(s, province).second; + } + return true; + } + else + return false; } - - cv::Mat output(charVecSize, kChineseNumber, CV_32FC1); - annGray_->predict(featureRows, output); - - for (size_t output_index = 0; output_index < charVecSize; output_index++) { - CCharacter& character = charVec[output_index]; - Mat output_row = output.row(output_index); - bool isChinese = true; - + // 用于识别输入的图像数据是否是一个中文字符。 + std::pair CharsIdentify::identifyChinese(cv::Mat input, float &out, bool &isChinese) + { + // 调用charFeatures函数提取输入图像的特征,并存储在feature变量中。 + // 然后,它调用预先训练好的模型annChinese_进行预测,并将预测结果存储在output变量中。 + cv::Mat feature = charFeatures(input, kChineseSize); float maxVal = -2; int result = 0; - for (int j = 0; j < kChineseNumber; j++) { - float val = output_row.at(j); - //std::cout << "j:" << j << "val:" << val << std::endl; - if (val > maxVal) { + cv::Mat output(1, kChineseNumber, CV_32FC1); + annChinese_->predict(feature, output); + + // 遍历输出数组,找到最大的值及其索引。 + // 如果最大值大于0.9,则将isChinese设置为true,表示输入的字符可能是中文。 + for (int j = 0; j < kChineseNumber; j++) + { + float val = output.at(j); + // std::cout << "j:" << j << "val:" << val << std::endl; + if (val > maxVal) + { maxVal = val; result = j; } } // no match - if (-1 == result) { + // 如果索引值为-1(即没有匹配的字符), + // 则将result设置为0,maxVal设置为0,并将isChinese设置为false,表示输入的字符不是中文。 + if (-1 == result) + { result = 0; maxVal = 0; isChinese = false; } - + else if (maxVal > 0.9) + { + isChinese = true; + } + // 通过索引值获取字符的标签和省份,并将最大值保存到out中。函数返回一个由字符标签和省份组成的pair。 auto index = result + kCharsTotalNumber - kChineseNumber; - const char* key = kChars[index]; + const char *key = kChars[index]; std::string s = key; std::string province = kv_->get(s); + out = maxVal; - /*std::cout << "result:" << result << std::endl; - std::cout << "maxVal:" << maxVal << std::endl;*/ - - character.setCharacterScore(maxVal); - character.setCharacterStr(province); - character.setIsChinese(isChinese); - } -} - -void CharsIdentify::classifyChinese(std::vector& charVec){ - size_t charVecSize = charVec.size(); - - if (charVecSize == 0) - return; - - Mat featureRows; - for (size_t index = 0; index < charVecSize; index++) { - Mat charInput = charVec[index].getCharacterMat(); - Mat feature = charFeatures(charInput, kChineseSize); - featureRows.push_back(feature); + return std::make_pair(s, province); } - - cv::Mat output(charVecSize, kChineseNumber, CV_32FC1); - annChinese_->predict(featureRows, output); - - for (size_t output_index = 0; output_index < charVecSize; output_index++) { - CCharacter& character = charVec[output_index]; - Mat output_row = output.row(output_index); - bool isChinese = true; - + // 从输入的图像(可能是一个灰度图像)中识别出可能的中文字符。 + std::pair CharsIdentify::identifyChineseGray(cv::Mat input, float &out, bool &isChinese) + { + cv::Mat feature; + // 通过extractFeature函数提取输入图像的特征,并将特征保存在feature变量中。 + // 然后,它使用预先训练好的模型annGray_进行预测,并将预测结果存储在output变量中。 + extractFeature(input, feature); float maxVal = -2; int result = 0; - - for (int j = 0; j < kChineseNumber; j++) { - float val = output_row.at(j); - //std::cout << "j:" << j << "val:" << val << std::endl; - if (val > maxVal) { + cv::Mat output(1, kChineseNumber, CV_32FC1); + annGray_->predict(feature, output); + // 遍历输出数组,找到最大的值及其索引。 + // 如果最大值大于0.9,则将isChinese设置为true,表示输入的字符可能是中文。 + for (int j = 0; j < kChineseNumber; j++) + { + float val = output.at(j); + // std::cout << "j:" << j << "val:" << val << std::endl; + if (val > maxVal) + { maxVal = val; result = j; } } - // no match - if (-1 == result) { + // 如果索引值为-1(即没有匹配的字符), + // 则将result设置为0,maxVal设置为0,并将isChinese设置为false,表示输入的字符不是中文 + if (-1 == result) + { result = 0; maxVal = 0; isChinese = false; } - + else if (maxVal > 0.9) + { + isChinese = true; + } + // 通过索引值获取字符的标签和省份,并将最大值保存到out中。函数返回一个由字符标签和省份组成的pair。 auto index = result + kCharsTotalNumber - kChineseNumber; - const char* key = kChars[index]; + const char *key = kChars[index]; std::string s = key; std::string province = kv_->get(s); - - /*std::cout << "result:" << result << std::endl; - std::cout << "maxVal:" << maxVal << std::endl;*/ - - character.setCharacterScore(maxVal); - character.setCharacterStr(province); - character.setIsChinese(isChinese); - } -} - -int CharsIdentify::classify(cv::Mat f, float& maxVal, bool isChinses, bool isAlphabet){ - int result = 0; - - cv::Mat output(1, kCharsTotalNumber, CV_32FC1); - ann_->predict(f, output); - - maxVal = -2.f; - if (!isChinses) { - if (!isAlphabet) { - result = 0; - for (int j = 0; j < kCharactersNumber; j++) { - float val = output.at(j); - // std::cout << "j:" << j << "val:" << val << std::endl; - if (val > maxVal) { - maxVal = val; - result = j; - } - } - } - else { - result = 0; - // begin with 11th char, which is 'A' - for (int j = 10; j < kCharactersNumber; j++) { - float val = output.at(j); - // std::cout << "j:" << j << "val:" << val << std::endl; - if (val > maxVal) { - maxVal = val; - result = j; - } - } - } - } - else { - result = kCharactersNumber; - for (int j = kCharactersNumber; j < kCharsTotalNumber; j++) { - float val = output.at(j); - //std::cout << "j:" << j << "val:" << val << std::endl; - if (val > maxVal) { - maxVal = val; - result = j; - } - } - } - //std::cout << "maxVal:" << maxVal << std::endl; - return result; -} - -bool CharsIdentify::isCharacter(cv::Mat input, std::string& label, float& maxVal, bool isChinese) { - cv::Mat feature = charFeatures(input, kPredictSize); - auto index = static_cast(classify(feature, maxVal, isChinese)); - - if (isChinese) { - //std::cout << "maxVal:" << maxVal << std::endl; + out = maxVal; + return std::make_pair(s, province); } - float chineseMaxThresh = 0.2f; - - if (maxVal >= 0.9 || (isChinese && maxVal >= chineseMaxThresh)) { - if (index < kCharactersNumber) { - label = std::make_pair(kChars[index], kChars[index]).second; + // 用于识别输入的图像数据是否是一个字符。 + std::pair CharsIdentify::identify(cv::Mat input, bool isChinese, bool isAlphabet) + { + // 过调用charFeatures函数提取输入图像的特征,并存储在feature变量中。 + // 然后,它调用预先训练好的模型classify进行预测,并将预测结果存储在index变量中。 + cv::Mat feature = charFeatures(input, kPredictSize); + float maxVal = -2; + auto index = static_cast(classify(feature, maxVal, isChinese, isAlphabet)); + // 检查索引值index是否小于字符集大小kCharactersNumber。如果是,则返回由相同字符组成的pair; + // 否则,获取索引对应的字符作为键,并使用键值对kv_查找对应的省份。 + if (index < kCharactersNumber) + { + return std::make_pair(kChars[index], kChars[index]); } - else { - const char* key = kChars[index]; + else + { + const char *key = kChars[index]; std::string s = key; std::string province = kv_->get(s); - label = std::make_pair(s, province).second; + return std::make_pair(s, province); } - return true; } - else - return false; -} - -std::pair CharsIdentify::identifyChinese(cv::Mat input, float& out, bool& isChinese) { - cv::Mat feature = charFeatures(input, kChineseSize); - float maxVal = -2; - int result = 0; - - cv::Mat output(1, kChineseNumber, CV_32FC1); - annChinese_->predict(feature, output); - for (int j = 0; j < kChineseNumber; j++) { - float val = output.at(j); - //std::cout << "j:" << j << "val:" << val << std::endl; - if (val > maxVal) { - maxVal = val; - result = j; + // 用于处理一组输入的图像数据并识别出对应的字符和省份。 + // 函数参数包括输入图像数据(inputs),输出结果(outputs)以及一个布尔值向量(isChineseVec) + int CharsIdentify::identify(std::vector inputs, std::vector> &outputs, + std::vector isChineseVec) + { + // featureRows创建一个空的Mat对象。它将被用于存储所有输入图像的特征。 + Mat featureRows; + size_t input_size = inputs.size(); + // 每一张图像提取特征,并将这些特征添加到featureRows中。 + for (size_t i = 0; i < input_size; i++) + { + Mat input = inputs[i]; + cv::Mat feature = charFeatures(input, kPredictSize); + featureRows.push_back(feature); } - } - - // no match - if (-1 == result) { - result = 0; - maxVal = 0; - isChinese = false; - } - else if (maxVal > 0.9){ - isChinese = true; - } - - auto index = result + kCharsTotalNumber - kChineseNumber; - const char* key = kChars[index]; - std::string s = key; - std::string province = kv_->get(s); - out = maxVal; - return std::make_pair(s, province); -} - -std::pair CharsIdentify::identifyChineseGray(cv::Mat input, float& out, bool& isChinese) { - cv::Mat feature; - extractFeature(input, feature); - float maxVal = -2; - int result = 0; - cv::Mat output(1, kChineseNumber, CV_32FC1); - annGray_->predict(feature, output); - - for (int j = 0; j < kChineseNumber; j++) { - float val = output.at(j); - //std::cout << "j:" << j << "val:" << val << std::endl; - if (val > maxVal) { - maxVal = val; - result = j; - } - } - // no match - if (-1 == result) { - result = 0; - maxVal = 0; - isChinese = false; - } else if (maxVal > 0.9){ - isChinese = true; - } - auto index = result + kCharsTotalNumber - kChineseNumber; - const char* key = kChars[index]; - std::string s = key; - std::string province = kv_->get(s); - out = maxVal; - return std::make_pair(s, province); -} - - -std::pair CharsIdentify::identify(cv::Mat input, bool isChinese, bool isAlphabet) { - cv::Mat feature = charFeatures(input, kPredictSize); - float maxVal = -2; - auto index = static_cast(classify(feature, maxVal, isChinese, isAlphabet)); - if (index < kCharactersNumber) { - return std::make_pair(kChars[index], kChars[index]); - } - else { - const char* key = kChars[index]; - std::string s = key; - std::string province = kv_->get(s); - return std::make_pair(s, province); - } -} - -int CharsIdentify::identify(std::vector inputs, std::vector>& outputs, - std::vector isChineseVec) { - Mat featureRows; - size_t input_size = inputs.size(); - for (size_t i = 0; i < input_size; i++) { - Mat input = inputs[i]; - cv::Mat feature = charFeatures(input, kPredictSize); - featureRows.push_back(feature); - } - - std::vector maxIndexs; - std::vector maxVals; - classify(featureRows, maxIndexs, maxVals, isChineseVec); - - for (size_t row_index = 0; row_index < input_size; row_index++) { - int index = maxIndexs[row_index]; - if (index < kCharactersNumber) { - outputs[row_index] = std::make_pair(kChars[index], kChars[index]); - } - else { - const char* key = kChars[index]; - std::string s = key; - std::string province = kv_->get(s); - outputs[row_index] = std::make_pair(s, province); + std::vector maxIndexs; + std::vector maxVals; + // 调用classify函数,输入特征矩阵featureRows,并返回最大值的索引(maxIndexs)和最大值(maxVals)。 + // 同时,根据这些最大值,更新isChineseVec中的对应元素。 + classify(featureRows, maxIndexs, maxVals, isChineseVec); + // 遍历所有的输入图像,对于每一张图像,根据其对应的最大值索引,构造一个输出对,并存储在outputs中。 + // 如果索引小于字符集大小kCharactersNumber,则输出对由相同字符组成; + // 否则,获取索引对应的字符作为键,并使用键值对kv_查找对应的省份。 + + for (size_t row_index = 0; row_index < input_size; row_index++) + { + int index = maxIndexs[row_index]; + if (index < kCharactersNumber) + { + outputs[row_index] = std::make_pair(kChars[index], kChars[index]); + } + else + { + const char *key = kChars[index]; + std::string s = key; + std::string province = kv_->get(s); + outputs[row_index] = std::make_pair(s, province); + } } + return 0; } - return 0; -} } diff --git a/src/src/core/chars_recognise.cpp b/src/src/core/chars_recognise.cpp index d101fd3..26831a9 100644 --- a/src/src/core/chars_recognise.cpp +++ b/src/src/core/chars_recognise.cpp @@ -6,29 +6,43 @@ namespace easypr { CCharsRecognise::CCharsRecognise() { m_charsSegment = new CCharsSegment(); } +//CCharsRecogniseĹ캯һµCCharsSegmentʵֵַԱm_charsSegment CCharsRecognise::~CCharsRecognise() { SAFE_RELEASE(m_charsSegment); } +//CCharsRecognise +//SAFE_RELEASEͷţɾÿգm_charsSegmentָָCCharsSegment int CCharsRecognise::charsRecognise(Mat plate, std::string& plateLicense) { + //ַʶһMat͵plateҪʶijͼ + //һstd::string&͵plateLicenseһãڴ洢ʶijƺ롣 std::vector matChars; + //matCharsڴ洢ַָĵַͼ int result = m_charsSegment->charsSegment(plate, matChars); + //m_charsSegmentcharsSegmentַָ + //ַָ洢matCharsСcharsSegmentķֵ洢resultС if (result == 0) { + //result = 0ʾַָɹŽгƺʶ int num = matChars.size(); for (int j = 0; j < num; j++) { Mat charMat = matChars.at(j); + //ѭmatCharsеÿַͼ bool isChinses = false; float maxVal = 0; if (j == 0) { bool judge = true; isChinses = true; + //һַΪĺ auto character = CharsIdentify::instance()->identifyChinese(charMat, maxVal, judge); plateLicense.append(character.second); + //CharsIdentify::instance()->identifyChineseʶ𣬲ʶ׷ӵplateLicense } else { isChinses = false; + //ǵһַΪĺ֣ auto character = CharsIdentify::instance()->identify(charMat, isChinses); plateLicense.append(character.second); + //CharsIdentify::instance()->identifyʶ𣬲ʶ׷ӵplateLicenseС } } @@ -36,20 +50,27 @@ int CCharsRecognise::charsRecognise(Mat plate, std::string& plateLicense) { if (plateLicense.size() < 7) { return -1; } + // plateLicenseijǷС7С7򷵻 - 1ʾƺʶʧܡ򣬷resultʾƺʶɹ return result; } int CCharsRecognise::charsRecognise(CPlate& plate, std::string& plateLicense) { +//ͬʶƵĺһCPlate&͵plateҪʶijƶ +// һstd::string&͵plateLicenseһãڴ洢ʶijƺ std::vector matChars; std::vector grayChars; + //matCharsڴ洢ַָĵַͼgrayCharsڴ洢Ҷͼ Mat plateMat = plate.getPlateMat(); + //plate.getPlateMat()ȡͼ񣬲洢plateMatС if (0) writeTempImage(plateMat, "plateMat/plate"); Color color; if (plate.getPlateLocateType() == CMSER) { color = plate.getPlateColor(); } + //ѡͬķʽȡɫƶλΪCMSERֱʹplate.getPlateColor()ȡɫ + //򣬴ӳͼнȡһ򣬲getPlateType()ȡ͡ else { int w = plateMat.cols; int h = plateMat.rows; @@ -58,35 +79,42 @@ int CCharsRecognise::charsRecognise(CPlate& plate, std::string& plateLicense) { } int result = m_charsSegment->charsSegmentUsingOSTU(plateMat, matChars, grayChars, color); - + //charsSegmentUsingOSTUijͼַָ if (result == 0) { int num = matChars.size(); for (int j = 0; j < num; j++) { + //forѭÿһַͼ Mat charMat = matChars.at(j); Mat grayChar = grayChars.at(j); + //ͨmatChars.at(j)grayChars.at(j)ȡjַͼҶͼ if (color != Color::BLUE) grayChar = 255 - grayChar; - + //ɫɫԻҶͼв bool isChinses = false; std::pair character; float maxVal; if (0 == j) { isChinses = true; bool judge = true; + //趨һַΪַ character = CharsIdentify::instance()->identifyChineseGray(grayChar, maxVal, judge); plateLicense.append(character.second); - + //ʹCharsIdentify::instance()->identifyChineseGrayʶҶͼgrayChar + // 洢characterС // set plate chinese mat and str plate.setChineseMat(grayChar); plate.setChineseKey(character.first); + //óƵͼַ if (0) writeTempImage(grayChar, "char_data/" + character.first + "/chars_"); } else if (1 == j) { isChinses = false; bool isAbc = true; character = CharsIdentify::instance()->identify(charMat, isChinses, isAbc); + //ʹCharsIdentify::instance()->identifyʶַͼcharMat plateLicense.append(character.second); + //ʶĵڶַӵplateLicense } else { isChinses = false; @@ -96,17 +124,21 @@ int CCharsRecognise::charsRecognise(CPlate& plate, std::string& plateLicense) { } CCharacter charResult; + //CCharacterͼַ charResult.setCharacterMat(charMat); charResult.setCharacterGrayMat(grayChar); if (isChinses) charResult.setCharacterStr(character.first); else charResult.setCharacterStr(character.second); - + + //isChinsesΪtrueʹʶĵһַΪַַʹʶĵڶַ plate.addReutCharacter(charResult); + //ַӵƶС } if (plateLicense.size() < 7) { return -1; + //ʶַ7 - 1򷵻result } } 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,可能表示操作成功 } } diff --git a/src/src/core/core_func.cpp b/src/src/core/core_func.cpp index a4e419e..179ada5 100644 --- a/src/src/core/core_func.cpp +++ b/src/src/core/core_func.cpp @@ -6,47 +6,56 @@ #include "thirdparty/mser/mser2.hpp" #include -namespace easypr { - Mat colorMatch(const Mat &src, Mat &match, const Color r, - const bool adaptive_minsv) { +namespace easypr +{ + + // 用于在HSV颜色空间中匹配特定的颜色。它可以用于识别图像中的蓝色、黄色和白色区域 + Mat colorMatch(const Mat &src, Mat &match, const Color r, // 函数接受四个参数:一个源图像src,一个输出图像match,一个颜色枚举r,和一个布尔值adaptive_minsv + const bool adaptive_minsv) + { // if use adaptive_minsv // min value of s and v is adaptive to h + // 定义了一些常量,包括最大的饱和度和亮度值max_sv,参考的最小饱和度和亮度值minref_sv,和绝对的最小饱和度和亮度值minabs_sv const float max_sv = 255; const float minref_sv = 64; - const float minabs_sv = 95; //95; + const float minabs_sv = 95; // 95; - // H range of blue + // 定义了蓝色、黄色和白色在HSV颜色空间中的H值范围 + // H range of blue - const int min_blue = 100; // 100 - const int max_blue = 140; // 140 + const int min_blue = 100; // 100 + const int max_blue = 140; // 140 // H range of yellow - const int min_yellow = 15; // 15 - const int max_yellow = 40; // 40 + const int min_yellow = 15; // 15 + const int max_yellow = 40; // 40 // H range of white - const int min_white = 0; // 15 - const int max_white = 30; // 40 + const int min_white = 0; // 15 + const int max_white = 30; // 40 + // 将源图像从BGR颜色空间转换到HSV颜色空间 Mat src_hsv; // convert to HSV space cvtColor(src, src_hsv, CV_BGR2HSV); + // 将HSV图像分割成三个通道,对V通道进行直方图均衡化,然后再合并回去 std::vector hsvSplit; split(src_hsv, hsvSplit); equalizeHist(hsvSplit[2], hsvSplit[2]); merge(hsvSplit, src_hsv); // match to find the color - + // 根据输入的颜色枚举r,设置H值的范围 int min_h = 0; int max_h = 0; - switch (r) { + switch (r) + { case BLUE: min_h = min_blue; max_h = max_blue; @@ -64,38 +73,54 @@ namespace easypr { break; } + // 计算H值的差值和平均值 float diff_h = float((max_h - min_h) / 2); float avg_h = min_h + diff_h; + // 获取图像的通道数、行数和列数。如果图像是连续的,那么将列数乘以行数,行数设置为1 int channels = src_hsv.channels(); int nRows = src_hsv.rows; // consider multi channel image int nCols = src_hsv.cols * channels; - if (src_hsv.isContinuous()) { + if (src_hsv.isContinuous()) + { nCols *= nRows; nRows = 1; } + // 对图像进行遍历,对每个像素进行颜色匹配。如果像素的颜色匹配成功,那么将其设置为红色(255),否则设置为黑色(0) + // 定义变量用于后续的计算和循环 int i, j; - uchar* p; + uchar *p; float s_all = 0; float v_all = 0; float count = 0; - for (i = 0; i < nRows; ++i) { + + // 双重循环,遍历图像的每个像素。对于每个像素,我们获取其在HSV颜色空间中的H(色相)、S(饱和度)和V(亮度)值 + for (i = 0; i < nRows; ++i) + { p = src_hsv.ptr(i); - for (j = 0; j < nCols; j += 3) { - int H = int(p[j]); // 0-180 - int S = int(p[j + 1]); // 0-255 - int V = int(p[j + 2]); // 0-255 + for (j = 0; j < nCols; j += 3) + { + int H = int(p[j]); // 0-180 + int S = int(p[j + 1]); // 0-255 + int V = int(p[j + 2]); // 0-255 + // 此处计算了所有像素的S和V值的总和,以及像素的数量 s_all += S; v_all += V; count++; + // 以下这段代码是颜色匹配的核心部分。 + // 首先,检查像素的H值是否在预设的范围内。 + // 如果在范围内,计算H值与平均H值的差值,然后根据这个差值和预设的阈值来计算最小的S和V值。 + // 如果像素的S和V值都在这个范围内,认为这个像素的颜色匹配成功。 bool colorMatched = false; - if (H > min_h && H < max_h) { + if (H > min_h && H < max_h) + { ////如果在范围内,计算H值与平均H值的差值 + float Hdiff = 0; if (H > avg_h) Hdiff = H - avg_h; @@ -105,25 +130,27 @@ namespace easypr { float Hdiff_p = float(Hdiff) / diff_h; float min_sv = 0; - if (true == adaptive_minsv) + if (true == adaptive_minsv) // 然后根据这个差值和预设的阈值来计算最小的S和V值 min_sv = - minref_sv - - minref_sv / 2 * - (1 - - Hdiff_p); // inref_sv - minref_sv / 2 * (1 - Hdiff_p) + minref_sv - + minref_sv / 2 * + (1 - Hdiff_p); // inref_sv - minref_sv / 2 * (1 - Hdiff_p) else - min_sv = minabs_sv; // add + min_sv = minabs_sv; // add - if ((S > min_sv && S < max_sv) && (V > min_sv && V < max_sv)) + if ((S > min_sv && S < max_sv) && (V > min_sv && V < max_sv)) // 如果像素的S和V值都在这个范围内,认为这个像素的颜色匹配成功 colorMatched = true; } - if (colorMatched == true) { + // 如果像素的颜色匹配成功,将其设置为红色(在HSV颜色空间中,H=0,S=0,V=255表示红色)。否则,将其设置为黑色(H=0,S=0,V=0) + if (colorMatched == true) + { p[j] = 0; p[j + 1] = 0; p[j + 2] = 255; } - else { + else + { p[j] = 0; p[j + 1] = 0; p[j + 2] = 0; @@ -135,7 +162,8 @@ namespace easypr { // cout << "avg_v:" << v_all / count << endl; // get the final binary - + // 将处理后的HSV图像分割成三个通道,取出V通道作为灰度图像,并返回这个灰度图像 + // 在这个灰度图像中,匹配的颜色区域被标记为白色,其余区域为黑色 Mat src_grey; std::vector hsvSplit_done; split(src_hsv, hsvSplit_done); @@ -143,45 +171,64 @@ namespace easypr { match = src_grey; - return src_grey; + return src_grey; // 函数返回一个灰度图像,其中匹配的颜色区域被标记为白色,其余区域为黑色 } - bool bFindLeftRightBound1(Mat &bound_threshold, int &posLeft, int &posRight) { - + // 该函数用于在图像中找到左右边界。它通过计算图像中的白色像素数量来确定边界 + // 函数接受一个Mat类型的引用bound_threshold,以及两个整型引用posLeft和posRight作为参数 + bool bFindLeftRightBound1(Mat &bound_threshold, int &posLeft, int &posRight) + { + // 首先,计算图像行数的20%,作为一个跨度span float span = bound_threshold.rows * 0.2f; - for (int i = 0; i < bound_threshold.cols - span - 1; i += 3) { + // 然后,从左到右遍历图像,每次跳过3列,计算每个跨度内的白色像素数量 + for (int i = 0; i < bound_threshold.cols - span - 1; i += 3) + { int whiteCount = 0; - for (int k = 0; k < bound_threshold.rows; k++) { - for (int l = i; l < i + span; l++) { - if (bound_threshold.data[k * bound_threshold.step[0] + l] == 255) { + for (int k = 0; k < bound_threshold.rows; k++) + { + for (int l = i; l < i + span; l++) + { + if (bound_threshold.data[k * bound_threshold.step[0] + l] == 255) + { whiteCount++; } } } - if (whiteCount * 1.0 / (span * bound_threshold.rows) > 0.15) { + // 如果白色像素的比例超过15%,则认为找到了左边界 + if (whiteCount * 1.0 / (span * bound_threshold.rows) > 0.15) + { posLeft = i; break; } } - span = bound_threshold.rows * 0.2f; - + span = bound_threshold.rows * 0.2f; // 再次计算跨度,用于寻找右边界 - for (int i = bound_threshold.cols - 1; i > span; i -= 2) { + // 接着,从右到左遍历图像,每次跳过2列,计算每个跨度内的白色像素数量 + for (int i = bound_threshold.cols - 1; i > span; i -= 2) + { int whiteCount = 0; - for (int k = 0; k < bound_threshold.rows; k++) { - for (int l = i; l > i - span; l--) { - if (bound_threshold.data[k * bound_threshold.step[0] + l] == 255) { + for (int k = 0; k < bound_threshold.rows; k++) + { + for (int l = i; l > i - span; l--) + { + if (bound_threshold.data[k * bound_threshold.step[0] + l] == 255) + { whiteCount++; } } } - if (whiteCount * 1.0 / (span * bound_threshold.rows) > 0.06) { + // 如果白色像素的比例超过6%,则认为找到了右边界 + if (whiteCount * 1.0 / (span * bound_threshold.rows) > 0.06) + { posRight = i; - if (posRight + 5 < bound_threshold.cols) { + if (posRight + 5 < bound_threshold.cols) + { // 如果右边界加5后仍在图像列数范围内,则右边界加5 posRight = posRight + 5; - } else { + } + else + { // 否则右边界设为图像列数减1 posRight = bound_threshold.cols - 1; } @@ -189,139 +236,190 @@ namespace easypr { } } - if (posLeft < posRight) { + // 最后,如果左边界小于右边界,返回真,否则返回假 + if (posLeft < posRight) + { return true; } return false; } - bool bFindLeftRightBound(Mat &bound_threshold, int &posLeft, int &posRight) { - + // 该函数用于在图像中找到左右边界。它通过计算图像中的白色像素数量来确定边界 + // 函数接受三个参数:一个Mat类型的引用bound_threshold,以及两个整型引用posLeft和posRight + // 函数返回一个布尔值,表示是否成功找到左右边界 + bool bFindLeftRightBound(Mat &bound_threshold, int &posLeft, int &posRight) + { + // 首先,计算图像行数的20%,作为一个跨度span float span = bound_threshold.rows * 0.2f; - - for (int i = 0; i < bound_threshold.cols - span - 1; i += 2) { + // 然后,从左到右遍历图像,每次跳过2列,计算每个跨度内的白色像素数量。如果白色像素的比例超过36%,则认为找到了左边界 + for (int i = 0; i < bound_threshold.cols - span - 1; i += 2) + { int whiteCount = 0; - for (int k = 0; k < bound_threshold.rows; k++) { - for (int l = i; l < i + span; l++) { - if (bound_threshold.data[k * bound_threshold.step[0] + l] == 255) { + for (int k = 0; k < bound_threshold.rows; k++) + { + for (int l = i; l < i + span; l++) + { + if (bound_threshold.data[k * bound_threshold.step[0] + l] == 255) + { whiteCount++; } } } - if (whiteCount * 1.0 / (span * bound_threshold.rows) > 0.36) { + if (whiteCount * 1.0 / (span * bound_threshold.rows) > 0.36) + { posLeft = i; break; } } - span = bound_threshold.rows * 0.2f; - + span = bound_threshold.rows * 0.2f; // 再次计算跨度,用于寻找右边界 - for (int i = bound_threshold.cols - 1; i > span; i -= 2) { + // 接着,从右到左遍历图像,每次跳过2列,计算每个跨度内的白色像素数量。如果白色像素的比例超过26%,则认为找到了右边界 + for (int i = bound_threshold.cols - 1; i > span; i -= 2) + { int whiteCount = 0; - for (int k = 0; k < bound_threshold.rows; k++) { - for (int l = i; l > i - span; l--) { - if (bound_threshold.data[k * bound_threshold.step[0] + l] == 255) { + for (int k = 0; k < bound_threshold.rows; k++) + { + for (int l = i; l > i - span; l--) + { + if (bound_threshold.data[k * bound_threshold.step[0] + l] == 255) + { whiteCount++; } } } - if (whiteCount * 1.0 / (span * bound_threshold.rows) > 0.26) { + if (whiteCount * 1.0 / (span * bound_threshold.rows) > 0.26) + { posRight = i; break; } } - - if (posLeft < posRight) { + // 最后,如果左边界小于右边界,返回真,否则返回假 + if (posLeft < posRight) + { return true; } return false; } - bool bFindLeftRightBound2(Mat &bound_threshold, int &posLeft, int &posRight) { - + // 该函数接受一个Mat类型的引用bound_threshold和两个整型引用posLeft和posRight作为参数 + // 函数返回一个布尔值,表示是否成功找到左右边界 + bool bFindLeftRightBound2(Mat &bound_threshold, int &posLeft, int &posRight) + { + // 计算图像行数的20%,作为一个跨度span float span = bound_threshold.rows * 0.2f; - for (int i = 0; i < bound_threshold.cols - span - 1; i += 3) { + for (int i = 0; i < bound_threshold.cols - span - 1; i += 3) + { // 从左到右遍历图像,每次跳过3列 + // 在每个跨度内,计算白色像素的数量 int whiteCount = 0; - for (int k = 0; k < bound_threshold.rows; k++) { - for (int l = i; l < i + span; l++) { - if (bound_threshold.data[k * bound_threshold.step[0] + l] == 255) { + for (int k = 0; k < bound_threshold.rows; k++) + { + for (int l = i; l < i + span; l++) + { + if (bound_threshold.data[k * bound_threshold.step[0] + l] == 255) + { whiteCount++; } } } - if (whiteCount * 1.0 / (span * bound_threshold.rows) > 0.32) { + // 如果白色像素的比例超过32%,则认为找到了左边界 + if (whiteCount * 1.0 / (span * bound_threshold.rows) > 0.32) + { posLeft = i; break; } } + // 再次计算跨度,用于寻找右边界 span = bound_threshold.rows * 0.2f; - - for (int i = bound_threshold.cols - 1; i > span; i -= 3) { + for (int i = bound_threshold.cols - 1; i > span; i -= 3) + { // 从右到左遍历图像,每次跳过3列 + // 在每个跨度内,计算白色像素的数量 int whiteCount = 0; - for (int k = 0; k < bound_threshold.rows; k++) { - for (int l = i; l > i - span; l--) { - if (bound_threshold.data[k * bound_threshold.step[0] + l] == 255) { + for (int k = 0; k < bound_threshold.rows; k++) + { + for (int l = i; l > i - span; l--) + { + if (bound_threshold.data[k * bound_threshold.step[0] + l] == 255) + { whiteCount++; } } } - - if (whiteCount * 1.0 / (span * bound_threshold.rows) > 0.22) { + // 如果白色像素的比例超过22%,则认为找到了右边界 + if (whiteCount * 1.0 / (span * bound_threshold.rows) > 0.22) + { posRight = i; break; } } - - if (posLeft < posRight) { + // 如果左边界小于右边界,返回真,否则返回假 + if (posLeft < posRight) + { return true; } return false; } - + // 该函数接受一个Mat类型的引用src,一个颜色枚举r,一个布尔值adaptive_minsv,和一个浮点数引用percent作为参数 + // 函数返回一个布尔值,表示颜色是否匹配 bool plateColorJudge(const Mat &src, const Color r, const bool adaptive_minsv, - float &percent) { - + float &percent) + { + // 定义一个常量thresh,表示阈值 const float thresh = 0.45f; + // 调用colorMatch函数,将源图像转换为灰度图像 Mat src_gray; colorMatch(src, src_gray, r, adaptive_minsv); + // 计算灰度图像中非零像素的比例 percent = float(countNonZero(src_gray)) / float(src_gray.rows * src_gray.cols); // cout << "percent:" << percent << endl; + // 如果非零像素的比例大于阈值,返回真,否则返回假 if (percent > thresh) return true; else return false; } - Color getPlateType(const Mat &src, const bool adaptive_minsv) { + // 用于判断图像中的颜色类型(蓝色、黄色或白色) + // 接受两个参数:一个Mat类型的引用src(源图像)和一个布尔值adaptive_minsv + // 函数返回一个布尔值,表示颜色是否匹配。 + Color getPlateType(const Mat &src, const bool adaptive_minsv) + { + // 定义两个变量:max_percent用于存储最大的颜色匹配百分比,max_color用于存储对应的颜色。初始值分别设为0和UNKNOW float max_percent = 0; Color max_color = UNKNOWN; - + // 定义三个变量,用于存储蓝色、黄色和白色的匹配百分比。初始值都设为0 float blue_percent = 0; float yellow_percent = 0; float white_percent = 0; - - if (plateColorJudge(src, BLUE, adaptive_minsv, blue_percent) == true) { + // 调用plateColorJudge函数,判断源图像中的颜色是否为蓝色、黄色或白色。如果是,返回对应的颜色 + if (plateColorJudge(src, BLUE, adaptive_minsv, blue_percent) == true) + { // cout << "BLUE" << endl; return BLUE; - } else if (plateColorJudge(src, YELLOW, adaptive_minsv, yellow_percent) == - true) { + } + else if (plateColorJudge(src, YELLOW, adaptive_minsv, yellow_percent) == + true) + { // cout << "YELLOW" << endl; return YELLOW; - } else if (plateColorJudge(src, WHITE, adaptive_minsv, white_percent) == - true) { + } + else if (plateColorJudge(src, WHITE, adaptive_minsv, white_percent) == + true) + { // cout << "WHITE" << endl; return WHITE; - } else { - //std::cout << "OTHER" << std::endl; + } + else + { // 如果源图像中的颜色既不是蓝色、也不是黄色、也不是白色,那么默认返回蓝色 + // std::cout << "OTHER" << std::endl; /*max_percent = blue_percent > yellow_percent ? blue_percent : yellow_percent; max_color = blue_percent > yellow_percent ? BLUE : YELLOW; @@ -332,55 +430,79 @@ namespace easypr { } } - void clearLiuDingOnly(Mat &img) { - const int x = 7; - Mat jump = Mat::zeros(1, img.rows, CV_32F); - for (int i = 0; i < img.rows; i++) { - int jumpCount = 0; - int whiteCount = 0; - for (int j = 0; j < img.cols - 1; j++) { - if (img.at(i, j) != img.at(i, j + 1)) jumpCount++; - - if (img.at(i, j) == 255) { + // 该函数用于清除图像中的噪声,提高图像处理的准确性 + // 接受一个Mat类型的引用img作为参数 + // 该函数主要通过计算图像中每一行的跳变次数(即像素值从0变为255,或从255变为0的次数)来判断是否为噪声,如果跳变次数小于某个阈值,那么就将这一行的所有像素值设为0,即黑色 + void clearLiuDingOnly(Mat &img) + { + const int x = 7; // 设置阈值为7 + Mat jump = Mat::zeros(1, img.rows, CV_32F); // 创建一个大小为图像行数,类型为浮点数的Mat对象,用于存储每一行的跳变次数 + for (int i = 0; i < img.rows; i++) + { // 遍历图像的每一行 + int jumpCount = 0; // 初始化跳变次数为0 + int whiteCount = 0; // 初始化白色像素的数量为0 + for (int j = 0; j < img.cols - 1; j++) + { // 遍历当前行的每一个像素 + if (img.at(i, j) != img.at(i, j + 1)) + jumpCount++; // 如果当前像素与下一个像素的值不同,跳变次数加1 + + if (img.at(i, j) == 255) + { // 如果当前像素的值为255,即白色,白色像素的数量加1 whiteCount++; } } - - jump.at(i) = (float) jumpCount; + // 将当前行的跳变次数存储到jump中 + jump.at(i) = (float)jumpCount; } - for (int i = 0; i < img.rows; i++) { - if (jump.at(i) <= x) { - for (int j = 0; j < img.cols; j++) { - img.at(i, j) = 0; + for (int i = 0; i < img.rows; i++) + { // 再次遍历图像的每一行 + if (jump.at(i) <= x) + { // 如果当前行的跳变次数小于阈值 + for (int j = 0; j < img.cols; j++) + { // 遍历当前行的每一个像素 + img.at(i, j) = 0; // 将像素值设为0,即黑色 } } } } - bool clearLiuDing(Mat &img) { + // 该函数用于清除图像中的噪声 + // 接受一个Mat类型的引用img作为参数,返回一个布尔值,表示是否成功清除噪声 + bool clearLiuDing(Mat &img) + { + // 初始化一个浮点数向量fJump,一个整数whiteCount用于计数白色像素,一个常量x设为7,以及一个全零的Mat对象jump,大小为1行,列数为图像的行数 std::vector fJump; int whiteCount = 0; const int x = 7; Mat jump = Mat::zeros(1, img.rows, CV_32F); - for (int i = 0; i < img.rows; i++) { + + // 遍历图像的每一行,计算每一行的跳变次数(即像素值从0变为255,或从255变为0的次数)和白色像素的数量,将跳变次数存储到jump中 + for (int i = 0; i < img.rows; i++) + { int jumpCount = 0; - for (int j = 0; j < img.cols - 1; j++) { - if (img.at(i, j) != img.at(i, j + 1)) jumpCount++; + for (int j = 0; j < img.cols - 1; j++) + { + if (img.at(i, j) != img.at(i, j + 1)) + jumpCount++; - if (img.at(i, j) == 255) { + if (img.at(i, j) == 255) + { whiteCount++; } } - jump.at(i) = (float) jumpCount; + jump.at(i) = (float)jumpCount; } + // 遍历jump,将其值添加到fJump中,如果jump的值在16到45之间,iCount加1 int iCount = 0; - for (int i = 0; i < img.rows; i++) { + for (int i = 0; i < img.rows; i++) + { fJump.push_back(jump.at(i)); - if (jump.at(i) >= 16 && jump.at(i) <= 45) { + if (jump.at(i) >= 16 && jump.at(i) <= 45) + { // jump condition iCount++; @@ -388,18 +510,25 @@ namespace easypr { } // if not is not plate - if (iCount * 1.0 / img.rows <= 0.40) { + // 如果iCount占图像行数的比例小于或等于40%,或者白色像素占图像总像素的比例小于15%或大于50%,则返回假,表示未能成功清除噪声 + if (iCount * 1.0 / img.rows <= 0.40) + { return false; } if (whiteCount * 1.0 / (img.rows * img.cols) < 0.15 || - whiteCount * 1.0 / (img.rows * img.cols) > 0.50) { + whiteCount * 1.0 / (img.rows * img.cols) > 0.50) + { return false; } - for (int i = 0; i < img.rows; i++) { - if (jump.at(i) <= x) { - for (int j = 0; j < img.cols; j++) { + // 遍历图像的每一行,如果该行的跳变次数小于或等于x,则将该行的所有像素值设为0,即黑色。最后返回真,表示成功清除噪声 + for (int i = 0; i < img.rows; i++) + { + if (jump.at(i) <= x) + { + for (int j = 0; j < img.cols; j++) + { img.at(i, j) = 0; } } @@ -407,296 +536,384 @@ namespace easypr { return true; } - -void clearBorder(const Mat &img, Rect& cropRect) { - int r = img.rows; - int c = img.cols; - Mat boder = Mat::zeros(1, r, CV_8UC1); - const int noJunpCount_thresh = int(0.15f * c); - - // if nojumpcount > - for (int i = 0; i < r; i++) { - int nojumpCount = 0; - int isBorder = 0; - for (int j = 0; j < c - 1; j++) { - if (img.at(i, j) == img.at(i, j + 1)) - nojumpCount++; - if (nojumpCount > noJunpCount_thresh) { - nojumpCount = 0; - isBorder = 1; - break; + // 该函数用于清除图像的边界 + // 接受一个Mat类型的图像img和一个Rect类型的裁剪矩形cropRect作为参数 + void clearBorder(const Mat &img, Rect &cropRect) + { + // 获取图像的行数和列数 + int r = img.rows; + int c = img.cols; + + // 创建一个全零的单通道矩阵boder,用于存储每一行是否为边界 + Mat boder = Mat::zeros(1, r, CV_8UC1); + + // 设置一个阈值,如果一行中像素值没有变化的次数超过这个阈值,那么认为这一行是边界 + const int noJunpCount_thresh = int(0.15f * c); + + // if nojumpcount > + // 遍历图像的每一行,计算每一行中像素值没有变化的次数,如果这个次数超过阈值,那么认为这一行是边界,将boder对应的位置设为1 + for (int i = 0; i < r; i++) + { + int nojumpCount = 0; + int isBorder = 0; + for (int j = 0; j < c - 1; j++) + { + if (img.at(i, j) == img.at(i, j + 1)) + nojumpCount++; + if (nojumpCount > noJunpCount_thresh) + { + nojumpCount = 0; + isBorder = 1; + break; + } } + boder.at(i) = (char)isBorder; } - boder.at(i) = (char) isBorder; - } - - const int mintop = int(0.1f * r); - const int maxtop = int(0.9f * r); - - int minMatTop = 0; - int maxMatTop = r - 1; - - for (int i = 0; i < mintop; i++) { - if (boder.at(i) == 1) { - minMatTop = i; + // 设置上下边界的搜索范围,只在图像的中间80%的区域内搜索边界 + const int mintop = int(0.1f * r); + const int maxtop = int(0.9f * r); + // 初始化裁剪矩形的上下边界 + int minMatTop = 0; + int maxMatTop = r - 1; + // 从上到下搜索上边界 + for (int i = 0; i < mintop; i++) + { + if (boder.at(i) == 1) + { + minMatTop = i; + } } - } - - for (int i = r - 1; i > maxtop; i--) { - if (boder.at(i) == 1) { - maxMatTop = i; + // 从下到上搜索下边界 + for (int i = r - 1; i > maxtop; i--) + { + if (boder.at(i) == 1) + { + maxMatTop = i; + } } + // 根据找到的上下边界创建裁剪矩形 + cropRect = Rect(0, minMatTop, c, maxMatTop - minMatTop + 1); } - cropRect = Rect(0, minMatTop, c, maxMatTop - minMatTop + 1); - -} - - void clearLiuDing(Mat mask, int &top, int &bottom) { - const int x = 7; - - for (int i = 0; i < mask.rows / 2; i++) { + // 用于清除图像中的噪声—————函数的目的是找到图像中的有效区域,即去除上下边缘的噪声 + // 函数接受一个Mat类型的图像mask和两个整数引用top和bottom作为参数 + void clearLiuDing(Mat mask, int &top, int &bottom) + { + const int x = 7; // 定义一个常量x,值为7 + // 遍历图像的上半部分,计算每一行的跳变次数(即像素值从0变为255,或从255变为0的次数)和白色像素的数量 + for (int i = 0; i < mask.rows / 2; i++) + { int whiteCount = 0; int jumpCount = 0; - for (int j = 0; j < mask.cols - 1; j++) { - if (mask.at(i, j) != mask.at(i, j + 1)) jumpCount++; + for (int j = 0; j < mask.cols - 1; j++) + { + if (mask.at(i, j) != mask.at(i, j + 1)) + jumpCount++; - if ((int) mask.at(i, j) == 255) { + if ((int)mask.at(i, j) == 255) + { whiteCount++; } } + // 如果跳变次数小于x且白色像素占该行的比例大于15%,或者白色像素的数量小于4,那么将top设为当前行数 if ((jumpCount < x && whiteCount * 1.0 / mask.cols > 0.15) || - whiteCount < 4) { + whiteCount < 4) + { top = i; } } + // 将top减1,如果top小于0,那么将top设为0 top -= 1; - if (top < 0) { + if (top < 0) + { top = 0; } // ok, find top and bottom boudnadry - - for (int i = mask.rows - 1; i >= mask.rows / 2; i--) { + // 遍历图像的下半部分,计算每一行的跳变次数和白色像素的数量 + for (int i = mask.rows - 1; i >= mask.rows / 2; i--) + { int jumpCount = 0; int whiteCount = 0; - for (int j = 0; j < mask.cols - 1; j++) { - if (mask.at(i, j) != mask.at(i, j + 1)) jumpCount++; - if (mask.at(i, j) == 255) { + for (int j = 0; j < mask.cols - 1; j++) + { + if (mask.at(i, j) != mask.at(i, j + 1)) + jumpCount++; + if (mask.at(i, j) == 255) + { whiteCount++; } } + // 如果跳变次数小于x且白色像素占该行的比例大于15%,或者白色像素的数量小于4,那么将bottom设为当前行数 if ((jumpCount < x && whiteCount * 1.0 / mask.cols > 0.15) || - whiteCount < 4) { + whiteCount < 4) + { bottom = i; } } + // 将bottom加1,如果bottom大于等于图像的行数,那么将bottom设为图像的行数减1 bottom += 1; - if (bottom >= mask.rows) { + if (bottom >= mask.rows) + { bottom = mask.rows - 1; } - - if (top >= bottom) { + // 如果top大于等于bottom,那么将top设为0,bottom设为图像的行数减1 + if (top >= bottom) + { top = 0; bottom = mask.rows - 1; } } - int ThresholdOtsu(Mat mat) { - int height = mat.rows; - int width = mat.cols; + int ThresholdOtsu(Mat mat) + { // 定义函数ThresholdOtsu,参数为OpenCV的Mat类型,返回值为int类型 + int height = mat.rows; // 获取图像的行数 + int width = mat.cols; // 获取图像的列数 // histogram - float histogram[256] = {0}; - for (int i = 0; i < height; i++) { - for (int j = 0; j < width; j++) { - unsigned char p = (unsigned char) ((mat.data[i * mat.step[0] + j])); - histogram[p]++; + float histogram[256] = {0}; // 初始化一个长度为256的浮点数数组,用于存储图像的灰度直方图 + for (int i = 0; i < height; i++) + { // 遍历图像的每一行 + for (int j = 0; j < width; j++) + { // 遍历图像的每一列 + unsigned char p = (unsigned char)((mat.data[i * mat.step[0] + j])); // 获取当前像素的灰度值 + histogram[p]++; // 对应的灰度直方图的值加1 } } // normalize histogram - int size = height * width; - for (int i = 0; i < 256; i++) { - histogram[i] = histogram[i] / size; + int size = height * width; // 计算图像的总像素数 + for (int i = 0; i < 256; i++) + { // 遍历灰度直方图 + histogram[i] = histogram[i] / size; // 将灰度直方图的每一个值除以总像素数,得到每个灰度值的概率 } // average pixel value - float avgValue = 0; - for (int i = 0; i < 256; i++) { - avgValue += i * histogram[i]; + float avgValue = 0; // 初始化平均灰度值为0 + for (int i = 0; i < 256; i++) + { // 遍历灰度直方图 + avgValue += i * histogram[i]; // 计算平均灰度值 } - int thresholdV; - float maxVariance = 0; - float w = 0, u = 0; - for (int i = 0; i < 256; i++) { - w += histogram[i]; - u += i * histogram[i]; + int thresholdV; // 初始化阈值 + float maxVariance = 0; // 初始化最大方差为0 + float w = 0, u = 0; // 初始化两个变量w和u,分别用于存储阈值以下和阈值以上的像素数的累积和 + for (int i = 0; i < 256; i++) + { // 遍历灰度直方图 + w += histogram[i]; // 计算阈值以下的像素数的累积和 + u += i * histogram[i]; // 计算阈值以上的像素数的累积和 - float t = avgValue * w - u; - float variance = t * t / (w * (1 - w)); - if (variance > maxVariance) { - maxVariance = variance; - thresholdV = i; + float t = avgValue * w - u; // 计算类间方差 + float variance = t * t / (w * (1 - w)); // 计算类间方差 + if (variance > maxVariance) + { // 如果当前的类间方差大于最大方差 + maxVariance = variance; // 更新最大方差 + thresholdV = i; // 更新阈值 } } - return thresholdV; + return thresholdV; // 返回阈值 } - - Mat histeq(Mat in) { - Mat out(in.size(), in.type()); - if (in.channels() == 3) { + // 该函数接受一个Mat类型的输入图像,并返回一个经过直方图均衡化处理的图像 + Mat histeq(Mat in) + { + Mat out(in.size(), in.type()); // 创建一个与输入图像同样大小和类型的Mat对象out,用于存储处理后的图像 + if (in.channels() == 3) + { // 判断输入图像的通道数,如果为3,即为彩色图像,执行以下操作 + // 创建一个Mat对象hsv和一个Mat向量hsvSplit,然后使用cvtColor函数将输入图像从BGR色彩空间转换为HSV色彩空间,结果存储在hsv中 Mat hsv; std::vector hsvSplit; cvtColor(in, hsv, CV_BGR2HSV); + // 使用split函数将hsv图像的三个通道分离到hsvSplit向量中,然后对V通道(亮度)进行直方图均衡化 split(hsv, hsvSplit); equalizeHist(hsvSplit[2], hsvSplit[2]); + // 使用merge函数将处理后的三个通道合并回hsv,然后使用cvtColor函数将hsv图像从HSV色彩空间转换回BGR色彩空间,结果存储在out中 merge(hsvSplit, hsv); cvtColor(hsv, out, CV_HSV2BGR); - } else if (in.channels() == 1) { + } + else if (in.channels() == 1) + { // 如果输入图像的通道数为1,即为灰度图像,执行以下操作 + // 直接对输入图像进行直方图均衡化,结果存储在out中 equalizeHist(in, out); } - return out; + return out; // 返回处理后的图像 } #define HORIZONTAL 1 #define VERTICAL 0 - Mat CutTheRect(Mat &in, Rect &rect) { - int size = in.cols; // (rect.width>rect.height)?rect.width:rect.height; + // 从输入图像中裁剪出一个指定的矩形区域,并将其放置在一个新的正方形图像的中心位置 + Mat CutTheRect(Mat &in, Rect &rect) + { // 接受一个Mat类型的引用in和一个Rect类型的引用rect作为参数 + int size = in.cols; // (rect.width>rect.height)?rect.width:rect.height; + // 创建了一个新的Mat对象dstMat,其大小为sizexsize,类型为CV_8UC1(8位无符号单通道),并将其所有元素设置为0 Mat dstMat(size, size, CV_8UC1); dstMat.setTo(Scalar(0, 0, 0)); + // 计算矩形rect在新图像中的起始位置(x, y) + int x = (int)floor((float)(size - rect.width) / 2.0f); + int y = (int)floor((float)(size - rect.height) / 2.0f); - int x = (int) floor((float) (size - rect.width) / 2.0f); - int y = (int) floor((float) (size - rect.height) / 2.0f); - - for (int i = 0; i < rect.height; ++i) { + // 遍历矩形rect中的每一个像素,并将其复制到新图像dstMat的相应位置 + for (int i = 0; i < rect.height; ++i) + { - for (int j = 0; j < rect.width; ++j) { + for (int j = 0; j < rect.width; ++j) + { dstMat.data[dstMat.step[0] * (i + y) + j + x] = in.data[in.step[0] * (i + rect.y) + j + rect.x]; } } - // + // 返回处理后的新图像dstMat return dstMat; } - Rect GetCenterRect(Mat &in) { - Rect _rect; - - int top = 0; - int bottom = in.rows - 1; + // 其目的是在输入图像中找到一个矩形区域,该区域包含了图像中所有像素值大于20的部分 + // 函数接受一个 Mat 类型的引用 in 作为参数,返回一个 Rect 类型的对象 + Rect GetCenterRect(Mat &in) + { + Rect _rect; // 定义一个矩形对象 - // find the center rect + int top = 0; // 初始化矩形的上边界 + int bottom = in.rows - 1; // 初始化矩形的下边界为图像的行数减1 - for (int i = 0; i < in.rows; ++i) { + // 从上到下遍历图像的每一行,找到第一个包含像素值大于20的行,将其行数设为矩形的上边界 + for (int i = 0; i < in.rows; ++i) + { bool bFind = false; - for (int j = 0; j < in.cols; ++j) { - if (in.data[i * in.step[0] + j] > 20) { + for (int j = 0; j < in.cols; ++j) + { + if (in.data[i * in.step[0] + j] > 20) + { top = i; bFind = true; break; } } - if (bFind) { + if (bFind) + { break; } - } - for (int i = in.rows - 1; - i >= 0; - --i) { + + // 从下到上遍历图像的每一行,找到第一个包含像素值大于20的行,将其行数设为矩形的下边界 + for (int i = in.rows - 1; i >= 0; --i) + { bool bFind = false; - for (int j = 0; j < in.cols; ++j) { - if (in.data[i * in.step[0] + j] > 20) { + for (int j = 0; j < in.cols; ++j) + { + if (in.data[i * in.step[0] + j] > 20) + { bottom = i; bFind = true; break; } } - if (bFind) { + if (bFind) + { break; } - } + int left = 0; // 初始化矩形的左边界 + int right = in.cols - 1; // 初始化矩形的右边界为图像的列数减1 - int left = 0; - int right = in.cols - 1; - for (int j = 0; j < in.cols; ++j) { + // 从左到右遍历图像的每一列,找到第一个包含像素值大于20的列,将其列数设为矩形的左边界 + for (int j = 0; j < in.cols; ++j) + { bool bFind = false; - for (int i = 0; i < in.rows; ++i) { - if (in.data[i * in.step[0] + j] > 20) { + for (int i = 0; i < in.rows; ++i) + { + if (in.data[i * in.step[0] + j] > 20) + { left = j; bFind = true; break; } } - if (bFind) { + if (bFind) + { break; } - } - for (int j = in.cols - 1; - j >= 0; - --j) { + + // 从右到左遍历图像的每一列,找到第一个包含像素值大于20的列,将其列数设为矩形的右边界 + for (int j = in.cols - 1; j >= 0; --j) + { bool bFind = false; - for (int i = 0; i < in.rows; ++i) { - if (in.data[i * in.step[0] + j] > 20) { + for (int i = 0; i < in.rows; ++i) + { + if (in.data[i * in.step[0] + j] > 20) + { right = j; bFind = true; - break; } } - if (bFind) { + if (bFind) + { break; } } + // 设置矩形的位置和大小 _rect.x = left; _rect.y = top; _rect.width = right - left + 1; _rect.height = bottom - top + 1; - return _rect; + return _rect; // 返回矩形对象 } - float countOfBigValue(Mat &mat, int iValue) { + // 计算图像中大于给定阈值的像素数量 + float countOfBigValue(Mat &mat, int iValue) + { float iCount = 0.0; - if (mat.rows > 1) { - for (int i = 0; i < mat.rows; ++i) { - if (mat.data[i * mat.step[0]] > iValue) { + // 如果图像的行数大于1,遍历每一行 + if (mat.rows > 1) + { + for (int i = 0; i < mat.rows; ++i) + { + // 如果当前像素的值大于阈值,计数器加1 + if (mat.data[i * mat.step[0]] > iValue) + { iCount += 1.0; } } return iCount; - - } else { - for (int i = 0; i < mat.cols; ++i) { - if (mat.data[i] > iValue) { + } + else + { + // 如果图像的行数不大于1,遍历每一列 + for (int i = 0; i < mat.cols; ++i) + { + // 如果当前像素的值大于阈值,计数器加1 + if (mat.data[i] > iValue) + { iCount += 1.0; } } - return iCount; } } - Mat ProjectedHistogram(Mat img, int t, int threshold) { + // 计算图像的投影直方图 + Mat ProjectedHistogram(Mat img, int t, int threshold) + { + // 根据参数t的值,确定直方图的长度 int sz = (t) ? img.rows : img.cols; + // 创建一个长度为sz的零矩阵,用于存储直方图 Mat mhist = Mat::zeros(1, sz, CV_32F); - for (int j = 0; j < sz; j++) { + for (int j = 0; j < sz; j++) + { + // 根据参数t的值,确定是对图像的行还是列进行操作 Mat data = (t) ? img.row(j) : img.col(j); - + // 计算大于阈值的像素数量,并存储在直方图中 mhist.at(j) = countOfBigValue(data, threshold); } - // Normalize histogram + // 归一化直方图 double min, max; minMaxLoc(mhist, &min, &max); @@ -706,144 +923,168 @@ void clearBorder(const Mat &img, Rect& cropRect) { return mhist; } - Mat showHistogram(const Mat &hist) { + // 显示直方图 + Mat showHistogram(const Mat &hist) + { int height = 32; int width = hist.cols; + // 创建一个高度为32,宽度为直方图长度的零矩阵,用于显示直方图 Mat show = Mat::zeros(height, width, CV_8UC1); - for (int i = 0; i < width; i++) { - int len = int((float) height * hist.at(i)); - for (int j = height - 1; j >= 0; j--) { + for (int i = 0; i < width; i++) + { + // 计算直方图的每一列的长度 + int len = int((float)height * hist.at(i)); + for (int j = height - 1; j >= 0; j--) + { + // 将直方图的每一列的长度以像素的形式显示出来 if (height - j <= len) - show.at(j, i) = (char) 255; + show.at(j, i) = (char)255; } } return show; } - Mat preprocessChar(Mat in, int char_size) { + // 对输入图像进行预处理 + // 预处理包括创建一个单位矩阵,计算图像的最大维度,对输入图像进行仿射变换,然后将变换后的图像缩放到指定的字符大小 + Mat preprocessChar(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; // 获取字符大小 - Mat transformMat = Mat::eye(2, 3, CV_32F); - int m = max(w, h); - transformMat.at(0, 2) = float(m / 2 - w / 2); + 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()); + Mat warpImage(m, m, in.type()); // 创建一个新的图像,大小为m*m,类型与输入图像相同 warpAffine(in, warpImage, transformMat, warpImage.size(), INTER_LINEAR, - BORDER_CONSTANT, Scalar(0)); + BORDER_CONSTANT, Scalar(0)); // 对输入图像进行仿射变换 Mat out; - cv::resize(warpImage, out, Size(charSize, charSize)); + cv::resize(warpImage, out, Size(charSize, charSize)); // 将变换后的图像缩放到指定的字符大小 - return out; + return out; // 返回处理后的图像 } - Rect GetChineseRect(const Rect rectSpe) { - int height = rectSpe.height; - float newwidth = rectSpe.width * 1.10f; - int x = rectSpe.x; - int y = rectSpe.y; + // 接受一个特定的矩形,然后计算新的宽度(为原宽度的110%) + // 新的x坐标(为原x坐标减去新宽度的110%),如果新的x坐标小于0,将其设置为0 + // 然后创建一个新的矩形,并返回 + Rect GetChineseRect(const Rect rectSpe) + { + int height = rectSpe.height; // 获取特定矩形的高度 + float newwidth = rectSpe.width * 1.10f; // 计算新的宽度,为原宽度的110% + int x = rectSpe.x; // 获取特定矩形的x坐标 + int y = rectSpe.y; // 获取特定矩形的y坐标 - int newx = x - int(newwidth * 1.10f); - newx = newx > 0 ? newx : 0; + int newx = x - int(newwidth * 1.10f); // 计算新的x坐标,为原x坐标减去新宽度的110% + newx = newx > 0 ? newx : 0; // 如果新的x坐标小于0,将其设置为0 - Rect a(newx, y, int(newwidth), height); + Rect a(newx, y, int(newwidth), height); // 创建一个新的矩形 - return a; + return a; // 返回新的矩形 } - bool verifyCharSizes(Rect r) { + // 接受一个矩形,然后计算字符的宽高比,矩形的宽高比…… + bool verifyCharSizes(Rect r) + { // Char sizes 45x90 - float aspect = 45.0f / 90.0f; - float charAspect = (float) r.width / (float) r.height; - float error = 0.35f; - float minHeight = 25.f; - float maxHeight = 50.f; + float aspect = 45.0f / 90.0f; // 计算字符的宽高比 + float charAspect = (float)r.width / (float)r.height; // 计算矩形的宽高比 + float error = 0.35f; // 设置误差范围 + float minHeight = 25.f; // 设置最小高度 + float maxHeight = 50.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; // 设置最大宽高比 // bb area - int bbArea = r.width * r.height; + int bbArea = r.width * r.height; // 计算矩形的面积 if (charAspect > minAspect && charAspect < maxAspect /*&& - r.rows >= minHeight && r.rows < maxHeight*/) - return true; + r.rows >= minHeight && r.rows < maxHeight*/ + ) + return true; // 如果矩形的宽高比在最小和最大宽高比之间,返回true else - return false; + return false; // 否则返回false } - - Mat scaleImage(const Mat &image, const Size &maxSize, double &scale_ratio) { + // 计算图像宽度和最大宽度的比例,图像高度和最大高度的比例,获取宽度和高度比例中的最大值 + // 计算新的宽度和新的高度,将图像缩放到新的大小,设置缩放比例 + // 如果图像的大小已经小于或等于最大大小,直接返回原图像,设置缩放比例为1.0 + Mat scaleImage(const Mat &image, const Size &maxSize, double &scale_ratio) + { Mat ret; - if (image.cols > maxSize.width || image.rows > maxSize.height) { - double widthRatio = image.cols / (double) maxSize.width; - double heightRatio = image.rows / (double) maxSize.height; - double m_real_to_scaled_ratio = max(widthRatio, heightRatio); + if (image.cols > maxSize.width || image.rows > maxSize.height) + { + double widthRatio = image.cols / (double)maxSize.width; // 计算图像宽度和最大宽度的比例 + double heightRatio = image.rows / (double)maxSize.height; // 计算图像高度和最大高度的比例 + double m_real_to_scaled_ratio = max(widthRatio, heightRatio); // 获取宽度和高度比例中的最大值 - int newWidth = int(image.cols / m_real_to_scaled_ratio); - int newHeight = int(image.rows / m_real_to_scaled_ratio); + int newWidth = int(image.cols / m_real_to_scaled_ratio); // 计算新的宽度 + int newHeight = int(image.rows / m_real_to_scaled_ratio); // 计算新的高度 - cv::resize(image, ret, Size(newWidth, newHeight), 0, 0); - scale_ratio = m_real_to_scaled_ratio; - } else { - ret = image; - scale_ratio = 1.0; + cv::resize(image, ret, Size(newWidth, newHeight), 0, 0); // 将图像缩放到新的大小 + scale_ratio = m_real_to_scaled_ratio; // 设置缩放比例 + } + else + { + ret = image; // 如果图像的大小已经小于或等于最大大小,直接返回原图像 + scale_ratio = 1.0; // 设置缩放比例为1.0 } - return ret; + return ret; // 返回处理后的图像 } - -// Scale back RotatedRect - RotatedRect scaleBackRRect(const RotatedRect &rr, const float scale_ratio) { - float width = rr.size.width * scale_ratio; - float height = rr.size.height * scale_ratio; - float x = rr.center.x * scale_ratio; - float y = rr.center.y * scale_ratio; - RotatedRect mserRect(Point2f(x, y), Size2f(width, height), rr.angle); - - return mserRect; + // Scale back RotatedRect + // 将一个旋转矩形(RotatedRect)按照给定的缩放比例进行反向缩放,返回一个新的旋转矩形 + RotatedRect scaleBackRRect(const RotatedRect &rr, const float scale_ratio) + { + float width = rr.size.width * scale_ratio; // 根据缩放比例计算原始宽度 + float height = rr.size.height * scale_ratio; // 根据缩放比例计算原始高度 + float x = rr.center.x * scale_ratio; // 根据缩放比例计算原始中心点的x坐标 + float y = rr.center.y * scale_ratio; // 根据缩放比例计算原始中心点的y坐标 + RotatedRect mserRect(Point2f(x, y), Size2f(width, height), rr.angle); // 创建一个新的RotatedRect对象,使用原始的尺寸和角度 + + return mserRect; // 返回新的RotatedRect对象 } - bool verifyPlateSize(Rect mr) { - float error = 0.6f; + // 验证一个矩形是否符合预设的车牌尺寸,包括面积和宽高比,如果符合则返回true,否则返回false + bool verifyPlateSize(Rect mr) + { + float error = 0.6f; // 设置误差范围 // Spain car plate size: 52x11 aspect 4,7272 // China car plate size: 440mm*140mm,aspect 3.142857 // Real car plate size: 136 * 32, aspect 4 - float aspect = 3.75; + float aspect = 3.75; // 设置期望的宽高比 // Set a min and max area. All other patchs are discarded - // int min= 1*aspect*1; // minimum area - // int max= 2000*aspect*2000; // maximum area - int min = 34 * 8 * 1; // minimum area - int max = 34 * 8 * 200; // maximum area + int min = 34 * 8 * 1; // 设置最小面积 + int max = 34 * 8 * 200; // 设置最大面积 // Get only patchs that match to a respect ratio. - float rmin = aspect - aspect * error; - float rmax = aspect + aspect * error; - - float area = float(mr.height * mr.width); - float r = (float) mr.width / (float) mr.height; - if (r < 1) r = (float) mr.height / (float) mr.width; + float rmin = aspect - aspect * error; // 计算最小宽高比 + float rmax = aspect + aspect * error; // 计算最大宽高比 - // cout << "area:" << area << endl; - // cout << "r:" << r << endl; + float area = float(mr.height * mr.width); // 计算矩形的面积 + float r = (float) mr.width / (float) mr.height; // 计算矩形的宽高比 + if (r < 1) r = (float) mr.height / (float) mr.width; // 如果宽高比小于1,取其倒数 + // 判断矩形的面积和宽高比是否在指定的范围内 if ((area < min || area > max) || (r < rmin || r > rmax)) - return false; + return false; // 如果不在指定范围内,返回false else - return true; + return true; // 如果在指定范围内,返回true } +// 接收一个RotatedRect对象和一个布尔值showDebug作为参数 +// 该函数的主要目的是验证一个旋转矩形(通常是车牌)是否符合预设的尺寸和比例 bool verifyRotatedPlateSizes(RotatedRect mr, bool showDebug) { - float error = 0.65f; + float error = 0.65f; // 定义误差值error,以及车牌的宽高比aspect // Spain car plate size: 52x11 aspect 4,7272 // China car plate size: 440mm*140mm,aspect 3.142857 @@ -853,10 +1094,11 @@ void clearBorder(const Mat &img, Rect& cropRect) { // Set a min and max area. All other patchs are discarded // int min= 1*aspect*1; // minimum area // int max= 2000*aspect*2000; // maximum area - //int min = 34 * 8 * 1; // minimum area - //int max = 34 * 8 * 200; // maximum area + // int min = 34 * 8 * 1; // minimum area + // int max = 34 * 8 * 200; // maximum area // Get only patchs that match to a respect ratio. + // 计算最小和最大的面积min和max,以及最小和最大的宽高比aspect_min和aspect_max float aspect_min = aspect - aspect * error; float aspect_max = aspect + aspect * error; @@ -865,52 +1107,59 @@ void clearBorder(const Mat &img, Rect& cropRect) { float min = float(width_min * width_min / aspect_max); // minimum area float max = float(width_max * width_max / aspect_min); // maximum area - +// 获取旋转矩形的宽度、高度、面积、宽高比和角度 float width = mr.size.width; float height = mr.size.height; float area = width * height; float ratio = width / height; float angle = mr.angle; - if (ratio < 1) { + if (ratio < 1) { // 如果宽高比小于1,交换宽度和高度的值,并调整角度 swap(width, height); ratio = width / height; angle = 90.f + angle; - //std::cout << "angle:" << angle << std::endl; + // std::cout << "angle:" << angle << std::endl; } - +// 定义角度的最小和最大值 float angle_min = -60.f; float angle_max = 60.f; //std::cout << "aspect_min:" << aspect_min << std::endl; //std::cout << "aspect_max:" << aspect_max << std::endl; - +// 通过一系列的条件判断,验证旋转矩形是否符合预设的尺寸和比例 +// 如果不符合,返回false;如果符合,返回true if (area < min || area > max) { if (0 && showDebug) { std::cout << "area < min || area > max: " << area << std::endl; } return false; - } else if (ratio < aspect_min || ratio > aspect_max) { - if (0 && showDebug) { + } + else if (ratio < aspect_min || ratio > aspect_max) + { + if (0 && showDebug) + { std::cout << "ratio < aspect_min || ratio > aspect_max: " << ratio << std::endl; } return false; - } else if (angle < angle_min || angle > angle_max) { - if (0 && showDebug) { + } + else if (angle < angle_min || angle > angle_max) + { + if (0 && showDebug) + { std::cout << "angle < angle_min || angle > angle_max: " << angle << std::endl; } return false; - } else if (width < width_min || width > width_max) { - if (0 && showDebug) { std::cout << "width < width_min || width > width_max: " << width << std::endl; } return false; - } else { + } + else + { return true; } @@ -918,42 +1167,47 @@ void clearBorder(const Mat &img, Rect& cropRect) { } //! non-maximum suppression +// 该函数实现了非极大值抑制(Non-Maximum Suppression,NMS)的功能 +// 非极大值抑制是一种常用于目标检测中的技术,用于消除多余的重叠区域 +// 该函数主要删除inVec中重叠度过高的CCharacter对象,只保留最具代表性的对象 void NMStoCharacter(std::vector &inVec, double overlap) { - - std::sort(inVec.begin(), inVec.end()); - +// 函数接受两个参数,一个是CCharacter对象的向量inVec,另一个是重叠阈值overlap + std::sort(inVec.begin(), inVec.end()); // 对inVec进行排序 +// 遍历inVec中的每一个CCharacter对象 std::vector::iterator it = inVec.begin(); for (; it != inVec.end(); ++it) { - CCharacter charSrc = *it; + CCharacter charSrc = *it; // 对于每一个CCharacter对象,获取其位置信息rectSrc //std::cout << "plateScore:" << plateSrc.getPlateScore() << std::endl; Rect rectSrc = charSrc.getCharacterPos(); - +// 遍历当前CCharacter对象之后的所有CCharacter对象 std::vector::iterator itc = it + 1; for (; itc != inVec.end();) { - CCharacter charComp = *itc; + CCharacter charComp = *itc; // 对于每一个后续的CCharacter对象,也获取其位置信息rectComp Rect rectComp = charComp.getCharacterPos(); //Rect rectInter = rectSrc & rectComp; //Rect rectUnion = rectSrc | rectComp; //double r = double(rectInter.area()) / double(rectUnion.area()); - +// 计算当前CCharacter对象与后续CCharacter对象的交并比(Intersection over Union,IoU) float iou = computeIOU(rectSrc, rectComp); - +// 如果IoU大于设定的阈值overlap,则删除后续的CCharacter对象。否则,继续检查下一个CCharacter对象 if (iou > overlap) { itc = inVec.erase(itc); - } else { + } + else + { ++itc; } } } } -// judge weather two CCharacter are nearly the same; - bool compareCharRect(const CCharacter &character1, const CCharacter &character2) { Rect rect1 = character1.getCharacterPos(); Rect rect2 = character2.getCharacterPos(); // the character in plate are similar height + // 判断两个字符的高度是否相似。计算它们的高度差的绝对值并用最小的高度归一化, + // 如果比值超过0.25,则函数返回false表示两个字符不相似 float width_1 = float(rect1.width); float height_1 = float(rect1.height); @@ -967,6 +1221,8 @@ void clearBorder(const Mat &img, Rect& cropRect) { return false; // the character in plate are similar in the y-axis + // 判断两个字符在y轴上的位置是否相似。计算它们左上角的y坐标差的绝对值并用最小的高度归一化, + // 如果比值超过0.5,则函数返回false表示两个字符不相似 float y_1 = float(rect1.tl().y); float y_2 = float(rect2.tl().y); @@ -977,6 +1233,8 @@ void clearBorder(const Mat &img, Rect& cropRect) { return false; // the character center in plate are not to near in the x-axis + // 检查两个字符在x轴上的中心位置是否过近。计算它们中心的x坐标差的绝对值并用最小的高度归一化, + // 如果比值小于0.25,则函数返回false表示两个字符不相似 float x_1 = float(rect1.tl().x + rect1.width / 2); float x_2 = float(rect2.tl().x + rect2.width / 2); @@ -987,6 +1245,8 @@ void clearBorder(const Mat &img, Rect& cropRect) { return false; // the character in plate are near in the x-axis but not very near + // 检查两个字符在x轴上的位置是否过远。计算它们在x轴上距离的差并用最小的高度归一化, + // 如果比值超过1.0,则函数返回false表示两个字符不相似 float x_margin_left = float(min(rect1.br().x, rect2.br().x)); float x_margin_right = float(max(rect1.tl().x, rect2.tl().x)); @@ -996,29 +1256,43 @@ void clearBorder(const Mat &img, Rect& cropRect) { if (x_margin_diff_ratio > 1.0) return false; + // 如果所有的检查都通过,函数则返回true表示这两个字符相似 return true; } -//! merge chars to group, using the similarity + //! merge chars to group, using the similarity + // 定义了一个名为 mergeCharToGroup 的函数, + // 该函数的功能是将相似的字符合并为一组 void mergeCharToGroup(std::vector vecRect, - std::vector> &charGroupVec) { - + std::vector> &charGroupVec) + { + // 定义一个 labels 向量来存储每个字符的类别标签 std::vector labels; + // 使用 partition 函数计算字符的分区,每个分区表示一组相似的字符。 + // partition 函数的第三个参数是一个函数指针, + // 用于比较两个 CCharacter 实例是否相似(compareCharRect)。 int numbers = 0; + if (vecRect.size() > 0) numbers = partition(vecRect, labels, &compareCharRect); - for (size_t j = 0; j < size_t(numbers); j++) { + // 遍历所有的类别(分区) + for (size_t j = 0; j < size_t(numbers); j++) + { + // 对于每一个类别,创建一个新的 charGroup 向量来表示一个字符组 std::vector charGroup; - for (size_t t = 0; t < vecRect.size(); t++) { + // 遍历所有的字符,如果字符的类别标签是当前的类别,则将该字符加入字符组 + for (size_t t = 0; t < vecRect.size(); t++) + { int label = labels[t]; if (label == j) charGroup.push_back(vecRect[t]); } - + // 检查每个字符组的大小(charGroup.size()),如果大小小于 2(即只包含一个字符), + // 则不会将它加入到输出的字符组中 (charGroupVec)。会继续计算循环下一个字符 if (charGroup.size() < 2) continue; @@ -1026,22 +1300,31 @@ void clearBorder(const Mat &img, Rect& cropRect) { } } - + // 在输入的图像 (image) 上绘制一个旋转的矩形 (rrect)。 void rotatedRectangle(InputOutputArray image, RotatedRect rrect, const Scalar &color, int thickness, int lineType, - int shift) { + int shift) + { + // 定义了一个 Point2f 类型的数组,用于存储矩形的四个顶点 Point2f rect_points[4]; rrect.points(rect_points); - for (int j = 0; j < 4; j++) { + // 循环遍历矩形的四个顶点,使用 OpenCV 中的 line 函数将每一对相邻的顶点连接起来绘制边。 + // (j + 1) % 4 确保了连接的是当前点和下一个点 + for (int j = 0; j < 4; j++) + { cv::line(image, rect_points[j], rect_points[(j + 1) % 4], color, thickness, lineType, shift); } } - + // 用于处理一些种子字符(可能就是车牌的字符)。 + // 它接收一系列的输入参数,大部分用于控制和帮助进行字符搜索的条件。 + // 从整体上看,这是一个筛选并排序字符(按给定条件)的过程 void searchWeakSeed(const std::vector &charVec, std::vector &mserCharacter, double thresh1, double thresh2, const Vec4f &line, Point &boundaryPoint, const Rect &maxrect, Rect &plateResult, Mat result, - CharSearchDirection searchDirection) { + CharSearchDirection searchDirection) + { + // 设置一些初始值,包括直线的斜率 k,线上某点 (x_1, y_1),空的搜索结果向量 searchWeakSeedVec float k = line[1] / line[0]; float x_1 = line[2]; float y_1 = line[3]; @@ -1049,30 +1332,40 @@ void clearBorder(const Mat &img, Rect& cropRect) { std::vector searchWeakSeedVec; searchWeakSeedVec.reserve(8); - for (auto weakSeed : charVec) { + // 遍历传入的 charVec 中的每一个种子字符 weakSeed,并对其内容进行一些检查 for (auto weakSeed : charVec) + { Rect weakRect = weakSeed.getCharacterPos(); - //cv::rectangle(result, weakRect, Scalar(255, 0, 255)); + // cv::rectangle(result, weakRect, Scalar(255, 0, 255)); Point weakCenter(weakRect.tl().x + weakRect.width / 2, weakRect.tl().y + weakRect.height / 2); - float x_2 = (float) weakCenter.x; + float x_2 = (float)weakCenter.x; - if (searchDirection == CharSearchDirection::LEFT) { - if (weakCenter.x + weakRect.width / 2 > boundaryPoint.x) { + // 如果种子在搜索方向(searchDirection)对应的边界(boundaryPoint.x)的另一侧则直接跳过 + if (searchDirection == CharSearchDirection::LEFT) + { + if (weakCenter.x + weakRect.width / 2 > boundaryPoint.x) + { continue; } - } else if (searchDirection == CharSearchDirection::RIGHT) { - if (weakCenter.x - weakRect.width / 2 < boundaryPoint.x) { + } + else if (searchDirection == CharSearchDirection::RIGHT) + { + if (weakCenter.x - weakRect.width / 2 < boundaryPoint.x) + { continue; } } float y_2l = k * (x_2 - x_1) + y_1; - float y_2 = (float) weakCenter.y; + float y_2 = (float)weakCenter.y; float y_diff_ratio = abs(y_2l - y_2) / maxrect.height; - if (y_diff_ratio < thresh1) { + // 计算weakCenter点到直线 (k, x_1, y_1) 的垂直距离比率 y_diff_ratio。 + // 如果比率小于阈值 thresh1,则计算 weakRect(就是种子字符的边界矩形)的高和宽与 maxrect 矩形的高和宽之间的差别比率, + if (y_diff_ratio < thresh1) + { float width_1 = float(maxrect.width); float height_1 = float(maxrect.height); @@ -1085,26 +1378,36 @@ void clearBorder(const Mat &img, Rect& cropRect) { float width_diff = abs(width_1 - width_2); double width_diff_ratio = width_diff / maxrect.width; - if (height_diff_ratio < thresh1 && width_diff_ratio < 0.5) { - //std::cout << "h" << height_diff_ratio << std::endl; - //std::cout << "w" << width_diff_ratio << std::endl; + // 如果两个比率都小于相应阈值(thresh1 和 0.5),则将 weakSeed 添加到 searchWeakSeedVec + if (height_diff_ratio < thresh1 && width_diff_ratio < 0.5) + { + // std::cout << "h" << height_diff_ratio << std::endl; + // std::cout << "w" << width_diff_ratio << std::endl; searchWeakSeedVec.push_back(weakSeed); - } else { - + } + else + { } } } // form right to left to split - if (searchWeakSeedVec.size() != 0) { - if (searchDirection == CharSearchDirection::LEFT) { + // 根据搜索方向,将 searchWeakSeedVec 中的种子字符按 x 值(getCharacterPos().tl().x)进行排序 + if (searchWeakSeedVec.size() != 0) + { + if (searchDirection == CharSearchDirection::LEFT) + { std::sort(searchWeakSeedVec.begin(), searchWeakSeedVec.end(), - [](const CCharacter &r1, const CCharacter &r2) { + [](const CCharacter &r1, const CCharacter &r2) + { return r1.getCharacterPos().tl().x > r2.getCharacterPos().tl().x; }); - } else if (searchDirection == CharSearchDirection::RIGHT) { + } + else if (searchDirection == CharSearchDirection::RIGHT) + { std::sort(searchWeakSeedVec.begin(), searchWeakSeedVec.end(), - [](const CCharacter &r1, const CCharacter &r2) { + [](const CCharacter &r1, const CCharacter &r2) + { return r1.getCharacterPos().tl().x < r2.getCharacterPos().tl().x; }); } @@ -1114,19 +1417,26 @@ void clearBorder(const Mat &img, Rect& cropRect) { Point firstWeakCenter(firstWeakRect.tl().x + firstWeakRect.width / 2, firstWeakRect.tl().y + firstWeakRect.height / 2); - float ratio = (float) abs(firstWeakCenter.x - boundaryPoint.x) / (float) maxrect.height; - if (ratio > thresh2) { - if (0) { + // 从排序后的向量 searchWeakSeedVec 中取出第一个种子字符(即最左或最右一个)。 + // 如果其中心到边界点 boundaryPoint 的水平距离占总高度的比例 ratio 大于 thresh2,则返回 + float ratio = (float)abs(firstWeakCenter.x - boundaryPoint.x) / (float)maxrect.height; + if (ratio > thresh2) + { + if (0) + { std::cout << "search seed ratio:" << ratio << std::endl; } return; } + // 否则将其添加到 mserCharacter,并将该种子的 Rect 合并到 plateResult,并更新 boundaryPoint mserCharacter.push_back(firstWeakSeed); plateResult |= firstWeakRect; boundaryPoint = firstWeakCenter; - for (size_t weakSeedIndex = 0; weakSeedIndex + 1 < searchWeakSeedVec.size(); weakSeedIndex++) { + // 遍历 searchWeakSeedVec 中的其他种子字符,并检查其与前一个字符之间的 x 距离比例 x_margin_diff_ratio。 + for (size_t weakSeedIndex = 0; weakSeedIndex + 1 < searchWeakSeedVec.size(); weakSeedIndex++) + { CCharacter weakSeed = searchWeakSeedVec[weakSeedIndex]; CCharacter weakSeedCompare = searchWeakSeedVec[weakSeedIndex + 1]; @@ -1150,21 +1460,31 @@ void clearBorder(const Mat &img, Rect& cropRect) { float x_margin_diff = abs(x_margin_left - x_margin_right); double x_margin_diff_ratio = x_margin_diff / min(height_1, height_2); - if (x_margin_diff_ratio > thresh2) { - if (0) { + // 如果 x_margin_diff_ratio 小于 thresh2,则将其添加到 mserCharacter 并更新 boundaryPoint 和 plateResult + if (x_margin_diff_ratio > thresh2) + { + if (0) + { std::cout << "search seed x_margin_diff_ratio:" << x_margin_diff_ratio << std::endl; } break; - } else { + } + else + { //::rectangle(result, weakRect, Scalar(255, 0, 0), 1); mserCharacter.push_back(weakSeedCompare); plateResult |= weakRect; - if (searchDirection == CharSearchDirection::LEFT) { - if (weakCenter.x < boundaryPoint.x) { + if (searchDirection == CharSearchDirection::LEFT) + { + if (weakCenter.x < boundaryPoint.x) + { boundaryPoint = weakCenter; } - } else if (searchDirection == CharSearchDirection::RIGHT) { - if (weakCenter.x > boundaryPoint.x) { + } + else if (searchDirection == CharSearchDirection::RIGHT) + { + if (weakCenter.x > boundaryPoint.x) + { boundaryPoint = weakCenter; } } @@ -1173,10 +1493,13 @@ void clearBorder(const Mat &img, Rect& cropRect) { } } + // 在一张车牌图像上沿特定的直线搜索字符。给定一条线(通过其极坐标定义)、一个搜索方向、以及一些配置参数 void slideWindowSearch(const Mat &image, std::vector &slideCharacter, const Vec4f &line, Point &fromPoint, const Vec2i &dist, double ostu_level, float ratioWindow, float threshIsCharacter, const Rect &maxrect, Rect &plateResult, - CharSearchDirection searchDirection, bool isChinese, Mat &result) { + CharSearchDirection searchDirection, bool isChinese, Mat &result) + { + // 根据给定的直线方程计算搜索窗口的中心位置 float k = line[1] / line[0]; float x_1 = line[2]; float y_1 = line[3]; @@ -1184,22 +1507,33 @@ void clearBorder(const Mat &img, Rect& cropRect) { int slideLength = int(ratioWindow * maxrect.width); int slideStep = 1; int fromX = 0; - if (searchDirection == CharSearchDirection::LEFT) { + // 根据搜索方向(左/右)设置搜索的起始 x 坐标 + if (searchDirection == CharSearchDirection::LEFT) + { fromX = fromPoint.x - dist[0]; - } else if (searchDirection == CharSearchDirection::RIGHT) { + } + else if (searchDirection == CharSearchDirection::RIGHT) + { fromX = fromPoint.x + dist[0]; } + // 在指定的滑动窗口范围内,以步长 slideStep 对图像进行遍历 std::vector charCandidateVec; - for (int slideX = -slideLength; slideX < slideLength; slideX += slideStep) { + for (int slideX = -slideLength; slideX < slideLength; slideX += slideStep) + { + + // 计算区域的中心点 float x_slide = 0; - if (searchDirection == CharSearchDirection::LEFT) { + if (searchDirection == CharSearchDirection::LEFT) + { x_slide = float(fromX - slideX); - } else if (searchDirection == CharSearchDirection::RIGHT) { + } + else if (searchDirection == CharSearchDirection::RIGHT) + { x_slide = float(fromX + slideX); } - + // 在 result 图像上绘制圆点,可用于可视化 float y_slide = k * (x_slide - x_1) + y_1; Point2f p_slide(x_slide, y_slide); cv::circle(result, p_slide, 2, Scalar(255, 255, 255), 1); @@ -1207,26 +1541,30 @@ void clearBorder(const Mat &img, Rect& cropRect) { int chineseWidth = int(maxrect.width * 1.05); int chineseHeight = int(maxrect.height * 1.05); + // 生成候选区域的矩形框 rect Rect rect(Point2f(x_slide - chineseWidth / 2, y_slide - chineseHeight / 2), Size(chineseWidth, chineseHeight)); + // 对矩形框内的图像区域进行检查以确保没有越界 if (rect.tl().x < 0 || rect.tl().y < 0 || rect.br().x >= image.cols || rect.br().y >= image.rows) continue; Mat region = image(rect); Mat binary_region; - + // 对矩形框内的图像区域进行二值化处理 cv::threshold(region, binary_region, ostu_level, 255, CV_THRESH_BINARY); - //double ostu_level = threshold(region, binary_region, 0, 255, CV_THRESH_BINARY | CV_THRESH_OTSU); - //std::cout << "ostu_level:" << ostu_level << std::endl;*/ + // double ostu_level = threshold(region, binary_region, 0, 255, CV_THRESH_BINARY | CV_THRESH_OTSU); + // std::cout << "ostu_level:" << ostu_level << std::endl;*/ + // 对字符图像进行预处理,获取准备用于分类的图像 Mat charInput = preprocessChar(binary_region, 20); - if (0) { + if (0) + { imshow("charInput", charInput); waitKey(0); destroyWindow("charInput"); } - + // 建立把检测到的候选区域存储为 CCharacter 对象,并添加到候选向量 charCandidateVec 中 CCharacter charCandidate; charCandidate.setCharacterPos(rect); charCandidate.setCharacterMat(charInput); @@ -1234,26 +1572,38 @@ void clearBorder(const Mat &img, Rect& cropRect) { charCandidateVec.push_back(charCandidate); } - if (isChinese) { + // 根据 isChinese 标记对 charCandidateVec 中的所有候选项进行分类 + if (isChinese) + { CharsIdentify::instance()->classifyChinese(charCandidateVec); - } else { + } + else + { CharsIdentify::instance()->classify(charCandidateVec); } + // 应用非极大抑制,过滤重叠的候选区域 double overlapThresh = 0.1; NMStoCharacter(charCandidateVec, overlapThresh); - for (auto character : charCandidateVec) { + // 遍历筛选后的候选字符集,对于那些高于特定分数阈值且不是字符 "1" 的区域 + for (auto character : charCandidateVec) + { + // 更新 plateResult 并添加到 slideCharacter 结果容器中 Rect rect = character.getCharacterPos(); + // 更新搜索的起始点 fromPoint Point center(rect.tl().x + rect.width / 2, rect.tl().y + rect.height / 2); - if (character.getCharacterScore() > threshIsCharacter && character.getCharacterStr() != "1") { - //cv::rectangle(result, rect, Scalar(255, 255, 255), 1); + // 识别结果存于 slideCharacter 和 plateResult 参数中,其中 plateResult 为识别出的车牌区域 + if (character.getCharacterScore() > threshIsCharacter && character.getCharacterStr() != "1") + { + // cv::rectangle(result, rect, Scalar(255, 255, 255), 1); plateResult |= rect; slideCharacter.push_back(character); fromPoint = center; - if (0) { + if (0) + { std::cout << "label:" << character.getCharacterStr(); std::cout << "__score:" << character.getCharacterScore() << std::endl; } @@ -1261,9 +1611,9 @@ void clearBorder(const Mat &img, Rect& cropRect) { } } - bool judegMDOratio2(const Mat &image, const Rect &rect, std::vector &contour, Mat &result, const float thresh, - bool useExtendHeight) { + bool useExtendHeight) + { Mat mser = image(rect); Mat mser_mat; @@ -1279,8 +1629,9 @@ void clearBorder(const Mat &img, Rect& cropRect) { float MserDiffOstuRatio = float(countdiff) / float(rect.area()); - if (MserDiffOstuRatio > thresh) { - //std::cout << "MserDiffOstuRatio:" << MserDiffOstuRatio << std::endl; + if (MserDiffOstuRatio > thresh) + { + // std::cout << "MserDiffOstuRatio:" << MserDiffOstuRatio << std::endl; /*imshow("tmpMat", mser_mat); waitKey(0); imshow("tmpMat", thresh_mat); @@ -1293,7 +1644,8 @@ void clearBorder(const Mat &img, Rect& cropRect) { return true; } - Rect interRect(const Rect &a, const Rect &b) { + Rect interRect(const Rect &a, const Rect &b) + { Rect c; int x1 = a.x > b.x ? a.x : b.x; int y1 = a.y > b.y ? a.y : b.y; @@ -1306,7 +1658,8 @@ void clearBorder(const Mat &img, Rect& cropRect) { return c; } - Rect mergeRect(const Rect &a, const Rect &b) { + Rect mergeRect(const Rect &a, const Rect &b) + { Rect c; int x1 = a.x < b.x ? a.x : b.x; int y1 = a.y < b.y ? a.y : b.y; @@ -1318,7 +1671,8 @@ void clearBorder(const Mat &img, Rect& cropRect) { } bool computeIOU(const RotatedRect &rrect1, const RotatedRect &rrect2, const int width, const int height, - const float thresh, float &result) { + const float thresh, float &result) + { Rect_ safe_rect1; calcSafeRect(rrect1, width, height, safe_rect1); @@ -1328,18 +1682,20 @@ void clearBorder(const Mat &img, Rect& cropRect) { Rect inter = interRect(safe_rect1, safe_rect2); Rect urect = mergeRect(safe_rect1, safe_rect2); - float iou = (float) inter.area() / (float) urect.area(); + float iou = (float)inter.area() / (float)urect.area(); result = iou; - if (iou > thresh) { + if (iou > thresh) + { return true; } return false; } - float computeIOU(const RotatedRect &rrect1, const RotatedRect &rrect2, const int width, const int height) { + float computeIOU(const RotatedRect &rrect1, const RotatedRect &rrect2, const int width, const int height) + { Rect_ safe_rect1; calcSafeRect(rrect1, width, height, safe_rect1); @@ -1349,51 +1705,56 @@ void clearBorder(const Mat &img, Rect& cropRect) { Rect inter = interRect(safe_rect1, safe_rect2); Rect urect = mergeRect(safe_rect1, safe_rect2); - float iou = (float) inter.area() / (float) urect.area(); - //std::cout << "iou" << iou << std::endl; + float iou = (float)inter.area() / (float)urect.area(); + // std::cout << "iou" << iou << std::endl; return iou; } - bool computeIOU(const Rect &rect1, const Rect &rect2, const float thresh, float &result) { + bool computeIOU(const Rect &rect1, const Rect &rect2, const float thresh, float &result) + { Rect inter = interRect(rect1, rect2); Rect urect = mergeRect(rect1, rect2); - float iou = (float) inter.area() / (float) urect.area(); + float iou = (float)inter.area() / (float)urect.area(); result = iou; - if (iou > thresh) { + if (iou > thresh) + { return true; } return false; } - float computeIOU(const Rect &rect1, const Rect &rect2) { + float computeIOU(const Rect &rect1, const Rect &rect2) + { Rect inter = interRect(rect1, rect2); Rect urect = mergeRect(rect1, rect2); - float iou = (float) inter.area() / (float) urect.area(); + float iou = (float)inter.area() / (float)urect.area(); return iou; } - -// the slope are nealy the same along the line -// if one slope is much different others, it should be outliers -// this function to remove it + // the slope are nealy the same along the line + // if one slope is much different others, it should be outliers + // this function to remove it void removeRightOutliers(std::vector &charGroup, std::vector &out_charGroup, double thresh1, - double thresh2, Mat result) { + double thresh2, Mat result) + { std::sort(charGroup.begin(), charGroup.end(), - [](const CCharacter &r1, const CCharacter &r2) { + [](const CCharacter &r1, const CCharacter &r2) + { return r1.getCenterPoint().x < r2.getCenterPoint().x; }); std::vector slopeVec; float slope_last = 0; - for (size_t charGroup_i = 0; charGroup_i + 1 < charGroup.size(); charGroup_i++) { + for (size_t charGroup_i = 0; charGroup_i + 1 < charGroup.size(); charGroup_i++) + { // line_between_two_points Vec4f line_btp; CCharacter leftChar = charGroup.at(charGroup_i); @@ -1405,52 +1766,64 @@ void clearBorder(const Mat &img, Rect& cropRect) { float slope = line_btp[1] / line_btp[0]; slopeVec.push_back(slope); - if (0) { + if (0) + { cv::line(result, leftChar.getCenterPoint(), rightChar.getCenterPoint(), Scalar(0, 0, 255)); } } int uniformity_count = 0; int outlier_index = -1; - for (size_t slopeVec_i = 0; slopeVec_i + 1 < slopeVec.size(); slopeVec_i++) { + for (size_t slopeVec_i = 0; slopeVec_i + 1 < slopeVec.size(); slopeVec_i++) + { float slope_1 = slopeVec.at(slopeVec_i); float slope_2 = slopeVec.at(slopeVec_i + 1); float slope_diff = abs(slope_1 - slope_2); - if (0) { + if (0) + { std::cout << "slope_diff:" << slope_diff << std::endl; } - if (slope_diff <= thresh1) { + if (slope_diff <= thresh1) + { uniformity_count++; } - if (0) { + if (0) + { std::cout << "slope_1:" << slope_1 << std::endl; std::cout << "slope_2:" << slope_2 << std::endl; } - if (1/*(slope_1 <= 0 && slope_2 >= 0) || (slope_1 >= 0 && slope_2 <= 0)*/) { - if (uniformity_count >= 2 && slope_diff >= thresh2) { + if (1 /*(slope_1 <= 0 && slope_2 >= 0) || (slope_1 >= 0 && slope_2 <= 0)*/) + { + if (uniformity_count >= 2 && slope_diff >= thresh2) + { outlier_index = slopeVec_i + 2; break; } } } - if (0) { + if (0) + { std::cout << "uniformity_count:" << uniformity_count << std::endl; std::cout << "outlier_index:" << outlier_index << std::endl; } - for (int charGroup_i = 0; charGroup_i < (int) charGroup.size(); charGroup_i++) { - if (charGroup_i != outlier_index) { + for (int charGroup_i = 0; charGroup_i < (int)charGroup.size(); charGroup_i++) + { + if (charGroup_i != outlier_index) + { CCharacter theChar = charGroup.at(charGroup_i); out_charGroup.push_back(theChar); } } - if (0) { + if (0) + { std::cout << "end:" << std::endl; } } - Rect getSafeRect(Point2f center, float width, float height, Mat image) { + Rect getSafeRect(Point2f center, float width, float height, Mat image) + { int rows = image.rows; int cols = image.cols; @@ -1465,35 +1838,40 @@ void clearBorder(const Mat &img, Rect& cropRect) { x_tl = x_tl > 0.f ? x_tl : 0.f; y_tl = y_tl > 0.f ? y_tl : 0.f; - x_br = x_br < (float) image.cols ? x_br : (float) image.cols; - y_br = y_br < (float) image.rows ? y_br : (float) image.rows; + x_br = x_br < (float)image.cols ? x_br : (float)image.cols; + y_br = y_br < (float)image.rows ? y_br : (float)image.rows; - Rect rect(Point((int) x_tl, int(y_tl)), Point((int) x_br, int(y_br))); + Rect rect(Point((int)x_tl, int(y_tl)), Point((int)x_br, int(y_br))); return rect; } -// based on the assumptions: distance beween two nearby characters in plate are the same. -// add not found rect and combine two small and near rect. + // based on the assumptions: distance beween two nearby characters in plate are the same. + // add not found rect and combine two small and near rect. void reFoundAndCombineRect(std::vector &mserCharacter, float min_thresh, float max_thresh, - Vec2i dist, Rect maxrect, Mat result) { - if (mserCharacter.size() == 0) { + Vec2i dist, Rect maxrect, Mat result) + { + if (mserCharacter.size() == 0) + { return; } std::sort(mserCharacter.begin(), mserCharacter.end(), - [](const CCharacter &r1, const CCharacter &r2) { + [](const CCharacter &r1, const CCharacter &r2) + { return r1.getCenterPoint().x < r2.getCenterPoint().x; }); int comparDist = dist[0] * dist[0] + dist[1] * dist[1]; - if (0) { + if (0) + { std::cout << "comparDist:" << comparDist << std::endl; } std::vector reCharacters; size_t mserCharacter_i = 0; - for (; mserCharacter_i + 1 < mserCharacter.size(); mserCharacter_i++) { + for (; mserCharacter_i + 1 < mserCharacter.size(); mserCharacter_i++) + { CCharacter leftChar = mserCharacter.at(mserCharacter_i); CCharacter rightChar = mserCharacter.at(mserCharacter_i + 1); @@ -1506,35 +1884,40 @@ void clearBorder(const Mat &img, Rect& cropRect) { // distance between two centers int distance2 = x_diff * x_diff + y_diff * y_diff; - if (0) { + if (0) + { std::cout << "distance2:" << distance2 << std::endl; } - float ratio = (float) distance2 / (float) comparDist; - if (ratio > max_thresh) { - float x_add = (float) (leftCenter.x + rightCenter.x) / 2.f; - float y_add = (float) (leftCenter.y + rightCenter.y) / 2.f; + float ratio = (float)distance2 / (float)comparDist; + if (ratio > max_thresh) + { + float x_add = (float)(leftCenter.x + rightCenter.x) / 2.f; + float y_add = (float)(leftCenter.y + rightCenter.y) / 2.f; - float width = (float) maxrect.width; - float height = (float) maxrect.height; + float width = (float)maxrect.width; + float height = (float)maxrect.height; float x_tl = (x_add - width / 2.f); float y_tl = (y_add - height / 2.f); - //Rect rect_add((int)x_tl, (int)y_tl, (int)width, (int)height); + // Rect rect_add((int)x_tl, (int)y_tl, (int)width, (int)height); Rect rect_add = getSafeRect(Point2f(x_add, y_add), width, height, result); reCharacters.push_back(leftChar); CCharacter charAdd; - charAdd.setCenterPoint(Point((int) x_add, (int) y_add)); + charAdd.setCenterPoint(Point((int)x_add, (int)y_add)); charAdd.setCharacterPos(rect_add); reCharacters.push_back(charAdd); - if (1) { + if (1) + { cv::rectangle(result, rect_add, Scalar(0, 128, 255)); } - } else if (ratio < min_thresh) { + } + else if (ratio < min_thresh) + { Rect rect_union = leftChar.getCharacterPos() | rightChar.getCharacterPos(); /*float x_add = (float)(leftCenter.x + rightCenter.x) / 2.f; float y_add = (float)(leftCenter.y + rightCenter.y) / 2.f;*/ @@ -1545,28 +1928,33 @@ void clearBorder(const Mat &img, Rect& cropRect) { charAdd.setCenterPoint(Point(x_add, y_add)); charAdd.setCharacterPos(rect_union); reCharacters.push_back(charAdd); - if (1) { + if (1) + { cv::rectangle(result, rect_union, Scalar(0, 128, 255)); } mserCharacter_i++; - } else { + } + else + { reCharacters.push_back(leftChar); } } - if (mserCharacter_i + 1 == mserCharacter.size()) { + if (mserCharacter_i + 1 == mserCharacter.size()) + { reCharacters.push_back(mserCharacter.at(mserCharacter_i)); } mserCharacter = reCharacters; } - - void removeOutliers(std::vector &charGroup, double thresh, Mat result) { + void removeOutliers(std::vector &charGroup, double thresh, Mat result) + { std::vector points; Vec4f line; - for (auto character : charGroup) { + for (auto character : charGroup) + { points.push_back(character.getCenterPoint()); } @@ -1582,34 +1970,40 @@ void clearBorder(const Mat &img, Rect& cropRect) { float b = -1; float c = y_1 - k * x_1; float sumdistance = 0; - for (auto character : charGroup) { + for (auto character : charGroup) + { Point center = character.getCenterPoint(); float distance = (a * center.x + b * center.y + c) / std::sqrt(a * a + b * b); std::cout << "distance:" << distance << std::endl; sumdistance += distance; } - float avgdistance = sumdistance / (float) charGroup.size(); + float avgdistance = sumdistance / (float)charGroup.size(); std::vector::iterator it = charGroup.begin(); - for (; it != charGroup.end();) { + for (; it != charGroup.end();) + { Point center = it->getCenterPoint(); float distance = a * center.x + b * center.y + c; float ratio = distance / avgdistance; std::cout << "ratio:" << ratio << std::endl; - if (ratio > (float) thresh) { + if (ratio > (float)thresh) + { it = charGroup.erase(it); - } else { + } + else + { ++it; } } } -//! use verify size to first generate char candidates + //! use verify size to first generate char candidates void mserCharMatch(const Mat &src, std::vector &match, std::vector &out_plateVec_blue, std::vector &out_plateVec_yellow, bool usePlateMser, std::vector &out_plateRRect_blue, std::vector &out_plateRRect_yellow, int img_index, - bool showDebug) { + bool showDebug) + { Mat image = src; std::vector>> all_contours; @@ -1629,7 +2023,7 @@ void clearBorder(const Mat &img, Rect& cropRect) { const int imageArea = image.rows * image.cols; const int delta = 1; - //const int delta = CParams::instance()->getParam2i();; + // const int delta = CParams::instance()->getParam2i();; const int minArea = 30; const double maxAreaRatio = 0.05; @@ -1642,7 +2036,8 @@ void clearBorder(const Mat &img, Rect& cropRect) { // color_index = 1 : mser+, detect dark characters, which is in yellow plate. #pragma omp parallel for - for (int color_index = 0; color_index < 2; color_index++) { + for (int color_index = 0; color_index < 2; color_index++) + { Color the_color = flags.at(color_index); std::vector charVec; @@ -1662,23 +2057,29 @@ void clearBorder(const Mat &img, Rect& cropRect) { const int char_max_count = 7; // verify char size and output to rects; - for (size_t index = 0; index < size; index++) { + for (size_t index = 0; index < size; index++) + { Rect rect = all_boxes.at(color_index)[index]; std::vector &contour = all_contours.at(color_index)[index]; // sometimes a plate could be a mser rect, so we could // also use mser algorithm to find plate - if (usePlateMser) { + if (usePlateMser) + { RotatedRect rrect = minAreaRect(Mat(contour)); - if (verifyRotatedPlateSizes(rrect)) { - //rotatedRectangle(result, rrect, Scalar(255, 0, 0), 2); - if (the_color == BLUE) out_plateRRect_blue.push_back(rrect); - if (the_color == YELLOW) out_plateRRect_yellow.push_back(rrect); + if (verifyRotatedPlateSizes(rrect)) + { + // rotatedRectangle(result, rrect, Scalar(255, 0, 0), 2); + if (the_color == BLUE) + out_plateRRect_blue.push_back(rrect); + if (the_color == YELLOW) + out_plateRRect_yellow.push_back(rrect); } } // find character - if (verifyCharSizes(rect)) { + if (verifyCharSizes(rect)) + { Mat mserMat = adaptive_image_from_points(contour, rect, Size(char_size, char_size)); Mat charInput = preprocessChar(mserMat, char_size); Rect charRect = rect; @@ -1687,11 +2088,12 @@ void clearBorder(const Mat &img, Rect& cropRect) { Mat tmpMat; double ostu_level = cv::threshold(image(charRect), tmpMat, 0, 255, CV_THRESH_BINARY | CV_THRESH_OTSU); - //cv::circle(result, center, 3, Scalar(0, 0, 255), 2); + // cv::circle(result, center, 3, Scalar(0, 0, 255), 2); // use judegMDOratio2 function to // remove the small lines in character like "zh-cuan" - if (judegMDOratio2(image, rect, contour, result)) { + if (judegMDOratio2(image, rect, contour, result)) + { CCharacter charCandidate; charCandidate.setCharacterPos(charRect); charCandidate.setCharacterMat(charInput); @@ -1713,7 +2115,7 @@ void clearBorder(const Mat &img, Rect& cropRect) { // use nms to remove the character are not likely to be true. double overlapThresh = 0.6; - //double overlapThresh = CParams::instance()->getParam1f(); + // double overlapThresh = CParams::instance()->getParam1f(); NMStoCharacter(charVec, overlapThresh); charVec.shrink_to_fit(); @@ -1724,19 +2126,25 @@ void clearBorder(const Mat &img, Rect& cropRect) { std::vector littleSeedVec; littleSeedVec.reserve(64); - //size_t charCan_size = charVec.size(); - for (auto charCandidate : charVec) { - //CCharacter& charCandidate = charVec[char_index]; + // size_t charCan_size = charVec.size(); + for (auto charCandidate : charVec) + { + // CCharacter& charCandidate = charVec[char_index]; Rect rect = charCandidate.getCharacterPos(); double score = charCandidate.getCharacterScore(); - if (charCandidate.getIsStrong()) { + if (charCandidate.getIsStrong()) + { strongSeedVec.push_back(charCandidate); - } else if (charCandidate.getIsWeak()) { + } + else if (charCandidate.getIsWeak()) + { weakSeedVec.push_back(charCandidate); - //cv::rectangle(result, rect, Scalar(255, 0, 255)); - } else if (charCandidate.getIsLittle()) { + // cv::rectangle(result, rect, Scalar(255, 0, 255)); + } + else if (charCandidate.getIsLittle()) + { littleSeedVec.push_back(charCandidate); - //cv::rectangle(result, rect, Scalar(255, 0, 255)); + // cv::rectangle(result, rect, Scalar(255, 0, 255)); } } @@ -1758,7 +2166,8 @@ void clearBorder(const Mat &img, Rect& cropRect) { // to fit a line which is the middle line of the plate. std::vector plateVec; plateVec.reserve(16); - for (auto charGroup : charGroupVec) { + for (auto charGroup : charGroupVec) + { Rect plateResult = charGroup[0].getCharacterPos(); std::vector points; points.reserve(32); @@ -1781,9 +2190,10 @@ void clearBorder(const Mat &img, Rect& cropRect) { roCharGroup.reserve(32); removeRightOutliers(charGroup, roCharGroup, 0.2, 0.5, result); - //roCharGroup = charGroup; + // roCharGroup = charGroup; - for (auto character : roCharGroup) { + for (auto character : roCharGroup) + { Rect charRect = character.getCharacterPos(); cv::rectangle(result, charRect, Scalar(0, 255, 0), 1); plateResult |= charRect; @@ -1791,40 +2201,46 @@ void clearBorder(const Mat &img, Rect& cropRect) { Point center(charRect.tl().x + charRect.width / 2, charRect.tl().y + charRect.height / 2); points.push_back(center); mserCharVec.push_back(character); - //cv::circle(result, center, 3, Scalar(0, 255, 0), 2); + // cv::circle(result, center, 3, Scalar(0, 255, 0), 2); ostu_level_sum += character.getOstuLevel(); - if (charRect.area() > maxarea) { + if (charRect.area() > maxarea) + { maxrect = charRect; maxarea = charRect.area(); } - if (center.x < leftPoint.x) { + if (center.x < leftPoint.x) + { leftPoint = center; } - if (center.x > rightPoint.x) { + if (center.x > rightPoint.x) + { rightPoint = center; } } - double ostu_level_avg = ostu_level_sum / (double) roCharGroup.size(); - if (1 && showDebug) { + double ostu_level_avg = ostu_level_sum / (double)roCharGroup.size(); + if (1 && showDebug) + { std::cout << "ostu_level_avg:" << ostu_level_avg << std::endl; } - float ratio_maxrect = (float) maxrect.width / (float) maxrect.height; + float ratio_maxrect = (float)maxrect.width / (float)maxrect.height; - if (points.size() >= 2 && ratio_maxrect >= 0.3) { + if (points.size() >= 2 && ratio_maxrect >= 0.3) + { fitLine(Mat(points), line, CV_DIST_L2, 0, 0.01, 0.01); float k = line[1] / line[0]; - //float angle = atan(k) * 180 / (float)CV_PI; - //std::cout << "k:" << k << std::endl; - //std::cout << "angle:" << angle << std::endl; - //std::cout << "cos:" << 0.3 * cos(k) << std::endl; - //std::cout << "ratio_maxrect:" << ratio_maxrect << std::endl; + // float angle = atan(k) * 180 / (float)CV_PI; + // std::cout << "k:" << k << std::endl; + // std::cout << "angle:" << angle << std::endl; + // std::cout << "cos:" << 0.3 * cos(k) << std::endl; + // std::cout << "ratio_maxrect:" << ratio_maxrect << std::endl; std::sort(mserCharVec.begin(), mserCharVec.end(), - [](const CCharacter &r1, const CCharacter &r2) { + [](const CCharacter &r1, const CCharacter &r2) + { return r1.getCharacterPos().tl().x < r2.getCharacterPos().tl().x; }); @@ -1843,7 +2259,8 @@ void clearBorder(const Mat &img, Rect& cropRect) { // two near characters in the plate, use dist we can // judege how to computer the max search range, and choose the // best location of the sliding window in the next steps. - for (size_t mser_i = 0; mser_i + 1 < mserCharVec.size(); mser_i++) { + for (size_t mser_i = 0; mser_i + 1 < mserCharVec.size(); mser_i++) + { Rect charRect = mserCharVec.at(mser_i).getCharacterPos(); Point center(charRect.tl().x + charRect.width / 2, charRect.tl().y + charRect.height / 2); @@ -1855,27 +2272,28 @@ void clearBorder(const Mat &img, Rect& cropRect) { Vec2i distVec(charRectCompare.x - charRect.x, charRectCompare.y - charRect.y); distVecVec.push_back(distVec); - //if (dist < mindist) { - // mindist = dist; - // mindistVec = distVec; - //} + // if (dist < mindist) { + // mindist = dist; + // mindistVec = distVec; + // } } std::sort(distVecVec.begin(), distVecVec.end(), - [](const Vec2i &r1, const Vec2i &r2) { + [](const Vec2i &r1, const Vec2i &r2) + { return r1[0] < r2[0]; }); avgdistVec = distVecVec.at(int((distVecVec.size() - 1) / 2.f)); - //float step = 10.f * (float)maxrect.width; - //float step = (float)mindistVec[0]; - float step = (float) avgdistVec[0]; + // float step = 10.f * (float)maxrect.width; + // float step = (float)mindistVec[0]; + float step = (float)avgdistVec[0]; - //cv::line(result, Point2f(line[2] - step, line[3] - k*step), Point2f(line[2] + step, k*step + line[3]), Scalar(255, 255, 255)); + // cv::line(result, Point2f(line[2] - step, line[3] - k*step), Point2f(line[2] + step, k*step + line[3]), Scalar(255, 255, 255)); cv::line(result, Point2f(midCenter.x - step, midCenter.y - k * step), Point2f(midCenter.x + step, k * step + midCenter.y), Scalar(255, 255, 255)); - //cv::circle(result, leftPoint, 3, Scalar(0, 0, 255), 2); + // cv::circle(result, leftPoint, 3, Scalar(0, 0, 255), 2); CPlate plate; plate.setPlateLeftPoint(leftPoint); @@ -1897,7 +2315,8 @@ void clearBorder(const Mat &img, Rect& cropRect) { // because we use strong seed to build the middle lines of the plate, // we can simply use this to consider weak seeds only lie in the // near place of the middle line - for (auto plate : plateVec) { + for (auto plate : plateVec) + { Vec4f line = plate.getPlateLine(); Point leftPoint = plate.getPlateLeftPoint(); Point rightPoint = plate.getPlateRightPoint(); @@ -1929,16 +2348,20 @@ void clearBorder(const Mat &img, Rect& cropRect) { // draw weak seed and little seed from line; // search for mser rect - if (1 && showDebug) { + if (1 && showDebug) + { std::cout << "search for mser rect:" << std::endl; } - if (0 && showDebug) { + if (0 && showDebug) + { std::stringstream ss(std::stringstream::in | std::stringstream::out); - ss << "resources/image/tmp/" << img_index << "_1_" << "searcgMserRect.jpg"; + ss << "resources/image/tmp/" << img_index << "_1_" + << "searcgMserRect.jpg"; imwrite(ss.str(), result); } - if (1 && showDebug) { + if (1 && showDebug) + { std::cout << "mserCharacter:" << mserCharacter.size() << std::endl; } @@ -1947,25 +2370,30 @@ void clearBorder(const Mat &img, Rect& cropRect) { // the same line as the strong seed. The judge condition contains the distance // between strong seed and weak seed , and the rect simily of each other to improve // the roubustnedd of the seed growing algorithm. - if (mserCharacter.size() < char_max_count) { + if (mserCharacter.size() < char_max_count) + { double thresh1 = 0.15; double thresh2 = 2.0; searchWeakSeed(searchCandidate, searchRightWeakSeed, thresh1, thresh2, line, rightPoint, maxrect, plateResult, result, CharSearchDirection::RIGHT); - if (1 && showDebug) { + if (1 && showDebug) + { std::cout << "searchRightWeakSeed:" << searchRightWeakSeed.size() << std::endl; } - for (auto seed : searchRightWeakSeed) { + for (auto seed : searchRightWeakSeed) + { cv::rectangle(result, seed.getCharacterPos(), Scalar(255, 0, 0), 1); mserCharacter.push_back(seed); } searchWeakSeed(searchCandidate, searchLeftWeakSeed, thresh1, thresh2, line, leftPoint, maxrect, plateResult, result, CharSearchDirection::LEFT); - if (1 && showDebug) { + if (1 && showDebug) + { std::cout << "searchLeftWeakSeed:" << searchLeftWeakSeed.size() << std::endl; } - for (auto seed : searchLeftWeakSeed) { + for (auto seed : searchLeftWeakSeed) + { cv::rectangle(result, seed.getCharacterPos(), Scalar(255, 0, 0), 1); mserCharacter.push_back(seed); } @@ -1986,8 +2414,10 @@ void clearBorder(const Mat &img, Rect& cropRect) { // sometimes there are still some characters could not be captured by mser algorithm, // such as blur, low light ,and some chinese characters like zh-cuan. // to handle this ,we use a simple slide window method to find them. - if (mserCharacter.size() < char_max_count) { - if (1 && showDebug) { + if (mserCharacter.size() < char_max_count) + { + if (1 && showDebug) + { std::cout << "search chinese:" << std::endl; std::cout << "judege the left is chinese:" << std::endl; } @@ -1998,29 +2428,33 @@ void clearBorder(const Mat &img, Rect& cropRect) { // the first thing is to judge the left charcater is // or not the chinese. bool leftIsChinese = false; - if (1) { + if (1) + { std::sort(mserCharacter.begin(), mserCharacter.end(), - [](const CCharacter &r1, const CCharacter &r2) { + [](const CCharacter &r1, const CCharacter &r2) + { return r1.getCharacterPos().tl().x < r2.getCharacterPos().tl().x; }); CCharacter leftChar = mserCharacter[0]; - //Rect theRect = adaptive_charrect_from_rect(leftChar.getCharacterPos(), image.cols, image.rows); + // Rect theRect = adaptive_charrect_from_rect(leftChar.getCharacterPos(), image.cols, image.rows); Rect theRect = leftChar.getCharacterPos(); - //cv::rectangle(result, theRect, Scalar(255, 0, 0), 1); + // cv::rectangle(result, theRect, Scalar(255, 0, 0), 1); Mat region = image(theRect); Mat binary_region; ostu_level = cv::threshold(region, binary_region, 0, 255, CV_THRESH_BINARY | CV_THRESH_OTSU); - if (1 && showDebug) { + if (1 && showDebug) + { std::cout << "left : ostu_level:" << ostu_level << std::endl; } - //plate.setOstuLevel(ostu_level); + // plate.setOstuLevel(ostu_level); Mat charInput = preprocessChar(binary_region, char_size); - if (0 /*&& showDebug*/) { + if (0 /*&& showDebug*/) + { imshow("charInput", charInput); waitKey(0); destroyWindow("charInput"); @@ -2029,9 +2463,10 @@ void clearBorder(const Mat &img, Rect& cropRect) { std::string label = ""; float maxVal = -2.f; leftIsChinese = CharsIdentify::instance()->isCharacter(charInput, label, maxVal, true); - //auto character = CharsIdentify::instance()->identifyChinese(charInput, maxVal, leftIsChinese); - //label = character.second; - if (0 /* && showDebug*/) { + // auto character = CharsIdentify::instance()->identifyChinese(charInput, maxVal, leftIsChinese); + // label = character.second; + if (0 /* && showDebug*/) + { std::cout << "isChinese:" << leftIsChinese << std::endl; std::cout << "chinese:" << label; std::cout << "__score:" << maxVal << std::endl; @@ -2042,16 +2477,19 @@ void clearBorder(const Mat &img, Rect& cropRect) { // this means we meed to slide a window to find the missed mser rect. // search for sliding window float ratioWindow = 0.4f; - //float ratioWindow = CParams::instance()->getParam3f(); + // float ratioWindow = CParams::instance()->getParam3f(); float threshIsCharacter = 0.8f; - //float threshIsCharacter = CParams::instance()->getParam3f(); - if (!leftIsChinese) { + // float threshIsCharacter = CParams::instance()->getParam3f(); + if (!leftIsChinese) + { slideWindowSearch(image, slideLeftWindow, line, leftPoint, dist, ostu_level, ratioWindow, threshIsCharacter, maxrect, plateResult, CharSearchDirection::LEFT, true, result); - if (1 && showDebug) { + if (1 && showDebug) + { std::cout << "slideLeftWindow:" << slideLeftWindow.size() << std::endl; } - for (auto window : slideLeftWindow) { + for (auto window : slideLeftWindow) + { cv::rectangle(result, window.getCharacterPos(), Scalar(0, 0, 255), 1); mserCharacter.push_back(window); } @@ -2060,54 +2498,63 @@ void clearBorder(const Mat &img, Rect& cropRect) { // if we still have less than max count characters, // we need to slide a window to right to search for the missed mser rect. - if (mserCharacter.size() < char_max_count) { + if (mserCharacter.size() < char_max_count) + { // change ostu_level float ratioWindow = 0.4f; - //float ratioWindow = CParams::instance()->getParam3f(); + // float ratioWindow = CParams::instance()->getParam3f(); float threshIsCharacter = 0.8f; - //float threshIsCharacter = CParams::instance()->getParam3f(); + // float threshIsCharacter = CParams::instance()->getParam3f(); slideWindowSearch(image, slideRightWindow, line, rightPoint, dist, plate.getOstuLevel(), ratioWindow, threshIsCharacter, maxrect, plateResult, CharSearchDirection::RIGHT, false, result); - if (1 && showDebug) { + if (1 && showDebug) + { std::cout << "slideRightWindow:" << slideRightWindow.size() << std::endl; } - for (auto window : slideRightWindow) { + for (auto window : slideRightWindow) + { cv::rectangle(result, window.getCharacterPos(), Scalar(0, 0, 255), 1); mserCharacter.push_back(window); } } // computer the plate angle - float angle = atan(k) * 180 / (float) CV_PI; - if (1 && showDebug) { + float angle = atan(k) * 180 / (float)CV_PI; + if (1 && showDebug) + { std::cout << "k:" << k << std::endl; std::cout << "angle:" << angle << std::endl; } // the plateResult rect need to be enlarge to contains all the plate, // not only the character area. - float widthEnlargeRatio = 1.15f; //1.15f; - float heightEnlargeRatio = 1.25f; //1.25f; + float widthEnlargeRatio = 1.15f; // 1.15f; + float heightEnlargeRatio = 1.25f; // 1.25f; RotatedRect platePos( - Point2f((float) plateResult.x + plateResult.width / 2.f, (float) plateResult.y + plateResult.height / 2.f), + Point2f((float)plateResult.x + plateResult.width / 2.f, (float)plateResult.y + plateResult.height / 2.f), Size2f(plateResult.width * widthEnlargeRatio, maxrect.height * heightEnlargeRatio), angle); // justify the size is likely to be a plate size. - if (verifyRotatedPlateSizes(platePos)) { + if (verifyRotatedPlateSizes(platePos)) + { rotatedRectangle(result, platePos, Scalar(0, 0, 255), 1); plate.setPlatePos(platePos); plate.setPlateColor(the_color); plate.setPlateLocateType(CMSER); - if (the_color == BLUE) out_plateVec_blue.push_back(plate); - if (the_color == YELLOW) out_plateVec_yellow.push_back(plate); + if (the_color == BLUE) + out_plateVec_blue.push_back(plate); + if (the_color == YELLOW) + out_plateVec_yellow.push_back(plate); } // use deskew to rotate the image, so we need the binary image. - if (1) { - for (auto mserChar : mserCharacter) { + if (1) + { + for (auto mserChar : mserCharacter) + { Rect rect = mserChar.getCharacterPos(); match.at(color_index)(rect) = 255; } @@ -2115,109 +2562,142 @@ void clearBorder(const Mat &img, Rect& cropRect) { } } - if (0 /*&& showDebug*/) { + if (0 /*&& showDebug*/) + { imshow("result", result); waitKey(0); destroyWindow("result"); } - if (0) { + if (0) + { imshow("match", match.at(color_index)); waitKey(0); destroyWindow("match"); } - if (1) { + if (1) + { std::stringstream ss(std::stringstream::in | std::stringstream::out); ss << "resources/image/tmp/plateDetect/plate_" << img_index << "_" << the_color << ".jpg"; imwrite(ss.str(), result); } } - - } -// this spatial_ostu algorithm are robust to -// the plate which has the same light shine, which is that -// the light in the left of the plate is strong than the right. - void spatial_ostu(InputArray _src, int grid_x, int grid_y, Color type) { + // this spatial_ostu algorithm are robust to + // the plate which has the same light shine, which is that + // the light in the left of the plate is strong than the right. + 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++) { + 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) { + if (type == BLUE) + { cv::threshold(src_cell, src_cell, 0, 255, CV_THRESH_OTSU + CV_THRESH_BINARY); - } else if (type == YELLOW) { + } + else if (type == YELLOW) + { cv::threshold(src_cell, src_cell, 0, 255, CV_THRESH_OTSU + CV_THRESH_BINARY_INV); - } else if (type == WHITE) { + } + else if (type == WHITE) + { cv::threshold(src_cell, src_cell, 0, 255, CV_THRESH_OTSU + CV_THRESH_BINARY_INV); - } else { + } + else + { cv::threshold(src_cell, src_cell, 0, 255, CV_THRESH_OTSU + CV_THRESH_BINARY); } } } } - - bool mat_valid_position(const Mat &mat, int row, int col) { + bool mat_valid_position(const Mat &mat, int row, int col) + { return row >= 0 && col >= 0 && row < mat.rows && col < mat.cols; } - - template - static void mat_set_invoke(Mat &mat, int row, int col, const Scalar &value) { - if (1 == mat.channels()) { - mat.at(row, col) = (T) value.val[0]; - } else if (3 == mat.channels()) { + template + static void mat_set_invoke(Mat &mat, int row, int col, const Scalar &value) + { + if (1 == mat.channels()) + { + mat.at(row, col) = (T)value.val[0]; + } + else if (3 == mat.channels()) + { T *ptr_src = mat.ptr(row, col); - *ptr_src++ = (T) value.val[0]; - *ptr_src++ = (T) value.val[1]; - *ptr_src = (T) value.val[2]; - } else if (4 == mat.channels()) { + *ptr_src++ = (T)value.val[0]; + *ptr_src++ = (T)value.val[1]; + *ptr_src = (T)value.val[2]; + } + else if (4 == mat.channels()) + { T *ptr_src = mat.ptr(row, col); - *ptr_src++ = (T) value.val[0]; - *ptr_src++ = (T) value.val[1]; - *ptr_src++ = (T) value.val[2]; - *ptr_src = (T) value.val[3]; + *ptr_src++ = (T)value.val[0]; + *ptr_src++ = (T)value.val[1]; + *ptr_src++ = (T)value.val[2]; + *ptr_src = (T)value.val[3]; } } - void setPoint(Mat &mat, int row, int col, const Scalar &value) { - if (CV_8U == mat.depth()) { + void setPoint(Mat &mat, int row, int col, const Scalar &value) + { + if (CV_8U == mat.depth()) + { mat_set_invoke(mat, row, col, value); - } else if (CV_8S == mat.depth()) { + } + else if (CV_8S == mat.depth()) + { mat_set_invoke(mat, row, col, value); - } else if (CV_16U == mat.depth()) { + } + else if (CV_16U == mat.depth()) + { mat_set_invoke(mat, row, col, value); - } else if (CV_16S == mat.depth()) { + } + else if (CV_16S == mat.depth()) + { mat_set_invoke(mat, row, col, value); - } else if (CV_32S == mat.depth()) { + } + else if (CV_32S == mat.depth()) + { mat_set_invoke(mat, row, col, value); - } else if (CV_32F == mat.depth()) { + } + else if (CV_32F == mat.depth()) + { mat_set_invoke(mat, row, col, value); - } else if (CV_64F == mat.depth()) { + } + else if (CV_64F == mat.depth()) + { mat_set_invoke(mat, row, col, value); } } - Rect adaptive_charrect_from_rect(const Rect &rect, int maxwidth, int maxheight, bool useExtendHeight) { + Rect adaptive_charrect_from_rect(const Rect &rect, int maxwidth, int maxheight, bool useExtendHeight) + { int expendWidth = 0; int extendHeight = 0; - if (rect.height > 3 * rect.width) { + if (rect.height > 3 * rect.width) + { expendWidth = int((int(rect.height * 0.5f) - rect.width) * 0.5f); - if (useExtendHeight) { + if (useExtendHeight) + { extendHeight = int(rect.height * 0.3f); } } - //Rect resultRect(rect.tl().x - expendWidth, rect.tl().y, - // rect.width + expendWidth * 2, rect.height); + // Rect resultRect(rect.tl().x - expendWidth, rect.tl().y, + // rect.width + expendWidth * 2, rect.height); int tlx = rect.tl().x - expendWidth > 0 ? rect.tl().x - expendWidth : 0; int tly = rect.tl().y - extendHeight > 0 ? rect.tl().y - extendHeight : 0; @@ -2229,26 +2709,31 @@ void clearBorder(const Mat &img, Rect& cropRect) { return resultRect; } - Mat adaptive_image_from_points(const std::vector &points, const Rect &rect, const Size &size, const Scalar &backgroundColor /* = ml_color_white */, - const Scalar &forgroundColor /* = ml_color_black */, bool gray /* = true */) { + const Scalar &forgroundColor /* = ml_color_black */, bool gray /* = true */) + { int expendHeight = 0; int expendWidth = 0; - if (rect.width > rect.height) { + if (rect.width > rect.height) + { expendHeight = (rect.width - rect.height) / 2; - } else if (rect.height > rect.width) { + } + else if (rect.height > rect.width) + { expendWidth = (rect.height - rect.width) / 2; } Mat image(rect.height + expendHeight * 2, rect.width + expendWidth * 2, gray ? CV_8UC1 : CV_8UC3, backgroundColor); - for (int i = 0; i < (int) points.size(); ++i) { + for (int i = 0; i < (int)points.size(); ++i) + { Point point = points[i]; Point currentPt(point.x - rect.tl().x + expendWidth, point.y - rect.tl().y + expendHeight); - if (mat_valid_position(image, currentPt.y, currentPt.x)) { + if (mat_valid_position(image, currentPt.y, currentPt.x)) + { setPoint(image, currentPt.y, currentPt.x, forgroundColor); } } @@ -2259,27 +2744,29 @@ void clearBorder(const Mat &img, Rect& cropRect) { return result; } -// calc safe Rect -// if not exit, return false + // calc safe Rect + // if not exit, return false bool calcSafeRect(const RotatedRect &roi_rect, const Mat &src, - Rect_ &safeBoundRect) { + Rect_ &safeBoundRect) + { Rect_ boudRect = roi_rect.boundingRect(); float tl_x = boudRect.x > 0 ? boudRect.x : 0; float tl_y = boudRect.y > 0 ? boudRect.y : 0; float br_x = boudRect.x + boudRect.width < src.cols - ? boudRect.x + boudRect.width - 1 - : src.cols - 1; + ? boudRect.x + boudRect.width - 1 + : src.cols - 1; float br_y = boudRect.y + boudRect.height < src.rows - ? boudRect.y + boudRect.height - 1 - : src.rows - 1; + ? boudRect.y + boudRect.height - 1 + : src.rows - 1; float roi_width = br_x - tl_x; float roi_height = br_y - tl_y; - if (roi_width <= 0 || roi_height <= 0) return false; + if (roi_width <= 0 || roi_height <= 0) + return false; // a new rect not out the range of mat @@ -2289,23 +2776,25 @@ void clearBorder(const Mat &img, Rect& cropRect) { } bool calcSafeRect(const RotatedRect &roi_rect, const int width, const int height, - Rect_ &safeBoundRect) { + Rect_ &safeBoundRect) + { Rect_ boudRect = roi_rect.boundingRect(); float tl_x = boudRect.x > 0 ? boudRect.x : 0; float tl_y = boudRect.y > 0 ? boudRect.y : 0; float br_x = boudRect.x + boudRect.width < width - ? boudRect.x + boudRect.width - 1 - : width - 1; + ? boudRect.x + boudRect.width - 1 + : width - 1; float br_y = boudRect.y + boudRect.height < height - ? boudRect.y + boudRect.height - 1 - : height - 1; + ? boudRect.y + boudRect.height - 1 + : height - 1; float roi_width = br_x - tl_x; float roi_height = br_y - tl_y; - if (roi_width <= 0 || roi_height <= 0) return false; + if (roi_width <= 0 || roi_height <= 0) + return false; // a new rect not out the range of mat @@ -2314,10 +2803,10 @@ void clearBorder(const Mat &img, Rect& cropRect) { return true; } - - Mat uniformResize(const Mat &result, float &scale) { + Mat uniformResize(const Mat &result, float &scale) + { const int RESULTWIDTH = kShowWindowWidth; // 640 930 - const int RESULTHEIGHT = kShowWindowHeight; // 540 710 + const int RESULTHEIGHT = kShowWindowHeight; // 540 710 Mat img_window; img_window.create(RESULTHEIGHT, RESULTWIDTH, CV_8UC3); @@ -2326,30 +2815,38 @@ void clearBorder(const Mat &img, Rect& cropRect) { int nCols = result.cols; Mat result_resize; - if (nCols <= img_window.cols && nRows <= img_window.rows) { + if (nCols <= img_window.cols && nRows <= img_window.rows) + { result_resize = result; - } else if (nCols > img_window.cols && nRows <= img_window.rows) { + } + else if (nCols > img_window.cols && nRows <= img_window.rows) + { scale = float(img_window.cols) / float(nCols); resize(result, result_resize, Size(), scale, scale, CV_INTER_AREA); - - } else if (nCols <= img_window.cols && nRows > img_window.rows) { + } + else if (nCols <= img_window.cols && nRows > img_window.rows) + { scale = float(img_window.rows) / float(nRows); resize(result, result_resize, Size(), scale, scale, CV_INTER_AREA); - - } else if (nCols > img_window.cols && nRows > img_window.rows) { + } + else if (nCols > img_window.cols && nRows > img_window.rows) + { float scale1 = float(img_window.cols) / float(nCols); float scale2 = float(img_window.rows) / float(nRows); scale = scale1 < scale2 ? scale1 : scale2; resize(result, result_resize, Size(), scale, scale, CV_INTER_AREA); - } else { + } + else + { result_resize = result; } return result_resize; } - Mat uniformResizePlates (const Mat &result, float &scale) { + Mat uniformResizePlates(const Mat &result, float &scale) + { const int RESULTWIDTH = kPlateResizeWidth; // 640 930 - const int RESULTHEIGHT = kPlateResizeHeight; // 540 710 + const int RESULTHEIGHT = kPlateResizeHeight; // 540 710 Mat img_window; img_window.create(RESULTHEIGHT, RESULTWIDTH, CV_8UC3); @@ -2358,46 +2855,51 @@ void clearBorder(const Mat &img, Rect& cropRect) { int nCols = result.cols; Mat result_resize; - if (nCols <= img_window.cols && nRows <= img_window.rows) { + if (nCols <= img_window.cols && nRows <= img_window.rows) + { result_resize = result; } - else if (nCols > img_window.cols && nRows <= img_window.rows) { + else if (nCols > img_window.cols && nRows <= img_window.rows) + { scale = float(img_window.cols) / float(nCols); resize(result, result_resize, Size(), scale, scale, CV_INTER_AREA); - } - else if (nCols <= img_window.cols && nRows > img_window.rows) { + else if (nCols <= img_window.cols && nRows > img_window.rows) + { scale = float(img_window.rows) / float(nRows); resize(result, result_resize, Size(), scale, scale, CV_INTER_AREA); - } - else if (nCols > img_window.cols && nRows > img_window.rows) { + else if (nCols > img_window.cols && nRows > img_window.rows) + { float scale1 = float(img_window.cols) / float(nCols); float scale2 = float(img_window.rows) / float(nRows); scale = scale1 < scale2 ? scale1 : scale2; resize(result, result_resize, Size(), scale, scale, CV_INTER_AREA); } - else { + else + { result_resize = result; } return result_resize; } - - - void showDectectResults(const Mat &img, const vector &plateVec, size_t num) { + void showDectectResults(const Mat &img, const vector &plateVec, size_t num) + { int index = 0; - if (1) { + if (1) + { Mat result; img.copyTo(result); - for (size_t j = 0; j < plateVec.size(); j++) { + for (size_t j = 0; j < plateVec.size(); j++) + { // add plates to left corner - const CPlate& item = plateVec.at(j); + const CPlate &item = plateVec.at(j); Mat plateMat = item.getPlateMat(); int height = 36; int width = 136; - if (height * index + height < result.rows) { + if (height * index + height < result.rows) + { Mat imageRoi = result(Rect(0, 0 + height * index, width, height)); addWeighted(imageRoi, 0, plateMat, 1, 0, imageRoi); } @@ -2411,9 +2913,12 @@ void clearBorder(const Mat &img, Rect& cropRect) { Point2f rect_points[4]; minRect.points(rect_points); Scalar lineColor = Scalar(255, 255, 255); - if (item.getPlateLocateType() == SOBEL) lineColor = Scalar(255, 0, 0); - if (item.getPlateLocateType() == COLOR) lineColor = Scalar(0, 255, 0); - if (item.getPlateLocateType() == CMSER) lineColor = Scalar(0, 0, 255); + if (item.getPlateLocateType() == SOBEL) + lineColor = Scalar(255, 0, 0); + if (item.getPlateLocateType() == COLOR) + lineColor = Scalar(0, 255, 0); + if (item.getPlateLocateType() == CMSER) + lineColor = Scalar(0, 0, 255); for (int j = 0; j < 4; j++) line(result, rect_points[j], rect_points[(j + 1) % 4], lineColor, 2, 8); @@ -2422,11 +2927,12 @@ void clearBorder(const Mat &img, Rect& cropRect) { } } - Mat showResult(const Mat &result, int img_index) { + Mat showResult(const Mat &result, int img_index) + { namedWindow("EasyPR", CV_WINDOW_AUTOSIZE); const int RESULTWIDTH = kShowWindowWidth; // 640 930 - const int RESULTHEIGHT = kShowWindowHeight; // 540 710 + const int RESULTHEIGHT = kShowWindowHeight; // 540 710 Mat img_window; img_window.create(RESULTHEIGHT, RESULTWIDTH, CV_8UC3); @@ -2435,24 +2941,29 @@ void clearBorder(const Mat &img, Rect& cropRect) { int nCols = result.cols; Mat result_resize; - if (nCols <= img_window.cols && nRows <= img_window.rows) { + if (nCols <= img_window.cols && nRows <= img_window.rows) + { result_resize = result; - - } else if (nCols > img_window.cols && nRows <= img_window.rows) { + } + else if (nCols > img_window.cols && nRows <= img_window.rows) + { float scale = float(img_window.cols) / float(nCols); resize(result, result_resize, Size(), scale, scale, CV_INTER_AREA); - - } else if (nCols <= img_window.cols && nRows > img_window.rows) { + } + else if (nCols <= img_window.cols && nRows > img_window.rows) + { float scale = float(img_window.rows) / float(nRows); resize(result, result_resize, Size(), scale, scale, CV_INTER_AREA); - - } else if (nCols > img_window.cols && nRows > img_window.rows) { + } + else if (nCols > img_window.cols && nRows > img_window.rows) + { float scale1 = float(img_window.cols) / float(nCols); float scale2 = float(img_window.rows) / float(nRows); float scale = scale1 < scale2 ? scale1 : scale2; resize(result, result_resize, Size(), scale, scale, CV_INTER_AREA); } - else { + else + { result_resize = result; } @@ -2461,13 +2972,15 @@ void clearBorder(const Mat &img, Rect& cropRect) { result_resize.cols, result_resize.rows)); addWeighted(imageRoi, 0, result_resize, 1, 0, imageRoi); - if (1) { + if (1) + { imshow("EasyPR", img_window); waitKey(0); destroyWindow("EasyPR"); } - if (1) { + if (1) + { std::stringstream ss(std::stringstream::in | std::stringstream::out); ss << "resources/image/tmp/Result/plate_" << img_index << ".jpg"; imwrite(ss.str(), img_window); @@ -2476,9 +2989,10 @@ void clearBorder(const Mat &img, Rect& cropRect) { return img_window; } - Rect rectEnlarge(const Rect &src, const int mat_width, const int mat_height) { - float w = (float) src.width; - float h = (float) src.height; + Rect rectEnlarge(const Rect &src, const int mat_width, const int mat_height) + { + float w = (float)src.width; + float h = (float)src.height; // enlarge the rect, // width to 120% // height to 105% @@ -2486,8 +3000,8 @@ void clearBorder(const Mat &img, Rect& cropRect) { float new_h = h * 1.05f; Rect_ boudRect; - boudRect.x = (float) src.x - w * 0.1f; - boudRect.y = (float) src.y - h * 0.025f; + boudRect.x = (float)src.x - w * 0.1f; + boudRect.y = (float)src.y - h * 0.025f; boudRect.width = new_w; boudRect.height = new_h; @@ -2495,11 +3009,11 @@ void clearBorder(const Mat &img, Rect& cropRect) { float tl_y = boudRect.y > 0 ? boudRect.y : 0; float br_x = boudRect.x + boudRect.width - 1 <= mat_width - 1 - ? boudRect.x + boudRect.width - 1 - : mat_width - 1; + ? boudRect.x + boudRect.width - 1 + : mat_width - 1; float br_y = boudRect.y + boudRect.height - 1 < mat_height - 1 - ? boudRect.y + boudRect.height - 1 - : mat_height - 1; + ? boudRect.y + boudRect.height - 1 + : mat_height - 1; float roi_width = br_x - tl_x + 1; float roi_height = br_y - tl_y + 1; @@ -2511,16 +3025,17 @@ void clearBorder(const Mat &img, Rect& cropRect) { //! a new rect not out the range of mat dst = Rect_(tl_x, tl_y, roi_width, roi_height); return dst; - } - Rect rectFit(const Rect &src, const int mat_width, const int mat_height) { + Rect rectFit(const Rect &src, const int mat_width, const int mat_height) + { float w = (float)src.width; float h = (float)src.height; float new_w = h * 0.5f; float new_h = h * 1.05f; - if (new_w <= w || new_h <= h) { + if (new_w <= w || new_h <= h) + { return src; } @@ -2537,11 +3052,11 @@ void clearBorder(const Mat &img, Rect& cropRect) { float tl_y = boudRect.y > 0 ? boudRect.y : 0; float br_x = boudRect.x + boudRect.width - 1 <= mat_width - 1 - ? boudRect.x + boudRect.width - 1 - : mat_width - 1; + ? boudRect.x + boudRect.width - 1 + : mat_width - 1; float br_y = boudRect.y + boudRect.height - 1 < mat_height - 1 - ? boudRect.y + boudRect.height - 1 - : mat_height - 1; + ? boudRect.y + boudRect.height - 1 + : mat_height - 1; float roi_width = br_x - tl_x + 1; float roi_height = br_y - tl_y + 1; @@ -2553,13 +3068,12 @@ void clearBorder(const Mat &img, Rect& cropRect) { //! a new rect not out the range of mat dst = Rect_(tl_x, tl_y, roi_width - 1, roi_height - 1); return dst; - } - - void writeTempImage(const Mat &outImg, const string path, int index) { + void writeTempImage(const Mat &outImg, const string path, int index) + { std::stringstream ss(std::stringstream::in | std::stringstream::out); - time_t t = time(0); // get time now + time_t t = time(0); // get time now struct tm *now = localtime(&t); char buf[80]; strftime(buf, sizeof(buf), "%Y-%m-%d %H_%M_%S", now); diff --git a/src/src/core/params.cpp b/src/src/core/params.cpp index 8809043..2b0ccef 100644 --- a/src/src/core/params.cpp +++ b/src/src/core/params.cpp @@ -9,4 +9,5 @@ namespace easypr { } return instance_; } -}/*! \namespace easypr*/ \ No newline at end of file +}/*! \namespace easypr*/ +//这段代码是一个单例模式的实现。CParams类的instance_成员变量被初始化为nullptr,然后instance()函数用来获取CParams类的唯一实例。如果instance_为空,就会创建一个新的实例并返回,否则直接返回现有的实例。这样可以确保在程序运行期间只有一个CParams类的实例存在。 \ No newline at end of file diff --git a/src/src/core/plate_detect.cpp b/src/src/core/plate_detect.cpp index 26d1ad9..37dedfc 100644 --- a/src/src/core/plate_detect.cpp +++ b/src/src/core/plate_detect.cpp @@ -11,7 +11,7 @@ namespace easypr { m_type = 0; m_showDetect = false; } - +// 这段代码是CPlateDetect类的构造函数,初始化了一些成员变量,包括m_plateLocate指针、m_maxPlates、m_type和m_showDetect。 CPlateDetect::~CPlateDetect() { SAFE_RELEASE(m_plateLocate); } int CPlateDetect::plateDetect(Mat src, std::vector &resultVec, int type, @@ -24,6 +24,11 @@ namespace easypr { mser_Plates.reserve(16); std::vector all_result_Plates; all_result_Plates.reserve(64); + // 这部分代码定义了CPlateDetect类的析构函数和plateDetect方法。 +// 在plateDetect方法中,使用了OpenMP并行处理来同时进行Sobel、颜色和MSER三种车牌定位方法。 +// 最后使用NMS(非极大值抑制)来判断车牌,并返回结果。 +// plateDetect方法还有一个重载版本,其中showDetectArea参数为false。 +// LoadSVM方法用于加载SVM模型。 #pragma omp parallel sections { #pragma omp section @@ -32,12 +37,16 @@ namespace easypr { m_plateLocate->plateSobelLocate(src, sobel_Plates, img_index); } } + // 这部分代码使用了OpenMP并行处理来进行Sobel车牌定位方法。 +// 如果type为0或者包含PR_DETECT_SOBEL标志位,则调用m_plateLocate的plateSobelLocate方法。 +// plateSobelLocate方法用于进行Sobel算子定位车牌。 #pragma omp section { if (!type || type & PR_DETECT_COLOR) { m_plateLocate->plateColorLocate(src, color_Plates, img_index); } } + //这段代码是使用OpenMP并行处理来调用m_plateLocate的plateColorLocate方法,用于进行颜色定位车牌。根据type参数的值,判断是否需要进行颜色定位 #pragma omp section { if (!type || type & PR_DETECT_CMSER) { @@ -64,7 +73,11 @@ namespace easypr { showDectectResults(src, resultVec, m_maxPlates); return 0; } - +// 这部分代码是CPlateDetect类的plateDetect方法的一部分。 +// 首先通过OpenMP并行处理调用plateMserLocate方法来进行MSER车牌定位。 +// 然后将定位结果分别设置为对应的类型(SOBEL、COLOR、CMSER)并存入all_result_Plates中。 +// 最后使用NMS(非极大值抑制)来判断车牌,并将结果存入resultVec中。 +// 代码中的if (0)条件语句似乎没有实际作用,可能需要进一步确认其意图。 int CPlateDetect::plateDetect(Mat src, std::vector &resultVec, int img_index) { int result = plateDetect(src, resultVec, m_type, false, img_index); return result; @@ -74,4 +87,6 @@ namespace easypr { PlateJudge::instance()->LoadModel(path); } -} \ No newline at end of file +} +// 这段代码中的plateDetect方法是CPlateDetect类的成员函数,用于调用另一个重载版本的plateDetect方法,并返回结果。 +// LoadSVM方法用于加载SVM模型,其中调用了PlateJudge类的LoadModel方法。 \ No newline at end of file diff --git a/src/src/core/plate_judge.cpp b/src/src/core/plate_judge.cpp index 4dfa032..84a76a2 100644 --- a/src/src/core/plate_judge.cpp +++ b/src/src/core/plate_judge.cpp @@ -1,46 +1,52 @@ +//该代码主要用于车牌识别。它定义了一个名为PlateJudge的类,该类包含了一系列用于车牌识别的方法。 + #include "easypr/core/plate_judge.h" #include "easypr/config.h" #include "easypr/core/core_func.h" #include "easypr/core/params.h" -namespace easypr { - +namespace easypr { //这部分代码实现了单例模式,确保PlateJudge类只有一个实例 +//定义了一个静态成员变量instance_,它是PlateJudge类的指针,初始化为nullptr。这是单例模式的关键,所有的PlateJudge实例都将共享这个变量 PlateJudge* PlateJudge::instance_ = nullptr; - PlateJudge* PlateJudge::instance() { - if (!instance_) { - instance_ = new PlateJudge; + +//确保PlateJudge类只有一个实例,当需要使用PlateJudge类时,只需要调用PlateJudge::instance()即可获取到这个唯一的实例 + PlateJudge* PlateJudge::instance() { //定义了一个静态成员函数instance(),它返回一个指向PlateJudge实例的指针 + if (!instance_) { //检查instance_是否为nullptr + instance_ = new PlateJudge; //如果是,那么就创建一个新的PlateJudge实例,并将instance_设置为指向这个新创建的实例 } - return instance_; + return instance_; //返回instance_,即指向PlateJudge实例的指针 } - PlateJudge::PlateJudge() { - bool useLBP = false; - if (useLBP) { - LOAD_SVM_MODEL(svm_, kLBPSvmPath); - extractFeature = getLBPFeatures; + PlateJudge::PlateJudge() { //PlateJudge决定了使用哪种特征提取方法 + bool useLBP = false; //定义一个布尔变量useLBP,并初始化为false + if (useLBP) { //如果useLBP为true,即使用LBP特征提取方法 + LOAD_SVM_MODEL(svm_, kLBPSvmPath); //加载LBP的SVM模型 + extractFeature = getLBPFeatures; //设置特征提取函数为getLBPFeatures } - else { - LOAD_SVM_MODEL(svm_, kHistSvmPath); - extractFeature = getHistomPlusColoFeatures; + else { //如果useLBP为false,即使用直方图特征提取方法 + LOAD_SVM_MODEL(svm_, kHistSvmPath); //加载直方图的SVM模型 + extractFeature = getHistomPlusColoFeatures; //设置特征提取函数为getHistomPlusColoFeatures } } - void PlateJudge::LoadModel(std::string path) { - if (path != std::string(kDefaultSvmPath)) { - if (!svm_->empty()) - svm_->clear(); - LOAD_SVM_MODEL(svm_, path); + void PlateJudge::LoadModel(std::string path) { //LoadModel函数用于加载SVM模型,函数接收一个字符串参数path,这个参数是SVM模型文件的路径 + if (path != std::string(kDefaultSvmPath)) { //检查输入的路径path是否与默认的SVM路径kDefaultSvmPath不同。如果不同,那么就需要加载新的SVM模型 + if (!svm_->empty()) //检查当前的SVM模型是否为空。如果不为空,那么在加载新的SVM模型之前,需要先清空当前的SVM模型 + svm_->clear(); //清空当前的SVM模型 + LOAD_SVM_MODEL(svm_, path); //加载新的SVM模型。LOAD_SVM_MODEL是一个宏,它接收两个参数:一个是SVM模型的指针,另一个是SVM模型文件的路径 } } // set the score of plate // 0 is plate, -1 is not. - int PlateJudge::plateSetScore(CPlate& plate) { - Mat features; - extractFeature(plate.getPlateMat(), features); - float score = svm_->predict(features, noArray(), cv::ml::StatModel::Flags::RAW_OUTPUT); + int PlateJudge::plateSetScore(CPlate& plate) { //plateSetScore函数用于设置车牌的评分,接收一个CPlate类型的引用作为参数 + Mat features; //定义一个Mat类型的变量features,用于存储特征 + extractFeature(plate.getPlateMat(), features); //调用extractFeature方法提取车牌图像的特征,结果存储在features中 + float score = svm_->predict(features, noArray(), cv::ml::StatModel::Flags::RAW_OUTPUT); //使用SVM模型对特征进行预测,得到的结果存储在score中 //std::cout << "score:" << score << std::endl; + +//这是一个调试用的代码块,如果条件为真(此处为0,所以不会执行),则显示车牌图像,等待用户按键,然后销毁窗口 if (0) { imshow("plate", plate.getPlateMat()); waitKey(0); @@ -49,67 +55,82 @@ namespace easypr { // score is the distance of margin,below zero is plate, up is not // when score is below zero, the samll the value, the more possibliy to be a plate. plate.setPlateScore(score); - if (score < 0.5) return 0; + if (score < 0.5) return 0; //如果评分小于0.5,返回0,表示这是一个车牌;否则返回-1,表示这不是一个车牌 else return -1; } - int PlateJudge::plateJudge(const Mat& plateMat) { - CPlate plate; - plate.setPlateMat(plateMat); - return plateSetScore(plate); +//定义了一个名为plateJudge的成员函数,它属于PlateJudge类。该函数接收一个Mat类型的常量引用参数plateMat,这个参数是需要进行车牌判断的图像 + int PlateJudge::plateJudge(const Mat& plateMat) { //plateJudge函数用于判断输入的图像是否为车牌。 + CPlate plate; //这行创建了一个CPlate类型的对象plate。CPlate是一个类,它可能包含车牌的相关信息,如车牌图像、车牌位置等 + plate.setPlateMat(plateMat); //调用了CPlate类的setPlateMat成员函数,将输入的图像plateMat设置为plate对象的车牌图像 + return plateSetScore(plate); //调用了PlateJudge类的plateSetScore成员函数,对plate对象进行评分,然后返回评分结果 } - int PlateJudge::plateJudge(const std::vector &inVec, - std::vector &resultVec) { - int num = inVec.size(); - for (int j = 0; j < num; j++) { - Mat inMat = inVec[j]; + int PlateJudge::plateJudge(const std::vector &inVec, //inVec是输入的图像向量,resultVec是输出的结果向量。 + std::vector &resultVec) { //对inVec中的每一张图像进行车牌判断。如果判断结果为车牌(即plateJudge(inMat)的返回值为0),则将该图像添加到resultVec中。 + int num = inVec.size(); // 获取输入图像的数量 + for (int j = 0; j < num; j++) { // 遍历每一张图像 + Mat inMat = inVec[j]; // 获取当前图像 int response = -1; - response = plateJudge(inMat); + response = plateJudge(inMat); // 对当前图像进行车牌判断 - if (response == 0) resultVec.push_back(inMat); + if (response == 0) resultVec.push_back(inMat); // 如果判断结果为车牌,将该图像添加到结果向量中 } - return 0; + return 0; // 返回0,表示执行成功 } - int PlateJudge::plateJudge(const std::vector &inVec, - std::vector &resultVec) { - int num = inVec.size(); - for (int j = 0; j < num; j++) { - CPlate inPlate = inVec[j]; +//属于PlateJudge类,用于判断输入的车牌向量中哪些是有效的车牌。这个方法的输入是一个CPlate对象的向量inVec,输出是一个有效车牌的CPlate对象的向量resultVec。 + int PlateJudge::plateJudge(const std::vector &inVec, + std::vector &resultVec) { //接收两个参数:一个CPlate对象的向量inVec(输入的车牌向量)和一个CPlate对象的向量resultVec(输出的有效车牌向量)。 + int num = inVec.size(); // 获取输入向量的大小 + for (int j = 0; j < num; j++) { //遍历输入向量中的每一个元素 + +//获取当前的CPlate对象和它的车牌图像 + CPlate inPlate = inVec[j]; Mat inMat = inPlate.getPlateMat(); + +//调用plateJudge方法判断当前的车牌图像是否有效,结果存储在response中 int response = -1; response = plateJudge(inMat); +//如果当前的车牌图像有效(response等于0),则将当前的CPlate对象添加到结果向量中 if (response == 0) resultVec.push_back(inPlate); +//如果当前的车牌图像无效,那么对车牌图像进行裁剪和调整大小 else { int w = inMat.cols; int h = inMat.rows; Mat tmpmat = inMat(Rect_(w * 0.05, h * 0.1, w * 0.9, h * 0.8)); Mat tmpDes = inMat.clone(); resize(tmpmat, tmpDes, Size(inMat.size())); - +//再次调用plateJudge方法判断调整后的车牌图像是否有效,如果有效则将当前的CPlate对象添加到结果向量中 response = plateJudge(tmpDes); if (response == 0) resultVec.push_back(inPlate); } } - return 0; + return 0; //结束循环并返回0,表示方法执行成功 } - // non-maximum suppression - void NMS(std::vector &inVec, std::vector &resultVec, double overlap) { - std::sort(inVec.begin(), inVec.end()); + // non-maximum suppression -->非极大值抑制 +//函数接收三个参数:一个CPlate对象的向量inVec(输入的车牌向量),一个CPlate对象的向量resultVec(输出的车牌向量),以及一个double类型的overlap(重叠阈值) + void NMS(std::vector &inVec, std::vector &resultVec, double overlap) { //NMS函数实现了非极大值抑制,用于消除重叠的车牌。 + std::sort(inVec.begin(), inVec.end()); //首先对输入的车牌向量进行排序 + +//然后遍历输入的车牌向量,对每一个车牌对象,获取其位置的边界矩形 std::vector::iterator it = inVec.begin(); for (; it != inVec.end(); ++it) { CPlate plateSrc = *it; //std::cout << "plateScore:" << plateSrc.getPlateScore() << std::endl; Rect rectSrc = plateSrc.getPlatePos().boundingRect(); + +//在内层循环中,对当前车牌对象之后的每一个车牌对象,也获取其位置的边界矩形 std::vector::iterator itc = it + 1; for (; itc != inVec.end();) { CPlate plateComp = *itc; Rect rectComp = plateComp.getPlatePos().boundingRect(); + +//计算两个边界矩形的交并比(Intersection over Union,IoU),如果IoU大于设定的重叠阈值,那么就从输入的车牌向量中删除当前的车牌对象;否则,继续处理下一个车牌对象 float iou = computeIOU(rectSrc, rectComp); if (iou > overlap) { itc = inVec.erase(itc); @@ -119,19 +140,23 @@ namespace easypr { } } } - resultVec = inVec; + resultVec = inVec; //最后,将处理后的车牌向量赋值给输出的车牌向量 } - // judge plate using nms - int PlateJudge::plateJudgeUsingNMS(const std::vector &inVec, std::vector &resultVec, int maxPlates) { - std::vector plateVec; - int num = inVec.size(); - bool useCascadeJudge = true; + // judge plate using nms --> 使用非极大值抑制进行车牌识别 +// 定义了一个名为plateJudgeUsingNMS的成员函数,它属于PlateJudge类。该函数使用非极大值抑制进行车牌识别,接收一个CPlate对象的向量inVec(输入的车牌向量)、一个CPlate对象的向量resultVec(输出的车牌向量)和一个整数maxPlates(最大车牌数量)作为参数 + int PlateJudge::plateJudgeUsingNMS(const std::vector &inVec, std::vector &resultVec, int maxPlates) { //plateJudgeUsingNMS函数使用非极大值抑制进行车牌识别。 + std::vector plateVec; // 创建一个CPlate对象的向量,用于存储识别出的车牌 + int num = inVec.size(); // 获取输入向量的大小 + bool useCascadeJudge = true; // 定义一个布尔变量,表示是否使用级联判断 +// 遍历输入向量中的每一个元素 for (int j = 0; j < num; j++) { - CPlate plate = inVec[j]; - Mat inMat = plate.getPlateMat(); - int result = plateSetScore(plate); + CPlate plate = inVec[j]; // 获取当前的CPlate对象 + Mat inMat = plate.getPlateMat(); // 获取当前CPlate对象的车牌图像 + int result = plateSetScore(plate); // 对当前的CPlate对象进行评分 + +// 如果评分结果为0,表示这是一个车牌 if (0 == result) { if (0) { imshow("inMat", inMat); @@ -139,17 +164,22 @@ namespace easypr { destroyWindow("inMat"); } - if (plate.getPlateLocateType() == CMSER) { - int w = inMat.cols; - int h = inMat.rows; + if (plate.getPlateLocateType() == CMSER) { // 如果plate的定位类型为CMSER + int w = inMat.cols; // 获取图像的宽度 + int h = inMat.rows; // 获取图像的高度 + // 对图像进行裁剪 Mat tmpmat = inMat(Rect_(w * 0.05, h * 0.1, w * 0.9, h * 0.8)); - Mat tmpDes = inMat.clone(); - resize(tmpmat, tmpDes, Size(inMat.size())); - plate.setPlateMat(tmpDes); + Mat tmpDes = inMat.clone(); // 克隆图像 + resize(tmpmat, tmpDes, Size(inMat.size())); // 调整图像大小 + plate.setPlateMat(tmpDes); // 设置plate的车牌图像为调整后的图像 + + // 如果使用级联判断 if (useCascadeJudge) { - int resultCascade = plateSetScore(plate); + int resultCascade = plateSetScore(plate); // 对调整后的图像进行评分 + // 如果plate的定位类型不是CMSER,将plate的车牌图像设置为原图像 if (plate.getPlateLocateType() != CMSER) plate.setPlateMat(inMat); + // 如果级联评分结果为0,将plate添加到plateVec中 if (resultCascade == 0) { if (0) { imshow("tmpDes", tmpDes); @@ -159,22 +189,24 @@ namespace easypr { plateVec.push_back(plate); } } - else + else // 如果不使用级联判断,直接将plate添加到plateVec中 plateVec.push_back(plate); } - else + else // 如果plate的定位类型不是CMSER,直接将plate添加到plateVec中 plateVec.push_back(plate); } } - std::vector reDupPlateVec; - double overlap = 0.5; + std::vector reDupPlateVec; // 创建一个CPlate对象的向量,用于存储非极大值抑制后的结果 + double overlap = 0.5; // 定义重叠阈值 // double overlap = CParams::instance()->getParam1f(); // use NMS to get the result plates +// 使用非极大值抑制处理plateVec,结果存储在reDupPlateVec中 NMS(plateVec, reDupPlateVec, overlap); - // sort the plates due to their scores + // sort the plates due to their scores --> 根据评分对reDupPlateVec进行排序 std::sort(reDupPlateVec.begin(), reDupPlateVec.end()); // output the plate judge plates +// 遍历reDupPlateVec,将结果添加到resultVec中,直到达到最大车牌数量 std::vector::iterator it = reDupPlateVec.begin(); int count = 0; for (; it != reDupPlateVec.end(); ++it) { @@ -188,6 +220,6 @@ namespace easypr { if (count >= maxPlates) break; } - return 0; + return 0; // 返回0,表示函数执行成功 } } diff --git a/src/src/core/plate_locate.cpp b/src/src/core/plate_locate.cpp index 90b8719..f9b25c2 100644 --- a/src/src/core/plate_locate.cpp +++ b/src/src/core/plate_locate.cpp @@ -11,14 +11,17 @@ const float DEFAULT_ERROR = 0.9f; // 0.6 const float DEFAULT_ASPECT = 3.75f; // 3.75 CPlateLocate::CPlateLocate() { + //CPlateLocate函数用于车牌定位 m_GaussianBlurSize = DEFAULT_GAUSSIANBLUR_SIZE; m_MorphSizeWidth = DEFAULT_MORPH_SIZE_WIDTH; m_MorphSizeHeight = DEFAULT_MORPH_SIZE_HEIGHT; - + //定义了高斯模糊大小、变形宽度、变形高度 m_error = DEFAULT_ERROR; m_aspect = DEFAULT_ASPECT; + //默认错误和方面 m_verifyMin = DEFAULT_VERIFY_MIN; m_verifyMax = DEFAULT_VERIFY_MAX; + //验证最小值与最大值 m_angle = DEFAULT_ANGLE; @@ -26,6 +29,7 @@ CPlateLocate::CPlateLocate() { } void CPlateLocate::setLifemode(bool param) { + //若参数param为真,设置成员变量为特定值,为假则设为初始值 if (param) { setGaussianBlurSize(5); setMorphSizeWidth(10); @@ -46,6 +50,8 @@ void CPlateLocate::setLifemode(bool param) { } bool CPlateLocate::verifySizes(RotatedRect mr) { + //验证图像中给定的车牌区域大小是否满足预设的大小限制 + //主要是在宽度、高度、面积,满足返回true float error = m_error; // Spain car plate size: 52x11 aspect 4,7272 // China car plate size: 440mm*140mm,aspect 3.142857 @@ -64,12 +70,14 @@ bool CPlateLocate::verifySizes(RotatedRect mr) { float rmax = aspect + aspect * error; float area = mr.size.height * mr.size.width; + //r为宽高比 float r = (float) mr.size.width / (float) mr.size.height; if (r < 1) r = (float) mr.size.height / (float) mr.size.width; // cout << "area:" << area << endl; // cout << "r:" << r << endl; + //判断车牌面积是否满足 if ((area < min || area > max) || (r < rmin || r > rmax)) return false; else @@ -78,29 +86,32 @@ bool CPlateLocate::verifySizes(RotatedRect mr) { //! mser search method int CPlateLocate::mserSearch(const Mat &src, vector &out, + //在输入的图像src中搜索并找到车牌区域, + // 并将找到的蓝色和黄色的车牌信息及其旋转矩形框信息和匹配后的灰度图像返回。 vector>& out_plateVec, bool usePlateMser, vector>& out_plateRRect, + int img_index, bool showDebug) { vector match_grey; - + //存储匹配后的灰度图像。 vector plateVec_blue; plateVec_blue.reserve(16); vector plateRRect_blue; plateRRect_blue.reserve(16); - + //创建两个向量来存储蓝色的车牌信息和其旋转矩形框信息。 vector plateVec_yellow; plateVec_yellow.reserve(16); - + //创建两个向量来存储黄色的车牌信息和其旋转矩形框信息。 vector plateRRect_yellow; plateRRect_yellow.reserve(16); mserCharMatch(src, match_grey, plateVec_blue, plateVec_yellow, usePlateMser, plateRRect_blue, plateRRect_yellow, img_index, showDebug); - + //调用MSER算法的函数,输入源图像和各种参数,输出匹配后的灰度图像以及两种颜色的车牌信息和其旋转矩形框信息。 out_plateVec.push_back(plateVec_blue); out_plateVec.push_back(plateVec_yellow); - + //将找到的蓝色和黄色的车牌信息添加到out_plateVec向量中。 out_plateRRect.push_back(plateRRect_blue); out_plateRRect.push_back(plateRRect_yellow); - + //将找到的蓝色和黄色的车牌的旋转矩形框信息添加到out_plateRRect向量中。 out = match_grey; return 0; @@ -111,21 +122,26 @@ int CPlateLocate::colorSearch(const Mat &src, const Color r, Mat &out, vector &outRects) { Mat match_grey; + //在输入的图像(src)中搜索特定颜色(r)的区域, + // 并返回找到的区域(RotatedRect)的列表 // width is important to the final results; const int color_morph_width = 10; const int color_morph_height = 2; colorMatch(src, match_grey, r, false); + //将输入图像·src转换为灰度图像match_grey SHOW_IMAGE(match_grey, 0); Mat src_threshold; threshold(match_grey, src_threshold, 0, 255, CV_THRESH_OTSU + CV_THRESH_BINARY); - + //使用阈值函数(threshold)对匹配后的图像进行处理, + // 将像素值从0到255进行二值化处理 Mat element = getStructuringElement( MORPH_RECT, Size(color_morph_width, color_morph_height)); morphologyEx(src_threshold, src_threshold, MORPH_CLOSE, element); - + //morphologyEx进行形态学闭运算, + // 主要用于去除噪声以及填充空洞 //if (m_debug) { // utils::imwrite("resources/image/tmp/color.jpg", src_threshold); //} @@ -139,17 +155,20 @@ int CPlateLocate::colorSearch(const Mat &src, const Color r, Mat &out, contours, // a vector of contours CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE); // all pixels of each contours - + //indContours函数会找到所有外部轮廓,即所有的区域都会被找到。 vector>::iterator itc = contours.begin(); while (itc != contours.end()) { RotatedRect mr = minAreaRect(Mat(*itc)); - + //对找到的每个轮廓执行minAreaRect函数,得到该轮廓的最小外接矩形(mr) + if (!verifySizes(mr)) itc = contours.erase(itc); else { ++itc; outRects.push_back(mr); } + //最后通过verifySizes函数检查得到的最小外接矩形是否满足预设的条件。 + // 如果满足条件,则将该矩形添加到outRects列表中;如果不满足条件,则删除该轮廓 } return 0; @@ -158,11 +177,15 @@ int CPlateLocate::colorSearch(const Mat &src, const Color r, Mat &out, int CPlateLocate::sobelFrtSearch(const Mat &src, vector> &outRects) { + //主要功能是在输入的图像(src)中搜索边缘, + // 并返回找到的区域(RotatedRect)的列表。 Mat src_threshold; - + //对输入图像进行边缘检测后的结果。 sobelOper(src, src_threshold, m_GaussianBlurSize, m_MorphSizeWidth, m_MorphSizeHeight); - + //调用sobelOper函数,对输入图像src进行边缘检测, + // 并将结果存储在src_threshold中 + //该函数还使用了高斯模糊和形态学操作来增强边缘检测的效果 vector> contours; findContours(src_threshold, contours, // a vector of contours @@ -170,19 +193,22 @@ int CPlateLocate::sobelFrtSearch(const Mat &src, CV_CHAIN_APPROX_NONE); // all pixels of each contours vector>::iterator itc = contours.begin(); - + //调用findContours函数找到边缘检测后的轮廓。 vector first_rects; while (itc != contours.end()) { RotatedRect mr = minAreaRect(Mat(*itc)); - + //计算其最小外接矩形(minAreaRect函数) if (verifySizes(mr)) { + //检查这个矩形是否满足预设的条件(verifySizes函数)。 + // 如果满足条件,将这个矩形加入到first_rects列表中 first_rects.push_back(mr); float area = mr.size.height * mr.size.width; float r = (float) mr.size.width / (float) mr.size.height; if (r < 1) r = (float) mr.size.height / (float) mr.size.width; + //计算其面积和宽高比(r)。如果r小于1,则重新计算宽高比。 } ++itc; @@ -190,11 +216,14 @@ int CPlateLocate::sobelFrtSearch(const Mat &src, for (size_t i = 0; i < first_rects.size(); i++) { RotatedRect roi_rect = first_rects[i]; - + Rect_ safeBoundRect; if (!calcSafeRect(roi_rect, src, safeBoundRect)) continue; - + //对于每一个在first_rects中的矩形, + // 计算其在原图中的安全边界矩形 + //如果计算失败,则跳过当前循环。 outRects.push_back(safeBoundRect); + //将每个安全边界矩形加入到outRects列表中。 } return 0; } @@ -202,20 +231,23 @@ int CPlateLocate::sobelFrtSearch(const Mat &src, int CPlateLocate::sobelSecSearchPart(Mat &bound, Point2f refpoint, vector &outRects) { + //功能:在输入的图像(bound)中搜索边缘,并返回找到的区域(RotatedRect)的列表(outRects) Mat bound_threshold; sobelOperT(bound, bound_threshold, 3, 6, 2); - + //使用sobelOperT函数进行边缘检测,其中3、6、2是sobel算子的大小。 Mat tempBoundThread = bound_threshold.clone(); clearLiuDingOnly(tempBoundThread); int posLeft = 0, posRight = 0; if (bFindLeftRightBound(tempBoundThread, posLeft, posRight)) { - + //使用bFindLeftRightBound函数查找图像的左右边界, // find left and right bounds to repair if (posRight != 0 && posLeft != 0 && posLeft < posRight) { + // 如果左边界不为0且右边界不为0且左边界小于右边界, + // 则在图像中央位置的左右边界之间填充白色 int posY = int(bound_threshold.rows * 0.5); for (int i = posLeft + (int) (bound_threshold.rows * 0.1); i < posRight - 4; i++) { @@ -224,13 +256,16 @@ int CPlateLocate::sobelSecSearchPart(Mat &bound, Point2f refpoint, } utils::imwrite("resources/image/tmp/repaireimg1.jpg", bound_threshold); - + //保存处理后的图像 // remove the left and right boundaries - for (int i = 0; i < bound_threshold.rows; i++) { + for (int i = 0; i < bound_threshold.rows; i++) + { + //每个行上,将左边界和右边界的像素设置为0。 bound_threshold.data[i * bound_threshold.cols + posLeft] = 0; bound_threshold.data[i * bound_threshold.cols + posRight] = 0; } + utils::imwrite("resources/image/tmp/repaireimg2.jpg", bound_threshold); } @@ -239,23 +274,28 @@ int CPlateLocate::sobelSecSearchPart(Mat &bound, Point2f refpoint, contours, // a vector of contours CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE); // all pixels of each contours - + //调用findContours函数找到边缘检测后的轮廓。 vector>::iterator itc = contours.begin(); vector second_rects; while (itc != contours.end()) { RotatedRect mr = minAreaRect(Mat(*itc)); second_rects.push_back(mr); + //对于每个轮廓,计算其最小外接矩形(minAreaRect函数) + //并将结果添加到second_rects列表中。 ++itc; } for (size_t i = 0; i < second_rects.size(); i++) { RotatedRect roi = second_rects[i]; + if (verifySizes(roi)) { + //对于second_rects中的每个矩形, + // 如果满足条件(verifySizes函数)则计算其中心、大小和角度 Point2f refcenter = roi.center + refpoint; Size2f size = roi.size; float angle = roi.angle; - + //创建一个新的RotatedRect对象,然后将其添加到outRects列表中。 RotatedRect refroi(refcenter, size, angle); outRects.push_back(refroi); } @@ -267,6 +307,8 @@ int CPlateLocate::sobelSecSearchPart(Mat &bound, Point2f refpoint, int CPlateLocate::sobelSecSearch(Mat &bound, Point2f refpoint, vector &outRects) { + //功能:在输入的图像(bound)中搜索边缘,并返回找到的区域(RotatedRect)的列表(outRects) + //函数内部的函数功能和sobelSecSearch函数差不多 Mat bound_threshold; @@ -307,19 +349,24 @@ int CPlateLocate::sobelSecSearch(Mat &bound, Point2f refpoint, int CPlateLocate::sobelOper(const Mat &in, Mat &out, int blurSize, int morphW, int morphH) { + //功能是对输入图像进行边缘检测,并返回检测到的边缘图像 Mat mat_blur; mat_blur = in.clone(); GaussianBlur(in, mat_blur, Size(blurSize, blurSize), 0, 0, BORDER_DEFAULT); + //对输入图像in进行高斯模糊处理,并将结果存储在mat_blur中 Mat mat_gray; if (mat_blur.channels() == 3) cvtColor(mat_blur, mat_gray, CV_RGB2GRAY); + //如果是彩色图,则转换为灰度图像 + //否则直接将mat_blur作为灰度图像。 else mat_gray = mat_blur; int scale = SOBEL_SCALE; int delta = SOBEL_DELTA; int ddepth = SOBEL_DDEPTH; + //三个变量分别表示Sobel算子中的尺度、偏差和深度。 Mat grad_x, grad_y; Mat abs_grad_x, abs_grad_y; @@ -327,30 +374,31 @@ int CPlateLocate::sobelOper(const Mat &in, Mat &out, int blurSize, int morphW, Sobel(mat_gray, grad_x, ddepth, 1, 0, 3, scale, delta, BORDER_DEFAULT); convertScaleAbs(grad_x, abs_grad_x); - + //调用Sobel函数,对灰度图像进行Sobel算子运算,得到梯度图像grad_x。 Mat grad; addWeighted(abs_grad_x, SOBEL_X_WEIGHT, 0, 0, 0, grad); - + //将x方向的梯度图像和y方向的梯度图像加权叠加,得到最终的梯度图像grad。 Mat mat_threshold; double otsu_thresh_val = threshold(grad, mat_threshold, 0, 255, CV_THRESH_OTSU + CV_THRESH_BINARY); - + //对梯度图像grad进行阈值处理,得到二值化图像mat_threshold Mat element = getStructuringElement(MORPH_RECT, Size(morphW, morphH)); morphologyEx(mat_threshold, mat_threshold, MORPH_CLOSE, element); - + //对二值化图像mat_threshold进行形态学闭运算,以填充孔洞和连接断开的边缘 out = mat_threshold; return 0; } void deleteNotArea(Mat &inmat, Color color = UNKNOWN) { + //用于删除图像中的非区域部分,并保留特定数据 Mat input_grey; cvtColor(inmat, input_grey, CV_BGR2GRAY); - + //将输入图像转换为灰度图像 int w = inmat.cols; int h = inmat.rows; - + //从输入图像中截取一个子区域。 Mat tmpMat = inmat(Rect_(w * 0.15, h * 0.1, w * 0.7, h * 0.7)); Color plateType; @@ -360,36 +408,42 @@ void deleteNotArea(Mat &inmat, Color color = UNKNOWN) { else { plateType = color; } - + //根据输入的颜色参数,确定要保留的颜色类型。 + //如果输入颜色为未知(UNKNOWN),则通过getPlateType函数确定颜色类型。 Mat img_threshold; if (BLUE == plateType) { img_threshold = input_grey.clone(); + //若车牌是蓝色,克隆灰度图像input_grey到img_threshold Mat tmp = input_grey(Rect_(w * 0.15, h * 0.15, w * 0.7, h * 0.7)); + // 在input_grey中截取中心70%的部分进行阈值分割 int threadHoldV = ThresholdOtsu(tmp); - + threshold(input_grey, img_threshold, threadHoldV, 255, CV_THRESH_BINARY); // threshold(input_grey, img_threshold, 5, 255, CV_THRESH_OTSU + // CV_THRESH_BINARY); - + // 应用计算出的最佳阈值进行二值化,即低于阈值的像素点变为0,高于阈值的变为255 utils::imwrite("resources/image/tmp/inputgray2.jpg", img_threshold); - - } else if (YELLOW == plateType) { + //保存二值化处理后的图像 + } else if (YELLOW == plateType) {// 如果车牌类型是黄色 + img_threshold = input_grey.clone(); + Mat tmp = input_grey(Rect_(w * 0.1, h * 0.1, w * 0.8, h * 0.8)); + // 在input_grey中截取中心80%的部分进行阈值分割 int threadHoldV = ThresholdOtsu(tmp); - + // 使用Otsu方法计算最佳阈值 threshold(input_grey, img_threshold, threadHoldV, 255, CV_THRESH_BINARY_INV); - + // 应用计算出的最佳阈值进行反向二值化,即高于阈值的像素点变为0,低于阈值的变为255 utils::imwrite("resources/image/tmp/inputgray2.jpg", img_threshold); // threshold(input_grey, img_threshold, 10, 255, CV_THRESH_OTSU + // CV_THRESH_BINARY_INV); - } else + } else// 如果既不是蓝色也不是黄色的车牌 threshold(input_grey, img_threshold, 10, 255, CV_THRESH_OTSU + CV_THRESH_BINARY); - + // 直接使用固定阈值10进行OTSU二值化处理 //img_threshold = input_grey.clone(); //spatial_ostu(img_threshold, 8, 2, plateType); @@ -399,13 +453,13 @@ void deleteNotArea(Mat &inmat, Color color = UNKNOWN) { int top = 0; int bottom = img_threshold.rows - 1; clearLiuDing(img_threshold, top, bottom); - - if (0) { + //清除图像中噪声 + if (0) { //用于调试使用 imshow("inmat", inmat); waitKey(0); destroyWindow("inmat"); } - + // if (bFindLeftRightBound1(img_threshold, posLeft, posRight)) { inmat = inmat(Rect(posLeft, top, w - posLeft, bottom - top)); if (0) { @@ -414,6 +468,7 @@ void deleteNotArea(Mat &inmat, Color color = UNKNOWN) { destroyWindow("inmat"); } } + // 如果找到了图像的左右边界,则将图像裁剪为这个边界内的部分。 } @@ -422,20 +477,21 @@ int CPlateLocate::deskew(const Mat &src, const Mat &src_b, vector &outPlates, bool useDeteleArea, Color color) { Mat mat_debug; src.copyTo(mat_debug); + //创建一个新的Mat对象,并将源图像复制到这个新对象。 - for (size_t i = 0; i < inRects.size(); i++) { + for (size_t i = 0; i < inRects.size(); i++) { //遍历输入的旋转句型 RotatedRect roi_rect = inRects[i]; float r = (float) roi_rect.size.width / (float) roi_rect.size.height; - float roi_angle = roi_rect.angle; + float roi_angle = roi_rect.angle;//计算旋转矩形的宽高比和角度。 Size roi_rect_size = roi_rect.size; if (r < 1) { roi_angle = 90 + roi_angle; swap(roi_rect_size.width, roi_rect_size.height); } - - if (m_debug) { + //如果宽高比小于1,说明矩形是竖直的,需要调整角度和宽高。 + if (m_debug) { //调试模式,绘制旋转矩形的边界 Point2f rect_points[4]; roi_rect.points(rect_points); for (int j = 0; j < 4; j++) @@ -448,14 +504,14 @@ int CPlateLocate::deskew(const Mat &src, const Mat &src_b, // rotation < m_angel; // m_angle=60 - if (roi_angle - m_angle < 0 && roi_angle + m_angle > 0) { + if (roi_angle - m_angle < 0 && roi_angle + m_angle > 0) { //旋转矩形的角度在合理范围时 Rect_ safeBoundRect; - bool isFormRect = calcSafeRect(roi_rect, src, safeBoundRect); + bool isFormRect = calcSafeRect(roi_rect, src, safeBoundRect); //计算旋转矩形的安全边界。 if (!isFormRect) continue; Mat bound_mat = src(safeBoundRect); Mat bound_mat_b = src_b(safeBoundRect); - + //根据安全边界,从源图像中截取子图像。 if (0) { imshow("bound_mat_b", bound_mat_b); waitKey(0); @@ -468,13 +524,16 @@ int CPlateLocate::deskew(const Mat &src, const Mat &src_b, if ((roi_angle - 5 < 0 && roi_angle + 5 > 0) || 90.0 == roi_angle || -90.0 == roi_angle) { deskew_mat = bound_mat; - } else { + //如果矩形角度接近0度或90度,直接使用截取的子图像, + + } else {//倾斜调整 Mat rotated_mat; Mat rotated_mat_b; if (!rotation(bound_mat, rotated_mat, roi_rect_size, roi_ref_center, roi_angle)) continue; - + //对bound_mat应用旋转,旋转后存储在rotated_mat中, + //若旋转失败,继续下一次循环 if (!rotation(bound_mat_b, rotated_mat_b, roi_rect_size, roi_ref_center, roi_angle)) continue; @@ -483,7 +542,9 @@ int CPlateLocate::deskew(const Mat &src, const Mat &src_b, // imshow("1roated_mat",rotated_mat); // imshow("rotated_mat_b",rotated_mat_b); if (isdeflection(rotated_mat_b, roi_angle, roi_slope)) { + // 检查rotated_mat_b是否需要倾斜调整 affine(rotated_mat, deskew_mat, roi_slope); + //应用仿射变换 } else deskew_mat = rotated_mat; } @@ -494,18 +555,23 @@ int CPlateLocate::deskew(const Mat &src, const Mat &src_b, // haitungaga add,affect 25% to full recognition. if (useDeteleArea) deleteNotArea(deskew_mat, color); + //用于删除非区域部分 if (deskew_mat.cols * 1.0 / deskew_mat.rows > 2.3 && deskew_mat.cols * 1.0 / deskew_mat.rows < 6) { + //检查经过校正的图像(deskew_mat)的长宽比是否在某个范围内 if (deskew_mat.cols >= WIDTH || deskew_mat.rows >= HEIGHT) + // 如果校正后的图像的宽(cols)或高(rows)超出预设的阈值(WIDTH或HEIGHT) + resize(deskew_mat, plate_mat, plate_mat.size(), 0, 0, INTER_AREA); + // 使用INTER_AREA(区域插值)方法缩小图像,保持plate_mat.size()大小 else resize(deskew_mat, plate_mat, plate_mat.size(), 0, 0, INTER_CUBIC); - - CPlate plate; + // 如果宽或高没有超出阈值,则使用INTER_CUBIC(三次插值)方法放大图像,保持plate_mat.size()大小 + CPlate plate; //存储车牌信息 plate.setPlatePos(roi_rect); plate.setPlateMat(plate_mat); - if (color != UNKNOWN) plate.setPlateColor(color); - outPlates.push_back(plate); + if (color != UNKNOWN) plate.setPlateColor(color); // 如果车牌颜色已知,则设置车牌颜色 + outPlates.push_back(plate); //将包含车牌信息的对象输出 } } } @@ -515,7 +581,7 @@ int CPlateLocate::deskew(const Mat &src, const Mat &src_b, bool CPlateLocate::rotation(Mat &in, Mat &out, const Size rect_size, const Point2f center, const double angle) { - if (0) { + if (0) {//通过打印imshow信息调试 imshow("in", in); waitKey(0); destroyWindow("in"); @@ -523,33 +589,33 @@ bool CPlateLocate::rotation(Mat &in, Mat &out, const Size rect_size, Mat in_large; in_large.create(int(in.rows * 1.5), int(in.cols * 1.5), in.type()); - + //创建一个1.5倍in图像的行数、1.5倍in图像的列数且同类的图像 float x = in_large.cols / 2 - center.x > 0 ? in_large.cols / 2 - center.x : 0; float y = in_large.rows / 2 - center.y > 0 ? in_large.rows / 2 - center.y : 0; float width = x + in.cols < in_large.cols ? in.cols : in_large.cols - x; float height = y + in.rows < in_large.rows ? in.rows : in_large.rows - y; - + //计算原始图像 in 在放大图像 in_large 中的位置,确保其居中且不超出 in_large 的边界。 /*assert(width == in.cols); assert(height == in.rows);*/ if (width != in.cols || height != in.rows) return false; - - Mat imageRoi = in_large(Rect_(x, y, width, height)); + //如果计算得到的宽度和高度与原图 in 不匹配,函数返回 false。 + Mat imageRoi = in_large(Rect_(x, y, width, height));//合并原图并放大 addWeighted(imageRoi, 0, in, 1, 0, imageRoi); - + //选择 in_large 中的一个区域 imageRoi,将原图 in 覆盖到这个区域上。 Point2f center_diff(in.cols / 2.f, in.rows / 2.f); Point2f new_center(in_large.cols / 2.f, in_large.rows / 2.f); Mat rot_mat = getRotationMatrix2D(new_center, angle, 1); - + //使用 OpenCV 函数 getRotationMatrix2D 生成旋转矩阵 rot_mat。 /*imshow("in_copy", in_large); waitKey(0);*/ Mat mat_rotated; warpAffine(in_large, mat_rotated, rot_mat, Size(in_large.cols, in_large.rows), CV_INTER_CUBIC); - + //使用 warpAffine 函数将旋转矩阵应用于放大后的图像 in_large,结果存储在 mat_rotated 中 /*imshow("mat_rotated", mat_rotated); waitKey(0);*/ @@ -558,8 +624,9 @@ bool CPlateLocate::rotation(Mat &in, Mat &out, const Size rect_size, new_center, img_crop); out = img_crop; - - if (0) { + //使用 getRectSubPix 函数根据指定的尺寸和中心点从旋转后的图像中裁剪出区域, + //结果存储在 img_crop 中,然后赋值给输出参数 out。 + if (0) { //调试代码 imshow("out", out); waitKey(0); destroyWindow("out"); @@ -574,7 +641,9 @@ bool CPlateLocate::rotation(Mat &in, Mat &out, const Size rect_size, bool CPlateLocate::isdeflection(const Mat &in, const double angle, double &slope) { /*imshow("in",in); waitKey(0);*/ - if (0) { + + //用于检测输入图像 in 是否有偏转,并计算斜率 slope + if (0) { //用于调试 imshow("in", in); waitKey(0); destroyWindow("in"); @@ -584,16 +653,16 @@ bool CPlateLocate::isdeflection(const Mat &in, const double angle, int nCols = in.cols; assert(in.channels() == 1); - + //获取图像的行数 nRows 和列数 nCols,并确认图像是单通道(灰度图)。 int comp_index[3]; int len[3]; - + // 分别计算1/4、1/2、3/4高度处的行索引 comp_index[0] = nRows / 4; comp_index[1] = nRows / 4 * 2; comp_index[2] = nRows / 4 * 3; const uchar* p; - + // 这个循环会在每个四分位的行上找到第一个非零值的位置 for (int i = 0; i < 3; i++) { int index = comp_index[i]; p = in.ptr(index); @@ -603,6 +672,7 @@ bool CPlateLocate::isdeflection(const Mat &in, const double angle, while (0 == value && j < nCols) value = int(p[j++]); len[i] = j; + } // cout << "len[0]:" << len[0] << endl; @@ -618,19 +688,15 @@ bool CPlateLocate::isdeflection(const Mat &in, const double angle, double PI = 3.14159265; double g = tan(angle * PI / 180.0); - + //检查最长和最短长度是否有显著差异 if (maxlen - len[1] > nCols / 32 || len[1] - minlen > nCols / 32) { double slope_can_1 = double(len[2] - len[0]) / double(comp_index[1]); double slope_can_2 = double(len[1] - len[0]) / double(comp_index[0]); double slope_can_3 = double(len[2] - len[1]) / double(comp_index[0]); - // cout<<"angle:"<=0) + + // 选择和输入角度的正切值差异最小的斜率为最终值 slope = abs(slope_can_1 - g) <= abs(slope_can_2 - g) ? slope_can_1 : slope_can_2; // cout << "slope:" << slope << endl; @@ -640,21 +706,23 @@ bool CPlateLocate::isdeflection(const Mat &in, const double angle, } return false; + } void CPlateLocate::affine(const Mat &in, Mat &out, const double slope) { - // imshow("in", in); - // waitKey(0); + + //对输入图像进行仿射变换,用于矫正车牌图像倾斜 Point2f dstTri[3]; Point2f plTri[3]; - + //输入图像的高度和宽度 float height = (float) in.rows; float width = (float) in.cols; float xiff = (float) abs(slope) * height; if (slope > 0) { + //如果斜率 slope > 0,变换将图像向右倾斜。 // right, new position is xiff/2 @@ -666,7 +734,7 @@ void CPlateLocate::affine(const Mat &in, Mat &out, const double slope) { dstTri[1] = Point2f(width - 1 - xiff / 2, 0); dstTri[2] = Point2f(xiff / 2, height - 1); } else { - + //如果斜率 slope < 0,变换将图像向左倾斜。 // left, new position is -xiff/2 plTri[0] = Point2f(0 + xiff, 0); @@ -679,15 +747,18 @@ void CPlateLocate::affine(const Mat &in, Mat &out, const double slope) { } Mat warp_mat = getAffineTransform(plTri, dstTri); - + //使用 OpenCV 的 getAffineTransform 函数,根据源点和目标点计算仿射变换矩阵 warp_mat。 Mat affine_mat; affine_mat.create((int) height, (int) width, TYPE); - if (in.rows > HEIGHT || in.cols > WIDTH) + if (in.rows > HEIGHT || in.cols > WIDTH)//根据输入图像的大小,选择不同的插值方法: + + //如果图像的大小超过预设的 HEIGHT 或 WIDTH,使用 CV_INTER_AREA 插值,这个通常用于缩小。 warpAffine(in, affine_mat, warp_mat, affine_mat.size(), CV_INTER_AREA); else + //否则使用 CV_INTER_CUBIC 插值,这个插值方法在放大时可以提供平滑的边界。 warpAffine(in, affine_mat, warp_mat, affine_mat.size(), CV_INTER_CUBIC); out = affine_mat; @@ -695,22 +766,24 @@ void CPlateLocate::affine(const Mat &in, Mat &out, const double slope) { int CPlateLocate::plateColorLocate(Mat src, vector &candPlates, int index) { + //初始化两个 RotatedRect 类型的向量 rects_color_blue 和 rects_color_yellow,以及两个 CPlate 类型的向量 plates_blue 和 plates_yellow vector rects_color_blue; rects_color_blue.reserve(64); vector rects_color_yellow; rects_color_yellow.reserve(64); - + //这些向量用于存储找到的蓝色和黄色车牌的位置和信息。 vector plates_blue; plates_blue.reserve(64); vector plates_yellow; plates_yellow.reserve(64); Mat src_clone = src.clone(); - + //使用 OpenMP 并行处理,分别对蓝色和黄色车牌进行搜索和倾斜矫正。 + //这是通过调用 colorSearch 和 deskew 函数完成的。 Mat src_b_blue; Mat src_b_yellow; #pragma omp parallel sections - { + { #pragma omp section { colorSearch(src, BLUE, src_b_blue, rects_color_blue); @@ -722,6 +795,7 @@ int CPlateLocate::plateColorLocate(Mat src, vector &candPlates, deskew(src_clone, src_b_yellow, rects_color_yellow, plates_yellow, true, YELLOW); } } + //将找到的蓝色和黄色车牌信息添加到 candPlates 向量中。 candPlates.insert(candPlates.end(), plates_blue.begin(), plates_blue.end()); candPlates.insert(candPlates.end(), plates_yellow.begin(), plates_yellow.end()); @@ -817,10 +891,13 @@ int CPlateLocate::plateMserLocate(Mat src, vector &candPlates, int img_i int CPlateLocate::sobelOperT(const Mat &in, Mat &out, int blurSize, int morphW, int morphH) { + //在输入图像(src)中使用 MSER(最大稳定极值区域)方法定位车牌 Mat mat_blur; mat_blur = in.clone(); GaussianBlur(in, mat_blur, Size(blurSize, blurSize), 0, 0, BORDER_DEFAULT); + //对输入图像进行高斯模糊,这是为了减少噪声 + //将模糊后的图像转换为灰度图像。如果原图像已经是灰度图,则直接使用。 Mat mat_gray; if (mat_blur.channels() == 3) cvtColor(mat_blur, mat_gray, CV_BGR2GRAY); @@ -834,24 +911,25 @@ int CPlateLocate::sobelOperT(const Mat &in, Mat &out, int blurSize, int morphW, int scale = SOBEL_SCALE; int delta = SOBEL_DELTA; int ddepth = SOBEL_DDEPTH; - + //对灰度图像应用 Sobel 操作,得到 x 和 y 方向的梯度。 Mat grad_x, grad_y; Mat abs_grad_x, abs_grad_y; Sobel(mat_gray, grad_x, ddepth, 1, 0, 3, scale, delta, BORDER_DEFAULT); convertScaleAbs(grad_x, abs_grad_x); - + //将 x 方向的梯度转换为绝对值,然后与 y 方向的梯度合并(假设 y 方向的梯度为0)。 Mat grad; addWeighted(abs_grad_x, 1, 0, 0, 0, grad); utils::imwrite("resources/image/tmp/graygrad.jpg", grad); + //使用 Otsu 的阈值法对得到的梯度图像进行二值化 Mat mat_threshold; double otsu_thresh_val = threshold(grad, mat_threshold, 0, 255, CV_THRESH_OTSU + CV_THRESH_BINARY); utils::imwrite("resources/image/tmp/grayBINARY.jpg", mat_threshold); - + //对二值化的图像进行形态学闭操作,这有助于连接相邻的区域。 Mat element = getStructuringElement(MORPH_RECT, Size(morphW, morphH)); morphologyEx(mat_threshold, mat_threshold, MORPH_CLOSE, element); @@ -865,6 +943,7 @@ int CPlateLocate::sobelOperT(const Mat &in, Mat &out, int blurSize, int morphW, int CPlateLocate::plateSobelLocate(Mat src, vector &candPlates, int index) { vector rects_sobel_all; + //引用传递的 CPlate 类的矢量,用于存储最后识别为候选车牌的结果。 rects_sobel_all.reserve(256); vector plates; @@ -874,11 +953,13 @@ int CPlateLocate::plateSobelLocate(Mat src, vector &candPlates, bound_rects.reserve(256); sobelFrtSearch(src, bound_rects); - + //使用 Sobel 算子处理输入的图像 src 并返回可能的边界矩形 bound_rects。 vector> bound_rects_part; bound_rects_part.reserve(256); // enlarge area + //通过扩大每个边界矩形的面积进行进一步处理,这通常是为了使候选区域更大, + //以包含整个车牌。代码通过改变矩形的 x 坐标,宽度,和 y 坐标,高度来实现此目的。 for (size_t i = 0; i < bound_rects.size(); i++) { float fRatio = bound_rects[i].width * 1.0f / bound_rects[i].height; if (fRatio < 3.0 && fRatio > 1.0 && bound_rects[i].height < 120) { @@ -901,6 +982,8 @@ int CPlateLocate::plateSobelLocate(Mat src, vector &candPlates, } // second processing to split one + //使用 OpenMP 并行处理进行第二次搜索。pragma omp parallel for 使循环并行执行, + //以加快处理速度。在每次循环中,对于每个边界矩形: #pragma omp parallel for for (int i = 0; i < (int)bound_rects_part.size(); i++) { Rect_ bound_rect = bound_rects_part[i]; @@ -916,12 +999,13 @@ int CPlateLocate::plateSobelLocate(Mat src, vector &candPlates, Rect_ safe_bound_rect(x, y, width, height); Mat bound_mat = src(safe_bound_rect); - + //调用 sobelSecSearchPart 函数,它可能进一步处理提取的子图并返回可能的车牌候选区域 rects_sobel vector rects_sobel; rects_sobel.reserve(128); sobelSecSearchPart(bound_mat, refpoint, rects_sobel); #pragma omp critical +//确保当多个线程尝试将其搜索结果添加到 rects_sobel_all 集合时,不会发生冲突。 { rects_sobel_all.insert(rects_sobel_all.end(), rects_sobel.begin(), rects_sobel.end()); } @@ -954,13 +1038,15 @@ int CPlateLocate::plateSobelLocate(Mat src, vector &candPlates, } Mat src_b; + //调用 sobelOper 函数来执行 Sobel 操作。 sobelOper(src, src_b, 3, 10, 3); + //每个可能的矩形区域都发送给 deskew 函数,这个函数可能旨在纠正候选车牌的偏斜。 deskew(src, src_b, rects_sobel_all, plates); //for (size_t i = 0; i < plates.size(); i++) // candPlates.push_back(plates[i]); - + //得到的车牌从 plates 转移至 candPlates candPlates.insert(candPlates.end(), plates.begin(), plates.end()); return 0; @@ -968,12 +1054,15 @@ int CPlateLocate::plateSobelLocate(Mat src, vector &candPlates, int CPlateLocate::plateLocate(Mat src, vector &resultVec, int index) { + //对输入图像src执行车牌定位,并将定位到的车牌图像放入resultVec中 vector all_result_Plates; - + //三个函数分别使用颜色定位、Sobel边缘检测和MSER算法来识别车牌 plateColorLocate(src, all_result_Plates, index); plateSobelLocate(src, all_result_Plates, index); plateMserLocate(src, all_result_Plates, index); + //循环通过all_result_Plates,对于每个CPlate对象, + //调用getPlateMat()获取车牌对应的图像,并将其添加到resultVec向量中。 for (size_t i = 0; i < all_result_Plates.size(); i++) { CPlate plate = all_result_Plates[i]; resultVec.push_back(plate.getPlateMat()); @@ -983,12 +1072,14 @@ int CPlateLocate::plateLocate(Mat src, vector &resultVec, int index) { } int CPlateLocate::plateLocate(Mat src, vector &resultVec, int index) { + //对输入图像src执行车牌定位,将定位到的车牌对象(CPlate)放入resultVec中 vector all_result_Plates; plateColorLocate(src, all_result_Plates, index); plateSobelLocate(src, all_result_Plates, index); plateMserLocate(src, all_result_Plates, index); - + + //循环通过all_result_Plates,将每一个CPlate对象直接添加到resultVec向量中。 for (size_t i = 0; i < all_result_Plates.size(); i++) { resultVec.push_back(all_result_Plates[i]); } diff --git a/src/src/core/plate_recognize.cpp b/src/src/core/plate_recognize.cpp index a351709..175659e 100644 --- a/src/src/core/plate_recognize.cpp +++ b/src/src/core/plate_recognize.cpp @@ -7,7 +7,10 @@ namespace easypr { CPlateRecognize::CPlateRecognize() { m_showResult = false; } - +// 这段代码是C++中的类定义,定义了CPlateRecognize类的构造函数,初始化了m_showResult成员变量为false。 +// 该类包含plateRecognize方法,用于车牌识别,包括车牌检测和字符识别两部分。 +// 还包含了一些Load方法,用于加载模型文件。 +// 代码中使用了OpenCV库中的Mat类和一些自定义的类和方法。 // main method, plate recognize, contain two parts // 1. plate detect @@ -20,6 +23,9 @@ int CPlateRecognize::plateRecognize(const Mat& src, std::vector &plateVe // 1. plate detect std::vector plateVec; int resultPD = plateDetect(img, plateVec, img_index); + // 这段代码是C++中的类方法CPlateRecognize::plateRecognize的一部分,用于车牌识别。 +// 首先对输入图像进行统一尺寸的调整,然后进行车牌检测,将结果存储在plateVec中。 +// 返回值resultPD表示车牌检测的结果,0表示成功,非0表示失败。 if (resultPD == 0) { size_t num = plateVec.size(); for (size_t j = 0; j < num; j++) { @@ -31,7 +37,12 @@ int CPlateRecognize::plateRecognize(const Mat& src, std::vector &plateVe item.setPlateScale(scale); RotatedRect rect = item.getPlatePos(); item.setPlatePos(scaleBackRRect(rect, 1.f / scale)); - +// 如果车牌检测成功(resultPD == 0),则对每个检测到的车牌进行处理: +// 1. 获取车牌图像并显示 +// 2. 调整车牌位置和大小 +// 3. 获取车牌颜色并识别字符 +// 4. 将识别结果存储,并根据识别结果设置车牌字符串 +// 5. 根据显示类型展示检测结果或识别结果 // get plate color Color color = item.getPlateColor(); if (color == UNKNOWN) { @@ -42,7 +53,10 @@ int CPlateRecognize::plateRecognize(const Mat& src, std::vector &plateVe if (0) { std::cout << "plateColor:" << plateColor << std::endl; } - +// 这段代码用于获取车牌颜色并进行处理。 +// 首先获取车牌颜色,如果颜色为UNKNOWN,则调用getPlateType方法获取颜色并设置到item中。 +// 然后将颜色转换为字符串形式并存储在plateColor中。 +// 最后,通过条件判断,如果条件为0,则输出plateColor到控制台。 // 2. chars recognize std::string plateIdentify = ""; int resultCR = charsRecognise(item, plateIdentify); @@ -59,6 +73,11 @@ int CPlateRecognize::plateRecognize(const Mat& src, std::vector &plateVe if (0) std::cout << "resultCR:" << resultCR << std::endl; } } + // 这段代码是C++中的类方法CPlateRecognize::plateRecognize的一部分,用于字符识别。 +// 首先定义了一个空字符串plateIdentify,并调用charsRecognise方法进行字符识别,结果存储在resultCR中。 +// 如果resultCR为0,则将plateColor和plateIdentify拼接成license字符串,并设置到item中,然后将item存入plateVecOut中。 +// 否则,只将plateColor设置到license字符串中,然后将item存入plateVecOut中。 +// 最后,根据条件判断,如果条件为0,则输出resultCR到控制台。 if (getResultShow()) { // param type: 0 detect, 1 recognize; int showType = 1; @@ -70,7 +89,9 @@ int CPlateRecognize::plateRecognize(const Mat& src, std::vector &plateVe } return resultPD; } - +// 这段代码是C++中的类方法CPlateRecognize::plateRecognize的一部分,用于根据getResultShow()的返回值决定是否展示检测结果或识别结果。 +// 如果getResultShow()返回true,则根据showType的值选择展示检测结果或识别结果。 +// 最后返回resultPD,表示车牌检测的结果。 void CPlateRecognize::LoadSVM(std::string path) { PlateJudge::instance()->LoadModel(path); } @@ -78,7 +99,12 @@ void CPlateRecognize::LoadSVM(std::string path) { void CPlateRecognize::LoadANN(std::string path) { CharsIdentify::instance()->LoadModel(path); } +// 评注该代码 +// 这部分代码定义了CPlateRecognize类的两个方法:LoadSVM和LoadANN。 +// LoadSVM方法用于加载SVM模型,调用PlateJudge类的实例的LoadModel方法加载模型。 +// LoadANN方法用于加载ANN模型,调用CharsIdentify类的实例的LoadModel方法加载模型。 +// 这些方法用于在车牌识别过程中加载相关的机器学习模型。 void CPlateRecognize::LoadChineseANN(std::string path) { CharsIdentify::instance()->LoadChineseModel(path); } @@ -90,7 +116,9 @@ void CPlateRecognize::LoadGrayChANN(std::string path) { void CPlateRecognize::LoadChineseMapping(std::string path) { CharsIdentify::instance()->LoadChineseMapping(path); } - +// 这部分代码定义了CPlateRecognize类的三个方法:LoadChineseANN、LoadGrayChANN和LoadChineseMapping。 +// 这些方法用于在字符识别过程中加载相关的中文字符识别模型和映射文件。 +// 分别调用CharsIdentify类的实例的LoadChineseModel、LoadGrayChANN和LoadChineseMapping方法加载模型和映射文件。 // deprected int CPlateRecognize::plateRecognize(const Mat& src, std::vector &licenseVec) { vector plates; @@ -102,4 +130,8 @@ int CPlateRecognize::plateRecognize(const Mat& src, std::vector &li return resultPR; } -} \ No newline at end of file +} +// 这段代码定义了CPlateRecognize类的一个新方法plateRecognize,接受一个输入图像和一个字符串向量作为参数。 +// 在方法内部,首先创建了一个CPlate类型的向量plates,并调用了原有的plateRecognize方法来进行车牌识别。 +// 然后遍历plates向量中的每个车牌,将其车牌字符串存入传入的licenseVec向量中。 +// 最后返回了车牌识别的结果resultPR。 \ No newline at end of file diff --git a/src/src/core/用例图.png b/src/src/core/用例图.png new file mode 100644 index 0000000..4903c0d Binary files /dev/null and b/src/src/core/用例图.png differ diff --git a/src/src/train/annCh_train.cpp b/src/src/train/annCh_train.cpp index ec98607..fef78cc 100644 --- a/src/src/train/annCh_train.cpp +++ b/src/src/train/annCh_train.cpp @@ -1,187 +1,257 @@ -#include -#include - -#include "easypr/train/annCh_train.h" -#include "easypr/config.h" -#include "easypr/core/chars_identify.h" -#include "easypr/core/feature.h" -#include "easypr/core/core_func.h" -#include "easypr/util/util.h" -#include "easypr/train/create_data.h" - -namespace easypr { - - AnnChTrain::AnnChTrain(const char* chars_folder, const char* xml) - : chars_folder_(chars_folder), ann_xml_(xml) - { - ann_ = cv::ml::ANN_MLP::create(); - type = 1; - kv_ = std::shared_ptr(new Kv); - kv_->load("resources/text/province_mapping"); - extractFeature = getGrayPlusProject; -} - - void AnnChTrain::train() - { - int classNumber = 0; - int input_number = 0; - int hidden_number = 0; - int output_number = 0; - - bool useLBP = false; - if (useLBP) - input_number = kCharLBPPatterns * kCharLBPGridX * kCharLBPGridY; - else - input_number = kGrayCharHeight * kGrayCharWidth; - - input_number += 64; - - classNumber = kChineseNumber; - hidden_number = kCharHiddenNeurans; - output_number = classNumber; - cv::Mat layers; - - int first_hidden_neurons = 48; - int second_hidden_neurons = 32; - - int N = input_number; - int m = output_number; - //int first_hidden_neurons = int(std::sqrt((m + 2) * N) + 2 * std::sqrt(N / (m + 2))); - //int second_hidden_neurons = int(m * std::sqrt(N / (m + 2))); - - bool useTLFN = false; - if (!useTLFN) { - layers.create(1, 3, CV_32SC1); - layers.at(0) = input_number; - layers.at(1) = hidden_number; - layers.at(2) = output_number; - } - else { - fprintf(stdout, ">> Use two-layers neural networks,\n"); - fprintf(stdout, ">> First_hidden_neurons: %d \n", first_hidden_neurons); - fprintf(stdout, ">> Second_hidden_neurons: %d \n", second_hidden_neurons); - - layers.create(1, 4, CV_32SC1); - layers.at(0) = input_number; - layers.at(1) = first_hidden_neurons; - layers.at(2) = second_hidden_neurons; - layers.at(3) = output_number; - } - - ann_->setLayerSizes(layers); - ann_->setActivationFunction(cv::ml::ANN_MLP::SIGMOID_SYM, 1, 1); - ann_->setTrainMethod(cv::ml::ANN_MLP::TrainingMethods::BACKPROP); - ann_->setTermCriteria(cvTermCriteria(CV_TERMCRIT_ITER, 30000, 0.0001)); - ann_->setBackpropWeightScale(0.1); - ann_->setBackpropMomentumScale(0.1); - - auto files = Utils::getFiles(chars_folder_); - if (files.size() == 0) { - fprintf(stdout, "No file found in the train folder!\n"); - fprintf(stdout, "You should create a folder named \"tmp\" in EasyPR main folder.\n"); - fprintf(stdout, "Copy train data folder(like \"annCh\") under \"tmp\". \n"); - return; +#include //包含了C++标准库中的头文件,提供了数值计算的相关函数和模板。 +#include //包含了C++标准库中的头文件,提供了关于时间和日期的相关函数和类型。 + +#include "easypr/train/annCh_train.h"//包含了EasyPR库中的annCh_train.h头文件,这个头文件可能包含了用于训练ANN(人工神经网络)字符识别的相关函数和类。 +#include "easypr/config.h"//包含了EasyPR库的config.h头文件,这个头文件可能包含了一些配置EasyPR库的全局变量和宏定义。 +#include "easypr/core/chars_identify.h"//包含了EasyPR库的chars_identify.h头文件,这个头文件可能包含了字符识别的核心功能的声明。 +#include "easypr/core/feature.h"//包含了EasyPR库的feature.h头文件,这个头文件可能包含了特征提取和处理的相关的函数和类。 +#include "easypr/core/core_func.h"//包含了EasyPR库的core_func.h头文件,这个头文件可能包含了一些核心的函数和类。 +#include "easypr/util/util.h"//包含了EasyPR库的util.h头文件,这个头文件可能包含了一些工具函数和类。 +#include "easypr/train/create_data.h"//包含了EasyPR库的create_data.h头文件,这个头文件可能包含了用于创建训练数据的函数和类。 + +namespace easypr { // 定义命名空间easypr + + AnnChTrain::AnnChTrain(const char* chars_folder, const char* xml) // 定义构造函数,参数为字符文件夹路径和xml文件路径 + : chars_folder_(chars_folder), ann_xml_(xml) // 初始化chars_folder_和ann_xml_成员变量 + { + ann_ = cv::ml::ANN_MLP::create(); // 创建一个MLP(Multilayer Perceptron,多层感知器)对象,用于字符识别 + type = 1; // 初始化type为1,可能表示某种类型或模式 + kv_ = std::shared_ptr(new Kv); // 创建一个Kv对象,并使用std::shared_ptr管理内存,实现共享所有权模型 + kv_->load("resources/text/province_mapping"); // 加载kv_对象,可能从文件"resources/text/province_mapping"中加载数据 + extractFeature = getGrayPlusProject; // 初始化extractFeature函数指针,指向getGrayPlusProject函数,用于特征提取 } - - // using raw data or raw + synthic data. - trainVal(m_number_for_count); +void AnnChTrain::train() +{ + int classNumber = 0; // 类别数量,初始化为0,需要在后续代码中赋值 + int input_number = 0; // 输入节点数量,初始化为0,需要在后续代码中赋值 + int hidden_number = 0; // 隐藏层节点数量,初始化为0,需要在后续代码中赋值 + int output_number = 0; // 输出节点数量,初始化为0,需要在后续代码中赋值 + + bool useLBP = false; // 是否使用LBP特征,初始化为false + if (useLBP) // 如果使用LBP特征 + input_number = kCharLBPPatterns * kCharLBPGridX * kCharLBPGridY; // 则设置输入节点数量为LBP特征的数量 + else + input_number = kGrayCharHeight * kGrayCharWidth; // 否则设置输入节点数量为字符图像的高度和宽度的乘积 + + input_number += 64; // 在输入节点数量基础上加64,可能是为了增加一些额外的输入节点 } -std::pair AnnChTrain::identifyGrayChinese(cv::Mat input) { - Mat feature; - extractFeature(input, feature); - float maxVal = -2; - int result = 0; - - cv::Mat output(1, kChineseNumber, CV_32FC1); - ann_->predict(feature, output); - - for (int j = 0; j < kChineseNumber; j++) { - float val = output.at(j); - //std::cout << "j:" << j << "val:" << val << std::endl; - if (val > maxVal) { - maxVal = val; - result = j; - } - } - - auto index = result + kCharsTotalNumber - kChineseNumber; - const char* key = kChars[index]; - std::string s = key; - std::string province = kv_->get(s); - - return std::make_pair(s, province); +classNumber = kChineseNumber; // 类别数量,这里假设 kChineseNumber 是一个定义好的常量 +hidden_number = kCharHiddenNeurons; // 隐藏层节点数量,这里假设 kCharHiddenNeurons 是一个定义好的常量 +output_number = classNumber; // 输出节点数量,等于类别数量 +cv::Mat layers; // 声明一个 OpenCV 的 Mat 对象,用于存储网络层的数据,但在这段代码中没有使用 + +int first_hidden_neurons = 48; // 第一隐藏层节点数量,硬编码为48 +int second_hidden_neurons = 32; // 第二隐藏层节点数量,硬编码为32 + +int N = input_number; // 输入节点数量,这里假设 input_number 是一个定义好的变量 +int m = output_number; // 输出节点数量,等于类别数量,这里假设 output_number 是一个定义好的变量 + +// 在这里注释掉了两行代码,它们原先可能是用于计算第一层和第二层隐藏层的节点数量的公式 +//int first_hidden_neurons = int(std::sqrt((m + 2) * N) + 2 * std::sqrt(N / (m + 2))); +//int second_hidden_neurons = int(m * std::sqrt(N / (m + 2))); + +bool useTLFN = false; // 是否使用TLFN,初始化为false,但在这段代码中没有使用 + +if (!useTLFN) { // 如果不使用两层神经网络(TLFN) + layers.create(1, 3, CV_32SC1); // 创建一个1行3列的OpenCV Mat对象,数据类型为32位有符号整数 + layers.at(0) = input_number; // 设置输入层节点数量 + layers.at(1) = hidden_number; // 设置隐藏层节点数量 + layers.at(2) = output_number; // 设置输出层节点数量 +} +else { // 如果使用两层神经网络(TLFN) + fprintf(stdout, ">> Use two-layers neural networks,\n"); // 打印信息到标准输出,表示正在使用两层神经网络 + fprintf(stdout, ">> First_hidden_neurons: %d \n", first_hidden_neurons); // 打印第一层隐藏层节点数量到标准输出 + fprintf(stdout, ">> Second_hidden_neurons: %d \n", second_hidden_neurons); // 打印第二层隐藏层节点数量到标准输出 + + layers.create(1, 4, CV_32SC1); // 创建一个1行4列的OpenCV Mat对象,数据类型为32位有符号整数 + layers.at(0) = input_number; // 设置输入层节点数量 + layers.at(1) = first_hidden_neurons; // 设置第一层隐藏层节点数量 + layers.at(2) = second_hidden_neurons; // 设置第二层隐藏层节点数量 + layers.at(3) = output_number; // 设置输出层节点数量 } - void AnnChTrain::test() { - //TODO +// 设置神经网络层的大小 +ann_->setLayerSizes(layers); + +// 设置激活函数为Sigmoid函数,其对称性取决于第二个参数,第三个参数是该函数的斜率 +ann_->setActivationFunction(cv::ml::ANN_MLP::SIGMOID_SYM, 1, 1); + +// 设置训练方法为反向传播法 +ann_->setTrainMethod(cv::ml::ANN_MLP::TrainingMethods::BACKPROP); + +// 设置训练终止条件为最大迭代次数30000次,或当误差小于0.0001时终止 +ann_->setTermCriteria(cvTermCriteria(CV_TERMCRIT_ITER, 30000, 0.0001)); + +// 设置权重的更新比例因子为0.1 +ann_->setBackpropWeightScale(0.1); + +// 设置权重的动量更新比例因子为0.1 +ann_->setBackpropMomentumScale(0.1); + +// 获取文件夹中的文件列表,如果文件列表为空,则打印错误信息并给出建议 +auto files = Utils::getFiles(chars_folder_); +if (files.size() == 0) { + fprintf(stdout, "No file found in the train folder!\n"); + fprintf(stdout, "You should create a folder named \"tmp\" in EasyPR main folder.\n"); + fprintf(stdout, "Copy train data folder(like \"annCh\") under \"tmp\". \n"); + return; +} + +// 使用原始数据或原始数据 + 合成的数据进行训练和验证,具体数量由 m_number_for_count 决定 +trainVal(m_number_for_count); + +// 定义一个方法,用于识别汉字 +// 参数:输入图像 +// 返回值:一个由汉字字符串和对应的省份字符串组成的pair +std::pair AnnChTrain::identifyGrayChinese(cv::Mat input) { + // 定义特征向量 + Mat feature; + // 从输入图像中提取特征 + extractFeature(input, feature); + // 初始化最大值为-2 + float maxVal = -2; + // 初始化结果为0 + int result = 0; + + // 定义输出矩阵,大小为1行,kChineseNumber列,数据类型为CV_32FC1(32位浮点型) + cv::Mat output(1, kChineseNumber, CV_32FC1); + // 使用神经网络模型进行预测,输入特征向量,输出结果到output矩阵中 + ann_->predict(feature, output); + + // 遍历输出矩阵中的每一个值 + for (int j = 0; j < kChineseNumber; j++) { + // 获取当前位置的值 + float val = output.at(j); + // 如果当前值大于maxVal,则更新maxVal和result的值 + //std::cout << "j:" << j << "val:" << val << std::endl; + if (val > maxVal) { + maxVal = val; + result = j; + } + } + + // 根据result的值计算索引index,注意这里进行了偏移操作,可能是因为字符集的索引与输出结果的索引之间存在偏移 + auto index = result + kCharsTotalNumber - kChineseNumber; + // 根据索引获取对应的字符key + const char* key = kChars[index]; + // 将字符key转换为字符串s + std::string s = key; + // 通过kv_(应该是某个键值对容器)获取与s对应的省份字符串,存储到province变量中 + std::string province = kv_->get(s); + + // 返回一个由字符s和省份province组成的pair对象 + return std::make_pair(s, province); +} + +// 定义一个方法,用于测试模型性能(目前为空) +void AnnChTrain::test() { + // TODO: 需要实现测试代码,评估模型的性能指标,如准确率、召回率等。 +} + +// 定义一个方法,用于训练验证集(目前为空) +void AnnChTrain::trainVal(size_t number_for_count) { + // 断言chars_folder_不为空,否则会抛出异常(TODO: 需要实现断言失败的处理逻辑) + assert(chars_folder_); + // 定义训练样本的存储容器train_samples(TODO: 这里需要解释这个变量名和变量的具体含义) + cv::Mat train_samples; + // 定义训练图像、验证图像的存储容器(TODO: 这里需要解释这些变量名和变量的具体含义) + std::vector train_images, val_images; + std::vector train_label, val_labels; + // 设置训练验证集分割比例为0.7(70%用于训练,30%用于验证) + float percentage = 0.7f; + // 设置类别数为kChineseNumber(TODO: 需要解释这个变量的具体含义)直接把代码改成评注形式 + +// 循环遍历每个字符类别 +for (int i = 0; i < classNumber; ++i) { + // 从kChars数组中获取当前字符的键 + auto char_key = kChars[i + kCharsTotalNumber - classNumber]; + // 定义一个字符数组sub_folder,用于存储子文件夹的路径,并初始化为0 + char sub_folder[512] = { 0 }; + // 使用sprintf函数将字符键和字符文件夹路径拼接,存入sub_folder + sprintf(sub_folder, "%s/%s", chars_folder_, char_key); + // 将字符键转化为字符串类型,方便后续操作 + std::string test_char(char_key); + // 如果test_char不等于"zh_yun",则跳过当前循环 + // if (test_char != "zh_yun") continue; + fprintf(stdout, ">> Testing characters %s in %s \n", char_key, sub_folder); + // 调用utils::getFiles函数获取子文件夹下的所有文件,存入chars_files + auto chars_files = utils::getFiles(sub_folder); + // 获取子文件夹下的文件数量 + size_t char_size = chars_files.size(); + fprintf(stdout, ">> Characters count: %d \n", (int)char_size); + + // 定义一个向量matVec,用于存储处理过的图像 + std::vector matVec; + // 为matVec预留空间,提高性能 + matVec.reserve(number_for_count); + // 内层循环,遍历子文件夹下的每一个文件 + for (auto file : chars_files) { + std::cout << file << std::endl; + // 使用OpenCV的imread函数读取图像,并将其转化为灰度图像 + auto img = cv::imread(file, IMREAD_GRAYSCALE); // a grayscale image + Mat img_resize; + // 为img_resize分配空间,并设置其大小和数据类型 + img_resize.create(kGrayCharHeight, kGrayCharWidth, CV_8UC1); + // 使用OpenCV的resize函数调整图像大小 + resize(img, img_resize, img_resize.size(), 0, 0, INTER_LINEAR); + // 将调整大小后的图像存入matVec + matVec.push_back(img_resize); + } +} +// 生成合成图像 +// genrate the synthetic images +for (int t = 0; t < (int)number_for_count - (int)char_size; t++) { + // 确定随机数的范围 + int rand_range = char_size + t; + // 生成一个随机数 + int ran_num = rand() % rand_range; + // 从matVec中获取一个图像 + auto img = matVec.at(ran_num); + // 显示该图像 + SHOW_IMAGE(img, 0); + // 生成合成图像 + auto simg = generateSyntheticImage(img); + // 显示合成图像 + SHOW_IMAGE(simg, 0); + // 将合成图像添加到matVec中 + matVec.push_back(simg); +} +// 输出matVec的大小 +fprintf(stdout, ">> Characters count: %d \n", (int)matVec.size()); + +// 对matVec进行随机排序 +// random sort the mat; +srand(unsigned(time(NULL))); +random_shuffle(matVec.begin(), matVec.end()); + +// 获取matVec的大小 +int mat_size = (int)matVec.size(); +// 计算分割索引 +int split_index = int((float)mat_size * percentage); +// 从后往前遍历matVec +for (int j = mat_size - 1; j >= 0; j--) { + // 从matVec中获取图像 + Mat img = matVec.at(j); + // 此处代码可能有误,因为该判断语句始终为真,无法起到分割训练集和验证集的作用 + // 应该根据split_index来分割训练集和验证集 + if (1) { + Mat feature; + // 提取图像特征 + extractFeature(img, feature); + if (j <= split_index) { + // 将特征和图像添加到训练样本和训练图像中 + train_samples.push_back(feature); + train_images.push_back(img); + train_label.push_back(i); + } + else { + // 将图像添加到验证图像中,将标签添加到验证标签中 + val_images.push_back(img); + val_labels.push_back(i); + } + } } - -void AnnChTrain::trainVal(size_t number_for_count) { - assert(chars_folder_); - cv::Mat train_samples; - std::vector train_images, val_images; - std::vector train_label, val_labels; - float percentage = 0.7f; - int classNumber = kChineseNumber; - - for (int i = 0; i < classNumber; ++i) { - auto char_key = kChars[i + kCharsTotalNumber - classNumber]; - char sub_folder[512] = { 0 }; - sprintf(sub_folder, "%s/%s", chars_folder_, char_key); - std::string test_char(char_key); - // if (test_char != "zh_yun") continue; - - fprintf(stdout, ">> Testing characters %s in %s \n", char_key, sub_folder); - auto chars_files = utils::getFiles(sub_folder); - size_t char_size = chars_files.size(); - fprintf(stdout, ">> Characters count: %d \n", (int)char_size); - - std::vector matVec; - matVec.reserve(number_for_count); - for (auto file : chars_files) { - std::cout << file << std::endl; - auto img = cv::imread(file, IMREAD_GRAYSCALE); // a grayscale image - Mat img_resize; - img_resize.create(kGrayCharHeight, kGrayCharWidth, CV_8UC1); - resize(img, img_resize, img_resize.size(), 0, 0, INTER_LINEAR); - matVec.push_back(img_resize); - } - // genrate the synthetic images - for (int t = 0; t < (int)number_for_count - (int)char_size; t++) { - int rand_range = char_size + t; - int ran_num = rand() % rand_range; - auto img = matVec.at(ran_num); - SHOW_IMAGE(img, 0); - auto simg = generateSyntheticImage(img); - SHOW_IMAGE(simg, 0); - matVec.push_back(simg); - } - fprintf(stdout, ">> Characters count: %d \n", (int)matVec.size()); - - // random sort the mat; - srand(unsigned(time(NULL))); - random_shuffle(matVec.begin(), matVec.end()); - - int mat_size = (int)matVec.size(); - int split_index = int((float)mat_size * percentage); - for (int j = mat_size - 1; j >= 0; j--) { - Mat img = matVec.at(j); - if (1) { - Mat feature; - extractFeature(img, feature); - if (j <= split_index) { - train_samples.push_back(feature); - train_images.push_back(img); - train_label.push_back(i); - } - else { - val_images.push_back(img); - val_labels.push_back(i); - } - } - } - } // generate train data train_samples.convertTo(train_samples, CV_32F); cv::Mat train_classes = cv::Mat::zeros((int)train_label.size(), classNumber, CV_32F); diff --git a/src/src/train/ann_train.cpp b/src/src/train/ann_train.cpp index 51aec1f..09525c4 100644 --- a/src/src/train/ann_train.cpp +++ b/src/src/train/ann_train.cpp @@ -21,6 +21,10 @@ AnnTrain::AnnTrain(const char* chars_folder, const char* xml) kv_->load("resources/text/province_mapping"); } +// 这段代码是C++的类成员函数AnnTrain::AnnTrain的构造函数实现。 +// 构造函数接受两个参数chars_folder和xml,并将其分别赋值给成员变量chars_folder_和ann_xml_。 +// 然后使用cv::ml::ANN_MLP::create()创建了一个神经网络对象ann_。 +// type被初始化为0,kv_被初始化为一个加载了"resources/text/province_mapping"的Kv对象。 void AnnTrain::train() { int classNumber = 0; @@ -45,7 +49,9 @@ void AnnTrain::train() { hidden_number = kNeurons; output_number = classNumber; } - +// 这段代码是AnnTrain类的train方法,根据type的值选择不同的classNumber、input_number、hidden_number和output_number。 +// 当type为0时,classNumber为kCharsTotalNumber,input_number为kAnnInput,hidden_number为kNeurons,output_number为classNumber。 +// 当type为1时,classNumber为kChineseNumber,input_number为kAnnInput,hidden_number为kNeurons,output_number为classNumber。 int N = input_number; int m = output_number; int first_hidden_neurons = int(std::sqrt((m + 2) * N) + 2 * std::sqrt(N / (m + 2))); @@ -70,7 +76,9 @@ void AnnTrain::train() { layers.at(2) = second_hidden_neurons; layers.at(3) = output_number; } - +//这段代码根据输入和输出的数量计算了两个隐藏层的神经元数量, +//并根据布尔变量useTLFN的值选择了创建三层或四层的神经网络层。如果useTLFN为false,则创建三层,否则创建四层。 +//在创建四层时,输出了两个隐藏层的神经元数量。 ann_->setLayerSizes(layers); ann_->setActivationFunction(cv::ml::ANN_MLP::SIGMOID_SYM, 1, 1); ann_->setTrainMethod(cv::ml::ANN_MLP::TrainingMethods::BACKPROP); @@ -99,7 +107,11 @@ void AnnTrain::train() { std::cout << "Your ANN Model was saved to " << ann_xml_ << std::endl; std::cout << "Training done. Time elapse: " << (end - start) / (1000 * 60) << "minute" << std::endl; } - +//这段代码是一个C++类成员函数AnnTrain::train的实现。 +//在这段代码中,神经网络ann_被训练并保存到ann_xml_文件中。 +//训练数据通过sdata(350)函数获取,然后使用ann_->train(traindata)进行训练。 +//训练完成后,会输出"Your ANN Model was saved to "以及训练所花费的时间。 +//同时,还会调用test()函数进行测试。如果训练文件夹中没有文件,则会输出相应的提示信息。 std::pair AnnTrain::identifyChinese(cv::Mat input) { cv::Mat feature = charFeatures2(input, kPredictSize); float maxVal = -2; @@ -124,7 +136,10 @@ std::pair AnnTrain::identifyChinese(cv::Mat input) { return std::make_pair(s, province); } - +// 这段代码是AnnTrain类的identifyChinese方法,接受一个cv::Mat类型的输入参数input。 +// 首先调用charFeatures2函数提取特征,然后使用神经网络ann_对特征进行预测,得到输出output。 +// 接着遍历output,找到最大值对应的索引result,并计算出最终的索引index。 +// 最后根据index获取对应的字符key,再通过kv_获取对应的省份province,最终返回一个包含字符和省份的pair。 std::pair AnnTrain::identify(cv::Mat input) { cv::Mat feature = charFeatures2(input, kPredictSize); @@ -155,7 +170,10 @@ std::pair AnnTrain::identify(cv::Mat input) { return std::make_pair(s, province); } } - +// 这段代码是AnnTrain类的identify方法,接受一个cv::Mat类型的输入参数input。 +// 首先调用charFeatures2函数提取特征,然后使用神经网络ann_对特征进行预测,得到输出output。 +// 接着遍历output,找到最大值对应的索引result,并计算出最终的索引index。 +// 最后根据index判断返回的字符和省份信息,返回一个包含字符和省份的pair。 void AnnTrain::test() { assert(chars_folder_); @@ -175,7 +193,10 @@ void AnnTrain::test() { auto chars_files = utils::getFiles(sub_folder); int corrects = 0, sum = 0; std::vector> error_files; - +// 这段代码是AnnTrain类的test方法,用于测试字符识别的准确率。 +// 首先根据type的值确定classNumber,然后遍历每个字符的文件夹进行测试。 +// 在测试过程中,会统计正确识别的字符数量和总测试字符数量,以及每个字符的识别准确率。 +// 最后输出总的测试结果和平均准确率。 for (auto file : chars_files) { auto img = cv::imread(file, 0); // a grayscale image if (!img.data) { @@ -196,6 +217,11 @@ void AnnTrain::test() { ++sum; ++sum_all; } +// 这段代码是一个循环,遍历chars_files中的文件,对每个文件进行处理。 +// 首先使用OpenCV的imread函数读取文件为灰度图像img,然后判断img是否为空。 +// 如果type为0,则调用identify函数对图像进行识别,否则调用identifyChinese函数。 +// 如果识别结果与char_key相同,则将corrects和corrects_all加一,否则将错误信息加入error_files。 +// 最后将sum和sum_all加一。 float rate = (float)corrects / (sum == 0 ? 1 : sum); fprintf(stdout, ">> [sum: %d, correct: %d, rate: %.2f]\n", sum, corrects, rate); rate_list.push_back(rate); @@ -217,6 +243,10 @@ void AnnTrain::test() { } fprintf(stdout, ">> [\n%s\n ]\n", error_string.c_str()); } +// 这段代码计算了识别准确率,并输出了每个字符的识别结果和错误信息。 +// 首先计算了识别准确率rate,并将其加入rate_list中。 +// 然后构建了错误信息字符串error_string,遍历error_files并将错误信息添加到字符串中。 +// 最后使用fprintf输出了总的识别结果和错误信息。 fprintf(stdout, ">> [sum_all: %d, correct_all: %d, rate: %.4f]\n", sum_all, corrects_all, (float)corrects_all / (sum_all == 0 ? 1 : sum_all)); @@ -226,6 +256,8 @@ void AnnTrain::test() { fprintf(stdout, ">> [classNumber: %d, avg_rate: %.4f]\n", classNumber, rate_mean); } +// 这段代码用于输出总的测试结果和平均准确率。 +// 首先输出总的测试结果和准确率,然后计算了每个字符的识别准确率的平均值并输出。 cv::Mat getSyntheticImage(const Mat& image) { int rand_type = rand(); Mat result = image.clone(); @@ -244,7 +276,11 @@ cv::Mat getSyntheticImage(const Mat& image) { return result; } - +// 该代码定义了一个函数getSyntheticImage,接受一个cv::Mat类型的参数image。 +//首先生成一个随机数rand_type,然后将result初始化为image的克隆。 +//如果rand_type为偶数,则生成两个随机数ran_x和ran_y,然后调用translateImg函数对result进行平移操作。 +// 如果rand_type为奇数,则生成一个随机角度angle,然后调用rotateImg函数对result进行旋转操作。 +// 最后返回result。 cv::Ptr AnnTrain::sdata(size_t number_for_count) { assert(chars_folder_); @@ -256,6 +292,9 @@ cv::Ptr AnnTrain::sdata(size_t number_for_count) { if (type == 1) classNumber = kChineseNumber; srand((unsigned)time(0)); + // 这段代码是AnnTrain类的sdata方法,用于生成训练数据。 +// 首先检查chars_folder_是否存在,然后初始化samples和labels。 +// 根据type的值确定classNumber,然后使用srand函数初始化随机数种子。 for (int i = 0; i < classNumber; ++i) { auto char_key = kChars[i + kCharsTotalNumber - classNumber]; @@ -274,7 +313,11 @@ cv::Ptr AnnTrain::sdata(size_t number_for_count) { auto img = cv::imread(file, 0); // a grayscale image matVec.push_back(img); } - +// 这段代码是一个循环,遍历每个字符文件夹中的文件,并将文件读取为灰度图像后存入matVec中。 +// 首先根据循环变量i计算出当前字符的关键字char_key,并构建对应的子文件夹路径sub_folder。 +// 然后使用utils::getFiles函数获取子文件夹中的文件列表chars_files,并统计文件数量char_size。 +// 接着初始化了一个存储灰度图像的向量matVec,并预留了number_for_count个元素的空间。 +// 遍历chars_files,使用cv::imread函数读取文件为灰度图像img,并将其存入matVec中。 for (int t = 0; t < (int)number_for_count - (int)char_size; t++) { int rand_range = char_size + t; int ran_num = rand() % rand_range; @@ -287,7 +330,10 @@ cv::Ptr AnnTrain::sdata(size_t number_for_count) { imwrite(ss.str(), simg); } } - +// 这段代码是一个循环,用于生成合成图像并保存到文件中。 +// 首先循环变量t从0到number_for_count - char_size,然后生成一个随机数ran_num。 +// 接着从matVec中获取对应索引的图像img,并调用getSyntheticImage函数生成合成图像simg。 +// 将simg添加到matVec中,并使用imwrite函数将simg保存为文件。 fprintf(stdout, ">> Characters count: %d \n", (int)matVec.size()); for (auto img : matVec) { @@ -297,7 +343,8 @@ cv::Ptr AnnTrain::sdata(size_t number_for_count) { labels.push_back(i); } } - +// 这段代码用于统计字符数量并将特征和标签添加到训练数据中。 +// 首先使用fprintf输出字符数量,然后遍历matVec中的图像,对每个图像提取特征并将特征和标签添加到训练数据中。 cv::Mat samples_; samples.convertTo(samples_, CV_32F); cv::Mat train_classes = @@ -310,7 +357,10 @@ cv::Ptr AnnTrain::sdata(size_t number_for_count) { return cv::ml::TrainData::create(samples_, cv::ml::SampleTypes::ROW_SAMPLE, train_classes); } - +// 该部分代码是AnnTrain类的tdata方法,用于生成训练数据。 +// 首先将samples转换为CV_32F类型的samples_,然后初始化train_classes为全零矩阵。 +// 接着遍历train_classes的每一行,将对应位置的值设为1。 +// 最后使用cv::ml::TrainData::create函数创建并返回训练数据对象。 cv::Ptr AnnTrain::tdata() { assert(chars_folder_); @@ -322,7 +372,9 @@ cv::Ptr AnnTrain::tdata() { int classNumber = 0; if (type == 0) classNumber = kCharsTotalNumber; if (type == 1) classNumber = kChineseNumber; - +// 这段代码是AnnTrain类的tdata方法,用于生成训练数据。 +// 首先检查chars_folder_是否存在,然后初始化samples和labels。 +// 根据type的值确定classNumber。 for (int i = 0; i < classNumber; ++i) { auto char_key = kChars[i + kCharsTotalNumber - classNumber]; char sub_folder[512] = {0}; @@ -340,7 +392,11 @@ cv::Ptr AnnTrain::tdata() { labels.push_back(i); } } - +// 这段代码是一个循环,遍历每个字符文件夹中的文件,并将文件读取为灰度图像后提取特征并添加到训练数据中。 +// 首先根据循环变量i计算出当前字符的关键字char_key,并构建对应的子文件夹路径sub_folder。 +// 然后使用utils::getFiles函数获取子文件夹中的文件列表chars_files,并遍历每个文件。 +// 对每个文件使用cv::imread函数读取为灰度图像img,然后调用charFeatures2函数提取特征fps,并将其添加到samples中。 +// 同时将当前字符的标签i添加到labels中。 cv::Mat samples_; samples.convertTo(samples_, CV_32F); cv::Mat train_classes = @@ -354,3 +410,6 @@ cv::Ptr AnnTrain::tdata() { train_classes); } } +// 该部分代码是用于生成训练数据的一部分,首先将samples转换为CV_32F类型的samples_, +// 然后初始化train_classes为全零矩阵,接着遍历train_classes的每一行,将对应位置的值设为1。 +// 最后使用cv::ml::TrainData::create函数创建并返回训练数据对象。 \ No newline at end of file diff --git a/src/src/train/create_data.cpp b/src/src/train/create_data.cpp index 5eb713a..dd8c5b8 100644 --- a/src/src/train/create_data.cpp +++ b/src/src/train/create_data.cpp @@ -7,7 +7,10 @@ namespace easypr { assert(img.type() == CV_8UC1); int w = img.cols; int h = img.rows; - +// 该部分代码定义了一个名为getBorderColor的函数,用于获取图像边界的颜色值 +// 函数首先确保输入图像是单通道灰度图像,然后获取图像的宽度和高度 +// 接下来通过遍历图像边界像素的方式计算边界颜色的平均值,并返回整数类型的平均值 +// 该文件还包含了一些其他图像处理函数,如平移、旋转、裁剪和生成合成图像的函数 float sum = 0; for (int i = 0; i < h; ++i) { sum += img.at(i, 0); @@ -21,7 +24,9 @@ namespace easypr { float avg = sum / float(w + w + h + h); return int(avg); } - +// 该部分代码计算了图像边界像素的平均值作为边界颜色值,并返回整数类型的平均值 +// 首先对图像边界像素进行遍历,累加像素值到sum中 +// 然后计算平均值avg,并返回整数类型的平均值 // shift an image Mat translateImg(Mat img, int offsetx, int offsety, int bk){ Mat dst; @@ -31,7 +36,12 @@ namespace easypr { warpAffine(img, dst, trans_mat, img.size(), 1, 0, Scalar(bk)); return dst; } - +// 该部分代码定义了一个名为translateImg的函数,用于对图像进行平移操作 +// 函数接受输入图像img以及x和y方向的偏移量offsetx和offsety,以及背景颜色bk +// 在函数内部,首先创建了一个空的目标图像dst +// 然后定义了一个2x3的变换矩阵trans_mat,用于表示平移操作 +// 最后调用warpAffine函数对输入图像进行平移操作,并将结果存储在目标图像dst中 +// 最终返回平移后的目标图像dst // rotate an image Mat rotateImg(Mat source, float angle, int bk){ Point2f src_center(source.cols / 2.0F, source.rows / 2.0F); @@ -40,7 +50,12 @@ namespace easypr { warpAffine(source, dst, rot_mat, source.size(), 1, 0, Scalar(bk)); return dst; } - +// 该部分代码定义了一个名为rotateImg的函数,用于对图像进行旋转操作 +// 函数接受输入图像source以及旋转角度angle和背景颜色bk作为参数 +// 在函数内部,首先计算了输入图像的中心点坐标src_center +// 然后利用getRotationMatrix2D函数计算了旋转矩阵rot_mat +// 最后调用warpAffine函数对输入图像进行旋转操作,并将结果存储在目标图像dst中 +// 最终返回旋转后的目标图像dst // crop the image Mat cropImg(Mat src, int x, int y, int shift, int bk){ int width = src.cols; @@ -61,7 +76,13 @@ namespace easypr { resize(dst, dst, Size(width, height)); return dst; } - +// 该部分代码定义了一个名为cropImg的函数,用于对图像进行裁剪操作 +// 函数接受输入图像src以及裁剪起始点的x和y坐标,裁剪尺寸shift和背景颜色bk作为参数 +// 在函数内部,首先获取输入图像的宽度和高度 +// 然后计算裁剪后的图像宽度和高度 +// 接着根据裁剪起始点和裁剪尺寸计算裁剪区域的矩形rect +// 将裁剪后的图像存储在目标图像dst中,并进行大小调整 +// 最终返回裁剪后的目标图像dst Mat generateSyntheticImage(const Mat& image, int use_swap) { int rd = rand(); int bkColor = getBoderColor(image); @@ -85,4 +106,10 @@ namespace easypr { return result; } } - +// 该部分代码定义了一个名为generateSyntheticImage的函数,用于生成合成图像 +// 函数接受输入图像image以及一个整数参数use_swap +// 首先通过rand函数生成一个随机数rd +// 然后调用getBorderColor函数获取输入图像的边界颜色值,并存储在bkColor中 +// 接着对输入图像进行克隆,存储在result中 +// 之后根据随机数rd的不同位进行裁剪、平移和旋转操作,并将结果存储在result中 +// 最终返回合成后的目标图像result diff --git a/src/src/util/kv.cpp b/src/src/util/kv.cpp index 84ee6c9..dea51ed 100644 --- a/src/src/util/kv.cpp +++ b/src/src/util/kv.cpp @@ -1,81 +1,81 @@ -#include "easypr/util/kv.h" -#include "easypr/util/util.h" +#include "easypr/util/kv.h" // 引入kv头文件 +#include "easypr/util/util.h" // 引入util头文件 -namespace easypr { +namespace easypr { // 定义easypr命名空间 -Kv::Kv() { } +Kv::Kv() { } // Kv类构造函数 -void Kv::load(const std::string &file) { - this->clear(); - std::ifstream reader(file); - assert(reader); +void Kv::load(const std::string &file) { // 加载文件 + this->clear(); // 清空数据 + std::ifstream reader(file); // 创建文件读取流 + assert(reader); // 断言文件读取流创建成功 - if (reader.is_open()) { - while (!reader.eof()) { - std::string line; - std::getline(reader, line); - if (line.empty()) continue; + if (reader.is_open()) { // 如果文件打开成功 + while (!reader.eof()) { // 当未到达文件末尾时 + std::string line; // 定义字符串变量line + std::getline(reader, line); // 读取一行数据到line + if (line.empty()) continue; // 如果line为空,则跳过本次循环 - 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 (i > 0 && str[i - 1] != ' ' && key.empty()) { - key = tmp; - tmp.clear(); + 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 (i > 0 && str[i - 1] != ' ' && key.empty()) { // 如果前一个字符不为空格且键为空 + key = tmp; // 将临时变量赋值给键 + tmp.clear(); // 清空临时变量 } } else { - tmp.push_back(ch); + tmp.push_back(ch); // 将当前字符添加到临时变量 } - if (i == len - 1) { - value = tmp; + if (i == len - 1) { // 如果当前字符是最后一个字符 + value = tmp; // 将临时变量赋值给值 } } - return std::make_pair(key, value); + return std::make_pair(key, value); // 返回键值对 }; - auto kv = parse(line); - this->add(kv.first, kv.second); + auto kv = parse(line); // 解析行数据 + this->add(kv.first, kv.second); // 添加键值对 } - reader.close(); + reader.close(); // 关闭文件读取流 } } -std::string Kv::get(const std::string &key) { - if (data_.find(key) == data_.end()) { - std::cerr << "[Kv] cannot find " << key << std::endl; - return ""; +std::string Kv::get(const std::string &key) { // 获取键对应的值 + if (data_.find(key) == data_.end()) { // 如果键不存在 + 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()); + v = utils::utf8_to_gbk(value.c_str());()); // 如果是Windows系统,将值转换为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; - return; +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(); // 清空数据 } } diff --git a/src/src/util/program_options.cpp b/src/src/util/program_options.cpp index a1b7c8d..4b07eb3 100644 --- a/src/src/util/program_options.cpp +++ b/src/src/util/program_options.cpp @@ -3,423 +3,429 @@ namespace program_options { // class ParseError - -ParseError::ParseError(const std::string& msg) : _msg(msg) {} - -const char* ParseError::what() const throw() { - std::string msg; - msg.append("Command line parse error: ").append(_msg).push_back('.'); - return msg.c_str(); +//定义了一个名为ParseError的类 +//ParseError类是一个用于处理命令行解析错误的异常类。 +ParseError::ParseError(const std::string& msg) : _msg(msg) {}//是ParseError类的构造函数 +//它接受一个字符串作为参数,并将这个字符串赋值给成员变量_msg。 +const char* ParseError::what() const throw() {//是一个成员函数,它返回一个描述错误的字符串 + std::string msg;//首先创建一个新的字符串 + msg.append("Command line parse error: ").append(_msg).push_back('.');//添加一个错误消息前缀,接着添加成员变量_msg,最后添加一个句点。 + return msg.c_str();//返回这个字符串的C风格字符串。 } -ParseError::~ParseError() throw() {} +ParseError::~ParseError() throw() {}// 是ParseError类的析构函数 // class Generator - -Generator::Generator() : parser_(nullptr) { - current_subroutine_ = Subroutine::get_default_name(); - add_subroutine(current_subroutine_.c_str()); +//定义了一个名为Generator的类,该类用于生成和管理命令行选项解析器和子程序。 +Generator::Generator() : parser_(nullptr) {//Generator类的构造函数 + current_subroutine_ = Subroutine::get_default_name();//初始化parser_为nullptr,设置当前子程序为默认子程序 + add_subroutine(current_subroutine_.c_str());//添加这个子程序。 } -Generator::~Generator() { +Generator::~Generator() {//Generator类的析构函数 if (parser_) { delete parser_; parser_ = nullptr; - } + }//它删除parser_和所有的子程序 for (auto it = subroutines_.begin(); it != subroutines_.end(); ++it) { if (it->second) { - delete it->second; - it->second = nullptr; + delete it->second;//它删除所有的子程序, + it->second = nullptr;//并将parser_和所有的子程序设置为nullptr。 } } } -Generator& Generator::make_usage(const char* first_line) { - get_subroutine()->set_first_line(first_line); - return *this; +Generator& Generator::make_usage(const char* first_line) {//是一个成员函数 + get_subroutine()->set_first_line(first_line);//它设置当前子程序的第一行 + return *this;//并返回this指针。 } -Parser* Generator::make_parser() { +Parser* Generator::make_parser() {//是一个成员函数 if (parser_) delete parser_; - parser_ = new Parser; - parser_->set_usage_subroutines(&subroutines_); - return parser_; + parser_ = new Parser;//它创建一个新的Parser对象 + parser_->set_usage_subroutines(&subroutines_);//设置其使用的子程序 + return parser_;//并返回这个Parser对象。 } -Generator& Generator::add_subroutine(const char* name) { - add_subroutine(name, ""); +Generator& Generator::add_subroutine(const char* name) {//是成员函数 + add_subroutine(name, "");//它们添加一个新的子程序。 return *this; } Generator& Generator::add_subroutine(const char* name, - const char* description) { - if (subroutines_.find(name) == subroutines_.end()) { + const char* description) {//成员函数 + if (subroutines_.find(name) == subroutines_.end()) {//如果子程序已经存在,它们不会添加。 // a new subroutine - current_subroutine_ = name; - Subroutine* routine = new Subroutine(name, description); - subroutines_.insert({current_subroutine_, routine}); + current_subroutine_ = name;//设立新名字 + Subroutine* routine = new Subroutine(name, description);//新建一个子程序。 + subroutines_.insert({current_subroutine_, routine});//添加一个新的子程序。 } return *this; } -std::map Generator::get_subroutine_list() { +std::map Generator::get_subroutine_list() {//是一个成员函数,它返回一个包含所有子程序名称和描述的映射。 std::map kv; - for (auto pr : subroutines_) { - Subroutine* subroutine = pr.second; - if (subroutine->get_name() != Subroutine::get_default_name()) - kv[subroutine->get_name()] = subroutine->get_description(); + for (auto pr : subroutines_) {//遍历所有的子程序 + Subroutine* subroutine = pr.second;//遍历所有的子程序 + if (subroutine->get_name() != Subroutine::get_default_name())//如果子程序的名称不是默认名称 + kv[subroutine->get_name()] = subroutine->get_description();//将子程序的名称和描述添加到映射中。 } - return std::move(kv); + return std::move(kv);//返回一个包含所有子程序名称和描述的映射 } bool Generator::add_usage_line(const char* option, const char* default_value, - const char* description) { + const char* description) {// 是一个成员函数,它添加一个使用行到当前子程序。 std::string option_str(option); auto delimiter_pos = option_str.find(kDelimiter); - +//定义新变量,将选项字符串赋给新变量 std::string option_short; std::string option_long; - +//将选项字符串分割为短选项和长选项 if (delimiter_pos != std::string::npos) { option_short.assign(std::move(option_str.substr(0, delimiter_pos))); option_long.assign(std::move(option_str.substr(delimiter_pos + 1))); - Row row; + Row row;//创建一个Row对象, row.oshort(option_short); row.olong(option_long); row.value(default_value); row.desc(description); - - get_subroutine()->add_usage_line(row); +////设置其短选项、长选项、默认值和描述 + get_subroutine()->add_usage_line(row);//将这个Row对象添加到当前子程序的使用行中。 return true; } return false; } -std::ostream& operator<<(std::ostream& out, Generator& generator) { - for (auto pr : generator.subroutines_) { +std::ostream& operator<<(std::ostream& out, Generator& generator) {// 是一个输出运算符重载函数,它打印所有子程序的名称和描述。 + for (auto pr : generator.subroutines_) {//遍历所有的子程序 Subroutine* subroutine = pr.second; if (subroutine->get_name() != Subroutine::get_default_name()) { out << subroutine->get_name() << "\t"; + }//如果子程序的名称不是默认名称,就打印子程序的名称 + out << subroutine->get_description();//打印子程序的描述 + if (!subroutine->get_usage().empty()) {//如果子程序的使用信息不为空,就打印一个换行符 + out << std::endl;//打印一个换行符 } - out << subroutine->get_description(); - if (!subroutine->get_usage().empty()) { - out << std::endl; - } - out << *subroutine; + out << *subroutine;//打印子程序的使用信息。 } return out; } // class ParseItem -ParseItem::ParseItem(const std::string& value) : value_(value) {} +ParseItem::ParseItem(const std::string& value) : value_(value) {}//ParseItem 类的构造函数 +//它接受一个 std::string 类型的参数 value。在构造函数体中,将传入的 value 直接赋值给类的成员变量 value_。 +//这个构造函数用于创建一个 ParseItem 对象,并初始化其 value_ 成员变量。 // class Parser -ParseItem* Parser::get(const std::string& key) { - if (pr_->find(key) != pr_->end()) { - return (*pr_)[key]; +ParseItem* Parser::get(const std::string& key) {//Parser 类的 get 方法,它接受一个 std::string 类型的参数 key。 + if (pr_->find(key) != pr_->end()) {//如果 key 在 pr_ 中存在 + return (*pr_)[key];//那么返回对应的 ParseItem 指针 } - return nullptr; + return nullptr;//返回 nullptr } -Parser::Parser() : subroutines_(nullptr), pr_(nullptr) {} - +Parser::Parser() : subroutines_(nullptr), pr_(nullptr) {}// +//Parser 类的构造函数,它使用初始化列表将 subroutines_ 和 pr_ 成员变量初始化为 nullptr。 Parser::~Parser() { this->cleanup(); } - -Parser::ParseResult* Parser::parse(const int argc, const char** argv) { - if (!this->init(argc, argv)) { - return nullptr; +//这是 Parser 类的析构函数,它调用 cleanup 方法来清理资源。 +Parser::ParseResult* Parser::parse(const int argc, const char** argv) {//Parser 类的 parse 方法,它接受命令行参数的数量 argc 和参数值 argv + if (!this->init(argc, argv)) {//它调用 init 方法来初始化解析过程 + return nullptr;//如果初始化失败,那么返回 nullptr } - auto ibegin = args_.begin() + 1; // ignore the first cmd name + auto ibegin = args_.begin() + 1; // 忽略第一个命令名 auto iend = args_.end(); - auto it = ibegin; + auto it = ibegin;//定义开始变量名 if (argc >= 2 && args_[1][0] != '-') { - // the second block may be a subroutine name + // 第二个块可能是一个子程序名 // e.g., ./exec pull --option if (subroutines_ && (subroutines_->find(args_[1]) != subroutines_->end())) { subroutine_name_ = args_[1]; - it++; // ignore the subroutine name + it++; // 忽略子程序名 } else { subroutine_name_ = args_[1]; } } else { - // there is no options as well as subroutine name - // e.g., ./exec + // 没有选项以及子程序名 + // 例如,./exec subroutine_name_ = Subroutine::get_default_name(); } - std::string block; - std::string previous(*ibegin); + std::string block;//声明变量 + std::string previous(*ibegin);//声明变量 - for (; it != iend; ++it) { - block.assign(*it); + for (; it != iend; ++it) {// 遍历所有的命令行参数 + block.assign(*it);// 将当前参数赋值给 block - switch (block.size()) { - case 1: - if (block == "-") { - throw ParseError("single '-' is not allowed"); + switch (block.size()) {//// 根据 block 的大小进行不同的处理 + case 1://// 如果 block 的大小为 1 + if (block == "-") {//// 如果 block 是一个单独的 "-" + throw ParseError("single '-' is not allowed");//// 抛出异常,因为单独的 "-" 是不允许的 } break; - case 2: - if (block[0] == '-') { - if (block[1] == '-') { - throw ParseError("option '--' is incomplete"); - } else if (block[1] == '=') { - throw ParseError("option '-=' is invalid"); + case 2:// // 如果 block 的大小为 2 + if (block[0] == '-') {//// 如果 block 的第一个字符是 "-" + if (block[1] == '-') {//// 如果 block 的第二个字符也是 "-" + throw ParseError("option '--' is incomplete");//// 抛出异常,因为 "--" 是不完整的选项 + + } else if (block[1] == '=') {//// 如果 block 的第二个字符是 "=" + throw ParseError("option '-=' is invalid");//// 抛出异常,因为 "-=" 是无效的选项 } else { - // single option + // 单个选项 // e.g., ./exec -s (*pr_)[block.substr(1)] = nullptr; } } break; default: // >=3 - if (block[0] == '-') { - if (block[1] == '-') { - size_t pos_equal = block.find('='); - if (pos_equal == std::string::npos) { - // a long format option + if (block[0] == '-') {//// 如果 block 的第一个字符是 "-" + if (block[1] == '-') {//// 如果 block 的第二个字符也是 "-" + size_t pos_equal = block.find('=');//// 查找 "=" 在 block 中的位置 + if (pos_equal == std::string::npos) {//// 如果没有找到 "=" + // 长格式选项 // e.g., ./exec --option - (*pr_)[block.substr(2)] = nullptr; + (*pr_)[block.substr(2)] = nullptr;//// 将选项添加到 pr_ 中,值为 nullptr } else { - if (pos_equal > 3) { + if (pos_equal > 3) {// 如果 "=" 的位置大于 3 // e.g, ./exec --op[..=]value - std::string key(block.substr(2, pos_equal - 2)); - if (block.size() > 5) + std::string key(block.substr(2, pos_equal - 2));// 获取选项名 + if (block.size() > 5)//// 如果 block 的大小大于 5 // e.g, ./exec --op=v - (*pr_)[key] = new ParseItem(block.substr(pos_equal + 1)); + (*pr_)[key] = new ParseItem(block.substr(pos_equal + 1));// 将选项和值添加到 pr_ 中 else - (*pr_)[key] = nullptr; + (*pr_)[key] = nullptr;// 将选项添加到 pr_ 中,值为 nullptr } else { - // a long format option but = is illegal + // 长格式选项但 = 是非法的 // e.g., ./exec --o=[...] - (*pr_)[block.substr(2)] = nullptr; + (*pr_)[block.substr(2)] = nullptr;//// 将选项添加到 pr_ 中,值为 nullptr } } - } else if (block[2] == '=') { - // a single option with = + } else if (block[2] == '=') {// // 如果 block 的第三个字符是 "=" + // 单个选项带有 = // e.g., ./exec -o=[...] std::string key; - key.push_back(block[1]); - if (block.size() > 3) - (*pr_)[key] = new ParseItem(block.substr(3)); + key.push_back(block[1]);// 获取选项名 + if (block.size() > 3)// 如果 block 的大小大于 3 + (*pr_)[key] = new ParseItem(block.substr(3));//// 将选项和值添加到 pr_ 中 else - (*pr_)[key] = nullptr; + (*pr_)[key] = nullptr;// 将选项添加到 pr_ 中,值为 nullptr } else { - // a combination options + // 组合选项 // e.g., ./exec -ab[...] - auto tbegin = block.begin() + 1; // ignore the first '-' + auto tbegin = block.begin() + 1; // 忽略第一个 '-' auto tend = block.end(); auto t = tbegin; - for (; t != tend; ++t) { + for (; t != tend; ++t) { // 遍历 block 中的每个字符 std::string key; - key.push_back(*t); - (*pr_)[key] = nullptr; + key.push_back(*t);// // 获取选项名 + (*pr_)[key] = nullptr; // 将选项添加到 pr_ 中,值为 nullptr } } } break; } // switch - if (block[0] != '-' && previous != block // not the first option + if (block[0] != '-' && previous != block // 如果 block 不是选项(不以 "-" 开头)并且不是第一个选项 ) { - if (previous[0] != '-') { - // previous is not an option, error occur + if (previous[0] != '-') {//// 如果 previous 不是选项 + // previous 不是一个选项,发生错误 // e.g., ./exec abc def - throw ParseError("'" + block + "' is not allowed here"); + throw ParseError("'" + block + "' is not allowed here");//抛出异常,因为在这里不允许非选项 } std::string key; - if (previous[0] == '-' && previous[1] == '-') { + if (previous[0] == '-' && previous[1] == '-') {//// 如果 previous 是一个长格式选项 // previous is a long format option. // e.g., ./exec --option value - key = previous.substr(2); + key = previous.substr(2);//// 获取选项名 } else { - // it's the value of previous option. + // 它是前一个选项的值。 // e.g., ./exec -o [...] // e.g., ./exec -opq [...] - key.push_back(*(previous.end() - 1)); + key.push_back(*(previous.end() - 1));// // 获取选项名 } - if (pr_->find(key) != pr_->end()) { - (*pr_)[key] = new ParseItem(block); + if (pr_->find(key) != pr_->end()) {//// 如果选项在 pr_ 中存在 + (*pr_)[key] = new ParseItem(block); // 将选项和值添加到 pr_ 中 } } - previous = block; + previous = block;//// 更新 previous 为当前的 block } // for if (subroutines_) { - this->set_addition(); + this->set_addition();// 如果存在子程序,调用 set_addition 方法处理额外的选项 } - return pr_; + return pr_;//返回解析结果 pr_ } -Parser::ParseResult* Parser::parse(const char* command_line) { - int i = 0; - std::string block; - std::vector blocks; - char c; - while ((c = command_line[i++]) != '\0') { - if (c != ' ') { - block.push_back(c); +Parser::ParseResult* Parser::parse(const char* command_line) {//Parser 类的 parse 方法 + int i = 0;//初始化计数器 + std::string block;//用于存储单个命令行参数 + std::vector blocks;//声明用于存储所有命令行参数 + char c;//声明用于存储当前字符 + while ((c = command_line[i++]) != '\0') {// 遍历命令行字符串 + if (c != ' ') {// 如果当前字符不是空格 + block.push_back(c);//// 将当前字符添加到 block } else { - if (!block.empty()) { - blocks.push_back(block); + if (!block.empty()) {// 如果 block 不为空 + blocks.push_back(block);// 将 block 添加到 blocks } - block.clear(); + block.clear();//清空 block } } - if (!block.empty()) { - blocks.push_back(block); + if (!block.empty()) {// 如果最后一个 block 不为空 + blocks.push_back(block);// 将 block 添加到 blocks } size_t size = blocks.size(); // argc - char** argv = new char*[size]; + char** argv = new char*[size];// 创建一个新的 char* 数组 i = 0; - std::for_each(blocks.begin(), blocks.end(), [argv, &i](const std::string& b) { - argv[i++] = const_cast(b.c_str()); + std::for_each(blocks.begin(), blocks.end(), [argv, &i](const std::string& b) {// 遍历 blocks + argv[i++] = const_cast(b.c_str());// 将每个 block 转换为 char* 并存储在 argv 中 }); auto pr = - this->parse(static_cast(size), const_cast(argv)); + this->parse(static_cast(size), const_cast(argv)); // 调用 parse 方法解析命令行参数 - delete[] argv; + delete[] argv;// 删除 argv argv = nullptr; - return pr; + return pr;// 返回解析结果 } -bool Parser::has(const char* key) { - std::string skey(key); +bool Parser::has(const char* key) {//Parser 类的 has 方法,它接受一个 char 指针 key,并检查 key 是否在 pr_ 中存在。 + std::string skey(key);// 将 key 转换为 std::string - if (pr_ && !pr_->empty() && !skey.empty()) { + if (pr_ && !pr_->empty() && !skey.empty()) {//判断是否存在 if (skey[0] == '-') { - // check combination options, e.g., Parser::has("-xyz") - for (size_t i = 1; i < skey.size(); ++i) { + // 如果 skey 是一个组合选项,例如 "-xyz" + for (size_t i = 1; i < skey.size(); ++i) {// 遍历 skey 的每个字符 std::string tkey; - tkey.push_back(skey[i]); - if (pr_->find(tkey) == pr_->end()) { + tkey.push_back(skey[i]);// 获取选项名 + if (pr_->find(tkey) == pr_->end()) { // 如果选项名在 pr_ 中不存在 return false; } } return true; } else { - // check single option, e.g., Parser::has("x") - return pr_->find(skey) != pr_->end(); + // 如果 skey 是一个单个选项,例如 "x" + return pr_->find(skey) != pr_->end();// 检查选项是否在 pr_ 中存在 } } - return false; + return false;// 如果 pr_ 为空或 skey 为空,返回 false } +//parser 类的 has_or 方法,它接受一个初始化列表 options,并检查 options 中的任何一个 key 是否在 pr_ 中存在。 bool Parser::has_or(std::initializer_list options) { - if (options.size() == 0) { + if (options.size() == 0) {// 如果 options 为空 return false; + }4 + for (auto key : options) {// 遍历 options 中的每个选项 + if (this->has(key)) return true;// 如果选项在 pr_ 中存在,返回 true } - for (auto key : options) { - if (this->has(key)) return true; - } - return false; + return false;// 如果 options 中的所有选项都不存在,返回 false } -bool Parser::has_and(std::initializer_list options) { - if (options.size() == 0) { +bool Parser::has_and(std::initializer_list options) { // Parser 类的 has_and 方法,接受一个初始化列表 options + if (options.size() == 0) {// 如果 options 为空 return false; } - for (auto key : options) { - if (!this->has(key)) return false; + for (auto key : options) {// 遍历 options 中的每个选项 + if (!this->has(key)) return false;// 如果选项在 pr_ 中不存在,返回 false } - return true; + return true;// 如果 options 中的所有选项都存在,返回 true } bool Parser::init(const int argc, const char** argv) { - argc_ = argc; + argc_ = argc;// 保存参数数量 // argv_ = argv; // don't save it, point to a local var in parse(const char* command_line). // use member var args_ instead. - if (argc > 0) { - this->cleanup(); + if (argc > 0) {// 如果参数数量大于 0 + this->cleanup(); // 清理之前的解析结果 + - args_.reserve(static_cast(argc_)); + args_.reserve(static_cast(argc_));// 为 args_ 预留空间 - for (int i = 0; i < argc_; ++i) { - args_.push_back(argv[i]); + for (int i = 0; i < argc_; ++i) {// 遍历所有的命令行参数 + args_.push_back(argv[i]);// 将参数添加到 args_ } - pr_ = new Parser::ParseResult; + pr_ = new Parser::ParseResult;// 创建新的解析结果 return true; } - return false; + return false;// 如果参数数量为 0,返回 false } -void Parser::cleanup() { - args_.clear(); - if (pr_) { +void Parser::cleanup() {// Parser 类的 cleanup 方法,用于清理解析结果 + args_.clear();// 清空 args_ + if (pr_) {// 如果 pr_ 不为空 auto ibegin = pr_->begin(); auto iend = pr_->end(); auto it = ibegin; - for (; it != iend; ++it) { + for (; it != iend; ++it) {// 遍历 pr_ 中的每个元素 ParseItem* item = it->second; - if (item) delete item; + if (item) delete item;// 删除元素 } - delete pr_; + delete pr_;// 删除 pr_ pr_ = nullptr; } } -void Parser::set_addition() { - if (subroutines_->find(subroutine_name_) != subroutines_->end()) { - for (const Row& row : *(subroutines_->at(subroutine_name_))) { +void Parser::set_addition() {// Parser 类的 set_addition 方法,用于处理额外的选项 + if (subroutines_->find(subroutine_name_) != subroutines_->end()) {// 如果子程序名在 subroutines_ 中存在 + for (const Row& row : *(subroutines_->at(subroutine_name_))) {// 遍历子程序中的每一行 // assume both -o and --option are allowed, // but only provide -o, // then set the another --option. // vice versa. - const std::string& def = row.value(); - const std::string& ops = row.oshort(); - const std::string& opl = row.olong(); - ParseResult& pr = *pr_; + const std::string& def = row.value();// 获取默认值 + const std::string& ops = row.oshort();// 获取短选项 + const std::string& opl = row.olong();// 获取长选项 + ParseResult& pr = *pr_; // 获取解析结果 - bool has_short = this->has(ops.c_str()); - bool has_long = this->has(opl.c_str()); + bool has_short = this->has(ops.c_str());// 检查短选项是否存在 + bool has_long = this->has(opl.c_str());// 检查长选项是否存在 // assume -o [ --option ] arg = 1 // but not provide option value, // then set to default 1. // otherwise, both set to user defined value - if (!ops.empty()) { - if (has_short) { - if (pr[ops] != nullptr && !opl.empty()) { - pr[opl] = new ParseItem(std::move(pr[ops]->val())); - } else if (pr[ops] == nullptr && !def.empty()) { - pr[ops] = new ParseItem(std::move(def)); - if (!opl.empty()) pr[opl] = new ParseItem(std::move(def)); + if (!ops.empty()) {// 如果短选项不为空 + if (has_short) {// 如果短选项存在 + if (pr[ops] != nullptr && !opl.empty()) {// 如果短选项有值且长选项不为空 + pr[opl] = new ParseItem(std::move(pr[ops]->val()));// 将短选项的值赋给长选项 + } else if (pr[ops] == nullptr && !def.empty()) {// 如果短选项没有值且默认值不为空 + pr[ops] = new ParseItem(std::move(def));// 将默认值赋给短选项 + if (!opl.empty()) pr[opl] = new ParseItem(std::move(def));// 如果长选项不为空,也将默认值赋给长选项 } else { - pr[opl] = nullptr; + pr[opl] = nullptr;// 将长选项的值设为 nullptr } } } - if (!opl.empty()) { - if (has_long) { - if (pr[opl] != nullptr && !ops.empty()) { - pr[ops] = new ParseItem(std::move(pr[opl]->val())); - } else if (pr[opl] == nullptr && !def.empty()) { - if (!ops.empty()) pr[ops] = new ParseItem(std::move(def)); - pr[opl] = new ParseItem(std::move(def)); + if (!opl.empty()) {// 如果长选项不为空 + if (has_long) { // 如果长选项存在 + if (pr[opl] != nullptr && !ops.empty()) { // 如果长选项有值且短选项不为空 + pr[ops] = new ParseItem(std::move(pr[opl]->val()));// 将长选项的值赋给短选项 + } else if (pr[opl] == nullptr && !def.empty()) {// 如果长选项没有值且默认值不为空 + if (!ops.empty()) pr[ops] = new ParseItem(std::move(def));// 如果短选项不为空,将默认值赋给短选项 + pr[opl] = new ParseItem(std::move(def));// 将默认值赋给长选项 } else { - pr[ops] = nullptr; + pr[ops] = nullptr;// 将短选项的值设为 nullptr } } } - if (!has_long && !has_short && !def.empty()) { - if (!opl.empty()) pr[opl] = new ParseItem(std::move(def)); - if (!ops.empty()) pr[ops] = new ParseItem(std::move(def)); + if (!has_long && !has_short && !def.empty()) {// 如果长选项和短选项都不存在且默认值不为 + if (!opl.empty()) pr[opl] = new ParseItem(std::move(def));// 如果长选项不为空,将默认值赋给长选项 + if (!ops.empty()) pr[ops] = new ParseItem(std::move(def));// 如果短选项不为空,将默认值赋给短选项 } } // for } // if @@ -427,114 +433,115 @@ void Parser::set_addition() { // class Row -Row::Row() : require_value(true) {} +Row::Row() : require_value(true) {} // Row 类的构造函数,初始化 require_value 为 true // class Subroutine -Subroutine::Subroutine() : first_line_("") {} +Subroutine::Subroutine() : first_line_("") {}// Subroutine 类的默认构造函数,初始化 first_line_ 为空字符串 Subroutine::Subroutine(const char* name, const char* description) - : first_line_(""), description_(description), name_(name) { - usages_.reserve(5); + : first_line_(""), description_(description), name_(name) { // Subroutine 类的构造函数,接受子程序名和描述作为参数 + usages_.reserve(5);// 为 usages_ 预留空间 } -void Subroutine::print_with_row(std::ostream& out) { +void Subroutine::print_with_row(std::ostream& out) {// Subroutine 类的 print_with_row 方法,接受一个输出流作为参数 // print the subroutine name and its description - if (strcmp(get_first_line(), "") != 0) { + // 打印子程序名和描述 + if (strcmp(get_first_line(), "") != 0) {// 如果 first_line_ 不为空 // print the first line out << get_first_line(); - if (!usages_.empty()) { - out << std::endl; + if (!usages_.empty()) {// 如果 usages_ 不为空 + out << std::endl;// 打印换行符 } } - auto begin = usages_.begin(); - auto end = usages_.end(); + auto begin = usages_.begin(); // 获取 usages_ 的开始迭代器 + auto end = usages_.end();// 获取 usages_ 的结束迭代器 - std::vector row_list; - row_list.reserve(usages_.size()); + std::vector row_list;// 创建一个字符串向量用于存储行 + row_list.reserve(usages_.size());// 为 row_list 预留空间 // build usage rows without description field, // find the max-len row at the same time. size_t max_len = 0; - std::for_each(begin, end, [&max_len, &row_list](const Row& row) { - std::stringstream ss; - ss << " "; - if (!row.oshort().empty()) { - ss << "-" << row.oshort() << " "; + std::for_each(begin, end, [&max_len, &row_list](const Row& row) {// 遍历 usages_ + std::stringstream ss;// 创建一个字符串流 + ss << " ";// 向字符串流中添加两个空格 + if (!row.oshort().empty()) {// 如果短选项不为空 + ss << "-" << row.oshort() << " "; // 添加短选项 } - if (!row.olong().empty()) { + if (!row.olong().empty()) {// 如果长选项不为空 if (!row.oshort().empty()) - ss << "[ --" << row.olong() << " ] "; + ss << "[ --" << row.olong() << " ] ";// 添加长选项 else - ss << "--" << row.olong() << " "; + ss << "--" << row.olong() << " "; // 添加长选项 } - if (row.required()) { - ss << "arg "; - if (!row.value().empty()) { - ss << "= " << row.value() << " "; + if (row.required()) {// 如果选项是必需的 + ss << "arg "; // 添加 "arg " + if (!row.value().empty()) {// 如果选项值不为空 + ss << "= " << row.value() << " ";// 添加选项值 } } - max_len = std::max(max_len, ss.str().size()); - row_list.push_back(std::move(ss.str())); + max_len = std::max(max_len, ss.str().size());// 更新最大长度 + row_list.push_back(std::move(ss.str()));// 将字符串流的内容添加到 row_list }); // show all rows and align description field - size_t row_count = usages_.size(); - for (size_t i = 0; i < row_count; ++i) { - std::string str_row(std::move(row_list[i])); + size_t row_count = usages_.size();// 获取 usages_ 的大小 + for (size_t i = 0; i < row_count; ++i) {// 遍历 usages_ + std::string str_row(std::move(row_list[i]));// 获取当前行 // print row without description - out << str_row; + out << str_row;// 打印当前行 // print spaces - size_t spaces = 0; - size_t len = str_row.size(); - if (max_len > len) spaces = max_len - len; + size_t spaces = 0;// 打印空格 + size_t len = str_row.size();// 获取当前行的长度 + if (max_len > len) spaces = max_len - len;// 计算需要打印的空格数量 - while (spaces--) { + while (spaces--) {// 打印空格 out << " "; } // print description - out << usages_.at(i).desc() << std::endl; + out << usages_.at(i).desc() << std::endl;// 打印描述 } } -void Subroutine::print_with_template(std::ostream& out) { - for (auto usage : usages_) { +void Subroutine::print_with_template(std::ostream& out) {// Subroutine 类的 print_with_template 方法,接受一个输出流作为参数 + for (auto usage : usages_) {// 遍历 usages_ size_t i = 0; - for (auto t = template_str_.begin(); t != template_str_.end(); ++t) { - if (*t == '%') { - switch (*(order_.begin() + i)) { + for (auto t = template_str_.begin(); t != template_str_.end(); ++t) {// 遍历模板字符串 + if (*t == '%') {// 如果当前字符是 '%' + switch (*(order_.begin() + i)) { // 根据 order_ 中的值决定打印哪个字段 case Row::kShort: - out << usage.oshort(); + out << usage.oshort();// 打印短选项 break; case Row::kLong: - out << usage.olong(); + out << usage.olong();// 打印长选项 break; case Row::kDefault: - out << usage.value(); + out << usage.value();// 打印默认值 break; case Row::kDescription: - out << usage.desc(); + out << usage.desc();// 打印描述 break; default: break; } ++i; } else { - out << *t; + out << *t;// 如果当前字符不是 '%',直接打印 } // if % } // for template_str_ - out << std::endl; + out << std::endl;// 打印换行符 } // for usages_ } -std::ostream& operator<<(std::ostream& out, Subroutine& subroutine) { - if (subroutine.template_str_.empty()) { - subroutine.print_with_row(out); +std::ostream& operator<<(std::ostream& out, Subroutine& subroutine) {// 重载 << 运算符,接受一个输出流和一个 Subroutine 对象作为参数 + if (subroutine.template_str_.empty()) {// 如果模板字符串为空 + subroutine.print_with_row(out);// 使用 print_with_row 方法打印 } else { - subroutine.print_with_template(out); + subroutine.print_with_template(out);// 使用 print_with_template 方法打印 } - return out; + return out;// 返回输出流 } } \ No newline at end of file diff --git a/src/src/util/util.cpp b/src/src/util/util.cpp index b293c2a..df37418 100644 --- a/src/src/util/util.cpp +++ b/src/src/util/util.cpp @@ -1,127 +1,127 @@ #include "easypr/util/util.h" #include -#ifdef OS_WINDOWS -#include -#include -#include -#define PATH_DELIMITER '\\' +// #ifdef OS_WINDOWS +#include // 包含windows.h头文件,用于Windows平台的系统调用 +#include // 包含direct.h头文件,用于Windows平台的目录操作 +#include // 包含io.h头文件,用于Windows平台的IO操作 +#define PATH_DELIMITER '\\' // 定义路径分隔符为'\\' #ifdef min -#undef min +#undef min // 如果已经定义了min,取消其定义 #endif #ifdef max -#undef max +#undef max // 如果已经定义了max,取消其定义 #endif #elif defined(OS_LINUX) || defined(OS_UNIX) -#include -#include -#include -#include +#include // 包含cstring头文件,用于字符串操作 +#include // 包含dirent.h头文件,用于目录操作 +#include // 包含sys/stat.h头文件,用于文件状态检查 +#include // 包含unistd.h头文件,用于Unix标准的系统调用 -#define PATH_DELIMITER '/' +#define PATH_DELIMITER '/' // 定义路径分隔符为'/' #endif #ifdef OS_UNIX -#include +#include // 包含sys/timeb.h头文件,用于时间操作 #endif -#include -#include +#include // 包含list头文件,用于list数据结构 +#include // 包含opencv的highgui模块,用于图像IO操作 -namespace easypr { +namespace easypr { // 定义easypr命名空间 -long Utils::getTimestamp() { +long Utils::getTimestamp() { // 定义获取时间戳的函数 #ifdef OS_WINDOWS - return static_cast(cv::getTickCount()); + return static_cast(cv::getTickCount()); // Windows平台下,使用opencv的getTickCount函数获取时间戳 #endif #ifdef OS_LINUX - struct timespec ts; + struct timespec ts; // 定义timespec结构体,用于获取时间 - clock_gettime(CLOCK_MONOTONIC, &ts); + clock_gettime(CLOCK_MONOTONIC, &ts); // 获取当前时间 - return (ts.tv_sec * 1e3 + ts.tv_nsec / 1e6); + return (ts.tv_sec * 1e3 + ts.tv_nsec / 1e6); // 返回毫秒级的时间戳 #endif #ifdef OS_UNIX // there is no function provided by osx to get system tick count. // but considering the purpose by using this function, // we can simply return a millisecond since 1970/1/1 to calc the time elapse. - struct timeb tb; - ftime(&tb); - return long(tb.time * 1e3 + tb.millitm); + struct timeb tb; // 定义timeb结构体,用于获取时间 + ftime(&tb); // 获取当前时间 + return long(tb.time * 1e3 + tb.millitm); // 返回毫秒级的时间戳 #endif } std::string Utils::getFileName(const std::string &path, - const bool postfix /* = false */) { - if (!path.empty()) { - size_t last_slash = utils::get_last_slash(path); - size_t last_dot = path.find_last_of('.'); + const bool postfix /* = false */) { // 定义获取文件名的函数 + if (!path.empty()) { // 如果路径不为空 + size_t last_slash = utils::get_last_slash(path); // 获取路径中最后一个斜杠的位置 + size_t last_dot = path.find_last_of('.'); // 获取路径中最后一个点的位置 if (last_dot < last_slash || last_dot == std::string::npos) { // not found the right dot of the postfix, // return the file name directly - return path.substr(last_slash + 1); + return path.substr(last_slash + 1); // 如果没有找到正确的后缀点,直接返回文件名 } else { // the path has a postfix if (postfix) { // return the file name including postfix - return path.substr(last_slash + 1); + return path.substr(last_slash + 1); // 如果路径有后缀,并且需要返回后缀,返回包含后缀的文件名 } // without postfix - return path.substr(last_slash + 1, last_dot - last_slash - 1); + return path.substr(last_slash + 1, last_dot - last_slash - 1); // 如果路径有后缀,但不需要返回后缀,返回不包含后缀的文件名 } } - return ""; + return ""; // 如果路径为空,返回空字符串 } std::vector Utils::splitString(const std::string &str, - const char delimiter) { - std::vector splited; - std::string s(str); - size_t pos; + const char delimiter) { // 定义字符串分割函数 + std::vector splited; // 定义存储分割结果的vector + std::string s(str); // 复制输入的字符串 + size_t pos; // 定义分割位置 - while ((pos = s.find(delimiter)) != std::string::npos) { - std::string sec = s.substr(0, pos); + while ((pos = s.find(delimiter)) != std::string::npos) { // 当找到分隔符时 + std::string sec = s.substr(0, pos); // 获取分隔符前的子串 - if (!sec.empty()) { - splited.push_back(s.substr(0, pos)); + if (!sec.empty()) { // 如果子串不为空 + splited.push_back(s.substr(0, pos)); // 将子串添加到分割结果中 } - s = s.substr(pos + 1); + s = s.substr(pos + 1); // 更新待分割的字符串 } - splited.push_back(s); + splited.push_back(s); // 将最后一个子串添加到分割结果中 - return splited; + return splited; // 返回分割结果 } std::vector Utils::getFiles(const std::string &folder, - const bool all /* = true */) { - std::vector files; - std::list subfolders; - subfolders.push_back(folder); + const bool all /* = true */) { // 定义获取文件列表的函数 + std::vector files; // 定义存储文件列表的vector + std::list subfolders; // 定义存储子文件夹的list + subfolders.push_back(folder); // 将输入的文件夹添加到子文件夹列表中 #ifdef OS_WINDOWS - while (!subfolders.empty()) { - std::string current_folder(subfolders.back()); + while (!subfolders.empty()) { // 当子文件夹列表不为空时 + std::string current_folder(subfolders.back()); // 获取当前处理的文件夹 if (*(current_folder.end() - 1) != '/') { - current_folder.append("/*"); + current_folder.append("/*"); // 如果当前文件夹的路径不以'/'结尾,添加'/*' } else { - current_folder.append("*"); + current_folder.append("*"); // 如果当前文件夹的路径以'/'结尾,添加'*' } - subfolders.pop_back(); + subfolders.pop_back(); // 从子文件夹列表中移除当前处理的文件夹 - struct _finddata_t file_info; - auto file_handler = _findfirst(current_folder.c_str(), &file_info); + struct _finddata_t file_info; // 定义文件信息结构体 + auto file_handler = _findfirst(current_folder.c_str(), &file_info); // 打开当前文件夹 - while (file_handler != -1) { + while (file_handler != -1) { // 当文件夹打开成功时 if (all && (!strcmp(file_info.name, ".") || !strcmp(file_info.name, ".."))) { if (_findnext(file_handler, &file_info) != 0) break; @@ -136,7 +136,7 @@ std::vector Utils::getFiles(const std::string &folder, folder.pop_back(); folder.append(file_info.name); - subfolders.push_back(folder.c_str()); + subfolders.push_back(folder.c_str()); // 如果是子文件夹,并且需要搜索子文件夹,将子文件夹添加到子文件夹列表中 } } else { // it's a file @@ -145,24 +145,24 @@ std::vector Utils::getFiles(const std::string &folder, file_path.assign(current_folder.c_str()).pop_back(); file_path.append(file_info.name); - files.push_back(file_path); + files.push_back(file_path); // 如果是文件,将文件路径添加到文件列表中 } if (_findnext(file_handler, &file_info) != 0) break; } // while - _findclose(file_handler); + _findclose(file_handler); // 关闭文件夹 } #elif defined(OS_LINUX) || defined(OS_UNIX) - while (!subfolders.empty()) { - std::string current_folder(subfolders.back()); + while (!subfolders.empty()) { // 当子文件夹列表不为空时 + std::string current_folder(subfolders.back()); // 获取当前处理的文件夹 if (*(current_folder.end() - 1) != '/') { - current_folder.push_back('/'); + current_folder.push_back('/'); // 如果当前文件夹的路径不以'/'结尾,添加'/' } - DIR* pdir = opendir(current_folder.c_str()); + DIR* pdir = opendir(current_folder.c_str()); // 打开当前文件夹 - subfolders.pop_back(); + subfolders.pop_back(); // 从子文件夹列表中移除当前处理的文件夹 if (!pdir) { continue; @@ -170,9 +170,9 @@ std::vector Utils::getFiles(const std::string &folder, dirent* dir = NULL; - while ((dir = readdir(pdir)) != NULL) { + while ((dir = readdir(pdir)) != NULL) { // 当读取到文件或文件夹时 // iterates the current folder, search file & sub folder - struct stat st; + struct stat st; // 定义文件状态结构体 if (all && (!strcmp(dir->d_name, ".") || !strcmp(dir->d_name, ".."))) { // must ignore . & .. @@ -201,93 +201,93 @@ std::vector Utils::getFiles(const std::string &folder, std::string subfolder(current_folder); subfolder.append(dir->d_name); - subfolders.push_back(subfolder.c_str()); + subfolders.push_back(subfolder.c_str()); // 如果是子文件夹,并且需要搜索子文件夹,将子文件夹添加到子文件夹列表中 } } else { // it's a file - files.push_back(file_path); + files.push_back(file_path); // 如果是文件,将文件路径添加到文件列表中 } } // while - closedir(pdir); + closedir(pdir); // 关闭文件夹 } #endif - return files; + return files; // 返回文件列表 } -bool Utils::mkdir(const std::string folder) { - std::string folder_builder; - std::string sub; +bool Utils::mkdir(const std::string folder) { // 定义创建文件夹的函数 + std::string folder_builder; // 定义文件夹路径构造器 + std::string sub; // 定义子路径 sub.reserve(folder.size()); - for (auto it = folder.begin(); it != folder.end(); ++it) { + for (auto it = folder.begin(); it != folder.end(); ++it) { // 遍历输入的文件夹路径 const char c = *it; sub.push_back(c); - if (c == PATH_DELIMITER || it == folder.end() - 1) { - folder_builder.append(sub); + if (c == PATH_DELIMITER || it == folder.end() - 1) { // 当遇到路径分隔符或路径结束时 + folder_builder.append(sub); // 将子路径添加到文件夹路径构造器中 #ifdef OS_WINDOWS - if (0 != ::_access(folder_builder.c_str(), 0)) { + if (0 != ::_access(folder_builder.c_str(), 0)) { // 如果文件夹不存在 #else - if (0 != ::access(folder_builder.c_str(), 0)) { + if (0 != ::access(folder_builder.c_str(), 0)) { // 如果文件夹不存在 #endif // this folder not exist #ifdef OS_WINDOWS - if (0 != ::_mkdir(folder_builder.c_str())) { + if (0 != ::_mkdir(folder_builder.c_str())) { // 如果创建文件夹失败 #else - if (0 != ::mkdir(folder_builder.c_str(), S_IRWXU)) { + if (0 != ::mkdir(folder_builder.c_str(), S_IRWXU)) { // 如果创建文件夹失败 #endif // create failed - return false; + return false; // 返回失败 } } - sub.clear(); + sub.clear(); // 清空子路径 } } - return true; + return true; // 返回成功 } -bool Utils::imwrite(const std::string &file, const cv::Mat &image) { - auto folder = file.substr(0, utils::get_last_slash(file)); - Utils::mkdir(folder); - return cv::imwrite(file, image); +bool Utils::imwrite(const std::string &file, const cv::Mat &image) { // 定义图像写入函数 + auto folder = file.substr(0, utils::get_last_slash(file)); // 获取文件所在的文件夹 + Utils::mkdir(folder); // 创建文件所在的文件夹 + return cv::imwrite(file, image); // 写入图像 } #ifdef OS_WINDOWS -std::string Utils::utf8_to_gbk(const char* utf8) { - int len = MultiByteToWideChar(CP_UTF8, 0, utf8, -1, NULL, 0); - wchar_t* wszGBK = new wchar_t[len + 1]; - memset(wszGBK, 0, len * 2 + 2); - MultiByteToWideChar(CP_UTF8, 0, utf8, -1, wszGBK, len); - len = WideCharToMultiByte(CP_ACP, 0, wszGBK, -1, NULL, 0, NULL, NULL); - char* szGBK = new char[len + 1]; - memset(szGBK, 0, len + 1); - WideCharToMultiByte(CP_ACP, 0, wszGBK, -1, szGBK, len, NULL, NULL); - std::string strTemp(szGBK); +std::string Utils::utf8_to_gbk(const char* utf8) { // 定义UTF-8到GBK的转换函数 + int len = MultiByteToWideChar(CP_UTF8, 0, utf8, -1, NULL, 0); // 获取转换后的长度 + wchar_t* wszGBK = new wchar_t[len + 1]; // 定义存储转换结果的宽字符数组 + memset(wszGBK, 0, len * 2 + 2); // 初始化宽字符数组 + MultiByteToWideChar(CP_UTF8, 0, utf8, -1, wszGBK, len); // 将UTF-8字符串转换为宽字符字符串 + len = WideCharToMultiByte(CP_ACP, 0, wszGBK, -1, NULL, 0, NULL, NULL); // 获取转换后的长度 + char* szGBK = new char[len + 1]; // 定义存储转换结果的字符数组 + memset(szGBK, 0, len + 1); // 初始化字符数组 + WideCharToMultiByte(CP_ACP, 0, wszGBK, -1, szGBK, len, NULL, NULL); // 将宽字符字符串转换为GBK字符串 + std::string strTemp(szGBK); // 将GBK字符串转换为std::string if (wszGBK) - delete[] wszGBK; + delete[] wszGBK; // 删除宽字符数组 if (szGBK) - delete[] szGBK; - return strTemp; + delete[] szGBK; // 删除字符数组 + return strTemp; // 返回转换结果 } #endif -std::size_t Utils::get_last_slash(const std::string &path) { +std::size_t Utils::get_last_slash(const std::string &path) { // 定义获取路径中最后一个斜杠的位置的函数 #ifdef OS_WINDOWS - size_t last_slash_1 = path.find_last_of("\\"); - size_t last_slash_2 = path.find_last_of("/"); + size_t last_slash_1 = path.find_last_of("\\"); // 获取路径中最后一个'\\'的位置 + size_t last_slash_2 = path.find_last_of("/"); // 获取路径中最后一个'/'的位置 size_t last_slash; if (last_slash_1 != std::string::npos && last_slash_2 != std::string::npos) { // C:/path\\to/file.postfix - last_slash = std::max(last_slash_1, last_slash_2); + last_slash = std::max(last_slash_1, last_slash_2); // 如果路径中既有'\\'又有'/',取最后出现的一个 } else { // C:\\path\\to\\file.postfix // C:/path/to/file.postfix last_slash = - (last_slash_1 == std::string::npos) ? last_slash_2 : last_slash_1; + (last_slash_1 == std::string::npos) ? last_slash_2 : last_slash_1; // 如果路径中只有'\\'或只有'/',取出现的那一个 } #else - size_t last_slash = path.find_last_of('/'); + size_t last_slash = path.find_last_of('/'); // 获取路径中最后一个'/'的位置 #endif - return last_slash; + return last_slash; // 返回最后一个斜杠的位置 } } // namespace easypr \ No newline at end of file