MENOU SDK MENOU SDK
MENOU SDK MENOU SDK
MENOUフォーラム

Plugin API ドキュメント

概要

MENOU SDK にて提供されるプラグイン機能を使用した撮像アプリケーションの実装方法について説明します。

プラグイン機能は MENOU-RN から外部の撮像アプリケーションを呼び出し、撮像アプリケーションで撮像した画像データを MENOU-RN に転送する機能です。

撮像アプリケーション実装方法

新規プロジェクト作成

C#のコンソールアプリプロジェクトを新規作成し、Windowsアプリケーションを指定します。

エリアセンサカメラを使用する場合と、ラインセンサカメラを使用する場合で実装方法が異なります。

エリアセンサカメラ時

// Program.cs 

// Copyright (c) 2025 MENOU. All rights reserved.

using Menou.Menoute.Runtime.Plugin.AreaCamera;
using OpenCvSharp;
using System.Runtime.InteropServices;

/// <summary>
/// エリアカメラ用テストアプリ。
/// 任意のUVCカメラを使用する。
/// </summary>
internal class Program
{
    /// <summary>
    /// カメラID
    /// </summary>
    private const int CameraId = 0;

    /// <summary>
    /// 撮像デバイス。
    /// </summary>
    private static VideoCapture? CaptureDevice = null;

    /// <summary>
    /// 撮像失敗時のエラーコード。
    /// 0は正常を意味するため、0以外を指定する。
    /// </summary>
    private static readonly int CaptureErrorCode = -1;

    private static async Task Main(string[] args)
    {
#if DEBUG
        // デバッガーによるプロセスアタッチがされるまで待つ。
        while (!System.Diagnostics.Debugger.IsAttached)
        {
            Thread.Sleep(1);
        }
#endif

        var name = args[0]; // 画像転送用のメモリマップ名
        var sizeName = args[1]; // 画像サイズ転送用のメモリマップ名
        var appDataDirectory = args[2]; // アプリケーションデータディレクトリ(ログ保存用)
        string? parentProcessIdStr = null;
        if (args.Length > 3)
        {
            parentProcessIdStr = args[3]; // 呼び出し元のプロセスIDの文字列。
        }

        using var pluginImageDeviceCommunicator = new PluginImageDeviceCommunicator(name, sizeName, appDataDirectory, parentProcessIdStr);

        // 撮像デバイス初期化
        CaptureDevice = new VideoCapture();

        // カメラオープン
        // カメラIDが重複しないようにCameraIdを設定すること。
        if (CaptureDevice?.Open(CameraId) != true)
        {
            return;
        }

        // 撮像要求時のイベント登録。
        pluginImageDeviceCommunicator.CaptureRequested += OnCaptureRequested;

        // 終了要求時のイベント登録
        pluginImageDeviceCommunicator.TerminateRequested += OnTerminateRequested;

        // 開始
        await pluginImageDeviceCommunicator.StartAsync();
    }

    /// <summary>
    /// 撮像要求時の処理
    /// このメソッドはサンプルコードですので、適宜変更してください。
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="eventArgs"></param>
    private static void OnCaptureRequested(object? sender, ICaptureEventArgs eventArgs)
    {
        using var frame = new Mat();

        // 撮像
        CaptureDevice?.Read(frame);

        if (!frame.Empty()) // 撮像成功時
        {
            eventArgs.ImageWidth = frame.Width;
            eventArgs.ImageHeight = frame.Height;
            eventArgs.ImageChannels = frame.Channels();

            var data = new byte[eventArgs.ImageWidth * eventArgs.ImageHeight * eventArgs.ImageChannels];
            Marshal.Copy(frame.Data, data, 0, data.Length);
            eventArgs.ImageBytes = data;
        }
        else // 撮像失敗時
        {
            // エラーコードを指定する
            eventArgs.ErrorCode = CaptureErrorCode;
        }
    }

    /// <summary>
    /// 終了要求時の処理。
    /// このメソッドはサンプルコードですので、適宜変更してください。
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="eventArgs"></param>
    private static void OnTerminateRequested(object? sender, EventArgs eventArgs)
    {
        // 撮像デバイスの解放
        DisposeCaptureDevice();
    }

