[.NET6]使用ML.NET+ONNX预训练模型整活B站经典《华强买瓜》
最近在看微软开源的机器学习框架ML.NET使用别人的预训练模型(开放神经网络交换格式.onnx)来识别图像,然后逛github发现一个好玩的repo。决定整活一期博客。
首先还是稍微科普一下机器学习相关的知识,这一块.NET虽然很早就开源了ML.NET框架,甚至在官方的ML.NET开源之前,就有一些三方社区的开源实现比如早期的AForge.NET实现。以及后来的基于python著名的神经网络框架tensorflow迁移的tensorflow.net亦或者是pytorch迁移的torchsharp来实现C#版本的深度学习,但是毕竟C#确实天生并不适合用来搞机器学习/深度学习,AI这一块也一直都是python的基本盘。但是不适合并不代表没有方案,现在AI逐渐普及的今天,我们普通的开发者依然可以使用一些别人训练好的模型来做一些应用落地。
今天我们会用到一些训练好的模型来实现我们的目的,需要准备以下环境和工具:
1、安装有.NET5或者6的windows开发环境
2、netron 用于解析模型的参数,下载地址:https://github.com/lutzroeder/netron/releases/tag/v5.8.9
3、ffmpeg 用于视频处理 下载地址:https://ffmpeg.org/download.html
4、onnx预训练udnie、super-resolution
udnie模型 下载地址:https://github.com/onnx/models/blob/main/vision/style_transfer/fast_neural_style/model/udnie-9.onnx
super-resolution模型 下载地址:https://github.com/onnx/models/blob/main/vision/super_resolution/sub_pixel_cnn_2016/model/super-resolution-10.tar.gz (需要解压提取内部的onnx文件)
操作流程如下:
1、首先我们将目标视频(我这里就用B站经典短视频《华强买瓜》为例)通过ffmpeg转换成普通的一帧一帧的图片
2、通过ML.NET加载【神经风格转换预训练模型】将每一帧原图迁移到新的风格(艺术风格:udnie,抽象主义)。
3、由于2只能将图片迁移到固定的240*240格式,所以我们还需要通过ML.NET加载【超分辨率预训练模型】将每一帧图片进行超分辨率放大得到一张672*672的图片
4、通过ffmpeg将新的图片合并成新的视频
首先先看看成品(这里我转换成gif方便演示):
原版视频《华强买瓜》1280*720 B站地址:https://www.bilibili.com/video/BV17h411W7aw
迁移后的抽象艺术版本 224*224
超分辨放大后的版本 672*672
接着我们看看如何一步一步来实现这个流程的
首先我们新建一个空白文件夹,将下载好的ffmpeg.exe和准备要处理的mp4视频文件放进这个空白文件夹
接着我们需要从视频中分离音频文件,用于后期合成视频时把音频合成回去,否则视频会没有声音,打开控制台CD到刚才的目录,执行命令:
./ffmpeg -i 1.mp4 -vn -y -acodec copy 1.aac
然后我们从视频中将每一帧拆解成一张一张的jpg图片,这里首先要创建一个img子文件夹,否则会报错。另外我选择的r 25意思就是每秒25帧。如果你的视频不是每秒25帧(右键-属性-详细信息-帧速率)则自行根据文件调整,最后合成的时候也需要按照这个帧率合成新的视频:
./ffmpeg -i 1.mp4 -r 25 -f image2 img/%d.jpeg
到这里为止,我们就将图片和音频拆解出来了,接下来准备编码,首先我们打开VS创建一个控制台程序,引入nuget包:
Install-Package Microsoft.ML.OnnxRuntime -Version 1.11.0
Install-Package Microsoft.ML.OnnxTransformer -Version 1.7.1
Install-Package System.Drawing.Common -Version 6.0.0
接着我们创建一个一个类文件用于加载模型以及完成相应的图片处理,在此之前我们需要使用安装好的netron来打开这两个onnx模型,查询他们的输入输出值,打开netron选择file-open,然后选择第一个模型udnie-9.onnx,点击input,可以看到右边已经展示出了这个模型的输入和输出项,接着我们创建类的时候,这里需要这一些数字。
接着我们打开VS创建好的项目,把我们的两个onnx模型引入进去。接着编写如下代码:
首先定义一个session用于加下onnx模型
static InferenceSession styleTransferSession = new InferenceSession("model/udnie-9.onnx");
接着我们创建一个方法调用这个模型
public static Bitmap ProcessStyleTransfer(Bitmap originBmp)
{
//根据netron得到的input,我们在这里构建对应的输入张量
var input = new DenseTensor<float>(new[] { 1, 3, 224, 224 });
//将bitmap转换成input
Tool.BitmapToTensor(originBmp, 224, 224, ref input, true);
//接着调用模型得到迁移后的张量output
using var results = styleTransferSession.Run(new[] { NamedOnnxValue.CreateFromTensor("input1", input) });
if (results.FirstOrDefault()?.Value is not Tensor<float> output)
throw new ApplicationException("无法处理图片");
//由于模型输出的是3*224*224的张量,所以这里只能构建出224*224的图片
return Tool.TensorToBitmap(output, 224, 224);
}
其实到这一步神经风格迁移就完成了,最后的bitmap就是迁移后的新图片,我们只需要调用bitmap.save即可保存到磁盘上
接着我们创建超分辨率模型的方法来,其实同上面的调用非常类似的代码
这里唯一需要注意的是超分辨率提取并非采用RGB直接放大,而是用了YCbCr来放大,所以这里需要有一个转换,原文在这里:https://github.com/onnx/models/tree/main/vision/super_resolution/sub_pixel_cnn_2016
static InferenceSession superResolutionSession = new InferenceSession("model/super_resolution.onnx");
public static Bitmap ProcessSuperResolution(Bitmap originBmp)
{
//根据netron得到的input,我们在这里构建对应的输入张量,由于该模型并非采用RGB而是YCbCr,所以中间会做一些转换,不过整体流程和上一个类似
var input = new DenseTensor<float>(new[] { 1, 1, 224, 224 });
//将bitmap转换成input
Tool.BitmapToTensor(originBmp, 224, 224, ref input, true);
//由于模型处理Y值,剩下的Cb和Cr需要我们单独调用System.Drawing.Common双三次插值算法放大得到对应的Cb和Cr值
var inputCbCr = new DenseTensor<float>(new[] { 1, 672, 672 });
inputCbCr = Tool.ResizeGetCbCr(originBmp, 672, 672);
//接着调用模型得到超分重建后的张量output
using var results = superResolutionSession.Run(new[] { NamedOnnxValue.CreateFromTensor("input", input) });
if (results.FirstOrDefault()?.Value is not Tensor<float> output)
throw new ApplicationException("无法处理图片");
//创建一个新的bitmap用于填充迁移后的像素,这里需要通过Y+CbCr转换为RGB填充
return Tool.TensorToBitmap(output, 224, 224,false, inputCbCr);
}
其实基本上到这两步,我们的整个核心代码就完成了。剩余的部分只是一些图片处理的代码。接着我们要做的就是在Program.cs调用它得到迁移后的图片
Directory.CreateDirectory("new img path");
foreach (var path in Directory.GetFiles("old img path"))
{
//由于ffmpeg拆帧后的图片就是按照帧率从1开始排序好的图片,所以我们只需要将上一层的文件夹名字修改一下即可得到要替换的新文件路径 like: D://img/1.jpeg -> D://newimg/1.jpeg
var newpath = path.Replace("old img path", "new img path");
using var originBitmap = new Bitmap(Image.FromFile(path));
using var transferBitmap = OnnxModelManager.ProcessStyleTransfer(originBitmap);
using var reSizeBitmap = OnnxModelManager.ProcessSuperResolution(transferBitmap);
reSizeBitmap.Save(newpath);
}
接着F5 run,然后静待,一般要转换20分钟左右(cpu i5)基本就转换完成了。最后我们只需要再使用工具合成新的视频(或者gif)
./ffmpeg -f image2 -i newimg/%d.jpeg -i 1.aac -map 0:0 -map 1:a -r 25 -shortest output.mp4
整体代码基本就完成了,下面是Tool相关图片转换的代码参考:


