3D visionTroubleshooting

Why exported point clouds differ from the viewer render

Why the exported point cloud differs from the viewer render: FOV overlap, clamped texture and zero-depth points, with tests and filters to make the PLY match.

LumaSpec Vision10 min read Share on LinkedIn

On this page 11 sections
  1. 01The render and the export are two pipelines
  2. 02Cause 1: the color camera sees less than the depth sensor
  3. 03Test: count the vertices the viewer never drew
  4. 04The fix: discard the rim, or crop to the intersection
  5. 05Cause 2: zero-depth pixels exported at the origin
  6. 06Cause 3: the filters ran on screen but not in the file
  7. 07Match your symptom in one minute
  8. 08When it is not the rim
  9. 09Hardware that removes the mismatch at the source
  10. 10Checks before you trust any export
  11. 11Questions

You export a point cloud from the 3D camera's viewer, open the PLY in a mesh editor such as MeshLab, and it looks nothing like the render: streaks of wrong color reaching in from the left and right edges, a dense spike at the origin, and thousands of points you never saw on screen. The camera did not change and the file is not corrupt. The viewer shows you less than the exporter writes: it draws only the region where depth and color overlap and drops invalid points, while the export path writes every depth pixel it is handed. This guide walks the three causes in order of likelihood, the test that isolates each one, the filter that makes the file match the render, and the checks to run before you trust any export.

The render and the export are two pipelines

Nothing in your set-up broke. A depth-plus-color camera is two sensors behind two lenses, and the viewer and the exporter treat their footprints differently. The viewer's GPU path rasterizes each depth point and discards any fragment whose color lookup lands outside the color image, so what you see on screen is the depth–color intersection — a narrower field of view with clean texture. The exporter walks the depth frame and writes one vertex per pixel, using software that samples the color image with ordinary addressing rules. Where the depth pixel has no color counterpart, those rules still return a color: the edge pixel, repeated. Same camera, same frames, two different results. Three differences follow, ranked by how often they explain the file you are looking at.

Cause 1: the color camera sees less than the depth sensor

In a stereo or structured-light module the depth field of view is usually wider than the color one; a common split is 90° × 58° for depth against 70° × 42° for color. Sensor pixels are linear in the tangent of the angle, not the angle itself, so the fraction of the depth frame the color image covers is:

coverage = tan(θ_color / 2) / tan(θ_depth / 2) coverage_area = coverage_width × coverage_height

Run the numbers for that split:

  • Width: tan(70° / 2) / tan(90° / 2) = 0.700 / 1.000 = 0.700, so 70% of the columns.
  • Height: tan(42° / 2) / tan(58° / 2) = 0.384 / 0.554 = 0.693, so 69% of the rows.
  • Area: 0.700 × 0.693 = 0.485 — under half the depth pixels have a real color source.

On a 1280 × 720 depth frame that is 896 of 1,280 columns, blind for 192 columns on each side, and 499 of 720 rows. Every depth pixel in that blind rim still receives a color in the PLY: with clamped texture addressing the sampler returns the last valid column forever, which paints the stretched streaks; with wrapped addressing you get a band of unrelated colors. The viewer discards those fragments before they reach the screen, so the artifact waited for you in the file.

Test: count the vertices the viewer never drew

Load the PLY anywhere you can script it — Python with numpy, or the filtering tools of a mesh editor — and count two things. First, vertices whose normalized texture coordinates u/w and v/h fall outside the range 0 to 1; with the split above, that is about 51% of the points, concentrated on the left and right margins. Second, vertices where x = y = z = 0. If the out-of-range texture count runs to tens of percent and sits on the margins, cause 1 is confirmed and you can stop hunting for calibration errors.

The fix: discard the rim, or crop to the intersection

The filter itself is one line per coordinate:

