Python API: grab() and reset_positional_tracking() hold the GIL, blocking other Python threads (IMU thread stalls ~2.8 s)

Hi,

In pyzed, other Python threads in the same process stop running while grab() or reset_positional_tracking() is running. It looks like the wrapper doesn’t release the GIL during these calls.

Setup: ZED X Mini, SVGA @ 60 fps, GEN_3 tracking with area memory, ZED SDK [version], Python 3.10, Jetson AGX Orin, JetPack [version].

What I see:

  • An IMU thread polling get_sensors_data(CURRENT) at 200 Hz only publishes at the camera’s 60 Hz.
  • During reset_positional_tracking() (2.8 s), it publishes nothing.
  • A plain heartbeat thread that only calls time.sleep(0.005) and never touches the camera also stalls for 2768 ms during the reset.

Repro:

python

import threading, time
import pyzed.sl as sl

zed = sl.Camera()
init = sl.InitParameters()
init.camera_resolution = sl.RESOLUTION.SVGA
init.camera_fps = 60
assert zed.open(init) == sl.ERROR_CODE.SUCCESS
tp = sl.PositionalTrackingParameters()
tp.enable_area_memory = True
zed.enable_positional_tracking(tp)

def heartbeat():
    last = time.monotonic()
    while True:
        time.sleep(0.005)
        now = time.monotonic()
        if now - last > 0.05:
            print(f"heartbeat stalled {1e3 * (now - last):.0f} ms")
        last = now
threading.Thread(target=heartbeat, daemon=True).start()

rt = sl.RuntimeParameters()
for i in range(300):
    zed.grab(rt)
    if i == 150:
        zed.reset_positional_tracking(sl.Transform())
zed.close()

Question: Does pyzed hold the GIL during these calls on purpose? Would it be safe to release it (with nogil:) so other threads, such as an IMU reader, keep running?

Thanks!

Hi @bajaj10
Welcome to the StereoLabs community-

thank you for the detailed report and the reproducer.

This is only partially correct. I checked the source code of the ZED Python API:

  • Camera.grab() releases the GIL (with nogil:) since ZED SDK v5.0.
  • Camera.get_sensors_data() releases the GIL since ZED SDK v5.1.
  • Camera.reset_positional_tracking() does not release the GIL in any version, including the latest v5.4.0.

During reset_positional_tracking() (2.8 s), it publishes nothing.
A plain heartbeat thread that only calls time.sleep(0.005) and never touches the camera also stalls for 2768 ms during the reset.

This is caused by the missing GIL release in reset_positional_tracking(). I’m going to report it to the StereoLabs development team so it can be fixed in a future release.

In the meantime, you can patch the wrapper locally since it’s open source. Clone the repository for your ZED SDK version and apply these two changes.

In src/pyzed/sl_c.pxd:

ERROR_CODE resetPositionalTracking(const Transform &path) nogil

In src/pyzed/sl.pyx, class Camera:

def reset_positional_tracking(self, Transform path) -> ERROR_CODE:
    cdef c_ERROR_CODE err
    with nogil:
        err = self.camera.resetPositionalTracking(path.transform[0])
    return _error_code_cache.get(<int>err, ERROR_CODE.FAILURE)

Then rebuild and install it by running python -m pip install . from the repository root, as described in the README.

With an up-to-date wrapper this should not be caused by the GIL, because both grab() and get_sensors_data() release it.

Your post still has [version] placeholders. Please share:

  • the exact ZED SDK version,
  • the pyzed version (python3 -m pip show pyzed),
  • the JetPack version.

A pyzed package that doesn’t match the installed ZED SDK version could explain this behavior.

Thanks for the quick and detailed answer. The patch worked. Here are the versions and results.

