オウルです。
先日オンライン開催されたMicrosoft Build 2021に参加しました。全体を通して、素晴らしいセッションで満足でしたが、機械学習にあまり馴染みがない僕のようなアプリケーションデベロッパー向けに、基礎的なML.NETを取りあげてくれると嬉しいのになという思いは少しだけ。。。(因みにAIをテーマとするセッションは5つありました)
前回の↓ 記事から随分日があきましたが、今回はML.NETです。
MNISTデータセットで学習させたONNX形式の学習済みモデルを利用して手書き数字認識させてみます。よく機械学習の”Hello World”と言われるやつです。ただ個人的にPythonと比べるとML.NETを目にする機会は、まだまだ少ないと感じているので、誰かの役に立てたら幸いです。
language | .NET 5 | C# |
editor | VSCode |
ONNX形式の学習済みモデルで手書き数値を分類
MNISTデータセットで学習させたモデルを試すだけでは、あまり面白くないのでAI OCR 的要素を取り入れてみます。例えば、書類等によくある生年月日欄の手書き数字を認識させてみましょう。
関連パッケージのインストール
前処理する必要があるので、OpenCV の.NET用ラッパーであるOpenCVSharpとSixLabors.ImageSharpをML.NETと一緒にインストールします。
1 2 3 4 5 6 7 8 9 |
// ML.NET dotnet add package Microsoft.ML --version 1.5.5 dotnet add package Microsoft.ML.OnnxRuntime --version 1.7.0 dotnet add package Microsoft.ML.OnnxTransformer --version 1.5.5 // OpenCvSharp dotnet add package OpenCvSharp4.Windows --version 4.5.2.20210404 dotnet add package System.Drawing.Common --version 5.0.2 // SixLabors.ImageSharp dotnet add package SixLabors.ImageSharp --version 1.0.3 |
後、https://github.com/shimat/opencvsharp にあるようにOpenCvSharpExtern.dllが必要です。
解析画像の準備
今回は前提として各枠線の位置情報は予め分かっているものとします。
前処理
ここからはコードベースです。
枠線除去
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
/// <summary> /// 枠線を除去します /// </summary> /// <param name="source">今回準備した生年月日の画像</param> /// <param name="rect">各生年月日の枠線の位置情報</param> /// <returns>枠線除去したOpenCvSharp.Mat</returns> private Mat RemoveFrameBorder(Mat source, OpenCvSharp.Rect rect) { using (var target = new Mat(source, rect)) // ROI using (var gray = new Mat()) using (var bin = new Mat()) using (var gaussian = new Mat()) using (var dilate = new Mat()) { // グレースケール Cv2.CvtColor(target, gray, ColorConversionCodes.RGB2GRAY); // 二値化 Cv2.Threshold(gray, bin, 0, 255, ThresholdTypes.Binary | ThresholdTypes.Otsu); // 輪郭を検出 ※ネガポジ反転はしない var contours = new OpenCvSharp.Point[][] { }; var hierarchy = new HierarchyIndex[] { }; // Contour Retrieval Mode: https://docs.opencv.org/master/d9/d8b/tutorial_py_contours_hierarchy.html bin.FindContours(out contours, out hierarchy, RetrievalModes.Tree, ContourApproximationModes.ApproxSimple); // 枠線除去 var points = GetInFramePoints(contours, rect); // ちょっとパディング return new Mat(bin, ApplyRoiPadding(Cv2.BoundingRect(InputArray.Create(points)))); } } private OpenCvSharp.Point[] GetInFramePoints(OpenCvSharp.Point[][] contours, OpenCvSharp.Rect rect) { // 面積の閾値を定義 var areaThreshold = rect.Width * rect.Height * areaRatio; var index = contours.Select((c, i) => new { Index = i, Area = (c.Max(p => p.X) - c.Min(p => p.X)) * (c.Max(p => p.Y) - c.Min(p => p.Y)) }).Where(o => o.Area >= areaThreshold).OrderBy(o => o.Area).First().Index; return contours[index]; } private OpenCvSharp.Rect ApplyRoiPadding(OpenCvSharp.Rect rect) => new OpenCvSharp.Rect(rect.X + roiPadding, rect.Y + roiPadding, rect.Size.Width - roiPadding * 2, rect.Size.Height - roiPadding * 2); |
枠線を除去すると、こんな感じの画像になります。
数字部分のROI
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
/// <summary> /// 枠線を除去します /// </summary> /// <param name="source">今回準備した生年月日の画像</param> /// <param name="rect">各生年月日の枠線の位置情報</param> /// <returns>前処理した画像のバイト配列</returns> private byte[] PreProcess(Mat source, OpenCvSharp.Rect rect) { using (var target = RemoveFrameBorder(source, rect)) using (var gaussian = new Mat()) using (var bin = new Mat()) using (var bitwiseNotMat = new Mat()) using (var dilate = new Mat()) { // ノイズ除去: ガウシアンフィルタ Cv2.GaussianBlur(target, gaussian, new OpenCvSharp.Size(11, 11), 0); // 二値化 Cv2.Threshold(gaussian, bin, 0, 255, ThresholdTypes.Binary | ThresholdTypes.Otsu); // ネガポジ反転 Cv2.BitwiseNot(bin, bitwiseNotMat); // 膨張 8近傍のカーネル // https://shimat.github.io/opencvsharp_docs/html/c5e6c07a-feae-9588-e690-703911dd81dc.htm var kernel = new Mat(new OpenCvSharp.Size(3, 3), MatType.CV_8U); for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { kernel.Set<byte>(i, j, 1); } } Cv2.Dilate(bitwiseNotMat, dilate, kernel, iterations: 1); // ROI 数字 var num = new Mat(dilate, GetNumericContours(dilate)); return Centered(num); } } private OpenCvSharp.Rect GetNumericContours(Mat target) { var contours = new OpenCvSharp.Point[][] { }; var hierarchy = new HierarchyIndex[] { }; target.FindContours(out contours, out hierarchy, RetrievalModes.External, ContourApproximationModes.ApproxSimple); var minX = contours.Min(m => m.Min(n => n.X)); var minY = contours.Min(m => m.Min(n => n.Y)); var maxX = contours.Max(m => m.Max(n => n.X)); var maxY = contours.Max(m => m.Max(n => n.Y)); var w = maxX - minX; var h = maxY - minY; return new OpenCvSharp.Rect(minX, minY, w, h); } |
数字部分のROI画像は、こんな感じです。
うーん、ノイズがあるせいで、いまいち。。。進めます。
中央寄せ
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
private byte[] Centered(Mat target) { byte[] imageBytes = null; using (var shift = new Mat(28 * 4, 28 * 4, MatType.CV_8UC3, new Scalar(0, 0, 0))) using (var affin = new Mat(2, 3, MatType.CV_32F)) { var m = Cv2.Moments(target); var cx = (int)(m.M10 / m.M00); var cy = (int)(m.M01 / m.M00); var sx = shift.Cols / 2 - cx; var sy = shift.Rows / 2 - cy; affin.Set<float>(0, 0, 1); affin.Set<float>(0, 1, 0); affin.Set<float>(0, 2, sx); affin.Set<float>(1, 0, 0); affin.Set<float>(1, 1, 1); affin.Set<float>(1, 2, sy); Cv2.WarpAffine(target, shift, affin, new OpenCvSharp.Size(shift.Cols, shift.Rows)); using (var bmp = BitmapConverter.ToBitmap(shift)) using (var memory = new MemoryStream()) { bmp.Save(memory, System.Drawing.Imaging.ImageFormat.Jpeg); imageBytes = memory.ToArray(); } } return imageBytes; } |
中央寄せすると、こんな感じの画像になります。
やはりノイズが残ってます。改善の余地ありですね。進めます。
認識
前処理が終わったので認識させてみます。ONNX形式の学習済みモデルをダウンロードします。
数字認識
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
public void Predict(FileInfo file) { // ダウンロードしたmnist-8.onnxファイル using (var session = new InferenceSession(file.FullName)) { var inputNoneName = session.InputMetadata.First().Key; // 入力の次元 // https://github.com/onnx/models/tree/master/vision/classification/mnist#input // [1,1,28,28] // 1 ミニバッチ数 // 1 チャネル // 28 row height // 28 col width // Input Dimensions: // images are 28 x 28 with 1 channel of color (gray) // https://github.com/Microsoft/CNTK/blob/master/Tutorials/CNTK_103D_MNIST_ConvolutionalNeuralNetwork.ipynb var innodedims = session.InputMetadata.First().Value.Dimensions; using (var memory = new MemoryStream(File.ReadAllBytes(@"C:\work\images\birthday.png"))) using (var src = System.Drawing.Image.FromStream(memory)) using (var bitmap = new Bitmap(src)) using (var source = bitmap.ToMat()) { // 認識対象の位置情報を取得 ※固定 var rects = GetRectangles(); foreach (var rect in rects) { // 前処理 var bytes = PreProcess(source, rect); if (bytes == null) continue; // 28 * 28 を 1 * 784 の列ベクトルに変換 var dimensions = new float[28 * 28]; using (var img = SixLabors.ImageSharp.Image.Load(bytes)) { // 前処理で定数倍している画像を 28 * 28 に縮小 img.Mutate(x => x.Resize(28, 28)); for (var x = 0; x < img.Width; x++) { for (var y = 0; y < img.Height; y++) { dimensions[x + y * img.Width] = (img[x, y].R == 255) ? 1 : 0; } } } // ↓にサンプルコードが公開されています // https://www.onnxruntime.ai/docs/reference/api/csharp-api.html#getting-started // 入力テンソルオブジェクトを作成 var inputTensor = new DenseTensor<float>(dimensions, innodedims); var namedOnnxValues = new List<NamedOnnxValue> { NamedOnnxValue.CreateFromTensor(inputNoneName, inputTensor) }; using (var results = session.Run(namedOnnxValues)) { var resultScores = results.First().AsTensor<float>().ToArray(); Console.WriteLine($"■ number: {rect.X} score"); for (int i = 0, n = resultScores.Count(); i < n; i++) { Console.WriteLine($"[{i}]: {resultScores[i]}"); } var result = Array.IndexOf(resultScores, resultScores.Max()); Console.WriteLine($"result!!: {result}"); } } } } } |
認識結果
結果は、↓こちらです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
// 生年月日の数字 左⇒右の結果です ■ number: (1) score [0]: -1.116373 [1]: 4.6407003 [2]: 0.43338832 [3]: -2.3770866 [4]: -2.2209399 [5]: -2.3868465 [6]: -1.2873131 [7]: 1.9657172 [8]: -1.4473467 [9]: -1.0774299 result!!: 1 ■ number: (2) score [0]: -0.9914572 [1]: -5.685889 [2]: -2.1891682 [3]: -0.20879257 [4]: 0.36104172 [5]: -1.6539364 [6]: -1.7442024 [7]: -1.7713101 [8]: 1.3888083 [9]: 5.8576884 result!!: 9 ■ number: (3) score [0]: 0.7519719 [1]: -0.9140129 [2]: 2.7131197 [3]: -1.7276947 [4]: -3.6965015 [5]: -2.3668027 [6]: -3.3618855 [7]: -0.9741749 [8]: 4.3273945 [9]: 1.0259147 result!!: 8 ■ number: (4) score [0]: -3.3387058 [1]: 7.554684 [2]: -0.04300058 [3]: -8.034364 [4]: 3.3517547 [5]: -3.2145717 [6]: 0.36625934 [7]: 2.2963145 [8]: -1.4259893 [9]: -2.3320167 result!!: 1 |
久しぶりに『ゼロから作るDeep Learning ―Pythonで学ぶディープラーニングの理論と実装』をもう一度読んでみようかな。因みにこの本は、以前参加した機械学習の講義で講師をしてくださった大学の先生が、入門書として良書と薦めてくれた書籍です。