NEURAL depth mode silently disabled after a camera-grab error burst — fps rises above the NEURAL ceiling, no error surfaced

# NEURAL depth mode silently disabled after a camera-grab error burst — fps rises above the NEURAL ceiling, no error surfaced

**Setup:** ZED Box Mini (Jetson Orin, JetPack 6.2 / L4T 36.4), ZED SDK 5.x, Python bindings (`pyzed`), camera opened once at process start with `depth_mode = NEURAL`, fixed wall mount (`set_as_static = True`), continuous 24/7 operation via Object Detection.

**Symptom:** After a burst of camera-grab failures (`sl.ERROR_CODE` grab errors, tens of consecutive attempts) that the SDK itself recovers from, depth mode appears to silently reset to a non-NEURAL state. FPS then reads consistently *above* what NEURAL depth mode’s own compute cost should allow (steady-state ~15 fps under NEURAL vs. ~25 fps observed post-recovery), object detection returns zero people even when people are present, and no error, warning, or exception is raised — every other queryable stat (camera connection, grab return code, tracking state) reads healthy. The only external tell is the fps ceiling being breached. `enable_image_validity_check` (per the related ZED X thread below) does not catch this.

**Workaround in place:** we track an EMA of fps and treat “sustained fps above the NEURAL ceiling for >N seconds” as depth-mode-degraded, then force a full camera re-open. This recovers cleanly every time (~4s), so the impact is bounded, but it’s a symptom-based inference, not a real fix — there’s no SDK signal we can query directly to detect “NEURAL depth silently turned off.”

**Recurrence:** happening roughly every 5–24 hours in continuous operation, with the interval between recurrences trending shorter over a multi-day soak test (23h → 13h → ~5h), which may indicate the underlying condition compounds rather than being a one-off environmental fluke.

**Ask:**

1. Is there a supported way to query actual active depth mode / detect this silent fallback directly, rather than inferring it from fps?

2. Is the shrinking recurrence interval a known pattern, or does it point to something else (thermal, VRAM fragmentation, camera-module wear)?

3. Is this the same root cause as the frozen-frame report linked below, or a separate depth-pipeline reset bug?

**Related report:** [ZED X camera publishes frozen (stale) image frames indefinitely — requires node restart to recover]( ZED X camera publishes frozen (stale) image frames indefinitely — requires node restart to recover ) — same failure family (NEURAL depth mode, silent degradation after a grab hiccup, `enable_image_validity_check` doesn’t catch it, only fix is a restart), confirmed by Stereolabs staff as a first-reported case for that exact symptom.

Hi @jeff.crouse
Welcome to the StereoLabs community.

We forwarded all the details to the ZED SDK team.
They will try to replicate and fix the problem.

Please let me know if you need any further information or details.

Thank you @Myzhar – full disclosure, I also sent this to support@stereolabs – I was thinking this was more of a community forum – I wasn’t trying to cross-post as such.

1 Like

Thanks @Myzhar. Since the SDK team is trying to replicate this, here is everything we have
measured since, including one session where we caught the fault live and the only candidate
trigger we have found. The short version: we can characterise it precisely but cannot
reproduce it on demand, and there is no API that would let us confirm what it actually is.

Setup: ZED Box Mini (Orin NX, JetPack 6.2 / L4T 36.4), ZED 2i over USB, pyzed,
SDK 5.2.3, HD1080@30, DEPTH_MODE.NEURAL, positional tracking with
set_as_static=True, object detection MULTI_CLASS_BOX_FAST, running continuously.

The healthy baseline is very tight

Across 54 timed open/measure/close cycles in one session: 14.6 to 15.1 fps, mean
14.9.
Depth validity 89 to 92% of pixels. NEURAL’s steady-state rate on this hardware is
essentially a constant, which is what makes any deviation obvious.

What the fault looks like

One session ran at a sustained 24 to 25 fps for about 16 minutes, a 65% deviation from
a baseline that varies by under a fifth of a frame. Throughout it:

  • grab() returned SUCCESS, grab_errors 0
  • retrieve_objects() returned SUCCESS and object detection kept working normally
  • find_floor_plane() succeeded, floor normal within 1 degree of vertical
  • positional tracking state OK
  • nothing logged by the SDK at all

