メモリーレコーディング
概要
100fpsを超える高速画像データをHDD/SSDに書き込むと、計算上保存される枚数よりも実際に保存される画像が少ないことがあります(フレーム落ちと呼びます)。カメラから送られてくる画像データはメモリを経由してHDD/SSDに保存されますが、カメラから送られてくる画像データよりもHDD/SSDに書き込む速度が遅いため、メモリにあるデータがHDD/SSDに保存される前に上書きされてしまうことで発生しています。 このサンプルではカメラから送られてくる画像データをメモリに保存し、後処理で静止画保存しています。 予め画像を保存するためのメモリを確保することで、高速画像データであってもメモリ上の画像データを上書きすることなく画像を保存することができます。
サンプルプログラム
サンプル(Python) | memory_recording_python.zip |
---|
サンプルツールの出力
なし
(Pythonのフォルダ内にPNG形式で画像が保存されます)
コード全体
###############
# 解説1
###############
import ctypes
import tisgrabber as tis
import numpy as np
import cv2
ic = ctypes.cdll.LoadLibrary("./tisgrabber_x64.dll")
tis.declareFunctions(ic)
###############
# 解説2
###############
class CallbackUserdata(ctypes.Structure):
def __init__(self):
self.Value1 = 0
self.Value2 = 0
self.BufferSize=200
self.end = False
self.firstindex = -1
###############
# 解説3
###############
def FrameCallback(hGrabber, hMemBuffer, framenumber, pData):
pData.Value1 = pData.Value1 + 1
# リングバッファの最初の画像のインデックスを取得します
if( pData.firstindex == -1 ):
Index = ctypes.c_long()
ic.IC_MemBufferGetIndex(hMemBuffer, Index)
pData.firstindex = Index.value
###############
# 解説4
###############
def saveimage(hGrabber, i):
#### 解説4-1
membuffer = ctypes.c_void_p()
ic.IC_GetMemBuffer(hGrabber,i, membuffer)
#### 解説4-2
Width = ctypes.c_long()
Height = ctypes.c_long()
BitsPerPixel = ctypes.c_int()
ic.IC_GetMemBufferDescription( membuffer,Width, Height, BitsPerPixel)
bpp = int(BitsPerPixel.value / 8.0)
buffer_size = Width.value * Height.value * BitsPerPixel.value
# イメージデータを取得
imagePtr = ctypes.c_char_p()
ic.IC_MemBufferGetDataPtr(membuffer, imagePtr)
imagedata = ctypes.cast(imagePtr,
ctypes.POINTER(ctypes.c_ubyte *
buffer_size))
#### 解説4-3
# numpy配列を作成します
image = np.ndarray(buffer=imagedata.contents,
dtype=np.uint8,
shape=(Height.value,
Width.value,
bpp))
#### 解説4-4
image = cv2.flip(image, 0) #画像を反転させない
cv2.imwrite("test{0}.png".format(i),image)
# 空きメモリ、もう必要ありません。
# 画像はまだリングバッファにあります。
ic.IC_ReleaseMemBuffer( membuffer )
###############
# 解説5
###############
#この関数は、このライブラリの他の関数が呼び出される前に1回だけ呼び出す必要があります。
ic.IC_InitLibrary(0)
#ユーザーデータをインスタンス化
Userdata = CallbackUserdata()
# 関数ポインタを作成
Callbackfuncptr = ic.FRAMEREADYCALLBACKEX(FrameCallback)
#ダイアログ画面を表示
hGrabber = tis.openDevice(ic)
###############
# 解説6
###############
#### 解説6-1
#デバイスが有効か確認
if(ic.IC_IsDevValid(hGrabber)):
ic.IC_SetFrameReadyCallbackEx(hGrabber, Callbackfuncptr, Userdata)
#リングバッファの枚数
ic.IC_SetRingBufferSize(hGrabber,Userdata.BufferSize)
ic.IC_SetContinuousMode(hGrabber, 0)
ic.IC_StartLive(hGrabber, 0) #ライブスタート開始 引数:0の時非表示、引数:1の時表示
#### 解説6-2
#リングバッファに200(Userdata.BufferSize)枚撮りためるまで待つ
while (Userdata.Value1<Userdata.BufferSize):
cv2.waitKey(1)
ic.IC_StopLive(hGrabber)
#### 解説6-3
# 取得した画像を保存する
for i in range(Userdata.firstindex, Userdata.Value1+Userdata.firstindex-1):
saveimage(hGrabber, i)
else:
ic.IC_MsgBox(tis.T("No device opened"), tis.T("Callback"))
ic.IC_ReleaseGrabber(hGrabber)
使用するファイルについて
【callback_python.py】を動かす為に必ず下記の【dll(ライブラリ)】や【tisgrabber.py】が必要となります。dll(Dynamic Link Library、ダイナミックリンクライブラリ)とは、Windowsにおける共有ライブラリのことで、メーカーがユーザレベルで操作できるように管理しています。これらのdllを【tisgrabber.py】経由で読み込むことで、TheImagingSource社のカメラに簡単にアクセスできます。
tisgrabber.py | TheImagingSource社が提供するAPIであるIC Imaging Controlをカプセル化するtisgrabber.dllのラッパープログラム(IC Imaging Controlを使いやすくするためのもの)となっています。IC Imaging Control DLLの32ビットバージョン(tisgrabber.dll、TIS_UDSHL11.dll)と64ビットバージョン(tisgrabber_x64.dll、TIS_UDSHL11_64.dll)のどちらを使用するかが自動的にチェックしたり、IC Imaging Controlで使用されているメソッドを呼び出すために使われています。 |
---|---|
tisgrabber.dll | IC Imaging ControlをPythonで呼び出すための32 bitバージョンのDLL |
TIS_UDSHL11.dll | IC Imaging Controlの32 bitバージョンのDLL |
tisgrabber_x64.dll | IC Imaging ControlをPythonで呼び出すための64 bitバージョンのDLL |
TIS_UDSHL11_64.dll | IC Imaging Controlの64 bitバージョンのDLL |
callback_python.py (任意のプログラム) |
実行されるメインプログラム。 |
解説1:importで宣言する
###############
# 解説1
###############
import ctypes
import tisgrabber as tis
import cv2
import numpy as np
ic = ctypes.cdll.LoadLibrary("./tisgrabber_x64.dll")
tis.declareFunctions(ic)
ic.IC_InitLibrary(0)
Pythonで下記のライブラリを使用するためにimportを使います。
ctype | このライブラリは C と互換性のあるデータ型を提供し、共有ライブラリ(Windowsでは*.dll)の関数を呼び出すことができます。 |
---|---|
tisgrabber | IC Imaging Controlを制御するラッパーファイルを読み込むために使用します。 |
cv2 | OpenCVで画像処理を行うために使用します。 |
numpy | Pythonでの機械学習の計算をより速く、効率的に行えるようにするために使用します。 |
上記のライブラリを使用するため、あらかじめターミナルにて下記のコマンドを入力し、各ライブラリをインストールしておいてください。
pip install opencv-python
pip install numpy
解説2:ユーザーデータで宣言する
###############
# 解説2
###############
class CallbackUserdata(ctypes.Structure):
def __init__(self):
self.Value1 = 0
self.Value2 = 0
self.BufferSize=200 #撮影するフレーム数を指定
self.end = False
self.firstindex = -1
コールバック関数内で使用される変数を構造体で宣言しています。
データを保持したままコールバック関数に値を渡すことができますので、コールバック関数内で到着した画像のトータル枚数を数えたり、メモリに保存するトータル枚数を指定できたりします。ここでは「self.BufferSize=200」とすることで200枚のフレームを取得しメモリに保存するように宣言しています。
解説3:コールバック関数を宣言する
###############
# 解説3
###############
def FrameCallback(hGrabber, hMemBuffer, framenumber, pData):
pData.Value1 = pData.Value1 + 1
# リングバッファの最初の画像のインデックス番号を取得します
if( pData.firstindex == -1 ):
Index = ctypes.c_long()
ic.IC_MemBufferGetIndex(hMemBuffer, Index)
pData.firstindex = Index.value
TISカメラから送られてくる画像をPCに受け取るごとに呼び出されるコールバック関数を宣言しています。IC_MemBufferGetIndexでリングバッファの最初のインデックス番号(配列の初めの番号)を取得し、ユーザーデータpData.firstindexに格納します。これによってリングバッファの初めの位置を確認することができます。
解説4:メモリ内の画像をHDD/SSD内に保存する
###############
# 解説4
###############
#### 解説4-1
def saveimage(hGrabber, i):
membuffer = ctypes.c_void_p()
ic.IC_GetMemBuffer(hGrabber,i, membuffer)
#### 解説4-2
Width = ctypes.c_long()
Height = ctypes.c_long()
BitsPerPixel = ctypes.c_int()
ic.IC_GetMemBufferDescription( membuffer,Width, Height, BitsPerPixel)
bpp = int(BitsPerPixel.value / 8.0)
buffer_size = Width.value * Height.value * BitsPerPixel.value
# イメージデータを取得
imagePtr = ctypes.c_char_p()
ic.IC_MemBufferGetDataPtr(membuffer, imagePtr)
imagedata = ctypes.cast(imagePtr,
ctypes.POINTER(ctypes.c_ubyte *
buffer_size))
#### 解説4-3
# numpy配列を作成します
image = np.ndarray(buffer=imagedata.contents,
dtype=np.uint8,
shape=(Height.value,
Width.value,
bpp))
#### 解説4-4
image = cv2.flip(image, 0) #画像を反転させない
cv2.imwrite("test{0}.png".format(i),image)
# 空きメモリ、もう必要ありません。
# 画像はまだリングバッファにあります。
ic.IC_ReleaseMemBuffer( membuffer )
リングバッファで撮りためた画像をOpenCVで画像処理しpng形式で保存しています。 主な手順は下記の通りです。
解説4-1:リングバッファのポインタを取得
#### 解説4-1
def saveimage(hGrabber, i):
membuffer = ctypes.c_void_p()
ic.IC_GetMemBuffer(hGrabber,i, membuffer)
メモリに保存されたデータを操作するためにメモリに保存された画像にアクセスする必要があります。 そのためにIC_GetMemBufferで画像データがリングバッファのポインタを取得します。
解説4-2:イメージデータを取得
#### 解説4-2
Width = ctypes.c_long()
Height = ctypes.c_long()
BitsPerPixel = ctypes.c_int()
ic.IC_GetMemBufferDescription( membuffer,Width, Height, BitsPerPixel)
bpp = int(BitsPerPixel.value / 8.0)
buffer_size = Width.value * Height.value * BitsPerPixel.value
# イメージデータを取得
imagePtr = ctypes.c_char_p()
ic.IC_MemBufferGetDataPtr(membuffer, imagePtr)
imagedata = ctypes.cast(imagePtr,
ctypes.POINTER(ctypes.c_ubyte *
buffer_size))
IC_GetImagePtrでbit形式のイメージデータのポインタimagePtrを取得しています。imagePtrのポインタ型を使って、画像データの先頭から画像の最後のアドレスを指定し、ポインタ型に変換(ctypes.cast)しています。ポインタ変数imagedata が格納している画像データが入っているアドレスそのものですので、imagedataには画像データが格納されていることになります。
解説4-3:NumPy配列化
#### 解説4-3
# numpy配列を作成します
image = np.ndarray(buffer=imagedata.contents,
dtype=np.uint8,
shape=(Height.value,
Width.value,
bpp))
上記によって取得したimagedataを使ってNumpy配列を生成します。OpenCVで読み込む画像イメージがNumPyの配列であるndarray形式で表現されるため、上記のように変換する必要があります。
解説4-4:リングバッファにある画像を画像処理し、保存する
#### 解説4-4
image = cv2.flip(image, 0) #画像を反転させない
cv2.imwrite("test{0}.png".format(i),image)
# 空きメモリ、もう必要ありません。
# 画像はまだリングバッファにあります。
ic.IC_ReleaseMemBuffer( membuffer )
下記のOpenCVの画像処理(flip:読み込んだ画像を上下左右に反転)を行い、HDD/SSD内にPng形式で画像保存をしています。
【解説3:コールバック関数を宣言する】内にあるFrameCallbackの関数内でも画像処理や保存することも可能ですが、処理時間がかかってしまうため、リアルタイムで画像処理が必要ない場合には上記のように事後処理で処理することをお勧めします。
最後にIC_ReleaseMemBufferでPCのメモリ内に保存している画像データを解放します。
解説5:カメラをオープンする
###############
# 解説5
###############
#この関数は、このライブラリの他の関数が呼び出される前に1回だけ呼び出す必要があります。
ic.IC_InitLibrary(0)
#ユーザーデータをインスタンス化
Userdata = CallbackUserdata()
# 関数ポインタを作成
Callbackfuncptr = ic.FRAMEREADYCALLBACKEX(FrameCallback)
#ダイアログ画面を表示
hGrabber = tis.openDevice(ic)
コールバック関数を設定するための下準備として解説2で登場したCallbackUserdataの使用を宣言します(インスタンス化)。
また、解説3で宣言したコールバック関数FrameCallbackの関数ポインタを宣言し、IC_SetFrameReadyCallbackExの引数に渡せるように(後述)しておきます。「ic.IC_InitLibrary(0)」でICImagingControlクラスライブラリを初期化します。その後、「tis.openDevice(ic)」で下図のDevice Settingsダイアログ画面を起動します。
Device Settingsダイアログでは接続しているカメラを画面(GUI)上で選択することができます。Device Settingsダイアログでは解像度やフレームレート、Device Propetiesダイアログ画面ではカメラのゲインや露光時間といったプロパティの設定、Select Custom Format画面では画像の切出しを行い任意の解像度に設定することができます(機種によってはビニング機能を有効化できます)。Device SettingsダイアログでOKボタンを押すタイミングでデバイスの情報やプロパティの設定が記録されたdevice.xmlがPythonプログラムと同じ場所に出力されます。device.xmlが存在した場合にはDevice Settingsダイアログ画面は起動せず、device.xmlに記載された情報をもとに自動的にカメラと設定情報を読み込み次の処理に遷移します。もう一度ダイアログ画面を呼び出したいときにはdevice.xmlを削除してください。
解説6:メイン処理
###############
# 解説6
###############
#### 解説6-1
if(ic.IC_IsDevValid(hGrabber)):
ic.IC_SetFrameReadyCallbackEx(hGrabber, Callbackfuncptr, Userdata)
#リングバッファの枚数
ic.IC_SetRingBufferSize(hGrabber,Userdata.BufferSize)
ic.IC_SetContinuousMode(hGrabber, 0)
ic.IC_StartLive(hGrabber, 0)
#### 解説6-2
#ic.IC_MsgBox(tis.T("OKで停止します。"), tis.T("Callback"))
while (Userdata.Value1<Userdata.BufferSize):
cv2.waitKey(1)
ic.IC_StopLive(hGrabber)
#### 解説6-3
# 取得した画像を保存する
for i in range(Userdata.firstindex, Userdata.Value1+Userdata.firstindex-1):
saveimage(hGrabber, i)
else:
ic.IC_MsgBox(tis.T("No device opened"), tis.T("Callback"))
ic.IC_ReleaseGrabber(hGrabber)
ここまで画像処理や保存するための下準備として関数などを宣言してきました。
【解説2:ユーザーデータで宣言する】、【解説3:コールバック関数を宣言する】、【解説4:メモリ内の画像をHDD/SSD内に保存する】で宣言した構造体や関数を【解説5:カメラをオープンする】で取得したカメラと紐づけて呼び出します。
主な手順は下記の通りです。
解説6-1:コールバック関数を使用し画像取得を開始する
#### 解説6-1
#デバイスが有効か確認
if(ic.IC_IsDevValid(hGrabber)):
ic.IC_SetFrameReadyCallbackEx(hGrabber, Callbackfuncptr, Userdata)
#リングバッファの枚数
ic.IC_SetRingBufferSize(hGrabber,Userdata.BufferSize)
ic.IC_SetContinuousMode(hGrabber, 0)
ic.IC_StartLive(hGrabber, 0) #ライブスタート開始 引数:0の時非表示、引数:1の時表示
「IC_SetFrameReadyCallbackEx」を使って、使用するカメラhGrabber・関数ポインタCallbackfuncptr・ユーザーデータUserdataを引数に渡すことで解説2のユーザーデータと解説3のコールバック関数をそれぞれカメラに紐づけることができます。hGrabberを使ってコールバック関数を自動的に引き渡すために「ic.IC_SetContinuousMode(hGrabber, 0) 」と宣言をします。(コールバック関数を使用しないメソッド「IC_SnapImage」を使う場合には「ic.IC_SetContinuousMode(hGrabber, 1) 」と宣言をします。
「IC_SetWindowPosition」にて画像の表示位置を指定し、「IC_StartLive」でカメラから画像を送るように指示します。この時、引数:0の時には画面表示がなし、引数:1の時画面表示がある状態となります。「IC_StartLive」でコールバック関数FrameCallbackがようやく呼ばれるようになります。
解説6-2:リングバッファに保存するまで待つ
#### 解説6-2
#リングバッファに200(Userdata.BufferSize)枚撮りためるまで待つ
while (Userdata.Value1<Userdata.BufferSize):
cv2.waitKey(1)
ic.IC_StopLive(hGrabber)
【解説6-1:コールバック関数を使用し画像取得を開始する】で「IC_StartLive」でコールバック関数FrameCallbackが開始されます。FrameCallbackが呼び出され、リングバッファに200枚(Userdata.BufferSize)溜まるまで待ち続けます。
解説6-3:リングバッファにある画像データを保存する
#### 解説6-3
# 取得した画像を保存する
for i in range(Userdata.firstindex, Userdata.Value1+Userdata.firstindex-1):
saveimage(hGrabber, i)
else:
ic.IC_MsgBox(tis.T("No device opened"), tis.T("Callback"))
ic.IC_ReleaseGrabber(hGrabber)
リングバッファに200枚溜まったら【解説4:メモリ内の画像をHDD/SSD内に保存する】で定義したsaveimageを呼び出します。リングバッファに撮りためた最初の画像のインデックス番号(画像の1枚目)~最後の番号(画像の200枚目)まですべてpng形式で保存するようにしています。