DALSA線陣CCD開發紀要(C++)
應用背景:晶體表面疵病工業檢測,導軌運動的光柵尺反饋系統產生的脈衝用於外觸發Dalsa相機進行影象採集。
解決問題:Dalsa線陣CCD直接採集的影象是當前一行的影象,配套的採集卡中用於儲存影象的緩衝區有限,當平臺連續長距離運動時,如果不及時讀取緩衝區的影象,新採集的影象將覆蓋之前採集的影象。
閱讀Dalsa相機的開發文件中的繼承圖,如下:
我們最為關心的是緩衝區的內容SapBuffer和將採集內容轉運到緩衝區的SapAcqToBuf,細心一點的話還能看到採集內容轉運到緩衝區的回撥函式的Info。
檢視官方提供的一些開發Demo
// Transfer callback function is called each time a complete frame is transferred. // The function below is a user defined callback function. void XferCallback(SapXferCallbackInfo *pInfo) { // Display the last transferred frame SapView *pView = (SapView *) pInfo->GetContext(); pView->Show(); } // Example program // main() { // Allocate acquisition object SapAcquisition *pAcq = new SapAcquisition(SapLocation (“X64-CL_1”, 0), “MyCamera.ccf”); // Allocate buffer object, taking settings directly from the acquisition SapBuffer *pBuffer = new SapBuffer(1, pAcq); // Allocate view object, images will be displayed directly on the desktop SapView *pView = new SapView(pBuffer, SapHwndDesktop); // Allocate transfer object to link acquisition and buffer SapTransfer *pTransfer = new SapTransfer(XferCallback, pView); pTransfer->AddPair(SapXferPair(pAcq, pBuffer)); // Create resources for all objects BOOL success = pAcq->Create(); success = pBuffer->Create(); success = pView->Create(); success = pTransfer->Create(); // Start a continuous transfer (live grab) success = pTransfer->Grab(); printf("Press any key to stop grab\n"); getch(); // Stop the transfer and wait (timeout = 5 seconds) success = pTransfer->Freeze(); success = pTransfer->Wait(5000); printf("Press any key to terminate\n"); getch(); // Release resources for all objects success = pTransfer->Destroy(); success = pView->Destroy(); success = pBuffer->Destroy(); success = pAcq->Destroy(); // Free all objects delete pTransfer; delete pView; delete pBuffer; delete pAcq; return 0; }
不難發現:
首先需要建立Acquisition,這裡有裝置資訊,需要採集的影象資訊和設定(如影象寬度,高度),亦可通過官方SDK自帶的GUI對話方塊讀取配置檔案很快鍵地得到。
其次建立我們想要的緩衝區,由於我們的基本配置資訊已經通過Acquisition得到,因此在SapBuffer的眾多過載函式中選取了
SapBuffer( int count, SapXferNode* pSrcNode, SapBuffer::Type type = SapBuffer::TypeScatterGather, SapLocation loc = SapLocation::ServerSystem );
其中count為緩衝區的數目,它們顯然具有同配置檔案中影象的大小,資料格式。SapXferNode為SapAcquisition的父類,直接傳遞Acquisition的指標即可,後面採用預設的引數。
(接下來是用於顯示影象的SapView類,將緩衝區與用於顯示的控制元件的視窗控制代碼繫結起來,就可以將影象顯示在指定的控制元件上。這個與採集過程關係不大,但視覺化這個,大家懂得。)
接下來是比較重要的步驟,建立了從影象資料採集到緩衝區的轉移步驟。由於獲取完整影象(理想情況下影象高度設定在80000,但緩衝區大小有限,設定在30000)時間較長,因此有足夠的時間將緩衝區資料讀取出來,不存在採集的速率高於轉移的速率,用不上垃圾快取區,因此使用下面的函式
SapTransfer(
SapXferCallback pCallback = NULL,
void* pContext = NULL,
SapLocation loc = SapLocation::ServerUnknown
);
第一個為回撥函式,第二個為回撥函式的上下文資訊,其實就是用於傳遞到回撥函式的引數。
SDK中對此有一段話解答了我對這種線陣CCD影象和採集卡的使用方式:之前一直誤以為一個Acquisition是獲取當前一幀影象(即一次曝光獲取的影象,一行影象),而事實上是已經拼接成整張影象(設定資訊中的影象寬度和高度)。
By default, regular and trash buffer callback functions are called at each end of frame event, that is, when a complete image has been transferred.
SDK中特別指出
If you use this class , you must use the AddPair method to add transfer pairs of source and destination nodes. You must do this before calling the Create method.
所以後面緊接著寫了addPair,將快取區與採集區繫結起來。
接下里就是他們各自的建立了Create,使用預設的建立方法就好,其他定製化的方法沒必要,也不會。
仿照著寫了一個初始化的程式碼
// TODO: 在此新增額外的初始化程式碼
CAcqConfigDlg dlg(this, NULL);
if (dlg.DoModal() == IDOK)
{
// Define on-line objects
m_pAcq = new SapAcquisition(dlg.GetAcquisition());
m_pBuffer = new SapBuffer(2, m_pAcq);
m_pView = new SapView(m_pBuffer, GetDlgItem(IDC_STATIC_VIEW)->GetSafeHwnd());
m_pTransfer = new SapTransfer(XferCallback, this);
m_pTransfer->AddPair(SapXferPair(m_pAcq, m_pBuffer));
}
else
{
// Define off-line objects
m_pBuffer = new SapBuffer();
}
m_pAcq->Create();
m_pBuffer->Create();
m_pTransfer->Create();
m_pView->Create();
這裡用到了官方的GUI配置對話方塊CAcqConfigDlg,而且開啟了兩個快取區,這是因為在連續採集的過程中,如果要讀取影象需要時間,這時候只有一個緩衝區,該緩衝區的前半部分將會被新採集轉移的覆蓋,而開啟兩個緩衝區,可以將新採集影象資料的轉移到另一個緩衝區,迴圈錯開就能避免這個問題。
重點就是回撥函式的寫法了。上一個程式碼區主要是用來顯示新採集的影象。
我重寫個及時儲存影象的程式碼
void CDalsaCameraDlg::XferCallback( SapXferCallbackInfo *pInfo )
{
CDalsaCameraDlg* pDlg = (CDalsaCameraDlg*)(pInfo->GetContext());
int pitch = pDlg->m_pBuffer->GetPitch();
// Get the buffer data address
BYTE pData;
void* pDataAddr = &pData;
bool success = pDlg->m_pBuffer->GetAddress(staticCount, &pDataAddr);
int width = pDlg->m_pBuffer->GetWidth();
int height = pDlg->m_pBuffer->GetHeight();
Mat img = Mat::zeros(cv::Size(width, height), CV_8U);
memcpy(img.data, pDataAddr, width*height);
if (staticCount== 0)
{
imwrite("C:\\123.bmp", img);
staticCount = 1;
}
else (staticCount == 1)
{
imwrite("C:\\456.bmp", img);
staticCount = 0;
}
success = pDlg->m_pBuffer->ReleaseAddress(pDataAddr);
}
由於函調函式只能是靜態成員函式,因此無法呼叫非靜態成員函式和非靜態成員變數,我們又需要讀取緩衝區的內容,因此只能將整個類的指標傳遞到回撥函式中來,並重新生成了這個類(這個有點像太乙真人用蓮藕重造了哪吒的感覺)
這樣就能操縱採集到的緩衝區類SapBuffer了,利用getAddress獲取影象資料的首地址,這裡用了
BOOL GetAddress(int index, void** pData);
其中index為緩衝區的序號,這樣控制序號就可以交替讀取兩個緩衝區的資料內容了。每採集到一個完整影象之後讀取一個緩衝區,而這段時間內新採集的影象儲存在下一個緩衝區,資料之間不會存在覆蓋的問題,而且能及時讀出影象,儲存在硬盤裡。
這裡使用了OpenCV影象庫來儲存影象,主要是windows自帶的Bitmap不會(囧)。
最後程式終結直接記得釋放新開闢的指標變數
void CDalsaCameraDlg::OnDestroy()
{
CDialogEx::OnDestroy();
// TODO: 在此處新增訊息處理程式程式碼
// Release and free resources for SapBuffer object
if (nullptr != m_pView)
{
m_pView->Destroy();
delete m_pView;
}
if (nullptr != m_pTransfer)
{
m_pTransfer->Destroy();
delete m_pTransfer;
}
if (nullptr != m_pBuffer)
{
m_pBuffer->Destroy();
delete m_pBuffer;
}
if (nullptr != m_pAcq)
{
m_pAcq->Destroy();
delete m_pAcq;
}
}
最好是按照開闢的順序反過來一一釋放,原因在於如SapTransfer是依賴於繫結的Acquisition和Buffer的,倘若先釋放銷燬掉Acquisition和Buffer,再銷燬SapTransfer時欲解除與Acquisition和Buffer的聯絡時,發現已經找不到這兩位了。
希望趕緊做我的畢設,這個Dalsa相機的開發到此結束。