A process restart returned it to 14.9 fps immediately. Nothing else changed: same
nvpmodel power mode (15W), jetson_clocks inactive, SoC 60 C and not throttling.

So the only observable symptom is the frame rate. We infer a depth-mode change because a
cheaper mode is the only thing that would explain it, but we cannot confirm that
inference.
See question 1.

A candidate trigger, with a caveat

Every successful camera open on this box emits a burst of exactly two
uvcvideo ... Non-zero status (-71) kernel errors, followed by
usb ...: reset ... using tegra-xusb. That count is invariant: 30 observations, zero
deviation
, out of about 63 clean opens that day.

The one faulting session was the only open with a burst of three, and it went:

[ZED][ERROR] The camera could not be initialized. Please verify your camera connection...
[Init]  Failed to open camera. Trying to re-open for 5 seconds.
[Init]  Opening Camera. Attempt #2
[Init]  Camera successfully opened.      <- this session then ran at 24-25, not 15

Caveat, stated plainly: that is one observation. We tried to reproduce it and could
not. 54 further opens, all with a burst of 2, all clean, no internal retry. So the event is
rarer than 1 in 60 opens, and this is a correlation, not a demonstrated cause.

One thing we did rule out: camera contention is not the path. Holding the camera from a
second process fails differently and immediately, with CAMERA STREAM FAILED TO START in
0.2 s and no retry attempted. Only the “could not be initialized” path triggers the
5-second internal re-open.

