2 * GStreamer Tensor_Filter, Python Module
3 * Copyright (C) 2019 Samsung Electronics Co., Ltd. All rights reserved.
4 * Copyright (C) 2019 Dongju Chae <dongju.chae@samsung.com>
6 * This library is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Library General Public
8 * License as published by the Free Software Foundation;
9 * version 2.1 of the License.
11 * This library is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Library General Public License for more details.
18 * @file tensor_filter_python3.cc
20 * @brief Python3 module for tensor_filter gstreamer plugin
21 * @see http://github.com/nnstreamer/nnstreamer
22 * @author Dongju Chae <dongju.chae@samsung.com>
23 * @bug No known bugs except for NYI items
24 * @todo This would be better if this inherits tensor_filter_subplugin class.
28 * SECTION:element-tensor_filter_python3
30 * A filter that loads and executes a python3 script implementing a custom
32 * The python3 script should be provided.
35 * <title>Example launch line</title>
37 * gst-launch-1.0 videotestsrc ! video/x-raw,format=RGB,width=640,height=480 !
38 * tensor_converter ! tensor_filter framework="python3"
39 * model="${PATH_TO_SCRIPT}" ! tensor_sink
44 #if defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6))
45 #pragma GCC diagnostic ignored "-Wformat"
48 #define NO_ANONYMOUS_NESTED_STRUCT
49 #include <nnstreamer_plugin_api_filter.h>
50 #undef NO_ANONYMOUS_NESTED_STRUCT
52 #include <nnstreamer_conf.h>
53 #include <nnstreamer_util.h>
54 #include "nnstreamer_python3_helper.h"
57 * @brief Macro for debug mode.
63 static const gchar *python_accl_support[] = { NULL };
65 /** @brief Callback type for custom filter */
66 typedef enum _cb_type {
73 #define Py_LOCK() PyGILState_Ensure ()
74 #define Py_UNLOCK(gstate) PyGILState_Release (gstate)
77 * @brief Python embedding core structure
85 PYCore (const char *_script_path, const char *_custom);
88 int init (const GstTensorFilterProperties *prop);
90 const char *getScriptPath ();
91 int getInputTensorDim (GstTensorsInfo *info);
92 int getOutputTensorDim (GstTensorsInfo *info);
93 int setInputTensorDim (const GstTensorsInfo *in_info, GstTensorsInfo *out_info);
94 int run (const GstTensorMemory *input, GstTensorMemory *output);
96 void freeOutputTensors (void *data);
98 /** @brief Return callback type */
101 return callback_type;
104 int checkTensorType (int nns_type, int np_type);
105 int checkTensorSize (GstTensorMemory *output, PyArrayObject *array);
108 const std::string script_path; /**< from model_path property */
109 const std::string module_args; /**< from custom property */
111 std::string module_name;
112 std::map<void *, PyArrayObject *> outputArrayMap;
114 cb_type callback_type;
119 GstTensorsInfo inputTensorMeta; /**< The tensor info of input tensors */
120 GstTensorsInfo outputTensorMeta; /**< The tensor info of output tensors */
122 bool configured; /**< True if the script is successfully loaded */
123 void *handle; /**< returned handle by dlopen() */
128 #endif /* __cplusplus */
129 void init_filter_py (void) __attribute__ ((constructor));
130 void fini_filter_py (void) __attribute__ ((destructor));
133 #endif /* __cplusplus */
136 * @brief PYCore creator
137 * @param _script_path : the logical path to '{script_name}.py' file
138 * @note the script of _script_path will be loaded simultaneously
141 PYCore::PYCore (const char *_script_path, const char *_custom)
142 : script_path (_script_path), module_args (_custom != NULL ? _custom : "")
144 if (openPythonLib (&handle))
145 throw std::runtime_error (dlerror ());
147 _import_array (); /** for numpy */
150 * Parse script path to get module name
151 * The module name should drop its extension (i.e., .py)
153 module_name = script_path;
155 const size_t last_idx = module_name.find_last_of ("/\\");
156 if (last_idx != std::string::npos)
157 module_name.erase (0, last_idx + 1);
159 const size_t ext_idx = module_name.rfind ('.');
160 if (ext_idx != std::string::npos)
161 module_name.erase (ext_idx);
163 addToSysPath (script_path.substr (0, last_idx).c_str ());
165 gst_tensors_info_init (&inputTensorMeta);
166 gst_tensors_info_init (&outputTensorMeta);
168 callback_type = CB_END;
175 * @brief PYCore Destructor
180 gst_tensors_info_free (&inputTensorMeta);
181 gst_tensors_info_free (&outputTensorMeta);
183 PyGILState_STATE gstate = Py_LOCK ();
184 Py_SAFEDECREF (core_obj);
185 Py_SAFEDECREF (shape_cls);
194 * @brief initialize the object with python script
195 * @return 0 if OK. non-zero if error.
198 PYCore::init (const GstTensorFilterProperties *prop)
200 PyGILState_STATE gstate = Py_LOCK ();
202 /** Find nnstreamer_api module */
203 PyObject *api_module = PyImport_ImportModule ("nnstreamer_python");
204 if (api_module == NULL) {
205 Py_ERRMSG ("Cannt find `nnstreamer_python` module");
209 shape_cls = PyObject_GetAttrString (api_module, "TensorShape");
210 Py_SAFEDECREF (api_module);
212 if (shape_cls == NULL) {
213 Py_ERRMSG ("Failed to get `TensorShape` from `nnstreamer_python` module");
217 gst_tensors_info_copy (&inputTensorMeta, &prop->input_meta);
218 gst_tensors_info_copy (&outputTensorMeta, &prop->output_meta);
227 * @brief get the script path
228 * @return the script path.
231 PYCore::getScriptPath ()
233 return script_path.c_str ();
237 * @brief load the py script
238 * @note the script will be loaded
239 * @return 0 if OK. non-zero if error.
240 * -1 if the py file is not loaded.
241 * -2 if the target class is not found.
242 * -3 if the class is not created.
245 PYCore::loadScript ()
248 gint64 start_time = g_get_real_time ();
252 PyGILState_STATE gstate = Py_LOCK ();
254 PyObject *module = PyImport_ImportModule (module_name.c_str ());
256 PyObject *cls = PyObject_GetAttrString (module, "CustomFilter");
259 if (!module_args.empty ()) {
260 gchar **g_args = g_strsplit (module_args.c_str (), " ", 0);
261 char **args = g_args;
263 while (*(args++) != NULL)
268 ml_loge ("Cannot load python script for python-custom-filter.\n");
272 py_args = PyTuple_New (argc);
274 for (int i = 0; i < argc; i++)
275 PyTuple_SetItem (py_args, i, PyUnicode_FromString (g_args[i]));
277 core_obj = PyObject_CallObject (cls, py_args);
279 Py_SAFEDECREF (py_args);
282 core_obj = PyObject_CallObject (cls, NULL);
285 /** check whther either setInputDim or getInputDim/getOutputDim are
287 if (PyObject_HasAttrString (core_obj, (char *) "setInputDim"))
288 callback_type = CB_SETDIM;
289 else if (PyObject_HasAttrString (core_obj, (char *) "getInputDim")
290 && PyObject_HasAttrString (core_obj, (char *) "getOutputDim"))
291 callback_type = CB_GETDIM;
293 callback_type = CB_END;
295 Py_ERRMSG ("Fail to create an instance 'CustomFilter'\n");
302 Py_ERRMSG ("Cannot find 'CustomFilter' class in the script\n");
307 Py_SAFEDECREF (module);
309 Py_ERRMSG ("the script is not properly loaded\n");
317 gint64 stop_time = g_get_real_time ();
318 g_message ("Script is loaded: %" G_GINT64_FORMAT, (stop_time - start_time));
328 * @brief check the data type of tensors in array
329 * @param nns_type : tensor type for output tensor
330 * @param np_type : python array type for output tensor
331 * @return a boolean value for whether the types are matched
334 PYCore::checkTensorType (int nns_type, int np_type)
338 return np_type == NPY_INT64;
340 return np_type == NPY_UINT64;
342 return np_type == NPY_INT32;
344 return np_type == NPY_UINT32;
346 return np_type == NPY_INT16;
348 return np_type == NPY_UINT16;
350 return np_type == NPY_INT8;
352 return np_type == NPY_UINT8;
354 return np_type == NPY_FLOAT64;
356 return np_type == NPY_FLOAT32;
363 * @brief check the data size of tensors in array
364 * @param output : tensor memory for output tensors
365 * @param array : python array
366 * @return a boolean value for whether the sizes are matched
369 PYCore::checkTensorSize (GstTensorMemory *output, PyArrayObject *array)
371 if (nullptr == output || nullptr == array)
372 throw std::invalid_argument ("Null pointers are given to PYCore::checkTensorSize().\n");
374 PyGILState_STATE gstate = Py_LOCK ();
375 size_t total_size = PyArray_ITEMSIZE (array);
377 for (int i = 0; i < PyArray_NDIM (array); i++)
378 total_size *= PyArray_DIM (array, i);
382 return (output->size == total_size);
386 * @brief return the Dimension of Input Tensor.
387 * @param[out] info Structure for tensor info.
388 * @todo return whole array rather than index 0
389 * @return 0 if OK. non-zero if error.
392 PYCore::getInputTensorDim (GstTensorsInfo *info)
397 throw std::invalid_argument ("A null pointer is given to PYCore::getInputTensorDim().\n");
399 PyGILState_STATE gstate = Py_LOCK ();
401 PyObject *result = PyObject_CallMethod (core_obj, (char *) "getInputDim", NULL);
403 res = parseTensorsInfo (result, info);
404 Py_SAFEDECREF (result);
406 Py_ERRMSG ("Fail to call 'getInputDim'");
416 * @brief return the Dimension of Tensor.
417 * @param[out] info Structure for tensor info.
418 * @todo return whole array rather than index 0
419 * @return 0 if OK. non-zero if error.
422 PYCore::getOutputTensorDim (GstTensorsInfo *info)
427 throw std::invalid_argument ("A null pointer is given to PYCore::getOutputTensorDim().\n");
429 PyGILState_STATE gstate = Py_LOCK ();
431 PyObject *result = PyObject_CallMethod (core_obj, (char *) "getOutputDim", NULL);
433 res = parseTensorsInfo (result, info);
434 Py_SAFEDECREF (result);
436 Py_ERRMSG ("Fail to call 'getOutputDim'");
446 * @brief set the Dimension of Input Tensor and return the Dimension of Input Tensor.
447 * @param[in] info Structure for input tensor info.
448 * @param[out] info Structure for output tensor info.
449 * @todo return whole array rather than index 0
450 * @return 0 if OK. non-zero if error.
453 PYCore::setInputTensorDim (const GstTensorsInfo *in_info, GstTensorsInfo *out_info)
455 GstTensorInfo *_info;
458 if (nullptr == in_info || nullptr == out_info)
459 throw std::invalid_argument ("Null pointers are given to PYCore::setInputTensorDim().\n");
461 PyGILState_STATE gstate = Py_LOCK ();
463 /** to Python list object */
464 PyObject *param = PyList_New (in_info->num_tensors);
465 if (nullptr == param)
466 throw std::runtime_error ("PyList_New(); has failed.");
468 for (unsigned int i = 0; i < in_info->num_tensors; i++) {
471 _info = gst_tensors_info_get_nth_info ((GstTensorsInfo *) in_info, i);
472 shape = PyTensorShape_New (shape_cls, _info);
473 if (nullptr == shape)
474 throw std::runtime_error ("PyTensorShape_New(); has failed.");
476 PyList_SetItem (param, i, shape);
480 = PyObject_CallMethod (core_obj, (char *) "setInputDim", (char *) "(O)", param);
482 Py_SAFEDECREF (param);
485 gst_tensors_info_copy (&inputTensorMeta, in_info);
486 res = parseTensorsInfo (result, out_info);
488 gst_tensors_info_copy (&outputTensorMeta, out_info);
489 Py_SAFEDECREF (result);
491 Py_ERRMSG ("Fail to call 'setInputDim'");
501 * @brief free output tensor corresponding to the given data
502 * @param[data] The data element
505 PYCore::freeOutputTensors (void *data)
507 std::map<void *, PyArrayObject *>::iterator it;
508 PyGILState_STATE gstate = Py_LOCK ();
510 it = outputArrayMap.find (data);
511 if (it != outputArrayMap.end ()) {
512 Py_SAFEDECREF (it->second);
513 outputArrayMap.erase (it);
515 ml_loge ("Cannot find output data: 0x%lx", (unsigned long) data);
522 * @brief run the script with the input.
523 * @param[in] input : The array of input tensors
524 * @param[out] output : The array of output tensors
525 * @return 0 if OK. non-zero if error.
526 * -1 if the script does not work properly.
527 * -2 if the output properties are different with script.
530 PYCore::run (const GstTensorMemory *input, GstTensorMemory *output)
532 GstTensorInfo *_info;
537 gint64 start_time = g_get_real_time ();
540 if (nullptr == output || nullptr == input)
541 throw std::invalid_argument ("Null pointers are given to PYCore::run().\n");
543 PyGILState_STATE gstate = Py_LOCK ();
545 PyObject *param = PyList_New (inputTensorMeta.num_tensors);
546 for (unsigned int i = 0; i < inputTensorMeta.num_tensors; i++) {
547 _info = gst_tensors_info_get_nth_info (&inputTensorMeta, i);
549 /** create a Numpy array wrapper (1-D) for NNS tensor data */
550 tensor_type nns_type = _info->type;
551 npy_intp input_dims[]
552 = { (npy_intp) (input[i].size / gst_tensor_get_element_size (nns_type)) };
553 PyObject *input_array = PyArray_SimpleNewFromData (
554 1, input_dims, getNumpyType (nns_type), input[i].data);
555 PyList_SetItem (param, i, input_array);
559 result = PyObject_CallMethod (core_obj, (char *) "invoke", (char *) "(O)", param);
562 if ((unsigned int) PyList_Size (result) != outputTensorMeta.num_tensors) {
564 ml_logf ("The Python allocated size mismatched. Cannot proceed.\n");
565 Py_SAFEDECREF (result);
569 for (unsigned int i = 0; i < outputTensorMeta.num_tensors; i++) {
570 PyArrayObject *output_array
571 = (PyArrayObject *) PyList_GetItem (result, (Py_ssize_t) i);
573 _info = gst_tensors_info_get_nth_info (&outputTensorMeta, i);
575 /** type/size checking */
576 if (checkTensorType (_info->type, PyArray_TYPE (output_array))
577 && checkTensorSize (&output[i], output_array)) {
578 /** obtain the pointer to the buffer for the output array */
579 output[i].data = PyArray_DATA (output_array);
580 Py_XINCREF (output_array);
581 outputArrayMap.insert (std::make_pair (output[i].data, output_array));
583 ml_loge ("Output tensor type/size is not matched\n");
589 Py_SAFEDECREF (result);
591 Py_ERRMSG ("Fail to call 'invoke'");
596 Py_SAFEDECREF (param);
600 gint64 stop_time = g_get_real_time ();
601 g_message ("Invoke() is finished: %" G_GINT64_FORMAT, (stop_time - start_time));
608 * @brief The mandatory callback for GstTensorFilterFramework
609 * @param prop: property of tensor_filter instance
610 * @param private_data : python plugin's private data
611 * @param input : The array of input tensors
612 * @param output : The array of output tensors
615 py_run (const GstTensorFilterProperties *prop, void **private_data,
616 const GstTensorMemory *input, GstTensorMemory *output)
618 PYCore *core = static_cast<PYCore *> (*private_data);
621 g_return_val_if_fail (core, -EINVAL);
622 g_return_val_if_fail (input, -EINVAL);
623 g_return_val_if_fail (output, -EINVAL);
625 return core->run (input, output);
629 * @brief The optional callback for GstTensorFilterFramework
630 * @param data : The data element.
631 * @param private_data : python plugin's private data
634 py_destroyNotify (void **private_data, void *data)
636 PYCore *core = static_cast<PYCore *> (*private_data);
639 core->freeOutputTensors (data);
644 * @brief The optional callback for GstTensorFilterFramework
645 * @param[in] prop read-only property values
646 * @param[in/out] private_data python plugin's private data
647 * @param[in] in_info structure of input tensor info
648 * @param[out] out_info structure of output tensor info
651 py_setInputDim (const GstTensorFilterProperties *prop, void **private_data,
652 const GstTensorsInfo *in_info, GstTensorsInfo *out_info)
654 PYCore *core = static_cast<PYCore *> (*private_data);
656 g_return_val_if_fail (core && in_info && out_info, -EINVAL);
658 if (core->getCbType () != CB_SETDIM) {
662 return core->setInputTensorDim (in_info, out_info);
666 * @brief The optional callback for GstTensorFilterFramework
667 * @param prop: property of tensor_filter instance
668 * @param private_data : python plugin's private data
669 * @param[out] info The dimesions and types of input tensors
672 py_getInputDim (const GstTensorFilterProperties *prop, void **private_data, GstTensorsInfo *info)
674 PYCore *core = static_cast<PYCore *> (*private_data);
676 g_return_val_if_fail (core && info, -EINVAL);
678 if (core->getCbType () != CB_GETDIM) {
682 return core->getInputTensorDim (info);
686 * @brief The optional callback for GstTensorFilterFramework
687 * @param prop: property of tensor_filter instance
688 * @param private_data : python plugin's private data
689 * @param[out] info The dimesions and types of output tensors
692 py_getOutputDim (const GstTensorFilterProperties *prop, void **private_data,
693 GstTensorsInfo *info)
695 PYCore *core = static_cast<PYCore *> (*private_data);
697 g_return_val_if_fail (core && info, -EINVAL);
699 if (core->getCbType () != CB_GETDIM) {
703 return core->getOutputTensorDim (info);
707 * @brief Free privateData and move on.
710 py_close (const GstTensorFilterProperties *prop, void **private_data)
712 PYCore *core = static_cast<PYCore *> (*private_data);
715 g_return_if_fail (core != NULL);
718 *private_data = NULL;
722 * @brief Load python model
723 * @param prop: property of tensor_filter instance
724 * @param private_data : python plugin's private data
725 * @return 0 if successfully loaded. 1 if skipped (already loaded).
726 * -1 if the object construction is failed.
727 * -2 if the object initialization if failed
730 py_loadScriptFile (const GstTensorFilterProperties *prop, void **private_data)
733 const gchar *script_path;
735 if (prop->num_models != 1)
739 * prop->model_files[0] contains the path of a python script
740 * prop->custom contains its arguments seperated by ' '
742 core = static_cast<PYCore *> (*private_data);
743 script_path = prop->model_files[0];
746 if (g_strcmp0 (script_path, core->getScriptPath ()) == 0)
747 return 1; /* skipped */
749 py_close (prop, private_data);
753 *private_data = NULL;
755 PyGILState_STATE gstate = PyGILState_Ensure ();
756 core = new PYCore (script_path, prop->custom_properties);
758 g_printerr ("Failed to allocate memory for filter subplugin: Python\n");
759 PyGILState_Release (gstate);
763 if (core->init (prop) != 0) {
765 g_printerr ("failed to initailize the object: Python\n");
766 PyGILState_Release (gstate);
770 /** check methods in python script */
771 if (core->getCbType () != CB_SETDIM && core->getCbType () != CB_GETDIM) {
773 g_printerr ("Wrong callback type\n");
774 PyGILState_Release (gstate);
777 PyGILState_Release (gstate);
779 *private_data = core;
785 * @brief The open callback for GstTensorFilterFramework. Called before anything else
786 * @param prop: property of tensor_filter instance
787 * @param private_data : python plugin's private data
790 py_open (const GstTensorFilterProperties *prop, void **private_data)
792 if (!Py_IsInitialized ())
793 throw std::runtime_error ("Python is not initialize.");
794 int status = py_loadScriptFile (prop, private_data);
800 * @brief Check support of the backend
801 * @param hw: backend to check support of
804 py_checkAvailability (accl_hw hw)
806 if (g_strv_contains (python_accl_support, get_accl_hw_str (hw)))
812 static gchar filter_subplugin_python[] = "python3";
814 static GstTensorFilterFramework NNS_support_python = { .version = GST_TENSOR_FILTER_FRAMEWORK_V0,
818 .name = filter_subplugin_python,
819 .allow_in_place = FALSE, /** @todo: support this to optimize performance later. */
820 .allocate_in_invoke = TRUE,
821 .run_without_model = FALSE,
822 .verify_model_path = TRUE,
823 .statistics = nullptr,
825 /** dimension-related callbacks are dynamically updated */
826 .getInputDimension = py_getInputDim,
827 .getOutputDimension = py_getOutputDim,
828 .setInputDimension = py_setInputDim,
829 .destroyNotify = py_destroyNotify,
830 .reloadModel = nullptr,
831 .handleEvent = nullptr,
832 .checkAvailability = py_checkAvailability,
833 .allocateInInvoke = nullptr,
836 static PyThreadState *st;
837 /** @brief Initialize this object for tensor_filter subplugin runtime register */
839 init_filter_py (void)
841 /** Python should be initialized and finalized only once */
842 if (!Py_IsInitialized ())
844 PyEval_InitThreads_IfGood ();
845 st = PyEval_SaveThread ();
847 nnstreamer_filter_probe (&NNS_support_python);
848 nnstreamer_filter_set_custom_property_desc (filter_subplugin_python, "${GENERAL_STRING}",
849 "There is no key-value pair defined by python3 subplugin. "
850 "Provide arguments for the given python3 script.",
854 /** @brief Destruct the subplugin */
856 fini_filter_py (void)
858 PyEval_RestoreThread (st);
859 nnstreamer_filter_exit (NNS_support_python.v0.name);
862 * @todo Remove below lines after this issue is addressed.
863 * Tizen issues: After python version has been upgraded from 3.9.1 to 3.9.10,
864 * python converter is stopped at Py_Finalize. Since Py_Initialize is not called
865 * twice from this object, Py_Finalize is temporarily removed.
866 * We do not know if it's safe to call this at this point.
867 * We can finalize when ALL python subplugins share the same ref counter.
870 /** Python should be initialized and finalized only once */
871 if (Py_IsInitialized ())