【Windows】共有メモリを使用してC++とPython間で画像の送受信を行う方法

C++とPython共有メモリで画像共有

PythonとC++の間でデータのやり取りをしたい場合があると思います。
単純なアプリならどちらかをDLL化してしまえば解決しますが、外部のAPIを使用していると片方には対応していなかったり、 処理が複雑でプロセスごとに切り分けたいなど、そういう場面ではプロセス間通信が必要になる場合があります。
ファイルを介してデータのやり取りを行う実装は簡単ですが、処理速度が遅いので今回は共有メモリを使用してC++⇔Python間のデータのやり取りを行う方法についてまとめました。
※共有メモリはOSの環境に依存するので、今回はWindows 11を想定しています。

1. C++のプロセス間通信

まずはC++側で共有メモリを使用したプロセス間通信を実装します。

1.1. Windows APIの使用

C++で共有メモリを使用するには、Windows APIの関数を呼び出す必要があります。
使用するAPIは以下の通りです。

HANDLE CreateFileMappingA(
  HANDLE                hFile,
  LPSECURITY_ATTRIBUTES lpFileMappingAttributes,
  DWORD                 flProtect,
  DWORD                 dwMaximumSizeHigh,
  DWORD                 dwMaximumSizeLow,
  LPCSTR                lpName
);
引数内容
hFileファイルへのハンドルを指定します。
共有メモリにファイルを使用しない
場合は「INVALID_HANDLE_VALUE」
を指定します。
lpFileMappingAttributesセキュリティ属性を指定します。
アクセス制御リストをして
できますが、デフォルトでいい
場合は「NULL」を指定します。
flProtect保護属性を指定します。
PAGE_READONLY:読み取り専用
PAGE_READWRITE:読み取り/書き込み
PAGE_WRITECOPY:書き込み専用
dwMaximumSizeHigh最大サイズの上位32bit
dwMaximumSizeLow最大サイズの下位32bit
lpName共有メモリの名前
LPVOID MapViewOfFile(
  HANDLE hFileMappingObject,
  DWORD  dwDesiredAccess,
  DWORD  dwFileOffsetHigh,
  DWORD  dwFileOffsetLow,
  SIZE_T dwNumberOfBytesToMap
);
引数内容
hFileMappingObjectファイルマッピングオブジェクトのハンドル
CreateFileMappingAの戻り値を指定
dwDesiredAccessアクセスの種類を指定します。
FILE_MAP_ALL_ACCESS:読み取り/書き込み
FILE_MAP_READ:読み取り専用
FILE_MAP_WRITE:こちらも読み取りと
書き込み両方できるが
FILE_MAP_ALL_ACCESSが
推奨されている
dwFileOffsetHighマッピング開始オフセットの上位32bit
dwFileOffsetLowマッピング開始オフセットの32bit
dwNumberOfBytesToMapファイルマッピングのバイト数
HANDLE OpenFileMappingA(
  DWORD  dwDesiredAccess,
  BOOL   bInheritHandle,
  LPCSTR lpName
);
引数内容
dwDesiredAccessアクセスの種類を指定します。
FILE_MAP_ALL_ACCESS:読み取り/書き込み
FILE_MAP_READ:読み取り専用
FILE_MAP_WRITE:
こちらも読み取り/書き込み両方できるが
FILE_MAP_ALL_ACCESSが推奨されている
bInheritHandleプロセスのハンドルを子プロセスに
継承させたい場合は「TRUE」
デフォルトは「FALSE」
lpName共有メモリの名前
BOOL UnmapViewOfFile(
  LPCVOID lpBaseAddress
);
引数内容
lpBaseAddressファイルマッピングビューのポインタ
MapViewOfFileの戻り値
BOOL CloseHandle(
  HANDLE hObject
);
引数内容
hObjectオブジェクトのハンドル
CreateFileMappingA(OpenFileMappingA)の戻り値

1.2. 送信側の実装

まずは送信側を実装します。
このコードでは先頭に送受信用のフラグ1Byte+データやり取り用の領域1024Byteで、先頭のフラグが1になったら受信側で共有メモリに書き込まれたデータを表示し、フラグを0に設定します。
送信側はフラグが0になったことを確認してデータのやり取りを完了します。

