You ran an OpenCV erode over a depth frame, the result looked right, and the next function read the original data anyway. Nothing is wrong with your morphology: the edit never reached the buffer the frame exposes. Assigning the result to a numpy name only re-points the name, and pushing data in through an attribute on the frame object is refused as read-only. The fix is one line — a slice assignment on the same buffer view you read from. Below are the buffer semantics behind the behaviour, the exact write-back steps in NumPy, a worked 640 × 480 uint16 example with the arithmetic, and the tests that prove the change persisted.
Why the edit does not stick
A depth frame wrapper is a handle into memory the SDK owns. Whether an edit survives is decided by numpy mechanics, not by an SDK quirk.
np.asarray(frame.get_data()) returns a view: numpy lays its array structure over the frame's own bytes and copies nothing. The view behaves like a normal array, so cv2.erode(depth, kernel) works on it directly and returns a fresh, separate array. The line depth = cv2.erode(depth, kernel) re-points the name depth at that new array and detaches it from the frame. The buffer still holds the original values, which is exactly why the next consumer sees no change.
Pushing the data in through an attribute fails differently: the wrapper raises an error that the attribute is not writable, because frames come from the device and expose no setter.
| Attempt | What actually happens |
|---|---|
processed = cv2.erode(depth, kernel) |
Runs, and the name points to a new array. The frame's buffer is untouched. |
depth = cv2.erode(depth, kernel) |
The same rebinding. depth stops aliasing the frame's memory. |
depth_frame.data = processed |
Refused: the attribute is not writable. |
np.asarray(depth_frame.get_data())[:, :] = processed |
Copies values into the frame's own memory. Every later read sees them. |
The zero-copy view is the same mechanism that lets you wrap an SDK frame buffer in an OpenCV cv::Mat without copying. And the slice assignment is the one move that acts on memory instead of names: it copies element values into the shared buffer.
What you need
- The depth frame object from your SDK's frame set, exposing its raw buffer through
get_data(). - numpy (
np) and OpenCV (cv2) in the same environment as the SDK. - An 8-bit structuring kernel:
cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))ornp.ones((5, 5), dtype=np.uint8). - Depth samples in a morphology-capable dtype. Depth buffers arrive as uint16 millimetres in the common setup, and
cv2.erodeandcv2.dilateaccept 16-bit unsigned directly.
Write the result back in place
-
Pull the view.
depth = np.asarray(depth_frame.get_data())gives a 2-D uint16 array of shape(H, W)that shares the frame's memory. No copy happens here. -
Snapshot once for the test.
before = depth.copy()costs one frame-sized copy and is only needed to prove the point. -
Process into a new array.
clean = cv2.erode(depth, kernel)produces an independent result; the frame's buffer still holds the original. -
Write back with a slice assignment.
np.asarray(depth_frame.get_data())[:, :] = cleancopies the values into the frame's own buffer. The indexer[:, :]is what turns the expression into a write into memory rather than a name change. -
Continue with the frame. Later reads of
depth_frame.get_data()— and ofdepth, which still aliases the same memory — see the eroded data.
import numpy as np
import cv2
# depth_frame: the SDK's depth frame object (raw buffer accessor: get_data()).
depth = np.asarray(depth_frame.get_data()) # uint16 view, shape (H, W)
before = depth.copy() # own snapshot, only for the test
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
clean = cv2.erode(depth, kernel) # new uint16 array
np.asarray(depth_frame.get_data())[:, :] = clean # the fix: write back in place
Worked example: eroding a 640 × 480 uint16 depth frame
Example inputs, a typical compact depth mode rather than a product limit: 640 × 480 pixels, one uint16 sample per pixel (2 bytes), a 5 × 5 kernel, a stream of 30 frame/s.
The rule in words: a view shares the frame's memory; a slice assignment copies values into that shared memory; a name rebind changes only what the name points to.
The arithmetic, units carried through:
- Buffer size: 640 × 480 = 307,200 px; 307,200 px × 2 B/px = 614,400 B = 600 KiB (614,400 ÷ 1024).
- Each morphology call allocates its own new 600 KiB result. The frame's 600 KiB buffer keeps the original values until the write-back overwrites them in place.
- The write-back is one 600 KiB copy. At 30 frame/s that is 600 KiB × 30 = 18,000 KiB/s ≈ 17.6 MiB/s of pure copying — a small cost, and only worth paying when something reads the buffer again.
# Example inputs: 640 x 480 uint16 depth frame, 5 x 5 kernel, 30 frame/s
depth = np.asarray(depth_frame.get_data()) # uint16, shape (480, 640)
before = depth.copy() # 600 KiB snapshot
clean = cv2.erode(depth, np.ones((5, 5), np.uint8))
# Test 1: the erode alone left the frame's buffer untouched
print(np.array_equal(depth, before)) # True
np.asarray(depth_frame.get_data())[:, :] = clean
# Test 2: the frame now carries the eroded data
print(np.array_equal(np.asarray(depth_frame.get_data()), clean)) # True
# Test 3: the first view still aliases the same, now-updated memory
print(np.array_equal(depth, clean)) # True
The three True prints are the proof. Test 1 reproduces the failure you started with: morphology alone changed nothing in the buffer. Test 2 shows the write-back landed. Test 3 shows depth and the buffer are one memory, so no second write is needed.
Decision: write back when the frame object must carry the edit for code you do not control — a saved frame set, an SDK-side consumer, another module that re-pulls get_data(). Skip it when your pipeline is numpy end to end: use clean directly and save the 600 KiB copy per frame.
Common mistakes
-
Kernel dtype. OpenCV morphology kernels are 8-bit:
getStructuringElementreturns one, whilenp.ones((5, 5))defaults to float64. Passdtype=np.uint8. -
Silent casting on write-back. Assigning a float result into the uint16 view casts values during the copy. Round and convert once with
.astype(np.uint16)at a point you chose. -
A read-only source array. If the wrapper exposes immutable bytes,
np.asarrayreturns a read-only array and[:, :] =raises an assignment-destination error. There is no write-back path then: copy once into your own array and stay on it. SDK refusals usually have a reason — a locked pixel format, for instance, frees up once the scan type is set.
If corruption follows a write-back, look at buffer recycling next: the stream buffer handling mode on the transport layer decides which frames are kept and which are handed back.
When to keep a copy instead
Choose by who reads the data next.
-
Your pipeline only. Copy once and forget the frame:
depth = np.asarray(depth_frame.get_data()).copy(). This is the copy-out discipline that applies whenever bytes leave the frame, as with stereo frames saved from the SDK buffer. - Occasional lookups. Skip both copy and write-back and index the view directly, the way you would read a distance from a depth image pixel without the SDK.
- Downstream readers of the frame. Do the in-place write-back from the steps above.
If the next stage builds a point cloud or mesh from the modified data, strides and dtypes matter more than where the bytes live; the numpy depth image to point cloud guide covers that conversion.
Depth hardware for this pipeline
Everything above is buffer mechanics: it applies to any depth buffer pulled through a frame wrapper, whatever the sensor type underneath.
- Type
- Depth / ToF camera
- Type
- Stereo / structured light
The first is a ToF sensing camera; the second builds depth from projected structured light. Both sit in the 3D sensors category alongside laser profilers, stereo cameras and 2.5D inspection systems, and the camera finder narrows that range by type.
How the sensor is positioned matters as much as the code: the 3D camera working distance guide covers standoff and geometry. When the pipeline is ready to specify, start the configurator at the camera step, and the 3D topic page collects the rest of the 3D guides.
Questions
Why does my OpenCV edit to the depth frame disappear?
cv2.erode and cv2.dilate return a brand-new array, and assigning it to a name only re-points that name. The frame's buffer never receives the values, so the next read sees the original data. Use the new array directly, or write it back with np.asarray(depth_frame.get_data())[:, :] = result. np.shares_memory(depth, result) tells you in one call whether two arrays still alias the same buffer.
Does the same write-back work for infrared and colour frames?
Yes, provided the array shape matches the buffer. An infrared frame is single-channel, so [:, :] fits. A colour frame is usually packed as three channels, so assign through [:, :, :] and keep the channel order identical to what get_data() handed you. If the SDK decodes a compressed format for you, the decoded array can be a fresh copy, and only that copy changes.
Can I feed my processed depth data into the SDK's own filters afterwards?
Built-in filter chains consume frames produced by the device pipeline, not arrays you assembled, so there is normally no documented route back in. Keep that processing in your own code and run the steps in your chosen order on the arrays: erode, hole-fill, median, then whatever measures the part. The write-back exists to make the frame's own buffer carry your result, not to re-enter the filter chain.
How much memory do the view, the snapshot and the result use?
The view costs nothing new, because it wraps the frame's own memory. For the 640 x 480 uint16 example, the snapshot and the erode result each take 614,400 bytes (600 KiB). Scaling is linear in pixels: a 1280 x 720 uint16 frame is 1280 x 720 x 2 = 1,843,200 bytes, about 1800 KiB, so the same pattern adds roughly 3600 KiB per frame cycle.
Which data types does OpenCV morphology accept on a depth buffer?
cv2.erode and cv2.dilate accept 8-bit unsigned, 16-bit unsigned, 16-bit signed, and 32- or 64-bit floating input, so a uint16 millimetre depth buffer runs directly. Kernels are the exception and must be 8-bit; cv2.getStructuringElement builds one. If another filter rejects uint16, convert once with astype, process, and cast back before the write-back so both dtypes match.

