Draw Function Usage & Customization

Often, the pipeline must display metadata on frames. The developer may utilize OpenCV CUDA functionality within a custom pyfunc to do that. However, Savant provides a special facility called draw_func that is designed for displaying overlay information on frames.

There are two ways to use draw_func:

  • predefined draw_func, configurable in a declarative manner;

  • custom draw_func implementation in Python.

Declarative Configuration

The predefined draw_func supports many of operations required. Usually, it is enough for pipelines when there is no need to display advanced graphics.

To set the parameters for the draw_func operation, the general parameters section is used, since the rendering step always occurs in a fixed place: after the completion of all elements of the pipeline, before passing frames and metadata to external consumers.

parameters:
  draw_func:
    module: draw_func.module
    class_name: CustomDrawFuncClass
    kwargs: {}

In the example above, a custom draw_func implementation is used. In simple cases you may use a predefined draw_func implementation.

Note

To disable draw_func functionality, remove parameters.draw_func from the manifest completely.

It is important to note that rendering is not performed if no frame encoding scheme is configured for the pipeline. To set the frame coding scheme, you must specify the codec in the parameters.output_frame.codec parameter to one of the following values: jpeg, h264, hevc, raw-rgba.

parameters:
  output_frame:
    codec: jpeg

Savant has a default implementation of draw_func that is used when draw_func parameter is set to an empty dictionary:

parameters:
  draw_func: {}

Another way to use the default implementation is to populate the rendered_objects section, which the default implementation uses to define the objects of which classes are going to be rendered and their render specifications. The rendered_objects section is a dictionary of the following structure:

parameters:
  draw_func:
    rendered_objects:
      <unit_name>:
        <class_label>:
          bbox:
            backgound_color: <color_str>
            border_color: <color_str>
            thickness: <int>
            padding:
              left: <int>
              top: <int>
              right: <int>
              bottom: <int>
          label:
            background_color: <color_str>
            border_color: <color_str>
            font_color: <color_str>
            font_scale: <float>
            thickness: <int>
            format:
              - "Label: {label}"
              - "Confidence: {confidence:.2f}"
              - "Track ID: {track_id}"
              - "Model: {model}"
            padding:
              left: <int>
              top: <int>
              right: <int>
              bottom: <int>
            position:
              position: TopLeftInside / TopLeftOutside / Center
              margin_x: <int>
              margin_y: <int>
          central_dot:
            color: <color_str>
            radius: <int>
          blur: <true/false>

