You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Galaxy/annCh_train.cpp

348 lines
17 KiB

#include <numeric>//包含了C++标准库中的<numeric>头文件,提供了数值计算的相关函数和模板。
#include <ctime>//包含了C++标准库中的<ctime>头文件,提供了关于时间和日期的相关函数和类型。
#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(); // 创建一个MLPMultilayer Perceptron多层感知器对象用于字符识别
type = 1; // 初始化type为1可能表示某种类型或模式
kv_ = std::shared_ptr<Kv>(new Kv); // 创建一个Kv对象并使用std::shared_ptr管理内存实现共享所有权模型
kv_->load("resources/text/province_mapping"); // 加载kv_对象可能从文件"resources/text/province_mapping"中加载数据
extractFeature = getGrayPlusProject; // 初始化extractFeature函数指针指向getGrayPlusProject函数用于特征提取
}
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可能是为了增加一些额外的输入节点
}
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<int>(0) = input_number; // 设置输入层节点数量
layers.at<int>(1) = hidden_number; // 设置隐藏层节点数量
layers.at<int>(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<int>(0) = input_number; // 设置输入层节点数量
layers.at<int>(1) = first_hidden_neurons; // 设置第一层隐藏层节点数量
layers.at<int>(2) = second_hidden_neurons; // 设置第二层隐藏层节点数量
layers.at<int>(3) = output_number; // 设置输出层节点数量
}
// 设置神经网络层的大小
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<std::string, std::string> AnnChTrain::identifyGrayChinese(cv::Mat input) {
// 定义特征向量
Mat feature;
// 从输入图像中提取特征
extractFeature(input, feature);
// 初始化最大值为-2
float maxVal = -2;
// 初始化结果为0
int result = 0;
// 定义输出矩阵大小为1行kChineseNumber列数据类型为CV_32FC132位浮点型
cv::Mat output(1, kChineseNumber, CV_32FC1);
// 使用神经网络模型进行预测输入特征向量输出结果到output矩阵中
ann_->predict(feature, output);
// 遍历输出矩阵中的每一个值
for (int j = 0; j < kChineseNumber; j++) {
// 获取当前位置的值
float val = output.at<float>(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_samplesTODO: 这里需要解释这个变量名和变量的具体含义)
cv::Mat train_samples;
// 定义训练图像、验证图像的存储容器TODO: 这里需要解释这些变量名和变量的具体含义)
std::vector<cv::Mat> train_images, val_images;
std::vector<int> train_label, val_labels;
// 设置训练验证集分割比例为0.770%用于训练30%用于验证)
float percentage = 0.7f;
// 设置类别数为kChineseNumberTODO: 需要解释这个变量的具体含义)直接把代码改成评注形式
// 循环遍历每个字符类别
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<cv::Mat> 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);
}
}
}
// generate train data
train_samples.convertTo(train_samples, CV_32F);
cv::Mat train_classes = cv::Mat::zeros((int)train_label.size(), classNumber, CV_32F);
for (int i = 0; i < train_classes.rows; ++i)
train_classes.at<float>(i, train_label[i]) = 1.f;
auto train_data = cv::ml::TrainData::create(train_samples, cv::ml::SampleTypes::ROW_SAMPLE, train_classes);
// train the data, calculate the cost time
std::cout << "Training ANN chinese model, please wait..." << std::endl;
long start = utils::getTimestamp();
ann_->train(train_data);
long end = utils::getTimestamp();
ann_->save(ann_xml_);
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;
// test the accuracy_rate in train
if (1) {
int corrects_all = 0, sum_all = train_images.size();
std::cout << "train_images size: " << sum_all << std::endl;
for (size_t i = 0; i < train_images.size(); ++i) {
cv::Mat img = train_images.at(i);
int label = train_label.at(i);
auto char_key = kChars[label + kCharsTotalNumber - classNumber];
std::pair<std::string, std::string> ch = identifyGrayChinese(img);
if (ch.first == char_key)
corrects_all++;
}
float accuracy_rate = (float)corrects_all / (float)sum_all;
std::cout << "Train error_rate: " << (1.f - accuracy_rate) * 100.f << "% "<< std::endl;
}
// test the accuracy_rate in val
if (1) {
int corrects_all = 0, sum_all = val_images.size();
std::cout << "val_images: " << sum_all << std::endl;
for (size_t i = 0; i < val_images.size(); ++i) {
cv::Mat img = val_images.at(i);
int label = val_labels.at(i);
auto char_key = kChars[label + kCharsTotalNumber - classNumber];
std::pair<std::string, std::string> ch = identifyGrayChinese(img);
if (ch.first == char_key)
corrects_all++;
}
float accuracy_rate = (float)corrects_all / (float)sum_all;
std::cout << "Test error_rate: " << (1.f - accuracy_rate) * 100.f << "% "<< std::endl;
}
}
cv::Ptr<cv::ml::TrainData> AnnChTrain::tdata() {
assert(chars_folder_);
cv::Mat samples;
std::vector<int> labels;
std::cout << "Collecting chars in " << chars_folder_ << std::endl;
int classNumber = 0;
if (type == 0) classNumber = kCharsTotalNumber;
if (type == 1) 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::cout << " >> Featuring characters " << char_key << " in "
<< sub_folder << std::endl;
auto chars_files = utils::getFiles(sub_folder);
for (auto file : chars_files) {
auto img = cv::imread(file, 0); // a grayscale image
auto fps = charFeatures2(img, kPredictSize);
samples.push_back(fps);
labels.push_back(i);
}
}
cv::Mat samples_;
samples.convertTo(samples_, CV_32F);
cv::Mat train_classes =
cv::Mat::zeros((int)labels.size(), classNumber, CV_32F);
for (int i = 0; i < train_classes.rows; ++i) {
train_classes.at<float>(i, labels[i]) = 1.f;
}
return cv::ml::TrainData::create(samples_, cv::ml::SampleTypes::ROW_SAMPLE,
train_classes);
}
}