[.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 ...
随机推荐
- HCIE笔记-第七节-ICMP+ARP
ICMP重定向 作用:解决网络中的次优路径 触发:当某一个设备收到一个数据,进行转发时发现还要从该接口进行转发,于是触发ICMP重定向. 报文:Type=5,Code=0 ARP -- 地址解析协议 ...
- 2021.07.09 K-D树
2021.07.09 K-D树 前置知识 1.二叉搜索树 2.总是很长的替罪羊树 K-D树 建树 K-D树具有二叉搜索树的形态,对于每一个分类标准,小于标准的节点在父节点左边,大于标准的节点在父节点右 ...
- 《手把手教你》系列基础篇(九十三)-java+ selenium自动化测试-框架设计基础-POM设计模式实现-上篇(详解教程)
1.简介 上一篇介绍了POM的基础理论知识和非POM方式写脚本,这篇介绍利用页面工厂类(page factory)去实现POM,通过查看PageFactory类,我们可以知道它是一个初始化一个页面实例 ...
- bs4 & 二进制写入图片视频
适用于:数据都在网页源代码上,可以直接从中提取到对应数据 例子:北京新发地网 原理:拿到页面源代码的文本,交给BeautifulSoup解析,然后找到对应的标签,获取值 关键词:BeautifulSo ...
- [AcWing 801] 二进制中1的个数
点击查看代码 #include<iostream> using namespace std; int lowbit(int x) { return x & -x; } int ma ...
- ClickHouse 对付单表上亿条记录分组查询秒出, OLAP应用秒杀其他数据库
1. 启动并下载一个clickhouse-server, By default, starting above server instance will be run as default user ...
- wlile、 for循环和基本数据类型及内置方法
while + else 1.while与else连用 当while没有被关键字break主动结束的情况下 正常结束循环体代码之后执行else的子代码 """ while ...
- spring boot 默认日志替换为 log4j
移除默认日志 <dependency> <groupId>org.springframework.boot</groupId> <artifactId> ...
- 696. Count Binary Substrings - LeetCode
Question 696. Count Binary Substrings Example1 Input: "00110011" Output: 6 Explanation: Th ...
- css属性补充与JS数据类型
目录 溢出属性(overflow) 定位(position) z-index属性 opacity不透明度 JavaScript简介 变量与注释 数据类型 数值(Number) 字符串(String) ...