    /// <summary>
    /// 撮像デバイスの解放処理。
    /// </summary>
    private static void DisposeCaptureDevice()
    {
        CaptureDevice?.Dispose();
        CaptureDevice = null;
    }
}
  1. MENOU Plugin I/F を提供する名前空間をインポートします。

     using Menou.Menoute.Runtime.Plugin.AreaCamera;
    
  2. Plugin と MENOU-RN を通信する PluginImageDeviceCommunicator オブジェクトを生成します。

     var name = args[0]; // 画像転送用のメモリマップ名
     var sizeName = args[1]; // 画像サイズ転送用のメモリマップ名
     var appDataDirectory = args[2]; // アプリケーションデータディレクトリ(ログ保存用)
     string? parentProcessIdStr = null;
     if (args.Length > 3)
     {
         parentProcessIdStr = args[3]; // 呼び出し元のプロセスIDの文字列。
     }
     using var pluginImageDeviceCommunicator = new PluginImageDeviceCommunicator(name, sizeName, appDataDirectory, parentProcessIdStr);
    

    PluginImageDeviceCommunicator オブジェクトは不要になったタイミングで適宜解放(Dispose)してください。

  3. カメラを初期化、オープンします。以下はUVCカメラのオープン例です。

     CaptureDevice?.Open(CameraId)
    
  4. 撮像要求時に実行されるイベントを登録します。

    pluginImageDeviceCommunicator.CaptureRequested += OnCaptureRequested;
    

    撮像要求時処理では、撮像を行い、取得した撮像画像データおよび幅、高さ、チャンネル情報を ICaptureEventArgs オブジェクトにセットします。

    撮像に失敗した時は、任意のエラーコード(0以外)をセットします。

  5. 終了要求時に実行されるイベントを登録します。

    pluginImageDeviceCommunicator.TerminateRequested += OnTerminateRequested;
    

    終了要求時処理ではカメラのクローズ処理など、オブジェクトの解放処理を行います。

  6. MENOU-RNとの通信を非同期で開始します。

    await pluginImageDeviceCommunicator.StartAsync();
    

ラインセンサカメラ時

// Copyright (c) 2025 MENOU. All rights reserved.

using LineCamera.Omronsentech.Camera;
using Menou.Menoute.Runtime.Plugin.LineCamera;
using Menou.VI.Core.Logging;
using System.Collections.Concurrent;

namespace LineCamera.Omronsentech;

/// <summary>
/// プラグインデバイスのテストアプリ。
/// Omron Sentech社製ラインセンサカメラ用。
/// </summary>
internal class Program
{
    /// <summary>
    /// ラインセンサカメラ撮像設定。
    /// </summary>
    private static LineCameraCaptureConfig? Config;

    /// <summary>
    /// 撮像デバイス。
    /// </summary>
    private static GigeCamControl? GigeCaptureDevice = null;

    /// <summary>
    /// エラーコード。
    /// 0は正常を意味する。
    /// 1は画像が取得できなかった場合を意味する。
    /// それ以外は0,1以外を指定する。
    /// </summary>
    private static readonly int OpenErrorCode = -1;
    private static readonly int PrmErrorCode = -2;
    private static readonly int CaptureErrorCode = -3;

    /// <summary>
    /// 撮像画像一覧。
    /// </summary>
    private static ConcurrentQueue<CapturedImage>? CaptureImages;

