C#使用轻量向量模型Dewarp.tflite进行图像矫正变换
最近又研究上了拍摄图像矫正,就是类似于扫描全能王那样的对拍摄的弯曲文档照片还原成正常的排版的文档
在搜索huggingface模型站点的时候发现一个轻量级的模型DewarpNet-LiteRT,研究了下文档,发现实现的效果还可以

如上图所示,虽然和扫描全能王那种纯算法实现的纠正效果还有差距,但是总体而言还算可以接受的
同时模型的介绍页也提供了调用代码
import numpy as np, cv2, torch, torch.nn.functional as F
from ai_edge_litert.interpreter import Interpreter
it = Interpreter(model_path = "dewarp.tflite" ); it.allocate_tensors()
inp, out = it.get_input_details(), it.get_output_details()
it.set_tensor(inp[ 0 ][ "index" ], x) # [1,3,256,256] float32, BGR, x/255
it.invoke()
bm = it.get_tensor(out[ 0 ][ "index" ]) # [1,2,128,128]
bm = np.stack([cv2.resize(cv2.blur(bm[ 0 , 0 ],( 3 , 3 )), (W,H)),
cv2.resize(cv2.blur(bm[ 0 , 1 ],( 3 , 3 )), (W,H))], - 1 )[ None ]
flat = F.grid_sample(torch.tensor(imgorg / 255. ).permute( 2 , 0 , 1 )[ None ]. float (),
torch.tensor(bm). float (), align_corners = True ) # unwarpe
不得不说Python还真是各种模型调用的御用语言,这么简单几句话就可以进行模型调用了,但是我主要使用的是C#,所有也想着能够在C#上用上该模型
因此这篇博文的主要内容就是使用C#仿照上述Python代码进行tflite后缀的模型调用。
废话不多说,直接开始吧
首先新建C#项目这些自不必说,紧接着我们需要从Nuget安装几个库到当前项目中

以上就是项目调用模型需要用到的几个库,都可以使用最新稳定版,然后我们从模型下载页面下载

dewarp.tflite模型文件到项目的bin/debug/ 目录下面
然后我们使用
// 创建解释器并加载模型
var flatBufferModel = new FlatBufferModel("dewarp.tflite");
var interpreter = new Interpreter(flatBufferModel);
interpreter.AllocateTensors();
上述这几句话加载该模型,但是这个时候我们其实并不知道该怎么使用该模型对图片进行处理的,因此我们需要先获得该模型的信息,以帮助我们构建能够提交给该模型处理的图片数据

