未来の自分へ知識貯金

ゆーきのエンジニアブログ

約5分 C#PythonVisual Studioプログラミング

C#とPython間で画像を受け渡す【クラス】

C#とPython間で画像を受け渡す【クラス】

C# でデスクトップアプリを作っていると、「この処理だけ Python でやりたい」という場面がよくあります。私の場合は OCR(文字認識)でした。画面まわりの UI は C#(Windows フォーム)が作りやすい一方、OCR や画像処理のライブラリは Python 側が圧倒的に充実しています。

そこでこの記事では、Python を Flask でローカル API サーバーにして、C# から HTTP 通信で画像を送り、処理結果を受け取るという構成を紹介します。それぞれ得意なことに専念させる、いいとこ取りの構成です。

なぜ HTTP でつなぐのか

C# から Python を使う方法は他にもあります。プロセス起動(Process.Start で python.exe を叩く)や、.NET 上で Python を動かすライブラリなどです。その中で HTTP を選んだ理由は次のとおりです。

  • 疎結合になる: C# 側は「JSON を POST する」だけ、Python 側は「JSON を返す」だけ。お互いの内部実装を知らなくてよい
  • 起動コストを払うのは最初の1回だけ: プロセス起動方式だと呼び出しのたびに Python の起動+ライブラリ読み込みが走るが、サーバー方式なら起動しっぱなしにできる
  • あとで分離できる: 同じ PC のローカル通信から、別マシンの GPU サーバーに Python 側を引っ越す、といった変更が URL の書き換えだけで済む

全体の構成

やり取りは次の形の JSON で行います。画像は JSON に載せられるように Base64 文字列に変換して詰めます。

{
  "command": "処理の種類など",
  "img_b64": "画像のBase64文字列",
  "result": "処理結果"
}

画像⇔Base64 文字列の変換(C# 側の Base64Encoder)は以前記事にしているので、そちらを参照してください。

C#側:HTTP クライアントクラス

まず、JSON を POST して JSON を受け取る汎用クラスを用意します。

using System.Net.Http;
using System.Text;
using System.Text.Json;

public class RestClient
{
    private static readonly string BASE_URL = "http://localhost:5000";

    private HttpClient client = new HttpClient();

    public async Task<S> PostAsync<F, S>(F reqData)
    {
        string url = $"{BASE_URL}/api/data";

        var jsonOptions = new JsonSerializerOptions();
        string jsonString = JsonSerializer.Serialize(reqData, jsonOptions);
        var content = new StringContent(jsonString, Encoding.UTF8, "application/json");

        HttpResponseMessage response = await client.PostAsync(url, content);
        if (response.IsSuccessStatusCode == false)
        {
            return default(S);
        }

        string responseBody = await response.Content.ReadAsStringAsync();
        S res = JsonSerializer.Deserialize<S>(responseBody, jsonOptions);
        return res;
    }
}

ポイントは2つです。

  • ジェネリックにしてあるF が送るデータの型、S が受け取るデータの型)ので、やり取りする JSON の形が増えてもこのクラスは書き換え不要です
  • HttpClient はフィールドに1つだけ持って使い回します。呼び出しのたびに new HttpClient() するコードをよく見かけますが、接続(ソケット)が解放されずに溜まっていき、連続で呼ぶと通信できなくなることがあります(後述のハマりポイント参照)

C#側:呼び出しコード

画面の一部をキャプチャして Python に送る例です。

public class tModel
{
    public string command { get; set; } = "";
    public string img_b64 { get; set; } = "";
    public string result { get; set; } = "";
}

private RestClient restClient = new RestClient();

private async void buttonCommTest_Click(object sender, EventArgs e)
{
    Rectangle screenRect = Program.App.Param.Regions[0]; // 画面上の領域
    using (Bitmap bmp = this.CaptureScreen(screenRect))
    {
        tModel req = new tModel();
        req.img_b64 = Base64Encoder.EncodeBitmap(bmp); // 画像をBase64文字列に変換
        tModel res = await this.restClient.PostAsync<tModel, tModel>(req);
        await Console.Out.WriteLineAsync(res.result);
    }
}