    public static async Task Main(string[] args)
    {
#if DEBUG
                // デバッガーによるプロセスアタッチがされるまで待つ。
                while (!System.Diagnostics.Debugger.IsAttached)
                {
                    Thread.Sleep(1);
                }
#endif

        var name = args[0]; // メモリマップ名
        var appDataDirectory = args[1]; // アプリケーションデータディレクトリ(ログ保存用)
        var parentProcessIdStr = args[2]; // 呼び出し元のプロセスIDの文字列。

        using var pluginImageDeviceCommunicator = new PluginLineCameraImageDeviceCommunicator(name, appDataDirectory, parentProcessIdStr);

        CaptureImages = new ConcurrentQueue<CapturedImage>();

        // 初期化要求時のイベント登録。
        pluginImageDeviceCommunicator.InitializeRequested += OnInitializeRequested;

        // 撮像要求時のイベント登録。
        pluginImageDeviceCommunicator.CaptureRequested += OnCaptureRequested;

        // 撮像画像取得要求時のイベント登録。
        pluginImageDeviceCommunicator.CaptureImageRequested += OnCaptureImageRequested;

        // 撮像停止要求時のイベント登録。
        pluginImageDeviceCommunicator.CaptureStopRequested += OnCaptureStopRequested;

        // 終了要求時のイベント登録
        pluginImageDeviceCommunicator.TerminateRequested += OnTerminateRequested;

        // 開始
        await pluginImageDeviceCommunicator.StartAsync();
    }

    /// <summary>
    /// 初期化要求時の処理。
    /// このメソッド内でカメラの初期化、カメラのオープン処理を行ってください。
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="initializeEventArgs">初期化イベント引数。</param>
    /// <exception cref="NotImplementedException"></exception>
    private static void OnInitializeRequested(object? sender, IInitializeEventArgs initializeEventArgs)
    {
        if (initializeEventArgs == null)
            throw new ArgumentNullException(nameof(initializeEventArgs));

        // 撮像デバイス初期化
        GigeCaptureDevice = new GigeCamControl();

        // 撮像完了時のイベントを登録。
        GigeCaptureDevice!.CaptureEndEvent += CaptureEndEvent;

        // カメラオープン
        string ipAddress = initializeEventArgs.IpAddress;
        if (GigeCaptureDevice!.CameraOpen(ipAddress) != true)
        {
            LogManager.Logger.Error("カメラオープンエラー。");
            // エラーコードを指定する
            initializeEventArgs.ErrorCode = OpenErrorCode;
        }
        else // カメラオープン成功時。
        {
            CameraStop(); // カメラ停止。

            Config = new LineCameraCaptureConfig(initializeEventArgs.CaptureCount, initializeEventArgs.ImageHeight, initializeEventArgs.OverlapImageHeight); // 設定取得。

            try
            {
                GigeCaptureDevice?.CameraConfig(Config);    // カメラ設定
            }
            catch (Exception ex)
            {
                LogManager.Logger.Error("パラメータ設定エラー", ex);
                initializeEventArgs.ErrorCode = PrmErrorCode;
                return;
            }

            try
            {
                CameraStart(); // カメラ開始
            }
            catch (Exception ex)
            {
                LogManager.Logger.Error("取り込み開始エラー", ex);
                initializeEventArgs.ErrorCode = CaptureErrorCode;
                return;
            }
        }
    }

    /// <summary>
    /// 撮像要求時の処理。
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="eventArgs"></param>
    private static void OnCaptureRequested(object? sender, ICaptureEventArgs eventArgs)
    {
        LogManager.Logger.Information("撮像開始。");
        GigeCaptureDevice?.Trigger();
        LogManager.Logger.Information("撮像開始完了。");
    }

