Распознавание с помощью 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