#include <iostream>
#include <windows.h>
#include <chrono>

#define D_SHARED_MEMORY_NAME    "SHARED_MEMORY"
#define D_SHARED_MEMORY_SIZE    1025
#define D_TIME_OUT              3000    //タイムアウト(ms)

using namespace std;

int main(void) {
    auto start = chrono::system_clock::now();
    auto end = chrono::system_clock::now();
    const char send_flg = 0x01;
    const char reset_flg = 0x00;

    // 共有メモリ作成
    HANDLE hMapFile = CreateFileMappingA(
        INVALID_HANDLE_VALUE, 
        NULL, 
        PAGE_READWRITE, 
        0, 
        D_SHARED_MEMORY_SIZE,
        D_SHARED_MEMORY_NAME
    );

    if (hMapFile == NULL) {
        cerr << "CreateFileMapping:Error\n";
        return -1;
    }

    // ファイルマッピングのビューをアドレス空間にマッピング
    LPVOID pBuf = MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, D_SHARED_MEMORY_SIZE);
    if (pBuf == NULL) {
        cerr << "MapViewOfFile:Error\n";
        CloseHandle(hMapFile);
        return -1;
    }

    cout << "入力待ち" << endl;

    string str_input;
    while (1) {
        cin >> str_input;
        if (str_input == "!end") {
            memset(((char*)pBuf) + 1, 0, D_SHARED_MEMORY_SIZE - 1);
            strcpy_s(((char*)pBuf) + 1, str_input.length() + 1, str_input.c_str());
            ((char*)pBuf)[0] = 0x01;
            break;
        }

        // 書き込み
        int len = str_input.length();
        if (len > D_SHARED_MEMORY_SIZE-1) {
            cerr << "サイズ超過:" << len << "\n";
            continue;
        }

        memset(((char*)pBuf) + 1, 0, D_SHARED_MEMORY_SIZE - 1);
        strcpy_s(((char*)pBuf) + 1, str_input.length()+1, str_input.c_str());
        ((char*)pBuf)[0] = 0x01;

        char check = 1;
        double elapsed = 0.0;
        start = chrono::system_clock::now();
        while (1) {
            Sleep(10);
            if (elapsed > D_TIME_OUT) {
                strcpy_s((char*)pBuf, D_SHARED_MEMORY_SIZE, &reset_flg);
                cerr << "タイムアウト\n";
                break;
            }
            if (((char*)pBuf)[0] == 0x00) break;
            end = chrono::system_clock::now();
            elapsed = chrono::duration_cast(end - start).count();
        }
    }

    cout << "終了" << endl;

    UnmapViewOfFile(pBuf);
    CloseHandle(hMapFile);

	return 0;
}

1.3. 受信側の実装

次に受信側を実装します。

#include <iostream>
#include <windows.h>
#include <chrono>

#define D_SHARED_MEMORY_NAME    "SHARED_MEMORY"
#define D_SHARED_MEMORY_SIZE    1025

using namespace std;

int main(void) {
    const char check_flg = 0x01;
    const char reset_flg = 0x00;

    HANDLE hMapFile = OpenFileMappingA(FILE_MAP_ALL_ACCESS, FALSE, D_SHARED_MEMORY_NAME);
    if (hMapFile == NULL) {
        std::cerr << "OpenFileMapping failed\n";
        return 1;
    }

    // ファイルマッピングのビューをアドレス空間にマッピング
    LPVOID pBuf = MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, D_SHARED_MEMORY_SIZE);
    if (pBuf == NULL) {
        cerr << "MapViewOfFile:Error\n";
        CloseHandle(hMapFile);
        return -1;
    }

    string str_input;
    while (1) {
        Sleep(10);
        if (((char*)pBuf)[0] == check_flg) {
            char recv_data[D_SHARED_MEMORY_SIZE - 1] = {};
            memcpy(recv_data, (char*)pBuf + 1, D_SHARED_MEMORY_SIZE - 1);

            string message(recv_data, strnlen(recv_data, D_SHARED_MEMORY_SIZE - 1));
            
            cout << message << endl;
            memset((char*)pBuf + 1, 0, D_SHARED_MEMORY_SIZE - 1);
            ((char*)pBuf)[0] = reset_flg;

            if (message == "!end") break;
        }
    }

    cout << "終了" << endl;

    UnmapViewOfFile(pBuf);
    CloseHandle(hMapFile);

    return 0;
}

