C#にpythonで作った処理を組み込む【pythonnetによるtensorflowモデルの組込】

C#,python,tensorflowのイメージ図 プログラミング
C#,python,tensorflowのイメージ図

概要

pythonで作った処理(主にはtensorflow, pytorch等のディープラーニング系)をGUIから動かして結果を表示したいということがあると思います。


C#は製造業で使用する検査システム等でよく使われるため、組み込みができると

自社で作ったAIモデルをベンダーさんへ提供して組み込んでもらうことできます。

tensorflowで作ったモデルを使ってアプリを作るところまでの手順を記事したいと思います。
本記事はtensorflowの組み込み編です。

セグメンテーションのモデルを作ってモデルに組み込んでみます。

準備編は以下の私の記事のリンクを参考にしてください。

C#にpythonで作った処理を組み込む【pythonnet準備+確認編 2021年2月最新】

前提条件

開発の環境
visualstudio2019 community Version 16.7.7
NuGet パッケージ マネージャー 5.7.0
Anaconda Navigator 1.9.12

C#の環境

.NET Framework 4.7.2

pythonの環境

Python 3.8.5
tensorflow 2.3.0
numpy 1.18.5

アプリの完成形

アプリ機能

・必要なライブラリのバージョン表示

・AIモデルの読み込み

・画像の読み込みと表示

・推論と結果画像の表示

製作するアプリの画面

C#の通常のコーディングではない部分のみを解説していきます。

セグメンテーションモデルの作成と保存

tensorflow2.0のセグメンテーションのチュートリアルをそのまま使いました。
Image segmentation

チュートリアルをそのまま実行するとセグメンテーションのモデルが作成できると思います。
学習後に以下のコードを実行してモデルを保存してください。

model.save('saved_model/segmentation_model')

これを後でC#のアプリで使います。

C#アプリにtensorflowの推論プログラムを組み込む

組み込むに当たりポイントがいくつかあります。

1. モデルの読み込み
2. C#で読み込んだbitmapをpythonのnumpy配列にする
3. pythonで推論したnumpy配列をC#のbitmapにする

というコードが必要になってきます。
1つずつ解説していきます。

モデルの読み込み

pythonnetさえ導入できればそこまで難しくありません。
tensorflowをインポートすればpythonとほぼ同じようにC#で実装できます。

アプリのモデル読み込みの時に”segmentation_model”フォルダを選択すると読み込むことができます。

//tensorflowのモジュールを入れるための変数
public static dynamic tf;
//tensorflowのインポート
tf = Py.Import("tensorflow");

// tensorflowのモデルのための変数
public static dynamic model;
//tensorflowのモデルを読み込む
model = tf.keras.models.load_model(@"saved_model/segmentation_model");

C#で読み込んだbitmapをpythonのnumpy配列にする

C#のフォームアプリで画像を読み込んで表示する場合、以下のように簡単実装できます。

PictureBox.ImageLocation = "画像のパス";

また、Bitmapとして表示されたPictureBoxから取得するには以下のように実装できます。

Bitmap img = new Bitmap(PictureBox.Image);

このBitmapデータをnumpyに変換する必要があります。

方法の1つとしてメモリ上のBitmapデータにアクセスしてnumpy配列にする方法があります。

メモリ上のBitmapデータにアクセスについてよくわからない方は
【C#】Bitmap画像データのメモリ構造
を読むとイメージがつかめると思います。

メモリ上の画像データを参照するには

ポインタの先頭:intptr

ストライド(1行あたりのメモリサイズ):strides
画像サイズ:width, height

が必要になってきます。

それを元に考える実装イメージ図は以下となります。

メモリ上でのC#からpythonへの画像渡し方

