最近在看微软开源的机器学习框架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站经典《华强买瓜》的更多相关文章

  1. ONNX预训练模型加载

    tvm官网中,对从ONNX预训练模型中加载模型的教程说明 教程来自于:https://docs.tvm.ai/tutorials/frontend/from_onnx.html#sphx-glr-tu ...

  2. [Pytorch]Pytorch加载预训练模型(转)

    转自:https://blog.csdn.net/Vivianyzw/article/details/81061765 东风的地方 1. 直接加载预训练模型 在训练的时候可能需要中断一下,然后继续训练 ...

  3. C#中的深度学习(五):在ML.NET中使用预训练模型进行硬币识别

    在本系列的最后,我们将介绍另一种方法,即利用一个预先训练好的CNN来解决我们一直在研究的硬币识别问题. 在这里,我们看一下转移学习,调整预定义的CNN,并使用Model Builder训练我们的硬币识 ...

  4. XLNet预训练模型,看这篇就够了!(代码实现)

    1. 什么是XLNet XLNet 是一个类似 BERT 的模型,而不是完全不同的模型.总之,XLNet是一种通用的自回归预训练方法.它是CMU和Google Brain团队在2019年6月份发布的模 ...

  5. 使用MxNet新接口Gluon提供的预训练模型进行微调

    1. 导入各种包 from mxnet import gluon import mxnet as mx from mxnet.gluon import nn from mxnet import nda ...

  6. 文本分类实战(十)—— BERT 预训练模型

    1 大纲概述 文本分类这个系列将会有十篇左右,包括基于word2vec预训练的文本分类,与及基于最新的预训练模型(ELMo,BERT等)的文本分类.总共有以下系列: word2vec预训练词向量 te ...

  7. 文本分类实战(九)—— ELMO 预训练模型

    1 大纲概述 文本分类这个系列将会有十篇左右,包括基于word2vec预训练的文本分类,与及基于最新的预训练模型(ELMo,BERT等)的文本分类.总共有以下系列: word2vec预训练词向量 te ...

  8. 【转载】最强NLP预训练模型!谷歌BERT横扫11项NLP任务记录

    本文介绍了一种新的语言表征模型 BERT--来自 Transformer 的双向编码器表征.与最近的语言表征模型不同,BERT 旨在基于所有层的左.右语境来预训练深度双向表征.BERT 是首个在大批句 ...

  9. pytorch预训练模型的下载地址以及解决下载速度慢的方法

    https://github.com/pytorch/vision/tree/master/torchvision/models 几乎所有的常用预训练模型都在这里面 总结下各种模型的下载地址: 1 R ...

随机推荐

  1. Istio实践(4)- 故障注入、熔断及ServiceEntry

    前言:接上一篇istio多服务应用部署及调用,本文介绍通过流量管理(故障注入.请求超时等)以及ServiceEntry外部服务部署应用 1.设置服务延迟 修改springbootapp-vs-v1.y ...

  2. 使用Proftpd支持FTP/SFTP权限管控

    简介 FTP 文件传输协议,FTP由FTP服务器(存储文件)和FTP客户端(通过FTP协议访问FTP服务器上的资源)组成 传输方式 主动模式(Port) 客户端与服务器端的TCP 21端口建立连接 - ...

  3. day02 真正的高并发还得看IO多路复用

    教程说明 C++高性能网络服务保姆级教程 首发地址 day02 真正的高并发还得看IO多路复用 本节目的 使用epoll实现一个高并发的服务器 从单进程讲起 上节从一个基础的socket服务说起我们实 ...

  4. 学习HTTP——HTTPS

    前言 因为工作需要,需要用到大量的关于 HTTP 协议的知识,目前掌握的关于 HTTP 请求以及协议的知识都是零散的,打算针对知识盲区系统的学习一些,理清概念. 为什么会出现 HTTPS 因为 HTT ...

  5. .NET Core 企业微信消息推送

    接口定义 应用支持推送文本.图片.视频.文件.图文等类型.请求方式:POST(HTTPS)请求地址: https://qyapi.weixin.qq.com/cgi-bin/message/send? ...

  6. 2┃音视频直播系统之浏览器中通过 WebRTC 拍照片加滤镜并保存

    一.拍照原理 好多人小时候应该都学过,在几张空白的纸上画同一个物体,并让物体之间稍有一些变化,然后连续快速地翻动这几张纸,它就形成了一个小动画,音视频播放器就是利用这样的原理来播放音视频文件的 播放器 ...

  7. 中国电子云数据库 Mesh 项目 DBPack 的实践

    作者:刘晓敏 2022 年 4 月,中国电子云开源了其云原生数据库 Mesh 项目 DBPack.该项目的诞生,旨在解决用户上云过程中面临的一些技术难点,诸如分布式事务.分库分表等.由于它数据库 Me ...

  8. 电机噪声之谐波分析(内附simulink中FFT分析的相关参数配置与解析)

    电机噪声之谐波分析(内附simulink中FFT分析的相关参数配置与解析) 目录 电机噪声之谐波分析(内附simulink中FFT分析的相关参数配置与解析) 写在前面 正文 电机噪声 谐波的产生 什么 ...

  9. Hbase数据库安装部署

    Hbase单机版安装 hbase介绍 HBase – Hadoop Database是一个分布式的.面向列的开源数据库,该技术来源于Chang et al所撰写的Google论文"Bigta ...

  10. git 本地项目关联新repo

    git initgit remote add origin repo-url git pull origin master --allow-unrelated-histories git add . ...