u = uv[:, 0] / color_w        # normalized texture column
v = uv[:, 1] / color_h        # normalized texture row
in_frame = (u >= 0) & (u <= 1) & (v >= 0) & (v <= 1)
not_zero = ~np.all(xyz == 0, axis=1)
cloud, colors = xyz[in_frame & not_zero], rgb[in_frame & not_zero]

For the mechanics of hunting bad texture coordinates in large clouds, see filtering invalid texture coordinates in a point cloud; for the registration math that produces u and v in the first place, registering RGB and depth images for a colored point cloud works through it.

Then decide what the file is for. To match the render, keep the filter: with the numbers above you keep 447,104 of 921,600 pixels (896 × 499), which is the intersection the viewer was showing you all along. To keep the full depth footprint, the rim has no color source — write it black or flag it, and never let the sampler clamp without reporting it, because streaks of repeated border pixels poison color-based segmentation far more than honest black does.

Cause 2: zero-depth pixels exported at the origin

Depth the sensor could not resolve — glare, out-of-range surfaces, dark or mirror-like parts — arrives as the value 0, and a raw exporter deprojects it anyway, producing a vertex at (0, 0, 0). The viewer discards invalid depth before drawing, so the mesh editor is the first place you see the resulting ball or spike at the origin, often the densest cluster in the file. A frame with 5% invalid depth carries about 46,000 of these vertices. The test is one count; the fix is the not_zero mask above. If the holes themselves matter to the inspection, missing points in a 3D camera point cloud attacks the capture-side causes, and depth pixel deprojection zero traces how the zero vertices arise.

Cause 3: the filters ran on screen but not in the file

Viewers apply decimation, spatial and temporal filters to what they draw; a raw export path often writes the untouched frame. The tell is the vertex count: a 1280 × 720 frame exported raw carries exactly 921,600 vertices, while the 2 × 2 decimation you watched on screen corresponds to about 230,400. Run the same filter chain on the frames, then export from that block.

Writing several hundred thousand vertices has its own cost on embedded boards; speeding up slow PLY point cloud export covers the buffered writer.

Match your symptom in one minute

Symptom in the saved file Cause One-minute test Fix
Color streaks stretched in from the left and right margins Color field of view narrower than depth; texture clamped at the border Count u and v outside 0 to 1; expect about 51% with the 90° / 70° split Discard out-of-range texture, or crop to the intersection
Dense spike or ball at the origin Invalid depth written as (0, 0, 0) Count vertices with x = y = z = 0 Drop them; recover the holes at capture time if they matter
Far more vertices than the render showed Decimation and spatial filters ran on screen only Compare the vertex count with the filter setting Put the same filters in the export path
Whole cloud displaced, mirrored or uniformly miscolored Registration, extrinsics or coordinate frame Check whether the depth stream is aligned to color at all Follow the registration and frame checks below

When it is not the rim

If the whole cloud is wrong — geometry shifted by the mounting, axes mirrored, color offset everywhere rather than at the margins — the field-of-view explanation does not apply. First check whether the depth stream is aligned to color before export: is depth image_raw aligned to RGB? covers the check. Depth points arrive in the depth sensor's optical frame, so a tool that assumes the camera frame applies the wrong rotation; transforming depth point clouds between the optical and camera frame shows the fix. And if the viewer and your export script run different SDK versions, align them first — 3D camera header version mismatch lists the failure signs.

Hardware that removes the mismatch at the source

If every point needs true color, the color camera's field of view must be at least as wide as the depth one. For a rectilinear lens that sets a ceiling on focal length:

f <= (sensor_width / 2) / tan(θ_depth / 2)

Covering a 90° depth field of view takes f ≤ 3.2 mm on a 1/2-inch sensor (6.4 mm wide) and f ≤ 4.4 mm on a 2/3-inch sensor (8.8 mm wide). The lens focal length calculator runs the same arithmetic for whatever sensor you mount.

Lens focal length, field of view and working distance
Units
Solve for
Focal length
– mm
Field of view
– mm
Working distance
– mm
Magnification
– x
Angle of view
– °
Pixel on the part
– mm

