2019年4月19日 星期五

C# TensorFlowWrapper:Emgu.TF

C#如果要導入TensorFlow套件

常見的有兩套TensorFlowSharp與Emgu.TF

本篇以Emgu.TF為主來做介紹


由於過去有使用過Emgu.CV套件

因此對於同團隊推出的Emgu.TF想必會比較熟悉

但由於Emgu.TF推出的時間還很早

目前每個小版本的更新都有不少的差異

官方提供的歷史版本可在此下載:

https://sourceforge.net/projects/emgutf/files/

GitHub與Nuget上的版本最新到1.13.1

https://github.com/emgucv/emgutf/releases/tag/1.13.1


雖然Emgu.TF在Nuget上可以參考

但如果想要多了解實作方式,還是建議下載Source Code來看

裡面有提供Example專案

這裡以Emgu.TF 1.8.0的版本介紹,因為裡面有兩組Example的專案

裡面提供了兩個範例專案

InceptionObjectRecognition、MultiboxPeopleDetection

Emgu.TF這個套件的好處是,幾乎檔案抓下來

就能直接編譯建置成功

這兩個執行專案若是第一次執行

會把所需的model進行下載

Model下載中


而這兩個範例的差別在於

InceptionObjectRecognition的範例是使用tensorflow_inception_graph.pb這個model

一次只辨識一張影像中的單一一個物件

如果你影像中只有一個物件,就適合這個model

文字顯示辨識出來的物件是什麼,機率是多高,花了多久的時間辨識~


MultiboxPeopleDetection的範例是使用multibox_model.pb這個model

可以偵測出影像中人的部分,並請把人的部分框選起來

但是似乎無法辨識其他物件?

只能框選出物件,但無法提供該物件是什麼類型


1.12的版本在Emgu.TF.Models.Shared專案中新增了一個model

MaskRcnnInceptionV2Coco.cs

目前最新版就提供這三種model類別

因此若是你想要使用其他的model,必須自己進行包裝

也就是本篇要介紹的內容


在要進行影像辨識時,通常需要兩個檔案

一個是物件辨識的model,一個是物件辨識的清單文件

常見的物件辨識的model在tensorflow官網可以找到

tensorflow detection model zoo

常見的分類就是SSD與Faster RCNN這兩個系列

在影像辨識常用的訓練集為COCO dataset

COCO dataset是一個open的資料集,提供的影像與標記物件相當豐富

所以大多數影像辨識的演算法都會優先採用此資料集做訓練

這樣與其他辨識演算法就能有比較客觀的比較基準


可以從剛剛COCO-trained models中下載一個比較輕量的model來做測試

ssd_mobilenet_v1_coco

下載後解壓縮如下圖,只需要其中的model檔frozen_inference_graph.pb


再來就是要下載COCO dataset的物件清單

可從tensorflow的github tensorflow model data

下載mscoco_label_map.pbtxt

(可以直接把此內容複製到記事本另存成mscoco_label_map.pbtxt.txt)


接下來就可以開始進行測試了

這邊用一個.net framework的主控台應用程式來示範

建立好專案後

先來用nuget參考所需的套件

包含Emgu.TF 1.13.1

參考的部分加入組件System.Drawing

Nuget套件與參考設定


由於需要讀取mscoco_label_map.pbtxt這個清單檔案

建議先放置在執行目錄下的Catalog資料夾

mscoco_label_map.pbtxt本身其實就是一個純文字檔

要自己寫parser去讀取裡面的內容也是可以

但這裡直接參考TensorFlowSharp的做法

這裡建立一個叫做CatalogUtil的類別,該類別請參考TensorFlowSharp

直接把TensorFlowSharp裡面的CatalogUtil的class借來用 XDD

接著把剛剛下載的model檔frozen_inference_graph.pb

也放在執行目錄下的model資料夾裡


接下來就開始寫程式了

整個main function就這樣而已