送信用クラス tModel のプロパティ名が小文字始まりなのは、Python 側の JSON のキー名とそのまま一致させるためです。C# の命名規約的には大文字始まりにして JsonSerializerOptionsPropertyNamingPolicy で変換する方がきれいですが、まずは一致させるのが手っ取り早いです。

なお CaptureScreen(画面キャプチャ)の実装はこちらの記事にあります。

Python側:Flask でサーバーを立てる

Python 側は Flask を使います。pip install flask opencv-python numpy で入ります。

まず、Python 側にも Base64⇔画像(OpenCV 形式)の変換クラスを用意します。

import base64
import cv2
import numpy as np

class Base64Encoder:
    @staticmethod
    def DecodeImage(b64_str):
        """ Base64文字列 -> OpenCV画像 """
        img_bytes = base64.b64decode(b64_str)
        array = np.frombuffer(img_bytes, dtype=np.uint8)
        return cv2.imdecode(array, cv2.IMREAD_COLOR)

    @staticmethod
    def EncodeImage(image):
        """ OpenCV画像 -> Base64文字列 """
        _, buffer = cv2.imencode('.png', image)
        return base64.b64encode(buffer).decode('utf-8')

サーバー本体はこれだけです。受け取った画像をデコードして、処理して、結果を詰めて返します。

from flask import Flask, jsonify, request

app = Flask(__name__)

# POSTリクエストに対するエンドポイント
@app.route('/api/data', methods=['POST'])
def post_api_data():
    request_data = request.get_json()

    # 受け取ったBase64文字列を画像に戻す
    img_b64 = request_data['img_b64']
    image = Base64Encoder.DecodeImage(img_b64)

    # ここで画像処理を行う(OCR・物体検出など好きな処理)
    result = ""

    # 結果を詰めて返す
    response_data = request_data
    response_data['img_b64'] = Base64Encoder.EncodeImage(image)
    response_data['result'] = result
    return jsonify(response_data)

if __name__ == '__main__':
    app.run(host='127.0.0.1', port=5000)

これを実行しておけば、C# 側からボタンを押すたびに画像が Python へ渡り、処理結果が返ってきます。「ここで画像処理を行う」の部分に、Python の得意な処理を好きなだけ書けるわけです。私はここに Tesseract の OCR 処理を入れていました。

ハマりポイント

  • HttpClient を毎回 new しない HttpClientusing で囲んで使い捨てにすると、閉じたはずの接続がしばらく残り続けます(TIME_WAIT)。タイマーで定期的に画像を送るような使い方だとソケットを食い潰して通信エラーになります。アプリ内で1インスタンスを使い回してください
  • 接続できない(HttpRequestException まず Python 側のサーバーが起動しているか、ポート番号が合っているかを確認。ブラウザで http://localhost:5000/api/data を開いて何か応答があるか(GET なので 405 Method Not Allowed が返れば OK。サーバーは生きています)
  • 日本語が文字化けする C# 側の StringContentEncoding.UTF8 を指定し忘れると、result に日本語を入れたときに化けます
  • 大きい画像で失敗する Base64 は元のサイズの約1.33倍になります。フルHDの全画面キャプチャ程度なら問題ありませんが、巨大な画像を頻繁に送るなら JPEG で圧縮してから Base64 化するとよいです
  • host='0.0.0.0' は必要なときだけ 同じ PC 内で完結するなら 127.0.0.1 にしておくのが安全です。0.0.0.0 にすると他の端末からも接続できるようになるので、別マシンから使う場合だけにしてください(Flask の開発サーバーは認証なしです)

まとめ

  • C# の UI と Python のライブラリ資産は、Flask のローカル API サーバーでつなぐと疎結合でいいとこ取りできる
  • 画像は Base64 文字列にして JSON に載せる
  • C# 側は HttpClient + System.Text.Jsonインスタンスは使い回す(最重要)
  • JSON のキー名は C# のプロパティ名と一致させる
  • ローカル完結なら 127.0.0.1 で待ち受ける

一度この形を作っておくと、「Python でしかできない処理」が出てくるたびにエンドポイントを足すだけで済むようになります。C# アプリから Python の資産を使いたい方の参考になれば幸いです。

この記事をシェアする

ゆーき
ゆーき

プログラマー経験10年以上。好きなプログラム言語はC#。使える言語はC++, Python他。好きな開発環境はVisual Studio。

プロフィールを見る →