    /// <summary>
    /// 撮像画像取得要求時の処理。
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="eventArgs"></param>
    private static void OnCaptureImageRequested(object? sender, ICaptureImageEventArgs eventArgs)
    {
        // 撮像画像が取得できない場合は画像なしのエラーとして終了。
        if (!CaptureImages!.TryDequeue(out CapturedImage? captureImage))
        {
            eventArgs.ErrorCode = CapturedImage.NoImageError;
            return;
        }

        if (captureImage!.ErrorCode == 0) // 撮像成功時
        {
            LogManager.Logger.Debug($"撮像成功。幅:{captureImage.ImageWidth}, 高さ:{captureImage.ImageHeight}, チャンネル:{captureImage.ImageChannels}, 深度:{captureImage.ImageDepth}");

            eventArgs.ImageWidth = captureImage.ImageWidth;
            eventArgs.ImageHeight = captureImage.ImageHeight;
            eventArgs.ImageChannels = captureImage.ImageChannels;
            eventArgs.ImageBytes = captureImage.ImageBytes;
            eventArgs.ImageDepth = captureImage.ImageDepth; // 8bitグレースケールまたは24bitカラー画像の場合は1を指定する。

            // 取り込み成功 データ作成
            // オーバーラップ部
            int olcount = 0;
            if (OverLap != null) olcount = eventArgs.ImageWidth * (int)GigeCaptureDevice!.Overlap * eventArgs.ImageChannels * eventArgs.ImageDepth;
            int imglen = eventArgs.ImageWidth * eventArgs.ImageHeight * eventArgs.ImageChannels * eventArgs.ImageDepth;
            byte[] imgdata = new byte[imglen + olcount];
            OverLap!.CopyTo(imgdata, 0);     // 前回画像先頭に挿入
            Array.Copy(eventArgs.ImageBytes, 0, imgdata, olcount, imglen);   // メイン画像コピー
            Array.Copy(eventArgs.ImageBytes, imglen - olcount, OverLap, 0, olcount);   // 次回用に画像保存
            eventArgs.ImageBytes = imgdata;
            eventArgs.ImageHeight = eventArgs.ImageHeight + (int)GigeCaptureDevice!.Overlap;

            // 確認画像保存用
            //if (FrameCnt <= 2)
            //{
            //    Bitmap m_Bitmap = null;
            //    if (captureImage.ImageChannels == 3)
            //    {
            //        m_Bitmap = new Bitmap((int)captureImage.ImageWidth, (int)captureImage.ImageHeight, PixelFormat.Format24bppRgb);
            //    }
            //    else
            //    {
            //        m_Bitmap = new Bitmap((int)captureImage.ImageWidth, (int)captureImage.ImageHeight, PixelFormat.Format8bppIndexed);
            //        ColorPalette palette = m_Bitmap.Palette;
            //        for (int i = 0; i < 256; ++i) palette.Entries[i] = Color.FromArgb(i, i, i);
            //        m_Bitmap.Palette = palette;
            //    }
            //    BitmapData bmpData = m_Bitmap.LockBits(new Rectangle(0, 0, m_Bitmap.Width, m_Bitmap.Height), ImageLockMode.WriteOnly, m_Bitmap.PixelFormat);

            //    // Place the pointer to the buffer of the bitmap.
            //    IntPtr ptrBmp = bmpData.Scan0;
            //    Marshal.Copy(captureImage.ImageBytes, 0, ptrBmp, captureImage.ImageBytes.Length);
            //    m_Bitmap.UnlockBits(bmpData);
            //    string filename = string.Format("{0}.bmp", FrameCnt);
            //    m_Bitmap.Save(filename);
            //}
        }
        else // 撮像失敗時
        {
            LogManager.Logger.Error($"撮像異常。エラーコード:{CaptureErrorCode} 幅:{captureImage.ImageWidth}, 高さ:{captureImage.ImageHeight}, チャンネル:{captureImage.ImageChannels}, 深度:{captureImage.ImageDepth}");
            // エラーコード送信
            eventArgs.ErrorCode = CaptureErrorCode;
        }
    }

    /// <summary>
    /// 撮像停止要求時の処理。
    /// このメソッド内でカメラの停止処理を行ってください。
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="eventArgs"></param>
    private static void OnCaptureStopRequested(object? sender, ICaptureStopEventArgs eventArgs)
    {
        CameraStop();
        if (CaptureImages != null)
        {
            while (true)
            {
                if (!CaptureImages.TryDequeue(out _))
                {   // 撮影停止時にキューが残っていれば空にする
                    break;
                }
            }
        }

        try
        {
            // 撮像要求時に即座に撮像を実行できるようカメラを再始動。
            CameraStart();
        }
        catch (Exception ex)
        {
            LogManager.Logger.Error("取り込み開始エラー", ex);
            eventArgs.ErrorCode = CaptureErrorCode;
            return;
        }

    }

    /// <summary>
    /// 終了要求時の処理。
    /// このメソッド内でカメラの解放処理を行ってください。
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="eventArgs"></param>
    private static void OnTerminateRequested(object? sender, ITerminateEventArgs eventArgs)
    {
        // 撮像デバイスの解放
        DisposeCaptureDevice();
    }