Three questions

  1. Is there any way to query the ACTIVE depth mode at runtime?
    get_init_parameters() only echoes what was requested, so an application cannot tell
    whether NEURAL is still running. This is the single most useful thing you could give us:
    with it we could detect and correct this ourselves regardless of root cause.
  2. Does the internal re-open path (Trying to re-open for 5 seconds / Attempt #2)
    preserve InitParameters.depth_mode?
    If it can silently fall back, that would explain
    everything we see, and it would make the trigger a -71 cluster during open.
  3. Does a -71 cluster of 3 during open, against a normal 2, mean anything to you?

A note on SDK 5.4.1

We are staying on 5.2.3 deliberately. On 5.4.1, healthy NEURAL measures 24.8 to 27.4 fps on
this hardware, the same range the fault produces, so frame rate no longer separates the two
states and the fault becomes undetectable. We would happily move to 5.4.1 if there were an
API for the active depth mode, since frame rate would no longer be our only signal.

Hi @jeff.crouse,

thank you for this level of detail, it is exactly what the ZED SDK team needs. I forwarded the new measurements, the log excerpt, and the -71 statistics to them.

Answers and a few suggestions below.

Is there any way to query the ACTIVE depth mode at runtime?

Not in 5.2.3, and not in 5.4.1 either: get_init_parameters() returns the requested configuration, and no getter exposes the mode effectively used by the depth pipeline. I opened a feature request for an “effective runtime configuration” getter and attached your use case to it, since it is a good justification.

Two things that are queryable today, and that would narrow the cause a lot before that API exists:

  1. get_camera_information().camera_configuration.resolution and .fps, read immediately after open() in every session. If the internal re-open renegotiated a different video mode, NEURAL inference cost drops and fps rises while everything else reads healthy, with no depth mode change involved. This is my first suspect, and it is cheap to log.
  2. get_timestamp(sl.TIME_REFERENCE.IMAGE) on each successful grab. If the deltas are ~40 ms during the fault, the pipeline is genuinely producing more frames; if your loop spins at 25 Hz while timestamps advance at ~66 ms or repeat, then grab() is returning duplicated data and depth is not the variable at all. Those are two very different bugs, and this check separates them.

I would also add a content based fingerprint instead of relying on fps: valid pixel ratio plus depth standard deviation over a fixed ROI on a low texture region of your static scene. The AI modes fill low texture areas that stereo matching leaves empty, so the signature differs clearly, and it stays valid on 5.4.1 where the rates overlap; see Depth Modes. Finally, please log tegrastats (GR3D and EMC frequencies) during a faulting session, to exclude a DVFS/clock state as the source of the +65%.

Does the internal re-open path (Trying to re-open for 5 seconds / Attempt #2) preserve InitParameters.depth_mode?

That path reuses the same InitParameters, so depth_mode is not modified by it, and there is no designed silent fallback to a cheaper mode. I asked the SDK team to confirm it with a code review of that specific path in 5.2.3, because the retry also renegotiates the USB video mode; that part I do not consider proven yet, which is why point 1 above matters.

Does a -71 cluster of 3 during open, against a normal 2, mean anything to you?

-71 is EPROTO reported by uvcvideo, a USB transfer protocol error. Two of them followed by a tegra-xusb reset are normal on Tegra while the USB3 stream is being configured, and the SDK tolerates them. A third one means one extra failed configure/reset cycle before enumeration succeeded, which is precisely what surfaces as “The camera could not be initialized” and triggers the 5 second internal re-open. So it points to a marginal USB link (cable, connector, host controller state), not to the depth pipeline, and its rarity is consistent with your 1 in 60.

Practical steps on that side:

  • camera on the USB 3.0 Type-A port of the ZED Box Mini, no hub and no extension cable, using the cable supplied with the ZED 2i;
  • run with sdk_verbose=1 and sdk_verbose_log_file set, and share the full log of a faulting session (InitParameters reference);
  • async_grab_camera_recovery=True makes recovery explicit, with grab() returning CAMERA_REBOOTING instead of hiding the reconnection inside the call.

About 5.4.1: the higher NEURAL rate is expected and not a regression, 5.4 introduced an optimized inference path with faster depth inference and lower GPU load, so the fps ceiling stopped being a discriminator by design. The content based fingerprint above is version independent, so it should let you move to 5.4.x. One more request: when your EMA detector fires, record a short SVO2, or at least save the left image and the depth map. A faulting depth map compared with a healthy one from the same static scene would likely settle the question immediately, because an AI depth map and a stereo matching one are visually unmistakable.

Last note, on the hardware side. In a permanently mounted 24/7 installation the USB enumeration path is the weakest link, and it disappears with GMSL2. Your ZED Box Mini already exposes two GMSL2 FAKRA-Z connectors, so a ZED X or ZED X Mini connects directly, with locking connectors, power over the same cable, global shutter sensors and an IP67 enclosure; you can order them here: ZED X Stereo Camera. It removes this failure family from the deployment rather than detecting it.

I will update the thread as soon as the SDK team comes back to me.

Following up with a live capture. The short version is that this is not a depth-mode
problem at all, and both of the hypotheses in your last reply are ruled out by the same
measurement. I owe you a correction on one of my own claims too.

Setup unchanged: ZED Box Mini (Orin NX, JetPack 6.2 / L4T 36.4), ZED 2i over USB,
pyzed, SDK 5.2.3, HD1080@30, DEPTH_MODE.NEURAL, positional tracking
set_as_static=True, object detection MULTI_CLASS_BOX_FAST.

We caught it at 05:18 on 2026-09-09 and deliberately let it run for 3 hours 40 minutes
with our auto-restart disabled, so everything below is from a single sustained occurrence
rather than inference after the fact.

The image buffers come back zero-filled

The left image, written by the SDK itself via Mat::write() rather than by any code of
ours, is uniform RGB(0,134,0) across all 2,073,600 pixels. Not one pixel differs.

That value is the giveaway: an all-zero YUV frame (Y=U=V=0) converted through BT.601 gives
R=0, G=135, B=0. So what we are being handed is an uninitialised image buffer, while
grab() continues to return SUCCESS.

retrieve_measure(MEASURE::DEPTH) at the same moment is 99.94% NaN. The 0.06% that is
finite all sits between 0.39 m and 0.88 m, which is noise rather than scene.

Depth is therefore not falling back to a cheaper mode. There is no image behind it. And the
frame rate rises (14.9 to 25) for the same reason: processing nothing is fast.

Every other signal reads healthy, and the camera goes blind

Throughout the whole 3h40m, with a person standing in view:

  • grab() returns SUCCESS, grab_errors 0
  • retrieve_objects() returns SUCCESS, od_errors 0
  • positional tracking state OK, floor plane still locked
  • camera_ok true, camera still enumerated normally as 2b03:f880
  • object detection returned nothing at all: our raw pre-filter detection counter sat
    frozen for the entire period while somebody stood in front of the camera

So the practical consequence is total blindness with no error surfaced anywhere. That also
resolves an inconsistency in my own reports: my July description (zero people detected
while people are present) was correct, and a later message where I said object detection
kept working normally was wrong.

Your two hypotheses, both ruled out

Not a video-mode renegotiation. We now log
get_camera_information().camera_configuration at open and re-read it every 2 s. At
bring-up: 1920x1080 @ 30. During the fault: 1920x1080 @ 30, unchanged. No
renegotiation occurred.

Not duplicated or stale frames. get_timestamp(TIME_REFERENCE::IMAGE) deltas went from
67 ms healthy to 34 ms during the fault, with zero repeated timestamps across the
sampling window. The pipeline genuinely is producing more frames. They are just empty.

The trigger, three for three

async_grab_camera_recovery=True made this observable in-band, which was the single most
useful suggestion from your last reply. The sequence is now identical on all three
occurrences we have instrumented:

05:18:21  grab() returns CAMERA_REBOOTING
05:18:22  [Grab] Camera module reset
05:18:27  [Grab] Automatic recovery from image capture failure succeeded.
05:18:28  frame rate jumps; every frame from here is blank

Underneath it, the kernel shows a full re-enumeration, with the HID interface dropping
before the video interface:

05:18:22  usb 1-2.2: USB disconnect, device number 11      (HID, 12 Mbps, bus 1)
05:18:23  usb 2-1:   USB disconnect, device number 10      (video, 5 Gbps, bus 2)
05:18:23  usb 2-1:   new SuperSpeed USB device number 11 using tegra-xusb
05:18:26  uvcvideo 2-1:1.1: Non-zero status (-71) in video completion handler

So the shape of it is: USB re-enumeration, your internal recovery reports success, and from
that moment the capture buffers are zero-filled. Our reading is that the recovery re-opens
the device but the image buffers are never re-bound to the new handle. You will know
whether that is plausible.

A correction: enable_image_validity_check DOES catch this

In my first post I said it does not. That was wrong, and I want to withdraw it clearly
because it matters. With enable_image_validity_check = 1, at the exact fault boundary:

05:18:00   low_image_quality=false  low_depth_reliability=false
05:19:00   low_image_quality=TRUE   low_depth_reliability=TRUE     (held for 3h40m)

low_lighting stayed false throughout, correctly. So your own diagnostic flags this
within a minute. We had been journalling those fields for months without alerting on them.
We now restart on it and the fault self-heals in about a minute.

The cure is a process restart, nothing more

Restarting our process clears it completely, and all five signals return together: frame
rate 25 to 14.9, valid depth pixels 0.001 to 0.912, image timestamp interval 34 ms to
67 ms, and both validity flags back to false. We have never needed
sl::Camera::reboot(), usbreset, a power cycle or physical access for this fault.

Questions

  1. Why does the internal recovery leave the image buffers zero-filled? If the recovery
    re-opens the device without re-binding capture buffers, that would explain everything
    here, and it sounds fixable.
  2. Can grab() be made to report this? Right now an application has no return code to
    act on: SUCCESS with an empty buffer is indistinguishable from SUCCESS with a picture.
    Given the validity check already knows, could that state surface as an ERROR_CODE, or
    is polling get_health_status() after every grab the intended pattern?
  3. Is this the same root cause as the ZED X frozen-frame report linked in my first post?
    Both are the image pipeline returning invalid frames while reporting success. Ours are
    blank rather than stale, but the family looks the same.
  4. Would you like the artifacts? We have the SDK-written left image and depth map, the raw
    depth array, lsusb -t, the kernel window and the service journal across the boundary.

Finally, this thread’s title is now misleading. Nothing about the depth mode changes.
Happy to open a new thread with an accurate title if that helps the SDK team, or to leave
this one as the record. Your call.

Hi @jeff.crouse
Please note that what you have described is a known problem fixed with the ZED SDK v5.5 which will be released in the next few days.