DeepStream SDK FAQ

40.[ ALL_Python_APP]How to save a frame or object in Python

1.Add Python binding for nvds_obj_enc_process

diff --git a/bindings/CMakeLists.txt b/bindings/CMakeLists.txt
index 7b01fb2..6df8952 100644
--- a/bindings/CMakeLists.txt
+++ b/bindings/CMakeLists.txt
@@ -98,7 +98,7 @@ function(add_ds_lib libname)
         target_link_libraries(pyds ${libname})
 endfunction()

-foreach(nvds_lib nvds_osd nvds_meta nvds_infer nvdsgst_meta nvbufsurface nvbufsurftransform nvdsgst_helper)
+foreach(nvds_lib nvds_osd nvds_meta nvds_infer nvdsgst_meta nvbufsurface nvbufsurftransform nvdsgst_helper nvds_batch_jpegenc)
         add_ds_lib(${nvds_lib})
 endforeach()

diff --git a/bindings/docstrings/functionsdoc.h b/bindings/docstrings/functionsdoc.h
index 2796605..371d77b 100644
--- a/bindings/docstrings/functionsdoc.h
+++ b/bindings/docstrings/functionsdoc.h
@@ -627,5 +627,35 @@ namespace pydsdoc

             #add this code in plugin probe function.
             num_sources_in_batch = pyds.nvds_measure_buffer_latency(hash(gst_buffer));)pyds";
-       }
+
+        constexpr const char* nvds_obj_enc_create_context=R"pyds(
+            Create context and return a handle to NvObjEncCtx.
+
+            :arg gpu_id: gpu id.
+
+            :returns: handle to NvObjEncCtx.)pyds";
+
+        constexpr const char* nvds_obj_enc_process=R"pyds(
+            Enqueue an object crop for JPEG encode.
+            This is a non-blocking call and user should call `nvds_obj_enc_finish()`
+            to make sure all enqueued object crops have been processed.
+
+            :arg context: handle to NvObjEncCtx.
+            :arg args: An object of type :class:`NvDsObjEncUsrArgs`.
+            :arg buffer: GstBuffer from which to retrieve the :class:`NvBufSurface`
+            :arg obj_meta: An object of type :class:`NvDsOjbectMeta`.
+            :arg frame_meta: An object of type :class:`NvDsFrameMeta`.
+
+            :returns: 0 for success, -1 for failure.)pyds";
+
+        constexpr const char* nvds_obj_enc_finish=R"pyds(
+            Wait for all enqueued crops to be encoded.
+
+            :arg context: handle to NvObjEncCtx.)pyds";
+
+        constexpr const char* nvds_obj_enc_destroy_context=R"pyds(
+            Destroy NvObjEncCtx context.
+
+            :arg context: handle to NvObjEncCtx.)pyds";
+    }
 }
