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/src/src/train/ann_train.cpp

415 lines
17 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#include <numeric>
#include <ctime>
#include "easypr/train/ann_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/train/create_data.h"
#include "easypr/util/util.h"
namespace easypr {
AnnTrain::AnnTrain(const char* chars_folder, const char* xml)
: chars_folder_(chars_folder), ann_xml_(xml) {
ann_ = cv::ml::ANN_MLP::create();
// type=0, all characters
// type=1, only chinese
type = 0;
kv_ = std::shared_ptr<Kv>(new Kv);
kv_->load("resources/text/province_mapping");
}
// 这段代码是C++的类成员函数AnnTrain::AnnTrain的构造函数实现。
// 构造函数接受两个参数chars_folder和xml并将其分别赋值给成员变量chars_folder_和ann_xml_。
// 然后使用cv::ml::ANN_MLP::create()创建了一个神经网络对象ann_。
// type被初始化为0kv_被初始化为一个加载了"resources/text/province_mapping"的Kv对象。
void AnnTrain::train() {
int classNumber = 0;
cv::Mat layers;
int input_number = 0;
int hidden_number = 0;
int output_number = 0;
if (type == 0) {
classNumber = kCharsTotalNumber;
input_number = kAnnInput;
hidden_number = kNeurons;
output_number = classNumber;
}
else if (type == 1) {
classNumber = kChineseNumber;
input_number = kAnnInput;
hidden_number = kNeurons;
output_number = classNumber;
}
// 这段代码是AnnTrain类的train方法根据type的值选择不同的classNumber、input_number、hidden_number和output_number。
// 当type为0时classNumber为kCharsTotalNumberinput_number为kAnnInputhidden_number为kNeuronsoutput_number为classNumber。
// 当type为1时classNumber为kChineseNumberinput_number为kAnnInputhidden_number为kNeuronsoutput_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)));
int second_hidden_neurons = int(m * std::sqrt(N / (m + 2)));
bool useTLFN = false;
if (!useTLFN) {
layers.create(1, 3, CV_32SC1);
layers.at<int>(0) = input_number;
layers.at<int>(1) = hidden_number;
layers.at<int>(2) = output_number;
}
else {
// Two-layers neural networks is hard to train, So do not try it
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<int>(0) = input_number;
layers.at<int>(1) = first_hidden_neurons;
layers.at<int>(2) = second_hidden_neurons;
layers.at<int>(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);
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 \"ann\") under \"tmp\". \n");
return;
}
//using raw data or raw + synthic data.
auto traindata = sdata(350);
std::cout << "Training ANN model, please wait..." << std::endl;
long start = utils::getTimestamp();
ann_->train(traindata);
long end = utils::getTimestamp();
ann_->save(ann_xml_);
test();
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<std::string, std::string> AnnTrain::identifyChinese(cv::Mat input) {
cv::Mat feature = charFeatures2(input, kPredictSize);
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<float>(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);
}
// 这段代码是AnnTrain类的identifyChinese方法接受一个cv::Mat类型的输入参数input。
// 首先调用charFeatures2函数提取特征然后使用神经网络ann_对特征进行预测得到输出output。
// 接着遍历output找到最大值对应的索引result并计算出最终的索引index。
// 最后根据index获取对应的字符key再通过kv_获取对应的省份province最终返回一个包含字符和省份的pair。
std::pair<std::string, std::string> AnnTrain::identify(cv::Mat input) {
cv::Mat feature = charFeatures2(input, kPredictSize);
float maxVal = -2;
int result = 0;
//std::cout << feature << std::endl;
cv::Mat output(1, kCharsTotalNumber, CV_32FC1);
ann_->predict(feature, output);
//std::cout << output << std::endl;
for (int j = 0; j < kCharsTotalNumber; j++) {
float val = output.at<float>(j);
//std::cout << "j:" << j << "val:" << val << std::endl;
if (val > maxVal) {
maxVal = val;
result = j;
}
}
auto index = result;
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);
}
}
// 这段代码是AnnTrain类的identify方法接受一个cv::Mat类型的输入参数input。
// 首先调用charFeatures2函数提取特征然后使用神经网络ann_对特征进行预测得到输出output。
// 接着遍历output找到最大值对应的索引result并计算出最终的索引index。
// 最后根据index判断返回的字符和省份信息返回一个包含字符和省份的pair。
void AnnTrain::test() {
assert(chars_folder_);
int classNumber = 0;
if (type == 0) classNumber = kCharsTotalNumber;
if (type == 1) classNumber = kChineseNumber;
int corrects_all = 0, sum_all = 0;
std::vector<float> rate_list;
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);
fprintf(stdout, ">> Testing characters %s in %s \n", char_key, sub_folder);
auto chars_files = utils::getFiles(sub_folder);
int corrects = 0, sum = 0;
std::vector<std::pair<std::string, std::string>> error_files;
// 这段代码是AnnTrain类的test方法用于测试字符识别的准确率。
// 首先根据type的值确定classNumber然后遍历每个字符的文件夹进行测试。
// 在测试过程中,会统计正确识别的字符数量和总测试字符数量,以及每个字符的识别准确率。
// 最后输出总的测试结果和平均准确率。
for (auto file : chars_files) {
auto img = cv::imread(file, 0); // a grayscale image
if (!img.data) {
//cout << "Null pointer!" << endl;
continue;
}
std::pair<std::string, std::string> ch;
if (type == 0) ch = identify(img);
if (type == 1) ch = identifyChinese(img);
if (ch.first == char_key) {
++corrects;
++corrects_all;
} else {
error_files.push_back(std::make_pair(utils::getFileName(file), ch.second));
}
++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);
std::string error_string;
auto end = error_files.end();
if (error_files.size() >= 10) {
end -= static_cast<size_t>(error_files.size() * (1 - 0.1));
}
for (auto k = error_files.begin(); k != end; ++k) {
auto kv = *k;
error_string.append(" ").append(kv.first).append(": ").append(
kv.second);
if (k != end - 1) {
error_string.append(",\n");
} else {
error_string.append("\n ...");
}
}
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));
double rate_sum = std::accumulate(rate_list.begin(), rate_list.end(), 0.0);
double rate_mean = rate_sum / (rate_list.size() == 0 ? 1 : rate_list.size());
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();
if (rand_type % 2 == 0) {
int ran_x = rand() % 5 - 2;
int ran_y = rand() % 5 - 2;
result = translateImg(result, ran_x, ran_y);
}
else if (rand_type % 2 != 0) {
float angle = float(rand() % 15 - 7);
result = rotateImg(result, angle);
}
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<cv::ml::TrainData> AnnTrain::sdata(size_t number_for_count) {
assert(chars_folder_);
cv::Mat samples;
std::vector<int> labels;
int classNumber = 0;
if (type == 0) classNumber = kCharsTotalNumber;
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];
char sub_folder[512] = { 0 };
sprintf(sub_folder, "%s/%s", chars_folder_, char_key);
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<cv::Mat> matVec;
matVec.reserve(number_for_count);
for (auto file : chars_files) {
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;
auto img = matVec.at(ran_num);
auto simg = getSyntheticImage(img);
matVec.push_back(simg);
if (1) {
std::stringstream ss(std::stringstream::in | std::stringstream::out);
ss << sub_folder << "/" << i << "_" << t << "_" << ran_num << ".jpg";
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) {
auto fps = charFeatures2(img, kPredictSize);
samples.push_back(fps);
labels.push_back(i);
}
}
// 这段代码用于统计字符数量并将特征和标签添加到训练数据中。
// 首先使用fprintf输出字符数量然后遍历matVec中的图像对每个图像提取特征并将特征和标签添加到训练数据中。
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);
}
// 该部分代码是AnnTrain类的tdata方法用于生成训练数据。
// 首先将samples转换为CV_32F类型的samples_然后初始化train_classes为全零矩阵。
// 接着遍历train_classes的每一行将对应位置的值设为1。
// 最后使用cv::ml::TrainData::create函数创建并返回训练数据对象。
cv::Ptr<cv::ml::TrainData> AnnTrain::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;
// 这段代码是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};
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);
}
}
// 这段代码是一个循环,遍历每个字符文件夹中的文件,并将文件读取为灰度图像后提取特征并添加到训练数据中。
// 首先根据循环变量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 =
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);
}
}
// 该部分代码是用于生成训练数据的一部分首先将samples转换为CV_32F类型的samples_
// 然后初始化train_classes为全零矩阵接着遍历train_classes的每一行将对应位置的值设为1。
// 最后使用cv::ml::TrainData::create函数创建并返回训练数据对象。