關於作者
Igor Murzov 擁有計算機科學碩士學位。他目前是 Xperience.AI 的軟體工程師,從事 OpenCV 和物聯網軟體開發工作。
簡介
在過去十年中,3D 技術已成為日常生活的一部分——我們觀看 3D 電影,玩 VR 遊戲,使用 3D 印表機建立立體細節,等等。如今,甚至智慧手機也配備了深度感測器,以提供高階 3D 功能。因此,可以肯定地說,在不久的將來,擁有一個 3D 攝像頭將變得非常普遍。3D 攝像頭可用於任何地方:機器人技術、遊戲、增強現實、虛擬現實、智慧家居、醫療保健、零售、自動化等等。我相信 3D 攝像頭在未來將得到更廣泛的應用。
3D 攝像頭能夠實現更高階的用例。與傳統僅處理彩色畫素圖的計算機視覺演算法相比,3D 攝像頭增加了另一維資料,這使得能夠從根本上擴充套件可以應用於該資料的計算機視覺演算法的範圍,因為除了彩色畫素圖之外,還有深度資訊。在下面的示例影像中,您可以看到顯示相同場景的彩色幀和深度幀(深度幀是彩色的,顏色越深表示場景中物體越近)。僅檢視彩色幀,很難區分植物葉子和牆上繪製的葉子,但深度資料使區分變得很容易。
在這篇文章中,我們將瞭解如何使用 Orbbec Astra Pro 攝像頭和開源 OpenNI API。Orbbec 是領先的 3D 攝像頭製造商之一。Astra Pro 攝像頭提供高階響應速度、深度測量、平滑漸變和精確輪廓,以及過濾低質量深度畫素的功能。該攝像頭可以以每秒 30 幀的速度傳輸高畫質影片和 VGA 深度影像。
因此,讓我們學習如何使用 OpenCV 庫操作 Astra Pro 攝像頭。
安裝
為了使用 Astra 攝像頭的深度感測器與 OpenCV,您應該首先安裝 Orbbec OpenNI2 SDK,並在啟用該 SDK 支援的情況下構建 OpenCV。目前沒有提供帶有 OpenNI2 SDK 支援的預構建 OpenCV,因此您需要自己進行操作。
- 從 這裡下載最新版本的 Orbbec OpenNI2 SDK。
- 解壓縮存檔,根據您的作業系統選擇構建版本,並按照 Readme 檔案中提供的安裝步驟進行操作。
- 然後,透過在 CMake 中將 WITH_OPENNI2 標誌設定為 ON,配置啟用 OpenNI2 支援的 OpenCV。您可能還想啟用 BUILD_EXAMPLES 標誌以獲得一個與您的 Astra 攝像頭配合使用的程式碼示例。
更詳細的說明可以在以下 教程中找到。
影片流設定
Astra Pro 攝像頭有兩個感測器——深度感測器和彩色感測器。可以使用 OpenNI 介面和 cv::VideoCapture 類讀取深度感測器。彩色感測器的影片流無法透過 OpenNI API 獲取,只能透過常規攝像頭介面提供。要獲取深度和彩色幀,應建立兩個 cv::VideoCapture 物件。
// Open depth stream VideoCapture depthStream(CAP_OPENNI2_ASTRA); // Open color stream VideoCapture colorStream(0, CAP_V4L2);
第一個物件將使用 OpenNI2 API 檢索深度資料。第二個物件使用 Video4Linux2 介面訪問彩色感測器。請注意,以上示例假設 Astra 攝像頭是系統中的第一個攝像頭。如果連線了多個攝像頭,您可能需要顯式設定正確的攝像頭編號。
在使用建立的 VideoCapture 物件之前,我們需要設定流引數,即 VideoCapture 屬性。最重要的引數是幀寬度、幀高度和 fps。對於此示例,我們將兩個流的寬度和高度都配置為 VGA 解析度,這是兩個感測器可用的最大解析度,並且我們希望兩個流引數相同,以便更輕鬆地進行顏色到深度的校準。
// Set color and depth stream parameters
colorStream.set(CAP_PROP_FRAME_WIDTH, 640);
colorStream.set(CAP_PROP_FRAME_HEIGHT, 480);
depthStream.set(CAP_PROP_FRAME_WIDTH, 640);
depthStream.set(CAP_PROP_FRAME_HEIGHT, 480);
depthStream.set(CAP_PROP_OPENNI2_MIRROR, 0);

