System designTroubleshooting

Pass bounding box coordinates to a depth function

Detection coordinates stay at their initial values inside the depth function: Python scope keeps locals apart. Pass the bounding box as arguments to fix it.

LumaSpec Vision9 min read Share on LinkedIn

On this page 9 sections
  1. 01Symptom: two prints, two different boxes
  2. 02Why Python keeps the depth function on the old values
  3. 03Likely causes in test order
  4. 04Fix: pass the four coordinates as parameters
  5. 05The global alternative
  6. 06Worked example: what the stale crop costs
  7. 07Prevention: one box tuple and a shape assert
  8. 08When relative depth is not enough
  9. 09Questions

Your detection loop prints live pixel coordinates, yet the depth function keeps cropping the depth map with the initial values 0, 0, 340, 640, and every distance that follows is wrong. That is a variable-scope problem, not a vision problem, and it takes minutes to fix. It matters to anyone wiring a detector to a monocular depth network on a vision controller or a workstation: the same pattern silently breaks every value computed in one stage and read in another. This guide gives the likely causes in test order, the parameter fix with a print that proves it, the slice order that crops the box correctly, and the arithmetic that shows what a stale crop costs. When you finish, the depth function receives the same four numbers the detection loop prints.

Symptom: two prints, two different boxes

The detection stage prints this frame's coordinates. The depth function, called on the same frame, prints the values you initialised at the top of the file.

Say the loop assigns 95 210 335 470 and the depth function prints 0 0 340 640. Nothing raises; the pipeline runs to completion. That silence is the trap: the depth map is cropped with a box that belongs to no object in the scene.

The stale crop covers 340 × 640 = 217,600 px² of the upper-left region. The stale crop is 3.5 times larger than the real box and mostly background, so a minimum over it returns the distance of whatever sits top-left, not of the object.

Why Python keeps the depth function on the old values

Three scoping rules of the language explain the whole fault:

  • A name assigned anywhere inside a function is local to that call, unless the function declares it global.
  • A name that is only read is looked up in the function's local scope first, then in enclosing scopes, then in the module's global names.
  • Two functions never share local variables, no matter how similar the names look.

The script sets detx1, dety1, detx2, dety2 = 0, 0, 340, 640 at module level. The detection stage runs inside its own function, so its fresh assignments are locals that die when the function returns. The depth function reads names it never received, finds no local binding, and falls through to the module globals, where the initial values still sit.

If your loop really assigns at module level, reads would see the writes. Seeing defaults means one of two things: the assignment is trapped in another function's scope, or it never ran that frame because the detection list was empty.

Likely causes in test order

Work down the table and stop when a test reproduces the fault.

# Cause Fast test Fix
1 The detection values are locals of the detection function Print the four names just before the call and on the first line inside the depth function; different numbers confirm it Pass them as parameters (next section)
2 The depth call runs before the assignment, or no detection that frame Print a frame counter beside the values on both sides; an empty detection list leaves the defaults Guard the call: run depth only when a box was assigned this frame
3 Coordinates refer to the color frame, but the depth map is smaller Print the map's shape next to the frame's; for example (360, 640) against (720, 1280) Scale the four coordinates by map width / frame width before slicing
4 Rows and columns swapped in the slice Print the crop's shape and compare it with (box height, box width) Slice [y1:y2, x1:x2]: rows first, columns second

Cause 1 matches the symptom exactly, real values in the loop and initial values in the function with no error, so test it first.

Fix: pass the four coordinates as parameters

  1. Change the depth function's signature to receive the box: def Predict(inputs, detx1, dety1, detx2, dety2):. All four are integer pixel indices in xyxy order.
  2. Call it after the detection block has assigned this frame's values: distances = Predict(frame, detx1, dety1, detx2, dety2).
  3. Inside the function, slice rows first and columns second: crop = depth_map[dety1:dety2, detx1:detx2].
  4. If the depth map is smaller than the color frame, scale the four coordinates before slicing, as in the worked example below.
  5. Confirm the fix with a paired print:
# detection stage, same frame
detx1, dety1, detx2, dety2 = 95, 210, 335, 470   # int pixel indices, xyxy
print('loop :', detx1, dety1, detx2, dety2)      # loop : 95 210 335 470

def Predict(inputs, detx1, dety1, detx2, dety2): # coordinates now arrive
    print('depth:', detx1, dety1, detx2, dety2)  # depth: 95 210 335 470
    crop = inputs[dety1:dety2, detx1:detx2]      # rows (y) first
    return crop

crop = Predict(frame, detx1, dety1, detx2, dety2) # frame: the image array
print('crop shape:', crop.shape)                 # crop shape: (260, 240)

The two prints now match, and the crop shape equals box height 260 by box width 240. If the depth print still shows 0 0 340 640, the call site is passing values from before the assignment, which is cause 2. These are the interpreter's rules, not the hardware's: the same fix holds wherever the script runs.

The global alternative

Where changing the signature is impractical, declare the names global in the scope that assigns them:

def detect(frame):
    global detx1, dety1, detx2, dety2   # writes now reach module level
    detx1, dety1, detx2, dety2 = 95, 210, 335, 470

A global line inside the depth function does nothing there; the declaration matters where the assignment happens. Parameters remain the better fix for a pipeline you intend to keep, because the call site shows which frame's box the depth stage consumes and no hidden state survives a refactor or a second thread. Keep global for short single-file scripts that process one frame at a time.

Worked example: what the stale crop costs

