Measurement Image Formats
This guide explains how to control the encoding of the device images that Elements Hub returns inside a measurement (RAE) container, and how to decode the raw image payload from C++.
Background
When a measurement is triggered against an Aesthetix device, the resulting RAE container can include one or more device images — for example the scratch view, the surface view, the LED sparkle stack or the LED shade stack. By default these images are encoded as PNG. Callers can choose the encoding per measurement: PNG, JPEG, or the unaltered raw bytes that the device sent.
Selecting the encoding
The encoding is selected through the optional imageFormat operation parameter when triggering a measurement:
POST /v1/devices/{deviceId}/measurement-triggers
Content-Type: application/json
{
"measurementKey": "measure",
"containerFormat": "raeBinary",
"parameters": [
{ "key": "metricGroupKeys", "value": ["..."] },
{ "key": "imageFormat", "value": "image/x-raw" }
]
}
imageFormat value |
Encoding used in the container | Container MIME type |
|---|---|---|
| (omitted) | PNG (default) | image/png |
image/png |
PNG | image/png |
image/jpeg |
JPEG, quality 90 | image/jpeg |
image/x-raw |
Raw device bytes (see below) | image/x-raw |
Unknown values are rejected with HTTP 400 and the error code E42InvalidImageFormat.
A small number of device images (surface, waviness, spot) are post-processed (cropped) on the hub side before being encoded. With
image/pngandimage/jpegyou receive the cropped variant; withimage/x-rawyou receive the original, uncropped bytes as emitted by the device. Most images — including the shade images (ledShadeImages_*) — are not post-processed, so all three encodings cover identical pixel data.
Locating images in the container
The container exposes the device images as File components in the measurement tree. Array images such as the shade stack use one entry per slot with a numeric suffix:
ledShadeImages (Array)
├── ledShadeImages_0 (File, MimeType=image/x-raw)
├── ledShadeImages_1 (File)
├── ledShadeImages_2 (File)
├── ledShadeImages_3 (File)
├── ledShadeImages_4 (File)
└── ledShadeImages_5 (File)
The BinaryData field of each File component holds the byte stream — either the encoded PNG/JPEG or the raw layout described in the next section, depending on the imageFormat you requested.
Layout of image/x-raw
Every raw image starts with a fixed 16-byte little-endian header, immediately followed by the pixel data:
+------------------------------------------+
| Header (16 bytes, little-endian) |
+------------------------------------------+
| Offset 0..3 : Height (int32)|
| Offset 4..7 : Width (int32)|
| Offset 8..11 : Channels (int32)|
| Offset 12..15 : BytesPerPixel (int32)|
+------------------------------------------+
| Pixel data (Height * Stride bytes) |
| Row-major, top-down |
+------------------------------------------+
- Endianness: little-endian for every header field.
- Stride (bytes per row):
(Width * Channels * BytesPerPixel * 8 + 7) / 8— byte-aligned, no row padding. For all current device images this is simplyWidth * Channels * BytesPerPixel. - No padding between the header and the pixel data.
Pixel formats
The combination of Channels and BytesPerPixel describes the pixel layout:
| Channels | BytesPerPixel | Pixel format | Element type |
|---|---|---|---|
| 1 | 1 | 8-bit grayscale | uint8 |
| 1 | 4 | 32-bit float grayscale (e.g. physical measurement values) | float32 |
| 3 | 1 | 24-bit colour, BGR order (not RGB) | uint8[3] |
No other combinations occur in current Aesthetix firmware.
Parse the format out of the header rather than hard-coding a format per image type. The device firmware may change which format it uses for a given image in future updates.
Parsing the raw stream in C++
Header & pixel view (endian-safe, no external dependencies)
#include <cstdint>
#include <cstring>
#include <stdexcept>
struct AesthetixRawImage {
int32_t height;
int32_t width;
int32_t channels;
int32_t bytesPerPixel;
const uint8_t* pixels; // points into the original buffer
std::size_t pixelByteCount;
};
inline int32_t readInt32LE(const uint8_t* p) {
return static_cast<int32_t>(
static_cast<uint32_t>(p[0]) |
static_cast<uint32_t>(p[1]) << 8 |
static_cast<uint32_t>(p[2]) << 16 |
static_cast<uint32_t>(p[3]) << 24);
}
AesthetixRawImage parseAesthetixRaw(const uint8_t* data, std::size_t size) {
if (size < 16) throw std::runtime_error("Header too short");
AesthetixRawImage img{};
img.height = readInt32LE(data + 0);
img.width = readInt32LE(data + 4);
img.channels = readInt32LE(data + 8);
img.bytesPerPixel = readInt32LE(data + 12);
const std::size_t expected =
static_cast<std::size_t>(img.height) * img.width *
img.channels * img.bytesPerPixel;
if (size - 16 < expected) throw std::runtime_error("Pixel buffer too short");
img.pixels = data + 16;
img.pixelByteCount = expected;
return img;
}
Building an OpenCV cv::Mat
OpenCV uses BGR internally, so it matches the Aesthetix colour layout without any cvtColor conversion.
#include <opencv2/core.hpp>
cv::Mat toCvMat(const AesthetixRawImage& img) {
int cvType;
if (img.channels == 1 && img.bytesPerPixel == 1) cvType = CV_8UC1;
else if (img.channels == 1 && img.bytesPerPixel == 4) cvType = CV_32FC1;
else if (img.channels == 3 && img.bytesPerPixel == 1) cvType = CV_8UC3; // BGR
else throw std::runtime_error("Unsupported pixel format");
// Wraps the bytes without copying; clone if the source buffer may be freed.
return cv::Mat(img.height, img.width, cvType,
const_cast<uint8_t*>(img.pixels)).clone();
}
Normalising float images for display
// channels=1, bytesPerPixel=4 holds real float32 values (e.g. physical
// quantities) whose range is not bounded to [0,1]. Scale before rendering:
cv::Mat displayable;
cv::normalize(floatMat, displayable, 0, 255, cv::NORM_MINMAX, CV_8UC1);
Common pitfalls
- BGR is not RGB. Three-channel images use BGR ordering. Swap the channels (or rely on OpenCV's native BGR handling) before passing the data to libraries that expect RGB.
- Float images need scaling.
bytesPerPixel = 4images are realfloat32data whose value range is not bounded to[0, 1]. Renderers must normalise the values. - Read header values with
memcpyor byte-shifting. Areinterpret_cast<int32_t*>is undefined behaviour on architectures with strict alignment when the buffer is not 4-byte aligned. ThereadInt32LEhelper above sidesteps this and is also endian-portable. - Compute the stride; don't hard-code it. Although the byte size today is
Width * Channels * BytesPerPixel, computing the stride keeps your code robust if new pixel formats are introduced. - Validate header values. Reject negative or implausibly large
Height/Widthvalues before multiplying them; a corrupt stream could otherwise overflowsize_t. - PNG/JPEG of post-processed images vs. raw originals. A handful of device images (surface, waviness, spot) are cropped server-side before being encoded as PNG or JPEG. The
image/x-rawpayload is the uncropped original. The shade images and most other images are not post-processed, so all three encodings cover identical pixel data.
See also
- Swagger UI Guide — explore the
measurement-triggersendpoint interactively and try the differentimageFormatvalues. - Client Code Generation — generate a strongly typed REST client for C++ (
cpp-restsdk) from the OpenAPI specification.