KAYA VISION POINT II / C++ IMPLEMENTATION GUIDE
CAMERA PROGRAMMING / BEGINNER'S GUIDE

4つの手順で学ぶ
カメラ操作とメモリー記録

APIの役割、使う理由、次へ進む条件を、コードの前後で解説します。
フレームグラバーとStreamの準備・解放も、この4つの手順に含めます。

STEP 01

01. カメラオープン

SDKとフレームグラバーを準備し、カメラの設定を読み書きできる状態まで進めます。この章では画像取得を開始しません。

SDK初期化 → TL → フレームグラバー → 必要な給電 → カメラ → Remote Port → XML/パラメータコレクション

1-1. 指定フォルダーのサンプルを理解する

対象は KYVP_QueuedBuffers_Example です。このサンプルは画像を受信し、受信バッファを再利用する基本構造を示しています。指定枚数の画像を別のRAMに残す処理は、本書の第3章で追加します。

ファイル/関数役割と本書で扱う場所
KYVP_QueuedBuffers_Example.sln / .vcxprojx64のVisual Studioプロジェクト。v143、Windows SDK 10.0、Console構成。
KYVP_QueuedBuffers_Example.c / .cpp / main添付Cソースと指定フォルダーの.cppは同一内容。SDK・Interface・Deviceの列挙とオープン、画像条件の読み出しとSetImageDetailsを扱う。第1・2章。
ParseXmlUrl / LoadXML_* / RemoteDevice_Transport / Stream_TransportXML読込みと機器へのレジスタアクセスを結び付ける。第1・3章。
InititializeParameterHandlerCollection原本の綴りのまま。GenICamパラメータコレクションを準備する。第1章。
CreateStream / StartAcquisition / StreamCallbackStream・通知・バッファを準備して取得する。第3章。
StreamEventNewBufferThreadImpl / StartThread / StopThreadDirect Callbackを使わない場合の代替経路。第3・4章で区別。
StopAcquisition / StreamDelete / CloseParameterHandlerCollection / CloseAll / ReleaseResources取得停止、通知解除、Buffer・Collection・Stream・Device・Interface・TL・所有メモリの解放。第4章。

既定では KYVP_USE_STREAM_DIRECT_CALLBACK が有効です。 KYVP_ENABLE_USER_MANAGED_BUFFER_MEMORYKYVP_CUDA_BUFFERS は無効なので、基本経路はSDKが確保したバッファをCallbackで受け取ります。本書もこの経路で説明します。キー入力や画面操作の実装は扱いません。

ビルド設定指定プロジェクトの値
追加のインクルードディレクトリ$(KAYA_VISION_POINT_2_INCLUDE_PATH)
追加のライブラリディレクトリ$(KAYA_VISION_POINT_2_LIB_PATH)
リンクするライブラリKYVPLibTL_vc141.libKYVPLibExtension_vc141.libKYVPParametersHandler_vc141.libKYFoundation_vc141.lib
本書の追加コード説明を統一するためC++を使用。指定プロジェクトは.cppをビルド対象としている。Cとして利用する場合は、メモリ・例外・同期処理をC用に置き換える。

1-2. エラー判定とハンドルの初期状態を用意する

ハンドルは、SDKが管理する接続先を識別する値です。最初は無効値にし、オープンに成功したものだけを終了時に閉じます。checkは失敗を呼び出し元へ伝える補助関数です。Callbackの中では例外を投げず、第3章のエラーフラグを使います。では、ハンドルだけで成功を判定してよいでしょうか。途中まで初期化された状態もあるため、この例では成功フラグも別に持ちます。こうして「どこまで準備できたか」を残すと、未作成の対象を閉じる誤りを防げます。また、画像通知は別の実行経路から届くので、通常処理と同じ例外処理を流用しないことが設計の出発点になります。

説明用C++ · 指定サンプルのAPIを基に構成
// 1-2. エラー判定とハンドルの初期状態を用意する:本文と同じ順番で処理する。
#define KYVP_USE_EXPERIMENTAL_APIS
#define KYVP_USE_STREAM_DIRECT_CALLBACK
#include "KYVPLibTL.h"
#include "KYVPLibExtension.h"
#include "KYVPParametersHandler.h"
#include "KYFoundation.h"
#include <windows.h>
#include <cstdint>
#include <cstring>
#include <stdexcept>
#include <vector>
#include <atomic>
#include <mutex>
#include <limits>
#include <cmath>
#include <cstdio>

static void check(KY_RESULT r, const char* api) {
    if (KY_RESULT_FAILED(r)) throw std::runtime_error(api);
}
static KYVP_TL_HANDLE hTLHandle = KYVP_TL_HANDLE_INVALID;
static KYVP_PCI_INTERFACE_HANDLE hPCIInterfaceHandle = KYVP_PCI_INTERFACE_HANDLE_INVALID;
static KYVP_DEVICE_HANDLE hLocalDeviceHandle = KYVP_DEVICE_HANDLE_INVALID;
static KYVP_REMOTE_DEVICE_HANDLE hRemoteDeviceHandle = KYVP_REMOTE_DEVICE_HANDLE_INVALID;
static KYVP_STREAM_HANDLE hStreamHandle = KYVP_STREAM_HANDLE_INVALID;
static KYVP_COLLECTION_HANDLE hRemoteDeviceParameterCollectionHandle = KYVP_COLLECTION_HANDLE_INVALID;
static KYVP_COLLECTION_HANDLE hStreamParameterCollectionHandle = KYVP_COLLECTION_HANDLE_INVALID;
static bool libraryReady=false, tlOpen=false, interfaceOpen=false;
static bool deviceOpen=false, streamOpen=false, callbackRegistered=false;
static bool streamStarted=false, cameraStartAttempted=false;
struct CollectionState { bool created=false,transport=false,callback=false; };
static CollectionState cameraCollectionState,streamCollectionState;
static void KYVP_CALLCONV StreamCallback(KYVP_BUFFER_HANDLE,void*);

原本のASSERT_KY_RESULTは、致命的でない失敗ではログを出して処理を続けます。本書では初期化失敗後の誤操作を避けるため、通常の制御処理をそこで中断します。第4章では例外を受けても、成功済みの操作を逆順に終了する構成を示します。したがって、checkの導入は単なる記述の短縮ではなく、失敗後に次のAPIへ進まないための境界になります。一方、後片付けまで同じ例外で中断すると接続が残るので、終了処理では戻り値を保存して判断します。さらに、エラーを調べる際は関数名とSDKの結果値を一緒に記録すると、失敗した段階を特定しやすくなります。

VP2 API資料の参照ページ
冊子p.21:KY_RESULT・結果判定冊子p.14:SDK初期化と終了

1-3. SDKとフレームグラバーを開く

InitLibはSDK全体、TLOpenはシステムモジュールを準備します。続いてInterface一覧を更新し、件数とIDを取得します。Interfaceがフレームグラバーに対応します。件数確認を先に行うことで、未接続時に存在しないインデックスを開くことを防げます。ここで押さえたいのは、列挙とオープンの違いです。列挙は利用候補を調べる処理であり、それだけでは撮像に必要な接続は用意されません。そこで、件数を確認してからIDを取得し、そのIDを指定して対象を開きます。また、途中で失敗した場合にもTLやSDKが残り得るため、成功した段階ごとに終了対象を記録します。

説明用C++ · 指定サンプルのAPIを基に構成
// 1-3. SDKとフレームグラバーを開く:本文と同じ順番で処理する。
// SDK全体を初期化する。最初に1回呼び、最後にCloseLibで終了する。
check(KYVPLibTL_InitLib(), "InitLib"); libraryReady=true;
// Interfaceを列挙する親モジュールTLを開く。
check(KYVPLibTL_TLOpen_V1(&hTLHandle), "TLOpen"); tlOpen=true;
KY_BOOL changed=KY_FALSE;
// PC上のフレームグラバー一覧を最新状態に更新する。
check(KYVPLibTL_TLUpdateInterfaceList_V1(hTLHandle,&changed,0), "TLUpdateInterfaceList");
uint32_t count=0;
// 接続されたInterfaceの件数を取得し、範囲外の選択を防ぐ。
check(KYVPLibTL_TLGetNumInterfaces_V1(hTLHandle,&count), "TLGetNumInterfaces");
const uint32_t selectedInterface=0;
if (selectedInterface>=count) throw std::runtime_error("No selected frame grabber");
char id[128]={}; size_t size=sizeof(id);
// 選んだInterfaceを開くためのID文字列を取得する。
check(KYVPLibTL_TLGetInterfaceID_V1(hTLHandle,selectedInterface,id,&size), "TLGetInterfaceID");
const KYVP_PCI_INTERFACE_INFO* info=nullptr;
KYVP_INFO_DATATYPE type; size=sizeof(info);
// ボードの識別情報を取得する。戻る情報の所有者はSDK。
check(KYVPLibTL_TLGetInterfaceInfo_V1(hTLHandle,id,
    KYVP_INTERFACE_INFO_CMD_KYVP_PCI_INTERFACE_INFO,&type,&info,&size), "TLGetInterfaceInfo");
// 選択したフレームグラバーを操作可能にする。
check(KYVPLibTL_TLOpenInterface_V1(hTLHandle,id,&hPCIInterfaceHandle), "TLOpenInterface");
interfaceOpen=true;

TLGetInterfaceInfoはボード情報を調べる任意の処理です。複数ボードがある場合は、この情報とIDで目的の1台を選びます。取得したinfoはSDKが所有する情報なのでfreeしません。ここではフレームグラバーを開いただけで、カメラやStreamはまだ開いていません。つまり、ボードのオープン成功は準備の第一段階です。次は、そのボードに接続されたカメラを探します。ただし、複数枚構成で常に先頭を選ぶと目的の接続先を取り違える可能性があるため、実運用では識別情報を照合してください。また、IDや情報の返却形式と所有権を確認し、SDK管理の領域を自分で解放しないようにします。

VP2 API資料の参照ページ
冊子p.10:Figure 1:呼出し順冊子p.11:InitLib・TLOpen・Interface列挙/オープン冊子p.14:System・Interfaceの説明

1-4. 必要な場合だけPoCXP給電を準備する

カメラがPoCXPで動く構成では、検出の前に給電が必要です。指定QueuedBuffersサンプルにはInterface用Collectionの作成がないため、既存サンプルのInterface XML読込みとTransportを追加します。外部電源で起動済みなら、この処理は不要です。なぜ給電をこの位置で扱うのでしょうか。電源が入っていないカメラは、後段の一覧更新を繰り返しても検出できないからです。そのため、まず接続構成を確認し、給電が必要な場合だけInterfaceの設定経路を準備します。なお、電源投入直後には起動待ちが必要なので、給電の成功とカメラの検出成功は分けて扱います。

