Canvas

Azure Kinects calibration

HOST.onstartup.toe

Host: Intel NUC, TouchDesigner
Layer: sensing (back end)
In: 3 × Azure Kinect
Out: fused skeletons (OSC), camera images (NDI)

The Kinect host is the computer that the three Azure Kinect sensors are connected to. It runs a TouchDesigner project that reads each sensor, calibrates its data into the room's coordinate frame, decides which bodies seen by which sensors belong to the same person, fuses those views into one skeleton per person, and publishes the result to the lab network. Users never open it. The Azure Kinect sensors and Azure Kinect cameras components on the workstation are its clients. Keeping sensing on its own machine means the processing for depth, body tracking and video encoding does not compete with rendering on the main workstation. It also means every project in the lab receives the same calibrated data.

Role in the setup

The host turns three independent sensors into one room-wide tracking system. It does three things:

The host deliberately makes no choice on behalf of the user. It always sends everything. Which person and which joints a project uses is decided on the client side (see Azure Kinect sensors), so several projects or users can use the same stream at the same time.

Technical specification

Signal chain

Kinect left  ─► Kinect Azure CHOP ─► transformxyz_Y2  (pose left)  ─┐
Kinect right ─► Kinect Azure CHOP ─► transformxyz_Y19 (pose right) ─┼─► fusion (Script CHOP) ─► OSC Out ─► lab network
Kinect rear  ─► Kinect Azure CHOP ─► transformxyz_Y13 (pose rear)  ─┘

There is no selection, renaming or smoothing between these nodes. All of the decision making happens in one place: the Script CHOP fusion inside Sensor_Azure_Multiple_HOST, with its code in the DAT fusion_callbacks. Smoothing is done on the client, not here, so the host adds no delay.

Sensors

Each Kinect Azure CHOP runs Microsoft's body tracking for its own sensor. Relevant settings:

  • World Space Positions on. Joint positions in metres, relative to that sensor.
  • Bone Confidence on. Adds a :confidence channel per joint (see Fusion per joint).
  • Max Players. The number of bodies one sensor tracks. Each extra body costs GPU time on the NUC.

Per tracked body, a sensor outputs p{k}/id, and for each of its 32 joints p{k}/{joint}:tx/ty/tz/confidence. The k here is the sensor's own body number. It means nothing outside that sensor (see Decision making).

Calibration

The sensors hang on drop rods below the pipe grid, 3.03 m above the floor. All three face the centre wall and tilt down by about 45°.

SensorXYZRotate (Rx, Ry, Rz)
Left−1.253.034.4045°, 180°, 0°
Right1.253.034.4043.5°, 180°, 0°
Rear centre03.036.3145.87°, 180°, 0°

Each pose is stored in a Transform XYZ CHOP per sensor (transform order Scale Rotate Translate, rotate order Rx Ry Rz). The Transform CHOP turns that sensor's skeleton data from sensor coordinates into room coordinates. Calibration therefore comes down to these six values per sensor. If a sensor is moved, only its own Transform CHOP needs updating. The poses are also shown in the Lab model.

Calibration is what makes the decision making possible: only after this step can a position measured by one sensor be compared with a position measured by another. Any remaining calibration error shows up as the same person being placed in slightly different spots by different sensors. In a test walk (25 September 2026) that difference was 0.1–0.5 m, with the largest differences always involving the left sensor (about 0.3 m in X and 0.1 m in Y).

Decision making

The problem

Microsoft's body tracking runs separately on each sensor, and each sensor numbers the bodies it sees on its own. "Body 1" on the left sensor and "body 1" on the right sensor are unrelated labels. The same person usually has a different number on each sensor, and that number changes when they leave a sensor's view and come back.

An earlier version of the host worked around this by picking one sensor's data per moment ("switch phases" based on margins). That broke as soon as more than one person was in the room, or someone walked from one sensor's field of view into another's.

The host therefore ignores the sensors' own body numbers and decides identities itself, based only on where bodies are in the room. Every frame, fusion answers four questions:

  1. Which detections are real?
  2. Which detections belong to a person who is already known?
  3. Which of the remaining detections are new people, and are they real?
  4. For each joint of each person, which sensors should be trusted?

The answers are kept between frames as a list of tracks. A track is one person: their number (or none yet), their last fused pose, how many frames in a row they were seen, and how many frames they have been missed.

Step 1: valid detections

