Распознавание с помощью 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 выглядит так:

Архитектура LeNet

Рассмотрим функцию TF_Run. Она принимает на вход изображение image, которое необходимо распознать, detectedRects - вектор интересующих областей на изображении image, alphSize -  количество возможных букв в распознаваемом алфавите. Функция возвращает распознанную строку:

std::string TF_Run(cv::Mat image, std::vector & detectedRects, size_t alphSize);

Загрузим обученную сеть. Для этого необходимо иметь четыре файла: 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