1.4. 動作確認

実際に動作確認をしてみます。
画像のように、送信側で入力したデータを受信側で受け取ったことが分かります。

2. Pythonのプロセス間通信

次にPythonでプロセス間通信を実装してみます。

2.1. SharedMemoryの使用

Pythonで共有メモリを使用する方法はいくつかありますが、今回はこちらの関数を使用します。

class multiprocessing.shared_memory.SharedMemory(name=None, create=False, size=0)
引数内容
name共有メモリの名前
create新規作成なら「True」
既存の共有メモリにアクセスする場合は「False」
sizeバッファサイズ(Byte)

2.2. 送信側の実装

C++と同じように送信側を実装します。

import time
from multiprocessing import shared_memory
import time

D_SHARED_MEMORY_NAME = "SHARED_MEMORY"
D_SHARED_MEMORY_SIZE = 1025

mem = shared_memory.SharedMemory(name=D_SHARED_MEMORY_NAME, create=True, size=D_SHARED_MEMORY_SIZE)

while(True):
    input_data = input("入力待ち")
    data = input_data.encode("utf-8")

    if input_data == "!end":
        mem.buf[1:1+len(data)] = data
        mem.buf[0] = 0x01
        break

    if len(data) > (D_SHARED_MEMORY_SIZE-1):
        print("サイズ超過:"+len(data))
        continue

    mem.buf[1:1+len(data)] = data
    mem.buf[0] = 0x01
    
    start = time.time()
    while(1):
        time.sleep(0.01)
        if mem.buf[0] == 0x00:
            break
        end = time.time()
        if end-start > 3:
            print("タイムアウト")
            break

mem.close()
mem.unlink()

2.3. 受信側の実装

次に受信側を実装します。

from multiprocessing import shared_memory

D_SHARED_MEMORY_NAME = "SHARED_MEMORY"
D_SHARED_MEMORY_SIZE = 1025

mem = shared_memory.SharedMemory(name=D_SHARED_MEMORY_NAME)

# データ読み取り(バッファから先頭のデータを取得)
while(True):
    read_data = bytes(mem.buf[:D_SHARED_MEMORY_SIZE])
    if read_data[0] == 0x01:
        read_data = bytes(mem.buf[:D_SHARED_MEMORY_SIZE])
        message = read_data[1:].rstrip(b'\x00').decode('utf-8')
        print("受信メッセージ:", message)
        mem.buf[1:D_SHARED_MEMORY_SIZE] = b'\x00' * (D_SHARED_MEMORY_SIZE-1) 
        mem.buf[0] = 0x00
        if message == "!end":
            break

mem.close()
mem.unlink()

2.4. 動作確認

画像のように、データの送受信ができることが確認できました。

3. C++とPythonのプロセス間通信

今回の方法であれば、ソースコードを変更しなくてもC++とPythonのプロセス間通信が可能になります。
例えばC++側の送信用プログラムとPython側の受信用プログラムでも同じように動作します。
最後に、Python側でOpenCVで読み込んだ画像をC++側に送り、C++側で何か処理を加えてPython側に返して保存してみます。
※今回使用した画像はこちら

3.1. Python側実装(送信側)

まずはPython側を実装します。
画像を読み込んでから何かキーを押したら共有メモリにフラグと画像を送信し、フラグが0になったら再び共有メモリから画像を読みだして保存します。

import cv2
import numpy as np
from multiprocessing import shared_memory
import time

D_SHARED_MEMORY_NAME = "SHARED_MEMORY"
D_WIDTH  = 445
D_HEIGHT = 368

img = cv2.imread('test.png', cv2.IMREAD_COLOR)
h = D_HEIGHT
w = D_WIDTH
c = 3
memsize = 1 + h * w * c

mem = shared_memory.SharedMemory(name=D_SHARED_MEMORY_NAME, create=True, size=memsize)

input("入力待ち")

mem.buf[1:memsize] = img.tobytes()
mem.buf[0] = 0x01