\ No newline at end of file
diff --git a/bindings/docstrings/utilsdoc.h b/bindings/docstrings/utilsdoc.h
new file mode 100644
index 0000000..866c21f
--- /dev/null
+++ b/bindings/docstrings/utilsdoc.h
@@ -0,0 +1,44 @@
+/*
+ * SPDX-FileCopyrightText: Copyright (c) 2021-2023 NVIDIA CORPORATION &
+ * AFFILIATES. All rights reserved. SPDX-License-Identifier: Apache-2.0
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+namespace pydsdoc {
+namespace utilsdoc {
+namespace NvDsObjEncUsrArgsDoc {
+constexpr const char *descr = R"pyds(
+                user parameters for a nvds_obj_enc_process call
+
+                :ivar numDims: *int*, Number of dimesions of the layer.
+                :ivar numElements: *int*, Number of elements in the layer including all dimensions.
+                :ivar d: *np.array of int*, Size of the layer in each dimension.)pyds";
+
+constexpr const char *cast =
+    R"pyds(cast given object/data to :class:`NvDsObjEncUsrArgs`, call pyds.NvDsObjEncUsrArgs.cast(data))pyds";
+} // namespace NvDsObjEncUsrArgsDoc
+
+namespace NvDsObjEncOutParamsDoc {
+constexpr const char *descr = R"pyds(
+                output parameters of NVDS_CROP_IMAGE_META
+
+                :ivar outBuffer: *int*, The output buffer JPEG Encoded Object.)pyds";
+
+constexpr const char *cast =
+    R"pyds(cast given object/data to :class:`NvDsObjEncOutParams`, call pyds.NvDsObjEncOutParams.cast(data))pyds";
+} // namespace NvDsObjEncOutParamsDoc
+} // namespace utilsdoc
+} // namespace pydsdoc
\ No newline at end of file
diff --git a/bindings/include/utils.hpp b/bindings/include/utils.hpp
index f713b9b..223be0a 100644
--- a/bindings/include/utils.hpp
+++ b/bindings/include/utils.hpp
@@ -43,6 +43,9 @@ namespace pydeepstream {
     pybind11::arg operator ""_a(const char *str, size_t len);
 }

+namespace pydeepstream {
+    void bindutils(py::module &m);
+}

 namespace pydeepstream::utils {

diff --git a/bindings/src/bindfunctions.cpp b/bindings/src/bindfunctions.cpp
index cc2c9cc..88e7163 100644
--- a/bindings/src/bindfunctions.cpp
+++ b/bindings/src/bindfunctions.cpp
@@ -17,6 +17,7 @@

 #include "bind_string_property_definitions.h"
 #include "bindfunctions.hpp"
+#include "nvds_obj_encode.h"

 namespace py = pybind11;
 using namespace std;
@@ -839,5 +840,49 @@ namespace pydeepstream {
               },
               "gst_buffer"_a, py::return_value_policy::reference,
               pydsdoc::methodsDoc::nvds_measure_buffer_latency);
+
+        m.def("nvds_obj_enc_create_context",
+            [](int gpu_id) -> size_t {
+                auto handle = nvds_obj_enc_create_context(gpu_id);
+                std::cout << "nvds_obj_enc_create_context: " << handle << std::endl;
+                return reinterpret_cast<size_t>(handle);
+            }, py::return_value_policy::reference,
+            pydsdoc::methodsDoc::nvds_obj_enc_create_context);
+
+        m.def("nvds_obj_enc_process",
+            [](size_t ctx, NvDsObjEncUsrArgs *args,
+                size_t gst_buffer, NvDsObjectMeta *obj_meta, NvDsFrameMeta *frame_meta) -> bool {
+                auto *buffer = reinterpret_cast<GstBuffer *>(gst_buffer);
+                auto *handle = reinterpret_cast<NvDsObjEncCtxHandle>(ctx);
+                if (buffer == nullptr || handle == nullptr) {
+                    std::cout << "buffer: " << buffer << " handle: " << handle << std::endl;
+                    return false;
+                }
+
+                GstMapInfo inmap;
+                gst_buffer_map(buffer, &inmap, GST_MAP_READ);
+                auto *inputnvsurface = reinterpret_cast<NvBufSurface *>(inmap.data);
+                gst_buffer_unmap(buffer, &inmap);
+
+                return nvds_obj_enc_process(handle, args, inputnvsurface, obj_meta, frame_meta);
+            }, "ctx"_a, "args"_a, "gst_buffer"_a, "obj_meta"_a, "frame_meta"_a,
+            py::return_value_policy::reference,
+            pydsdoc::methodsDoc::nvds_obj_enc_process);
+
+        m.def("nvds_obj_enc_finish",
+            [](size_t ctx) {
+                auto *handle = reinterpret_cast<NvDsObjEncCtxHandle>(ctx);
+                if (handle != nullptr) {
+                    nvds_obj_enc_finish(handle);
+                }
+            }, "ctx"_a, pydsdoc::methodsDoc::nvds_obj_enc_finish);
+
+        m.def("nvds_obj_enc_destroy_context",
+            [](size_t ctx) {
+                auto *handle = reinterpret_cast<NvDsObjEncCtxHandle>(ctx);
+                if (handle != nullptr) {
+                    nvds_obj_enc_destroy_context(handle);
+                }
+            }, "ctx"_a, pydsdoc::methodsDoc::nvds_obj_enc_destroy_context);
     }
 }
diff --git a/bindings/src/pyds.cpp b/bindings/src/pyds.cpp
index 30d02ff..d1ac84e 100644
--- a/bindings/src/pyds.cpp
+++ b/bindings/src/pyds.cpp
@@ -63,6 +63,7 @@ namespace pydeepstream {
         bindnvbufsurface(m);
         bindnvdsinfer(m);
         bindopticalflowmeta(m);
+        bindutils(m);
         bindcustom(m);

     }   // end PYBIND11_MODULE(pyds, m)
diff --git a/bindings/src/utils.cpp b/bindings/src/utils.cpp
index 356ff8a..9bafe24 100644
--- a/bindings/src/utils.cpp
+++ b/bindings/src/utils.cpp
@@ -24,8 +24,13 @@
 #include <functional>
 #include <iostream>
 #include "gstnvdsmeta.h"
+#include "pyds.hpp"
 #include "nvdsmeta_schema.h"
 #include "utils.hpp"
+#include "nvds_obj_encode.h"
+#include "bind_string_property_definitions.h"
+#include "../../docstrings/utilsdoc.h"
+
 /**
  * Specifies the type of function to copy meta data.
  * It is passed the pointer to meta data and user specific data.
@@ -44,12 +49,53 @@ using COPYFUNC_SIG = gpointer(gpointer, gpointer);
  */
 using RELEASEFUNC_SIG = void(gpointer, gpointer);