説明用C++ · 指定サンプルのAPIを基に構成
// 1-4. 必要な場合だけPoCXP給電を準備する:本文と同じ順番で処理する。
// 前提:Interface XMLとTransportを登録済みのCollectionを渡す。
static void enablePoCxp(KYVP_COLLECTION_HANDLE interfaceParams,
                       const char* portName, int64_t onValue) {
    KY_BOOL available=KY_FALSE;
    // 対象ノードを現在の構成で利用できるか調べる。
    check(KYParametersHandler_IsParameterAvailable_V1(
        interfaceParams,portName,&available,nullptr,nullptr), "PoCXP available");
    if (!available) throw std::runtime_error("PoCXP node unavailable");
    // XMLで確認した列挙値の数値を設定する。
    check(KYParametersHandler_SetValueEnum_V1(
        interfaceParams,portName,onValue,nullptr,nullptr), "PoCXP on");
}
// 例:PoCXP0のOn列挙値が1であることを実機のXMLで確認した場合。
// enablePoCxp(interfaceParams, "PoCXP0", 1);
// 必要な接続ポートだけを対象にし、起動待ち後にカメラを検出する。

PoCXPのポート名・列挙値・必要本数はボードとカメラに依存します。4ポートすべてを無条件にONにせず、自分で変更したポートを記録してください。Interface XMLがレジスタ上にある場合はPCIInterface_ReadPortを使い、カメラ用RemoteDevice_ReadPortと混同しません。この区別は、読書きAPIの操作対象が異なるためです。したがって、給電用の設定をカメラ用Collectionへ渡しても正しい制御にはなりません。また、終了時に元の給電状態へ戻す運用では、開始前の値も保存します。これにより、対象外のポートや別の機器へ影響する設定変更を避けやすくなります。

VP2 API資料の参照ページ
冊子p.12:Interfaceの役割冊子p.17:ParametersHandlerによる設定の流れ(給電ノード固有の説明は対象外)

1-5. カメラを検出して開く

IFUpdateDeviceListは、開いているフレームグラバーに接続されたカメラ一覧を更新します。給電直後は検出まで時間がかかるため、上限付きで再試行します。IFOpenDeviceのCONTROL権限は、後の章で撮像条件を書き換えるために指定します。ここでは「見つかること」と「制御できること」を順に確かめます。まず件数が得られたらIDを取り出し、そのIDでDeviceを開きます。続いて画像経路の数とRemote Portを取得すると、次のXML読込みへ進めます。ただし、別のプロセスが接続を使用している場合などはオープンに失敗するため、検出済みでも結果の確認は必要です。

説明用C++ · 指定サンプルのAPIを基に構成
// 1-5. カメラを検出して開く:本文と同じ順番で処理する。
KY_BOOL changed=KY_FALSE; uint32_t count=0;
for (int retry=0; retry<10; ++retry) {
    // このフレームグラバーに接続したカメラを再検出する。
    check(KYVPLibTL_IFUpdateDeviceList_V1(hPCIInterfaceHandle,&changed,500), "IFUpdateDeviceList");
    // 検出済みカメラの件数を確認する。
    check(KYVPLibTL_IFGetNumDevices_V1(hPCIInterfaceHandle,&count), "IFGetNumDevices");
    if (count>0) break;
}
const uint32_t selectedCamera=0;
if (selectedCamera>=count) throw std::runtime_error("No selected camera");
char id[128]={}; size_t size=sizeof(id);
// 開くカメラのIDを取得する。
check(KYVPLibTL_IFGetDeviceID_V1(hPCIInterfaceHandle,selectedCamera,id,&size), "IFGetDeviceID");
const KYVP_DEVICE_INFO* info=nullptr;
KYVP_INFO_DATATYPE type; size=sizeof(info);
// カメラの型番や識別情報を調べるための情報を取得する。
check(KYVPLibTL_IFGetDeviceInfo_V1(hPCIInterfaceHandle,id,
    KYVP_DEVICE_INFO_CMD_KYVP_DEVICE_INFO,&type,&info,&size), "IFGetDeviceInfo");
// カメラを制御権限付きで開く。以後、撮像条件を変更できる。
check(KYVPLibTL_IFOpenDevice_V1(hPCIInterfaceHandle,id,
    KYVP_DEVICE_ACCESS_FLAGS_CONTROL,&hLocalDeviceHandle), "IFOpenDevice");
deviceOpen=true;
uint32_t streams=0;
// カメラが持つ画像受信用Streamの数を確認する。
check(KYVPLibTL_DevGetNumDataStreams_V1(hLocalDeviceHandle,&streams), "DevGetNumDataStreams");
if (streams==0) throw std::runtime_error("Camera has no stream");
// カメラ内部のレジスタへ接続するRemote Portを取得する。
check(KYVPLibTL_DevGetPort_V1(hLocalDeviceHandle,&hRemoteDeviceHandle), "DevGetPort");

Local Deviceは接続管理、Remote Portはカメラ内部のレジスタへアクセスする入口です。DevGetPortが返すRemoteハンドルを独立してオープンした機器のように扱う必要はありません。終了時は子Streamとパラメータ処理を片付けてからDevCloseし、両方のハンドルを無効化します。二つを分けると、設定と画像受信の関係が整理できます。すなわち、設定にはRemote Portを使い、画像には後から開くStreamを使います。また、どちらもDeviceに依存するため、Deviceを先に閉じると後続の操作を継続できません。この親子関係が、第4章の終了順を決める根拠になります。

VP2 API資料の参照ページ
冊子p.11:Device列挙/オープン・DevGetPort冊子p.15:Deviceの説明

1-6. カメラXMLを読み込み、パラメータ操作をつなぐ

WidthやExposureTimeという名前を、カメラが理解するレジスタ操作に変換するのがParametersHandlerです。その変換規則をXMLから読み込みます。Transportは、ParametersHandlerからの読込み・書込み要求をRemote Portへ渡すアプリケーション側の関数です。したがって、XMLを読むだけでは設定値を機器へ送れません。名前と型を解釈するCollectionに加え、実際に通信するTransportを登録して初めて設定経路がつながります。また、CameraとStreamでは読書き先が異なるため、同じ仕組みでも対象に合うTransportが必要です。

説明用C++ · 指定サンプルのAPIを基に構成
// 1-6. カメラXMLを読み込み、パラメータ操作をつなぐ:本文と同じ順番で処理する。
// XMLDescriptor、ParseXmlUrl、RemoteDevice_Transportは指定Cサンプルを使用。
// これらの関数定義とremoteDeviceXmlFileは、呼出しより前に配置する。
char cameraXmlUrl[128]={}; size_t size=sizeof(cameraXmlUrl);
// カメラ機能を定義したXMLの取得場所を調べる。
check(KYVPLibTL_RemoteDevice_GetPortURL_V1(
    hRemoteDeviceHandle,cameraXmlUrl,&size), "RemoteDevice_GetPortURL");
ParseXmlUrl(cameraXmlUrl,&remoteDeviceXmlFile,LoadXML_ModuleRegisterMap_RemoteDevice);
if (!remoteDeviceXmlFile.pData || remoteDeviceXmlFile.uSize==0)
    throw std::runtime_error("Camera XML is empty");

KYParametersHandler_InitParameters init={}; init.version=1;
// XMLを解釈するParametersHandlerを初期化する。
check(KYParametersHandler_Initialize_V1(&init,nullptr,nullptr), "ParametersHandler Initialize");
// 対象モジュール専用のパラメータ集合を作る。
check(KYParametersHandler_CreateParameterCollection_V1(
    &hRemoteDeviceParameterCollectionHandle,nullptr,nullptr), "Create camera collection");
cameraCollectionState.created=true;
// 名前による設定操作を、実際のPort読書きへつなぐ。
check(KYParametersHandler_RegisterParameterCollectionTransport_V1(
    hRemoteDeviceParameterCollectionHandle,RemoteDevice_Transport,
    nullptr,nullptr,nullptr), "Register camera transport");
cameraCollectionState.transport=true;
// 登録したXMLとTransportを使い、パラメータ操作を使用可能にする。
check(KYParametersHandler_InitializeParameterCollection_V1(
    hRemoteDeviceParameterCollectionHandle,nullptr,nullptr), "Initialize camera collection");

原本のInititializeParameterHandlerCollectionがまとめて行う処理を展開しています。パラメータ変更Callbackの登録は任意で、この例では使いません。Create・Transport登録の成功を別々に記録し、XMLデータはCollectionを削除するまで保持します。レジスタ読込みの失敗もTransportの結果へ反映してください。要点は、作成と利用開始を別の段階で管理することです。初期化だけが失敗した場合も、作成済みCollectionは終了処理の対象になります。したがって、失敗した行から一律に戻るのではなく、登録状態に応じて解除する設計にしてください。

VP2 API資料の参照ページ
冊子p.17:Collection作成・Transport登録・初期化

この章の完了条件
カメラのRemote PortとパラメータCollectionが有効になりました。次はこのCollectionを使って撮像条件を設定します。Stream作成と撮像開始は第3章で行います。

STEP 02

02. プロパティ設定

画像形式と撮像条件を確定します。設定を変更した後に値を読み戻し、その結果をフレームグラバーへ伝えます。

型・範囲確認 → PixelFormat/解像度 → 連続撮像/露光 → ゲイン → OptrImageStamp → 読み戻し → SetImageDetails

カメラ仕様:CyclonePlusマニュアル p.16–17(画像形式・Stamp)、p.21–22(露光・取得)、p.24(ゲイン)。ノードの実際の型と範囲は接続カメラのXMLで確認します。

2-1. 型に合う書込み関数を用意する

パラメータには整数、実数、列挙型があります。SetValueには値のアドレスだけでなく、その値のサイズも渡します。型を間違えると正しい値として解釈されません。以下の補助関数は可用性・書込み可否・範囲を確認してから、対応するSDK APIを呼びます。例えば幅は整数ですが、露光時間は実数として扱います。どちらも数値だからと同じ変数型で渡すと、バイト列の意味が変わってしまいます。そこで、型ごとに補助関数を分け、呼び出す側から意図が見えるようにしています。また、撮像中は書込み禁止になる項目もあるため、範囲だけでなく現在のアクセス状態まで確認する必要があります。