Thin-lens model: m = sensor width / field of view, f = WD × m / (1 + m). Accurate to a few percent above about 150 mm; check the lens's minimum object distance.

Find lenses with this focal length Open the full calculator

A C-mount color body accepts the lens that number calls for, and the area scan cameras finder filters by mount, sensor format and interface.

Area scan camerasHikrobot MV-CS200-10GC 20 MP Area Scan Camera, GigE, IMX183, Color, BasicMV-CS200-10GC
Sensor format
1"
Lens mount
C-mount
Interface
GigE
Resolution
16-25 MP
$739.00View partBuild a system with it

On a 1-inch sensor, 12.8 mm across, the same 90° coverage needs f ≤ 6.4 mm — wide, but available in C-mount.

The other route is to stop building a two-sensor rig. Projected structured-light and ToF units in 3D sensors deliver the cloud from one housing on one optical axis, so there is no second footprint to reconcile and one specification states the coverage. When a full station is on the table, 3D camera working distance and positioning settles where that axis sits.

3D sensorsHikrobot MV-DPS200P-02 Projected structured-light 3D cameraMV-DPS200P-02
Type
Stereo / structured light
$8,829.00 · quotedView and quote
3D sensorsHikrobot MV-DT01WMU ToF sensing cameraMV-DT01WMU
Type
Depth / ToF camera
Price on requestView and quote

If the unit you choose carries a laser-classed emitter, treat it as a laser product: block the beam whenever a person is in the scanning plane, and never look into the aperture.

Checks before you trust any export

  • The vertex count equals the pixel count times your decimation, not the raw pixel count by accident.
  • No vertices remain at (0, 0, 0) after filtering.
  • The out-of-range texture count matches one minus the area coverage from the field-of-view numbers.
  • The file's bounding box matches the render, with no origin spike pulling a corner to zero.
  • Colors in the outermost columns differ from their neighbours rather than copying the last valid column.
  • Keep one known-good PLY and diff new exports against it after every SDK update.

Planning a new capture station rather than repairing an export? The system configurator walks camera, lens, light and controller in order, and the contact page puts the odd cases in front of an applications engineer.

Questions

Does exporting to OBJ or CSV instead of PLY avoid the texture problem?

No. The format is not the cause; the rim outside the color field of view is. OBJ stores the same lookups as UV coordinates, so the bad ones appear as UVs outside the unit square instead of visible streaks, and CSV simply carries the clamped RGB values. Filter the points first, then pick the format your downstream tool reads.

Should I align depth to color or color to depth before exporting?

Aligning depth to color crops the depth data to the color field of view: you lose the rim permanently, but every exported pixel has true color. Aligning color to depth keeps the full depth footprint and leaves the rim without a color source. Choose the first when color feeds segmentation or measurement; choose the second when the rim geometry matters more than its appearance.

Why do the streaks look like stretched copies of the image border?

Clamped texture addressing never fails a lookup: past the last column it returns that last column, forever. Each successive depth pixel in the blind rim maps further beyond the border and receives the same edge color, so one pixel column paints across hundreds of points. Wrapped addressing wraps around instead and shows as a band of unrelated colors rather than streaks.

Is the missing rim color stored anywhere in the file, waiting to be recovered?

No. The geometry of the rim is in the PLY, but the color was never captured: those scene points fell outside the color camera's field of view, so no pixel ever saw them. Nothing in the file can restore it. Color over the full footprint means recapturing with a wider color lens or adding a second color camera.

Why does the vertex count still differ from the render after filtering?

Temporal filtering averages several frames, so the viewer shows points that no single frame contains and a one-frame export cannot reproduce them. Count also the points removed by spatial filtering and by the origin filter, and remember the viewer culls occluded points while rasterizing. Compare like with like: export exactly the frame set the viewer last drew.