+namespace py = pybind11;
+
 namespace pydeepstream {
     pybind11::arg operator ""_a(const char *str, size_t len) {
         return pybind11::arg(str);
     }
 }

+namespace pydeepstream {
+    void bindutils(py::module &m) {
+        py::class_<NvDsObjEncOutParams>(m, "NvDsObjEncOutParams",
+                                        pydsdoc::utilsdoc::NvDsObjEncOutParamsDoc::descr)
+                .def(py::init<>())
+                .def("outBuffer", [](const NvDsObjEncOutParams &self) {
+                    // Use this if the C++ buffer should NOT be deallocated
+                    // once Python no longer has a reference to it
+                    py::capsule buffer_handle([](){});
+                    return py::array(self.outLen, self.outBuffer, buffer_handle);
+                })
+                .def("cast", [](void *data) {
+                         return (NvDsObjEncOutParams *) data;
+                     },
+                     py::return_value_policy::reference,
+                     pydsdoc::utilsdoc::NvDsObjEncOutParamsDoc::cast);
+
+        py::class_<NvDsObjEncUsrArgs>(m, "NvDsObjEncUsrArgs",
+                                     pydsdoc::utilsdoc::NvDsObjEncUsrArgsDoc::descr)
+                .def(py::init<>())
+                .def_readwrite("saveImg", &NvDsObjEncUsrArgs::saveImg)
+                .def_readwrite("attachUsrMeta", &NvDsObjEncUsrArgs::attachUsrMeta)
+                .def_readwrite("scaleImg", &NvDsObjEncUsrArgs::scaleImg)
+                .def_readwrite("scaledWidth", &NvDsObjEncUsrArgs::scaledWidth)
+                .def_readwrite("scaledHeight", &NvDsObjEncUsrArgs::scaledHeight)
+                .def_property("fileNameImg", STRING_CHAR_ARRAY(NvDsObjEncUsrArgs, fileNameImg))
+                .def_readwrite("objNum", &NvDsObjEncUsrArgs::objNum)
+                .def_readwrite("quality", &NvDsObjEncUsrArgs::quality)
+                .def_readwrite("isFrame", &NvDsObjEncUsrArgs::isFrame)
+                .def_readwrite("calcEncodeTime", &NvDsObjEncUsrArgs::calcEncodeTime)
+                .def("cast",
+                     [](void *data) {
+                         return (NvDsObjEncUsrArgs *) data;
+                     },
+                     py::return_value_policy::reference,
+                     pydsdoc::utilsdoc::NvDsObjEncUsrArgsDoc::cast);
+    }
+}
+
 namespace pydeepstream::utils {

     std::unordered_map<std::string, std::shared_ptr<char>> font_name_memory;

2.recompile and reinstall pyds

cd /deepstream_python_apps/bindings/build
make -j 
# If you use jetson, please replace it with the correct `whl` name
pip3 install --force-reinstall ./pyds-1.1.11-py3-none-linux_x86_64.whl

3.How to use the bindings? Here we use deepstream-test1.py as an example.
You can also compare the native sample code, the code is located at
/opt/nvidia/deepstream/deepstream/sources/apps/sample_apps/deepstream-image-meta-test

diff --git a/apps/deepstream-test1/deepstream_test_1.py b/apps/deepstream-test1/deepstream_test_1.py
index 1367fb4..1d92204 100755
--- a/apps/deepstream-test1/deepstream_test_1.py
+++ b/apps/deepstream-test1/deepstream_test_1.py
@@ -34,9 +34,75 @@ PGIE_CLASS_ID_PERSON = 2
 PGIE_CLASS_ID_ROADSIGN = 3
 MUXER_BATCH_TIMEOUT_USEC = 33000

+def pgie_src_pad_buffer_probe(pad, info, u_data):
+    gst_buffer = info.get_buffer()
+    if not gst_buffer:
+        print("Unable to get GstBuffer ")
+        return
+
+    obj_ctx_handle = u_data
+    # Retrieve batch metadata from the gst_buffer
+    # Note that pyds.gst_buffer_get_nvds_batch_meta() expects the
+    # C address of gst_buffer as input, which is obtained with hash(gst_buffer)
+    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:
+            # Note that l_frame.data needs a cast to pyds.NvDsFrameMeta
+            # The casting is done by pyds.NvDsFrameMeta.cast()
+            # The casting also keeps ownership of the underlying memory
+            # in the C code, so the Python garbage collector will leave
+            # it alone.
+            frame_meta = pyds.NvDsFrameMeta.cast(l_frame.data)
+            frame_number=frame_meta.frame_num
+            if frame_number < 3:
+                frameData = pyds.NvDsObjEncUsrArgs()
+                frameData.isFrame = 1
+                frameData.saveImg = False
+                frameData.attachUsrMeta = True
+                frameData.scaleImg = False
+                frameData.scaledWidth = 0
+                frameData.scaledHeight = 0
+                frameData.quality = 80
+                frameData.calcEncodeTime = 1
+                ret = pyds.nvds_obj_enc_process (obj_ctx_handle, frameData, hash(gst_buffer), None, frame_meta)
+        except StopIteration:
+            break
+        l_obj=frame_meta.obj_meta_list
+        num_rects = 0
+        while l_obj is not None:
+            try:
+                # Casting l_obj.data to pyds.NvDsObjectMeta
+                # For demonstration, we will encode the first object in the frame
+                obj_meta=pyds.NvDsObjectMeta.cast(l_obj.data)
+                if frame_number < 3 and obj_meta.class_id == PGIE_CLASS_ID_VEHICLE and num_rects == 0:
+                    num_rects += 1
+                    objData = pyds.NvDsObjEncUsrArgs()
+                    objData.saveImg = False
+                    objData.attachUsrMeta = True
+                    objData.scaleImg = False
+                    objData.scaledWidth = 0
+                    objData.scaledHeight = 0
+                    objData.objNum = num_rects
+                    objData.quality = 80
+                    objData.calcEncodeTime = 1
+                    ret = pyds.nvds_obj_enc_process (obj_ctx_handle, objData, hash(gst_buffer), obj_meta, frame_meta)
+            except StopIteration:
+                break
+            try:
+                l_obj=l_obj.next
+            except StopIteration:
+                break
+        try:
+            l_frame=l_frame.next
+        except StopIteration:
+            break
+        pyds.nvds_obj_enc_finish(obj_ctx_handle)
+    return Gst.PadProbeReturn.OK
+
 def osd_sink_pad_buffer_probe(pad,info,u_data):
-    frame_number=0
     num_rects=0
+    frame_number=0

     gst_buffer = info.get_buffer()
     if not gst_buffer:
@@ -56,6 +122,21 @@ def osd_sink_pad_buffer_probe(pad,info,u_data):
             # in the C code, so the Python garbage collector will leave
             # it alone.
             frame_meta = pyds.NvDsFrameMeta.cast(l_frame.data)
+            frame_number=frame_meta.frame_num
+            fusrMetaList = frame_meta.frame_user_meta_list
+            while fusrMetaList is not None:
+                try:
+                    fuser_meta = pyds.NvDsUserMeta.cast(fusrMetaList.data)
+                    if fuser_meta.base_meta.meta_type == pyds.NvDsMetaType.NVDS_CROP_IMAGE_META:
+                        fenc_output = pyds.NvDsObjEncOutParams.cast(fuser_meta.user_meta_data)
+                        foutput = fenc_output.outBuffer()
+                        foutput.tofile(f"{frame_number}-out.jpg")
+                except StopIteration:
+                    break
+                try:
+                    fusrMetaList = fusrMetaList.next
+                except StopIteration:
+                    break
         except StopIteration:
             break

@@ -66,17 +147,31 @@ def osd_sink_pad_buffer_probe(pad,info,u_data):
             PGIE_CLASS_ID_BICYCLE:0,
             PGIE_CLASS_ID_ROADSIGN:0
         }