説明用C++ · 指定サンプルのAPIを基に構成
// 2-1. 型に合う書込み関数を用意する:本文と同じ順番で処理する。
static KYVP_NodeDescriptor* writableNode(const char* name) {
    KYVP_NodeDescriptor* node=nullptr;
    // ノードの型、書込み可否、最小値・最大値・刻み幅を調べる。
    check(KYParametersHandler_GetNodeDescriptor_V1(
        hRemoteDeviceParameterCollectionHandle,name,&node,nullptr,nullptr), name);
    if (!node || !node->m_bIsImplemented || !node->m_bIsAvailable || !node->m_bIsWritable)
        throw std::runtime_error(name);
    return node;
}
static void setInt(const char* name, int64_t value) {
    auto* n=writableNode(name);
    if (n->m_eInterfaceType!=KYVP_ParameterInterfaceType_IInteger ||
        value<n->m_nMinIntValue || value>n->m_nMaxIntValue)
        throw std::runtime_error(name);
    if (n->m_nIncIntValue>0 &&
        (static_cast<uint64_t>(value)-static_cast<uint64_t>(n->m_nMinIntValue))
            % static_cast<uint64_t>(n->m_nIncIntValue)!=0)
        throw std::runtime_error(name);
    size_t size=sizeof(value);
    // 型とサイズを合わせた値を対象パラメータへ書き込む。
    check(KYParametersHandler_SetValue_V1(hRemoteDeviceParameterCollectionHandle,
        name,&value,&size,nullptr,nullptr), name);
}
static void setFloat(const char* name, double value) {
    auto* n=writableNode(name);
    if (n->m_eInterfaceType!=KYVP_ParameterInterfaceType_IFloat || !std::isfinite(value) ||
        value<n->m_fMinFloatValue || value>n->m_fMaxFloatValue)
        throw std::runtime_error(name);
    if (n->m_fIncFloatValue>0) {
        double steps=(value-n->m_fMinFloatValue)/n->m_fIncFloatValue;
        if (std::abs(steps-std::round(steps))>1e-6) throw std::runtime_error(name);
    }
    size_t size=sizeof(value);
    // 型とサイズを合わせた値を対象パラメータへ書き込む。
    check(KYParametersHandler_SetValue_V1(hRemoteDeviceParameterCollectionHandle,
        name,&value,&size,nullptr,nullptr), name);
}
static void setEnum(const char* name, const char* value) {
    if (writableNode(name)->m_eInterfaceType!=KYVP_ParameterInterfaceType_IEnumeration)
        throw std::runtime_error(name);
    // 列挙型パラメータをOnやMono8などの値名で設定する。
    check(KYParametersHandler_SetValueEnumByValueName_V1(
        hRemoteDeviceParameterCollectionHandle,name,value,nullptr,nullptr), name);
}

Min・Maxは設定できる範囲、Incは値を増減できる刻み幅です。ROIやPixelFormatを変えると他の範囲も変わるため、書込みのたびに調べます。Enumの値名が実機で使えない場合も失敗として扱います。以下の設定値は一例であり、全機種に共通する固定値ではありません。このため、起動時に一度調べた範囲を使い続ける設計には注意が必要です。例えば画像条件を変更した後は、関連項目の上限が以前と同じとは限りません。したがって、現在の条件で確認してから設定し、その後に値を読み戻します。この三段階をそろえると、要求した値と実際に採用された値の違いを追跡しやすくなります。

VP2 API資料の参照ページ
冊子p.17:GetValue/SetValueの流れ(型別補助APIの個別仕様は未掲載)

2-2. 解像度と画像形式を設定する

WidthとHeightはカメラから送る画像の画素数です。OffsetXとOffsetYはセンサー上の切り出し位置を示します。ROIを変えると画像のサイズと必要な受信メモリが変わるため、Streamのバッファを確保する前に設定します。ここでは説明を統一するためMono8を使用します。ここでの解像度は表示サイズではなく、実際に転送する画像の寸法です。したがって、幅や高さを変えれば、後で保存する一枚分の領域も変わります。また、切り出し位置を先に原点へ戻すのは、以前の位置が新しい寸法の設定を妨げる場合があるためです。最後に実値を読み戻してから、受信側の画像条件へ反映します。

説明用C++ · 指定サンプルのAPIを基に構成
// 2-2. 解像度と画像形式を設定する:本文と同じ順番で処理する。
// 撮像停止中に実行。例の値が現在のMin/Max/Incに適合することが前提。
setEnum("PixelFormat", "Mono8");
// Offsetが変更できる機種の例。中央固定の機種では対応しない軸を設定しない。
setInt("OffsetX", 0);
setInt("OffsetY", 0);
setInt("Width", 640);
setInt("Height", 480);

int64_t actualWidth=0, actualHeight=0;
size_t size=sizeof(actualWidth);
// 現在の実値を読み戻す。sizeは呼出しごとに設定する。
check(KYParametersHandler_GetValue_V1(hRemoteDeviceParameterCollectionHandle,
    "Width",&actualWidth,&size,nullptr,nullptr), "Read Width");
size=sizeof(actualHeight);
// 現在の実値を読み戻す。sizeは呼出しごとに設定する。
check(KYParametersHandler_GetValue_V1(hRemoteDeviceParameterCollectionHandle,
    "Height",&actualHeight,&size,nullptr,nullptr), "Read Height");

ROIには幅・高さとオフセットの依存関係があります。上の順序は原点から640×480を切り出せる機種の例です。OffsetYが中央固定の機種などでは、その設定を省いて実際の範囲に合わせます。読込みに失敗した場合に仮の640×480で続行せず、設定エラーとして止めます。なぜ仮の寸法で続けないのでしょうか。カメラと受信側が異なる一枚の大きさを想定すると、データの解釈やメモリ確保が一致しなくなるからです。そのため、設定できない場合は範囲と刻み幅を見直します。また、撮像を始めた後に変更する場合は、その場で値だけを書き換えず、停止と受信領域の再準備を組み合わせます。

VP2 API資料の参照ページ
冊子p.17:パラメータの読書き
カメラ固有仕様:CyclonePlus取扱説明書 p.16:Width・Height・Offset・PixelFormat

2-3. 連続撮像、フレームレート、露光時間を設定する

ExposureTimeは、ExposureModeがTimedのときの露光時間で、CyclonePlusでは単位がµsです。AcquisitionFrameRateは1秒間の撮像回数です。外部トリガーがない基本例では、選択したトリガーをOffにし、カメラ内部の周期で連続撮像させます。まず撮像方式をそろえる理由は、同じ数値でもモードによって使われ方が変わるためです。例えば外部信号の幅で露光を決める設定では、時間の値だけを変更しても狙いどおりになりません。そこで、連続撮像と時間指定の条件を先に確定し、その後で露光時間とフレームレートを設定します。最後は両方の実値を読み戻します。

説明用C++ · 指定サンプルのAPIを基に構成
// 2-3. 連続撮像、フレームレート、露光時間を設定する:本文と同じ順番で処理する。
setEnum("AcquisitionMode", "Continuous");
setEnum("ExposureMode", "Timed");
setEnum("TriggerSelector", "ExposureStart");
setEnum("TriggerMode", "Off");
// 実機で他のTriggerSelectorを使っていた場合は、その有効状態も確認する。
// ExposureAutoやAcquisitionFrameRateEnableが存在する機種では事前に確認する。
setFloat("ExposureTime", 1000.0);          // 1 ms
setFloat("AcquisitionFrameRate", 100.0);   // 100 fps
double actualExposure=0, actualFps=0;
size_t size=sizeof(actualExposure);
// 現在の実値を読み戻す。sizeは呼出しごとに設定する。
check(KYParametersHandler_GetValue_V1(hRemoteDeviceParameterCollectionHandle,
    "ExposureTime",&actualExposure,&size,nullptr,nullptr), "Read ExposureTime");
size=sizeof(actualFps);
// 現在の実値を読み戻す。sizeは呼出しごとに設定する。
check(KYParametersHandler_GetValue_V1(hRemoteDeviceParameterCollectionHandle,
    "AcquisitionFrameRate",&actualFps,&size,nullptr,nullptr), "Read AcquisitionFrameRate");

100 fpsでは1フレームの周期は10 msです。露光時間はこの周期などの条件で制限されます。実機の初期状態によっては、先にフレームレートを下げるなど、設定可能な順序へ調整してください。TriggerWidthなどのモードではExposureTimeだけで露光時間を決められません。したがって、露光と周期は独立したつまみとして考えないことが大切です。明るくしたくて露光時間を伸ばす場合も、現在の撮像周期で許される範囲を確認します。一方、動きの速い対象では長い露光がぶれにつながるため、必要な時間を決めてから照明やゲインも検討します。設定失敗時は、単位とモードを先に見直しましょう。

VP2 API資料の参照ページ
冊子p.17:パラメータの読書き
カメラ固有仕様:CyclonePlus取扱説明書 p.21:ExposureTime・ExposureMode・AcquisitionFrameRate

2-4. ゲインを設定する

Gainは画像信号の増幅量で、CyclonePlusマニュアルではdB単位とされています。まず通常のGainノードを使い、明るさを調整します。露光時間とは別の設定であり、値を上げれば同じ撮像時間でも信号が増幅されますが、飽和やノイズも確認する必要があります。ここでは基準を作るため、まず0 dBを指定して実値を読み戻します。そのうえで画像の明るさが不足する場合に、露光時間や照明条件と併せて調整します。ただし、ゲインを上げても失われた階調が戻るわけではありません。したがって、数値が設定できたことに加え、対象の明部が飽和していないかを画像で確認する工程が必要です。

説明用C++ · 指定サンプルのAPIを基に構成
// 2-4. ゲインを設定する:本文と同じ順番で処理する。
// XML上でGainがFloat型として提供されている機種の例。
// GainAutoがある機種では、手動設定の前にOffへ切り替える。
setFloat("Gain", 0.0);
double actualGain=0.0;
size_t size=sizeof(actualGain);
// 現在の実値を読み戻す。sizeは呼出しごとに設定する。
check(KYParametersHandler_GetValue_V1(hRemoteDeviceParameterCollectionHandle,
    "Gain",&actualGain,&size,nullptr,nullptr), "Read Gain");
printf("Gain = %.3f dB\n", actualGain);

// OptrAnalogGainは別の機種依存機能。通常のGainの代替として自動設定しない。
// 型・可用性・メーカーの適用条件を確認してから専用に実装する。

OptrAnalogGainはA/D変換前の増幅を扱う別ノードです。マニュアルには機種による注意があるため、基本例では変更しません。DGainやAGainという名前のノードがあっても、Gainと同じdB値を設定しないでください。接続機種のノード名・型・単位が一致するか確認します。つまり、似た名前のノードへ同じ数値を渡すだけでは、同じ増幅にはなりません。まず通常のGainで基準画像を記録し、その後に必要な調整を行うと変化を比較できます。また、機種を変更したときは設定値をそのまま流用せず、XMLから範囲と型を確認します。これにより、設定は成功したのに明るさが想定と違う状況を減らせます。

VP2 API資料の参照ページ
冊子p.17:パラメータの読書き
カメラ固有仕様:CyclonePlus取扱説明書 p.24:Gain・OptrAnalogGain

2-5. OptrImageStampとは何か

