英伟达Deepstream Kafka人车检测应用

513 阅读7分钟

在我之前的文章中:在生产中基于AI的计算机视觉应用的Nvidia Deepstream中,我谈到了如何使用NVIDIA Deepstream SDK实现一个最小的人员检测应用。

在这篇文章中,我们将扩展这个最初的物体检测应用,只要在预定义的区域内检测到一个人或车辆,就会向Kafka代理发送一个事件消息,其中包含该物体的元数据信息。

在这个视频中,你可以看到应用程序的输出。在预定义区域之外的红色物体通过绿色的边界框显示出来,如果它们进入这个区域,其边界框的颜色就会变成红色,同时,我们可以看到客户端正在接收消息。

NVIDIA Deesptream应用程序的视觉输出与Kafka代理接收消息。

利益区域(RoI)访问检测应用

整个应用程序的架构可以在下图中看到:

image.png

应用程序的架构

在这个应用中,我们可以有几个摄像头或视频流,由Deepstream管道处理。该管道检测人员和车辆,并确定他们是否在预先定义的感兴趣的区域(RoI)内,如果是这样,它将事件消息发送到Kafka服务器。然后,这些消息被客户端的Kafka消费者消费。

关于如何运行该应用程序的代码和说明可以在其GitHub仓库中看到。

Deepstream管道

正如我们在下图中看到的,管道是由几个插件或模块组成的,其中每一个都在处理管道中执行一个特定的任务。

深流管道

  1. 源仓:执行视频流的解码,在这种情况下,它来自一个URL。
  2. 混频器:将几个输入信号混合在一起,形成一批图像。
  3. Pgie:它是物体检测模型的推理引擎,这个插件的输出是模型检测到的人和车的边界框。
  4. 视频转换器:改变输入缓冲区的格式。例如,从RGBA到RGB。
  5. 滤波器:对输入信号进行过滤,只让特定类型的视频格式通过,或从视频中过滤音频。
  6. 编排:将不同数据流中的帧排列成一个单一的数据流。
  7. 显示器(On-Screate-Display):在平铺的画布上绘制由Pgie插件(检测模型)生成的边界框。
  8. Tee:将流水线分成几个分支,将相同的信息传递给所有的分支。
  9. 消息转换器:根据附加在批处理元数据上的信息生成一个有效载荷,在执行Pgie模块或任何其他分析模块时生成。
  10. 消息中介:将有效载荷发送至基于Apache Kafka等若干协议的消息代理。
  11. 水槽:它渲染带有在这个过程中创建的所有视觉信息(标示框、文本、RoI等)的平铺框架。

正如我在之前的文章中提到的,Deepstream建立在G-Streamer之上,其中主要的信息单位是GstBuffer。这个Gst-Buffer被依次传送到每个插件,获取现有的元数据,处理信息,并生成新的元数据,附加到缓冲区。

然而,我们也可以在主管道之外使用一个叫做Gst Buffer Probes的东西进行一些处理,正如它的名字一样,它作为一个信号探针从主管道中读取缓冲区,以便提取元数据并在需要时执行一些自定义逻辑。在我们的案例中,检测一个物体是否在预定义区域内的逻辑是通过这些探针实现的。

我们的管道的实现可以在pipeline.py脚本中看到。下面我们可以看到实现的代码片段,我们首先需要创建一个Gst.Pipeline对象,然后我们可以创建其余的插件,在这种情况下,是streammux和pgie插件。

# Initializing librariesGObject.threads_init()Gst.init(None)# creating deepstream elementslogging.info("Creating pipeline")self._pipeline = Gst.Pipeline()if not self._pipeline:    raise RuntimeError("Unable to create pipeline")logging.info("Creating streammux module")streammux = Gst.ElementFactory.make("nvstreammux", "stream-muxer")if not streammux:    raise RuntimeError("Unable to create streammux module")logging.info("Creating inference module")pgie = Gst.ElementFactory.make("nvinfer", "primary-inference")if not pgie:    raise RuntimeError("Unable to create inference module")

有些插件在被添加到管道之前需要进行配置,例如在streammux插件的情况下,我们需要设置所需的帧的宽度和高度,然后才能将它们堆叠成一个批次,以及批次的大小,等等。或者在pgie插件的情况下,我们需要指出对象检测器的配置文件的路径。

# Configuring the pluginsstreammux.set_property("width", 1920)streammux.set_property("height", 1080)streammux.set_property("batch_size", num_sources)pgie.set_property("config-file-path", self._model_config_path)

之后,我们可以将插件添加到管道中,并按顺序进行它们之间的连接。

logging.info("Adding elements to Pipeline")self._pipeline.add(streammux)self._pipeline.add(pgie)self._pipeline.add(nvvidconv1)self._pipeline.add(filter)logging.info("Linking elements in the pipeline")streammux.link(pgie)pgie.link(nvvidconv1)nvvidconv1.link(filter)

管道中的每个元素都有一个源垫和一个汇垫,源垫输出Gst-Buffer,同时,汇垫接收上游插件的Gst-Buffer。

插件垫

当我们连接插件时,我们所做的基本上是将第一个插件的源焊盘连接到第二个插件的汇焊盘。一些元素中的焊盘数量是固定的,或者它们只有一个,但在其他元素中,比如发球机插件,这些焊盘是动态的,可以根据路由信息所需的分支数量而实时实例化。在这个代码片断中,我们首先得到两个分支的队列插件的汇合垫,我们还从三通插件中请求两个源垫,然后我们执行源垫和汇合垫之间的连接。