start = time.time()
while(1):
    time.sleep(0.01)
    if mem.buf[0] == 0x00:
        img_recv = np.frombuffer(mem.buf[1:memsize], dtype=np.uint8).reshape((h, w, c))
        break
    end = time.time()
    if end-start > 3:
        print("タイムアウト")
        break

cv2.imwrite("result.png", img_recv)

del img_recv

mem.close()
mem.unlink()

3.2 C++側実装(受信側)

次にC++側を実装します。
共有メモリから画像を読み込んだらcv::Matに変換し、正方形を描画して再び共有メモリに書き込みます。

#include <iostream>
#include <windows.h>
#include <opencv2/opencv.hpp<

#define D_SHARED_MEMORY_NAME    "SHARED_MEMORY"
#define D_WIDTH                 445
#define D_HEIGHT                368
#define D_SHARED_MEMORY_SIZE    D_WIDTH*D_HEIGHT*3+1

using namespace std;
using namespace cv;

int main(void) {
    const char check_flg = 0x01;
    const char reset_flg = 0x00;

    HANDLE hMapFile = OpenFileMappingA(FILE_MAP_ALL_ACCESS, FALSE, D_SHARED_MEMORY_NAME);
    if (hMapFile == NULL) {
        std::cerr << "OpenFileMapping failed\n";
        return 1;
    }

    // ファイルマッピングのビューをアドレス空間にマッピング
    LPVOID pBuf = MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, D_SHARED_MEMORY_SIZE);
    if (pBuf == NULL) {
        cerr << "MapViewOfFile:Error\n";
        CloseHandle(hMapFile);
        return -1;
    }

    string str_input;
    while (1) {
        Sleep(10);
        if (((char*)pBuf)[0] == check_flg) {
            uchar* recv_data = new uchar[D_SHARED_MEMORY_SIZE - 1];
            memcpy(recv_data, (char*)pBuf + 1, D_SHARED_MEMORY_SIZE - 1);
            Mat mat_img(D_HEIGHT, D_WIDTH, CV_8UC3, recv_data);
            rectangle(mat_img, cv::Rect(D_WIDTH / 2 - D_WIDTH / 4, D_HEIGHT / 2 - D_HEIGHT / 4, D_WIDTH / 2, D_HEIGHT / 2), cv::Scalar(0, 0, 255), 2);
            memcpy((char*)pBuf + 1, mat_img.data, D_SHARED_MEMORY_SIZE - 1);
            delete[] recv_data;
            ((char*)pBuf)[0] = reset_flg;
            break;
        }
    }

    cout << "終了" << endl;

    UnmapViewOfFile(pBuf);
    CloseHandle(hMapFile);

    return 0;
}

3.3. 動作確認

結果は以下の通りです。
Pythonで読み込んだ画像がC++側で正方形が描画されて返ってきました。

今回は以上です。

4. 参考文献・参考サイト

・CreateFileMappingA 関数 (winbase.h)
https://learn.microsoft.com/ja-jp/windows/win32/api/winbase/nf-winbase-createfilemappinga

・CreateFileMapping(A)
https://chokuto.ifdef.jp/advanced/function/CreateFileMapping.html

・MapViewOfFile 関数 (memoryapi.h)
https://learn.microsoft.com/ja-jp/windows/win32/api/memoryapi/nf-memoryapi-mapviewoffile

・MapViewOfFile
https://chokuto.ifdef.jp/urawaza/api/MapViewOfFile.html

・OpenFileMappingA 関数 (winbase.h)
https://learn.microsoft.com/ja-jp/windows/win32/api/winbase/nf-winbase-openfilemappinga

・OpenFileMapping
https://www.tokovalue.jp/function/OpenFileMapping.htm

・UnmapViewOfFile 関数 (memoryapi.h)
https://learn.microsoft.com/ja-jp/windows/win32/api/memoryapi/nf-memoryapi-unmapviewoffile

・CloseHandle 関数 (handleapi.h)
https://learn.microsoft.com/ja-jp/windows/win32/api/handleapi/nf-handleapi-closehandle

・C++でフリープラットフォームな時間計測
https://qiita.com/yukiB/items/01f8e276d906bf443356

・multiprocessing.shared_memory — プロセス間で直接アクセス可能な共有メモリ
https://docs.python.org/ja/3.12/library/multiprocessing.shared_memory.html

コメント

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