正式なXMLノード名は OptrImageStamp です。Onにすると、画像先頭の画素が撮像管理用データに置き換わります。独立したファイルヘッダーではなく画像内に埋め込まれるため、その場所の元の明るさ情報は残りません。フレームの連続性や受理された外部トリガーとの関係を調べる用途で利用できます。

Mono8の先頭位置格納内容
画素0–1Image Counter(画像カウンター)16 bit
画素2–4Microsecond Counter(µsカウンター)24 bit
画素5–6Trigger Counter(カメラが受理した外部トリガーのカウンター)16 bit
画素7–8OffsetX16 bit
画素9–10OffsetY16 bit

根拠:CyclonePlusマニュアル p.17、OptrImageStamp。本書はMono8を対象にします。10-bitでは先頭9画素のビット配置が異なり、Unpacked時のRAM上の画素サイズも考慮します。12-bit等へこの読取り式を流用しません。

2-6. OptrImageStampを有効にして読み戻す

ImageStampは画像を受信する前にカメラ側で有効にします。SetValueEnumByValueNameは数値ではなくOnという列挙名を指定できるため、意味を読み取りやすくできます。ノード名の大文字・小文字を含めてXMLと一致させ、設定後にOnが返ることを確認します。なぜ読戻しまで行うのでしょうか。後段の解析は、先頭画素が撮像情報へ置き換わっていることを前提にするからです。設定に失敗したまま解析すると、普通の画素値をカウンターとして誤認します。そのため、この機能を使う記録では、有効化と確認を取得開始前の必須条件にします。また、解析形式はPixelFormatにも合わせます。

説明用C++ · 指定サンプルのAPIを基に構成
// 2-6. OptrImageStampを有効にして読み戻す:本文と同じ順番で処理する。
setEnum("OptrImageStamp", "On");
char stampMode[32]={}; size_t size=sizeof(stampMode);
// 適用された列挙値を文字列で読み戻す。
check(KYParametersHandler_GetValueEnumAsString_V1(
    hRemoteDeviceParameterCollectionHandle,"OptrImageStamp",
    stampMode,&size,nullptr,nullptr), "Read OptrImageStamp");
stampMode[sizeof(stampMode)-1]='\0';
if (std::strcmp(stampMode,"On")!=0)
    throw std::runtime_error("Image stamp was not enabled");
// 読み戻した結果を保持し、Callback内で毎回パラメータを問い合わせない。

未対応の場合は、先頭画素をStampと解釈してはいけません。Stamp必須の用途なら取得を中止し、任意ならOffとして通常画像を扱います。本書ではOn確認後に第3章へ進みます。KAYA側のフレームIDやns時刻とは別の情報なので、同じ変数へ混在させず保持します。さらに、Stampは画像の前に新しい領域を付け足す仕組みではありません。既存の先頭画素を置き換えるため、その部分の被写体情報は保持されません。したがって、解析や保存では撮像情報が含まれる領域を意識して扱います。一方、連番や位置情報を画像と一緒に残せるので、取得結果を後から照合する手掛かりになります。

VP2 API資料の参照ページ
冊子p.17:パラメータの読書き(OptrImageStampはカメラ固有機能)
カメラ固有仕様:CyclonePlus取扱説明書 p.17:OptrImageStamp

2-7. 読み戻した画像条件を受信側へ通知する

カメラへ設定しただけでなく、フレームグラバー側にも画像の解釈に必要な情報を伝えます。指定サンプルのSetImageDetailsがこの役割です。PixelFormatの数値・名前、幅・高さを読み戻して渡すことで、次のPayloadサイズ取得を確定した画像条件に基づかせます。ここで設定値を二度扱うのは、カメラが作る画像と、ボードが受け取る画像の認識を一致させるためです。特に画素形式を変えた場合は、一画素あたりのデータ量も変わり得ます。そこで、希望した値ではなく実際に読み戻した条件を通知します。この手順を終えてからStream側の転送形式と受信領域の大きさを確認します。

説明用C++ · 指定サンプルのAPIを基に構成
// 2-7. 読み戻した画像条件を受信側へ通知する:本文と同じ順番で処理する。
int64_t width=0,height=0; uint64_t formatValue=0;
size_t size=sizeof(width);
// 現在の実値を読み戻す。sizeは呼出しごとに設定する。
check(KYParametersHandler_GetValue_V1(hRemoteDeviceParameterCollectionHandle,
    "Width",&width,&size,nullptr,nullptr), "Read Width");
size=sizeof(height);
// 現在の実値を読み戻す。sizeは呼出しごとに設定する。
check(KYParametersHandler_GetValue_V1(hRemoteDeviceParameterCollectionHandle,
    "Height",&height,&size,nullptr,nullptr), "Read Height");
size=sizeof(formatValue);
// 現在の実値を読み戻す。sizeは呼出しごとに設定する。
check(KYParametersHandler_GetValue_V1(hRemoteDeviceParameterCollectionHandle,
    "PixelFormat",&formatValue,&size,nullptr,nullptr), "Read PixelFormat value");
if (width<=0 || height<=0 || uint64_t(width)>UINT32_MAX || uint64_t(height)>UINT32_MAX)
    throw std::runtime_error("Invalid image size");
KYVP_DEVICE_IMAGE_DETAILS details={}; details.m_uVersion=1;
details.m_uWidth=uint32_t(width); details.m_uHeight=uint32_t(height);
details.m_uPixelFormat=uint32_t(formatValue);
size=sizeof(details.m_szPixelFormatName);
// 適用された列挙値を文字列で読み戻す。
check(KYParametersHandler_GetValueEnumAsString_V1(hRemoteDeviceParameterCollectionHandle,
    "PixelFormat",details.m_szPixelFormatName,&size,nullptr,nullptr), "Read PixelFormat name");
KY_BOOL tapAvailable=KY_FALSE;
// 対象ノードを現在の構成で利用できるか調べる。
check(KYParametersHandler_IsParameterAvailable_V1(hRemoteDeviceParameterCollectionHandle,
    "DeviceTapGeometry",&tapAvailable,nullptr,nullptr), "DeviceTapGeometry available");
if (tapAvailable) {
    size=sizeof(details.m_szTapGeometryType);
    // 適用された列挙値を文字列で読み戻す。
    check(KYParametersHandler_GetValueEnumAsString_V1(hRemoteDeviceParameterCollectionHandle,
        "DeviceTapGeometry",details.m_szTapGeometryType,&size,nullptr,nullptr), "Read tap geometry");
}
// 確定した画像条件をカメラ接続の受信側へ知らせる。
check(KYVPExtension_RemoteDevice_SetImageDetails_V1(hRemoteDeviceHandle,details), "SetImageDetails");

このAPIはカメラのWidthやHeightを書き換えるAPIではありません。確定した設定を受信側へ知らせます。DeviceTapGeometryは対応する場合だけ取得します。録画の途中でROIやPixelFormatを変えたくなった場合も、いったん停止し、この通知とバッファ確保をやり直します。つまり、画像条件の変更はカメラだけで完結しません。受信側の設定とメモリ容量まで一組で更新する必要があります。また、前回の値を変数に残したまま再利用すると不一致を見落としやすいため、再設定時も読戻しから繰り返します。こうして条件を確定すると、次の章では実際の転送に必要な領域を基準に準備できます。

VP2 API資料の参照ページ
冊子p.16:KYVPLibExtension概要(SetImageDetailsの個別仕様は未掲載)

この章の完了条件
解像度・露光・ゲイン・Stampの設定と読み戻しが完了しました。画像条件を受信側へ通知した状態で、次の章のStreamとRAMの準備へ進みます。

STEP 03

03. メモリーレコーディング

Streamと受信バッファを作り、到着した画像をアプリケーション所有のRAMへコピーします。受信バッファを保持するだけでは録画になりません。

Streamオープン → XML/転送形式 → 通知登録 → Payload → 受信Buffer → 録画RAM → 取得開始 → コピー/再Queue → 指定枚数またはタイムアウト

カメラSDK受信Buffer(16個を再利用)→ コピー →録画RAM(100枚を保持)

3-1. Streamを開き、転送形式を確定する

Streamは画像データを受け取る通信経路です。DevGetNumDataStreamsで数を確認し、IDを指定して開きます。その後、Stream専用のXMLとCollectionを用意します。カメラの設定用CollectionとStreamの設定用Collectionは役割が異なるため、同じものを使い回しません。Streamを開いても、画像が自動で保存されるわけではありません。受信する場所と通知方法を準備し、最後に取得を開始する必要があります。そのため、この段階では転送条件を確定することに集中します。また、画像の詰め方が変わる設定は必要な容量にも関わるので、バッファ確保より前に扱います。

説明用C++ · 指定サンプルのAPIを基に構成
// 3-1. Streamを開き、転送形式を確定する:本文と同じ順番で処理する。
uint32_t count=0;
// カメラが持つ画像受信用Streamの数を確認する。
check(KYVPLibTL_DevGetNumDataStreams_V1(hLocalDeviceHandle,&count), "DevGetNumDataStreams");
if (count==0) throw std::runtime_error("No stream");
char id[128]={}; size_t size=sizeof(id);
// 指定インデックスのStreamを識別するIDを取得する。
check(KYVPLibTL_DevGetDataStreamID_V1(hLocalDeviceHandle,0,id,&size), "DevGetDataStreamID");
// 画像の受信経路を開く。この時点では撮像を開始しない。
check(KYVPLibTL_DevOpenDataStream_V1(hLocalDeviceHandle,id,&hStreamHandle), "DevOpenDataStream");
streamOpen=true;
char url[128]={}; size=sizeof(url);
// 受信側の設定を定義したStream XMLの場所を取得する。
check(KYVPLibTL_Stream_GetPortURL_V1(hStreamHandle,url,&size), "Stream_GetPortURL");
ParseXmlUrl(url,&streamXmlFile,LoadXML_ModuleRegisterMap_Stream);
if (!streamXmlFile.pData || streamXmlFile.uSize==0) throw std::runtime_error("Stream XML is empty");
// 対象モジュール専用のパラメータ集合を作る。
check(KYParametersHandler_CreateParameterCollection_V1(
    &hStreamParameterCollectionHandle,nullptr,nullptr), "Create stream collection");
streamCollectionState.created=true;
// 名前による設定操作を、実際のPort読書きへつなぐ。
check(KYParametersHandler_RegisterParameterCollectionTransport_V1(
    hStreamParameterCollectionHandle,Stream_Transport,nullptr,nullptr,nullptr), "Register stream transport");
streamCollectionState.transport=true;
// 登録したXMLとTransportを使い、パラメータ操作を使用可能にする。
check(KYParametersHandler_InitializeParameterCollection_V1(
    hStreamParameterCollectionHandle,nullptr,nullptr), "Initialize stream collection");