Take the real box detx1, dety1, detx2, dety2 = 95, 210, 335, 470 and the defaults 0, 0, 340, 640.

  • Area: the real box is (335 − 95) × (470 − 210) = 240 × 260 = 62,400 px². The default is 340 × 640 = 217,600 px², 3.5 times larger and in the wrong place.
  • The ::5, ::5 stride keeps every fifth row and column: the real crop yields 48 × 52 = 2,496 samples, the default 68 × 128 = 8,704. The extra 6,208 samples come from outside the object, and a minimum over the list picks the nearest of those instead.
  • Scale check: with a 1280 × 720 color frame and a 640 × 360 depth map, halve each coordinate: x1 = round(95 × 0.5) = 48, y1 = round(210 × 0.5) = 105, x2 = round(335 × 0.5) = 168, y2 = round(470 × 0.5) = 235. The slice becomes map[105:235, 48:168], 130 × 120 px.
  • Skip the scaling and nothing errors: rows 0:340 on a 360-row map silently clamp to 0:360. NumPy slicing clamps out-of-range indices instead of raising, which is why a wrong frame reads as a plausible result.

The units matter too: a monocular network returns normalized values in the 0–1 range. Multiplying by 1000 converts to model units, not millimetres, so calibrate before printing a unit, as the last section shows.

Prevention: one box tuple and a shape assert

Make the fault impossible to reintroduce:

  • Return the box from the detection stage and pass it as one argument: box = (detx1, dety1, detx2, dety2). Four separate names can drift apart; one tuple cannot.
  • Assert the box before slicing: assert 0 <= detx1 < detx2 <= W and 0 <= dety1 < dety2 <= H. A bad box then fails loudly at the right line instead of quietly cropping a wrong region.
  • Keep each frame's data in one dictionary of box, class, confidence and frame index, and pass that dictionary to every stage.
  • Re-run the paired print after any pipeline change; it exposes a stale default in seconds.

For the coordinate mapping itself, bounding box depth: mapping image pixels to 3D points walks the pixel-to-3D transform, and cropping an image ROI when the corner coordinates are unknown covers the crop mechanics from the other side. Cropping an XY region on a 3D depth camera point cloud is the same operation one stage later. To debug coordinates that arrive wrong rather than stale, use the object detection output coordinates guide.

When relative depth is not enough

A monocular depth network estimates relative depth: normalized values that order distances correctly but do not measure them. To print a physical unit, calibrate one scale factor from a known distance:

s = D_known / v_known distance ≈ s × v

Here D_known is a measured distance to a target and v_known is the network's value at the same target. The factor holds near the calibration distance and drifts as the scene changes, so re-check it across the working range. When the number must be trustworthy, for clearance checks, dimensioning or robot approach limits, measure instead of estimate: 3D sensors return calibrated depth per pixel, with laser profilers for profiles and structured-light or stereo cameras for full-frame depth.

The MV-DPS200P-02 projects structured light and returns measured depth, so the box you now pass correctly crops a real measurement instead of a network estimate.

3D sensorsHikrobot MV-DPS200P-02 Projected structured-light 3D cameraMV-DPS200P-02
Type
Stereo / structured light
$8,829.00 · quotedView and quote

Laser-based sensors carry a class label on the housing; follow the label's requirements for the beam path during set-up and never view the beam at close range.

Whatever runs the loop must hold the detector, the depth model and the camera streams in one process. The MV-VC5525-256G70-NN vision controller has 32 GB of memory, PoE camera ports and 8 built-in light channels, so the camera, the lighting and both models share one box.

Vision controllersHikrobot MV-VC5525-256G70-NN VC5000 Vision Controller, 12th-I5, 32GB+256G SSD, Supported PCIEMV-VC5525-256G70-NN
LAN ports
5+ LAN ports
USB 3 ports
3-4 USB 3 ports
PoE
PoE camera ports
Light outputs
8 built-in channels
$4,479.00 · quotedView and quoteBuild a system with it

If the inspection can run on the device instead of a controller, the smart cameras category is the shorter path: the detector, the box handling and the depth stage live on the camera itself. The systems topic page collects more fixes in this series, and the contact page is where to send a set-up that resists these checks.

Questions

Why does my function print the initial values while the main loop prints the real ones?

Assignment inside a function creates a local name that disappears when the function returns. The depth function never receives those locals, so it falls back to the module-level defaults, 0, 0, 340, 640. Both prints are correct for their own scope; the values simply never cross between the two functions. Pass the four coordinates as arguments and both scopes see the same numbers.

Does declaring global inside the depth function fix the stale values?

No. The global declaration matters in the scope that assigns a name, not in one that only reads it. Put global detx1, dety1, detx2, dety2 at the top of the detection function, where the assignments happen, so the writes reach module level. Parameters stay the cleaner fix, because the call site then shows which frame's box the depth stage uses.

Which index order crops a NumPy image or depth map correctly?

Rows first, then columns: map[y1:y2, x1:x2]. For a box stored in xyxy order that reads map[dety1:dety2, detx1:detx2]. A slice written the other way round transposes the window; on a non-square image the region is wrong, and on a square image the bug hides completely, because the transposed window still looks like a valid crop.

How do I turn monocular depth values into millimetres?

Calibrate one scale factor against a known distance: place a target at a measured distance D, read the network's value v at that target, then multiply every output by D / v. The factor holds only near that calibration distance, so re-check it at the working distances you actually use. For traceable measurements, use a calibrated 3D sensor rather than a monocular estimate.

What should the pipeline do on frames where the detector finds nothing?

Decide it explicitly: skip the depth call and report the last distance with its frame age, or output a no-object result. Never fall through to the initial values, because a default box is a plausible-looking wrong answer. Guard at the call site so the depth function only runs when a box was assigned in the same frame.