-        frame_number=frame_meta.frame_num
         num_rects = frame_meta.num_obj_meta
         l_obj=frame_meta.obj_meta_list
         while l_obj is not None:
             try:
                 # Casting l_obj.data to pyds.NvDsObjectMeta
                 obj_meta=pyds.NvDsObjectMeta.cast(l_obj.data)
+                obj_counter[obj_meta.class_id] += 1
+                obj_meta.rect_params.border_color.set(0.0, 0.0, 1.0, 0.8) #0.8 is alpha (opacity)
+
+                usrMetaList = obj_meta.obj_user_meta_list
+                while usrMetaList is not None:
+                    try:
+                        user_meta = pyds.NvDsUserMeta.cast(usrMetaList.data)
+                        if user_meta.base_meta.meta_type == pyds.NvDsMetaType.NVDS_CROP_IMAGE_META:
+                            enc_output = pyds.NvDsObjEncOutParams.cast(user_meta.user_meta_data)
+                            output = enc_output.outBuffer()
+                            output.tofile(f"{frame_number}-{num_rects}-out.jpg")
+                    except StopIteration:
+                        break
+                    try:
+                        usrMetaList = usrMetaList.next
+                    except StopIteration:
+                        break
             except StopIteration:
                 break
-            obj_counter[obj_meta.class_id] += 1
-            obj_meta.rect_params.border_color.set(0.0, 0.0, 1.0, 0.8) #0.8 is alpha (opacity)
             try:
                 l_obj=l_obj.next
             except StopIteration:
