diff --git a/src/python/k4a/setup.py b/src/python/k4a/setup.py index bd3343f9b..e82bc59f9 100644 --- a/src/python/k4a/setup.py +++ b/src/python/k4a/setup.py @@ -2,7 +2,7 @@ setup( name='k4a', - version='0.0.2', + version='0.0.3', author='Jonathan Santos', author_email='jonsanto@microsoft.com', description='Python interface to Azure Kinect API.', diff --git a/src/python/k4a/src/k4a/_bindings/image.py b/src/python/k4a/src/k4a/_bindings/image.py index d066b325d..71cbe95e8 100644 --- a/src/python/k4a/src/k4a/_bindings/image.py +++ b/src/python/k4a/src/k4a/_bindings/image.py @@ -13,11 +13,11 @@ import numpy as _np import copy as _copy -from .k4atypes import _ImageHandle, EStatus, EImageFormat +from .k4atypes import _ImageHandle, EStatus, EImageFormat, _memory_destroy_cb from .k4a import k4a_image_create, k4a_image_create_from_buffer, \ k4a_image_release, k4a_image_get_buffer, \ - k4a_image_reference, k4a_image_release, k4a_image_get_format, \ + k4a_image_reference, k4a_image_get_format, \ k4a_image_get_size, k4a_image_get_width_pixels, \ k4a_image_get_height_pixels, k4a_image_get_stride_bytes, \ k4a_image_get_device_timestamp_usec, k4a_image_set_device_timestamp_usec, \ @@ -280,6 +280,19 @@ def _create_from_existing_image_handle( return image + @staticmethod + @_memory_destroy_cb + def _buffer_destroy_callback(buffer: _ctypes.c_void_p, context: _ctypes.c_void_p): + """ + Skip decrementing the reference count on the array when the buffer is released + :param buffer: The image buffer + :param context: The array buffer object + :return: None + """ + # This function will probably not be called, but the buffer shouldn't be released + # since it is referenced by a numpy array + pass + @staticmethod def create( image_format:EImageFormat, @@ -400,47 +413,49 @@ def create_from_ndarray( ''' image = None - assert(isinstance(arr, _nd.ndarray)), "arr must be a numpy ndarray object." - assert(isinstance(image_format, EImageFormat)), "image_format parameter must be an EImageFormat." + assert (isinstance(arr, _np.ndarray)), "arr must be a numpy ndarray object." + assert (isinstance(image_format, EImageFormat)), "image_format parameter must be an EImageFormat." # Get buffer pointer and sizes of the numpy ndarray. - buffer_ptr = ctypes.cast( - _ctypes.addressof(np.ctypeslib.as_ctypes(arr)), - _ctypes.POINTER(_ctypes.c_uint8)) - - width_pixels = _ctypes.c_int(width_pixels_custom) - height_pixels = _ctypes.c_int(height_pixels_custom) - stride_bytes = _ctypes.c_int(stride_bytes_custom) - size_bytes = _ctypes.c_size_t(size_bytes_custom) - - # Use the ndarray sizes if the custom size info is not passed in. - if width_pixels == 0: - width_pixels = _ctypes.c_int(arr.shape[0]) - - if height_pixels == 0: - height_pixels = _ctypes.c_int(arr.shape[1]) - - if size_bytes == 0: - size_bytes = _ctypes.c_size_t(arr.itemsize * arr.size) - - if len(arr.shape) > 2 and stride_bytes == 0: - stride_bytes = _ctypes.c_int(arr.shape[2]) - - # Create image from the numpy buffer. - image_handle = _ImageHandle() - status = k4a_image_create_from_buffer( - image_format, - width_pixels, - height_pixels, - stride_bytes, - buffer_ptr, - size_bytes, - None, - None, - _ctypes.byref(image_handle)) - - if status == EStatus.SUCCEEDED: - image = Image._create_from_existing_image_handle(image_handle) + buffer_ptr = _ctypes.cast(_np.ctypeslib.as_ctypes(arr), _ctypes.POINTER(_ctypes.c_uint8)) + array_obj = _ctypes.py_object(arr) + # Hold onto the buffer memory by incrementing the reference count on the array object + _ctypes.pythonapi.Py_IncRef(array_obj) + + try: + # Use the ndarray sizes if the custom size info is not passed in. + width_pixels = _ctypes.c_int(arr.shape[1]) \ + if width_pixels_custom == 0 else _ctypes.c_int(width_pixels_custom) + height_pixels = _ctypes.c_int(arr.shape[0]) \ + if height_pixels_custom == 0 else _ctypes.c_int(height_pixels_custom) + stride_bytes = _ctypes.c_int(arr.strides[0]) \ + if stride_bytes_custom == 0 else _ctypes.c_int(stride_bytes_custom) + size_bytes = _ctypes.c_size_t(arr.itemsize * arr.size) \ + if size_bytes_custom == 0 else _ctypes.c_size_t(size_bytes_custom) + + # Create image from the numpy buffer. + image_handle = _ImageHandle() + status = k4a_image_create_from_buffer( + image_format, + width_pixels, + height_pixels, + stride_bytes, + buffer_ptr, + size_bytes, + Image._buffer_destroy_callback, + _ctypes.cast(_ctypes.pointer(array_obj), _ctypes.c_void_p), + _ctypes.byref(image_handle)) + + if status == EStatus.SUCCEEDED: + image = Image._create_from_existing_image_handle(image_handle) + # Delete the array object created indirectly by _create_from_existing_image_handle + # without deleting the data + del image.data + # Use the given array object instead (this won't create a new reference) + image._data = arr + except _ctypes.ArgumentError as details: + _ctypes.pythonapi.Py_DecRef(array_obj) + raise _ctypes.ArgumentError(details) return image @@ -514,7 +529,7 @@ def __deepcopy__(self, memo): def __enter__(self): return self - def __exit__(self): + def __exit__(self, *exception_details): del self def __str__(self): @@ -549,7 +564,7 @@ def _image_handle(self): @_image_handle.deleter def _image_handle(self): - + # Release the image before deleting. if isinstance(self._data, _np.ndarray): if not self._data.flags.owndata: diff --git a/src/python/k4a/src/k4a/_bindings/k4a.py b/src/python/k4a/src/k4a/_bindings/k4a.py index 3746aa353..dab16c9d9 100644 --- a/src/python/k4a/src/k4a/_bindings/k4a.py +++ b/src/python/k4a/src/k4a/_bindings/k4a.py @@ -23,9 +23,30 @@ _TransformationHandle, _Calibration, _Float2, _Float3, \ _memory_allocate_cb, _memory_destroy_cb - -__all__ = [] - +__all__ = ['k4a_image_create', 'k4a_image_create_from_buffer', 'k4a_set_debug_message_handler', + 'k4a_image_release', 'k4a_image_get_buffer', + 'k4a_image_reference', 'k4a_image_get_format', + 'k4a_image_get_size', 'k4a_image_get_width_pixels', + 'k4a_image_get_height_pixels', 'k4a_image_get_stride_bytes', + 'k4a_image_get_device_timestamp_usec', 'k4a_image_set_device_timestamp_usec', + 'k4a_image_get_system_timestamp_nsec', 'k4a_image_set_system_timestamp_nsec', + 'k4a_image_get_exposure_usec', 'k4a_image_set_exposure_usec', + 'k4a_image_get_white_balance', 'k4a_image_set_white_balance', + 'k4a_image_get_iso_speed', 'k4a_image_set_iso_speed', 'k4a_calibration_get_from_raw', + 'k4a_capture_create', 'k4a_capture_release', 'k4a_capture_reference', + 'k4a_capture_get_color_image', 'k4a_capture_set_color_image', + 'k4a_capture_get_depth_image', 'k4a_capture_set_depth_image', + 'k4a_capture_get_ir_image', 'k4a_capture_set_ir_image', + 'k4a_capture_get_temperature_c', 'k4a_capture_set_temperature_c', + 'k4a_device_get_installed_count', 'k4a_device_open', + 'k4a_device_get_serialnum', 'k4a_device_get_version', + 'k4a_device_get_color_control_capabilities', 'k4a_device_close', + 'k4a_device_get_imu_sample', 'k4a_device_get_color_control', + 'k4a_device_start_cameras', 'k4a_device_stop_cameras', + 'k4a_device_start_imu', 'k4a_device_stop_imu', + 'k4a_device_set_color_control', 'k4a_device_get_raw_calibration', + 'k4a_device_get_sync_jack', 'k4a_device_get_capture', 'k4a_device_get_calibration' + ] # Load the k4a.dll. try: @@ -182,9 +203,7 @@ k4a_image_create_from_buffer.restype = EStatus k4a_image_create_from_buffer.argtypes=( _ctypes.c_int, _ctypes.c_int, _ctypes.c_int, _ctypes.c_int, _ctypes.POINTER(_ctypes.c_uint8), - _ctypes.c_size_t, _memory_allocate_cb, _ctypes.c_void_p, _ctypes.POINTER(_ImageHandle)) - - + _ctypes.c_size_t, _memory_destroy_cb, _ctypes.c_void_p, _ctypes.POINTER(_ImageHandle)) #K4A_EXPORT uint8_t *k4a_image_get_buffer(k4a_image_t image_handle); k4a_image_get_buffer = _k4a_lib.k4a_image_get_buffer diff --git a/src/python/k4a/tests/test_create_image.py b/src/python/k4a/tests/test_create_image.py new file mode 100644 index 000000000..dfef2c9c4 --- /dev/null +++ b/src/python/k4a/tests/test_create_image.py @@ -0,0 +1,59 @@ +""" +test_create_image.py + +Tests for the creating Image objects using pre-allocated arrays. + +""" + +import numpy as np +from numpy.random import default_rng +import unittest + +import k4a + + +class TestImages(unittest.TestCase): + """Test allocation of images. + """ + + @classmethod + def setUpClass(cls): + pass + + @classmethod + def tearDownClass(cls): + pass + + def test_unit_create_from_ndarray(self): + # from inspect import currentframe, getframeinfo # For debugging if needed + + # Create an image with the default memory allocation and free + an_image = k4a.Image.create(k4a.EImageFormat.CUSTOM16, 640, 576, 640*2) + self.assertEqual(an_image.height_pixels, 576) + del an_image # this will call the default image_release function + + for index in range(5): + # We want a diverse source for the values, but also we want it to be deterministic + seed = 12345 + index + rng = default_rng(seed=seed) + two_channel_uint16 = rng.integers(0, high=65536, size=(576, 640), dtype=np.uint16) + # Uncomment these two lines to change the image buffer to show that the verification below works + # two_channel_uint16[0, 0] = (0x12 << 8) + 0x34 + # two_channel_uint16[0, 1] = (0x56 << 8) + 0x78 + + custom_image = k4a.Image.create_from_ndarray(k4a.EImageFormat.CUSTOM16, two_channel_uint16) + two_channel_uint16 = rng.integers(0, high=32767, size=(576, 640), dtype=np.uint16) + # Ensure that the original array is no longer known locally, but the data is still available through custom_image + self.assertFalse(np.all(two_channel_uint16 == custom_image.data)) + del two_channel_uint16 + + # Regenerate the values of the custom_image using the same seed for comparison + rng = default_rng(seed=seed) + verify_two_channel_uint16 = rng.integers(0, high=65536, size=(576, 640), dtype=np.uint16) + + self.assertTrue(np.all(verify_two_channel_uint16 == custom_image.data)) + del custom_image + + +if __name__ == '__main__': + unittest.main()