【C#】【Python】実行中のPythonにリアルタイムにC#からデータを送る

はじめに

前回実行中のPythonからC#へ文字列をリアルタイムに送るプログラムを書きました。

今回はその逆のプログラムを書きたいと思います。つまりC#からPythonへデータをリアルタイムに送ります。


Pythonスクリプトを実行する時に引数を渡すことは簡単ですが今回は実行中のPythonにデータを送るのでやや難しくなります。

今回やったこと

画像をみてもらった方がわかりやすいと思います。

f:id:touch-sp:20211014131139p:plain:w600
左上がWPFで作成したWindowです。真ん中にスライダーを配置しています。
右下はPython内のOpneCVでWebカメラからの映像を出力しているものです。
左上のWindow内のスライダーを動かすと右下の映像のぼかし強度がリアルタイムに変わるようになっています。

環境

Windows 10 

Visual Studio Community 2019
.NET 5.0

Python 3.9.7
opencv-python==4.5.3.56

Pythonにインストールしたのは「opencv-python」のみです。pipでインストール可能です。

方法

Webカメラからの映像を画面出力する

import cv2

cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)

while True:
    ret, frame = cap.read()
    
    cv2.imshow('normal', frame)   

    if cv2.waitKey(1) & 0xFF == 27:
        break
    
cap.release()
cv2.destroyAllWindows()

上のスクリプトが動画を扱う時のopencv-pythonの基本形です。

Webカメラからの映像にぼかしを入れて画面出力する

画像にぼかしを入れるときには次の2行を追加するだけです。

strength = 10
frame = cv2.blur(frame, (strength, strength))

「strength」の値は任意です。数字が大きくなるとぼかし強度が強くなります。
最終的にはこうなります。

import cv2

strength = 10

cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)

while True:
    ret, frame = cap.read()
    
    frame = cv2.blur(frame, (strength, strength))
    cv2.imshow('blur', frame)   

    if cv2.waitKey(1) & 0xFF == 27:
        break
    
cap.release()
cv2.destroyAllWindows()

「cv2.imshow」の引数「'normal'」、「'blur'」はWindowタイトルなので何でも構いません。

ぼかし強度をインタラクティブに変更する

上記スクリプトの「strength」変数の値を変更するとぼかし強度が変更できます。
それをインタラクティブに行うには入力を受け付けるスレッドを別に立てる必要があります。
次の3つをスクリプトに追加しました。

from threading import Thread
def input_from_terminal():
    global strength
    while True: 
        input_word = input()
        if input_word == 'up' and strength < 50:
            strength += 10
        if input_word == 'down' and strength > 10:
            strength -= 10
thread1 = Thread(target=input_from_terminal)
thread1.daemon = True
thread1.start()

これでPythonスクリプトを実行したターミナル(コマンドプロンプトなど)に「up」、「down」と入力するとぼかし強度が変更されます。
最終的にはこうなります。

import cv2
from threading import Thread

strength = 10

def input_from_terminal():
    global strength
    while True: 
        input_word = input()
        if input_word == 'up' and strength < 50:
            strength += 10
        if input_word == 'down' and strength > 10:
            strength -= 10

thread1 = Thread(target=input_from_terminal)
thread1.daemon = True
thread1.start()

cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)

while True:
    ret, frame = cap.read()
    
    frame = cv2.blur(frame, (strength, strength))
    cv2.imshow('blur', frame)   

    if cv2.waitKey(1) & 0xFF == 27:
        break
    
cap.release()
cv2.destroyAllWindows()

この入力部分(「up」、「down」の入力)をC#側に任せてしまおうというのが今回の目的です。

C#から数字を受け取る

「up」、「down」ではなくC#側からは直接数字を受け取るようにしました。
それにより「input_from_terminal」はより簡潔に書けます。

def input_from_terminal():
    global strength
    while True: 
        input_word = input()   
        strength = int(input_word)

最終的にはこうなります。

import cv2
from threading import Thread

strength = 10

def input_from_terminal():
    global strength
    while True: 
        input_word = input()   
        strength = int(input_word)

thread1 = Thread(target=input_from_terminal)
thread1.daemon = True
thread1.start()

cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)

while True:
    ret, frame = cap.read()
    
    frame = cv2.blur(frame, (strength, strength))
    cv2.imshow('blur', frame)   

    if cv2.waitKey(1) & 0xFF == 27:
        break
    
cap.release()
cv2.destroyAllWindows()

Python側はこれで終了です。このスクリプトC#から実行します。


C#側のポイント

ポイントは3点です。

RedirectStandardInput = true,
StreamWriter myStreamWriter;
myStreamWriter = myProcess.StandardInput;
private void slider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
        {
            myStreamWriter.WriteLine(sl.Value.ToString());
        }

C#コード

メインコード

GUI部分を除くコードは最終的にこのようになりました。

using System;
using System.Diagnostics;
using System.IO;
using System.Windows;

namespace toPython
{
    public partial class MainWindow : Window
    {
        Process myProcess;
        StreamWriter myStreamWriter;