# Get sink pads from both queue elementsqueue1_sink_pad = queue1.get_static_pad("sink")queue2_sink_pad = queue2.get_static_pad("sink")# Request source pads from the teetee_msg_pad = tee.get_request_pad('src_%u')tee_render_pad = tee.get_request_pad('src_%u')# Connect themtee_msg_pad.link(queue1_sink_pad)tee_render_pad.link(queue2_sink_pad)

最后,我们需要将探针添加到管道中。在我们的应用程序中,探针已经被添加到tiler模块的sink pad之前。这个探针与回调函数self._tiler_sink_buffer_probe相连,实现了兴趣区域逻辑。

tiler_sink_pad = tiler.get_static_pad("sink")tiler_sink_pad.add_probe(    Gst.PadProbeType.BUFFER,    self._tiler_sink_buffer_probe,    0)

感兴趣的区域逻辑

利益区域检测背后的逻辑非常简单,是由self._tiler_sink_buffer_probe函数实现的。该函数获取gst缓冲区,遍历每一帧中的每个物体,并提取边界框的坐标。RoI区是在每个流的配置文件中定义的。对于每个区域,代码会评估被分析对象的坐标是否在RoI多边形内。如果是这样,边界框的颜色就会变成红色。此外,用函数generate_event_msg_meta()创建一个特殊的事件消息元数据对象,它读取在帧和对象级别附加的元数据,并填充NvDsEventMsgMeta对象的字段。NvDsEventMsgMeta有一个固定的模式,但我们可以使用extMsg属性来扩展要填充的字段。一旦我们生成了元数据,我们就创建一个NvDsUserMeta对象,该对象持有用户元数据,并且是消息转换器插件将从中读取信息以生成要传输的有效载荷的对象。这个NvDsUserMeta对象是由先前生成的NvDsEventMsgMeta对象使用user_meta_data属性填充的。最后,这个NvDsUserMeta对象被附加到当前帧上。

def _tiler_sink_buffer_probe(self, pad, info, u_data):    gst_buffer = info.get_buffer()        if not gst_buffer:        logging.warning("Unable to get GstBuffer")            batch_meta = pyds.gst_buffer_get_nvds_batch_meta(hash(gst_buffer))    l_frame = batch_meta.frame_meta_list        while l_frame is not None:        try:            frame_meta = pyds.NvDsFrameMeta.cast(l_frame.data)        except StopIteration:            break                # Extracting the configuration for the current frame        source_id = frame_meta.source_id        stream_config = self.config[str(source_id)]        restricted_zones = stream_config["restricted_zones"]                # Display the restricted zones        for zone in restricted_zones:            display_meta = pyds.nvds_acquire_display_meta_from_pool(batch_meta)            display_meta.num_lines = len(zone)            lines_params = display_meta.line_params            for line_idx in range(display_meta.num_lines):                params = lines_params[line_idx]                line = zone[line_idx]                                                    params.x1 = line[0][0]                params.y1 = line[0][1]                params.x2 = line[1][0]                params.y2 = line[1][1]                                params.line_width = 3                params.line_color.set(1.0, 0, 0, 1.0)                            pyds.nvds_add_display_meta_to_frame(frame_meta, display_meta)                l_obj = frame_meta.obj_meta_list                while l_obj is not None:            try:                obj_meta = pyds.NvDsObjectMeta.cast(l_obj.data)            except StopIteration:                break                        # Making all the boxes green            obj_meta.rect_params.border_color.set(0, 1.0, 0, 1.0)                        # Only high confidence detections are evaluated            if (obj_meta.class_id == 0 and obj_meta.confidence >= stream_config["car_confidence"]) or \               (obj_meta.class_id == 2 and obj_meta.confidence >= stream_config["person_confidence"]):                                   feet_point_x = obj_meta.rect_params.left + obj_meta.rect_params.width / 2                feet_point_y = obj_meta.rect_params.top + obj_meta.rect_params.height                person_position = Point(feet_point_x, feet_point_y)                                for zone in restricted_zones:                    # Create polygons from lines, used to evaliated persons positions                    zone = MultiLineString(zone).convex_hull                    alarm = zone.contains(person_position)                    if alarm:                        obj_meta.rect_params.border_color.set(1.0, 0, 0, 1.0)                        msg_meta = generate_event_msg_meta(obj_meta, frame_meta)                        user_event_meta = pyds.nvds_acquire_user_meta_from_pool(batch_meta)                        if user_event_meta:                            user_event_meta.user_meta_data = msg_meta                            user_event_meta.base_meta.meta_type = pyds.NvDsMetaType.NVDS_EVENT_MSG_META                            pyds.user_copyfunc(user_event_meta, meta_copy_func)                            pyds.user_releasefunc(user_event_meta, meta_free_func)                            pyds.nvds_add_user_meta_to_frame(frame_meta, user_event_meta)                        else:                            logging.warning("Error in attaching event meta to buffer")                        break            else:                obj_meta.rect_params.border_width = 0                obj_meta.text_params.display_text = ""                obj_meta.text_params.set_bg_clr = 0                        try:                l_obj = l_obj.next            except StopIteration:                break                    try:            l_frame = l_frame.next        except StopIteration:            break            return Gst.PadProbeReturn.OK

这就是全部。为了测试这个应用程序,你还必须启动一个Kafka服务器,创建一个主题,并启动一个Kafka消费者。所有与这部分相关的说明也都包含在资源库中。

尽管我们通过使用Gst探针在管道外创建了这些元数据,但正确的方法是通过实现一个自定义插件,实现RoI逻辑和相应的事件消息元数据附件。我将在下一篇文章中解释这一点。希望你喜欢这篇文章,如果你有任何问题或建议,请在资源库中创建一个问题或留下评论。谢谢!