A drawing dimensions the angle across a bend, a V-groove's opening, or a chamfer to a face — and your 3D camera hands you a point cloud, not angles. The answer, up front: fit a plane to each surface, take the two unit normals, and compute θ = arccos(n1 · n2). For normals (0, 0, 1) and (0.707, 0, 0.707) the dot product is 0.707, so θ = 45°.
Most machine-vision libraries fit planes readily but ship no single operator for the angle between two of them, so the computation falls to you. It is short: two plane fits, two unit normals, one dot product. This guide gives you the formula, the sign convention that decides whether you report 30° or 150°, paste-ready code, an accuracy budget in degrees, and the sensor and controller choices that make the number trustworthy on the line.
How do you calculate the angle between two planes from 3D data?
Fit a plane to each surface's points, take the two unit normal vectors, and compute arccos of their dot product: for normals (0, 0, 1) and (0.707, 0, 0.707) the dot product is 0.707, so the angle is arccos(0.707) = 45°.
An angle between planes is really an angle between directions, so where the surfaces sit in space never enters the number — only their tilt does. That is why the result survives part translation and rotation unchanged, and why two patches measured at different heights still compare cleanly. Once each normal is unit length, the whole computation is one multiply-add per axis and one arccos; if your vision library fits planes but stops there, the missing operator is the three lines shown in the code section below.
Why does the dot product of the normals give the angle between the planes?
Because the angle between two planes is defined as the angle between their normals, and the dot product of two unit vectors equals the cosine of the angle between them: n1 · n2 = cos θ, so θ = arccos(n1 · n2), a value between 0° and 180°.
With unit normals:
The third line is the general form: when the fitting code returns normals of arbitrary length, divide the dot product by both lengths before the arccos. Keeping everything unit-length from the start makes the first line the only line you ever run.
How do you get each unit normal from a point cloud?
Segment the two planar regions, fit a plane to each by least squares or RANSAC — the fit returns z = a·x + b·y + c — then normalize the normal as n = (−a, −b, 1) / √(a² + b² + 1), which makes its length exactly 1.
Least squares is the default on a clean, segmented patch; RANSAC earns its keep when edge points or neighbouring faces survive segmentation. The trade-offs are worked through in Plane fitting on 3D point clouds: least squares vs RANSAC, and Segment planes and cylinders in a point cloud covers the split when both faces arrive as one region.
Normalizing in practice: for a fit with a = 0.05 and b = −0.10, the divisor is √(1 + 0.0025 + 0.0100) = √1.0125 = 1.0062, so n = (−0.0497, 0.0994, 0.9938). That patch is tilted arctan(√(a² + b²)) = arctan(0.1118) = 6.4° from the map plane.
Do you report the normal angle, the acute angle, or the opening angle?
arccos of the dot product returns the angle between the normals, anywhere from 0° to 180°; with both normals pointing outward from the material, the opening measured through the material side is 180° minus that value.
Worked example — bent bracket. The top flange is horizontal, so n1 = (0, 0, 1). The second flange rises 30° from horizontal, with outward normal n2 = (0, −0.5, 0.866). The dot product is 0×0 + 0×(−0.5) + 1×0.866 = 0.866, and arccos(0.866) = 30°. A drawing that dimensions the bend through the material side calls the same corner 180° − 30° = 150°. Same measurement, two reports — agree on the convention before the first part runs.
| Quantity you report | Formula | Bent-bracket value |
|---|---|---|
| Angle between the normals | arccos(n1 · n2) | 30° |
| Acute angle between the planes | min(θ, 180° − θ) | 30° |
| Opening through the material side | 180° − θ with outward normals | 150° |
A V-groove behaves the same way from the other side: faces forming a 120° opening have outward normals 60° apart — normals (−0.5, 0.866, 0) and (0.5, 0.866, 0) dot to 0.500, and arccos(0.500) = 60° — so report 180° − 60° = 120° when the drawing dimensions the opening. For a full pipeline from raw depth to a reported bend angle, see Measure distance and bend angle from depth point clouds in Python.
How do you compute the angle in code?
With unit normals the computation is one dot product, one clamp against rounding error and one arccos — and the test below prints 45.0 for normals (0, 0, 1) and (0.7071, 0, 0.7071).
import numpy as np
n1 = np.array([0.0, 0.0, 1.0]) # unit normal, plane 1
n2 = np.array([0.70710678, 0.0, 0.70710678]) # unit normal, plane 2
dot = float(np.dot(n1, n2)) # 0.70710678
dot = max(-1.0, min(1.0, dot)) # guard float rounding at +/-1
angle_deg = float(np.degrees(np.arccos(dot)))
print(round(angle_deg, 3)) # 45.0 <- test passes
# better-conditioned over the whole 0-180 deg range
angle_deg = float(np.degrees(
np.arctan2(np.linalg.norm(np.cross(n1, n2)), np.dot(n1, n2))))
print(round(angle_deg, 3)) # 45.0 <- agrees
If your vision software fits planes and returns normals but ships no angle operator, this is the entire missing piece: take the two normal vectors and run the same dot, clamp and arccos in whatever language you script in. The atan2 form needs no clamp and stays accurate where arccos loses resolution; the failure-mode section below puts a number on that.
How accurate is an angle measured from real data?
With 0.05 mm height noise, a 40 mm patch and 1,000 fit points, the random part of the corner error is about 0.011°, so a working budget of a few hundredths of a degree is realistic — and tighter budgets buy their accuracy with longer patches.
The estimate comes from least-squares slope statistics. For one planar patch of length L, per-point noise σ and N points spread uniformly, the tilt error of the fitted plane is approximately:
Put numbers in: σ = 0.05 mm, L = 40 mm, N = 1000. Then √12 = 3.464 and √N = 31.62, so one plane contributes 0.05 × 3.464 / (40 × 31.62) = 0.000137 rad = 0.0078°, and the two-plane corner gives √2 × 0.0078° = 0.011°. Treat that as the statistical floor: calibration, scan straightness and thermal drift add systematic error on top, so prove the real budget by measuring an angle gauge block of known value — and halve the random part by doubling the patch length or quadrupling the point count.
When does the arccos answer go wrong?
Four failures produce most bad angle numbers: normals that were never normalized, a sign that flips one normal, a fit that runs across a fillet or a bow, and noise amplification from arccos near 0° and 180°.
- Un-normalized normals. A normal left at length 2 doubles every dot product and can push it outside arccos's domain of [-1, 1]; a silent clamp then hides the bug and the result reads 0° or 180°. Check √(a² + b² + c²) = 1 before believing any output.
- Sign flips. Plane fitting returns whichever sign the solver chose, and negating one normal turns θ into 180° − θ — a 30° corner reports as 150°. When your number is exactly the supplement of the drawing value, one normal is flipped; negate it or take 180° − θ.
- Fitted curvature. Fillet and blend regions swing the normal across their arc and bias both fits toward each other, shrinking the reported angle. Exclude a band of one fillet radius plus a few point spacings around the tangent lines and fit only the flat land. A bow across a whole face is a different defect — Point cloud plane curvature: diagnosing a bowed flat surface reads that from the fit residuals.
- Near-parallel planes. arccos is flat near u = 1, so tiny dot errors become large angle errors. Dot values of 0.99975 and 0.99995 — 0.0002 apart — correspond to 1.28° and 0.57°, a swing of about 0.35° per 0.0001; at a 45° corner the same 0.0001 moves the result only 0.008°. Use the atan2 form from the code section whenever expected angles sit near 0° or 180°.
Which hardware captures two planes well enough?
Any 3D sensor that returns calibrated X, Y, Z for both faces will serve — a laser profiler scanning across the bend for single-sided parts, a stereo or structured-light camera when both faces must be captured in one shot, and a vision controller to run the fits inline.
For close-range, small patches — a chamfer, a small boss, a narrow land beside a bend — a compact profiler is the right tool:
- Type
- Laser profiler
For larger parts, longer scan strokes or higher point density on each face, step up within the same profiler family:
- Type
- Laser profiler
Inline, the two plane fits and the arccos run on a controller that also drives the lights and exchanges results with the PLC:
- LAN ports
- 5+ LAN ports
- USB 3 ports
- 3-4 USB 3 ports
- PoE
- PoE camera ports
- Light outputs
- 8 built-in channels
Size each patch from the accuracy budget, not from the part outline: the error scales with σ/L, so doubling the fitted length across a face halves the random error, and the point count only helps while the points stay spread over the full flat land.
Browse the 3D sensors range together with the vision controllers they pair with — the finder pages filter by type, series, LAN ports and PoE. The 3D vision topic page collects the related point-cloud geometry guides. If you want a second opinion on the budget before ordering, send the drawing angle and a sample scan through the contact page and we will work through it with you.
Questions
Can I compute the angle from two profile scans instead of a full point cloud?
Yes, when each scan line crosses both faces perpendicular to the bend line. Fit a straight line to each face in the profile with slopes m1 and m2, and the dihedral follows from tan θ = (m2 − m1) / (1 + m1·m2). One good profile per part can be enough, but fixture the part so the scan crosses the bend at 90°: a skewed scan reads projected slopes and underestimates the angle.
How close to the bend line can the fit points come?
Stop at the fillet. Exclude a band of one fillet radius plus two or three point spacings on each side of the tangent line — for a 2 mm fillet with 0.02 mm point spacing, that means no fit points within about 2.06 mm of it. Points inside the fillet bend both fitted planes toward each other and shrink the reported angle, by several tenths of a degree or more on tight radii.
How many points does each plane need?
Enough that the random error fits the budget: in the 0.05 mm, 40 mm example, 1,000 points give about 0.011° and 100 points about 0.035°, because the error falls as 1/√N — four times the points halve it. Spread the points over the whole flat land rather than clustering near the bend; clustering inflates the error the same way a short patch does.
Does the same method work on a 2.5D height map?
Yes. A height map is a point cloud on a regular grid, so fit z = a·x + b·y + c to each region exactly as before; each region's tilt from the map plane is arctan(√(a² + b²)), and the angle between two regions still comes from the dot product of the two normalized (−a, −b, 1) vectors. The grid also keeps point spacing uniform, which the error budget assumes.
How do I verify the measurement against a physical reference?
Measure an angle gauge block of known value — 30°, 45° and 60° pieces are common — fixtured like a production part, and repeat ten times; agreement within a few hundredths of a degree up to about 0.1° is healthy. Also rotate the part 180° about the viewing axis and remeasure: a rigid rotation cannot change the true dihedral, so any shift in the reading exposes a systematic error in fixture, calibration or segmentation.


