iT邦幫忙

0

Python與C#之間互相傳輸資料

  • 分享至 

  • xImage
  •  
  • 因為是直接將資料保存在記憶體中,並將記憶體的資訊傳遞給對方去存取,所以效率很高
  • 可以傳遞任何格式(包含圖片)
  • 只能在同一台電腦上執行,想要在不同電腦上傳輸資料請使用Socket
  • 這邊做一個簡單的範例,透過OpenCV提供載入圖片、顯示圖片、翻轉圖片與保存圖片的功能

建立一個cv.py檔

from multiprocessing import shared_memory # 使用Python的共享記憶體
import cv2 # pip install opencv-python
import numpy as np # pip install numpy

class ImageLoader:
    def __init__(self):
        # 因為Python有自己的記憶體管理機制,使得我們沒辦法像C++一樣直接存取到它的記憶體資料(因為會自動回收記憶體空間內的資料導致後續出現非法存取的例外訊息)
        # 所以我們需要使用Python的共享記憶體功能
        # 共享記憶體需要我們手動建立與刪除
        self.share_memory = dict()
        
    def CreateShareMemory(self, name, size):
        # 手動建立共享記憶體
        self.share_memory[name] = shared_memory.SharedMemory(create=True, size=size, name=name)
        
    def ClearAllShareMemory(self):
        # 手動刪除所有共享記憶體
        for key, value in self.share_memory.items():
            value.close()
            value.unlink()
        self.share_memory.clear()
    
    def ClearShareMemory(self, name):
        # 刪除指定的共享記憶體
        if (name in self.share_memory.keys()):
            self.share_memory[name].close()
            self.share_memory[name].unlink()
            del self.share_memory[name]
            
    def Load(self, path:str, share_name:str) -> tuple:
        image = cv2.imread(path) # 載入影像
        height, width, channels = image.shape
        
        share_frame =  np.ndarray((height, width, channels), dtype=image.dtype, buffer=self.share_memory[share_name].buf) # 建立一個np.ndarray,並指定該array的位置為共享記憶體空間的位置
        share_frame[:] = image[:] # 將載入的影像資料保存到共享記憶體空間中
        
        return int(height), int(width), int(channels) # 將影像的參數回傳
    
    def Show(self, window_name:str, share_name:str, height:int, width:int, channel:int):
        shape = (height, width, channel) # 影像的尺寸
        image = np.ndarray(shape, dtype=np.uint8, buffer=self.share_memory[share_name].buf) # 建立一個np.ndarray,並指定該array的位置為共享記憶體空間的位置
        cv2.imshow(window_name, image) # 顯示影像
    
    def Delay(self, delay):
        cv2.waitKey(delay)
    
    def CloseWindows(self):
        cv2.destroyAllWindows()
        
    def Flip(self, share_name_src:str, share_name_dst:str, direct:int, height:int, width:int, channel:int):
        '''
        direct=-1, Vertically and Horizontal flip
        direct=0, Vertically flip
        direct=1, Horizontal flip
        '''
        shape = (height, width, channel)
        image_share = np.ndarray(shape, dtype=np.uint8, buffer=self.share_memory[share_name_src].buf) # 建立一個np.ndarray,並指定該array的位置為共享記憶體空間的位置
        
        dst_share = np.ndarray(shape, dtype=np.uint8, buffer=self.share_memory[share_name_dst].buf) # 建立一個np.ndarray,並指定該array的位置為共享記憶體空間的位置
        dst = cv2.flip(image_share, direct) # 翻轉影像
        dst_share[:] = dst[:] # 將翻轉後的結果保存到共享記憶體中
        
    def SaveImage(self, filename:str, share_name:str, height:int=0, width:int=0, channel:int=0):
        origin_shape = (height, width, channel)
        image = np.ndarray(origin_shape, dtype=np.uint8, buffer=self.share_memory[share_name].buf) # 建立一個np.ndarray,並指定該array的位置為共享記憶體空間的位置
        cv2.imwrite(filename, image) # 保存圖片

接下來就是C#端

  1. 建立一個C#主控台應用程式專案,我命名為net,Framework是.NET 8.0
  2. 工具 > NuGet套件管理員(N) > 管理方案的NuGet套件(N) > 搜尋「pythonnet」安裝
  3. 或者,去PythonNET的網站下載DLL後放到專案內
  4. 我這邊示範載入圖片並顯示原始與翻轉後的圖片,然後將翻轉後的圖片保存
  5. 若要將圖片從共享記憶體中取出,請參考更下面的程式碼
using Python.Runtime;

