前言
最近在看微軟開源的機器學習框架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://t.ly/8iEk8
3、ffmpeg 用于視頻處理
下載地址:https://t.ly/ro0G
4、onnx預訓練udnie、super-resolution
udnie模型
下載地址:https://t.ly/0cUt
super-resolution模型
下載地址:https://t.ly/rnsi(需要解壓提取內部的onnx文件)
操作流程
1、首先我們將目標視頻(我這里就用B站經典短視頻《華強買瓜》為例)通過ffmpeg轉換成普通的一幀一幀的圖片
2、通過ML.NET加載【神經風格轉換預訓練模型】將每一幀原圖遷移到新的風格(藝術風格:udnie,抽象主義)。
3、由于2只能將圖片遷移到固定的240240格式,所以我們還需要通過ML.NET加載【超分辨率預訓練模型】將每一幀圖片進行超分辨率放大得到一張672672的圖片
4、通過ffmpeg將新的圖片合并成新的視頻
首先先看看成品(這里我轉換成gif方便演示):
原版視頻《華強買瓜》 1280*720
遷移后的抽象藝術版本 224*224

超分辨放大后的版本 672*672
接著我們看看如何一步一步來實現這個流程的
首先我們新建一個空白文件夾,將下載好的ffmpeg.exe和準備要處理的mp4視頻文件放進這個空白文件夾
接著我們需要從視頻中分離音頻文件,用于后期合成視頻時把音頻合成回去,否則視頻會沒有聲音,打開控制臺CD到剛才的目錄,執行命令:

然后我們從視頻中將每一幀拆解成一張一張的jpg圖片,這里首先要創建一個img子文件夾,否則會報錯。另外我選擇的r 25意思就是每秒25幀。如果你的視頻不是每秒25幀(右鍵-屬性-詳細信息-幀速率)則自行根據文件調整,最后合成的時候也需要按照這個幀率合成新的視頻:

到這里為止,我們就將圖片和音頻拆解出來了,接下來準備編碼,首先我們打開VS創建一個控制臺程序,引入nuget包:

接著我們創建一個一個類文件用于加載模型以及完成相應的圖片處理,在此之前我們需要使用安裝好的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轉換成inputTool.BitmapToTensor(originBmp,?224,?224,?ref?input,?true);//接著調用模型得到遷移后的張量outputusing?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轉換成inputTool.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);//接著調用模型得到超分重建后的張量outputusing?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.jpegvar?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
1?internal?class?Tool2?????{3?????????///?<summary>4?????????///?將bitmap轉換為tensor5?????????///?</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?????????????????????else26?????????????????????{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轉換成對應的bitmap36?????????///?</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?????????????????????else54?????????????????????{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轉YCbCr65?????????///?</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轉RGB75?????????///?</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?????}
總結
這一期整活基本到此就結束了,雖然只是調用了兩個小模型搞著玩,但是其實只要能搞到業界主流的開源預訓練模型,其實可以解決很多實際的商業場景,比如我們最近在使用美團開源的yolov6模型做一些圖像對象檢測來落地就是一個很好的例子這里就不再展開。另外微軟也承諾ML.NET的RoadMap會包含對預訓練模型的遷移學習能力,這樣我們可以通過通用的預訓練模型根據我們自己的定制化場景只需要提供小規模數據集即可完成特定場景的遷移學習來提高模型對特定場景問題的解決能力。今天就到這里吧,下次再見。
作者:a1010
原文:https://www.cnblogs.com/gmmy/p/16433499.html
END