// 列挙型パラメータをOnやMono8などの値名で設定する。
check(KYParametersHandler_SetValueEnumByValueName_V1(
    hStreamParameterCollectionHandle,"PackedDataMode","Unpacked",nullptr,nullptr), "PackedDataMode");

LoadXML_ModuleRegisterMap_StreamはStream_ReadPortを使います。RemoteDevice用ローダーを渡すと、別の対象からXMLを読もうとして失敗します。転送形式を決めてからPayloadを取得してください。原本CreateStreamは準備をまとめていますが、本書ではサイズに影響する設定を先に確定する順に分けています。したがって、Stream用Collectionの作成も記録します。後続の設定が失敗しても、開いたStreamと作成済みCollectionは残るからです。第4章ではこれらを子から順に片付け、XMLの寿命もその順序に合わせます。

VP2 API資料の参照ページ
冊子p.11:Stream ID取得/オープン冊子p.15:Streamの説明冊子p.17:Collection初期化

3-2. 受信通知を登録し、受信バッファをQueueする

DataStream_Callback_Registerは、画像が到着した際に呼ぶ関数を登録します。DSAllocAndAnnounceBufferは受信メモリを確保してStreamへ登録し、DSQueueBufferはそのバッファを次の受信に使える状態にします。確保するだけでは受信に使われないので、Queueまで行います。16個は使い回す受信領域です。一方、100枚を後で参照するための保存領域は次の節で別に用意します。両者を分けるのは、受信に戻したバッファへ次の画像が書き込まれるためです。また、画像到着直後から処理できるように、通知登録と全バッファのQueueを取得開始前に完了させます。

説明用C++ · 指定サンプルのAPIを基に構成
// 3-2. 受信通知を登録し、受信バッファをQueueする:本文と同じ順番で処理する。
// 以下はCallbackからも参照する共有変数として定義する。
static constexpr size_t NUMBER_OF_BUFFERS=16;
static KYVP_BUFFER_HANDLE buffers[NUMBER_OF_BUFFERS];
static size_t allocatedBuffers=0, payloadBytes=0;
// StreamCallbackは3-5で定義し、ここより前に同じ宣言を置く。
// 画像到着時に実行する関数を、取得開始前に登録する。
check(KYVPExtension_DataStream_Callback_Register_V1(
    hStreamHandle,StreamCallback,nullptr), "Register callback");
callbackRegistered=true;
KYVP_INFO_DATATYPE type; size_t size=sizeof(payloadBytes);
// Streamの情報を取得する。ここでは必要な受信領域サイズを調べる。
check(KYVPLibTL_DSGetInfo_V1(hStreamHandle,KYVP_STREAM_INFO_CMD_PAYLOAD_SIZE,
    &type,&payloadBytes,&size), "Get payload size");
if (payloadBytes==0) throw std::runtime_error("Empty payload");
for (size_t i=0;i<NUMBER_OF_BUFFERS;++i) {
    // SDKに受信領域を確保させ、Streamへ登録する。
    check(KYVPLibTL_DSAllocAndAnnounceBuffer_V1(
        hStreamHandle,payloadBytes,nullptr,&buffers[i]), "Allocate buffer");
    ++allocatedBuffers;
    // バッファを次の受信に利用できる状態へ戻す。二重Queueしない。
    check(KYVPLibTL_DSQueueBuffer_V1(hStreamHandle,buffers[i]), "Queue buffer");
}

PayloadはSDKが要求する受信領域の大きさです。幅×高さだけで推定せず、APIの値を使います。allocatedBuffersを確保成功直後に増やす理由は、その次のQueueが失敗しても解放対象を失わないためです。途中失敗時は第4章で成功済みバッファだけを解除します。ここをもう少し掘り下げると、確保とQueueは別々に失敗し得る操作です。したがって、ループの最後だけで個数を増やすと、確保済みの領域を後片付けの対象から外してしまいます。また、通知登録の成功も同様に記録します。こうして段階ごとの状態を保持すると、途中まで準備できた場合にも対応する解除を選べます。

VP2 API資料の参照ページ
冊子p.11:Buffer確保・Queue冊子p.15:Bufferの状態冊子p.16:Extension概要(Direct Callbackの個別仕様は未掲載)

3-3. 100枚分の録画RAMを準備する

受信バッファは次々に再利用されるため、画像を後で使うには別の保存先が必要です。録画RAMにはPayload×枚数の領域を確保します。さらにStampやKAYA時刻の保存場所を用意し、画像と同じインデックスで管理すると、撮影順序を後から確認できます。ここで区別したいのは、画像を運ぶための領域と、結果を残すための領域です。前者をそのまま保存先と考えると、次の受信で過去の画像が上書きされます。そこで、通知を受けるたびに後者へコピーします。また、記録枚数や終了要求は制御側からも参照するため、共有状態を同期して扱い、書込み途中の結果を読まないようにします。

説明用C++ · 指定サンプルのAPIを基に構成
// 3-3. 100枚分の録画RAMを準備する:本文と同じ順番で処理する。
struct Stamp {
    uint16_t imageCounter=0,triggerCounter=0,offsetX=0,offsetY=0;
    uint32_t microseconds=0;
};
static constexpr size_t recordFrames=100;
static std::vector<uint8_t> recordingMemory;
static std::vector<Stamp> imageStamps;
static std::vector<uint64_t> kayaTimes;
static std::atomic<size_t> recorded{0};
static std::atomic<bool> recording{false},receiveFailed{false};
static std::mutex copyMutex;
if (payloadBytes<11 || recordFrames>(std::numeric_limits<size_t>::max)()/payloadBytes)
    throw std::runtime_error("Invalid recording allocation size");
// 受信バッファとは別に、全フレームを保持する領域を先に確保する。
recordingMemory.resize(payloadBytes*recordFrames);
// 画像と同じ添字で、カメラ側StampとKAYA側の時刻を保存する。
imageStamps.resize(recordFrames);
kayaTimes.assign(recordFrames,0);
recorded=0; recording=false; receiveFailed=false;

例えばPayloadが2 MiBなら、録画100枚だけで200 MiB、SDK受信16枚でさらに32 MiBが必要です。積の桁あふれと確保失敗を処理してください。この例ではStampを有効にしたMono8を前提とするため、先頭11画素を読めるサイズであることも確認しています。したがって、記録枚数を増やす前に、受信領域も含めた合計容量を見積もります。特に幅と高さや画素形式を変えた場合は、前回と同じ枚数でも使用量が変わります。また、確保を画像到着のたびに行うと処理時間がばらつくため、開始前にまとめて用意します。準備に失敗した場合はカメラを動かさず、終了処理へ進めます。

VP2 API資料の参照ページ
冊子p.15:Bufferの所有・再利用(録画RAMはアプリ側の追加処理)

3-4. Mono8のOptrImageStampを読み取る

Mono8では1画素が1バイトなので、先頭11バイトから5種類の情報を復元できます。複数バイトの値は上位側から並んでいるため、シフトして組み立てます。これは画像の一部を読む処理であり、KAYAのBuffer Infoから返るフレーム番号や時刻を読む処理とは別です。例えば二つのバイトを使う値は、先頭側を8ビット左へ移し、次の値と合わせて復元します。これにより、実行するPCのメモリ上の並び方に依存せず解釈できます。ただし、10ビット形式では配置が異なるため、この関数をそのまま流用できません。したがって、形式の確認と11バイト以上の有効データがあることの確認を先に行います。

説明用C++ · 指定サンプルのAPIを基に構成
// 3-4. Mono8のOptrImageStampを読み取る:本文と同じ順番で処理する。
static Stamp readMono8Stamp(const uint8_t* p) {
    Stamp s;
    // 先頭2画素を上位・下位として連結し、画像カウンターを復元する。
    s.imageCounter = uint16_t((uint16_t(p[0])<<8)|p[1]);
    // 3画素を連結する。単位はµs、値の幅は24 bit。
    s.microseconds = (uint32_t(p[2])<<16)|(uint32_t(p[3])<<8)|p[4];
    // カメラが受理した外部トリガーの数を読む。
    s.triggerCounter = uint16_t((uint16_t(p[5])<<8)|p[6]);
    // この画像に対応するROIの横方向・縦方向の位置を読む。
    s.offsetX = uint16_t((uint16_t(p[7])<<8)|p[8]);
    s.offsetY = uint16_t((uint16_t(p[9])<<8)|p[10]);
    return s;
}
// 呼出し条件:Mono8、Stamp=On、画像先頭アドレス、11バイト以上を確認済み。
// 24-bit時刻の差分例(間隔が1周未満の場合に限る):
// uint32_t deltaUs=(current.microseconds-previous.microseconds)&0x00FFFFFFu;

24-bitのµsカウンターは計算上約16.78秒で一周するため、単純に大小を比較すると時間が逆転したように見えます。画像カウンターも16-bitの範囲で一周します。欠落や長い停止、リセットも考慮し、KAYA時刻などと合わせて評価してください。Stampの画素を通常の画像解析へ混ぜないことも重要です。つまり、カウンターの値が小さくなっただけで欠落や異常と決め付けることはできません。連続した記録で一周をまたいだのか、取得が途切れたのかを周辺の情報と照合します。また、画像番号と外部トリガー番号も意味が異なるため、別の項目として保存します。これにより、後から撮影と信号の関係を調べやすくなります。

VP2 API資料の参照ページ
冊子p.15:Bufferの位置付け(Stamp復元はVP2 APIではない)
カメラ固有仕様:CyclonePlus取扱説明書 p.17:ImageStampのビット配置

3-5. Callbackで画像をコピーしてバッファを返す

DSGetBufferInfoで受信画像の先頭アドレスを取り出し、録画中だけRAMへコピーします。コピーが終わる前にQueueするとSDKが同じ領域を再利用する可能性があるため、再Queueは最後です。Callbackではファイル保存やパラメータ設定をせず、短い処理に限定します。ここで受け取るポインターはSDKの受信領域を指しており、保存済み画像そのものではありません。したがって、後から必要になるデータは再Queueより前に複写します。一方、再Queueを忘れると利用できる受信領域が減るため、エラー経路にも注意が必要です。また、通知内の処理が長引くほど次の受信処理へ影響しやすくなります。