namespace net
{
	internal class Program
	{
		static void Main(string[] args)
		{
			Console.WriteLine("Init Python");
			PythonInit();

			Console.WriteLine("Load Python Module");
			//module和class的類型必須是dyname不然編譯器不給過
			dynamic py_module = Py.Import("cv"); //傳入要載入的py檔名稱(不包含副檔名)
			dynamic py_class = py_module.ImageLoader();
			string src_name = "src_" + Guid.NewGuid(); //使用UUID功能,避免共享記憶體空間的名字重複
			string dst_name = "dst_" + Guid.NewGuid(); //使用UUID功能,避免共享記憶體空間的名字重複

			using (Py.GIL()) //所有呼叫Python端的程式都需要放在using(Py.GIL()){}內
			{
				/* 
				 * 呼叫Python端的CreateShareMemory()方法,建立共享記憶體
				 * 因為事先不知道圖片多大,所以先預設給予一個很大的記憶體空間
				 * 3840 * 2160 * 3 = 24,883,200 bits = 3,110,400 bytes = 3,037 KB = 2.966 MB
				 */
				py_class.CreateShareMemory(src_name, 3840 * 2160 * 3);
				py_class.CreateShareMemory(dst_name, 3840 * 2160 * 3);

				PyObject result = py_class.Load(@"G:\cat.jpg", src_name);
				int height = result[0].As<int>();
				int width = result[1].As<int>();
				int channels = result[2].As<int>();
				Console.WriteLine($"Height:{height}, Width:{width}, Channels:{channels}");

				py_class.Flip(src_name, dst_name, 1, height, width, channels); //翻轉影像

				//顯示圖片
				py_class.Show("Origin", src_name, height, width, channels);
				py_class.Delay(1);
				py_class.Show("Flip", dst_name, height, width, channels);
				py_class.Delay(3000); //先讓畫面顯示3000ms

				//保存圖片
				py_class.SaveImage(@"G:\cat_flip.jpg", dst_name, height, width, channels);

				py_class.CloseWindows(); //關閉所有顯示的視窗

				py_class.ClearAllShareMemory(); //釋放所有的共享記憶體空間
			}
			Console.WriteLine("Finish");
		}

		private static void PythonInit()
		{
			//因為我使用Anaconda來建立Python環境,所以這邊要設定該環境的路徑
			string pathToVirtualEnv = @"D:\OtherApp\anaconda3\envs\opencv"; //指定虛擬環境的位置
			
			//因為我的環境使用的是Python 3.12,可以在該虛擬環境資料夾下找到python312.dll這個檔案
			Runtime.PythonDLL = Path.Combine(pathToVirtualEnv, "python312.dll");
			
			//指定python terminal路徑,這個通常不需要修改
			PythonEngine.PythonHome = Path.Combine(pathToVirtualEnv, "python.exe");

			/*
			 * 設定Python的環境參數,如果有多個環境參數要使用分號「;」區隔
			 * 第一個「D:\\Project\\Python\\ToCSharp\\python」是我的cv.py檔案位置
			 * 「{pathToVirtualEnv}\\Lib\\site-packages;{pathToVirtualEnv}\\Lib;{pathToVirtualEnv}\\DLLs」這段要放在最後面,讓程式可以獲得虛擬環境中安裝好的其他Library
			 */
			PythonEngine.PythonPath = $"D:\\Project\\Python\\ToCSharp\\python;{pathToVirtualEnv}\\Lib\\site-packages;{pathToVirtualEnv}\\Lib;{pathToVirtualEnv}\\DLLs";

			// 執行Python初始化
			PythonEngine.Initialize();
		}
	}
}

將影像資料從Python端的共享記憶體讀取出來並保存到SkiaSharp的SKImage中的程式碼

class MemoryConvert
{
	/// <summary>
	/// 取得SKBitmap的H, W, C數值
	/// </summary>
	/// <param name="bitmap"></param>
	/// <returns></returns>
	public static int[] GetSKBitmapParams(SKBitmap bitmap)
	{
		int width = bitmap.Width;
		int height = bitmap.Height;
		int channel = 0;
		switch (bitmap.ColorType)
		{
			case SKColorType.Bgra8888:
				channel = 4;
				break;
			case SKColorType.Rgb888x:
				channel = 3;
				break;
			default:
				channel = 1;
				break;
		}
		return new int[] { height, width, channel };
	}

	/// <summary>
	/// 從指定的共享記憶體區讀取資料並儲存至SKBitmap中
	/// </summary>
	/// <param name="share_name">共享記憶體區名稱</param>
	/// <param name="width">Bitmap圖片寬度</param>
	/// <param name="height">Bitmap圖片高度</param>
	/// <param name="channel">Bitmap圖片通道數</param>
	/// <returns></returns>
	public static SKBitmap ShareMemory_To_SKBitmap(string share_name, int width, int height, int channel)
	{
		using (MemoryMappedFile mmf = MemoryMappedFile.OpenExisting(share_name))
		{
			using (MemoryMappedViewStream stream = mmf.CreateViewStream())
			{
				byte[] bytes = new byte[height * width * channel];
				stream.Read(bytes, 0, bytes.Length);

				SKColorType colorType;
				switch (channel)
				{
					case 4:
						colorType = SKColorType.Bgra8888;
						break;
					case 3:
						colorType = SKColorType.Rgb888x;
						break;
					default:
						colorType = SKColorType.Gray8;
						break;
				}

				SKBitmap bitmap = new SKBitmap(new SKImageInfo(width, height, colorType));
				IntPtr ptr = bitmap.GetPixels();
				Marshal.Copy(bytes, 0, ptr, bytes.Length);
				return bitmap;
			}
		}
	}

	/// <summary>
	///  將SKBitmap圖片資料寫入到指定的共享記憶體中
	/// </summary>
	/// <param name="bitmap">圖片Bitmap</param>
	/// <param name="share_name">共享記憶體區名稱</param>
	public static void SKBitmap_To_ShareMemory(SKBitmap bitmap, string share_name)
	{
		var shape = GetSKBitmapParams(bitmap);

		byte[] bytes = new byte[shape[0] * shape[1] * shape[2]];
		IntPtr ptr = bitmap.GetPixels();
		Marshal.Copy(ptr, bytes, 0, bytes.Length);

		using (MemoryMappedFile mmf = MemoryMappedFile.OpenExisting(share_name))
		{
			using (MemoryMappedViewStream stream = mmf.CreateViewStream())
			{
				stream.Write(bytes, 0, bytes.Length);
			}
		}
}

圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言