    /// <summary>
    /// 撮像デバイスの解放処理。
    /// </summary>
    private static void DisposeCaptureDevice()
    {
        CameraStop(); //カメラ停止
        GigeCaptureDevice?.CameraClose(); // カメラのリソース解放
    }

    private static int nowcnt = 0;
    /// <summary>
    /// 撮像転送キュー作成。
    /// </summary>
    /// <returns>撮像画像一覧。</returns>
    private static ConcurrentQueue<CapturedImage> Capture(ImageEventArgs frame)
    {
        var capturedImages = new ConcurrentQueue<CapturedImage>();
        var capturedImage = new CapturedImage();

        if (frame != null) // 撮像成功時
        {
            if (frame.ErrorCode == 0)
            {
                capturedImage.ImageWidth = frame.Width;
                capturedImage.ImageHeight = frame.Height;
                capturedImage.ImageChannels = frame.Channels;
                capturedImage.ImageBytes = frame.Data;
                capturedImage.ImageDepth = frame.Depth;
                nowcnt = frame.FrameID;
            }
            else
            {
                capturedImage.ErrorCode = CaptureErrorCode;
                LogManager.Logger.Error(frame.ErrorMsg, null);
            }

        }
        else // 撮像失敗時
        {
            // カメラ初期化または撮像異常時はエラーコード(0以外の値)をセットし、クライアントアプリに通知する。
            capturedImage.ErrorCode = CaptureErrorCode;
            LogManager.Logger.Error("撮像処理異常。");
        }

        capturedImages.Enqueue(capturedImage);
        return capturedImages;
    }

    /// <summary>
    /// 撮像完了のコールバック
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="frame"></param>
    private static void CaptureEndEvent(object? sender, ImageEventArgs frame)
    {
        LogManager.Logger.Debug("撮像完了コールバック処理開始。");
        foreach (var captureImage in Capture(frame))
        {
            CaptureImages!.Enqueue(captureImage);        // 転送キューに追加
        }

        LogManager.Logger.Debug("撮像完了コールバック処理完了。");
    }

    private static byte[]? OverLap = null;

    /// <summary>
    /// カメラ開始。
    /// </summary>
    private static void CameraStart()
    {
        OverLap = new byte[(int)GigeCaptureDevice!.Width * GigeCaptureDevice!.Overlap * 3 * 2]; // オーバーラップ領域初期化 カラー・深度最大領域

        if (Config?.CaptureCount == 1)
        {
            // 1枚のみ撮像時。
            GigeCaptureDevice?.CaptureSnap();
        }
        else
        {
            GigeCaptureDevice?.CaptureStart();  // カメラ再開
        }
    }

    /// <summary>
    /// カメラ停止。
    /// </summary>
    private static void CameraStop()
    {
        if (GigeCaptureDevice?.IsRun == true)
        {
            GigeCaptureDevice?.CaptureStop(); //カメラ停止
        }
    }
}

// Copyright (c) 2025 MENOU. All rights reserved.

namespace LineCamera.Omronsentech;

/// <summary>
/// ラインセンサカメラ撮像設定クラス。
/// </summary>
public class LineCameraCaptureConfig
{
    /// <summary>
    /// 撮像数。
    /// 0指定時は無限長撮像。
    /// </summary>
    public int CaptureCount { get; set; }

    /// <summary>
    /// 画像の高さ(px)。
    /// </summary>
    public int ImageHeight { get; set; }

    /// <summary>
    /// 重複する画像の高さ(px)。
    /// </summary>
    public int OverlapImageHeight { get; set; }

    /// <summary>
    /// コンストラクタ。
    /// </summary>
    /// <param name="captureCount">撮像数。</param>
    /// <param name="imageHeight">画像の高さ(px)。</param>
    /// <param name="overlapImageHeight">重複する画像の高さ(px)。</param>
    public LineCameraCaptureConfig(int captureCount, int imageHeight, int overlapImageHeight)
    {
        CaptureCount = captureCount;
        ImageHeight = imageHeight;
        OverlapImageHeight = overlapImageHeight;
    }
}

// Copyright (c) 2025 MENOU. All rights reserved.