説明用C++ · 指定サンプルのAPIを基に構成
// 3-5. Callbackで画像をコピーしてバッファを返す:本文と同じ順番で処理する。
static void KYVP_CALLCONV StreamCallback(KYVP_BUFFER_HANDLE buffer,void*) {
    if (KYVPLibTL_BufferHandleIsNull(buffer)) return;
    std::lock_guard<std::mutex> guard(copyMutex);
    const KYVP_BUFFER_INFO* info=nullptr;
    KYVP_INFO_DATATYPE type; size_t size=sizeof(info);
    // 受信したバッファの先頭アドレスや時刻情報を取得する。
    KY_RESULT r=KYVPLibTL_DSGetBufferInfo_V1(hStreamHandle,buffer,
        KYVP_BUFFER_INFO_CMD_KYVP_BUFFER_INFO,&type,&info,&size);
    if (KY_RESULT_FAILED(r) || !info || !info->m_pBase) {
        receiveFailed=true;
    } else if (recording.load()) {
        size_t i=recorded.load();
        if (i<recordFrames) {
            // 完全な画像でコピー可能な長さであることをSDKの情報で確認する。
            // この最小断片は正常なMono8フレームを前提とする。
            auto* pixels=static_cast<const uint8_t*>(info->m_pBase);
            // i枚目の保存位置へコピーする。Queue前にコピーを終える。
            std::memcpy(recordingMemory.data()+i*payloadBytes,pixels,payloadBytes);
            imageStamps[i]=readMono8Stamp(pixels);
            uint64_t timeNs=0; size=sizeof(timeNs);
            // 受信したバッファの先頭アドレスや時刻情報を取得する。
            if (KY_RESULT_SUCCEEDED(KYVPLibTL_DSGetBufferInfo_V1(hStreamHandle,buffer,
                    KYVP_BUFFER_INFO_CMD_TIMESTAMP_NS_CHRONO,&type,&timeNs,&size)))
                kayaTimes[i]=timeNs;
            // 画像と付随情報を書き終えてから、完了枚数を公開する。
            recorded.store(i+1);
            if (i+1==recordFrames) recording=false;
        }
    }
    // バッファを次の受信に利用できる状態へ戻す。二重Queueしない。
    if (KY_RESULT_FAILED(KYVPLibTL_DSQueueBuffer_V1(hStreamHandle,buffer)))
        receiveFailed=true;
}

原本StreamCallbackはBuffer情報を取得して再Queueします。本書では、その間に画像コピーとStamp読取りを追加しました。実装では不完全フレーム・有効データ長も使用SDKのBuffer情報で判定してください。エラーはreceiveFailedで制御側へ伝え、Callback内部からCloseAllを呼ばない構成にします。とくに、確保容量と受信済みの有効長は同じとは限りません。したがって、この例の正常フレームという前提を実製品で使う際は、コピー前の検証を追加します。また、異常を制御側へ伝えた後も、終了が確認できるまでは共有データの寿命を保ちます。

VP2 API資料の参照ページ
冊子p.8:Callback内の処理・スレッド安全性冊子p.11:再Queueの順序冊子p.15:Buffer情報の取得

3-6. 受信側、カメラ側の順に取得を開始する

DSStartAcquisitionはフレームグラバー側の受信を開始します。その後、カメラのAcquisitionStartコマンドを実行して画像の送信を開始させます。先にカメラを動かすと受信準備が間に合わないため、この順序を守ります。RAMの準備も完了してから記録を有効にします。この順序は、受け皿を用意してから送信を始めると考えると理解しやすくなります。また、開始APIが成功しても、外部条件や通信状態によって画像が届かない場合があります。そこで、待機には時間制限と受信エラーの確認を入れます。指定枚数に達した場合も待機を抜けるだけで終わらず、次の停止処理まで必ず進めます。

説明用C++ · 指定サンプルのAPIを基に構成
// 3-6. 受信側、カメラ側の順に取得を開始する:本文と同じ順番で処理する。
recording=true;
// カメラの送信を始める前に、フレームグラバー側の受信を開始する。
check(KYVPLibTL_DSStartAcquisition_V1(hStreamHandle,
    KYVP_ACQ_START_FLAGS_DEFAULT,0), "DSStartAcquisition");
streamStarted=true;
int64_t startValue=1; size_t size=sizeof(startValue);
cameraStartAttempted=true;
// 型とサイズを合わせた値を対象パラメータへ書き込む。
check(KYParametersHandler_SetValue_V1(hRemoteDeviceParameterCollectionHandle,
    "AcquisitionStart",&startValue,&size,nullptr,nullptr), "AcquisitionStart");
const ULONGLONG begin=GetTickCount64();
bool timedOut=false;
while (recorded.load()<recordFrames && !receiveFailed.load()) {
    if (GetTickCount64()-begin>30000) { timedOut=true; break; }
    Sleep(10);
}
recording=false;
// 次は必ず第4章の停止へ進む。ここではRAMを解放しない。

取得数0は、このサンプルの連続取得指定です。100枚到達で止めているのはRAMへのコピーであり、カメラとStreamは第4章で明示的に止めます。原本の限定枚数取得はDSStartAcquisitionへ枚数を渡す別の使い方です。RAM記録フラグの変更はTriggerSoftwareの発行ではありません。したがって、「必要枚数の保存」と「機器の停止」は分けて管理します。前者だけでメモリを解放すると、まだ届く通知がその領域に触れる可能性があるからです。また、待機の途中で失敗した場合も、開始済みの経路に対応する停止が必要です。正常終了と異常終了を同じ後片付けへ集めると流れを追いやすくなります。

VP2 API資料の参照ページ
冊子p.10:Figure 1:受信開始→カメラ開始冊子p.11:DSStartAcquisitionとカメラへの書込み冊子p.17:名前によるコマンド実行の関連フロー

3-7. 参考:PDFのイベント方式との対応

API Data BookのフローはNEW_BUFFERイベントを待つ方式です。指定サンプルはコンパイル時の定義を切り替えると、この経路を使えます。本書の基本例はDirect Callbackなので、以下を重ねて実行しません。登録・受信・解除を選んだ方式でそろえることが重要です。二つの方式の違いは、画像到着をアプリケーションへ伝える方法にあります。Callback方式は登録した関数へ通知され、イベント方式は受信側がイベントを待ちます。ただし、どちらの場合も画像の所有権や再Queueの必要性は意識しなければなりません。そのため、方式を切り替える際は通知部分と終了部分を一緒に見直します。

説明用C++ · 指定サンプルのAPIを基に構成
// 3-7. 参考:PDFのイベント方式との対応:本文と同じ順番で処理する。
// Direct Callback方式の代わりに使う場合のみ。
KYVP_EVENT_HANDLE eventHandle;
// Direct Callbackの代わりにNEW_BUFFERイベントを登録する。
check(KYVPLibTL_DSRegisterEvent_V1(hStreamHandle,
    KYVP_EVENT_TYPE_NEW_BUFFER,&eventHandle), "DSRegisterEvent");
// 専用スレッドのループ内:
KYVPLIBTL_EVENT_NEW_BUFFER_DATA eventData={};
size_t size=sizeof(eventData);
// 受信スレッドで新しい画像を待つ。無期限待ちを避ける。
KY_RESULT r=KYVPLibTL_EventGetData_V1(eventHandle,&eventData,&size,1000);
if (KY_RESULT_SUCCEEDED(r) && !KYVPLibTL_BufferHandleIsNull(eventData.hBufferHandle))
    StreamCallback(eventData.hBufferHandle,nullptr);
// 上の関数が再Queueするため、ここでは二重にQueueしない。

イベント方式では終了要求をスレッドへ伝え、有限時間の待機から戻った後にjoinします。Direct Callback方式ではこの受信スレッドは作りません。どちらも最終的には同じ画像コピーを行います。第4章では、DirectのUnregisterとイベントのDSUnregisterEventを対応付けます。また、待機を無期限にすると、画像が来ない状況で終了要求を確認できないことがあります。そこで、有限時間の待機と終了フラグを組み合わせます。ただし、待機が戻っただけではスレッド終了の証明にならないため、joinで確認します。その後に通知や関連メモリを片付けることで、終了途中のアクセスを防ぎます。

VP2 API資料の参照ページ
冊子p.11:DSRegisterEvent・EventGetData・DSUnregisterEvent

この章の完了条件
指定枚数に到達したか、タイムアウト/受信エラーを検出しました。撮像はまだ動作している可能性があるので、次の章の停止・終了処理へ必ず進みます。

STEP 04

04. カメラクローズ

最初に受信を止めてから録画結果を確定し、Stream、カメラ、フレームグラバー、SDKの順に片付けます。途中失敗時も同じ方向で戻します。

記録無効化 → 取得停止 → 通知解除/受信終了 → 結果確定 → Buffer解除 → Stream → カメラ → フレームグラバー → TL/SDK → 所有メモリ

4-1. Streamとカメラの取得を停止する

停止は、DSStopAcquisitionによる受信側停止と、AcquisitionStopによるカメラ側停止の両方が必要です。どちらか一方の失敗で、もう一方の停止を省略しないようにします。原本StopAcquisitionとPDFの順序に合わせ、受信側を先に停止します。まず記録フラグを下げるのは、新しい画像を保存対象へ追加しないためです。ただし、それだけで通信や通知が停止するわけではありません。そこで、受信側とカメラ側へそれぞれ停止を要求します。また、片方の戻り値だけを見て終了を完了扱いにせず、両方の結果を確認してから通知の解除へ進むことが重要です。

説明用C++ · 指定サンプルのAPIを基に構成
// 4-1. Streamとカメラの取得を停止する:本文と同じ順番で処理する。
recording=false;
bool stopOk=true;
if (streamStarted) {
    // Streamの受信を停止する。続いてカメラ側も止める。
    KY_RESULT r=KYVPLibTL_DSStopAcquisition_V1(hStreamHandle,KYVP_ACQ_STOP_FLAGS_DEFAULT);
    if (KY_RESULT_SUCCEEDED(r)) streamStarted=false;
    else stopOk=false;
}
if (cameraStartAttempted) {
    int64_t value=0; size_t size=sizeof(value);
    // 型とサイズを合わせた値を対象パラメータへ書き込む。
    KY_RESULT r=KYParametersHandler_SetValue_V1(hRemoteDeviceParameterCollectionHandle,
        "AcquisitionStop",&value,&size,nullptr,nullptr);
    if (KY_RESULT_SUCCEEDED(r)) cameraStartAttempted=false;
    else stopOk=false;
}
// stopOkがfalseなら失敗を保存し、正常停止した前提でメモリを解放しない。

cameraStartAttemptedを開始コマンドの直前に立てた理由は、エラーが返ってもカメラへ命令が届いている可能性を考慮するためです。停止が成功するまでは、受信Bufferや録画RAMの寿命を保ちます。終了APIの失敗は記録し、使用SDKに合わせた回復処理へ進めます。つまり、開始APIの失敗は「何も始まらなかった」と必ずしも同じではありません。通信の途中で結果を受け取れなかった場合も考え、停止を試みる対象を記録します。一方、停止に失敗した状態で領域だけを破棄すると、受信処理が無効な場所へ触れ得ます。したがって、後続の解放へ進める条件を明確にしておきます。

VP2 API資料の参照ページ
冊子p.11:DSStopAcquisition→カメラ停止