        public MainWindow()
        {
            InitializeComponent();
            WindowSettings();
        }
        private void PushStartButton(object sender, RoutedEventArgs e)
        {
            if (String.IsNullOrEmpty(selectedPythonPath)) return;

            PythonStart.IsEnabled = false;
            PythonStop.IsEnabled = true;
            sl.Value = 10;

            string myPythonApp = "cv.py";

            myProcess = new Process
            {
                StartInfo = new ProcessStartInfo(selectedPythonPath)
                {
                    UseShellExecute = false,
                    RedirectStandardInput = true,
                    CreateNoWindow = true,
                    Arguments = myPythonApp
                }
            };
            myProcess.Start();
            myStreamWriter = myProcess.StandardInput;
        }
        private void PushStopButton(object sender, RoutedEventArgs e)
        {
            myProcess.Kill();
            PythonStop.IsEnabled = false;
            PythonStart.IsEnabled = true;
        }
        private void slider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
        {
            myStreamWriter.WriteLine(sl.Value.ToString());
        }
    }
}

GUI部分のコード

using System.Windows;
using System.Windows.Controls;
using Microsoft.Win32;
using System.Windows.Media;
using System.Windows.Controls.Primitives;

namespace toPython
{
    public partial class MainWindow : Window
    {
        string selectedPythonPath;

        Button selectPythonButton = new Button()
        {
            Content = new Viewbox() { Child = new Label() { Content = "Select Python Environment" } },
            Margin = new Thickness(5)
        };
        Label selectedPythonLabel = new Label()
        {
            Content = "",
            VerticalAlignment = VerticalAlignment.Center,
            HorizontalAlignment = HorizontalAlignment.Left
        };
        Slider sl = new()
        {
            Foreground = Brushes.Black,
            Margin = new Thickness(10),
            Maximum = 50,
            Minimum = 10,
            TickFrequency = 10,
            TickPlacement = TickPlacement.BottomRight,
            IsSnapToTickEnabled = true
        };
        Button PythonStart = new()
        {
            Content = new Viewbox() { Child = new Label() { Content = "Start Python" } },
            Margin = new Thickness(20)
        };
        Button PythonStop = new()
        {
            Content = new Viewbox() { Child = new Label() { Content = "Stop Python" } },
            Margin = new Thickness(20),
            IsEnabled = false
        };

        private void WindowSettings()
        {
            Title = "Blur";

            selectPythonButton.Click += PushselectPythonButton;
            PythonStart.Click += PushStartButton;
            PythonStop.Click += PushStopButton;

            sl.ValueChanged += slider_ValueChanged;

            Grid mainGrid = Myfunc.NewGrid("1,5,3", "");
            Content = mainGrid;

            Grid upperGrid = Myfunc.NewGrid("", "3,7");
            mainGrid.AddChild(upperGrid, 0, 0, 1, 1);
            upperGrid.AddChild(selectPythonButton, 0, 0, 1, 1);
            upperGrid.AddChild(selectedPythonLabel, 0, 1, 1, 1);

            Grid centerGrid = Myfunc.NewGrid("1,1", "");
            mainGrid.AddChild(centerGrid, 1, 0, 1, 1);

            Label tickLabel = new Label()
            {
                Content = "弱 <=     => 強",
                VerticalAlignment = VerticalAlignment.Center,
                HorizontalAlignment = HorizontalAlignment.Center
            };
            Viewbox vb = new() { Child = tickLabel, Margin = new Thickness(20) };
            centerGrid.AddChild(vb, 0, 0, 1, 1);
            centerGrid.AddChild(sl, 1, 0, 1, 1);

            Grid lowerGrid = Myfunc.NewGrid("", "1,1");
            mainGrid.AddChild(lowerGrid, 2, 0, 1, 1);
            lowerGrid.AddChild(PythonStart, 0, 0, 1, 1);
            lowerGrid.AddChild(PythonStop, 0, 1, 1, 1);
        }
        private void PushselectPythonButton(object sender, RoutedEventArgs e)
        {
            var dialog = new OpenFileDialog();
            dialog.Filter = "Python実行ファイル (*.exe)|*.exe";
            if (dialog.ShowDialog() == true)
            {
                selectedPythonPath = dialog.FileName;
                selectedPythonLabel.Content = selectedPythonPath;
            }
        }
    }
}

MyFuncクラス

GUIを簡潔に書くためにMyFuncクラスを使用しています。
詳細はこちらを参照して下さい。

using System.Windows;
using System.Windows.Controls;

static class Myfunc
{
    public static Grid NewGrid(string row, string col) //引数はカンマ区切りで
    {
        Grid grid = new Grid();

        string[] grid_row = row.Split(",");
        string[] grid_col = col.Split(",");

        if (grid_row.Length > 1)
        {
            foreach (string each in grid_row)
            {
                if (int.TryParse(each, out int each_num))
                {
                    grid.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(each_num, GridUnitType.Star) });
                }
                else
                {
                    grid.RowDefinitions.Add(new RowDefinition() { Height = GridLength.Auto });
                }
            }
        }
        if (grid_col.Length > 1)
        {
            foreach (string each in grid_col)
            {
                if (int.TryParse(each, out int each_num))
                {
                    grid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(each_num, GridUnitType.Star) });
                }
                else
                {
                    grid.ColumnDefinitions.Add(new ColumnDefinition() { Width = GridLength.Auto });
                }
            }
        }
        return grid;
    }
    public static Grid AddChild(this Grid grid, UIElement element, int row, int col, int row_span, int col_span)
    {
        Grid.SetRow(element, row);
        Grid.SetRowSpan(element, row_span);
        Grid.SetColumn(element, col);
        Grid.SetColumnSpan(element, col_span);
        grid.Children.Add(element);

        return grid;
    }
}

さいごに

間違いや改善点があればコメント頂けましたら幸いです。