Распознавание с помощью Tensorflow на С-API и OpenCV
В официальной документации Tensorflow плохо представлены примеры использования на языке C. Данная статья направлена на облегчение использования Tensorflow. Предполагается, что вы владеете основами Tensorflow. Для начала, необходимо скачать заголовочные файлы и dll библиотеки: https://www.tensorflow.org/install/lang_c
В статье рассматривается, как использовать уже обученную сеть на примере классической сети LeNet. Упрощенно эту сеть можно представить так: conv_1 -> pool_1 -> lrn_1 -> conv_2 -> pool_2 -> lrn_2 -> dropout_1 -> dot_1 -> dropout_2 -> dot_2 -> dropout_3 -> logits_layer -> softmax_layer,
где conv - свёрточный слой (convolution), pool - слой подвыборки (pooling), lrn - слой нормализации локального отклика (local response normalization), dropout - отбрасывающий слой, dot - обычный полносвязный слой, logits_layer и softmax_layer - слои logit и softmax соответственно.
Оригинальная LeNet выглядит так:

Рассмотрим функцию TF_Run. Она принимает на вход изображение image, которое необходимо распознать, detectedRects - вектор интересующих областей на изображении image, alphSize - количество возможных букв в распознаваемом алфавите. Функция возвращает распознанную строку:
std::string TF_Run(cv::Mat image, std::vector
Загрузим обученную сеть. Для этого необходимо иметь четыре файла: saved.pb (в бинарном формате) и saved.ckpt (.data-00000-of-00001, .index, .meta).
Загрузка saved.pb:
std::string TF_Run(cv::Mat image, std::vector & detectedRects, size_t alphSize)
{
std::string root = "C:\\Users\\qvzqq\\net\\";
std::string modelPath = root + "saved.pb"; // Полное имя файла c обученной моделью
std::string checkpoint_path_str = root + "saved.ckpt"; // Полное имя файла-чекпоинта
TF_Buffer* graph_def = read_file(modelPath.c_str());
TF_Graph* graph = TF_NewGraph();
// Импортировать graph_def в graph
TF_Status* status = TF_NewStatus();
TF_ImportGraphDefOptions* opts = TF_NewImportGraphDefOptions();
TF_GraphImportGraphDef(graph, graph_def, opts, status);
TF_DeleteImportGraphDefOptions(opts);
if (TF_GetCode(status) != TF_OK)
{
std::cout << "ERROR: Unable to import graph " << TF_Message(status) << std::endl;
return std::string();
}
Функция read_file реализуется как чтение бинарного файла:
TF_Buffer* read_file(const char* file)
{
FILE *f = fopen(file, "rb");
fseek(f, 0, SEEK_END);
long fsize = ftell(f);
fseek(f, 0, SEEK_SET);
void* data = malloc(fsize);
fread(data, fsize, 1, f);
fclose(f);
TF_Buffer* buf = TF_NewBuffer();
buf->data = data;
buf->length = fsize;
buf->data_deallocator = free_buffer;
return buf;
}
void free_buffer(void* data, size_t length)
{
free(data);
}
Далее необходимо создать сессию:
TF_SessionOptions* opt = TF_NewSessionOptions();
TF_Session* sess = TF_NewSession(graph, opt, status);
TF_DeleteSessionOptions(opt);
if (TF_GetCode(status) != TF_OK)
{
std::cout << "ERROR: Unable to create session " << TF_Message(status) << std::endl;
return std::string();
}
Затем запустить операцию инициализации модели с помощью функции TF_SessionRun. В данном примере эта операция внутри модели называется init:
const TF_Operation* init_op = TF_GraphOperationByName(graph, "init");
const TF_Operation* const* targets_ptr = &init_op;
TF_SessionRun(sess,
/* RunOptions */ NULL,
/* Input tensors */ NULL, NULL, 0,
/* Output tensors */ NULL, NULL, 0,
/* Target operations */ targets_ptr, 1,
/* RunMetadata */ NULL,
/* Output status */ status);
if (TF_GetCode(status) != TF_OK)
{
std::cout << "ERROR: Unable to run init_op: " << TF_Message(status) << std::endl;
return std::string();
}
Запустить операцию восстановления состояния модели с помощью функции TF_SessionRun. В данном примере эта операция внутри модели выполняется с помощью save/Const и save/restore_all:
TF_Operation* checkpoint_op = TF_GraphOperationByName(graph, "save/Const");
TF_Operation* restore_op = TF_GraphOperationByName(graph, "save/restore_all");
// Перекодирование С-строки checkpoint_path_str.c_str() в формат TF_STRING
size_t checkpoint_path_str_len = strlen(checkpoint_path_str.c_str());
size_t encoded_size = TF_StringEncodedSize(checkpoint_path_str_len);
size_t total_size = sizeof(int64_t) + encoded_size;
char* input_encoded = (char*)malloc(total_size);
memset(input_encoded, 0, total_size);
TF_StringEncode(checkpoint_path_str.c_str(), checkpoint_path_str_len,
input_encoded + sizeof(int64_t), encoded_size, status);
if (TF_GetCode(status) != TF_OK)
{
std::cout << "ERROR: something wrong with encoding: " << TF_Message(status) << std::endl;
return std::string();
}
TF_Tensor* path_tensor = TF_NewTensor(TF_STRING, NULL, 0, input_encoded,
total_size, &deallocator, 0);
TF_Output* run_path = (TF_Output*)malloc(1 * sizeof(TF_Output));
run_path[0].oper = checkpoint_op;
run_path[0].index = 0;
TF_Tensor** run_path_tensors = (TF_Tensor**)malloc(1 * sizeof(TF_Tensor*));
run_path_tensors[0] = path_tensor;
TF_SessionRun(sess,
/* RunOptions */ NULL,
/* Input tensors */ run_path, run_path_tensors, 1,
/* Output tensors */ NULL, NULL, 0,
/* Target operations */ &restore_op, 1,
/* RunMetadata */ NULL,
/* Output status */ status);
if (TF_GetCode(status) != TF_OK)
{
std::cout << "ERROR: Unable to run restore_op: " << TF_Message(status) << std::endl;
return std::string();
}
На этом шаге необходимо загрузить и преобразовать входные данные в формате cv::Mat image к формату, используемому внутри обученной сети. Зачастую в Tensorflow при распознавании используется 4-D uint8 numpy массив array[index, y, x, depth].
Функция getFlattenDataTF подготавливает входное изображение img на областях интереса rects (растяжение/сжатие, нормализация и уплощение). Размер областей интереса приводится к размеру dstW (ширина), dstH (высота).
// img --> float* arr = new float [count * w * h * channels]
cv::Mat_ getFlattenDataTF(cv::Mat img, std::vector rects, int dstW, int dstH, int channels)
{
channels = 1;
cv::Mat_ flattenData(1, rects.size() * dstH * dstW * channels, 0.0f);
float* data = new float[rects.size() * dstH * dstW * channels];
for (int i = 0; i < rects.size(); i++)
{
cv::Rect r = rects[i];
// исходный рект r к размеру dstH х dstW
cv::Mat resizedMat(dstH, dstW, CV_32FC1);
cv::Mat origin = img(r).clone();
resize(origin, resizedMat, resizedMat.size());
// [0..255] -> [0..1]
cv::Mat normalizedMat;
resizedMat.convertTo(normalizedMat, CV_32FC1, 1.f / 255.f);
// flattern, то есть 2д (массив-изображение normalizedMat) в одномерный массив-строку flattenData
for (int j = 0; j < normalizedMat.rows; j++)
{
size_t imgOffset = i * normalizedMat.rows * normalizedMat.cols;
cv::Rect r1 = cv::Rect(0, j, normalizedMat.cols, 1);
cv::Rect r2 = cv::Rect(imgOffset + j * normalizedMat.cols, 0, normalizedMat.cols, 1);
cv::Mat_ smallRoi = normalizedMat(r1);
cv::Mat_ bigRoi = flattenData(r2);
smallRoi.copyTo(bigRoi);
}
}
return flattenData;
}
Функция getFlattenDataTF используется следующим образом:
TF_Operation* input_op_images = TF_GraphOperationByName(graph, "dataset_inputs/images"); int imgW = 28, imgH = 28, channels = 1; // Подготовить входное изображение image auto flatternMats = getFlattenDataTF(image, detectedRects, imgW, imgH, channels); float* raw_input_data_images = (float*)malloc(flatternMats.cols * sizeof(float)); memcpy(raw_input_data_images, flatternMats.data, flatternMats.cols * sizeof(float)); int64_t* raw_input_dims_images = (int64_t*)malloc(2 * sizeof(int64_t)); raw_input_dims_images[0] = detectedRects.size(); raw_input_dims_images[1] = imgW * imgH; TF_Tensor* input_tensor_images = TF_NewTensor(TF_FLOAT, raw_input_dims_images, 2, raw_input_data_images, flatternMats.cols * sizeof(float), &deallocator, 0); // Подготовить dropout_prob TF_Operation* input_op_dropout_prob = TF_GraphOperationByName(graph, "dropout_probability"); float* raw_input_data_dropout_prob = (float*)malloc(1 * sizeof(float)); raw_input_data_dropout_prob[0] = 1.f; int64_t* raw_input_dims_dropout_prob = (int64_t*)malloc(1 * sizeof(int64_t)); raw_input_dims_dropout_prob[0] = 1; TF_Tensor* input_tensor_dropout_prob = TF_NewTensor(TF_FLOAT, raw_input_dims_dropout_prob, 1, raw_input_data_dropout_prob, 1 * sizeof(float), &deallocator, NULL); TF_Output* run_inputs = (TF_Output*)malloc(2 * sizeof(TF_Output)); run_inputs[0].oper = input_op_images; run_inputs[0].index = 0; run_inputs[1].oper = input_op_dropout_prob; run_inputs[1].index = 0; TF_Tensor** run_inputs_tensors = (TF_Tensor**)malloc(2 * sizeof(TF_Tensor*)); run_inputs_tensors[0] = input_tensor_images; run_inputs_tensors[1] = input_tensor_dropout_prob; run_inputs_tensors[0] = input_tensor_images; run_inputs_tensors[1] = input_tensor_dropout_prob;
Так же необходимо указать и подготовить выходную операцию распознавания. В данном примере используется logits_layer/pre-activation:
TF_Operation* output_op_inference = TF_GraphOperationByName(graph, "logits_layer/pre-activation");
if (output_op_inference == NULL)
{
std::cout << "ERR! output_op_inference is null";
return std::string();
}
TF_Output* run_outputs = (TF_Output*)malloc(1 * sizeof(TF_Output));
run_outputs[0].oper = output_op_inference;
run_outputs[0].index = 0;
TF_Tensor** run_output_tensors = (TF_Tensor**)malloc(1 * sizeof(TF_Tensor*));
size_t raw_output_data_size = detectedRects.size() * alphSize * sizeof(float);
float* raw_output_data = (float*)malloc(raw_output_data_size);
memset(raw_output_data, 0, raw_output_data_size);
int raw_output_dims_cout = 2;
int64_t* raw_output_dims = (int64_t*)malloc(raw_output_dims_cout * sizeof(int64_t));
raw_output_dims[0] = detectedRects.size();
raw_output_dims[1] = alphSize;
TF_Tensor* output_tensor = TF_NewTensor(TF_FLOAT, raw_output_dims,
raw_output_dims_cout, raw_output_data, raw_output_data_size, &deallocator, NULL);
run_output_tensors[0] = output_tensor;
Наконец, выполнив все подготовительные действия, непосредственно распознавание:
TF_SessionRun(sess,
/* RunOptions */ NULL,
/* Input tensors */ run_inputs, run_inputs_tensors, 2,
/* Output tensors */ run_outputs, run_output_tensors, 1,
/* Target operations */ NULL, 0,
/* RunMetadata */ NULL,
/* Output status */ status);
if (TF_GetCode(status) != TF_OK)
{
std::cout << "ERROR: Unable to run output_op: " << TF_Message(status) << std::endl;
return std::string();
}
И получить результат:
float* netOutPut = (float*)TF_TensorData(run_output_tensors[0]);
std::string text, filteredText;
std::string outText;
double stringScore = 0;
for (int i = 0; i < detectedRects.size(); i++)
{
float* rowStart = netOutPut + i * alphSize;
float* rowEnd = netOutPut + i * alphSize + alphSize;
float* maxPtr = std::max_element(rowStart, rowEnd);
size_t count = alphSize; // rowEnd - rowStart;
size_t symbCode = maxPtr - rowStart;
stringScore += *maxPtr;
if (symbCode >= 0 && symbCode < alph.size())
{
std::cout << alph[symbCode] << " = " << *maxPtr << std::endl;
text += alph[symbCode];
if (*maxPtr > reliabSymbThresh)
filteredText += alph[symbCode];
std::cout << "symb #" << i << " ('" << alph[symbCode] << " = " << *maxPtr << "')" << std::endl;
for (int j = 0; j < alph.size(); j++)
{
std::cout << "'" << alph[j] << "'" << " = " << netOutPut[i * alph.size() + j] << ", ";
}
std::cout << std::endl;
}
else
std::cout << "Error symbol code: " << netOutPut[i] << ". Correct codes are 0.." << alph.size() - 1 << std::endl;
std::cout << std::endl;
}
std::cout << text << " (" << stringScore / detectedRects.size() << ")" << std::endl;
outText = text;
В результате можно получить значимость (или вероятность) распознаваемых символов

После использования необходимо освободить всю выделенную память с помощью функций TF_DeleteSession, TF_DeleteStatus, TF_DeleteBuffer, TF_DeleteGraph, TF_DeleteTensor:
TF_CloseSession(sess, status); TF_DeleteSession(sess, status); TF_DeleteStatus(status); TF_DeleteBuffer(graph_def); TF_DeleteGraph(graph); TF_DeleteTensor(path_tensor); // TF_DeleteTensor вызывает deallocator связанного с ним массива данных из С. Напр., path_tensor cвязан с input_encoded, input_tensor_images с raw_input_data_images, поэтому второе free не требуется free(run_path); free(run_path_tensors); TF_DeleteTensor(input_tensor_images); //free((void*)raw_input_data_images); ....
В некоторых случаях Tensorflow использует автоматическое управление памятью, поэтому в функции TF_NewTensor есть параметр deallocator. Его можно реализовать так:
void deallocator (void* ptr, size_t len, void* arg)
{
free(ptr);
}
Если возникают проблемы с определением названий операций внутри .pb модели, существует утилита netron для просмотра .pb и других файлов с моделями нейронных сетей https://github.com/lutzroeder/netron
Данная статья является примером, а код в ней не подготовлен для использования в производстве. Пример обученной сети можно найти здесь: https://yadi.sk/d/ZbiC23G8Z_SR3g