where:

  • <unit_name> the name of the unit defining the objects;

  • <class_label> the label of the object class set by a detector, draw label is used in place of the class label if it is set by the user;

  • <color_str> color used to draw the specified element, color is defined as a RGBA hex string (without the ‘#’ as it marks a comment in YAML), e.g. "00ff00ff" for green;

Any of the elements in the render specification (bbox, label, central_dot, blur) can be omitted, if the corresponding element is not required to be rendered. Blur is false by default.

Label format is defined as a list of strings, where each string is a format string that can contain the following placeholders: {label}, {confidence}, {track_id}, {model}. Each string in the list is rendered on a separate line. The 4 line config above is provided as an example.

Customization

Besides the standard draw_func, it is also possible to use a custom draw function. In this case, the function must inherit the NvDsDrawFunc class, overriding the draw_on_frame method or override_draw_spec method in it.

class CustomFunc(NvDsDrawFunc):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        # todo

    def override_draw_spec(
        self, object_meta: ObjectMeta, specification: ObjectDraw
    ) -> ObjectDraw:
        # todo
        return specification

    def draw_on_frame(self, frame_meta: NvDsFrameMeta, artist: Artist):
        # todo

In the draw_on_frame method, by processing meta-information, you can select the objects of interest to the user and, using the values of various object properties (class, coordinates, track id), add graphics to the frame through the methods of the Artist object.

The override_draw_spec method is a simpler way to customize drawing of objects. It allows overriding the configured drawing specification for a given object. The method receives the object meta and the default drawing specification and returns the changed drawing specification. The returned drawing specification is then used to draw the object. There’s no need to learn the Artist object API to use this method.

The draw_func feature uses Artist object which implements displaying a number of primitives like text labels, bounding boxes, etc. The Artist object also can be used directly from any pyfunc, but in current section we discuss the use of the draw_func function.

Artist Methods

Add_text Method

The add_text method allows you to add text to the frame, with a given value, position, text color and background color:

def add_text(
    self,
    text: str,
    anchor: Tuple[int, int],
    font_scale: float = 0.5,
    font_thickness: int = 1,
    font_color: Tuple[int, int, int, int] = (255, 255, 255, 255),  # white
    border_width: int = 0,
    border_color: Tuple[int, int, int, int] = (255, 0, 0, 255),  # red
    bg_color: Optional[Tuple[int, int, int, int]] = (0, 0, 0, 255),  # black
    padding: Tuple[int, int, int, int] = (0, 0, 0, 0),
    anchor_point_type: Position = Position.CENTER,
) -> int:

For example, such a call will add white text on a black background to the upper left corner of detected objects with the name of the object class.

for obj_meta in frame_meta.objects:
    artist.add_text(
        text=obj_meta.label,
        anchor=(int(obj_meta.bbox.left), int(obj_meta.bbox.top)),
        anchor_point=Position.LEFT_TOP,
    )

Add_bbox Method

The add_bbox method allows you to add a frame to the frame with specified coordinates, thickness, frame color, and background color inside the frame.

def add_bbox(
    self,
    bbox: Union[BBox, RBBox],
    border_width: int = 3,
    border_color: Tuple[int, int, int, int] = (0, 255, 0, 255),  # RGBA, Green
    bg_color: Optional[Tuple[int, int, int, int]] = None,  # RGBA
    padding: Tuple[int, int, int, int] = (0, 0, 0, 0),
):

For example, the following call will add a green border around each detected object.

for obj_meta in frame_meta.objects:
    artist.add_bbox(obj_meta.bbox)

Add_rounded_rect Method

The add_rounded_rect method allows you to add a rectangle with rounded corners of the specified color to the frame.

def add_rounded_rect(
    self,
    bbox: BBox,
    radius: int,
    bg_color: Tuple[int, int, int, int],  # RGBA
):

For example, the following call will add a blue rounded square with a width and height of 100 px in the top left corner of the frame.

from savant_rs.primitives.geometry import BBox


artist.add_rounded_rect(
    bbox=BBox(50, 50, 100, 100),
    radius=4,
    bg_color=(0, 0, 255, 255),
)

Add_circle Method

The add_circle method allows you to add a circle to the frame with the given coordinates, radius, and color.

def add_circle(
    self,
    center: Tuple[int, int],
    radius: int,
    color: Tuple[int, int, int, int],
    thickness: int,
    line_type: int = cv2.LINE_AA,
):

For example, the following call will add a red round bullet of radius 3 to the center of each object:

import cv2


for obj_meta in frame_meta.objects:
    center = round(obj_meta.bbox.xc), round(obj_meta.bbox.yc)
    artist.add_circle(center, 3, (255, 0, 0, 255), cv2.FILLED)

Add_polygon Method

The add_polygon method allows you to add an arbitrary polygon to the frame, defined by a sequence of points, with a specified outline thickness, outline color, and background color.

def add_polygon(
    self,
    vertices: List[Tuple[int, int]],
    line_width: int = 3,
    line_color: Tuple[int, int, int, int] = (255, 0, 0, 255),  # RGBA, Red
    bg_color: Optional[Tuple[int, int, int, int]] = None,  # RGBA
):

For example, the following call will add a red line segment to the frame between two points with given coordinates.

pt1 = (100, 100)
pt2 = (200, 200)
artist.add_polygon([pt1, pt2])

Add_graphic Method

The add_graphic method allows you to add an arbitrary sprite to the frame, previously loaded in OpenCV CUDA GpuMat, at a given position defined by the coordinates of the upper left corner.

def add_graphic(self, img: cv2.cuda.GpuMat, origin: Tuple[int, int])

For example, the following call will add to the frame an image read from a file at the given path, with the upper left corner of the image placed in the upper left corner of the frame.

import cv2

img = cv2.imread(path, cv2.IMREAD_UNCHANGED)
img = cv2.cvtColor(img , cv2.COLOR_BGRA2RGBA)
img = cv2.cuda.GpuMat(img)
artist.add_graphic(img , (0, 0))

Blur Method

The blur method allows you to apply Gaussian blur to a given area of the frame with the ability to set the standard deviation value.

def blur(
    self,
    bbox: BBox,
    padding: Tuple[int, int, int, int] = (0, 0, 0, 0),
    sigma: Optional[float] = None,
):

For example, the following call will apply a blur to the objects detected on the frame, while the sigma for each object will be calculated automatically based on its size.

for obj_meta in frame_meta.objects:
    artist.blur(obj_meta.bbox)