@@ -191,7 +286,8 @@ def main(args):
             sink = Gst.ElementFactory.make("nv3dsink", "nv3d-sink")
         else:
             print("Creating EGLSink \n")
-            sink = Gst.ElementFactory.make("nveglglessink", "nvvideo-renderer")
+            sink = Gst.ElementFactory.make("fakesink", "nv3d-sink")
+            #sink = Gst.ElementFactory.make("nveglglessink", "nvvideo-renderer")
         if not sink:
             sys.stderr.write(" Unable to create egl sink \n")

@@ -222,6 +318,9 @@ def main(args):
     source.link(h264parser)
     h264parser.link(decoder)

+    # Create Context for Object Encoding.
+    # Takes GPU ID as a parameter.
+    obj_ctx_handle = pyds.nvds_obj_enc_create_context (0)
     sinkpad = streammux.request_pad_simple("sink_0")
     if not sinkpad:
         sys.stderr.write(" Unable to get the sink pad of streammux \n")
@@ -240,6 +339,11 @@ def main(args):
     bus.add_signal_watch()
     bus.connect ("message", bus_call, loop)

+    pgiesrcpad = pgie.get_static_pad("src")
+    if not pgiesrcpad:
+        sys.stderr.write(" Unable to get src pad of pgie \n")
+    pgiesrcpad.add_probe(Gst.PadProbeType.BUFFER, pgie_src_pad_buffer_probe, obj_ctx_handle)
+
     # Lets add probe to get informed of the meta data generated, we add probe to
     # the sink pad of the osd element, since by that time, the buffer would have
     # had got all the metadata.
@@ -258,6 +362,8 @@ def main(args):
         pass
     # cleanup
     pipeline.set_state(Gst.State.NULL)
+    # Destroy context for Object Encoding
+    pyds.nvds_obj_enc_destroy_context (obj_ctx_handle)

 if __name__ == '__main__':