程式說明如下:

            string catalogPath = Path.Combine("catalog", "mscoco_label_map.pbtxt.txt");
            IEnumerable<CatalogItem> catalog = CatalogUtil.ReadCatalogItems(catalogPath);

            string modelFile = Path.Combine("model", "frozen_inference_graph.pb");
            byte[] model = File.ReadAllBytes(modelFile);

            Session TFSession = null;
            Graph TFGraph = new Graph();
            using (Emgu.TF.Buffer modelBuffer = Emgu.TF.Buffer.FromString(model))
            {              
                using (ImportGraphDefOptions options = new ImportGraphDefOptions())
                {
                    TFGraph.ImportGraphDef(modelBuffer, options);
                }
                TFSession = new Session(TFGraph);
            }

這邊主要的任務就是讀入剛剛所下載的mscoco_label_map.pbtxt.txt

以及frozen_inference_graph.pb

並且將tensorflow的session建立起來


接著就是讀取影像並且進行session.run去執行影像辨識

            Tensor image = ReadTensorFromImageFile("test.jpg");

            Tensor[] finalTensor = TFSession.Run(
                new Output[] { TFGraph["image_tensor"] }, 
                new Tensor[] { image },
                new Output[] { TFGraph["detection_boxes"], TFGraph["detection_scores"],                TFGraph["detection_classes"], TFGraph["num_detections"] }
                );

            var boxes = (float[,,])finalTensor[0].JaggedData;
            var scores = (float[,])finalTensor[1].JaggedData;
            var classes = (float[,])finalTensor[2].JaggedData;
            var num = (float[])finalTensor[3].Data;

            Image resultImage = Image.FromFile("test.jpg");

            resultImage = DrawBoxes(resultImage, boxes, scores, classes, 0.1, catalog);
            resultImage.Save("resultImage.jpg");

辨識後產生的結果則用boxes、scores、classes這幾個變數來儲存

num就是辨識出來的物件的數量


這邊額外寫了兩個function來協助讀取影像以及繪製結果
(藍色標記的ReadTensorFromImageFileDrawBoxes)

由於Emgu.TF.Model裡面提供的讀取影像的方法

使用上總是會有一些奇怪的問題

所以這裡調整了Emgu.TF.Model中ImageIO的方法來用,如下:

public static Tensor ReadTensorFromImageFile(String fileName, int inputHeight = -1, int inputWidth = -1, float inputMean = 0.0f, float scale = 1.0f, Status status = null)
        {

            using (StatusChecker checker = new StatusChecker(status))
            {
                var graph = new Graph();
                Operation input = graph.Placeholder(DataType.String);

                Operation jpegDecoder = graph.DecodeJpeg(input, 3); //dimension 3

                Operation floatCaster = graph.Cast(jpegDecoder, DstT: DataType.Float); //cast to float

                Tensor axis = new Tensor(0);
                Operation axisOp = graph.Const(axis, axis.Type, opName: "axis");
                Operation dimsExpander = graph.ExpandDims(floatCaster, axisOp); //turn it to dimension [1,3]

                Operation resized;
                bool resizeRequired = (inputHeight > 0) && (inputWidth > 0);
                if (resizeRequired)
                {
                    Tensor size = new Tensor(new int[] { inputHeight, inputWidth }); // new size;
                    Operation sizeOp = graph.Const(size, size.Type, opName: "size");
                    resized = graph.ResizeBilinear(dimsExpander, sizeOp); //resize image
                }
                else
                {
                    resized = dimsExpander;
                }

                Tensor mean = new Tensor(inputMean);
                Operation meanOp = graph.Const(mean, mean.Type, opName: "mean");
                Operation substracted = graph.Sub(resized, meanOp);

                Tensor scaleTensor = new Tensor(scale);
                Operation scaleOp = graph.Const(scaleTensor, scaleTensor.Type, opName: "scale");
                Operation scaled = graph.Mul(substracted, scaleOp);

                Operation finalCast = graph.Cast(scaled, DataType.Uint8, false, "finalCast");

                Session session = new Session(graph);
                Tensor imageTensor = Tensor.FromString(File.ReadAllBytes(fileName), status);
                Tensor[] imageResults = session.Run(new Output[] { input }, new Tensor[] { imageTensor },
                    new Output[] { finalCast });
                return imageResults[0];

            }
        }

主要與原本ImageIO.ReadTensorFromImageFile的差別就在紅字的部分

而在DrawBoxes的部分則也是參考了TensorFlowSharp的Example

並簡化了DrawBox的方法