Versions

  • ZED SDK 5.3.1 (headers and sl.Camera.get_sdk_version() agree)
  • pyzed 5.3, built for 5.3.1, Python 3.10.12, numpy 1.26.4
  • L4T R36.4.7 (JetPack 6.x; nvidia-jetpack meta-package not installed), CUDA 12.6
  • Ubuntu 22.04.5, ROS 2 Humble
  • ZED X Mini, FW 2001, SVGA@60, NEURAL_LIGHT, area memory on with a saved .area map

So SDK and pyzed were matched before the patch.

Test
A standalone script (no ROS) with the grab() + get_position() loop on the main thread, a heartbeat thread that only calls time.sleep(0.005), and an IMU thread. reset_positional_tracking() is called on the main thread 10 s into a 20 s run.

before patch after patch
reset_positional_tracking() duration 2.75 s 2.70 s
heartbeat max gap during reset 2759 ms 5.1 ms
heartbeat max gap during grab() 5.1 ms 5.3 ms

The nogil patch fixes the thread freeze. grab() was never the problem, as you said.

IMU rate: get_sensors_data(CURRENT) vs get_sensors_data_batch()
The 60 Hz I reported wasn’t the GIL. sensors_configuration reports accelerometer and gyroscope at 200 Hz. But get_sensors_data(CURRENT) polled at 200 Hz only returns a new IMU timestamp every 20.00 ms (about 50 Hz). get_sensors_data_batch() returns all samples at 5 ms spacing (200 Hz). It looks like samples arrive in groups of about 4 every 20 ms, and CURRENT only exposes the newest one. I’ve switched to the batch call.

Remaining issue: no IMU samples during reset_positional_tracking()
After the patch, with the IMU thread calling get_sensors_data_batch() at 200 Hz from its own thread:

  • the IMU thread keeps running during the reset (4003 successful calls in 20 s, about 200/s throughout)
  • but the IMU sensor timestamps have a 2220 ms gap during the reset. Those samples are never returned later, so they’re missing, not delayed.

Questions

  1. Is it expected that IMU data stops during reset_positional_tracking()? Is there a way to keep the IMU streaming through a reset?
  2. Is get_sensors_data(CURRENT) updating only every 20 ms expected on the ZED X Mini at 200 Hz?
  3. Is it safe to call get_sensors_data_batch() from a separate thread while grab() runs (and during a reset)? Your sample calls it right after grab().

Thanks again. Happy to run more tests if useful.

Hi @bajaj10,

thank you for the detailed tests and for confirming that the patch solves the thread freeze. I’ve reported the missing GIL release in reset_positional_tracking() to the development team so it can be fixed in a future release.

Is it expected that IMU data stops during reset_positional_tracking()? Is there a way to keep the IMU streaming through a reset?

This is caused by how get_sensors_data_batch() works: it returns all the sensor samples associated with the most recent grabbed frame (API reference). While reset_positional_tracking() runs on the main thread, grab() is not called; the samples received during the reset are not associated with any frame, so they are never returned, which matches the 2220 ms gap you measured.

To keep the IMU streaming during a reset, your IMU thread can call get_sensors_data(sl.TIME_REFERENCE.CURRENT) while the reset is in progress, because it does not depend on grab() (Retrieve New Sensor Data). I’ve also asked the development team to evaluate keeping the batched samples available across a reset.

Is get_sensors_data(CURRENT) updating only every 20 ms expected on the ZED X Mini at 200 Hz?

Yes. The IMU samples at 200 Hz, but the samples reach the host in groups, as you correctly noticed, and TIME_REFERENCE.CURRENT only exposes the newest one. get_sensors_data_batch() is the correct function to retrieve the full 200 Hz stream.

Is it safe to call get_sensors_data_batch() from a separate thread while grab() runs (and during a reset)?

Yes, the sensors functions can be called from a thread different from the one running grab(). I also recommend upgrading to the latest ZED SDK v5.5; it fixes possible crashes in applications that call the SDK from a thread other than the grab() one, for example reset_positional_tracking() now waits for the current grab() to complete (ZED SDK 5.5 release notes). Remember to apply the nogil patch again to the v5.5 wrapper.