【Mixing】在 C# 與 MATLAB 間交換影像資料

  • 450
  • 0
  • 2019-06-05

若同時使用兩種程式語言來開發專案,總是免不了資料交換這個步驟。
例如用 MATALB 畫張精美的圖片,再送到 C# 的展示給使用者,就是一個常見的流程。

就夏恩的經驗來看,交換一般的陣列比較容易。
如果要交換影像資料,事情就有點挑戰性了!

在正式開始前,先來介紹一下工作環境:

作業系統:Microsoft Windows 10 專業版

MATLAB​ 軟體版本:
  MATLAB                            Version 9.6      (R2019a)
  MATLAB Compiler          Version 7.0.1   (R2019a)
  MATLAB Compiler SDK    Version 6.6.1   (R2019a)

C# 作業平台:
  Visual Studio 2015 community
  emgucv 4.0.1.3373

其中使用 emgucv 內的 Image<Bgr, byte> 來操作影像,絕對會比使用 Bitmap 還要舒服!
有關 emgucv 的載點如下:emgucv 4.0.1.3373

下載完畢之後,順著安裝檔的指示按「下一步」即可。
同時請記得新增兩個環境變數:

C:\Emgu\emgucv-windesktop 4.0.1.3373\bin
C:\Emgu\emgucv-windesktop 4.0.1.3373\libs\x64

至此,EmguCV 已順利完成安裝!

情境1:在 MATLAB 中使用 C# 讀取影像

這是一個比較簡單的情境。

夏恩先來創建一個類別庫,名稱叫做 lib_test

創建類別之後,先把 Emgu.CV.World 加入參考:
該檔案的位置在:C:\Emgu\emgucv-windesktop 4.0.1.3373\bin\Emgu.CV.World.dll

加入參考後,就可以來寫一段簡單的程式:

// C# code
// 說明:供 MATLAB 使用的類別庫
//
// 2019.05.23 Shayne

using Emgu.CV;
using Emgu.CV.Structure;

// 請自行決定 namespace 的名稱
namespace lib_test
{
    public class Class1
    {
        // 建立讀取影像檔之函數:read_a_image
        public Image<Bgr, byte> read_a_image(string file_path)
        {
            return new Image<Bgr, byte>(file_path);
        }
    }
}

這段程式只有一個功能:就是接收一個路徑,接著讀取影像後回傳。
寫完後,按下建置專案

以夏恩為例,本專案創建時名稱為:lib_test,因此建置完畢後,
使用者可以在 ".\lib_test\lib_test\bin\Debug" 資料夾找到:lib_test.dll 這個檔案。

最後就是打開您的 MATLAB,要來使用這個檔案:
請注意,由於我們有用到 emgucv 的函數,因此務必要把 Emgu.CV.World.dll 和 lib_test.dll 放在同一個地方。

% MATLAB code
%
% 2019.05.23 Shayne

% 加入 lib_test.dll
% 請注意,加入 dll 檔時,"必須"使用絕對路徑,不然系統會報錯。
NET.addAssembly( [pwd, '\lib_test.dll'] );

% 測試影像
% 該影像從 Unsplash 取得,https://unsplash.com/
file_path = 'image.jpg';

% 宣告類別
myclass = lib_test.Class1;

% 調用函數,讀取影像檔
I = myclass.read_a_image(file_path);

程式執行到這邊,就可以得到讀取後的檔案,其格式為:Image<Emgu*CV*...略>。
看就知道這不是我們可以用的檔案,因此需要做二次加工!

%% 二次加工

% 取出I.Data,並轉成 uint8 格式
I1 = uint8(I.Data);

% 翻轉 BGR 為 RGB
I1 = flip(I1, 3);

imshow(I1)

各位觀眾!

當夏恩第一次發現 uint8(I.Data) 這個寫法的時候,感動得說不出話來。

這就是為什麼我會建議使用 Image<Bgr, byte> 這種影像格式,
若是使用 Bitmap,到這邊是沒有 Data 這個屬性可以用的!

唯一能用的就是 Bitmap 底下的 GetPixel 屬性,然後跑迴圈,一個、一個地把像素還原出來。
要在 MATLAB 內跑迴圈?真是勇氣可嘉,光是用想的就覺得累呀!

最後一句使用 flip 翻轉 BGR 影像為 RGB。
這是 C# 和 MATLAB 間的不同之處,影像三通道的順序顛倒,所以翻回來就搞定了!

以下為該程式的執行結果:

情境2:在 C# 中使用 MATLAB 讀取影像

跟情境1顛倒過來,現在我們要在 C# 內來操作影像讀取。
在此種情境下,MATLAB 程式將會完全獨立出來,不受軟體授權影響。

因此得先安裝好相對應的 MATLAB Runtime。
以本例來說,使用 Windows, R2019a 編譯,就得安裝對應的版本。

下載位置為:MATLAB Runtime

安裝完 MATLAB Runtime 後請記得重新啟動電腦。
若出現不能執行的問題,就新增一個環境變數到 Path 內:
C:\Program Files\MATLAB\MATLAB Runtime\v96\runtime\win64