From each of the three inputs, every body slot is read. A body counts as a detection only if:

  • its id is greater than 0 (the Kinect reports 0 for an empty slot), and
  • it has a pelvis joint.

Positions can't be used to recognise an empty slot, because the calibration transform turns the sensor's zeros into the sensor's own position in the room.

The pelvis is used as the reference point for all comparisons in steps 2 and 3. It is the root of the skeleton, the most stable joint, and the one the Kinect rates with the highest confidence.

Step 2: known people first (track-first matching)

Every detection is first compared with every existing track, using the distance between the detection's pelvis and the track's pelvis from the previous frame. All pairs are sorted from closest to farthest and handed out in that order (a greedy nearest-neighbour assignment):

  • A pair is accepted if the distance is under MATCH_DIST = 0.75 m. That is far more than anyone moves in one frame, and less than the usual distance between two people.
  • One body per sensor per track. A sensor can't contribute two bodies to the same person.
  • One track per body. Once a detection is used, it's gone.
  • On equal distance, a confirmed person wins over a candidate that has no number yet.

Then a second pass catches sensors that disagree more because of calibration error: a detection that is still unused may join a confirmed person within MERGE_DIST = 1.20 m, as long as that person has no body from that sensor yet. Without this pass, a badly calibrated sensor would keep creating a second, ghost person next to the real one.

Why track-first. The first version did it the other way around: it grouped the sensors' bodies into people first, and compared those groups with the previous frame afterwards. That caused numbers to jump (p1 → p2 → p3) every time a sensor picked someone up or lost them. When a sensor joined, the combined position shifted, or a sensor that briefly disagreed formed its own group and "stole" the number. In the current order, a sensor joining or leaving only adds a body to an existing person or takes one away. It can never create a new number for someone who is already tracked.

Step 3: new people, with confirmation

Only detections that matched no track can start a new person. They are grouped first: a detection joins the nearest group whose average pelvis is within MERGE_DIST, provided that group has no body from that sensor yet. Each group becomes a candidate: a track without a number.

A candidate gets a number only after it has been seen in CONFIRM_FRAMES = 5 frames. A candidate missed for more than TENTATIVE_DROP = 2 frames is discarded. This filters out one-frame glitches, such as a coat on a chair briefly recognised as a body, so they never take a number or reach the clients.

Which number. A confirmed candidate gets the lowest free number from 1 to MAX_SLOTS = 8. The first person in an empty room is always p1. That way a client set to Person 1 always gets someone as long as anyone is in the room. When a person leaves, the others keep their numbers: nobody is renumbered, and the free number goes to the next person who enters.

Step 4: keeping and releasing people

A confirmed person who isn't matched in a frame keeps their last fused pose and their number. They are released only after DROP_FRAMES = 15 frames in a row without a match (about 0.5 s at the host's 30 fps). A short occlusion, such as walking behind another person, therefore doesn't cost anyone their number.

Step 5: fusion per joint

For each person, every joint is fused from the bodies attached to that person in this frame. The Kinect rates every joint: 0 = none (out of range), 1 = low (occluded, predicted), 2 = medium. The decision per joint:

  1. Find the best confidence level any of the sensors reports for this joint.
  2. Use only the sensors at that level, and ignore the rest.
  3. Take their weighted average (weights: level 2 = 1.0, level 1 = 0.3).

If every sensor rates a joint 0, it is still averaged across all sensors rather than frozen. This matters for hands: the Kinect very often rates hand, hand tip and thumb as 0, even when the hand is clearly visible. An earlier version kept the last known position for such joints, and hands froze in place (in one test 2.5 m behind the body).

Output

Only confirmed people are sent, in number order:

ChannelContent
p{N}/{joint}:tx, :ty, :tzevery joint of every confirmed person, metres, room frame
p1/present … p8/present1 while that number belongs to someone, otherwise 0. Always sent, also as 0.
personshow many people are seen in this frame
heartbeata counter that changes every frame while the host runs

The presence flags and the heartbeat exist because of how OSC behaves on the receiving side: a channel that stops arriving keeps showing its last value forever. Without them, a client can't tell a person who left from one standing still, or a crashed host from a quiet room.

Timing at a glance

EventTakes
Someone enters the empty roomabout 5 frames (0.17 s) until p1 appears
A second sensor picks up a tracked personimmediately, same number
A person is briefly hiddenkeeps number and last pose for up to 15 frames (0.5 s)
A person leavesnumber released after 0.5 s, present goes to 0
A one-frame false detectiondiscarded, never gets a number

