From 076d31cada25925de4be75cfd4a1b5eef732737c Mon Sep 17 00:00:00 2001 From: Ian Caven Date: Thu, 14 Dec 2023 13:32:29 -0800 Subject: [PATCH 1/4] Add test for creating image. --- src/python/k4a/tests/test_create_image.py | 59 +++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 src/python/k4a/tests/test_create_image.py 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() From 3c825614130a8a45914df0939e187afd92dbaeff Mon Sep 17 00:00:00 2001 From: Ian Caven Date: Fri, 15 Dec 2023 11:49:28 -0800 Subject: [PATCH 2/4] Correct the image size initialization when the defaults are used and the buffer destroy callback. Change the release of an image so that the buffer is not released when held by a numpy array. --- src/python/k4a/src/k4a/_bindings/image.py | 111 ++++++++++++---------- src/python/k4a/src/k4a/_bindings/k4a.py | 31 ++++-- 2 files changed, 88 insertions(+), 54 deletions(-) diff --git a/src/python/k4a/src/k4a/_bindings/image.py b/src/python/k4a/src/k4a/_bindings/image.py index d066b325d..49577472e 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 @@ -453,7 +468,7 @@ def _reference(self): def __del__(self): # Deleting the _image_handle will release the image reference. - del self._image_handle + del self.image_handle del self.data del self.image_format @@ -470,7 +485,7 @@ def __del__(self): def __copy__(self): # Create a shallow copy. - new_image = Image(self._image_handle) + new_image = Image(self.image_handle) new_image._data = self._data.view() new_image._image_format = self._image_format new_image._size_bytes = self._size_bytes @@ -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): @@ -544,12 +559,12 @@ def __str__(self): # Define properties and get/set functions. ############### @property - def _image_handle(self): + def image_handle(self): return self.__image_handle - @_image_handle.deleter - 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 From 2aeaa7fdbef28bfd2e51c044c0e274ce9ae4b10d Mon Sep 17 00:00:00 2001 From: Ian Caven Date: Fri, 15 Dec 2023 11:54:30 -0800 Subject: [PATCH 3/4] Update version number for python bindings. --- src/python/k4a/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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.', From f2fce4b8903b66e22c54eae790f346dc8e833c1b Mon Sep 17 00:00:00 2001 From: Ian Caven Date: Fri, 15 Dec 2023 12:18:59 -0800 Subject: [PATCH 4/4] Revert the naming of the image_handle accessors. --- src/python/k4a/src/k4a/_bindings/image.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/python/k4a/src/k4a/_bindings/image.py b/src/python/k4a/src/k4a/_bindings/image.py index 49577472e..71cbe95e8 100644 --- a/src/python/k4a/src/k4a/_bindings/image.py +++ b/src/python/k4a/src/k4a/_bindings/image.py @@ -468,7 +468,7 @@ def _reference(self): def __del__(self): # Deleting the _image_handle will release the image reference. - del self.image_handle + del self._image_handle del self.data del self.image_format @@ -485,7 +485,7 @@ def __del__(self): def __copy__(self): # Create a shallow copy. - new_image = Image(self.image_handle) + new_image = Image(self._image_handle) new_image._data = self._data.view() new_image._image_format = self._image_format new_image._size_bytes = self._size_bytes @@ -559,11 +559,11 @@ def __str__(self): # Define properties and get/set functions. ############### @property - def image_handle(self): + def _image_handle(self): return self.__image_handle - @image_handle.deleter - def image_handle(self): + @_image_handle.deleter + def _image_handle(self): # Release the image before deleting. if isinstance(self._data, _np.ndarray):