搞定了環境的問題,我們就開始來寫程式吧!
首先寫一個 MATLAB 程式:

功能就是接收檔案路徑,讀檔後回傳影像。

function I = matlab_read_image(file_path)
% MATLAB code
% 說明:不簡單的讀檔程式
%
% 2019.05.23 Shayne

I = imread(file_path);

% 回傳給 C# 使用時,必須先調整影像格式:
% 1. 轉 RGB 為 BGR
I = flip(I, 3);
% 2. 調整陣列規格從 [R, C, 3] 為 [C, 3, R]
I = permute(I, [2, 3, 1]);

看過這支程式,有沒有發現什麼奇怪的地方?

沒錯,使用 imread 讀檔後,輸出時必須經過處理!
第一,用 flip 翻轉 RGB 為 BGR,這個很容易理解;
第二,用 permute 調整陣列規格。

這一步驟寫起來很簡單,一句話的事情而已。
實際上很困難,背後隱含相當多的資訊!

夏恩先針對 permute 這個函數說明一下:

permute 功能為重新排列N 維數組的維度,語法為:B = permute(A,order)。
按照向量 order 所指定的順序重新排列 A 的維度,order 的所有元素都必須是唯一的正整數實數值。

例如 A 是 5 x 4 x 3 的三維陣列,若我們想把維度改成 3 x 5 x 4 的話,則語法為:B = permute(A, [3 1 2])。
其中 3 表示第三個維度,且把第三個維度放在第一個位置;把第一個維度放到第二個位置,依此類推。

回到正題,在這裡是因為從 MATLAB 傳送影像給 C# 時候,陣列維度順序會改變。
原本在 MATLAB 中的影像維度為 [R, C, 3],送到了 C# 之後會變成 [3, R, C]。

下圖是在 C# 中執行到中斷點,我們可以藉此觀察資料型態在傳送後維度改變:

註:原圖大小為:8192 x 5461 x 3 pixel。

即「第三個維度」會跑到「第一個維度」,並把其他維度往後推。
此為兩種程式語言在交換資料時的特性,必須由我們手動調整程式克服。

等等,夏恩你要不要解釋一下這什麼概念?
呃...其實本恩也只是觀察到這個結果,實際原因可能要去問 MATLAB 總公司。
若要不負責任的推估的話,本恩認為是因為 MATLAB 與 C# 在讀取記憶體時,
有著不同的的體系,讀取順序不同,因此資料流傳遞時會受到影響。

總之,在 MATLAB 內要回傳影像時,預先調整影像維度,以利後續在 C# 中使用。
我們先把 [R, C, 3] 調整為 [C, 3, R],就是 I = permute(I, [2, 3, 1]); 這段程式。
當影像傳送時, [C, 3, R] 的「第三個維度」會挪移到第一個維度,也就成為我們需要的 [R, C, 3] 了!

到這邊,MATLAB 程式完成,緊接著把它打包成 dll 檔案。

以下為夏恩自製的簡易打包檔案流程:
如下圖,dll檔名為:my_dll,類別名稱為:myclass。

完成後,可以看到系統提示,並且可以在同一個資料夾底下找到檔案:
.\my_dll\for_redistribution_files_only\my_dll.dll

================== 我是分隔線 ==================

現在把畫面交還給 C#。

夏恩此時創建一個專案,名稱叫做 Csharp_example:

畫面不用太複雜,拉一個 picture_box 和一個 button 就好。
其中,button 的功能就是呼叫 MATLAB 的 dll 檔案,並在接收到資料後,更新 picture_box。

在寫程式之前,得先加入必要的參考,分別是:

1. Emgu.CV.World.dll
    如同前一節所述,位於:
    C:\Emgu\emgucv-windesktop 4.0.1.3373\bin\Emgu.CV.World.dll

2. MWArray.dll
    這個是 MATLAB 提供的,位置在:
    C:\Program Files\MATLAB\R2019a\toolbox\dotnetbuilder\bin\win64\v4.0\MWArray.dll

3. my_dll.dll
    至於這個嘛...應該不用夏恩多說,就自己放哪兒自己明白。

加入參考之後,要調整組態管理員,必須使用 x64 組態。
MATLAB 在 2016 年之後發行的版本,已經不支援 x86 的組態,請特別小心。

或許到這邊,您會覺得好麻煩,好多步驟...

千萬不要這麼覺得!

真正麻煩的只有為了要發文而截圖的夏恩而已。
實際上,若是自己作業,本恩配置上述那些東西根本不用一分鐘。
相信每位讀者在熟悉流程之後,閉著眼睛都能輕鬆搞定。

到這邊,終於可以來寫段 C# 程式了。

先複習一下,剛才打包 MATLAB 程式時,層級是這樣:

my_dll > myclass > matlab_read_image
(檔名)   > (類別名)  > (函數名)

所以正確的步驟應該為:
1. using 檔名
2. 類別名(myclass)宣告 
3. 套用 matlab_read_image 函數

請看程式如下:

// C# code
// 說明:調用 MATLAB 函數說明
//
// 2019.05.23 Shayne