Worked example

Two people, A and B, in an empty room:

  1. A walks in on the left. The left sensor sees one body. No tracks exist, so it becomes a candidate. After 5 frames it becomes p1.
  2. A walks to the middle. The rear sensor now also sees A, 0.3 m from where the left sensor places A. In step 2 that body is within 0.75 m of p1, so it simply joins p1. A's pelvis is now fused from two sensors.
  3. B enters on the right, 3 m from A. The right sensor's body is far from p1 (no match), so it becomes a candidate and, 5 frames later, p2.
  4. A leaves. After 15 frames without a match, p1 is released and p1/present goes to 0. B stays p2.
  5. C enters. C gets the lowest free number: p1.

Tuning and diagnosis

All constants are at the top of fusion_callbacks:

ConstantDefaultRaise it when…Lower it when…
MATCH_DIST0.75 mfast movement makes people get new numberstwo people close together swap numbers
MERGE_DIST1.20 mone person shows up twicetwo people standing close are merged; possible once the spread stays under 0.25 m (then about 0.6 m)
CONFIRM_FRAMES5glitches still take numbersnew people appear too late
DROP_FRAMES15people lose their number behind otherspeople who left linger too long
MAX_SLOTS8——

With DEBUG = True, the Textport prints once per second how many bodies each sensor sees and every track as (number, frames missed), where 0 means a candidate without a number. For every person seen by more than one sensor, it also prints where each sensor puts their pelvis and the spread between them: a direct measurement of the calibration. Under about 0.3 m is good. Regularly above 0.5 m means a Transform needs re-measuring; the line shows which sensor differs.

Why this design

  • Identities come from positions in the room, not from any single sensor. That makes the method independent of the hardware: the same code works with Azure Kinects or with a successor such as an Orbbec sensor (see Decision making: sensor succession).
  • Three overlapping views mean fewer dropouts when one sensor's view is blocked, and confidence-based fusion takes each joint from the sensors that see it best.
  • Track-first matching keeps numbers stable while people walk between sensor views.
  • The host never filters for a user. Everything is sent; clients choose. One stream serves any number of projects.

Network interface

All traffic stays on the lab's local network. Commands from the workstation are sent as broadcasts, so the components don't need to know the host's IP address.

DirectionProtocol / portContent
host → workstationOSC, UDP 1030every joint of every person (p{N}/{joint}:tx/ty/tz), p{N}/present, persons, heartbeat
host → workstationOSC, UDP 1031tracking frame rate
host → workstationNDIone video stream per sensor, named after the host and the sensor: <host> (AK1), (AK2), (AK3)
workstation → hostOSC broadcast, UDP 1051Configure (re-initialises the sensors), sent from Azure Kinect sensors
workstation → hostOSC broadcast, UDP 27Image, the stream type chosen in Azure Kinect cameras

What the client does with this stream (choosing a person and joints, detecting stale data, smoothing, sharing one receiver between copies) is described in Azure Kinect sensors.

Decision making: sensor succession

The Azure Kinect DK went out of production in 2023, and an Orbbec sensor is on the lab's purchase planning for 2026. The options discussed at the workshop meeting of 24 September 2026:

OptionAssessment
Orbbec Femto Bolt (×3) with a sync hubThe lower-risk choice for skeleton tracking. It works the way the Azure Kinect does, so the host and its fusion stay as they are.
Active USB-C cables (e.g. Newnex FireNEX-uLINK-C2)Carry USB 3.2 Gen 1 data and bus power up to 30 m, which is enough for the ceiling-grid mounting. Some users report the host not recognising a camera until it is restarted or moved to another port, so the exact cables should be tested before buying three.
PCIe USB card with one controller per portSeparate USB ports on a PC can share one host controller inside, which limits how many cameras run at once. A card with a controller per port has been verified with four Femto Bolts.
Orbbec Femto Mega (Ethernet)Only worth considering after testing Ethernet body tracking in TouchDesigner with one borrowed unit.

Working recommendation: three Femto Bolts, a sync hub, three tested active USB-C cables and a PCIe USB card with a separate controller per port, on a Windows host. Because the fusion works on room coordinates, swapping the sensors changes nothing for the clients or for projects built on them.

Limitations and open questions

Relations