static Image DrawBoxes(Image image, float[,,] boxes, float[,] scores, float[,] classes, double minScore, IEnumerable<CatalogItem> catalog)
        {
            var x = boxes.GetLength(0);
            var y = boxes.GetLength(1);
            var z = boxes.GetLength(2);

            float ymin = 0, xmin = 0, ymax = 0, xmax = 0;

            for (int i = 0; i < x; i++)
            {
                for (int j = 0; j < y; j++)
                {
                    if (scores[i, j] < minScore) continue;
                    for (int k = 0; k < z; k++)
                    {
                        var box = boxes[i, j, k];
                        switch (k)
                        {
                            case 0:
                                ymin = box;
                                break;
                            case 1:
                                xmin = box;
                                break;
                            case 2:
                                ymax = box;
                                break;
                            case 3:
                                xmax = box;
                                break;
                        }
                    }

                    int value = Convert.ToInt32(classes[i, j]);
                    CatalogItem catalogItem = catalog.FirstOrDefault(item => item.Id == value);
                    DrawBox(image, xmin, xmax, ymin, ymax, $"{catalogItem.DisplayName} : {(scores[i, j] * 100).ToString("0")}%");
                }
            }
            return image;
        }

DrawBoxes方法就是把boxes、scores、classes這幾個結果的陣列做處理

轉換成rectangle的座標位置、辨識率以及辨識物件名稱

接著DrawBox就是把image帶入,並透過.net framework的Graphics

把物件的資料畫在影像上,包含物件的框框、辨識物件的資訊

        public static Image DrawBox(Image image, float xmin, float xmax, float ymin, float ymax, string text = "", string colorName = "red")
        {
            var left = xmin * image.Width;
            var right = xmax * image.Width;
            var top = ymin * image.Height;
            var bottom = ymax * image.Height;

            using (Graphics graphics = Graphics.FromImage(image))
            {
                Color color = Color.FromName(colorName);
                Brush brush = new SolidBrush(color);
                Pen pen = new Pen(brush);

                graphics.DrawRectangle(pen, left, top, right - left, bottom - top);
                var font = new Font("Ariel", 12);
                SizeF size = graphics.MeasureString(text, font);
                graphics.DrawString(text, font, brush, new PointF(left, top - size.Height));
            }
            return image;
        }

整個測試的程式碼其實蠻簡單的

接著只要把測試的影像test.jpg放到執行目錄

執行完成後,即可看到結果儲存在resultImage.jpg

執行時若遇到下面的問題

表示該專案的執行目標平台不是設定在x64

到專案的屬性->建置 去設定即可
改成x64後就可正常執行

執行時會出現tensorflow的訊息

表示該環境是採用CPU進行運算處理

測試的原圖如下:


辨識後的結果如下:

以上就是利用Emgu.TF套件進行影像辨識

並參考TensorFlowSharp的一些方法來完成整個流程

用此方式,可以很快速的切換辨識的model

只要把執行目錄的model資料夾裡的model檔替換成其他model即可


不過Emgu.TF 僅能在CPU的環境運行

如果要在GPU上運作,請向Emgu.TF購買Commercial License
(其實很便宜,才一千多台幣而已)

而Emgu.TF的更新速度算是蠻快的,也一直持續有在更新

依照之前使用Emgu.CV的經驗

這應該會是個可以期待的tensorflow套件

4 則留言:

  1. 請問您這篇有放在Github上嗎?若沒有的話能麻煩您丟上去借我參考嗎?非常感謝!我剛入門嘗試了一下您的作法,再加入參考system drawing.dll 的地方發生錯誤"工作失敗,因為找不到AxPlme.exe",已經有安裝Windows SDK,不知道問題出在哪邊!

    回覆刪除
    回覆
    1. 你是用哪個版本的visual studio 以及建立的方案是.net framework哪個版本呢??

      刪除
  2. 請問在Graph TFGraph = new Graph();地方...........出現System.TypeInitializationException:''Emgu.TF.TfInvoke' 的類型初始設定式發生例外狀況。錯誤訊息....請問要如何處理

    回覆刪除
    回覆
    1. 文章內容有寫到,可能你設定的專案的執行目標平台不是設定在x64

      到專案的屬性->建置 去設定即可

      刪除