4-2. 通知を解除し、受信処理の終了を確認する

Callback登録解除には、登録時と同じStreamと関数を渡します。イベント方式を選んだ場合は、受信スレッドの終了を確認してからDSUnregisterEventを呼びます。通知の登録を残したままBufferやStreamを閉じると、終了後に無効なメモリを参照する危険があります。ここで確認するのは、これから届く通知だけではありません。すでに動き始めた処理が終了しているかも重要です。したがって、登録解除の結果に加え、使用SDKの同期保証に沿って実行中処理の完了を確かめます。また、共有ロックを保持したまま終了待ちを行うと、通知側が同じロックを必要とする場合に進めなくなります。

説明用C++ · 指定サンプルのAPIを基に構成
// 4-2. 通知を解除し、受信処理の終了を確認する:本文と同じ順番で処理する。
if (callbackRegistered) {
    // 登録時と同じ関数を指定し、画像到着通知を解除する。
    KY_RESULT r=KYVPExtension_DataStream_Callback_Unregister_V1(hStreamHandle,StreamCallback);
    if (KY_RESULT_SUCCEEDED(r)) callbackRegistered=false;
    else stopOk=false;
}
// イベント方式のみ:終了フラグ通知 → 受信スレッドjoin → 下記解除。
// KYVPLibTL_DSUnregisterEvent_V1(hStreamHandle,KYVP_EVENT_TYPE_NEW_BUFFER);

// 使用SDKの停止/解除契約に従い、新規通知がなく、実行中Callbackも
// 終了したことを確認する。終了確認前には次の段階へ進まない。

原本StreamDeleteはRevokeの後に通知解除を行います。本書では、受信処理が終わったことを先に確定してから解放する順に整理しています。解除APIが進行中Callbackを待つかはコードだけでは確定できません。SDKの保証と必要な同期を確認し、停止APIをCallback用ロックを持ったまま呼ばないでください。このため、登録解除と実行完了の確認を一つの言葉で済ませず、実装上の条件を分けておきます。とくにCallbackが保存先を参照している間は、その領域を解放できません。まず新しい利用を止め、次に残っている利用が終わったことを確認する順番が、以後のメモリ解放を支えます。

VP2 API資料の参照ページ
冊子p.11:DSUnregisterEvent冊子p.16:Extension概要(Direct Callback解除の個別仕様は未掲載)

4-3. 録画結果を確定して画像を利用する

最終フレーム数は、コピー処理が終わってから確定します。タイムアウト時にも何枚か取得できている場合があるため、正常完了と部分記録を区別します。保存や画像処理を行うならこの段階で行い、録画RAMを解放する前に利用を終えてください。なぜ停止後に枚数を確認するのでしょうか。記録中に制御側が数えた値と、その直後にコピーが完了した結果がずれる場合があるからです。そこで、通知処理の終了を確認してから最終結果を読みます。また、予定枚数に満たなくても取得済みデータが無意味になるとは限りません。完了理由と有効枚数を一緒に残すと、部分記録も適切に扱えます。

説明用C++ · 指定サンプルのAPIを基に構成
// 4-3. 録画結果を確定して画像を利用する:本文と同じ順番で処理する。
// 前提:4-1と4-2が成功し、受信処理の終了確認が済んでいる。
const size_t captured=recorded.load();
const bool complete=(captured==recordFrames && !receiveFailed.load());
for (size_t i=0;i<captured;++i) {
    const uint8_t* frame=recordingMemory.data()+i*payloadBytes;
    const Stamp& stamp=imageStamps[i];
    const uint64_t hostTimeNs=kayaTimes[i];
    // frameとstampを必要な処理へ渡す。所有メモリなのでこの時点では有効。
    // ファイル保存する場合もCallback内ではなくここで行う。
}
printf("Recorded %zu frames (%s)\n",captured,complete?"complete":"partial/error");

frameは1枚分の記録データの先頭です。Payloadには転送上の領域が含まれる場合があるため、ファイルへ画像だけを書き出すときはPixelFormatや行配置に従います。Stampが入った先頭画素は元の画像に復元できません。kayaTimesとµsカウンターも単位と起点を分けて扱います。この段階ではSDKの受信バッファと録画RAMを分けて考えることが役立ちます。画像を別のRAMへコピー済みなら、受信領域の再利用による上書きを心配せず処理できます。ただし、参照中の録画RAMを先に解放しないようにします。また、画像番号と付随情報の添字をそろえたまま渡すことで、対応関係を維持できます。

VP2 API資料の参照ページ
冊子p.15:Bufferと画像データ(保存済みRAMの利用はアプリ側の処理)

4-4. 受信バッファの登録を解除する

DSRevokeBufferは、Streamへ登録した受信バッファを取り外すAPIです。確保済みの個数だけ実行し、成功したものを記録します。基本例はSDKがメモリを確保したため、戻ってきたポインターをアプリケーションがfreeする必要はありません。ただし、冊子p.15ではInputまたはOutputのQueueに残るバッファは解除できないと説明されています。したがって、停止しただけで解除可能と決め付けず、Queueの状態と戻り値を確認します。使用SDKの手順でQueueを整理するか、子リソースを解放するDSCloseの仕様に従い、失敗した解除を成功扱いにしないことが必要です。

説明用C++ · 指定サンプルのAPIを基に構成
// 4-4. 受信バッファの登録を解除する:本文と同じ順番で処理する。
bool allRevoked=true;
for (size_t i=0;i<allocatedBuffers;++i) {
    void* memory=nullptr; void* context=nullptr;
    // 受信処理終了後、Streamから受信バッファの登録を取り外す。
    KY_RESULT r=KYVPLibTL_DSRevokeBuffer_V1(hStreamHandle,buffers[i],&memory,&context);
    if (KY_RESULT_FAILED(r)) { allRevoked=false; continue; }
    // 解除済みフラグをバッファごとに記録し、再試行で二重解除しない。
    // DSAllocAndAnnounceBufferの領域はSDK所有。free(memory)しない。
}
if (allRevoked) allocatedBuffers=0;
// ユーザー確保方式のみ:Revoke成功後に元の確保方法に対応した解放を行う。
// _aligned_malloc → _aligned_free、cudaHostAlloc → cudaFreeHost。

指定サンプルには、BUF_ALIGNMENTを取得してアプリ側で整列メモリを確保し、DSAnnounceBufferで登録する代替方式もあります。その場合は解放責任がアプリ側にあります。SDK確保とユーザー確保を混同せず、解除に失敗したメモリを先に解放しないようにします。ここでは「誰が確保したか」が解放方法を決めます。たとえ同じ画像を入れる領域でも、SDK所有とアプリ所有では後片付けの責任が異なるからです。また、GPU用の代替経路を選ぶ場合も、その確保方法に対応する解放が必要です。したがって、方式を切り替える際は確保コードだけでなく終了コードも一組として変更します。

VP2 API資料の参照ページ
冊子p.15:DSRevokeBuffer・Queue制約・DSClose冊子p.22:SDK確保メモリと終了

4-5. パラメータコレクションを片付ける

CollectionはXMLを解釈し、Transportや任意Callbackを保持しています。PDFのParametersHandlerフローでは、任意Callback解除、Transport解除、Collection削除の順です。Streamとカメラの両方に使える補助関数を用意し、親ハンドルを閉じる前に終了させます。この順序は、設定操作が親のPortを使うためです。親を閉じてから解除しようとすると、無効な接続へ触れるおそれがあります。そこで、登録済みの通知と通信の結び付きを外し、最後にCollection本体を削除します。また、作成途中の失敗にも対応できるよう状態別に実行します。

説明用C++ · 指定サンプルのAPIを基に構成
// 4-5. パラメータコレクションを片付ける:本文と同じ順番で処理する。
// CollectionStateは1-2で宣言済み。第1・3章で成功状態を記録する。
static bool closeCollection(KYVP_COLLECTION_HANDLE& h,CollectionState& s) {
    if (!s.created) return true;
    if (s.callback) {
        // 登録済みの場合だけ、パラメータ変更通知を解除する。
        if (KY_RESULT_FAILED(KYParametersHandler_UnregisterParameterCollectionCallback_V1(
            h,nullptr,nullptr))) return false;
        s.callback=false;
    }
    if (s.transport) {
        // パラメータ集合とPort操作の結び付きを解除する。
        if (KY_RESULT_FAILED(KYParametersHandler_UnregisterParameterCollectionTransport_V1(
            h,nullptr,nullptr))) return false;
        s.transport=false;
    }
    // パラメータ集合を削除する。XMLはこの成功後に解放できる。
    if (KY_RESULT_FAILED(KYParametersHandler_DeleteParameterCollection_V1(h,nullptr,nullptr)))
        return false;
    h=KYVP_COLLECTION_HANDLE_INVALID; s.created=false;
    return true;
}

原本CloseParameterHandlerCollectionはTransport解除とCallback解除がPDFと逆順です。本書ではマニュアルの順序に整理しています。XMLの解放はCollection削除より後に行い、参照先を先に失わないようにします。したがって、任意Callbackを登録していない基本例では、その解除を呼ぶ必要はありません。一方、Transportは登録に成功していれば解除対象です。ここを状態で分岐すると、初期化の途中で失敗した場合も、存在するものだけを片付けられます。また、削除に失敗したときは成功フラグを消さず、参照中のXMLを保持します。

VP2 API資料の参照ページ
冊子p.17:Figure 4・Callback解除冊子p.18:Transport解除・Collection削除

4-6. Streamを閉じる

通知とBufferを片付けた後、Stream用Collectionを削除してDSCloseを呼びます。DSCloseは画像の通信経路を閉じ、関連する子ハンドルを無効にします。カメラのDeviceはこの後まで必要なので、先にDevCloseを呼ばないでください。ここまでの処理は、Streamを使う側を先に終わらせるための準備でした。そのため、Streamを閉じる位置を途中へ移してはいけません。また、クローズが成功した後はハンドルを無効値へ戻し、再利用を防ぎます。一方、失敗した場合は開いた状態として管理を続け、親のカメラを閉じてよいかを改めて判断します。

説明用C++ · 指定サンプルのAPIを基に構成
// 4-6. Streamを閉じる:本文と同じ順番で処理する。
// 前提:通知解除、受信終了確認、Revokeが完了。
// streamCollectionStateは3-1のCreate/Register成功を記録した状態。
if (!closeCollection(hStreamParameterCollectionHandle,streamCollectionState))
    return false; // boolを返す終了関数内の例
if (streamOpen) {
    // 通知とバッファを片付けたStreamを閉じる。
    KY_RESULT r=KYVPLibTL_DSClose_V1(hStreamHandle);
    if (KY_RESULT_FAILED(r)) return false;
    hStreamHandle=KYVP_STREAM_HANDLE_INVALID;
    streamOpen=false;
}
// streamXmlFileのpData/name、動的確保したStream URL/IDは所有分を解放してNULLへ。

