[Python] validate tensors info
[platform/upstream/nnstreamer.git] / ext / nnstreamer / extra / nnstreamer_python3_helper.cc
1 /* SPDX-License-Identifier: LGPL-2.1-only */
2 /**
3  * @file        nnstreamer_python3_helper.cc
4  * @date        10 Apr 2019
5  * @brief       python helper structure for nnstreamer tensor_filter
6  * @see         http://github.com/nnstreamer/nnstreamer
7  * @author      Dongju Chae <dongju.chae@samsung.com>
8  * @bug         No known bugs except for NYI items
9  *
10  * A python module that provides a wrapper for internal structures in tensor_filter_python.
11  * Users can import this module to access such a functionality
12  *
13  * -- Example python script
14  *  import numpy as np
15  *  import nnstreamer_python as nns
16  *  dim = nns.TensorShape([1,2,3], np.uint8)
17  */
18
19 #if defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6))
20 #pragma GCC diagnostic ignored "-Wformat"
21 #endif
22
23 #include <nnstreamer_util.h>
24 #include "nnstreamer_python3_helper.h"
25
26 #ifdef __cplusplus
27 extern "C" {
28 #endif
29
30 /** @brief object structure for custom Python type: TensorShape */
31 typedef struct {
32   PyObject_HEAD PyObject *dims;
33   PyArray_Descr *type;
34 } TensorShapeObject;
35
36 /** @brief define a prototype for this python module */
37 PyMODINIT_FUNC PyInit_nnstreamer_python (void);
38
39 /**
40  * @brief method impl. for setDims
41  * @param self : Python type object
42  * @param args : arguments for the method
43  */
44 static PyObject *
45 TensorShape_setDims (TensorShapeObject *self, PyObject *args)
46 {
47   PyObject *dims;
48   PyObject *new_dims;
49   unsigned int i, len;
50
51   /** PyArg_ParseTuple() returns borrowed references */
52   if (!PyArg_ParseTuple (args, "O", &dims))
53     Py_RETURN_NONE;
54
55   len = PyList_Size (dims);
56   if (len < NNS_TENSOR_RANK_LIMIT) {
57     for (i = 0; i < NNS_TENSOR_RANK_LIMIT - len; i++)
58       PyList_Append (dims, PyLong_FromLong (0));
59     new_dims = dims;
60     Py_XINCREF (new_dims);
61   } else {
62     /** PyList_GetSlice() returns new reference */
63     new_dims = PyList_GetSlice (dims, 0, NNS_TENSOR_RANK_LIMIT);
64   }
65
66   /** swap 'self->dims' */
67   Py_SAFEDECREF (self->dims);
68   self->dims = new_dims;
69
70   Py_RETURN_NONE;
71 }
72
73 /**
74  * @brief method impl. for getDims
75  * @param self : Python type object
76  * @param args : arguments for the method
77  */
78 static PyObject *
79 TensorShape_getDims (TensorShapeObject *self, PyObject *args)
80 {
81   UNUSED (args);
82   return Py_BuildValue ("O", self->dims);
83 }
84
85 /**
86  * @brief method impl. for getType
87  * @param self : Python type object
88  * @param args : arguments for the method
89  */
90 static PyObject *
91 TensorShape_getType (TensorShapeObject *self, PyObject *args)
92 {
93   UNUSED (args);
94   return Py_BuildValue ("O", self->type);
95 }
96
97 /**
98  * @brief new callback for custom type object
99  * @param self : Python type object
100  * @param args : arguments for the method
101  * @param kw : keywords for the arguments
102  */
103 static PyObject *
104 TensorShape_new (PyTypeObject *type, PyObject *args, PyObject *kw)
105 {
106   TensorShapeObject *self = (TensorShapeObject *) type->tp_alloc (type, 0);
107   UNUSED (args);
108   UNUSED (kw);
109
110   g_assert (self);
111
112   /** Assign default values */
113   self->dims = PyList_New (0);
114   self->type = PyArray_DescrFromType (NPY_UINT8);
115   Py_XINCREF (self->type);
116
117   return (PyObject *) self;
118 }
119
120 /**
121  * @brief init callback for custom type object
122  * @param self : Python type object
123  * @param args : arguments for the method
124  * @param kw : keywords for the arguments
125  */
126 static int
127 TensorShape_init (TensorShapeObject *self, PyObject *args, PyObject *kw)
128 {
129   char *keywords[] = { (char *) "dims", (char *) "type", NULL };
130   PyObject *dims = NULL;
131   PyObject *type = NULL;
132
133   if (!PyArg_ParseTupleAndKeywords (args, kw, "|OO", keywords, &dims, &type))
134     return -1;
135
136   if (dims) {
137     PyObject *none = PyObject_CallMethod (
138         (PyObject *) self, (char *) "setDims", (char *) "O", dims);
139     Py_SAFEDECREF (none);
140   }
141
142   if (type) {
143     PyArray_Descr *dtype;
144     if (PyArray_DescrConverter (type, &dtype) != NPY_FAIL) {
145       /** swap 'self->type' */
146       Py_SAFEDECREF (self->type);
147       self->type = dtype;
148       Py_XINCREF (self->type);
149     } else
150       Py_ERRMSG ("Wrong data type.");
151   }
152
153   return 0;
154 }
155
156 /**
157  * @brief dealloc callback for custom type object
158  * @param self : Python type object
159  */
160 static void
161 TensorShape_dealloc (TensorShapeObject *self)
162 {
163   Py_SAFEDECREF (self->dims);
164   Py_SAFEDECREF (self->type);
165   Py_TYPE (self)->tp_free ((PyObject *) self);
166 }
167
168 /** @brief members for custom type object */
169 static PyMemberDef TensorShape_members[]
170     = { { (char *) "dims", T_OBJECT_EX, offsetof (TensorShapeObject, dims), 0, NULL },
171         { (char *) "type", T_OBJECT_EX, offsetof (TensorShapeObject, type), 0, NULL } };
172
173 /** @brief methods for custom type object */
174 static PyMethodDef TensorShape_methods[] = { { (char *) "setDims", (PyCFunction) TensorShape_setDims,
175                                                  METH_VARARGS | METH_KEYWORDS, NULL },
176   { (char *) "getDims", (PyCFunction) TensorShape_getDims, METH_VARARGS | METH_KEYWORDS, NULL },
177   { (char *) "getType", (PyCFunction) TensorShape_getType, METH_VARARGS | METH_KEYWORDS, NULL },
178   { NULL, NULL, 0, NULL } };
179
180 /** @brief Structure for custom type object */
181 static PyTypeObject TensorShapeType = [] {
182 #pragma GCC diagnostic push
183 #pragma GCC diagnostic ignored "-Wmissing-field-initializers"
184   PyTypeObject ret = { PyVarObject_HEAD_INIT (NULL, 0) };
185 #pragma GCC diagnostic pop
186   ret.tp_name = "nnstreamer_python.TensorShape";
187   ret.tp_basicsize = sizeof (TensorShapeObject);
188   ret.tp_itemsize = 0;
189   ret.tp_dealloc = (destructor) TensorShape_dealloc;
190   ret.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE;
191   ret.tp_doc = "TensorShape type";
192   ret.tp_methods = TensorShape_methods;
193   ret.tp_members = TensorShape_members;
194   ret.tp_init = (initproc) TensorShape_init;
195   ret.tp_new = TensorShape_new;
196   return ret;
197 }();
198
199 #pragma GCC diagnostic push
200 #pragma GCC diagnostic ignored "-Wmissing-field-initializers"
201 static PyModuleDef nnstreamer_python_module
202     = { PyModuleDef_HEAD_INIT, "nnstreamer_python", NULL, -1, NULL };
203 #pragma GCC diagnostic pop
204
205 /** @brief module initialization (python 3.x) */
206 PyMODINIT_FUNC
207 PyInit_nnstreamer_python (void)
208 {
209   PyObject *type_object = (PyObject *) &TensorShapeType;
210   PyObject *module;
211
212   /** Check TensorShape type */
213   if (PyType_Ready (&TensorShapeType) < 0)
214     return NULL;
215
216   module = PyModule_Create (&nnstreamer_python_module);
217   if (module == NULL)
218     return NULL;
219
220   /** For numpy array init. */
221   import_array ();
222
223   Py_INCREF (type_object);
224   PyModule_AddObject (module, "TensorShape", type_object);
225
226   return module;
227 }
228
229 /**
230  * @brief       return the data type of the tensor
231  * @param npyType       : the defined type of Python numpy
232  * @return the enum of defined _NNS_TYPE
233  */
234 tensor_type
235 getTensorType (NPY_TYPES npyType)
236 {
237   switch (npyType) {
238     case NPY_INT32:
239       return _NNS_INT32;
240     case NPY_UINT32:
241       return _NNS_UINT32;
242     case NPY_INT16:
243       return _NNS_INT16;
244     case NPY_UINT16:
245       return _NNS_UINT16;
246     case NPY_INT8:
247       return _NNS_INT8;
248     case NPY_UINT8:
249       return _NNS_UINT8;
250     case NPY_INT64:
251       return _NNS_INT64;
252     case NPY_UINT64:
253       return _NNS_UINT64;
254     case NPY_FLOAT32:
255       return _NNS_FLOAT32;
256     case NPY_FLOAT64:
257       return _NNS_FLOAT64;
258     default:
259       /** @todo Support other types */
260       break;
261   }
262
263   return _NNS_END;
264 }
265
266 /**
267  * @brief       return the data type of the tensor for Python numpy
268  * @param tType : the defined type of NNStreamer
269  * @return the enum of defined numpy datatypes
270  */
271 NPY_TYPES
272 getNumpyType (tensor_type tType)
273 {
274   switch (tType) {
275     case _NNS_INT32:
276       return NPY_INT32;
277     case _NNS_UINT32:
278       return NPY_UINT32;
279     case _NNS_INT16:
280       return NPY_INT16;
281     case _NNS_UINT16:
282       return NPY_UINT16;
283     case _NNS_INT8:
284       return NPY_INT8;
285     case _NNS_UINT8:
286       return NPY_UINT8;
287     case _NNS_INT64:
288       return NPY_INT64;
289     case _NNS_UINT64:
290       return NPY_UINT64;
291     case _NNS_FLOAT32:
292       return NPY_FLOAT32;
293     case _NNS_FLOAT64:
294       return NPY_FLOAT64;
295     default:
296       /** @todo Support other types */
297       break;
298   }
299   return NPY_NOTYPE;
300 }
301
302 /**
303  * @brief       load the python script
304  */
305 int
306 loadScript (PyObject **core_obj, const gchar *module_name, const gchar *class_name)
307 {
308   PyObject *module = PyImport_ImportModule (module_name);
309
310   if (module) {
311     PyObject *cls = PyObject_GetAttrString (module, class_name);
312     Py_SAFEDECREF (module);
313
314     if (cls) {
315       *core_obj = PyObject_CallObject (cls, NULL);
316       Py_SAFEDECREF (cls);
317     } else {
318       Py_ERRMSG ("Cannot find '%s' class in the script.\n", class_name);
319       return -2;
320     }
321   } else {
322     Py_ERRMSG ("The script (%s) is not properly loaded.\n", module_name);
323     return -1;
324   }
325
326   return 0;
327 }
328
329 /**
330  * @brief       loads the dynamic shared object of the python
331  */
332 int
333 openPythonLib (void **handle)
334 {
335   /**
336    * To fix import error of python extension modules
337    * (e.g., multiarray.x86_64-linux-gnu.so: undefined symbol: PyExc_SystemError)
338    */
339   gchar libname[32] = {
340     0,
341   };
342
343   g_snprintf (libname, sizeof (libname), "libpython%d.%d.%s", PY_MAJOR_VERSION,
344       PY_MINOR_VERSION, SO_EXT);
345   *handle = dlopen (libname, RTLD_LAZY | RTLD_GLOBAL);
346   if (NULL == *handle) {
347     /* check the python was compiled with '--with-pymalloc' */
348     g_snprintf (libname, sizeof (libname), "libpython%d.%dm.%s",
349         PY_MAJOR_VERSION, PY_MINOR_VERSION, SO_EXT);
350
351     *handle = dlopen (libname, RTLD_LAZY | RTLD_GLOBAL);
352     if (NULL == *handle)
353       return -1;
354   }
355
356   return 0;
357 }
358
359 /**
360  * @brief       Add custom python module to system path
361  */
362 int
363 addToSysPath (const gchar *path)
364 {
365   /** Add current/directory path to sys.path */
366   PyObject *sys_module = PyImport_ImportModule ("sys");
367   if (nullptr == sys_module) {
368     Py_ERRMSG ("Cannot import python module 'sys'.");
369     return -1;
370   }
371
372   PyObject *sys_path = PyObject_GetAttrString (sys_module, "path");
373   if (nullptr == sys_path) {
374     Py_ERRMSG ("Cannot import python module 'path'.");
375     Py_SAFEDECREF (sys_module);
376     return -1;
377   }
378
379   PyList_Append (sys_path, PyUnicode_FromString ("."));
380   PyList_Append (sys_path, PyUnicode_FromString (path));
381
382   Py_SAFEDECREF (sys_path);
383   Py_SAFEDECREF (sys_module);
384
385   return 0;
386 }
387
388 /**
389  * @brief parse the converting result to feed output tensors
390  * @param[result] Python object retunred by convert
391  * @param[info] info Structure for output tensors info
392  * @return 0 if no error, otherwise negative errno
393  */
394 int
395 parseTensorsInfo (PyObject *result, GstTensorsInfo *info)
396 {
397   guint i, j, rank;
398
399   if (PyList_Size (result) < 0)
400     return -1;
401
402   gst_tensors_info_init (info);
403   info->num_tensors = PyList_Size (result);
404   for (i = 0; i < info->num_tensors; i++) {
405     /** don't own the reference */
406     PyObject *tensor_shape = PyList_GetItem (result, (Py_ssize_t) i);
407     if (nullptr == tensor_shape) {
408       Py_ERRMSG ("The function %s has failed, cannot get TensorShape object.", __FUNCTION__);
409       return -1;
410     }
411
412     PyObject *shape_type = PyObject_CallMethod (tensor_shape, (char *) "getType", NULL);
413     if (nullptr == shape_type) {
414       Py_ERRMSG ("The function %s has failed, cannot get the tensor type.", __FUNCTION__);
415       return -1;
416     }
417
418     /** convert numpy type to tensor type */
419     info->info[i].type
420         = getTensorType ((NPY_TYPES) (((PyArray_Descr *) shape_type)->type_num));
421     Py_SAFEDECREF (shape_type);
422
423     PyObject *shape_dims = PyObject_CallMethod (tensor_shape, (char *) "getDims", NULL);
424     if (nullptr == shape_dims) {
425       Py_ERRMSG ("The function %s has failed, cannot get the tensor dimension.", __FUNCTION__);
426       return -1;
427     }
428
429     if (!PyList_CheckExact (shape_dims)) {
430       Py_ERRMSG ("The function %s has failed, dimension should be a list.", __FUNCTION__);
431       Py_SAFEDECREF (shape_dims);
432       return -EINVAL;
433     }
434
435     rank = (guint) PyList_Size (shape_dims);
436     if (rank > NNS_TENSOR_RANK_LIMIT) {
437       Py_ERRMSG ("The function %s has failed, max rank of tensor dimension is %d.",
438           __FUNCTION__, NNS_TENSOR_RANK_LIMIT);
439       Py_SAFEDECREF (shape_dims);
440       return -EINVAL;
441     }
442
443     for (j = 0; j < rank; j++) {
444       PyErr_Clear ();
445       PyObject *item = PyList_GetItem (shape_dims, (Py_ssize_t) j);
446       int val = -1;
447
448       if (PyErr_Occurred ()) {
449         PyErr_Print ();
450         PyErr_Clear ();
451         info->info[i].dimension[j] = 0;
452         Py_ERRMSG ("Python nnstreamer plugin has returned dimensions of the %u'th tensor not in an array. Python code should return int-type array for dimensions. Indexes are counted from 0.\n",
453             i + 1);
454         Py_SAFEDECREF (shape_dims);
455         return -EINVAL;
456       }
457
458       if (PyLong_Check (item)) {
459         val = (int) PyLong_AsLong (item);
460       } else if (PyFloat_Check (item)) {
461         /** Regard this as a warning. Don't return -EINVAL with this */
462         val = (int) PyFloat_AsDouble (item);
463         Py_ERRMSG ("Python nnstreamer plugin has returned the %u'th dimension value of the %u'th tensor in floating-point type (%f), which is casted as unsigned-int. Python code should return int-type for dimension values. Indexes are counted from 0.\n",
464             j + 1, i + 1, PyFloat_AsDouble (item));
465       } else {
466         info->info[i].dimension[j] = 0;
467         Py_ERRMSG ("Python nnstreamer plugin has returned the %u'th dimension value of the %u'th tensor neither in integer or floating-pointer. Python code should return int-type for dimension values. Indexes are counted from 0.\n",
468             j + 1, i + 1);
469         Py_SAFEDECREF (shape_dims);
470         return -EINVAL;
471       }
472
473       if (val < 0) {
474         Py_ERRMSG ("The %u'th dimension value of the %u'th tensor is invalid (%d).",
475             j + 1, i + 1, val);
476         Py_SAFEDECREF (shape_dims);
477         return -EINVAL;
478       }
479
480       info->info[i].dimension[j] = (uint32_t) val;
481     }
482
483     info->info[i].name = NULL;
484     Py_SAFEDECREF (shape_dims);
485   }
486
487   /* Validate output tensors info after parsing python object. */
488   if (!gst_tensors_info_validate (info)) {
489     gchar *info_str = gst_tensors_info_to_string (info);
490     Py_ERRMSG ("Failed to parse the tensors information from python script, it may include invalid tensor type or dimension value (%s).",
491         info_str);
492     g_free (info_str);
493     return -EINVAL;
494   }
495
496   return 0;
497 }
498
499 /**
500  * @brief       allocate TensorShape object
501  * @param info : the tensor info
502  * @return created object
503  */
504 PyObject *
505 PyTensorShape_New (PyObject *shape_cls, const GstTensorInfo *info)
506 {
507   _import_array (); /** for numpy */
508
509   PyObject *args = PyTuple_New (2);
510   PyObject *dims = PyList_New (NNS_TENSOR_RANK_LIMIT);
511   PyObject *type = (PyObject *) PyArray_DescrFromType (getNumpyType (info->type));
512
513   if (nullptr == args || nullptr == dims || nullptr == type) {
514     Py_ERRMSG ("The function %s has failed, cannot create args.", __FUNCTION__);
515     PyErr_Clear ();
516     Py_SAFEDECREF (args);
517     Py_SAFEDECREF (dims);
518     Py_SAFEDECREF (type);
519     return nullptr;
520   }
521
522   for (int i = 0; i < NNS_TENSOR_RANK_LIMIT; i++)
523     PyList_SetItem (dims, i, PyLong_FromLong ((uint64_t) info->dimension[i]));
524
525   PyTuple_SetItem (args, 0, dims);
526   PyTuple_SetItem (args, 1, type);
527
528   return PyObject_CallObject (shape_cls, args);
529   /* Its value is checked by setInputTensorDim */
530 }
531
532 #ifdef __cplusplus
533 } /* extern "C" */
534 #endif