using System;
using System.Windows.Forms;

// Emgu 相關檔案
using Emgu.CV;
using Emgu.CV.Structure;

// MATLAB 相關檔案
using MathWorks.MATLAB.NET.Arrays;

// 自製函式庫
using my_dll;

namespace Csharp_example
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        // 宣告自定義類別
        myclass my_cls = new myclass();

        private void button1_Click(object sender, EventArgs e)
        {
            // 開檔
            string filename = OpenFile();

            // 調用 MATLAB 函數
            MWArray I = my_cls.matlab_read_image(filename);

            // 使用 ToArray() 轉換成 byte[,,]
            byte[,,] I1 = (byte[,,])I.ToArray();

            // 轉成 Image<Bgr, byte>,以 Bitmap 格式更新 pictureBox1
            Image<Bgr, byte> I2 = new Image<Bgr, byte>(I1);
            pictureBox1.Image = I2.Bitmap;
        }

        private string OpenFile()
        {
            OpenFileDialog dialog = new OpenFileDialog();
            dialog.Title = "Select file";
            dialog.InitialDirectory = @".\";
            dialog.Filter = "影像檔|*.jpg";

            string filename;
            if (dialog.ShowDialog() == DialogResult.OK && dialog.FileName != null)
                filename = dialog.FileName;
            else
                filename = null;

            return filename;
        }
    }
}

在 C# 中調用 MATLAB 函數時,都是用 MWArray 系列的型態操作。
舉例來說,若函數回傳為整數或是浮點數,用 MWNumericArray;
若為字串則是 MWCharArray;結構用 MWStructArray 之類的。

此外雜七雜八難以分類的則用 MWArray。
在本例中,回傳值之型態為 uint8,就是使用該類別來承接。

把值接回來之後,先轉型成 C# 常用的 byte[,,],
最後透過 Image<Bgr, byte> 更新到 pictureBox1 上頭。

到這邊,執行程式就能看到結果:

在本範例中,是透過 MATLAB 讀檔之後,回傳影像給 C# 展示。
那如果想要「傳送」影像給 MATLAB 呢?

再延伸

這邊就接著情境2,再往下延伸一段。

現在新增另外一支 MATLAB 程式,叫做 matlab_show_image。
其程式寫法如下:

function matlab_show_image(I)
% MATLAB code
% 說明:不簡單的秀圖程式
%
% 2019.05.23 Shayne

% 承接 C# 影像時,必須先調整影像格式:

% 1. 調整陣列大小從 [C, 3, R] 為 [R, C, 3]
I = permute(I, [3, 1, 2]);

% 1. 轉 BGR 為 RGB
I = flip(I, 3);

imshow(I)

經歷過前面的洗禮,相信眼尖的讀者現在肯定一眼就看到重點了!

沒錯,就是 permuteflip 的使用時機!

有別於剛才是從 MATLAB 送給 C#,現在剛好是顛倒過來。
從 C# 送過來的時候,陣列維度會變成 [C, 3, R],因此我們必須先轉成 [R, C, 3]。

轉換完成後,才用 flip 來交換 RGB 影像通道。

程式寫完後,夏恩這邊把 matlab_show_image 與稍早提到的 matlab_read_image 包在一起。
然後在剛剛的 C# 程式中新增另外一個按鈕,button2。

連同之前的程式寫在一起:

private void button2_Click(object sender, EventArgs e)
{
    // 開檔
    string filename = OpenFile();

    // 讀檔
    Image<Bgr, byte> I = new Image<Bgr, byte>(filename);

    // 調用 MATLAB 函數
    my_cls.matlab_show_image((MWNumericArray)I.Data);
}

這段程式有別於 button1,是先在 C# 內讀取影像檔,在送到 MATLAB 函數中。
送過去的時候,夏恩提醒兩個要點:

1. 使用 Image<Bgr, byte> 讀檔,而非 Bitmap。
    理由就是 Bitmap 沒有 Data 屬性,又慢又難用!

2. 用 (MWNumericArray) 語法將 Data 屬性做轉換,即可傳送。
    如果沒有轉型這個步驟,程式是不會動的。

說明結束,現在可以來展示一下成果,點擊按鈕之後,MATLAB 的視窗就會彈跳出來:

小結

有關 C# 與 MATLAB 的混合編程的知識,應該算是冷門。
既要混編還要交換影像,更是冷門中的冷門。

畢竟有這種需求的人應該不太多。

夏恩會鑽研這些知識的動機,是因為身邊的夥伴都是用 C#.NET 或 VB.NET。
而本身主修的專業是 MATLAB & Python & 機器視覺。

欸?完全不相干啊!

是的,為了不要讓自己排擠所有人,所以才去學會這些東西。
如此一來,夏恩才可以散播各種程式供同事使用呀!

例如:

同事問:「欸,Shayne,你幫我寫一個 FFT 好不好,要多久?」
夏恩答:「FFT 嘛!一句話的事情而已,馬上來!」

您看,這感覺多好!

至於散播的到底是程式還是 bug,那就見仁見智了。