要設定和檢索感測器資料生成器的某些屬性,請分別使用 cv::VideoCapture::set 和 cv::VideoCapture::get 方法。深度生成器支援以下透過 OpenNI 介面提供的 3D 攝像頭屬性。
- cv::CAP_PROP_FRAME_WIDTH – 以畫素為單位的幀寬度。
- cv::CAP_PROP_FRAME_HEIGHT – 以畫素為單位的幀高度。
- cv::CAP_PROP_FPS – 以 FPS 為單位的幀率。
- cv::CAP_PROP_OPENNI_REGISTRATION – 透過更改深度生成器的視點(如果該標誌為“開啟”)或將該視點設定為其正常視點(如果該標誌為“關閉”)來註冊將深度圖重新對映到影像圖的標誌。註冊過程的結果影像畫素對齊,這意味著影像中的每個畫素都與深度影像中的一個畫素對齊。
- cv::CAP_PROP_OPENNI2_MIRROR – 用於啟用或停用此流的映象的標誌。設定為 0 以停用映象。
以下屬性僅用於獲取。
- cv::CAP_PROP_OPENNI_FRAME_MAX_DEPTH – 攝像頭的最大支援深度(以毫米為單位)。
- cv::CAP_PROP_OPENNI_BASELINE – 基線值(以毫米為單位)。
從影片流讀取
設定 VideoCapture 物件後,您可以開始讀取幀。OpenCV 的 VideoCapture 提供同步 API,因此您必須在新執行緒中抓取幀,以避免在讀取另一個流時阻塞流。由於有兩個影片源應同時讀取,因此有必要建立兩個“讀取器”執行緒以避免阻塞。示例實現從每個感測器在新執行緒中獲取幀,並將它們與時間戳一起儲存在列表中。
// Create two lists to store frames
std::list<Frame> depthFrames, colorFrames;
const std::size_t maxFrames = 64;
// Synchronization objects
std::mutex mtx;
std::condition_variable dataReady;
std::atomic<bool> isFinish;
isFinish = false;
// Start depth reading thread
std::thread depthReader([&]
{
while (!isFinish)
{
// Grab and decode new frame
if (depthStream.grab())
{
Frame f;
f.timestamp = cv::getTickCount();
depthStream.retrieve(f.frame, CAP_OPENNI_DEPTH_MAP);
if (f.frame.empty())
{
cerr << "ERROR: Failed to decode frame from depth stream\n";
break;
}
{
std::lock_guard<std::mutex> lk(mtx);
if (depthFrames.size() >= maxFrames)
depthFrames.pop_front();
depthFrames.push_back(f);
}
dataReady.notify_one();
}
}
});
// Start color reading thread
std::thread colorReader([&]
{
while (!isFinish)
{
// Grab and decode new frame
if (colorStream.grab())
{
Frame f;
f.timestamp = cv::getTickCount();
colorStream.retrieve(f.frame);
if (f.frame.empty())
{
cerr << "ERROR: Failed to decode frame from color stream\n";
break;
}
{
std::lock_guard<std::mutex> lk(mtx);
if (colorFrames.size() >= maxFrames)
colorFrames.pop_front();
colorFrames.push_back(f);
}
dataReady.notify_one();
}
}
});
VideoCapture 可以檢索以下資料。
- 深度生成器提供的資料。
- cv::CAP_OPENNI_DEPTH_MAP – 以毫米為單位的深度值 (CV_16UC1)
- cv::CAP_OPENNI_POINT_CLOUD_MAP – 以米為單位的 XYZ (CV_32FC3)
- cv::CAP_OPENNI_DISPARITY_MAP – 以畫素為單位的視差 (CV_8UC1)
- cv::CAP_OPENNI_DISPARITY_MAP_32F – 以畫素為單位的視差 (CV_32FC1)
- cv::CAP_OPENNI_VALID_DEPTH_MASK – 有效畫素掩碼(未遮擋、未陰影等)(CV_8UC1)
- 彩色感測器提供的資料是常規 BGR 影像 (CV_8UC3)。
當有新資料可用時,每個“讀取器”執行緒都會使用條件變數通知主執行緒。幀儲存在有序列表中——列表中的第一幀是最早捕獲的幀,最後一幀是最晚捕獲的幀。
影片流同步
由於深度和彩色幀是從獨立的源讀取的,因此即使兩個流都設定為相同的幀率,兩個影片流也可能不同步。可以對流應用後同步過程,以將深度和彩色幀組合成對。以下示例程式碼演示了此過程。
std::list<std::pair<Frame, Frame>> dcFrames;
std::mutex dcFramesMtx;
std::condition_variable dcReady;
// Pair depth and color frames
std::thread framePairing([&]
{
// Half of frame period is a maximum time diff between frames
const int64 maxTdiff = 1000000000 / (2 * colorStream.get(CAP_PROP_FPS));
while (!isFinish)
{
std::unique_lock<std::mutex> lk(mtx);
while (!isFinish && (depthFrames.empty() || colorFrames.empty()))
dataReady.wait(lk);
while (!depthFrames.empty() && !colorFrames.empty())
{
// Get a frame from the list
Frame depthFrame = depthFrames.front();
int64 depthT = depthFrame.timestamp;
// Get a frame from the list
Frame colorFrame = colorFrames.front();
int64 colorT = colorFrame.timestamp;
if (depthT + maxTdiff < colorT)
{
depthFrames.pop_front();
}
else if (colorT + maxTdiff < depthT)
{
colorFrames.pop_front();
}
else
{
dcFramesMtx.lock();
dcFrames.push_back(std::make_pair(depthFrame, colorFrame));
dcFramesMtx.unlock();
dcReady.notify_one();
depthFrames.pop_front();
colorFrames.pop_front();
}
}
}
});
在上面的程式碼段中,執行將被阻塞,直到兩個幀列表中都有一些幀。當有新幀時,將檢查它們的時間戳——如果它們之間的差異大於幀週期的二分之一,則丟棄其中一個幀。如果時間戳足夠接近,則將這兩個幀放入 dcFrames 列表中,並通知處理執行緒有新資料可用。
影片處理
處理執行緒函式可能如下所示。
// Processing thread
std::thread process([&]
{
while (!isFinish)
{
std::unique_lock<std::mutex> lk(dcFramesMtx);
while (!isFinish && dcFrames.empty())
dcReady.wait(lk);
while (!dcFrames.empty())
{
if (!lk.owns_lock())
lk.lock();
Frame depthFrame = dcFrames.front().first;
Frame colorFrame = dcFrames.front().second;
dcFrames.pop_front();
lk.unlock();
// >>> Do something here <<<
}
}
});
在處理執行緒中,您將有兩個幀:一個包含顏色資訊,另一個包含深度資訊。在 >>> 在這裡執行某些操作 <<< 標記之後插入您的程式碼——資料已準備好使用 OpenCV 或您的自定義演算法進行處理。
完整的實現可以在這裡找到 這裡。
總結
3D 攝像頭在現代工業中發揮著重要作用。它們為我們提供瞭解決無法使用 2D 資料完成的計算機視覺任務的絕佳機會。在這篇文章中,我們學習瞭如何使用 OpenNI2 介面在 OpenCV 中操作 3D 攝像頭,以及如何解決可能出現的同步問題。