実際のコード

            // この画像を引き渡したい
            Bitmap c_img = new Bitmap(pictureBoxImage.Image);

            //空のBitmapDataを作成
            BitmapData bitmapData = null;
            try
            {
                // 画像をロック
                bitmapData = c_img.LockBits(
                   new Rectangle(new Point(0, 0), c_img.Size),
                   ImageLockMode.ReadOnly,
                   PixelFormat.Format24bppRgb
                   );

                // 自作のpythonモジュールを使用して推論する
                dynamic output = module.segmentation_prediction(
                    bitmapData.Scan0.ToInt64(), // 画像バッファの先頭ポインタを引き渡す
                    bitmapData.Width, //当然ながら画像サイズとストライドも必要
                    bitmapData.Stride,
                    bitmapData.Height,
                    model
                    );
def segmentation_prediction(img_ptr, width, stride, height, model):
    # ポインタからnumpyのarrayを作る
    img = np.ctypeslib.as_array((stride * height * ctypes.c_uint8).from_address(img_ptr)).reshape(height, width, 3)

注意ポイントがあります。

pixelformatの選択→RGBの8bit(uint8)の画像であれば PixelFormat.Format24bppRg

他のピクセルフォーマット(モノクロ画像など)はその他のPixelformatを参考にしてみてください。

pythonで推論したnumpy配列をC#のbitmapにする

先ほど逆の処理となりますが

ポインタの先頭:intptr

ストライド(1行あたりのメモリサイズ):strides
画像サイズ:width, height

が必要なことには変わりありません。

それを元に考える実装イメージ図は以下となります。

pythonからC#へのメモリ上での画像の渡し方

実際のコード

    # Cの配列化
    pred_mask_c = pred_mask.ctypes.data_as(ctypes.POINTER(((ctypes.c_uint8 * pred_mask.shape[2]) * pred_mask.shape[1]) * pred_mask.shape[0])).contents
    # 配列の先頭メモリ
    pred_mask_c_address = ctypes.addressof(pred_mask_c)

    #画像のサイズ
    height_int = ctypes.c_uint32(pred_mask.shape[0]).value
    width_int = ctypes.c_uint32(pred_mask.shape[1]).value
    #画像のストライド
    strides_int = ctypes.c_uint32(pred_mask.strides[0]).value

    return height_int, width_int, strides_int, pred_mask_c_address
                // 自作のpythonモジュールを使用して推論する
                dynamic output = module.segmentation_prediction(
                    bitmapData.Scan0.ToInt64(), // 画像バッファの先頭ポインタを引き渡す
                    bitmapData.Width, //当然ながら画像サイズとストライドも必要
                    bitmapData.Stride,
                    bitmapData.Height,
                    model
                    );

                // 各出力をC#の型に入れる
                Int32 height_int = output[0];
                Int32 width_int = output[1];
                Int32 strides_int = output[2];
                Int64 img_address = output[3];

                // IntPtrにする
                IntPtr int_ptr = new IntPtr(img_address);

                //メモリアドレスから直接bitmapとして定義
                Bitmap img_out = new Bitmap(
                        width_int,
                        height_int,
                        strides_int,
                        PixelFormat.Format24bppRgb,
                        int_ptr);

全体コード

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Drawing.Imaging;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.IO;
using System.Runtime.InteropServices;

using Python.Runtime;

namespace c_charp_python_TF
{
    public partial class Form : System.Windows.Forms.Form
    {
        /// pythonのライブラリのための共有の変数
        public static dynamic np;
        public static dynamic cv2;
        public static dynamic tf;
        public static dynamic ctypes;
        public static dynamic module;

        // tensorflowのモデルのための変数
        public static dynamic model;

        /// <summary>
        /// プロセスの環境変数PATHに、指定されたディレクトリを追加する(パスを通す)。
        /// </summary>
        /// <param name="paths">PATHに追加するディレクトリ。</param>
        public static void AddEnvPath(params string[] paths)
        {
            var envPaths = Environment.GetEnvironmentVariable("PATH").Split(Path.PathSeparator).ToList();
            foreach (var path in paths)
            {
                if (path.Length > 0 && !envPaths.Contains(path))
                {
                    envPaths.Insert(0, path);
                }
            }
            Environment.SetEnvironmentVariable("PATH", string.Join(Path.PathSeparator.ToString(), envPaths), EnvironmentVariableTarget.Process);
        }
        public Form()
        {
            InitializeComponent();
        }

        private void Form_Load(object sender, EventArgs e)
        {
            // *-------------------------------------------------------*
            // * python環境の設定
            // *-------------------------------------------------------*

            // python環境にパスを通す
            // TODO: 環境に合わせてパスを直すこと
            var PYTHON_HOME = Environment.ExpandEnvironmentVariables(@"C:\Users\haruka\Anaconda3\envs\tensorflow-2-3-0");

            // pythonnetが、python本体のDLLおよび依存DLLを見つけられるようにする
            AddEnvPath(
              PYTHON_HOME,
              Path.Combine(PYTHON_HOME, @"Library\bin")
            );

            // python環境に、PYTHON_HOME(標準pythonライブラリの場所)を設定
            PythonEngine.PythonHome = PYTHON_HOME;

            // python環境に、PYTHON_PATH(モジュールファイルのデフォルトの検索パス)を設定
            PythonEngine.PythonPath = string.Join(
              Path.PathSeparator.ToString(),
              new string[] {
                  PythonEngine.PythonPath,// 元の設定を残す
                  Path.Combine(PYTHON_HOME, @"Lib\site-packages"), //pipで入れたパッケージはここに入る
                  Path.Combine(@"C:\Users\haruka\source\repos\c_charp_python_TF\c_charp_python_TF"), //自分で作った(動かしたい)pythonプログラムの置き場所も追加
              }
            );
            using (Py.GIL())
            {
                // pythonのライブラリをインポートして変数に格納
                cv2 = Py.Import("cv2");
                np = Py.Import("numpy");
                tf = Py.Import("tensorflow");
                ctypes = Py.Import("ctypes");
                module = Py.Import("module");
                // バージョンの情報を変数に格納
                dynamic cv2_version = cv2.__version__;
                dynamic np_version = np.__version__;
                dynamic tf_version = tf.__version__;
                // GPU有り無しの確認
                dynamic GPU_check = module.tensorflow_gpu_check();

                // ラベルに表示
                labelOpencvVersion.Text = "opencvバージョン:" + cv2_version.ToString();
                labelNumpyVersion.Text = "numpyバージョン:" + np_version.ToString();
                labelTFversion.Text = "tensorflowバージョン:" + tf_version.ToString();
                labelGPUCheck.Text = "GPUありなし:" + GPU_check;
            }
        }

        private void buttonModelLoad_Click(object sender, EventArgs e)
        {
            FolderBrowserDialog folderBrowserDialog = new FolderBrowserDialog();

            // ダイアログの説明文
            folderBrowserDialog.Description = "フォルダを選択してください。";

            // デフォルトのフォルダ
            folderBrowserDialog.SelectedPath = @"C:\Users\haruka\Documents\pythonnet";

            // 「新しいフォルダーの作成する」ボタンを表示する(デフォルトはtrue)
            folderBrowserDialog.ShowNewFolderButton = false;

            //ダイアログを表示する
            if (folderBrowserDialog.ShowDialog() == DialogResult.OK)
            {
                //tensorflowのモデルを読み込む
                model = tf.keras.models.load_model(folderBrowserDialog.SelectedPath);
                //ラベルに表示
                labelModelLoadResult.Text = "読み込み:完了";
            }
            else
            {
                MessageBox.Show("キャンセルされました。",
                                "エラー",
                                MessageBoxButtons.OK,
                                MessageBoxIcon.Error);
            }

            // オブジェクトを破棄する
            folderBrowserDialog.Dispose();
        }

        private void buttonImageLoad_Click(object sender, EventArgs e)
        {
            OpenFileDialog ofDialog = new OpenFileDialog();

            // デフォルトのフォルダを指定する
            ofDialog.InitialDirectory = @"C:\Users\haruka\Documents\pythonnet";

            //ダイアログのタイトルを指定する
            ofDialog.Title = "画像ファイルを選択";

            //ダイアログを表示する
            if (ofDialog.ShowDialog() == DialogResult.OK)
            {
                //画像の大きさをPictureBoxに合わせる
                pictureBoxImage.SizeMode = PictureBoxSizeMode.Zoom;
                pictureBoxImage.ImageLocation = ofDialog.FileName;
                labelImageLoadResult.Text = "読み込み:完了";
            }
            else
            {
                MessageBox.Show("キャンセルされました。",
                                "エラー",
                                MessageBoxButtons.OK,
                                MessageBoxIcon.Error);
            }

            // オブジェクトを破棄する
            ofDialog.Dispose();
        }

        private void buttonPrediction_Click(object sender, EventArgs e)
        {
            // この画像を引き渡したい
            Bitmap c_img = new Bitmap(pictureBoxImage.Image);

            //空のBitmapDataを作成
            BitmapData bitmapData = null;
            try
            {
                // 画像をロック
                bitmapData = c_img.LockBits(
                   new Rectangle(new Point(0, 0), c_img.Size),
                   ImageLockMode.ReadOnly,
                   PixelFormat.Format24bppRgb
                   );

                // 自作のpythonモジュールを使用して推論する
                dynamic output = module.segmentation_prediction(
                    bitmapData.Scan0.ToInt64(), // 画像バッファの先頭ポインタを引き渡す
                    bitmapData.Width, //当然ながら画像サイズとストライドも必要
                    bitmapData.Stride,
                    bitmapData.Height,
                    model
                    );

                // 各出力をC#の型に入れる
                Int32 height_int = output[0];
                Int32 width_int = output[1];
                Int32 strides_int = output[2];
                Int64 img_address = output[3];

                // IntPtrにする
                IntPtr int_ptr = new IntPtr(img_address);

                //メモリアドレスから直接bitmapとして定義
                Bitmap img_out = new Bitmap(
                        width_int,
                        height_int,
                        strides_int,
                        PixelFormat.Format24bppRgb,
                        int_ptr);

                //画像の大きさをPictureBoxに合わせる
                pictureBoxPred.SizeMode = PictureBoxSizeMode.Zoom;
                pictureBoxPred.Image = img_out;

            }
            finally
            {
                // ロックした画像を解放
                if (bitmapData != null) c_img.UnlockBits(bitmapData);
            }
        }
    }
}
from io import StringIO
import tensorflow as tf
import numpy as np
import ctypes.util
from ctypes import *
import cv2

def tensorflow_gpu_check():
    # GPUの使用の確認
    physical_devices = tf.config.list_physical_devices("GPU")
    # リストに何も入っていなければGPUなし
    if not physical_devices:
        GPU_result = "GPUなし"
    else:
        GPU_result = "GPUあり"
    return GPU_result

def segmentation_prediction(img_ptr, width, stride, height, model):
    # ポインタからnumpyのarrayを作る
    img = np.ctypeslib.as_array((stride * height * ctypes.c_uint8).from_address(img_ptr)).reshape(height, width, 3)
    # 配列をコピーする→別のメモリに格納
    img_out = img.copy()
    # BGRの順番なのでRGBにする
    img_out = cv2.cvtColor(img_out, cv2.COLOR_BGR2RGB)
    # モデルの入力のサイズにリサイズする
    img_out = cv2.resize(img_out, (128, 128))
    # 次元追加と正規化
    img_out = np.expand_dims(img_out, axis=0) / 255.
    # 推論
    pred = model.predict(img_out)
    # 各ピクセルのクラスを計算
    pred_mask = np.argmax(pred, axis=-1)[0]
    # クラスは0,1,2なので2で割ることで正規化
    pred_mask = pred_mask / 2
    # 0~255にしてuint8に変換+元画像サイズへリサイズ
    pred_mask = pred_mask * 255
    pred_mask = pred_mask.astype("uint8")
    pred_mask = cv2.resize(pred_mask, (width, height))
    # 疑似カラー化
    pred_mask = cv2.applyColorMap(pred_mask, cv2.COLORMAP_JET)
    # Cの配列化
    pred_mask_c = pred_mask.ctypes.data_as(ctypes.POINTER(((ctypes.c_uint8 * pred_mask.shape[2]) * pred_mask.shape[1]) * pred_mask.shape[0])).contents
    # 配列の先頭メモリ
    pred_mask_c_address = ctypes.addressof(pred_mask_c)

    #画像のサイズ
    height_int = ctypes.c_uint32(pred_mask.shape[0]).value
    width_int = ctypes.c_uint32(pred_mask.shape[1]).value
    #画像のストライド
    strides_int = ctypes.c_uint32(pred_mask.strides[0]).value

    return height_int, width_int, strides_int, pred_mask_c_address

最後に

簡単な可視化ツールを作るにはやりやすい方法だと思いますのでぜひ試してみてください。

コメント

タイトルとURLをコピーしました