namespace LineCamera.Omronsentech;

/// <summary>
/// 撮像画像クラス。
/// </summary>
public class CapturedImage
{
    /// <summary>
    /// 画像取得できなかった場合のエラーコード。
    /// </summary>
    public const int NoImageError = 1;

    /// <summary>
    /// 取得画像の幅[px]。
    /// </summary>
    public int ImageWidth { get; set; }

    /// <summary>
    /// 取得画像の高さ[px]。
    /// </summary>
    public int ImageHeight { get; set; }

    /// <summary>
    /// 取得画像のチャンネル数。
    /// </summary>
    public int ImageChannels { get; set; }

    /// <summary>
    /// 取得画像の深度。
    /// 8bit画像:1
    /// 16bit画像:2
    /// 32bit画像:4
    /// </summary>
    public int ImageDepth { get; set; }

    /// <summary>
    /// 画像データ。
    /// </summary>
    public byte[]? ImageBytes { get; set; }

    /// <summary>
    /// エラーコード。
    /// </summary>
    public int ErrorCode { get; set; }
}
  1. 前準備として、撮像アプリケーションのexeファイルが格納されるフォルダ内に "Config\ImagingApp.config" ファイルを作成し、以下の内容を記述してください。

    {
      "isLineCamera": true,
      "isValidInfiniteLength": true
    }
    

    "isLineCamera" はラインセンサカメラを使用する場合に true に設定してください。 "isValidInfiniteLength" は無限長撮像を有効にする場合に true に設定してください。

  2. Plugin I/F を提供する名前空間をインポートします。

     using Menou.Menoute.Runtime.Plugin.LineCamera;
    
  3. Plugin と MENOU-RN を通信する PluginLineCameraImageDeviceCommunicator オブジェクトを生成します。

     var name = args[0]; // メモリマップ名
     var appDataDirectory = args[1]; // アプリケーションデータディレクトリ(ログ保存用)
     var parentProcessIdStr = args[2]; // 呼び出し元のプロセスIDの文字列。
     using var pluginImageDeviceCommunicator = new PluginLineCameraImageDeviceCommunicator(name, appDataDirectory, parentProcessIdStr);
    

    PluginLineCameraImageDeviceCommunicator オブジェクトは不要になったタイミングで適宜解放(Dispose)してください。

  4. カメラ初期化時に実行されるイベントを登録します。

    pluginImageDeviceCommunicator.InitializeRequested += OnInitializeRequested;
    

    カメラ初期化要求時処理では、カメラオープン処理、カメラ設定処理を行います。

    IInitializeEventArgs オブジェクト にて MENOU-RN で設定したカメラパラメータを取得できます。

  5. 撮像要求時に実行されるイベントを登録します。

    pluginImageDeviceCommunicator.CaptureRequested += OnCaptureRequested;
    

    撮像要求時処理では、カメラのトリガーを開始するなど、撮像を開始します。

  6. 撮像画像取得要求時に実行されるイベントを登録します。

    pluginImageDeviceCommunicator.CaptureImageRequested += OnCaptureImageRequested;
    

    撮像画像取得要求時処理では、取得した撮像画像データおよび幅、高さ、チャンネル情報を ICaptureImageEventArgs オブジェクトにセットします。

    撮像に失敗した時は、任意のエラーコード(0、1以外)をセットします。

    撮像画像がない場合は、エラーコードに NoImageError(1) をセットします。

  7. 撮像停止要求時に実行されるイベントを登録します。

    pluginImageDeviceCommunicator.CaptureStopRequested += OnCaptureStopRequested;
    

    撮像停止要求時では、カメラの撮像を停止します。

  8. 終了要求時に実行されるイベントを登録します。

    pluginImageDeviceCommunicator.TerminateRequested += OnTerminateRequested;
    

    終了処理ではカメラのクローズ処理など、オブジェクトの解放処理を行ってください。

  9. MENOU-RNとの通信を非同期で開始します。

    await pluginImageDeviceCommunicator.StartAsync();
    

各 I/F の詳細については API リファレンス をご確認ください。

© Copyright 2021 MENOU Corp.