NVIDIA VPI架构解析
VPI 架構解析
文章目錄
- VPI 架構解析
- 概述
- 支持的平臺
- 算法
- 算法負載
- 無負載算法
- 后端
- CPU
- CUDA
- PVA
- VIC
- NVENC
- OFA
- 流
- 緩沖器
- Images
- 圖像視圖
- 鎖
- 圖像格式
- 外部存儲器接口
- 數(shù)組
- 鎖定
- 外部存儲器接口
- Pyramids
- 鎖定
- 事件
- 上下文
- Global Context
- 上下文堆棧
- 線程安全
- 簡單的流程
- 復雜的流程
概述
VPI 是一個軟件庫,提供了一系列計算機視覺和圖像處理算法,可以在各種硬件加速器中無縫執(zhí)行。 這些加速器稱為后端。
VPI 的目標是為計算后端提供統(tǒng)一的接口,同時保持高性能。 它通過暴露底層硬件及其操作的數(shù)據的薄而有效的軟件抽象來實現(xiàn)這一點。
此圖說明了 VPI 的體系結構:
API 遵循在初始化階段進行對象分配和設置的范例。 以下是應用程序循環(huán),主要處理發(fā)生在其中,使用在初始化期間創(chuàng)建的對象。 主處理完成后,銷毀創(chuàng)建的對象并清理環(huán)境。 在資源受限的嵌入式環(huán)境中,內存分配在時間和空間上都受到限制,VPI 提供的對內存分配和生命周期的控制是有益的。
VPI的核心組件包括
算法:表示不可分割的計算操作。
后端:代表負責實際計算的硬件引擎。
流:充當向其提交算法的異步隊列,最終在給定的后端按順序執(zhí)行。 流和事件是計算流程的構建塊。
緩沖區(qū):存儲輸入和輸出數(shù)據。
事件:提供對流和/或應用程序線程進行操作的同步原語。
上下文:保存 VPI 和已創(chuàng)建對象的狀態(tài)。
支持的平臺
VPI 可用于以下平臺/設備:
- Jetson AGX Xavier、Jetson AGX Xavier NX
- Jetson AGX Orin
- Linux x86_64 和 NVIDIA dGPU 從 Maxwell(sm_50 或更高版本)開始。
- 用 Ubuntu 18.04 和 Ubuntu 20.04 測試
算法
算法代表實際的計算操作。 它們作用于一個或多個輸入緩沖區(qū)并將其結果寫入用戶提供的輸出緩沖區(qū)。 它們相對于應用程序線程異步運行。 有關受支持算法的列表,請參閱算法部分。
有兩類算法:
- 需要有效負載的算法。
- 無負載算法。
算法負載
某些算法實現(xiàn),例如 FFT 或 KLT Feature Tracker,需要臨時資源才能正常運行。 這些資源由與算法關聯(lián)的 VPIPayload 對象封裝。
在執(zhí)行算法之前,您必須在初始化時創(chuàng)建相應的有效負載,傳遞用于分配臨時資源和指定可以執(zhí)行算法的后端的參數(shù)。 在執(zhí)行計算的主循環(huán)中,您將算法實例提交給流執(zhí)行,提供相應的有效負載以及輸入和輸出參數(shù)。 您可以在多個算法實例中重用負載,但您必須確保負載一次僅由一個實例使用。
當不再需要有效負載時,您必須通過調用 vpiPayloadDestroy 將其銷毀。 此函數(shù)釋放封裝在有效負載中的所有資源。
例子:
- 供 CUDA 后端使用的 FFT 有效負載創(chuàng)建,僅執(zhí)行一次。
- FFT 算法提交到流,根據需要多次調用,可能具有不同的輸入和輸出。
- 有效負載銷毀,在處理完成且不再需要有效負載時進行。
無負載算法
一些算法不需要臨時資源。 此類算法包括 Box Filter 和 Rescale 等。 對于這些算法,無需處理有效負載,并且簡化了操作順序。 在算法提交期間發(fā)送所有必需的數(shù)據。
例子:
- Box filter算法提交到 CUDA 后端,可根據需要多次調用。
后端
VPI 支持的每種算法都在一個或多個后端實現(xiàn)。 當給定相同的輸入時,相同算法的不同實現(xiàn)會返回相似的結果,但它們的結果之間可能會發(fā)生微小的變化。 這主要是由于針對特定后端量身定制的優(yōu)化,例如使用定點而不是浮點運算。
CPU
此后端代表設備的 CPU。 它可以創(chuàng)建一組后臺工作線程和數(shù)據結構,支持跨多個內核的高效并行執(zhí)行。 這些工作線程可能在不同的流和/或上下文實例之間共享。
VPI 提供的機制允許您通過使用 VPIParallelForCallback 函數(shù)調用 vpiContextSetParallelFor 來定義自己的 CPU 任務調度方案,當 CPU 任務需要執(zhí)行時,VPI 將調用該函數(shù)。
CUDA
CUDA 后端與特定的支持 CUDA 的 GPU 具有顯式關聯(lián),在構建流期間定義。這意味著提交在此流上執(zhí)行的算法由此 GPU 處理。
CUDA 后端管理允許它啟動底層 CUDA 內核的 cudaStream_t 句柄和其他 CUDA 設備信息。
VPI 利用 CUDA 內核啟動的異步特性來優(yōu)化其啟動。在某些情況下,特別是當沒有用戶定義的函數(shù)被提交到流時,后端直接從調用者線程啟動一個 CUDA 任務,完全繞過工作線程。
當僅涉及 CUDA 算法時,VPI 通常充當 CUDA SDK 之上的高效薄層。
在構造 API 上下文之前,您必須為調用線程正確設置 CUDA 上下文。生成的上下文對象使用相應的 CUDA 上下文進行內部內核調用。
注意
當前不支持使用多個 GPU,這可能會導致未定義的行為。使用多個 GPU 獲得的結果并不可靠。
PVA
可編程視覺加速器 (PVA) 是 NVIDIA? Jetson AGX Xavier? 和 NVIDIA? Jetson Xavier? NX 設備中的處理器,專門用于圖像處理和計算機視覺算法。
當您需要讓 GPU 自由運行其他只能由它執(zhí)行的任務時,請使用 PVA 后端,例如深度學習推理階段和僅在 CUDA 后端實現(xiàn)的算法。
PVA 硬件比 CPU 和 CUDA 硬件更節(jié)能。 因此,如果電力非常寶貴,請盡可能使用 PVA 后端。
每個 Jetson AGX Xavier 或 Jetson Xavier NX 設備包含兩個 PVA 處理器,每個處理器包含兩個矢量處理器。 因此,設備最多可以同時執(zhí)行四個獨立的 PVA 任務。
當多個 VPI 流啟用了 PVA 后端時,它們各自依次循環(huán)選擇一個可用的 PVA 矢量處理器。
注意
對于任何特定算法,PVA 后端不一定比 CUDA 或 CPU 后端快。
VIC
視頻圖像合成器 (VIC) 是 Jetson 設備中的固定功能處理器,專門用于低級圖像處理任務,例如重新縮放、色彩空間轉換、降噪和合成。
與 PVA 后端一樣,VIC 后端允許您從 GPU 卸載任務,如果性能不高,則將其留給其他處理。
NVENC
NVIDIA 編碼器引擎 (NVENC) 是 Jetson 設備中專用于視頻編碼的處理器。 編碼過程的某些階段可以重新用于其他任務,例如密集光流。
OFA
NVIDIA 光流加速器 (OFA) 是新 Jetson AGX Orin 設備中的專用處理器,用于計算圖像之間的光流。 它目前被用作 Stereo Disparity Estimator 的后端。
流
VPIStream 對象是 API 的主要入口點。它松散地基于 CUDA 的 cudaStream_t。該對象表示一個 FIFO 命令隊列,其中存儲要由某個后端執(zhí)行的命令列表。這些命令可能會運行特定的計算機視覺算法、執(zhí)行主機功能(使用 vpiSubmitHostFunction)或發(fā)出事件信號。
在初始化時,流被配置為使用后端來執(zhí)行提交給它的任務。默認情況下,它使用當前上下文啟用的后端。創(chuàng)建流時,您可以設置標志以進一步限制可用后端的數(shù)量并減少資源使用。
每個流啟動一個內部工作線程來分派任務,允許相對于調用(用戶)線程執(zhí)行異步任務。這意味著當對特定后端的 VPI 流調用算法提交調用時,該函數(shù)會將相應的命令推送到 VPIStream 工作線程并立即將執(zhí)行返回給調用線程。
推送到工作線程的任務不會立即處理。它們最初聚集在一個暫存隊列中。僅在刷新流時處理這些任務。此操作將所有任務從暫存隊列移動到處理隊列,最終將任務提交到與它們關聯(lián)的后端。
以下事件觸發(fā)流刷新:
- 通過調用以下函數(shù):
- vpiStreamFlush,用于顯式刷新流。 此調用不同步流。
- vpi事件記錄
- vpiSubmitHostFunction
- vpiSubmitHostFunctionEx
- vpiStreamSync
- vpiStreamDestroy,隱式同步(并因此刷新)流。
- 當暫存隊列已滿并且有新任務被推送到它時。 在這種情況下,流在新任務添加到隊列之前被刷新。
- 在每個算法提交操作之后,僅當使用 VPI_STREAM_GREEDY 標志創(chuàng)建流時。
暫存隊列允許處理流程優(yōu)化機會,例如最小化內存映射操作等。
有關詳細信息,請參閱 VPI - 視覺編程接口的“C API 參考”部分中的流。
緩沖器
緩沖區(qū)表示 VPI 算法使用的數(shù)據。 VPI 支持對三種數(shù)據的抽象:
- 圖像:保存二維數(shù)據。
- 數(shù)組:保存一維數(shù)據。
- Pyramid:保存一系列具有不同數(shù)量細節(jié)的圖像,從精細到粗糙。
VPI 可以分配所有三種類型的緩沖區(qū)。 對于圖像和數(shù)組,它可以將數(shù)據封裝到 VPI 緩沖區(qū)中,并將其存儲在預分配的內存中。 當應用程序需要與 VPI 以外的庫互操作時,這很有用,例如當它使用 OpenCV cv::Mat 緩沖區(qū)作為 VPI 算法的輸入時。
所有緩沖區(qū)類型共享大小和元素類型的屬性。
Images
VPI 圖像表示任何類型的 2D 數(shù)據,例如實際圖像、嵌入 2D 空間中的矢量場和 2D 熱圖。
VPI 圖像的特征在于它們的大小(寬度和高度)和格式。
當應用程序創(chuàng)建 VPIImage 對象時,它會傳遞指定圖像可以使用的后端的標志。 您可以使用 VPIBackend 枚舉之一或兩個或多個枚舉一起設置標志。 當沒有傳遞后端標志時,VPI 啟用當前上下文允許的所有后端,默認情況下都是可用的后端。
有關詳細信息,請參閱 VPI - 視覺編程接口的“C API 參考”部分中的圖像。
圖像視圖
VPI 圖像視圖表示圖像的現(xiàn)有 2D 數(shù)據中的一個矩形區(qū)域,請參閱上面的圖像描述。
VPI 圖像視圖是從現(xiàn)有的 VPI 圖像創(chuàng)建的,并以它們的剪輯區(qū)域為特征,即由起始位置(x,y)和大小(寬度,高度)定義的矩形。 它們共享原始源圖像的相同上下文和格式,但它們的大小(寬度和高度)是矩形區(qū)域的大小。
當應用程序將 VPIImage 對象創(chuàng)建為圖像視圖時,它會傳遞指定后端的標志,就像處理常規(guī)圖像一樣。
有關詳細信息,請參閱 vpiImageCreateView 和 vpiImageSetView 函數(shù)的參考文檔。
鎖
要使圖像內容可在 VPI 外部訪問,圖像必須通過調用 vpiImageLockData 函數(shù)來鎖定圖像緩沖區(qū)。這可確保對內存所做的所有更改都已提交并可供 VPI 外部訪問。
根據可用的緩沖區(qū)類型,圖像必須啟用某些后端,請參閱 vpiImageLockData 文檔了解更多詳細信息。 vpiImageLockData 用圖像信息填充 VPIImageData 對象,使您可以正確處理和解釋所有圖像像素。在主機上處理完圖像數(shù)據后,調用 vpiImageUnlock。
當圖像接口在 VPI 外部分配的緩沖區(qū)并且這些緩沖區(qū)在 VPI 外部訪問時,它們也必須被鎖定。在這種情況下,您對通過 VPI 調用檢索圖像內容不感興趣。相反,調用 vpiImageLock 來鎖定圖像內容。只有這樣才能通過接口的緩沖區(qū)直接訪問它們。完成后調用 vpiImageUnlock 讓 VPI 訪問緩沖區(qū)。同樣,當緩沖區(qū)被鎖定時,嘗試訪問其內容的流將失敗并顯示 VPI_ERROR_BUFFER_LOCKED。
當圖像被鎖定時,它不能被異步運行的算法訪問。但是,它可以被最初鎖定它的同一線程遞歸地鎖定。請記住將每個 vpiImageLockData 調用與相應的 vpiImageUnlock 配對。
圖像格式
VPI 支持表示不同像素類型的多種圖像格式,例如單通道 8、16 或 32 位、無符號和有符號、多通道 RGB 和 RGBA、半平面 NV12 等。
圖像格式由 VPIImageFormat 枚舉表示。每種格式都由幾個屬性定義,例如顏色空間、平面數(shù)和數(shù)據布局。具有從圖像格式中提取每個組件以及修改現(xiàn)有組件的功能。
并非所有算法都支持所有可識別的圖像格式。不過,大多數(shù)都提供多種格式的選擇。支持的格式列在每個算法的 API 參考文檔中。
2D 圖像最常以音高線性格式排列在內存中,即逐行、一個接一個地排列。每行都可以比保存圖像數(shù)據所需的大,以符合行地址對齊限制。
您還可以使用專有的塊線性布局創(chuàng)建或接口內存。對于某些算法和后端,使用這種格式創(chuàng)建 2D 內存會更有效。
有關詳細信息,請參閱 VPI - 視覺編程接口的“C API 參考”部分中的圖像格式。
外部存儲器接口
您可以使用函數(shù) vpiImageCreateWrapper 創(chuàng)建外部分配內存的圖像接口。 在每種情況下,您都必須使用所需信息填充 VPIImageData 結構并將其傳遞給函數(shù)。 有關可以封裝的內存類型的信息,請參閱其 API 參考文檔。
在所有這些情況下,VPIImage 對象都不擁有內存緩沖區(qū)。 當 VPIImage 被銷毀時,緩沖區(qū)不會被釋放。
與創(chuàng)建由 VPI 管理的圖像緩沖區(qū)的函數(shù)一樣,這些接口函數(shù)接受指定它們可以與哪些后端一起使用的標志。
數(shù)組
VPI 數(shù)組表示一維數(shù)據,例如關鍵點列表、邊界框和變換。
數(shù)組的特征在于它們的容量、大小和元素類型。 與圖像一樣,標志用于指定它們可以使用的后端。
數(shù)組類型是從枚舉 VPIArrayType 中提取的。 需要數(shù)組作為輸入或輸出的算法,例如 KLT 模板跟蹤器,通常接受一種特定的數(shù)組類型。
VPIArray 的行為與其他內存緩沖區(qū)略有不同:雖然數(shù)組的容量在對象的生命周期內是固定的,但它的大小可以改變。 任何寫入數(shù)組的 API 都必須將 size 參數(shù)設置為數(shù)組中有效元素的數(shù)量。 您可以使用 vpiArrayGetSize 和 vpiArraySetSize 來查詢和修改數(shù)組的大小。
有關詳細信息,請參閱 VPI - 視覺編程接口的“C API 參考”部分中的數(shù)組。
鎖定
可以使用 vpiArrayLockData 函數(shù)在 VPI 外部訪問數(shù)組數(shù)據。 此功能的工作方式與它的圖像對應物一樣。 它也支持同一線程的遞歸鎖定。
外部存儲器接口
您還可以使用函數(shù) vpiArrayCreateWrapper 創(chuàng)建外部分配的 CUDA 和主機內存的數(shù)組接口。 在這兩種情況下,您都必須使用所需信息填充 VPIArrayData 結構并將其傳遞給函數(shù)。
Pyramids
VPI Pyramids表示堆疊在一起的 VPI 圖像的集合,它們都具有相同的格式,但可能具有不同的尺寸。
金字塔的特征在于其級別數(shù)、基本級別維度、比例因子和圖像格式。比例因子表示一個級別的維度與前一個級別的維度的比率。例如,當 scale=0.5 時,金字塔是二元的,即維度是二次方。
通常需要將一個Pyramids級別處理為 VPI 算法的輸入或輸出。然后您必須使用 vpiImageCreateWrapperPyramidLevel 來識別Pyramids及其要封裝的級別。生成的圖像繼承了Pyramids啟用的后端。您可以像使用任何其他圖像一樣使用返回的 VPIImage 句柄。使用完圖像后,必須使用 vpiImageDestroy 將其銷毀。
有關更多信息,請參閱 VPI - 視覺編程接口的“C API 參考”部分中的Pyramids。
鎖定
與圖像和數(shù)組一樣,您可以使用函數(shù) vpiPyramidLockData 在 VPI 之外訪問整個Pyramid的內容,前提是Pyramid已啟用與返回的緩沖區(qū)類型對應的后端。有關詳細信息,請參閱 vpiPyramidLockData。此函數(shù)填充包含 VPIImageData 數(shù)組的 VPIPyramidData 結構。使用完 VPIPyramidData 后,調用 vpiPyramidUnlock 從主機取消金字塔的映射并釋放其資源。
遞歸鎖定適用于Pyramid,就像圖像和數(shù)組一樣。
事件
API 中的每個計算函數(shù)都相對于調用線程異步執(zhí)行;也就是說,它立即返回,而不是等待操作完成。有兩種方法可以將操作與后端同步。
一種方法是通過調用 vpiStreamSync 等到 VPIStream 隊列中的所有命令完成。這種方法很簡單,但它不能提供細粒度的同步(例如,“等到函數(shù) X 完成”)或流間的同步(例如,“等到流 D 中的函數(shù) C 完成,然后再運行函數(shù) A流 B")。
另一種方法通過使用 VPIEvent 對象提供更靈活的同步。這些對象在概念上類似于二進制信號量,旨在密切模仿 CUDA API 中的事件:
- 您可以在事件實例中捕獲提交給 VPIStream 實例的所有命令(請參閱 vpiEventRecord)。當所有捕獲的命令都已處理并從 VPIStream 命令隊列中刪除時,會發(fā)出該事件。
- 您可以使用 vpiStreamWaitEvent 調用執(zhí)行流間同步,該調用將命令推送到 VPIStream 隊列,該隊列阻止處理未來排隊的命令,直到發(fā)出給定事件的信號。
- 應用程序可以使用 vpiEventQuery 查詢事件的狀態(tài)。
- 應用程序線程可以阻塞,直到使用 vpiEventSync 完成事件。
- 事件可以在完成時加上時間戳。
- 您可以計算同一流中已完成事件的時間戳之間以及不同流之間的時間戳之間的差異。
有關詳細信息,請參閱 VPI - 視覺編程接口的“C API 參考”部分中的事件。
上下文
上下文封裝了 VPI 用于執(zhí)行操作的所有資源。當上下文被銷毀時,它會自動清理這些資源。
每個應用程序 CPU 線程都有一個活動上下文。每個上下文都擁有在其處于活動狀態(tài)時創(chuàng)建的 VPI 對象。
默認情況下,所有應用程序線程都與相同的全局上下文相關聯(lián),該全局上下文由 VPI 在創(chuàng)建第一個 VPI 資源時自動創(chuàng)建。在這種情況下,您不需要執(zhí)行任何顯式的上下文管理,一切都由 VPI 在后臺處理。
當需要更好地控制上下文時,用戶創(chuàng)建的上下文是一種選擇。一旦創(chuàng)建,上下文可以被推送到當前應用程序線程的上下文堆棧,或者可以替換當前上下文。這兩個動作都使創(chuàng)建的上下文處于活動狀態(tài)。有關如何操作上下文的更多信息,請參閱上下文堆棧。
您可以在創(chuàng)建上下文時指定與上下文關聯(lián)的多個屬性,例如當上下文處于活動狀態(tài)時,創(chuàng)建的對象支持哪些后端。這有效地允許您屏蔽對特定后端的支持。例如,如果當前上下文沒有設置 VPI_BACKEND_CUDA 標志,則為 CUDA 后端創(chuàng)建流會失敗。如果您不傳遞后端標志,則上下文會檢查正在運行的平臺并啟用與所有可用硬件引擎關聯(lián)的后端。
注意
CPU 后端不能被屏蔽,并且必須始終作為后備實現(xiàn)來支持。
對象(緩沖區(qū)、有效負載、事件等)不能在不同的上下文之間共享。
除可用內存外,創(chuàng)建的上下文的數(shù)量沒有限制。
有關詳細信息,請參閱 VPI - 視覺編程接口的“C API 參考”部分中的上下文。
Global Context
默認情況下,VPI 在創(chuàng)建任何 VPI 對象之前創(chuàng)建單個全局上下文。這個全局上下文最初在所有應用程序線程之間共享,并且不能被用戶銷毀。
對于大多數(shù)用例,應用程序可以使用全局上下文。當應用程序需要更好地控制對象如何組合在一起,或者它需要流程之間的一定程度的獨立性時,您可能需要顯式地創(chuàng)建和操作上下文。
上下文堆棧
每個應用程序線程都有一個不與其他線程共享的上下文堆棧。
堆棧中的頂部上下文是該線程的當前上下文。
默認情況下,上下文堆棧中有一個上下文,即全局上下文。因此,所有新線程都具有與當前線程相同的全局上下文集。
在給定堆棧中設置當前上下文相當于用給定上下文替換頂部上下文,無論是全局上下文還是最近推送的上下文。替換的上下文不再屬于堆棧。
但是,將上下文推入堆棧并不會取代任何東西。頂部上下文保存在堆棧中,新推送的上下文放在頂部,從而成為新的當前上下文。
用戶可以隨意從堆棧中推送和彈出上下文。這允許在新上下文中臨時創(chuàng)建流程,而不會干擾現(xiàn)有上下文。
為了避免泄漏,重要的是要匹配給定上下文堆棧上的推送和彈出次數(shù)。請注意,上下文堆棧中最多可以有八個上下文。
線程安全
所有 API 函數(shù)都是線程安全的。對 API 對象的并發(fā)主機訪問被序列化并以未指定的順序執(zhí)行。所有 API 調用都使用特定于線程并存儲在線程本地存儲 (TLS) 中的 VPIContext 實例。如果當前線程的上下文指針為 NULL(未設置上下文),則所有 API 調用都使用庫初始化期間創(chuàng)建的默認全局上下文。
API 對象沒有線程親和性的概念;也就是說,如果多個線程使用相同的上下文實例,則在一個線程中創(chuàng)建的對象可以安全地被另一個線程銷毀。
大多數(shù) API 函數(shù)都是非阻塞的。調用時可以阻塞的函數(shù)是 vpiStreamSync、vpiStreamDestroy、vpiContextDestroy、vpiEventSync 和幾個 vpiSubmit* 函數(shù)(當流命令隊列已滿時會阻塞)。由于 API 實現(xiàn)中的隱式同步很少,因此您必須確保生成的依賴函數(shù)調用順序是合法的。
流程示例,以及如何使用 VPI 實現(xiàn)它們,將在以下部分中進行說明。
簡單的流程
在此示例中,實現(xiàn)了具有簡單box filter操作的流程來處理輸入圖像。 這與圖像模糊教程非常相似。
創(chuàng)建要使用的輸入圖像緩沖區(qū)
該示例使用無符號 8 位像素元素創(chuàng)建 640x480 1 通道(灰度)輸入圖像,這些元素由 vpi.Format.U8 表示。 VPI 在創(chuàng)建時用零初始化圖像。
注意
此示例創(chuàng)建一個空的輸入圖像緩沖區(qū),但在實際用例中,可以將現(xiàn)有內存緩沖區(qū)封裝到 VPI 圖像緩沖區(qū)中,或者可以使用來自早期流程階段的圖像。 有關更完整的示例,請參閱圖像模糊教程。
在將 vpi.Backend.CUDA 定義為默認后端的 Python 上下文中,對輸入圖像調用 box_filter 方法。 3x3 box_filter算法將由 CUDA 后端在默認流上執(zhí)行。 結果將返回到新的圖像輸出中。
with vpi.Backend.CUDA:output = input.box_filter(3)在此示例中,NVIDIA 建議檢查多個 VPI 對象如何協(xié)同工作,并檢查對象之間的所有權關系。
這是提供的 C/C++ 示例的概念結構:
在上圖中:
- 默認上下文是自動創(chuàng)建并激活的上下文。在此示例中,默認上下文是流和圖像緩沖區(qū)。
- Stream 擁有一個工作線程,該線程將任務排隊并將其分派到后端設備并處理同步。它還擁有代表最終執(zhí)行算法的硬件后端的對象。
- Box Filter 是提交給流的算法。在內部,作業(yè) 1 是使用算法內核及其所有參數(shù)創(chuàng)建的。然后它在工作線程上排隊,當所有先前提交給它的任務完成時,它會將它提交給硬件。由于該算法沒有有效載荷(或狀態(tài)),因此無需擔心其生命周期。
- Sync 表示 vpiStreamSync 調用。它將作業(yè) 2 排入工作線程,作業(yè)在執(zhí)行時發(fā)出內部事件信號。調用線程一直等待,直到事件發(fā)出信號,保證到目前為止排隊的所有任務都已完成。其他線程的提交被阻止,直到 vpiStreamSync 返回。
注意
在此示例中,由于提交算法時工作線程為空且 CUDA 內核執(zhí)行是異步的,因此 VPI 將算法直接提交給 CUDA 設備,完全繞過工作線程。
當僅 CUDA 后端用于算法提交和同步時,底層 CUDA 執(zhí)行之上的 VPI 開銷通常被最小化,并且通常可以忽略不計。僅使用其他后端之一的流也是如此。將算法提交到同一流中的不同后端會產生很小的內部同步開銷。
復雜的流程
更復雜的場景可能會利用設備上的不同加速處理器,并創(chuàng)建一個最能利用其全部計算能力的流程。 為此,流程必須具有可并行化的階段。
下一個示例實現(xiàn)了完整的立體視差估計和Harris 角點提取流程,這為并行化提供了大量機會。
該圖揭示了三個階段的并行化機會:獨立的左右圖像預處理和Harris 角點提取。 流程對每個處理階段使用不同的后端,具體取決于每個后端的處理速度、功率要求、輸入和輸出限制以及可用性。 在此示例中,處理在以下后端之間進行拆分:
- VIC:進行立體聲對校正和縮小。
- CUDA:進行圖像格式轉換。
- PVA:進行立體視差計算。
- CPU:處理哈里斯角的一些預處理和提取。
這種后端選擇使 GPU 可以騰出時間來處理其他任務,例如深度學習推理階段。 CUDA上的圖像格式轉換操作相當快,干擾不大。 CPU 一直忙于不受干擾地提取 Harris 關鍵點。
下圖顯示了算法如何拆分為流以及流如何同步。
左流和右流都開始立體對預處理,而關鍵點流等待直到右灰度圖像準備好。 準備就緒后,Harris 角點檢測開始,同時流權繼續(xù)預處理。 當左側流上的預處理結束時,流會一直等待,直到右側的縮小圖像準備好。 最后,立體視差估計從它的兩個立體輸入開始。 在任何時候,主機線程都可以在左側和關鍵點流中發(fā)出 vpiStreamSync 調用,以等待視差和關鍵點數(shù)據準備好進行進一步處理或顯示。
上面的大綱解釋了實現(xiàn)這個流程的代碼:
導入所需要的頭文件和算法
#include <string.h> #include <vpi/Array.h> #include <vpi/Context.h> #include <vpi/Event.h> #include <vpi/Image.h> #include <vpi/LensDistortionModels.h> #include <vpi/Stream.h> #include <vpi/WarpMap.h> #include <vpi/algo/BilateralFilter.h> #include <vpi/algo/ConvertImageFormat.h> include <vpi/algo/HarrisCorners.h> #include <vpi/algo/Remap.h> #include <vpi/algo/Rescale.h> #include <vpi/algo/StereoDisparity.h>執(zhí)行初始化階段,創(chuàng)建所有必需的對象。
創(chuàng)建上下文并使其處于活動狀態(tài)。
盡管您可以使用自動創(chuàng)建的默認上下文來管理 VPI 狀態(tài),但創(chuàng)建一個上下文并使用它來處理鏈接到特定流程的所有對象在其生命周期內可能更方便。 最后,上下文銷毀會觸發(fā)在其下創(chuàng)建的對象的銷毀。 使用專用上下文還可以更好地隔離此流程與應用程序可能使用的其他流程。
int main() {VPIContext ctx;vpiContextCreate(0, &ctx);vpiContextSetCurrent(ctx);創(chuàng)建流。
創(chuàng)建帶有全零標志的流,這意味著它們可以處理所有后端的任務。
有兩個流用于處理立體對預處理,第三個流用于Harris 角點檢測。 預處理完成后,stream_left 被重新用于立體視差估計。
VPIStream stream_left, stream_right, stream_keypoints; vpiStreamCreate(0, &stream_left); vpiStreamCreate(0, &stream_right); vpiStreamCreate(0, &stream_keypoints);創(chuàng)建輸入圖像緩沖區(qū)接口。
假設輸入來自作為 EGLImage 的捕獲流程,您可以將緩沖區(qū)接口在 VPIImage 中以在 VPI 流程中使用。 所有流程需要的是來自每個立體聲輸入的一幀(通常是第一幀)。
EGLImageKHR eglLeftFrame = /* First frame from left camera */;EGLImageKHR eglRightFrame = /* First frame from right camera */;VPIImage left, right;VPIImageData dataLeft;dataLeft.bufferType = VPI_IMAGE_BUFFER_EGLIMAGE;dataLeft.buffer.egl = eglLeftFrame;vpiImageCreateWrapper(&dataLeft, NULL, 0, &left);VPIImageData dataRight;dataRight.bufferType = VPI_IMAGE_BUFFER_EGLIMAGE;dataRight.buffer.egl = eglRightFrame;vpiImageCreateWrapper(&dataRight, NULL, 0, &right);創(chuàng)建要使用的圖像緩沖區(qū)。
與簡單流程一樣,此流程創(chuàng)建空輸入圖像。 這些輸入圖像必須通過封裝內存中存在的圖像或從早期 VPI 流程的輸出來填充。
輸入是 640x480 NV12(彩色)立體聲對,通常由相機捕獲流程輸出。 需要臨時圖像來存儲中間結果。 格式轉換是必要的,因為立體視差估計器和哈里斯角提取器需要灰度圖像。 此外,立體視差預計其輸入正好是 480x270。 這是通過上圖中的重新縮放階段完成的。
VPIImage left_rectified, right_rectified; vpiImageCreate(640, 480, VPI_IMAGE_FORMAT_NV12_ER, 0, &left_rectified); vpiImageCreate(640, 480, VPI_IMAGE_FORMAT_NV12_ER, 0, &right_rectified);VPIImage left_grayscale, right_grayscale; vpiImageCreate(640, 480, VPI_IMAGE_FORMAT_U16, 0, &left_grayscale); vpiImageCreate(640, 480, VPI_IMAGE_FORMAT_U16, 0, &right_grayscale);VPIImage left_reduced, right_reduced; vpiImageCreate(480, 270, VPI_IMAGE_FORMAT_U16, 0, &left_reduced); vpiImageCreate(480, 270, VPI_IMAGE_FORMAT_U16, 0, &right_reduced);VPIImage disparity; vpiImageCreate(480, 270, VPI_IMAGE_FORMAT_U16, 0, &disparity);定義立體視差算法參數(shù)并創(chuàng)建有效負載。
立體視差處理需要一些臨時數(shù)據。 VPI 將此數(shù)據稱為有效負載。 在此示例中,調用 vpiCreateStereoDisparityEstimator 并傳遞內部分配器所需的所有參數(shù)以指定臨時數(shù)據的大小。
因為臨時數(shù)據是在后端設備上分配的,所以有效負載與后端緊密耦合。 如果要在不同的后端執(zhí)行相同的算法,或者在不同的流中同時使用相同的后端,則每個后端或流都需要一個有效負載。 在此示例中,為 PVA 后端執(zhí)行創(chuàng)建了有效負載。
算法參數(shù)方面,VPI立體視差估計器采用半全局立體匹配算法實現(xiàn)。 估計器需要人口普查變換窗口大小,指定為 5,最大視差級別數(shù),指定為 64。有關更多信息,請參閱立體視差估計器。
VPIStereoDisparityEstimatorParams stereo_params; stereo_params.windowSize = 5; stereo_params.maxDisparity = 64;VPIStereoDisparityEstimatorCreationParams stereo_creation_params; vpiInitStereoDisparityEstimatorCreationParams(&stereo_creation_params); stereo_params.maxDisparity = stereo_params.maxDisparity;VPIPayload stereo; vpiCreateStereoDisparityEstimator(VPI_BACKEND_CUDA, 480, 270, VPI_IMAGE_FORMAT_U16, &stereo_creation_params,&stereo);創(chuàng)建圖像校正負載和相應的參數(shù)。 它使用 Remap 算法進行鏡頭畸變校正。 這里指定了立體鏡頭參數(shù)。 因為左右鏡頭不同,所以創(chuàng)建了兩個重映射有效負載。 有關詳細信息,請參閱鏡頭失真校正。
VPIPolynomialLensDistortionModel dist; memset(&dist, 0, sizeof(dist)); dist.k1 = -0.126; dist.k2 = 0.004;const VPICameraIntrinsic Kleft = {{466.5, 0, 321.2},{0, 466.5, 239.5} }; const VPICameraIntrinsic Kright = {{466.2, 0, 320.3},{0, 466.2, 239.9} }; const VPICameraExtrinsic X = {{1, 0.0008, -0.0095, 0},{-0.0007, 1, 0.0038, 0},{0.0095, -0.0038, 0.9999, 0} };VPIWarpMap map; memset(&map, 0, sizeof(map)); map.grid.numHorizRegions = 1; map.grid.numVertRegions = 1; map.grid.regionWidth[0] = 640; map.grid.regionHeight[0] = 480; map.grid.horizInterval[0] = 4; map.grid.vertInterval[0] = 4; vpiWarpMapAllocData(&map);VPIPayload ldc_left; vpiWarpMapGenerateFromPolynomialLensDistortionModel(Kleft, X, Kleft, &dist, &map); vpiCreateRemap(VPI_BACKEND_VIC, &map, &ldc_left);VPIPayload ldc_right; vpiWarpMapGenerateFromPolynomialLensDistortionModel(Kright, X, Kleft, &dist, &map); vpiCreateRemap(VPI_BACKEND_VIC, &map, &ldc_right);為 Harris Corner Detector 創(chuàng)建輸出緩沖區(qū)。
該算法接收圖像并輸出兩個數(shù)組,一個包含關鍵點本身,另一個包含每個關鍵點的分數(shù)。 最多返回 8192 個關鍵點,這必須是數(shù)組容量。 關鍵點由 VPIKeypointF32 結構表示,分數(shù)由 32 位無符號值表示。 有關詳細信息,請參閱哈里斯角檢測器。
VPIArray keypoints, scores; vpiArrayCreate(8192, VPI_ARRAY_TYPE_KEYPOINT_F32, 0, &keypoints); vpiArrayCreate(8192, VPI_ARRAY_TYPE_U32, 0, &scores);定義 Harris 檢測器參數(shù)并創(chuàng)建檢測器的有效載荷。
用所需參數(shù)填充 VPIHarrisCornerDetectorParams 結構。 有關每個參數(shù)的更多信息,請參閱結構文檔。
與立體視差一樣,Harris 檢測器需要有效載荷。 這次只需要輸入大小 (640x480)。 流程僅接受此大小的輸入有效負載。
VPIHarrisCornerDetectorParams harris_params; vpiInitHarrisCornerDetectorParams(&harris_params); harris_params.gradientSize = 5; harris_params.blockSize = 5; harris_params.strengthThresh = 10; harris_params.sensitivity = 0.4f;VPIPayload harris; vpiCreateHarrisCornerDetector(VPI_BACKEND_CPU, 640, 480, &harris);創(chuàng)建事件以實現(xiàn)同步。
事件用于流間同步。 它們是用 VPIEvent 實現(xiàn)的。 流程需要兩個屏障:一個等待 Harris 角點提取的輸入準備好,另一個等待預處理的右圖像。
VPIEvent barrier_right_grayscale, barrier_right_reduced; vpiEventCreate(0, &barrier_right_grayscale); vpiEventCreate(0, &barrier_right_reduced);初始化之后是主要處理階段,它通過以正確的順序向流提交算法和事件來實現(xiàn)流程。 流程的主循環(huán)可以使用相同的事件、有效負載、臨時緩沖區(qū)和輸出緩沖區(qū)多次執(zhí)行此操作。 輸入通常為每次迭代重新定義,如下所示。
提交左幀處理階段。
鏡頭畸變校正、圖像格式轉換和縮小都提交給左流。 再次注意提交操作是非阻塞的并立即返回。
vpiSubmitRemap(stream_left, VPI_BACKEND_VIC, ldc_left, left, left_rectified, VPI_INTERP_CATMULL_ROM,VPI_BORDER_ZERO, 0); vpiSubmitConvertImageFormat(stream_left, VPI_BACKEND_CUDA, left_rectified, left_grayscale, NULL); vpiSubmitRescale(stream_left, VPI_BACKEND_VIC, left_grayscale, left_reduced, VPI_INTERP_LINEAR, VPI_BORDER_CLAMP,0);提交正確幀預處理的前幾個階段。
鏡頭畸變校正和圖像格式轉換階段產生灰度圖像,用于哈里斯角提取的輸入。
vpiSubmitRemap(stream_right, VPI_BACKEND_VIC, ldc_right, right, right_rectified, VPI_INTERP_CATMULL_ROM,VPI_BORDER_ZERO, 0); vpiSubmitConvertImageFormat(stream_right, VPI_BACKEND_CUDA, right_rectified, right_grayscale, NULL);記錄正確的流狀態(tài),以便關鍵點流可以與之同步。
關鍵點流只能在其輸入準備好時開始。 首先,barrier_right_grayscale 事件必須通過向其提交一個任務來記錄正確的流狀態(tài),該任務將在格式轉換完成時發(fā)出事件信號。
vpiEventRecord(barrier_right_grayscale, stream_right);通過縮小操作完成正確的幀預處理。
vpiSubmitRescale(stream_right, VPI_BACKEND_VIC, right_grayscale, right_reduced, VPI_INTERP_LINEAR, VPI_BORDER_CLAMP,0);記錄右流狀態(tài),以便左流可以與之同步。
提交了整個右預處理后,必須再次記錄流狀態(tài),以便左流可以等待,直到右?guī)瑴蕚浜谩?/p> vpiEventRecord(barrier_right_reduced, stream_right);
讓左流等待,直到右?guī)瑴蕚浜谩?/p>
立體視差需要左右?guī)瑴蕚浜谩A鞒淌褂?vpiStreamWaitEvent 向左側流提交任務,該任務將等待右側流上的 barrier_right_reduced 事件發(fā)出信號,這意味著右側幀預處理已完成。
vpiStreamWaitEvent(stream_keypoints, barrier_right_grayscale);提交立體視差算法。
輸入圖像現(xiàn)已準備就緒。 調用 vpiSubmitStereoDisparityEstimator 提交 disparty 估計器。
vpiSubmitStereoDisparityEstimator(stream_left, VPI_BACKEND_CUDA, stereo, left_reduced, right_reduced, disparity,NULL, &stereo_params);提交關鍵點檢測器流程。
對于關鍵點檢測,首先在barrier_right_grayscale事件上提交一個等待操作,讓流程等待,直到輸入準備好。 然后提交哈里斯角點檢測器就可以了。
vpiSubmitHarrisCornerDetector(stream_keypoints, VPI_BACKEND_CPU, harris, right_grayscale, keypoints, scores,&harris_params);同步流以使用檢測到的視差圖和關鍵點。
請記住,到目前為止在處理階段調用的函數(shù)都是異步的; 一旦作業(yè)在流中排隊等待稍后執(zhí)行,它們就會立即返回。
現(xiàn)在可以在主線程上執(zhí)行更多處理,例如更新 GUI 狀態(tài)信息或顯示前一幀。 這發(fā)生在 VPI 執(zhí)行流程時。 執(zhí)行此附加處理后,必須使用 vpiStreamSync 同步處理來自當前幀的最終結果的流。 然后可以訪問生成的緩沖區(qū)。
vpiStreamSync(stream_left); vpiStreamSync(stream_keypoints);獲取下一幀并更新輸入封裝器。
現(xiàn)有的輸入 VPI 圖像封裝器可以重新定義以封裝接下來的兩個立體對幀,前提是它們的尺寸和格式相同。 此操作非常有效,因為它無需堆內存分配即可完成。
eglLeftFrame = /* Fetch next frame from left camera */; eglRightFrame = /* Fetch next from right camera */;dataLeft.bufferType = VPI_IMAGE_BUFFER_EGLIMAGE; dataLeft.buffer.egl = eglLeftFrame; vpiImageSetWrapper(left, &dataLeft);dataRight.bufferType = VPI_IMAGE_BUFFER_EGLIMAGE; dataRight.buffer.egl = eglRightFrame; vpiImageSetWrapper(right, &dataRight);銷毀上下文。
此示例在當前上下文下創(chuàng)建了許多對象。 一旦所有處理完成并且不再需要管道,就銷毀上下文。 然后所有流與所有其他使用的對象一起被同步和銷毀。 沒有內存泄漏是可能的。
銷毀當前上下文會重新激活在當前上下文變?yōu)榛顒又疤幱诨顒訝顟B(tài)的上下文。
vpiContextDestroy(ctx);return 0; }這些示例的重要內容:
- 算法提交立即返回。
- 算法執(zhí)行相對于主機線程異步發(fā)生。
- 不同的流可以使用相同的緩沖區(qū),但您必須通過使用事件來避免競爭條件。
- 上下文在其處于活動狀態(tài)時擁有由一個用戶線程創(chuàng)建的所有對象。 這允許一些有趣的場景,其中一個線程設置上下文并觸發(fā)所有處理管道,然后將整個上下文移動到等待管道結束的另一個線程,然后觸發(fā)進一步的數(shù)據處理。
總結
以上是生活随笔為你收集整理的NVIDIA VPI架构解析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: z390能装2012服务器系统,z390
- 下一篇: 前端学习-字体图标