DSCloseはSDK確保メモリを解放しますが、ユーザー確保メモリまでは解放しません。本書では所有関係を明確にするため、先にRevokeする流れを示しました。複数Streamを開いた場合は、すべてを閉じてから次のカメラクローズへ進みます。なお、明示的なRevokeがQueue状態のために失敗した場合は、その結果を残し、SDKのDSCloseによる子リソース解放の扱いを確認します。単にポインターを消すだけでは解除にはなりません。したがって、アプリ所有の領域も含め、SDKから参照されなくなったことを確認してから解放する必要があります。

VP2 API資料の参照ページ
冊子p.11:DSCloseの順序冊子p.15:Stream終了と子リソース冊子p.22:メモリ解放

4-7. カメラを閉じる

カメラ用CollectionやDeviceイベントがカメラへアクセスしなくなったことを確認し、DevCloseで接続を終了します。Remote PortはLocal Deviceに対応するハンドルなので、架空のRemoteDevice_Closeを追加するのではなく、DevClose後に保持値を無効にします。これで、画像経路を先に閉じ、その後でカメラの制御接続を閉じる流れになります。また、XMLやイベントの処理もDeviceに依存します。したがって、関連処理が残っていないことを確認してからクローズし、成功後に状態フラグを更新します。失敗時に接続済みの状態を消さないことも大切です。

説明用C++ · 指定サンプルのAPIを基に構成
// 4-7. カメラを閉じる:本文と同じ順番で処理する。
// 前提:このカメラの全Streamをクローズ済み。
if (!closeCollection(hRemoteDeviceParameterCollectionHandle,cameraCollectionState))
    return false;
// Deviceイベントを原本のAPIで登録した場合のみ、同じ関数を指定して解除。
// KYVPExtension_Device_EventCallBackUnregister_V1(
//     hLocalDeviceHandle,Device_EventCallBackImpl);
// 解除の成否とイベント処理終了を確認してから進む。
if (deviceOpen) {
    // 全Streamを閉じたカメラの接続を終了する。
    KY_RESULT r=KYVPLibTL_DevClose_V1(hLocalDeviceHandle);
    if (KY_RESULT_FAILED(r)) return false;
    hLocalDeviceHandle=KYVP_DEVICE_HANDLE_INVALID;
    hRemoteDeviceHandle=KYVP_REMOTE_DEVICE_HANDLE_INVALID;
    deviceOpen=false;
}
// カメラXML本体・名前・動的URLは、Collectionの参照終了後に解放。

指定サンプルのDeviceイベントは任意機能で、基本録画には登録不要です。使った場合だけ対になる解除を追加します。カメラだけを接続し直す用途では、ここまでで止め、InterfaceとTLを保持したまま第1章のカメラ検出から再開できます。ただし、再接続後に以前のRemoteハンドルやCollectionを使い回すことはできません。新しく得た接続からPortとXMLを準備し、プロパティも改めて確認します。また、完全終了する場合はここで終わらず、次のInterfaceとTLのクローズへ進みます。このように再接続と全体終了を分けると、残す資源の範囲が明確になります。

VP2 API資料の参照ページ
冊子p.11:DevCloseの順序冊子p.15:Device終了

4-8. フレームグラバーとSDKを閉じる

全カメラを閉じた後でIFCloseを呼び、その親であるTLをTLClose、最後にSDKをCloseLibで終了します。原本のInterfaceイベントやAUXイベントを登録した場合は先に解除します。PoCXPを切る運用なら、Interfaceの設定にアクセスできるうちにOFFにします。この順序は、最初に開いたものほど最後に閉じるという親子関係に対応します。ただし、給電制御のために作成したCollectionもInterfaceへ依存するため、Interfaceより前に終了します。また、複数カメラを開いた場合は一台だけの終了で先へ進まず、同じInterfaceに属する全接続の終了を確認します。

説明用C++ · 指定サンプルのAPIを基に構成
// 4-8. フレームグラバーとSDKを閉じる:本文と同じ順番で処理する。
// 任意登録の対になる解除。登録した場合だけ成否を確認して実行。
// KYVPExtension_PCIInterface_EventCallBackUnregister_V1(
//     hPCIInterfaceHandle,PCIInterface_EventCallBackImpl);
// KYVPExtension_AuxDataCallback_Unregister_Args args={};
// args.version=1; args.hIFHandle=hPCIInterfaceHandle;
// args.pCallbackFunction=PCIInterface_AuxEventCallBackImpl;
// KYVPExtension_PCIInterface_AuxDataCallback_Unregister(&args);
// 自分がONにしたPoCXPを必要に応じてOFF → Interface Collection/XMLを解放。
if (interfaceOpen) {
    // 全カメラを閉じたフレームグラバーを終了する。
    if (KY_RESULT_FAILED(KYVPLibTL_IFClose_V1(hPCIInterfaceHandle))) return false;
    hPCIInterfaceHandle=KYVP_PCI_INTERFACE_HANDLE_INVALID; interfaceOpen=false;
}
if (tlOpen) {
    // 全Interfaceを閉じた後に親モジュールTLを終了する。
    if (KY_RESULT_FAILED(KYVPLibTL_TLClose_V1(hTLHandle))) return false;
    hTLHandle=KYVP_TL_HANDLE_INVALID; tlOpen=false;
}
if (libraryReady) {
    // SDKが保持する全体リソースを終了する。
    if (KY_RESULT_FAILED(KYVPLibTL_CloseLib())) return false;
    libraryReady=false;
}
std::vector<uint8_t>().swap(recordingMemory);
std::vector<Stamp>().swap(imageStamps);
std::vector<uint64_t>().swap(kayaTimes);

AUX解除は指定サンプルの切替処理にありますが、CloseAllには同じ処理がないため、登録状態によっては終了経路へ追加します。ReleaseResourcesが扱うID配列やURLなども所有分を解放します。InitLib成功後にTLOpenが失敗した場合も、CloseLibの対象であることを忘れないでください。つまり、「接続済み」フラグだけでは十分ではありません。SDKだけが初期化された状態や、TLまで開いた状態を分けて管理します。また、SDK内部のスレッド終了にも関係するため、DLLをアンロードする前に正規の終了APIを完了させます。これにより、繰返し起動時の資源残留を調べやすくなります。

VP2 API資料の参照ページ
冊子p.11:IFClose・TL終了・CloseLib冊子p.14:初期化と終了の対応冊子p.22:DLL終了前の後片付け

4-9. 成功時と失敗時を同じ終了経路へ集める

一連の操作の途中で失敗しても、作成済みのリソースは残ります。通常処理をtryで実行し、catchの後でも終了処理を呼ぶ構造にすると、解放漏れを減らせます。終了処理自体は例外で途中放棄せず、成功状態とエラーを記録しながら第4章の順で実装します。例えば16個のうち数個だけバッファを確保できた場合でも、その数個は解放が必要です。そこで、全体の成否だけでなく、各段階の成功状態と確保数を終了処理へ渡します。また、最初のエラーが後続の終了エラーで見えなくならないよう、発生順も残します。こうすると、原因の調査と残った資源の回復を別々に判断できるようになります。

説明用C++ · 指定サンプルのAPIを基に構成
// 4-9. 成功時と失敗時を同じ終了経路へ集める:本文と同じ順番で処理する。
// 構造を示す疑似コード。下記の関数名はSDK APIではない。
int result=0;
try {
    // 第1章:SDK・FG・カメラ・Collectionを準備
    // 第2章:画像形式・露光・ゲイン・Stamp・SetImageDetails
    // 第3章:Stream・Buffer・RAM・取得・待機
} catch (const std::exception& e) {
    std::fprintf(stderr,"Operation failed: %s\n",e.what());
    result=1;
}
// 第4章を、成功した操作の状態に応じて実行。
// 停止 → 通知終了 → 結果利用 → Buffer → Collection/Stream
// → Collection/Device → Interface → TL/SDK → 所有RAM
// 解放に失敗した場合はresult=1として未解放対象を記録する。
return result;

子リソースが使用中なのに、失敗を無視して親だけを閉じてはいけません。正常100枚、未接続、設定失敗、途中の確保失敗、タイムアウト、繰返し開閉を実機で確認します。原本CloseAllの一度だけ実行するフラグだけでは、部分失敗からの回復をすべて保証できない点にも注意します。さらに、終了に成功した対象だけを無効化すると、再度後片付けを試す際にも残っている対象を判別できます。ただし、無条件な再試行は避け、失敗したAPIの条件に合わせて回復します。したがって、異常系の確認ではエラー表示だけでなく、どの資源が残ったか、再接続できるかまで確かめることが実装の仕上げになります。

VP2 API資料の参照ページ
冊子p.21:エラー結果冊子p.22:Library Exiting(例外処理の構成は本書の補助実装)

4-10. Function Call Sequenceとの最終照合

PDF Figure 1の番号本書での位置対応する処理
1–7第1章 1-3InitLib → TLOpen → 一覧更新 → Interface件数/ID/任意Info → Interfaceオープン
8–13第1章 1-5・1-6Device件数/ID/任意Info → Deviceオープン → Stream件数 → Remote Port。添付どおりDevice一覧更新とXML準備を補う
14–18第3章 3-1・3-2Stream ID/オープン → 通知登録 → Buffer確保/Queue。Payload・XML・転送形式も準備
19–22第3章 3-5〜3-7Stream受信開始 → カメラ開始 → 受信処理 → Buffer再Queue。カメラ開始はGenICamコマンド経由
23–26第4章 4-1〜4-6Stream停止 → カメラ停止 → 通知解除 → Streamクローズ。受信終了同期・Revoke・Collection解放を補う
27–30第4章 4-7・4-8Device → Interface → TL → SDKの終了
Figure 4第1章 1-6/第4章 4-5Collection作成・Transport登録・任意Callback登録・初期化・Get/Set・解除・削除

参照:Vision Point II API Data Book、冊子p.10–11(Figure 1)冊子p.17–18(Figure 4)。PDFのCloseTL表記に対し、添付で使われる関数名はTLClose_V1です。図のイベント方式とDirect Callback方式を混在させないでください。

参照したソースとダウンロード

以下は指定フォルダー内のサンプルです。本書の説明用コードは未適用です。添付のCソースと同一内容の.cppを、プロジェクトのビルド対象に合わせて収録しています。HTMLへ埋め込んでいるので、説明書単体から取得できます。実際のビルドにはインストール済みKAYA SDKのヘッダー・ライブラリ・実行時DLLが必要です。

本書のMono8のStamp読取りは、Optronisマニュアルの配置に従い、画像番号・時刻・外部トリガー番号・OffsetX/OffsetYを扱います。