You call the RGBD helper, Python raises AttributeError, and the message names a module you never typed: module 'open3d.cpu.pybind.geometry' has no attribute 'create_rgbd_image_from_color_and_depth'. Nothing on your machine is broken. Two separate things happened: Open3D renamed its module-level factory functions when release 0.8 restructured the API, and the package bound its CPU backend at import time, which is why the path carries the cpu.pybind prefix. This page separates the two, gives the replacement calls with their defaults, and works a depth-plus-color to point-cloud conversion with a numeric test that runs in under a second on any machine, no camera attached.
The two facts inside the error message
The AttributeError combines a rename and a backend label. Fact one: since release 0.8 the factory functions live on the classes, as the official API documentation records. geometry.create_rgbd_image_from_color_and_depth(color, depth) became geometry.RGBDImage.create_from_color_and_depth(color, depth), and geometry.create_point_cloud_from_rgbd_image(rgbd, intrinsic) became geometry.PointCloud.create_from_rgbd_image(rgbd, intrinsic). Check your own install in one line: [m for m in dir(o3d.geometry.RGBDImage) if 'create' in m]. Fact two: open3d.cpu.pybind.geometry is the compiled module your import was actually served, which the next section explains.
| Before 0.8 | 0.8 and later |
|---|---|
open3d.geometry.create_rgbd_image_from_color_and_depth(color, depth) |
open3d.geometry.RGBDImage.create_from_color_and_depth(color, depth) |
open3d.geometry.create_point_cloud_from_rgbd_image(rgbd, intrinsic) |
open3d.geometry.PointCloud.create_from_rgbd_image(rgbd, intrinsic) |
Ranked by probability, the cause is almost always the first: a current package running an old snippet. The classes themselves never moved — Image, RGBDImage and PointCloud still live on open3d.geometry, which is why your Image() call succeeds while the old factory name fails. A distant second is an environment mix-up: documentation read for 0.7 in one virtual environment, code running in another. print(o3d.__version__) settles it in one line.
Why the path says open3d.cpu.pybind.geometry
The package selects one compiled backend at import time and re-exports it under the friendly names. On an install without a usable CUDA device — a CPU wheel, a machine with no CUDA-capable GPU, a driver mismatch — the loaded submodule is open3d.cpu.pybind, and its namespaces are what open3d.geometry, open3d.io and open3d.camera point to. On a CUDA build with a working driver, the same import binds open3d.cuda.pybind instead. AttributeError reports the lookup where it actually happened, so the message shows the full compiled path.
Two consequences follow. The prefix is a status read-out, not a fault: both backends expose identical classes and methods, so no code change belongs there. And one pip install carries one backend; running CPU and CUDA from the same environment means building Open3D from source, which its official build documentation covers. No second GPU backend ships with the pip wheels yet, so machines without CUDA always land on the CPU module.
import open3d as o3d
print(o3d.__version__) # the release your environment is running
print(o3d.geometry) # module 'open3d.cpu.pybind.geometry' on a CPU install
The second print names the backend directly. If it prints the cuda path on one workstation and the cpu path on another, both are correct for their hardware.
The replacement calls, with their defaults
import open3d as o3d
rgbd = o3d.geometry.RGBDImage.create_from_color_and_depth(
color, # uint8 RGB, H x W x 3
depth, # uint16 depth, raw units per depth_scale
depth_scale=1000.0, # raw units per meter; 1000.0 = millimeter depth
depth_trunc=3.0, # meters; pixels beyond this produce no point
convert_rgb_to_intensity=False,
)
pcd = o3d.geometry.PointCloud.create_from_rgbd_image(rgbd, intrinsic)
depth_scale=1000.0, depth_trunc=3.0 and convert_rgb_to_intensity=True are the shipped defaults: millimeter input, points kept out to 3.0 m, color collapsed to intensity unless you say otherwise. create_from_rgbd_image then projects every valid pixel with the pinhole model:
fx and fy are the focal lengths in pixels, u, v the pixel coordinates and cx, cy the principal point, so all three outputs are meters. Build intrinsic as o3d.camera.PinholeCameraIntrinsic(width, height, fx, fy, cx, cy); 319.5, 239.5 is the centered principal point of a 640 x 480 grid, and the worked example below uses 525.0 pixels for both focal lengths — replace all of it with your calibration. Points leave the call in the camera frame: x right, y down, z forward. When a rig delivers depth and color from separate sensors, choosing which camera's intrinsics deproject the cloud is a real decision, and the aligned depth intrinsics guide walks it.
The color and depth arrays must already be registered — same width, same height, one pixel grid — or colors land on neighboring points. If they are not, rectify first: the RGB and depth registration walkthrough shows the alignment step end to end.
A worked conversion you can verify
Synthetic input keeps the test hardware-free and the numbers checkable. Build a 640 x 480 pair: 640 × 480 = 307,200 pixels, depth as uint16 millimeters, color as uint8 RGB. Paint one 100 x 100 patch (columns 270 to 369, rows 190 to 289) at raw value 1500, and 1500 / 1000.0 = 1.5 m. Paint a second 50 x 50 patch, 2,500 pixels, at raw 4000 — 4.0 m, past depth_trunc = 3.0 m. Every other pixel stays at raw 0: no return.
import numpy as np
import open3d as o3d
width, height = 640, 480 # pixels
color = np.zeros((height, width, 3), dtype=np.uint8)
depth = np.zeros((height, width), dtype=np.uint16)
depth[190:290, 270:370] = 1500 # 1.5 m patch, 100 x 100
color[190:290, 270:370] = (255, 128, 0)
depth[400:450, 40:90] = 4000 # 4.0 m, past depth_trunc
intrinsic = o3d.camera.PinholeCameraIntrinsic(
width, height, 525.0, 525.0, 319.5, 239.5)
rgbd = o3d.geometry.RGBDImage.create_from_color_and_depth(
o3d.geometry.Image(color), o3d.geometry.Image(depth),
depth_scale=1000.0, depth_trunc=3.0,
convert_rgb_to_intensity=False)
pcd = o3d.geometry.PointCloud.create_from_rgbd_image(rgbd, intrinsic)
pts = np.asarray(pcd.points)
assert np.asarray(rgbd.color).shape == (480, 640, 3) # color survived stage one
assert len(pcd.points) == 10000 # 10,000 valid pixels
assert len(pcd.colors) == 10000
assert abs(pts[:, 0].max() - 0.14143) < 1e-3 # corner x in meters
assert np.allclose(np.asarray(pcd.colors)[0], [1.0, 0.502, 0.0], atol=1e-3)
The count check: 307,200 pixels minus the 10,000-point patch minus the 2,500 truncated pixels leaves 294,700 zero-depth pixels that produce nothing, so the cloud holds exactly 10,000 points and 10,000 colors. The coordinate check: the patch's outermost pixel sits at u = 369, v = 289, so X = (369 − 319.5) × 1.5 / 525 = 0.14143 m and Y = (289 − 239.5) × 1.5 / 525 = 0.14143 m. The color check proves the flag did its work: 128 / 255 = 0.502 in the green channel. Re-run with the default conversion and that last assertion fails, because every color comes back grey — the clearest demonstration of what convert_rgb_to_intensity does.
Common follow-on failures and their tests
Once the call runs, these are the failures behind most 'wrong' clouds, each with a one-line test.
-
Colors come out grey. The flag defaulted to True. Test: every red channel equals its green equals its blue. Fix: pass
convert_rgb_to_intensity=False. -
The scene ends at 3.0 m. That is the
depth_truncdefault, not your camera's range. Test:pts[:, 2].max()sits just under 3.0. Fix: setdepth_truncto the working distance, 10.0 for a 10 m cell. Holes past that are expected; holes inside it are a different diagnosis — the depth-hole checklist sorts them. -
Every distance is off by the same factor.
depth_scaledoes not match the camera's raw units. Quarter-millimeter data at the default 1000.0 reads a 2.0 m target as 0.5 m; the correct value is 4000.0. Test: one tape-measured flat plate. - Colors sit on the wrong points. The pair was never registered. Test: look for color fringing along the edges of a textured flat board. Fix: align before converting — the depth-preserving color alignment guide shows how to warp color without resampling depth.
-
A TypeError stops the call. Usually dtype or stride. Pass uint8 color (H, W, 3) and uint16 depth (H, W), wrapped as
o3d.geometry.Image(arr), and copy sliced views withnp.ascontiguousarrayfirst; the NumPy stride and dtype fix-up details the checks. - The numbers are right but the viewer looks wrong. Camera-frame axes put y down, so apply the flip from the tip above. If the point-cloud window itself stays empty while a color pane renders, the blank point-cloud view fix traces the display path.
Keep the rename from biting again
- Import the package as
import open3d as o3d, notfrom open3d import *. The star form hides which module served each name — exactly the confusion thecpu.pybindprefix exposed — and it collides with your own names. - Pin what you test:
open3d>=0.8in requirements keeps the class methods available, andprint(o3d.__version__)in your start-up log removes all doubt about which release is running. - Before porting an old tutorial, list the current factory methods:
[m for m in dir(o3d.geometry.RGBDImage) if 'create' in m]returnscreate_from_color_and_depthon any 0.8-or-later install. - Keep the synthetic-grid script in your test suite. It converts 10,000 points in well under a second on a CPU-only laptop and fails loudly if a dependency change moves an API again.
When the frames start coming from hardware instead of NumPy arrays, the 3D sensors category covers laser profilers, structured-light and ToF models, the camera finder filters them by type and series, and the 3D topic page collects the point-cloud fixes that pair with this one.
Questions
Which Open3D release renamed create_rgbd_image_from_color_and_depth?
Release 0.8. The module-level factories moved onto the classes as RGBDImage.create_from_color_and_depth and PointCloud.create_from_rgbd_image, and every release since 0.8 keeps those names. Code written against the class methods runs on any current install, while pre-0.8 tutorial snippets are the only code that still uses the old names.
Why does one machine report open3d.cpu.pybind and another open3d.cuda.pybind?
The package loads one compiled backend at import: the CUDA module when the build and driver are usable, the CPU module otherwise, including every machine without a CUDA-capable GPU. Both backends expose identical classes, so the prefix never changes what your code should call. One pip install cannot carry both; running them side by side means building Open3D from source.
What value should depth_scale have for my camera?
depth_scale is raw units per meter. A 16-bit depth image in millimeters takes the default 1000.0; a camera that reports quarter-millimeter steps needs 4000.0; float depth already in meters needs 1.0. Get it wrong by a factor of two and every distance in the point cloud is out by the same factor, so confirm one flat target against a tape measure before trusting the rest.
Why does my converted point cloud come out grey?
convert_rgb_to_intensity defaults to True, so the three color channels are collapsed to one intensity value before projection. Re-run with convert_rgb_to_intensity=False and the RGB triplets land in the cloud's colors array as floats from 0.0 to 1.0; check one with print(np.asarray(pcd.colors)[0]).
Why are red and blue swapped in my colored point cloud?
Open3D keeps whatever channel order you hand it. Most readers load frames with OpenCV, which stores BGR, so the cloud renders red where the part is blue. Convert with cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) before building the RGBDImage, or reverse the channel order in NumPy and pass a contiguous copy, then check a known red patch first.