Skip to content
This repository was archived by the owner on Aug 22, 2024. It is now read-only.

Bugfix/image buffer release #1964

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/python/k4a/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

setup(
name='k4a',
version='0.0.2',
version='0.0.3',
author='Jonathan Santos',
author_email='[email protected]',
description='Python interface to Azure Kinect API.',
Expand Down
101 changes: 58 additions & 43 deletions src/python/k4a/src/k4a/_bindings/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, \
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand Down
31 changes: 25 additions & 6 deletions src/python/k4a/src/k4a/_bindings/k4a.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
59 changes: 59 additions & 0 deletions src/python/k4a/tests/test_create_image.py
Original file line number Diff line number Diff line change
@@ -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()