1 internal class Tool
2 {
3 /// <summary>
4 /// 将bitmap转换为tensor
5 /// </summary>
6 /// <param name="bitmap"></param>
7 /// <returns></returns>
8 public static void BitmapToTensor(Bitmap originBmp, int resizeWidth, int resizeHeight, ref DenseTensor<float> input, bool toRGB)
9 {
10 using var inputBmp = new Bitmap(resizeWidth, resizeHeight);
11 using Graphics g = Graphics.FromImage(inputBmp);
12 g.DrawImage(originBmp, 0, 0, resizeWidth, resizeHeight);
13 g.Save();
14 for (var y = 0; y < inputBmp.Height; y++)
15 {
16 for (var x = 0; x < inputBmp.Width; x++)
17 {
18 var color = inputBmp.GetPixel(x, y);
19 if (toRGB)
20 {
21 input[0, 0, y, x] = color.R;
22 input[0, 1, y, x] = color.G;
23 input[0, 2, y, x] = color.B;
24 }
25 else
26 {
27 //将RGB转成YCbCr,此处仅保留Y值用于超分辨率放大
28 var ycbcr = RGBToYCbCr(color);
29 input[0, 0, y, x] = ycbcr.Y;
30 }
31 }
32 }
33 }
34 /// <summary>
35 /// 将tensor转换成对应的bitmap
36 /// </summary>
37 /// <param name="output"></param>
38 /// <returns></returns>
39 public static Bitmap TensorToBitmap(Tensor<float> output, int width, int height, bool toRGB = true, Tensor<float> inputCbCr = null)
40 {
41 //创建一个新的bitmap用于填充迁移后的像素
42 var newBmp = new Bitmap(width, height);
43 for (var y = 0; y < newBmp.Height; y++)
44 {
45 for (var x = 0; x < newBmp.Width; x++)
46 {
47 if (toRGB)
48 {
49 //由于神经风格迁移可能存在异常值,所以我们需要将迁移后的RGB值确保只在0-255这个区间内,否则会报错
50 var color = Color.FromArgb((byte)Math.Clamp(output[0, 0, y, x], 0, 255), (byte)Math.Clamp(output[0, 1, y, x], 0, 255), (byte)Math.Clamp(output[0, 2, y, x], 0, 255));
51 newBmp.SetPixel(x, y, color);
52 }
53 else
54 {
55 //分别将模型推理得出的Y值以及我们通过双三次插值得到的Cr、Cb值转换为对应的RGB色
56 var color = YCbCrToRGB(output[0, 0, y, x], inputCbCr[0, y, x], inputCbCr[1, y, x]);
57 newBmp.SetPixel(x, y, color);
58 }
59 }
60 }
61 return newBmp;
62 }
63 /// <summary>
64 /// RGB转YCbCr
65 /// </summary>
66 public static (float Y, float Cb, float Cr) RGBToYCbCr(Color color)
67 {
68 float fr = (float)color.R / 255;
69 float fg = (float)color.G / 255;
70 float fb = (float)color.B / 255;
71 return ((float)(0.2989 * fr + 0.5866 * fg + 0.1145 * fb), (float)(-0.1687 * fr - 0.3313 * fg + 0.5000 * fb), (float)(0.5000 * fr - 0.4184 * fg - 0.0816 * fb));
72 }
73 /// <summary>
74 /// YCbCr转RGB
75 /// </summary>
76 public static Color YCbCrToRGB(float Y, float Cb, float Cr)
77 {
78 return Color.FromArgb((byte)Math.Clamp(Math.Max(0.0f, Math.Min(1.0f, (float)(Y + 0.0000 * Cb + 1.4022 * Cr))) * 255, 0, 255),
79 (byte)Math.Clamp(Math.Max(0.0f, Math.Min(1.0f, (float)(Y - 0.3456 * Cb - 0.7145 * Cr))) * 255, 0, 255),
80 (byte)Math.Clamp(Math.Max(0.0f, Math.Min(1.0f, (float)(Y + 1.7710 * Cb + 0.0000 * Cr))) * 255, 0, 255)
81 );
82 }
83 /// <summary>
84 /// 双三次插值提取CbCr值
85 /// </summary>
86 public static DenseTensor<float> ResizeGetCbCr(Bitmap original, int newWidth, int newHeight)
87 {
88 var cbcr = new DenseTensor<float>(new[] { 2, newWidth, newHeight });
89 using var bitmap = new Bitmap(newWidth, newHeight);
90 using var g = Graphics.FromImage(bitmap);
91 g.InterpolationMode = InterpolationMode.HighQualityBicubic;
92 g.SmoothingMode = SmoothingMode.HighQuality;
93 g.DrawImage(original, new Rectangle(0, 0, newWidth, newHeight),
94 new Rectangle(0, 0, original.Width, original.Height), GraphicsUnit.Pixel);
95 g.Dispose();
96 for (var y = 0; y < bitmap.Width; y++)
97 {
98 for (var x = 0; x < bitmap.Height; x++)
99 {
100 var color = bitmap.GetPixel(x, y);
101 var ycbcr = RGBToYCbCr(color);
102 cbcr[0, y, x] = ycbcr.Cb;
103 cbcr[1, y, x] = ycbcr.Cr;
104 }
105 }
106 return cbcr;
107 }
108 }
Tools
这一期整活基本到此就结束了,虽然只是调用了两个小模型搞着玩,但是其实只要能搞到业界主流的开源预训练模型,其实可以解决很多实际的商业场景,比如我们最近在使用美团开源的yolov6模型做一些图像对象检测来落地就是一个很好的例子这里就不再展开。另外微软也承诺ML.NET的RoadMap会包含对预训练模型的迁移学习能力,这样我们可以通过通用的预训练模型根据我们自己的定制化场景只需要提供小规模数据集即可完成特定场景的迁移学习来提高模型对特定场景问题的解决能力。今天就到这里吧,下次再见。
[.NET6]使用ML.NET+ONNX预训练模型整活B站经典《华强买瓜》的更多相关文章
- ONNX预训练模型加载
tvm官网中,对从ONNX预训练模型中加载模型的教程说明 教程来自于:https://docs.tvm.ai/tutorials/frontend/from_onnx.html#sphx-glr-tu ...
- [Pytorch]Pytorch加载预训练模型(转)
转自:https://blog.csdn.net/Vivianyzw/article/details/81061765 东风的地方 1. 直接加载预训练模型 在训练的时候可能需要中断一下,然后继续训练 ...
- C#中的深度学习(五):在ML.NET中使用预训练模型进行硬币识别
在本系列的最后,我们将介绍另一种方法,即利用一个预先训练好的CNN来解决我们一直在研究的硬币识别问题. 在这里,我们看一下转移学习,调整预定义的CNN,并使用Model Builder训练我们的硬币识 ...
- XLNet预训练模型,看这篇就够了!(代码实现)
1. 什么是XLNet XLNet 是一个类似 BERT 的模型,而不是完全不同的模型.总之,XLNet是一种通用的自回归预训练方法.它是CMU和Google Brain团队在2019年6月份发布的模 ...
- 使用MxNet新接口Gluon提供的预训练模型进行微调
1. 导入各种包 from mxnet import gluon import mxnet as mx from mxnet.gluon import nn from mxnet import nda ...
- 文本分类实战(十)—— BERT 预训练模型
1 大纲概述 文本分类这个系列将会有十篇左右,包括基于word2vec预训练的文本分类,与及基于最新的预训练模型(ELMo,BERT等)的文本分类.总共有以下系列: word2vec预训练词向量 te ...
- 文本分类实战(九)—— ELMO 预训练模型
1 大纲概述 文本分类这个系列将会有十篇左右,包括基于word2vec预训练的文本分类,与及基于最新的预训练模型(ELMo,BERT等)的文本分类.总共有以下系列: word2vec预训练词向量 te ...
- 【转载】最强NLP预训练模型!谷歌BERT横扫11项NLP任务记录
本文介绍了一种新的语言表征模型 BERT--来自 Transformer 的双向编码器表征.与最近的语言表征模型不同,BERT 旨在基于所有层的左.右语境来预训练深度双向表征.BERT 是首个在大批句 ...
- pytorch预训练模型的下载地址以及解决下载速度慢的方法
https://github.com/pytorch/vision/tree/master/torchvision/models 几乎所有的常用预训练模型都在这里面 总结下各种模型的下载地址: 1 R ...
随机推荐
- Java函数的学习
函数的定义 - 定义的位置:定义在类的内部 - 组成部分: 函数修饰符 类型 函数名(形式参数){ 局部变量: 注释: 函数体: } 函数的调用 - 调用函数时使用 : `函数名():` - 函数在执 ...
- CAS如何解决ABA问题
点赞再看,养成习惯,微信搜索「小大白日志」关注这个搬砖人. 文章不定期同步公众号,还有各种一线大厂面试原题.我的学习系列笔记. CAS如何解决ABA问题 什么是ABA:在CAS过程中,线程1.线程2分 ...
- web服务报错类型
401:无权限(HttpStatus.UNAUTHORIZED) 404:页面找不到 405:不支持get/post请求,如只支持get请求但传了post请求 400:请求格式错误,如不为null但传 ...
- SpringMVC 配置 & 初识 & 注解 &重定向与转发
初识 在web.xml 中注册DispatcherServlet <servlet> <servlet-name>springmvc</servlet-name> ...
- Matlab学习笔记 绘图
1.二维曲线(1)plot函数①plot函数的基本用法:plot(x,y),其中x和y分别用于存储x坐标和y坐标数据. >>x=[1,2,3]; >>y=[4,5,6]; &g ...
- 【java】错误: 找不到或无法加载主类 Test.class
在配置java环境完成时,在cmd中运行 java -version 可以运行,但是当运行 helloworld 文件时,报错. 两种情况 解决: 1.运行 java helloworld 而不是 ...
- SpringBoot如何优雅关闭(SpringBoot2.3&Spring Boot2.2)
SpringBoot如何优雅关闭(SpringBoot2.3&Spring Boot2.2) 优雅停止&暴力停止 暴力停止:像日常开发过程中,测试区或者本地开发时,我们并不会考虑项目关 ...
- Dnscat2隧道
0x01 前言 DNS是用来做域名解析的,是连接互联网的关键,故即使是企业内网,在防火墙高度关闭下,也有着很好的连通性,但是黑客却可以通过将其他协议的内容封装再DNS协议中,然后通过DNS请求和响 ...
- 安卓导航抽屉 Navigation Drawer 实现沉浸通知栏
在使用 Navigation Drawer Activity 模版的时候,遇到了通知栏无法完全沉浸的问题,尝试搜索一些现有的解决方法,但是或多或少都会存在一些问题,通过反复尝试找到找到了一种比较靠谱的 ...
- 为什么 Redis 要有哨兵机制?
作者:小林coding 计算机八股文刷题网站:https://xiaolincoding.com 大家好,我是小林. 这次聊聊,Redis 的哨兵机制. 提纲 为什么要有哨兵机制? 在 Redis 的 ...