我们将鼠标指向interpreter,通过interpreter里面的属性,我们可以看到Inputs[0]属性里,JaggedData属性提示我们应该输入float[1,3,256,256]也就是意味着,模型需要输入float[dim,rgb,x,y]的信息
在Outputs[0]里,JaggedData属性提示我们模型会返回float[1,2,128,128]给我们处理
好了有了以上信息以后,我们可以构建代码了
// 创建解释器并加载模型
var flatBufferModel = new FlatBufferModel("dewarp.tflite");
var interpreter = new Interpreter(flatBufferModel);
interpreter.AllocateTensors();
//载入待处理的图片文件 并获得图片尺寸
var inputImage = CvInvoke.Imread("Test.png");
var originalWidth = inputImage.Width;
var originalHeight = inputImage.Height;
//模型只能处理256x256的图片,所以需要先将图片缩放到256,256大小
Mat resized = new Mat();//resized里保存着缩放后的图片数据
CvInvoke.Resize(inputImage, resized, new Size(256, 256));
//模型需要float[1,3,256,256]的数据,所有先创建一个空白的Mat数据,并将图片数据转换进去
Mat floatImage = new Mat();
resized.ConvertTo(floatImage, Emgu.CV.CvEnum.DepthType.Cv32F, 1.0 / 255.0);
var inputTensor = new float[1, 3, 256, 256];//将要输入给模型的向量
//下面将图片数据顺序存入到向量数组里
unsafe
{
float* src = (float*)floatImage.DataPointer;//图片数据的内存首地址
fixed (float* dst = inputTensor)//固定数组数据首地址
{
float* b = dst;
float* g = dst + 256 * 256;
float* r = dst + 256 * 256 * 2;
int pixels = 256 * 256;
for (int i = 0; i < pixels; i++)
{
b[i] = src[i * 3];
g[i] = src[i * 3 + 1];
r[i] = src[i * 3 + 2];
}
}
}
//下面将向量数组的数据拷贝给模型输入内存中
unsafe
{
fixed (float* p = inputTensor)
{
IntPtr ptr = (IntPtr)p;
Buffer.MemoryCopy(
(void*)ptr,
(void*)interpreter.Inputs[interpreter.InputIndices[0]].DataPointer,
inputTensor.Length * sizeof(float),
inputTensor.Length * sizeof(float));
}
}
//调用模型对输入的图片向量数据进行处理
interpreter.Invoke();
//获得模型输出的向量,向量格式为 float[1, 2, 128, 128],通过interpreter.Outputs[0]的JaggedData属性能看到
var GetTensor = interpreter.GetTensor(interpreter.OutputIndices[0]);
// 后处理:分离两个通道并调整大小
Mat channel0 = new Mat(128, 128, Emgu.CV.CvEnum.DepthType.Cv32F, 1);
Mat channel1 = new Mat(128, 128, Emgu.CV.CvEnum.DepthType.Cv32F, 1);
try
{
IntPtr basePtr = GetTensor.DataPointer;
int planeSize = 128 * 128 * sizeof(float);
unsafe
{
Buffer.MemoryCopy(
basePtr.ToPointer(),
channel0.DataPointer.ToPointer(),
planeSize,
planeSize);
Buffer.MemoryCopy(
(byte*)basePtr.ToPointer() + planeSize,
channel1.DataPointer.ToPointer(),
planeSize,
planeSize);
}
}
//分别对两个通道的数据进行均值滤波,去除噪点
Mat b0 = new Mat();
Mat b1 = new Mat();
CvInvoke.Blur(channel0, b0, new Size(3, 3), new Point(-1, -1));
CvInvoke.Blur(channel1, b1, new Size(3, 3), new Point(-1, -1));
//将滤波后的模式输出数据缩放还原成原图尺寸
Mat flowX = new Mat();
Mat flowY = new Mat();
CvInvoke.Resize(b0, flowX, new Size(originalWidth, originalHeight), 0, 0, Inter.Linear);
CvInvoke.Resize(b1, flowY, new Size(originalWidth, originalHeight), 0, 0, Inter.Linear);
//根据模型数据创建能够矫正图片的映射数据
Mat mapX = new Mat(originalHeight, originalWidth, DepthType.Cv32F, 1);
Mat mapY = new Mat(originalHeight, originalWidth, DepthType.Cv32F, 1);
unsafe
{
float* mx = (float*)mapX.DataPointer;
float* my = (float*)mapY.DataPointer;
float* fx = (float*)flowX.DataPointer;
float* fy = (float*)flowY.DataPointer;
float sx = (originalWidth - 1) / 2f;
float sy = (originalHeight - 1) / 2f;
int index = 0;
for (int y = 0; y < originalWidth; y++)
{
for (int x = 0; x < originalHeight; x++, index++)
{
float gx = fx[index];
float gy = fy[index];
mx[index] = (gx + 1) * sx;
my[index] = (gy + 1) * sy;
}
}
}
//最后一步,根据模型给出的数据对原图进行逆扭曲处理
Mat OutPutImage = new Mat();
CvInvoke.Remap(inputImage, OutPutImage, mapX, mapY, Inter.Linear, BorderType.Replicate, new MCvScalar());
CvInvoke.Imwrite("Output.jpg", OutPutImage);//保存输出结果
如下图所示,根据上述代码的调试结果,我们获得了和官方示例一模一样的弯曲图片矫正结果,虽然效果上和扫描全能王根据算法还原的效果还有些差距,不过大致上已经是能够接受的效果了。

笔者接下来会研究如何通过纯算法对图像进行矫正操作,有结果后再在博客园里分享经验。

浙公网安备 33010602011771号