我們寫好的插件若是要可靠,當然得替他寫出可重複利用的測試,GStreamer有提供一套測試框架叫gstcheck,它提供一套完整的機制來對元件進行單元測試,而我們今天的目標就是用這個測試框架來驗證ironmancolormagic是否可以將輸入的GstBuffer處理成正確的像素
先建立測試專案的資料夾
mkdir -p /ironman/source/gst/colormagic/test
然後在資料夾底下建立test_ironmancolormagic.c
#include <gst/video/video.h>
#include <gst/check/gstcheck.h>
#include <gst/check/gstharness.h>
#include <cuda_runtime_api.h>
#include <nvbufsurface.h>
#include <gstnvdsmeta.h>
#define TEST_GPU_ID (0)
typedef struct {
guint8 r;
guint8 g;
guint8 b;
guint8 a;
} Color;
static NvBufSurface*
create_test_surface(guint batch_size, guint width, guint height)
{
NvBufSurface* surf;
NvBufSurfaceCreateParams params;
params.gpuId = TEST_GPU_ID;
params.width = width;
params.height = height;
params.size = 0;
/* Our element only accepts RGBA input */
params.colorFormat = NVBUF_COLOR_FORMAT_RGBA;
params.memType = NVBUF_MEM_DEFAULT;
if (NvBufSurfaceCreate(&surf, batch_size, ¶ms) < 0) {
g_error("Failed create NvBufSurface");
}
return surf;
}
static void
destroy_surface(gpointer data)
{
if (data)
{
NvBufSurface* surf = data;
NvBufSurfaceDestroy(surf);
}
}
static void
copy_image_to_surface(const NvBufSurface* surf, guint batch_id, guint8* data)
{
g_return_if_fail(surf != NULL);
g_return_if_fail(batch_id < surf->batchSize);
g_return_if_fail(data != NULL);
cudaError_t cuda_err;
cuda_err = cudaSetDevice((gint)surf->gpuId);
if (cuda_err != cudaSuccess)
{
g_critical("cudaSetDevice failed: %s", cudaGetErrorString(cuda_err));
return;
}
const NvBufSurfaceParams* surf_params = &surf->surfaceList[batch_id];
g_return_if_fail(surf_params->colorFormat == NVBUF_COLOR_FORMAT_RGBA);
const gsize data_pitch = (gsize)surf_params->width * 4;
const gsize data_width_bytes = (gsize)surf_params->width * 4;
cuda_err = cudaMemcpy2D(surf_params->dataPtr,
surf_params->pitch,
data,
data_pitch,
data_width_bytes,
surf_params->height,
cudaMemcpyHostToDevice);
if (cuda_err != cudaSuccess)
{
g_critical("cudaMemcpy2D failed: %s", cudaGetErrorString(cuda_err));
return;
}
}
static guint8*
copy_image_from_surface(const NvBufSurface* surf, guint batch_id)
{
g_return_val_if_fail(surf != NULL, NULL);
g_return_val_if_fail(batch_id < surf->batchSize, NULL);
cudaError_t cuda_err;
cuda_err = cudaSetDevice((gint)surf->gpuId);
if (cuda_err != cudaSuccess)
{
g_critical("cudaSetDevice failed: %s", cudaGetErrorString(cuda_err));
return NULL;
}
const NvBufSurfaceParams* surf_params = &surf->surfaceList[batch_id];
g_return_val_if_fail(surf_params->colorFormat == NVBUF_COLOR_FORMAT_RGBA, NULL);
const gsize data_pitch = (gsize)surf_params->width * 4;
const gsize data_width_bytes = (gsize)surf_params->width * 4;
const gsize data_size = data_pitch * surf_params->height;
guint8* data = g_malloc0(data_size);
cuda_err = cudaMemcpy2D(data,
data_pitch,
surf_params->dataPtr,
surf_params->pitch,
data_width_bytes,
surf_params->height,
cudaMemcpyDeviceToHost);
if (cuda_err != cudaSuccess)
{
g_critical("cudaMemcpy2D failed: %s", cudaGetErrorString(cuda_err));
g_free(data);
return NULL;
}
return data;
}
static NvDsBatchMeta* create_batch_meta_from_surface(const NvBufSurface* surf)
{
g_return_val_if_fail(surf != NULL, NULL);
g_return_val_if_fail(surf->batchSize > 0, NULL);
g_return_val_if_fail(surf->numFilled > 0 && surf->numFilled <= surf->batchSize, NULL);
NvDsBatchMeta* batch_meta = nvds_create_batch_meta(surf->batchSize);
if (!batch_meta)
{
g_error("Failed to create NvDsBatchMeta");
}
for (guint i = 0; i < surf->numFilled; i++)
{
NvDsFrameMeta* frame_meta = nvds_acquire_frame_meta_from_pool(batch_meta);
/* We only care `batch_id` in `ironmancolormagic`, the other fields
* don't affect the result and we don't need to fill them */
frame_meta->batch_id = i;
nvds_add_frame_meta_to_batch(batch_meta, frame_meta);
}
return batch_meta;
}
static gboolean
attach_batch_meta_to_buffer(GstBuffer* buf, NvDsBatchMeta* batch_meta)
{
g_return_val_if_fail(buf != NULL, FALSE);
NvDsMeta* meta = gst_buffer_add_nvds_meta(buf,
batch_meta,
NULL,
nvds_batch_meta_copy_func,
nvds_batch_meta_release_func);
if (!meta)
{
g_critical("Failed to add NvDsBatchMeta to the GstBuffer");
return FALSE;
}
/* Must set 'meta_type' manually */
meta->meta_type = NVDS_BATCH_GST_META;
return TRUE;
}
static void
set_uniform_color(guint width, guint height, guint8* data, Color color)
{
g_return_if_fail(width > 0);
g_return_if_fail(height > 0);
g_return_if_fail(data != NULL);
const gsize stride = width * 4;
for (gsize y = 0; y < height; y++)
{
for (gsize x = 0; x < width; x++)
{
Color* pixel = (Color*)&data[stride * y + x * 4];
*pixel = color;
}
}
}
static gboolean
is_blob_equals(const guint8* a, const guint8* b, gsize size)
{
g_return_val_if_fail(a != NULL, FALSE);
g_return_val_if_fail(b != NULL, FALSE);
for (gsize i = 0; i < size; i++)
{
if (a[i] != b[i])
return FALSE;
}
return TRUE;
}
#define FEATURE_NVMM "memory:NVMM"
static GstStaticPadTemplate upstream_src_pad =
GST_STATIC_PAD_TEMPLATE ("src",
GST_PAD_SRC,
GST_PAD_ALWAYS,
GST_STATIC_CAPS (GST_VIDEO_CAPS_MAKE_WITH_FEATURES(FEATURE_NVMM, "{RGBA}"))
);
static GstStaticPadTemplate downstream_sink_pad =
GST_STATIC_PAD_TEMPLATE ("sink",
GST_PAD_SINK,
GST_PAD_ALWAYS,
GST_STATIC_CAPS (GST_VIDEO_CAPS_MAKE_WITH_FEATURES(FEATURE_NVMM, "{RGBA}"))
);
GST_START_TEST(test_transform_color)
{
/* Set up element */
GstElement* sut = gst_element_factory_make("ironmancolormagic", NULL);
fail_unless(sut != NULL);
g_object_set(sut, "gpu-id", TEST_GPU_ID, NULL);
/* Create a harness and add the element to it, the harness will play after created */
GstHarness* h = gst_harness_new_full(sut,
&upstream_src_pad,
"sink",
&downstream_sink_pad,
"src");
fail_unless(h != NULL);
{
GstCaps* caps = gst_caps_from_string(
"video/x-raw(memory:NVMM), format=RGBA, width=16, height=16");
/* Must call this function before pushing a GstBuffer */
gst_harness_set_src_caps(h, caps);
}
/* Prepare GstBuffer for testing */
NvBufSurface* surf = create_test_surface(1, 16, 16);
fail_unless(surf != NULL);
GstBuffer* in_buf = gst_buffer_new_wrapped_full(
GST_MEMORY_FLAG_READONLY,
surf,
sizeof(NvBufSurface),
0,
sizeof(NvBufSurface),
surf,
destroy_surface);
const gsize image_data_size = (gsize)16 * 4 * 16;
guint8* image_data = g_malloc0(image_data_size);
set_uniform_color(16, 16, image_data, (Color){150, 100, 50, 255});
copy_image_to_surface(surf, 0, image_data);
surf->numFilled = 1;
{
NvDsBatchMeta* batch_meta = create_batch_meta_from_surface(surf);
fail_unless(batch_meta != NULL);
fail_unless(attach_batch_meta_to_buffer(in_buf, batch_meta) == TRUE);
}
/* We only support transform_ip, so the input buffer
* and the output buffer should be the same instance */
gst_harness_push(h, in_buf);
GstBuffer* out_buf = gst_harness_pull(h);
fail_unless(in_buf == out_buf);
set_uniform_color(16, 16, image_data, (Color){100, 50, 150, 255});
guint8* result_image_data = copy_image_from_surface(surf, 0);
fail_unless(is_blob_equals(image_data, result_image_data, image_data_size) == TRUE);
g_free(image_data);
g_free(result_image_data);
gst_buffer_unref(out_buf);
gst_harness_teardown(h);
}
GST_END_TEST;
static Suite*
ironmancolormagic_suite(void)
{
Suite* s = suite_create("ironmancolormagic_test");
TCase* tc = tcase_create("change_color");
suite_add_tcase(s, tc);
tcase_add_test(tc, test_transform_color);
return s;
}
GST_CHECK_MAIN(ironmancolormagic);
建立Makefile
TARGET := test_ironmancolormagic
CUDA_DIR := /usr/local/cuda
DS_DIR := /opt/nvidia/deepstream/deepstream
CC := gcc
PKGS := gstreamer-check-1.0
CFLAGS := -g -Wall -Wextra $(shell pkg-config --cflags $(PKGS)) \
-I$(CUDA_DIR)/include \
-I$(DS_DIR)/sources/includes
LDFLAGS := $(shell pkg-config --libs $(PKGS)) \
-L$(CUDA_DIR)/lib64 -lcudart \
-L$(DS_DIR)/lib -lnvbufsurface -lnvds_meta -lnvdsgst_meta \
-Wl,-rpath,"$(CUDA_DIR)/lib64:$(DS_DIR)/lib"
SRCS := test_ironmancolormagic.c
OBJS := $(SRCS:.c=.o)
all: $(TARGET)
$(TARGET): $(OBJS)
$(CC) -o $@ $^ $(LDFLAGS)
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
%.o: %.cu
$(NVCC) $(NVCC_CFLAGS) -c $< -o $@
clean:
rm -f $(OBJS) $(TARGET)
test: all
GST_PLUGIN_PATH="../src" ./$(TARGET)
debug_test: all
GST_PLUGIN_PATH="../src" CK_FORK=no gdb ./$(TARGET)
.PHONY: all clean test debug_test
接著就可以編譯測試看看
make test
應該會看到這樣的輸出,這樣就代表成功了
GST_PLUGIN_PATH="../src" ./test_ironmancolormagic
Running suite(s): ironmancolormagic_test
100%: Checks: 1, Failures: 0, Errors: 0
Check suite ironmancolormagic ran in 0.121s (tests failed: 0)
GstCheck建立測試程式一個測試用例(Test Case)必須是以GST_START_TEST開頭,GST_END_TEST結尾,參考範例中的test_transform_color;再來要建立一個測試套件(Test Suite)去包裝你寫好的各種測試用例,參考範例中的ironmancolormagic_suite;最後一步就是定義GST_CHECK_MAIN,這樣執行檔就用GstCheck這個框架去執行測試
GstCheck預設會啟動子進程執行每一個測試,我們Makefile中的規則test就是用預設模式去執行。
test: all
GST_PLUGIN_PATH="../src" ./$(TARGET)
但是當測試不是那麼順利時,我們必須得從測試程式開始往下除錯,這時會面臨沒有辦法讓Debugger中斷在子進程的問題,所以規則debug_test多加上了CK_FORK=no來停用產生子進程這個運行模式
debug_test: all
GST_PLUGIN_PATH="../src" CK_FORK=no gdb ./$(TARGET)
至於這次的範例我是用gdb進行除錯,如果程式發生崩潰gdb可以直接查詢崩潰當下的Stack Trace,對於一般的除錯工作其實就很夠用了,如果你有興趣的話可以試著練習使用
/* Set up element */
GstElement* sut = gst_element_factory_make("ironmancolormagic", NULL);
fail_unless(sut != NULL);
g_object_set(sut, "gpu-id", TEST_GPU_ID, NULL);
因為GstHarness建立之後會自動進入PLAYING狀態,所以我們得在建立GstHarness之前先將GstElement建立好並且設定屬性
GstHarness /* Create a harness and add the element to it, the harness will play after created */
GstHarness* h = gst_harness_new_full(sut,
&upstream_src_pad,
"sink",
&downstream_sink_pad,
"src");
fail_unless(h != NULL);
其實GstHarness可以視為GStreamer提供的Mocking Library,它提供了假的上游Src Pad和下游Sink Pad,開發者可以使用GstHarness完美的模擬元件的各種輸入並取得輸出
{
GstCaps* caps = gst_caps_from_string(
"video/x-raw(memory:NVMM), format=RGBA, width=16, height=16");
/* Must call this function before pushing a GstBuffer */
gst_harness_set_src_caps(h, caps);
}
接著就是輸入GstBuffer前要記得呼叫gst_harness_set_src_caps,這樣GStreamer才會把Caps固定到測試狀態,否則會處於未定狀態並發出警告
NvBufSurface* surf = create_test_surface(1, 16, 16);
fail_unless(surf != NULL);
GstBuffer* in_buf = gst_buffer_new_wrapped_full(
GST_MEMORY_FLAG_READONLY,
surf,
sizeof(NvBufSurface),
0,
sizeof(NvBufSurface),
surf,
destroy_surface);
NvBufSurface得使用DeepStream SDK的函式分配/釋放資源,所以這邊得用到gst_buffer_new_wrapped_full建立GstBuffer。GstBuffer本身只是資料的容器,這邊需要做的就是告訴它我的資料有多大,注意是資料本身有多大,也就是NvBufSurface本身有多大
使用gst_buffer_new_wrapped_full如果要釋放資源,必須傳入user_data並搭配notify,為什麼是這樣呢?因為你要封入GstBuffer的資料可能只是你分配資源的某一個片段,所以得透過這種間接的手段去釋放資源。想知道DeepStream元件如何建立GstBuffer的話,可以去參考原始碼/opt/nvidia/deepstream/deepstream/sources/gst-plugins/gst-nvmultistream2/gstnvstreammux_impl.h的148行
const gsize image_data_size = (gsize)16 * 4 * 16;
guint8* image_data = g_malloc0(image_data_size);
set_uniform_color(16, 16, image_data, (Color){150, 100, 50, 255});
copy_image_to_surface(surf, 0, image_data);
surf->numFilled = 1;
要驗證元件的行為是否正確的話,我們得傳入一個已知像素的影像,這樣才能用已知輸出去驗證,這邊就是輸入一個16x16的RGBA影像,每個像素都是(150, 100, 50, 255),元件要把像素從RGBA轉成GBRA,所以輸出應該要得到每個像素都是(100, 50, 150, 255)
這邊是寫測試所以不必特別考慮效能,我們可以先在Host設定好測試影像再複製到NvBufSurfaceParams上,你總不會為了測試特別寫個CUDA Kernel對吧,詳細的做法可以看set_uniform_color和copy_image_to_surface。這邊提一下cudaMemcpy2D,這是複製二維影像的一個高效API,它可以允許src和dst擁有不同大小的pitch,而這正好是我們需要的,我們可以讓image_data分配剛好的大小,而NvBufSurface分配帶有特定pitch的大小
NvDsBatchMeta {
NvDsBatchMeta* batch_meta = create_batch_meta_from_surface(surf);
fail_unless(batch_meta != NULL);
fail_unless(attach_batch_meta_to_buffer(in_buf, batch_meta) == TRUE);
}
我們的元件需要NvDsBatchMeta來找到正確的NvBufSurfaceParams,所以測試流程也必須把NvDsBatchMeta掛到GstBuffer上。建立Meta的部分比較簡單,我們只需要用到NvDsFrameMeta::batch_id,所以把資料填對就行。新手比較要注意的是附加Meta用到的gst_buffer_add_nvds_meta,一定要記得設定回傳的NvDsMeta::meta_type,這邊要填入的是NVDS_BATCH_GST_META,如果你沒有做這一步,你的元件會撈不到NvDsBatchMeta
GstBuffer並驗證結果 /* We only support transform_ip, so the input buffer
* and the output buffer should be the same instance */
gst_harness_push(h, in_buf);
GstBuffer* out_buf = gst_harness_pull(h);
fail_unless(in_buf == out_buf);
set_uniform_color(16, 16, image_data, (Color){100, 50, 150, 255});
guint8* result_image_data = copy_image_from_surface(surf, 0);
fail_unless(is_blob_equals(image_data, result_image_data, image_data_size) == TRUE);
因為元件只支持transform_ip,輸入跟輸出的GstBuffer是同一個,所以這裡使用gst_harness_push和gst_harness_pull應該要得到同一個GstBuffer
再來就是要把影像從NvBufSurfaceParams::dataPtr複製回Host進行驗證,驗證用的參考影像我們可以重複使用image_data這個buffer,因為先前把影像複製到GPU後他的任就完成了,這邊就是把它的像素修改成我們預期的(100, 50, 150, 255),再把它拿來跟輸出的result_image_data進行比對
g_free(image_data);
g_free(result_image_data);
gst_buffer_unref(out_buf);
gst_harness_teardown(h);
最後就是要記得釋放我們測試過程中分配的資源,然後要留意哪些資源的所有權已經被轉移了,例如sut加入h之後,所有權就轉交到h;in_buf推入h之後所有權會轉交給h,但我們再從h拉取out_buf,此時所有權又轉交回我們身上。因此最後我們需要釋放的資源才是上面看到的這些
要寫出好的元件必須得搭配好的測試,而GStreamer運行時的變數非常複雜,透過GstCheck建立良好的單元測試能夠讓你更精準地排除潛在的Bug,並且每次更新元件時你可以非常有把握的驗證每一項功能,而不是一直運行Pipeline用肉眼看結果