Controlling RGB Exposure and Gain on Kinect for Xbox 360 Model 1473 with libfreenect
1. Overview
Current versions of libfreenect provide:
freenect_set_flag(dev, FREENECT_AUTO_EXPOSURE, FREENECT_OFF);
freenect_set_exposure(dev, time_us);However, on at least one real Kinect for Xbox 360 Model 1473, this path does not actually lock the RGB exposure.
The reason is important:
- Current
libfreenectcontrols exposure through Kinect command0x95, which directly accesses RGB CMOS registers. FREENECT_AUTO_EXPOSUREmodifies CMOS register0x0106.freenect_set_exposure()writes CMOS register0x0009.- This approach was successfully tested by the upstream manual-exposure work on a Model 1414, but it does not work correctly on the tested Model 1473.
Reverse engineering of Microsoft Kinect SDK 1.8 shows that its camera-control stack uses a different protocol for exposure:
Kinect proprietary command: 0x96
0xCC00 Auto/manual exposure mode
0xCC04 Exposure value, high 16 bits
0xCC06 Exposure value, low 16 bits
0xCC0E GainOn the tested Model 1473, these commands were accepted by the device, read back correctly, and caused the RGB image brightness to change as expected.
2. What Was Verified
The test device presented its RGB camera as:
VID:PID = 045e:02aeMicrosoft Kinect SDK 1.8 normally refuses to expose INuiColorCameraSettings for this device:
NuiGetColorCameraSettings -> 0x8301000F0x8301000F is E_NUI_HARDWARE_FEATURE_UNAVAILABLE.
Reverse engineering showed that this rejection is performed in Kinect10.dll before any USB request is sent.
After bypassing the SDK's software-only device-type checks for research purposes, the SDK eventually reached its real camera-control implementation. A USB capture showed that kinectcamera.sys translated those operations into Kinect proprietary GM command 0x96 requests.
The following operations were observed on the real Model 1473.
Disable automatic exposure
The important state is:
control 0xCC00 = 1Observed meanings:
0xCC00 = 1 -> manual exposure
0xCC00 = 2 -> automatic exposureThe device was subsequently read back and returned 1.
Set exposure to 5 ms
The requested exposure was:
5000 usThe wire representation was:
5000 / 100 = 50 = 0x0032The controls became:
0xCC04 = 0x0000
0xCC06 = 0x0032The device returned 0x0032 when read back.
Set exposure to 20 ms
The requested exposure was:
20000 usThe wire representation was:
20000 / 100 = 200 = 0x00C8The controls became:
0xCC04 = 0x0000
0xCC06 = 0x00C8The device returned 0x00C8 when read back.
The captured RGB data also changed at the same time as the exposure commands. Under unchanged scene conditions, the average captured RGB payload level moved roughly:
before change: ~130
5 ms:
~101
20 ms:
~169This is strong evidence that the real RGB sensor exposure changed, not merely a cached software property.
Set gain to 1x, 2x, 4x, 8x, and 16x
A second Model-1473 experiment used the same patched Microsoft SDK path to exercise INuiColorCameraSettings::SetGain().
The device accepted the following values through control 0xCC0E:
SDK Gain Raw 0xCC0E value
-------- ----------------
1.0 0x0020
2.0 0x0040
4.0 0x0080
8.0 0x0100
16.0 0x0200The mapping is therefore:
raw_gain = gain * 32Each write was followed by a readback from the device, and the returned raw value matched the requested value.
The captured RGB payload brightness also increased monotonically with gain under approximately unchanged scene conditions:
Gain 1x ~171
Gain 2x ~192
Gain 4x ~208
Gain 8x ~225
Gain 16x ~241This confirms that 0xCC0E changes the real RGB sensor gain rather than only changing an SDK-side cached property.
Gainshould be treated as a sensor gain multiplier, not as a calibrated photographic ISO value. Do not labelGain = 4asISO 400unless you have independently calibrated that relationship.
3. Why the Existing libfreenect Exposure API Fails on 1473
At the time of writing, the relevant upstream libfreenect implementation is essentially:
int freenect_set_exposure(freenect_device *dev, int time_us)
{
uint16_t cmos_value = 0;
/* conversion omitted */
return write_cmos_register(dev, 0x0009, cmos_value);
}The automatic-exposure flags are also implemented by reading and modifying:
CMOS register 0x0106The CMOS helper uses:
Kinect command 0x95Conceptually:
libfreenect
|
+-- command 0x95
|
+-- direct CMOS register access
|
+-- 0x0106 auto-exposure bits
+-- 0x0009 shutter widthThis path is not reliable on the tested Model 1473.
The Microsoft driver instead uses:
application
|
v
Kinect10.dll
|
v
DeviceIoControl
|
v
kinectcamera.sys
|
v
Kinect command 0x96
|
+-- 0xCC00
+-- 0xCC04
+-- 0xCC06
+-- 0xCC0EThe important conclusion is:
For Model 1473, use the
0x96 / 0xCCxxcamera-control path for manual exposure and gain instead of relying on the existing0x95CMOS exposure path.
4. Understanding libfreenect's send_cmd()
libfreenect already contains the transport required to send the newly discovered command.
In current src/flags.c, send_cmd() builds this header:
typedef struct {
uint8_t magic[2];
uint16_t len;
uint16_t cmd;
uint16_t tag;
} cam_hdr;The outgoing magic is:
47 4D = "GM"The response magic is:
52 42 = "RB"The transport uses USB vendor control transfers:
Host -> Kinect: bmRequestType = 0x40
Kinect -> Host: bmRequestType = 0xC0Therefore, no new libusb transport layer is required. The easiest implementation is to add Model-1473 camera-control helpers next to send_cmd() in src/flags.c.
5. Reverse-Engineered Command 0x96 Format
All 16-bit words are little-endian.
The capture revealed four useful forms.
5.1 Set an 8-bit camera control
Used for auto/manual exposure mode:
uint16_t cmd[5] = {
0x0002,
0xFF80,
control,
0xFF81,
value
};Send with:
send_cmd(dev, 0x96, cmd, sizeof(cmd), ...);For manual exposure:
control = 0xCC00
value = 0x0001For automatic exposure:
control = 0xCC00
value = 0x0002An observed manual-exposure request looked like:
47 4D 05 00 96 00 TT TT
02 00 80 FF 00 CC 81 FF 01 00TT TT is the normal libfreenect command tag and changes for every request.
5.2 Read an 8-bit camera control
Observed request payload:
uint16_t cmd[3] = {
0x0001,
0xFF83,
control
};For example:
control = 0xCC00The returned value was found in the fourth 16-bit response word after send_cmd() removes the RB header.
5.3 Set a 16-bit camera control
Observed request payload:
uint16_t cmd[3] = {
0x0001,
control,
value
};Examples:
control = 0xCC04
value = high 16 bits
control = 0xCC06
value = low 16 bitsFor 5 ms:
01 00 06 CC 32 00For 20 ms:
01 00 06 CC C8 005.4 Read a 16-bit camera control
Observed request payload:
uint16_t cmd[3] = {
0x0001,
0xFF82,
control
};The returned value is again present in the response data.
6. Recommended libfreenect Implementation
The following implementation is deliberately kept separate from the existing 0x95 functions so that 1414 behavior is not changed.
Add these helpers to src/flags.c, where send_cmd() and the endian conversion helpers are already available.
#define K1473_CMD_CAMERA_CONTROL 0x0096
#define K1473_CTRL_EXPOSURE_MODE 0xCC00
#define K1473_CTRL_EXPOSURE_HIGH 0xCC04
#define K1473_CTRL_EXPOSURE_LOW 0xCC06
#define K1473_CTRL_GAIN 0xCC0E
#define K1473_EXPOSURE_MANUAL 0x0001
#define K1473_EXPOSURE_AUTO 0x0002
static int k1473_set_u8_control(
freenect_device *dev,
uint16_t control,
uint16_t value)
{
uint16_t cmd[5];
uint16_t reply[5] = {0};
cmd[0] = fn_le16(0x0002);
cmd[1] = fn_le16(0xFF80);
cmd[2] = fn_le16(control);
cmd[3] = fn_le16(0xFF81);
cmd[4] = fn_le16(value);
int res = send_cmd(
dev,
K1473_CMD_CAMERA_CONTROL,
cmd,
sizeof(cmd),
reply,
sizeof(reply));
if (res < 0)
return res;
if (res < 2 || fn_le16(reply[0]) != 0x0000)
return -1;
return 0;
}
static int k1473_get_u8_control(
freenect_device *dev,
uint16_t control,
uint16_t *value)
{
uint16_t cmd[3];
uint16_t reply[4] = {0};
cmd[0] = fn_le16(0x0001);
cmd[1] = fn_le16(0xFF83);
cmd[2] = fn_le16(control);
int res = send_cmd(
dev,
K1473_CMD_CAMERA_CONTROL,
cmd,
sizeof(cmd),
reply,
sizeof(reply));
if (res < 0)
return res;
if (res < 8 || fn_le16(reply[0]) != 0x0000)
return -1;
*value = fn_le16(reply[3]);
return 0;
}
static int k1473_set_u16_control(
freenect_device *dev,
uint16_t control,
uint16_t value)
{
uint16_t cmd[3];
uint16_t reply[4] = {0};
cmd[0] = fn_le16(0x0001);
cmd[1] = fn_le16(control);
cmd[2] = fn_le16(value);
int res = send_cmd(
dev,
K1473_CMD_CAMERA_CONTROL,
cmd,
sizeof(cmd),
reply,
sizeof(reply));
if (res < 0)
return res;
if (res < 2 || fn_le16(reply[0]) != 0x0000)
return -1;
return 0;
}
static int k1473_get_u16_control(
freenect_device *dev,
uint16_t control,
uint16_t *value)
{
uint16_t cmd[3];
uint16_t reply[4] = {0};
cmd[0] = fn_le16(0x0001);
cmd[1] = fn_le16(0xFF82);
cmd[2] = fn_le16(control);
int res = send_cmd(
dev,
K1473_CMD_CAMERA_CONTROL,
cmd,
sizeof(cmd),
reply,
sizeof(reply));
if (res < 0)
return res;
if (res < 8 || fn_le16(reply[0]) != 0x0000)
return -1;
*value = fn_le16(reply[3]);
return 0;
}7. Public Functions
Now add a small public API.
7.1 Auto exposure
int freenect_1473_set_auto_exposure(
freenect_device *dev,
int enabled)
{
const uint16_t value =
enabled ? K1473_EXPOSURE_AUTO
: K1473_EXPOSURE_MANUAL;
int ret = k1473_set_u8_control(
dev,
K1473_CTRL_EXPOSURE_MODE,
value);
if (ret < 0)
return ret;
/* Verify that the hardware accepted it. */
uint16_t readback = 0;
ret = k1473_get_u8_control(
dev,
K1473_CTRL_EXPOSURE_MODE,
&readback);
if (ret < 0)
return ret;
if (readback != value)
return -1;
return 0;
}7.2 Fixed exposure time
The protocol stores exposure in units of 100 microseconds:
wire value = exposure_us / 100The value is 32-bit and split between 0xCC04 and 0xCC06.
int freenect_1473_set_exposure(
freenect_device *dev,
uint32_t exposure_us)
{
/*
* 100 us per protocol unit.
*
* Examples:
* 5000 us -> 50 -> 0x00000032
* 20000 us -> 200 -> 0x000000C8
*/
uint32_t value = exposure_us / 100;
uint16_t high = (uint16_t)(value >> 16);
uint16_t low = (uint16_t)(value & 0xFFFF);
/*
* The successful Microsoft-driver capture writes
* the high and low halves independently.
*/
int ret = k1473_set_u16_control(
dev,
K1473_CTRL_EXPOSURE_HIGH,
high);
if (ret < 0)
return ret;
ret = k1473_set_u16_control(
dev,
K1473_CTRL_EXPOSURE_LOW,
low);
if (ret < 0)
return ret;
/*
* Verify both values by reading them back from the device.
*/
uint16_t high_readback = 0;
uint16_t low_readback = 0;
ret = k1473_get_u16_control(
dev,
K1473_CTRL_EXPOSURE_HIGH,
&high_readback);
if (ret < 0)
return ret;
ret = k1473_get_u16_control(
dev,
K1473_CTRL_EXPOSURE_LOW,
&low_readback);
if (ret < 0)
return ret;
if (high_readback != high || low_readback != low)
return -1;
return 0;
}7.3 Set gain
The Microsoft driver uses:
0xCC0E = gain * 32The tested SDK range was:
1.0 <= gain <= 16.0A convenient implementation is:
int freenect_1473_set_gain(
freenect_device *dev,
float gain)
{
if (gain < 1.0f || gain > 16.0f)
return -1;
/*
* Verified mapping on Model 1473:
*
* 1x -> 0x0020
* 2x -> 0x0040
* 4x -> 0x0080
* 8x -> 0x0100
* 16x -> 0x0200
*/
uint16_t raw =
(uint16_t)(gain * 32.0f + 0.5f);
int ret = k1473_set_u16_control(
dev,
K1473_CTRL_GAIN,
raw);
if (ret < 0)
return ret;
uint16_t readback = 0;
ret = k1473_get_u16_control(
dev,
K1473_CTRL_GAIN,
&readback);
if (ret < 0)
return ret;
if (readback != raw)
return -1;
return 0;
}A matching getter can expose the actual hardware value:
int freenect_1473_get_gain(
freenect_device *dev,
float *gain)
{
if (!gain)
return -1;
uint16_t raw = 0;
int ret = k1473_get_u16_control(
dev,
K1473_CTRL_GAIN,
&raw);
if (ret < 0)
return ret;
*gain = (float)raw / 32.0f;
return 0;
}For deterministic manual camera control, set manual exposure mode first:
freenect_1473_set_auto_exposure(dev, 0);
freenect_1473_set_gain(dev, 4.0f);The Microsoft implementation only applies gain while the camera is in manual exposure mode.
7.4 Read the current exposure
int freenect_1473_get_exposure(
freenect_device *dev,
uint32_t *exposure_us)
{
if (!exposure_us)
return -1;
uint16_t high = 0;
uint16_t low = 0;
int ret = k1473_get_u16_control(
dev,
K1473_CTRL_EXPOSURE_HIGH,
&high);
if (ret < 0)
return ret;
ret = k1473_get_u16_control(
dev,
K1473_CTRL_EXPOSURE_LOW,
&low);
if (ret < 0)
return ret;
uint32_t value =
((uint32_t)high << 16) |
(uint32_t)low;
*exposure_us = value * 100;
return 0;
}Add declarations to the appropriate public header, for example:
FREENECTAPI int freenect_1473_set_auto_exposure(
freenect_device *dev,
int enabled);
FREENECTAPI int freenect_1473_set_exposure(
freenect_device *dev,
uint32_t exposure_us);
FREENECTAPI int freenect_1473_get_exposure(
freenect_device *dev,
uint32_t *exposure_us);
FREENECTAPI int freenect_1473_set_gain(
freenect_device *dev,
float gain);
FREENECTAPI int freenect_1473_get_gain(
freenect_device *dev,
float *gain);You may prefer less model-specific names if this protocol is later verified on additional Kinect revisions.
8. Minimal Usage Example
After opening the Kinect and configuring the RGB video mode:
freenect_set_video_mode(
dev,
freenect_find_video_mode(
FREENECT_RESOLUTION_MEDIUM,
FREENECT_VIDEO_RGB));disable automatic exposure:
if (freenect_1473_set_auto_exposure(dev, 0) < 0) {
fprintf(stderr, "Failed to disable auto exposure\n");
return -1;
}then request a fixed 5 ms exposure:
if (freenect_1473_set_exposure(dev, 5000) < 0) {
fprintf(stderr, "Failed to set exposure\n");
return -1;
}optionally set a fixed gain, for example 4x:
if (freenect_1473_set_gain(dev, 4.0f) < 0) {
fprintf(stderr, "Failed to set gain\n");
return -1;
}and start RGB streaming:
if (freenect_start_video(dev) < 0) {
fprintf(stderr, "Failed to start RGB stream\n");
return -1;
}The commands were also observed working while RGB streaming was already active. For a first implementation, setting exposure immediately before starting the stream is simpler and easier to debug.
9. Example: 20 ms Exposure
freenect_1473_set_auto_exposure(dev, 0);
freenect_1473_set_exposure(dev, 20000);Expected wire representation:
20000 us / 100 = 200
200 decimal = 0x000000C8
0xCC04 = 0x0000
0xCC06 = 0x00C810. Example: Fixed Exposure + Fixed Gain
For a fully manual RGB configuration:
freenect_1473_set_auto_exposure(dev, 0);
freenect_1473_set_exposure(dev, 5000);
freenect_1473_set_gain(dev, 4.0f);Expected controls:
Manual exposure:
0xCC00 = 0x0001
Exposure 5 ms:
0xCC04 = 0x0000
0xCC06 = 0x0032
Gain 4x:
0xCC0E = 0x0080Verified gain mapping:
1x -> 0x0020
2x -> 0x0040
4x -> 0x0080
8x -> 0x0100
16x -> 0x020011. Do Not Use the Existing freenect_set_exposure() for This Path
Do not mix the two mechanisms during testing.
Avoid:
freenect_set_flag(
dev,
FREENECT_AUTO_EXPOSURE,
FREENECT_OFF);
freenect_set_exposure(dev, 5000);for the Model-1473-specific path described here.
Those calls currently use:
0x95
-> CMOS register 0x0106
-> CMOS register 0x0009Instead use only:
0x96
-> 0xCC00
-> 0xCC04
-> 0xCC06This avoids having two independent camera-control mechanisms modifying the device.
12. How to Verify That Exposure and Gain Are Really Applied
A setter returning success is not enough.
Use all three checks below.
Check 1: Read the control back
After disabling AE:
uint16_t mode;
k1473_get_u8_control(
dev,
K1473_CTRL_EXPOSURE_MODE,
&mode);Expected:
mode = 1After setting 5 ms:
0xCC04 = 0
0xCC06 = 50After setting 20 ms:
0xCC04 = 0
0xCC06 = 200Check 2: Change the requested exposure
Use a static scene and fixed lighting.
Compare:
5 ms
20 msThe 20 ms image should be significantly brighter unless it is already saturated.
Check 3: Verify gain independently
Keep exposure fixed, then compare several gain values:
Gain 1x
Gain 2x
Gain 4x
Gain 8x
Gain 16xThe image should become brighter as gain increases. Noise should also generally increase at higher gain.
Read 0xCC0E back after every write. The expected raw values are:
1x -> 0x0020
2x -> 0x0040
4x -> 0x0080
8x -> 0x0100
16x -> 0x0200Check 4: Challenge the auto-exposure loop
With the exposure fixed:
- Point the camera at a stable scene.
- Cover part of the lens or switch a lamp on/off.
- Observe the image for several seconds.
- Confirm that the camera does not gradually compensate back toward its previous brightness.
This distinguishes a true manual exposure from a temporary brightness change while automatic exposure remains active.
13. Wireshark / USBPcap Validation
When analyzing a Model 1473 capture, first identify the Kinect camera's USB device address.
The tested device used:
VID:PID 045e:02aeThen filter the camera in Wireshark, for example:
usb.device_address == 20Replace 20 with the address from your own capture.
To find Kinect proprietary commands, inspect control transfers whose payload starts with:
47 4Dwhich is ASCII:
GMThe exposure protocol uses:
command = 0x0096Useful values to search for in packet bytes include:
00 CC # 0xCC00
04 CC # 0xCC04
06 CC # 0xCC06
0E CC # 0xCC0E (gain)
32 00 # exposure 50 -> 5 ms
C8 00 # exposure 200 -> 20 ms
20 00 # gain 1x
40 00 # gain 2x
80 00 # gain 4x
00 01 # gain 8x
00 02 # gain 16xExample request for 5 ms low word:
47 4D 03 00 96 00 TT TT
01 00 06 CC 32 00Example request for 20 ms low word:
47 4D 03 00 96 00 TT TT
01 00 06 CC C8 0014. Integrating This into a ROS / ROS 2 Driver
For a ROS 2 wrapper around libfreenect, expose parameters such as:
auto_exposure: false
exposure_us: 5000
gain: 4.0During camera initialization:
if (!auto_exposure) {
freenect_1473_set_auto_exposure(fn_dev_, 0);
freenect_1473_set_exposure(fn_dev_, exposure_us);
freenect_1473_set_gain(fn_dev_, gain);
}A better production implementation should:
- check every return code;
- read the controls back;
- report the actual exposure and gain readback values in logs;
- reject obviously invalid exposure or gain values;
- optionally allow runtime parameter updates;
- serialize camera-control commands if another thread is calling
freenect_process_events().
Example:
int ret = freenect_1473_set_auto_exposure(fn_dev_, 0);
if (ret < 0) {
RCLCPP_ERROR(
get_logger(),
"Failed to switch Kinect 1473 to manual exposure");
}
ret = freenect_1473_set_exposure(
fn_dev_,
exposure_us_);
if (ret < 0) {
RCLCPP_ERROR(
get_logger(),
"Failed to set Kinect 1473 exposure to %u us",
exposure_us_);
} else {
RCLCPP_INFO(
get_logger(),
"Kinect 1473 exposure set to %u us",
exposure_us_);
}15. Exposure and Gain Ranges
Only values actually tested on the Model 1473 should be treated as confirmed.
Confirmed exposure values in this experiment:
5000 us = 5 ms
20000 us = 20 msConfirmed gain values:
1x
2x
4x
8x
16xThe Microsoft SDK public gain range is 1.0 to 16.0, and the verified Model-1473 wire mapping is:
raw_gain = gain * 32At 30 FPS the frame interval is approximately:
33.33 msFor initial testing, keeping exposure below one frame interval is sensible:
100 us <= exposure <= ~33000 usThis is a conservative engineering recommendation, not a fully characterized hardware limit.
The Microsoft SDK internally represented exposure in units of 1/10000 s, which is the same 100-us granularity observed on the wire.
16. Summary
The key discovery is:
Stock libfreenect exposure path
--------------------------------
command 0x95
CMOS 0x0106 / 0x0009
Model 1473: does not reliably control exposure
Verified Model-1473 path
------------------------
command 0x96
0xCC00 = 1 manual exposure
0xCC00 = 2 automatic exposure
0xCC04 exposure high 16 bits
0xCC06 exposure low 16 bits
0xCC0E gain
wire exposure unit = 100 us
raw gain = gain * 32Therefore:
5 ms:
value = 5000 / 100
= 50
= 0x00000032
0xCC04 = 0x0000
0xCC06 = 0x0032
20 ms:
value = 20000 / 100
= 200
= 0x000000C8
0xCC04 = 0x0000
0xCC06 = 0x00C8
Gain 4x:
raw_gain = 4 * 32
= 128
= 0x0080
0xCC0E = 0x0080For Kinect for Xbox 360 Model 1473, this 0x96 / 0xCCxx mechanism is the experimentally verified route to fixed RGB exposure and fixed gain.
17. References
The implementation and investigation were cross-checked against:
- OpenKinect
libfreenect, especiallysrc/flags.c,src/cameras.c, andsrc/usb_libusb10.c. - OpenKinect issue history for Model 1473 support and auto-exposure behavior.
- The upstream manual-exposure implementation based on command
0x95, which was originally tested on Model 1414. - Microsoft Kinect SDK 1.8 binaries and headers.
- USBPcap captures from a real Model 1473.
Because the 0x96 exposure path documented here was recovered through reverse engineering and direct hardware experiments, readers should independently validate it on their own hardware before relying on it in production.