Cameras & sensorsHow-to

Read image header bit depth before loading image files

Read image header bit depth before loading: one header-only call returns the stored depth, you allocate the matching buffer, and 16-bit files never truncate.

LumaSpec Vision9 min read Share on LinkedIn

On this page 8 sections
  1. 01What a type mismatch destroys at load time
  2. 02Call file information, not the file loader
  3. 03Size the buffer from the header's numbers
  4. 04Five steps to a load that keeps every bit
    1. What it looks like in code
  5. 05Prove the bits survived
  6. 06Where the files come from, and where this bites
  7. 07Before you write the next load call
  8. 08Questions

An image file announces its own bit depth in its header, before a single pixel is decoded, so you never have to guess it and you should never load blind. Every established machine-vision library ships a file-information call that reads only that header and returns width, height and stored bit depth. Call it first, allocate a buffer of the depth it reports, and the classic failure — a 16-bit image read into an 8-bit buffer, where every pixel value above 255 is destroyed — cannot happen. This guide gives you the call pattern, the buffer arithmetic, a five-step load sequence and the gradient test file that proves full-scale values survive the trip from disk to memory.

What a type mismatch destroys at load time

The damage happens at the load, not at the disk read. With an 8-bit buffer receiving a 16-bit file, the loader converts each pixel to the buffer's type as it fills. Two conversions are common: clamping values above 255 down to 255, or keeping only the low byte. Take a pixel that left a 12-bit sensor at 3000. Clamped, it arrives as 255; low-byte, it arrives at 3000 − 11 × 256 = 184. Both are wrong, and neither is recoverable from the image.

The truncated picture still looks correctly exposed, which is what makes the failure expensive: the histogram has a plausible shape, thresholds fire where they always did, and gray-level measurements quietly saturate at 255.

The 16-bit files in a vision line come mostly from the cameras themselves, because many area scan sensors digitize at 10 to 12 bits and their saves land in 16-bit containers. Why a 10-bit sensor ships 8-bit images is its own story: ADC bit depth vs pixel format: why 10-bit sensors ship 8-bit images.

Call file information, not the file loader

The function you want is a header-only read. At least one established vision library documents it as GetFileInfo, and its .NET binding exposes the same call as a GetFileInformation method in the analysis namespace. It opens the file, parses the header and returns an information structure — width, height, stored bit depth, usually file type and frame count — without decoding a pixel. The depth is stored in the file for exactly this purpose: BMP records dimensions, color depth and compression in its DIB header (BMP file format), PNG carries them in the IHDR chunk, TIFF in its BitsPerSample tag. If your library offers no such call, read those fields directly; the header sits a few dozen bytes into the file.

You may hear that a file must be loaded before its depth is known. That holds only for raw pixel dumps with no header at all; every common container stores the depth, and the information call exists to parse it.

One name trap: the operating system's own GetFileInformationByHandle function returns file metadata — timestamps, attributes, link counts — not image bit depth. The header read that matters here belongs to your imaging library, not to the file system.

Size the buffer from the header's numbers

bytes = width × height × bit depth / 8 12-bit packed on disk: bytes = width × height × 1.5, then unpack to 16 bits

Worked example, one frame from a 12 MP camera. The LS-CMR-0166 saves 4000 × 3000 frames from its 1/1.7" IMX226 sensor.

Area scan cameras12 MP USB3 area scan camera, IMX226, 1/1.7" rolling shutter, 30 fps, 4000 × 3000 resolution (LS-CMR-0166)LS-CMR-0166
Sensor format
1/1.7"
Interface
USB3
Resolution
12 MP
Shutter
Rolling
$560.00View partBuild a system with it

What one of those frames occupies in memory, by stored depth:

  • 8 bits: 4000 × 3000 × 1 byte = 12,000,000 bytes ≈ 12 MB (11.4 MiB)
  • 16 bits: 4000 × 3000 × 2 bytes = 24,000,000 bytes ≈ 24 MB (22.9 MiB)
  • 12-bit packed: 4000 × 3000 × 1.5 bytes = 18,000,000 bytes ≈ 18 MB

A 16-bit frame needs the 24 MB buffer. Allocate 12 MB and the load truncates; allocate for packed and receive unpacked, and the load fails outright — the better outcome, because a failed load gets fixed and a silent conversion does not.

Five steps to a load that keeps every bit

  1. Read the header first. Call the file-information function on the full path and keep its width, height and bit depth. No pixels have moved yet.
  2. Set the buffer type from the reported depth. A depth of 8 or fewer takes an 8-bit buffer; 9 to 16 takes a 16-bit buffer. A 12-bit packed file is the special case: 1.5 bytes per pixel on disk, unpacked to 16 bits before processing, following the packed Bayer 12-bit conversion to a 16-bit image pattern.
  3. Size the allocation from the formula. width × height × depth / 8 bytes, times the plane count for color or multi-plane files; the multi-channel image buffer size checks cover that arithmetic.
  4. Load into that buffer with the type fixed. Where the loader accepts a type hint, set it from the reported depth, never from a library default.
  5. Verify, then convert deliberately. Read min, max and mean against what the header implies. Down-convert to 8 bits only as an explicit, re-verified step when you truly need it — the habit in converted image object pixel format: verify before processing.

What it looks like in code

' Illustrative pseudocode; the exact names come from your library.
' At least one vision library documents the header read as GetFileInfo,
' with a GetFileInformation method in its .NET analysis namespace.
Dim path As String = "part_0142.tif"
Dim info = GetFileInfo(path)           ' header only, no pixel decode
Dim width As Integer = info.width      ' e.g. 4000
Dim height As Integer = info.height    ' e.g. 3000
Dim depth As Integer = info.bitDepth   ' e.g. 16
Dim buffer As SixteenBitImage = Allocate(width, height, depth)
LoadInto(path, buffer)                 ' types match: no conversion occurs

Prove the bits survived

Build one small test file and keep it with the code: a 16-bit TIFF or PNG holding a gradient from 0 to 4095, say 4096 pixels wide and 16 tall. After the load, the buffer pixel fed 3000 must read exactly 3000 — not 255 (clamped) and not 184 (low byte, 3000 − 11 × 256). Do not use a constant 4095 file for this: 4095 = 15 × 256 + 255, so its low byte is 255 too, and the low-byte conversion passes the test by accident. The gradient catches clamp and low-byte conversions in one pass. If the assertion fails, the conversion happened in your buffer type or a loader default, and steps 2 and 4 above are the fix.

Where the files come from, and where this bites

The same decision repeats upstream at the camera. The MV-CS200-10UM captures 20 MP mono frames over USB3.0.

Area scan camerasHikrobot MV-CS200-10UM 20 MP Area Scan Camera, USB3.0, IMX183, 2nd Gen., MonoMV-CS200-10UM
Sensor format
1"
Lens mount
C-mount
Interface
USB3
Resolution
16-25 MP
$759.00View partBuild a system with it

Saved at 16 bits, one of those frames occupies 20,000,000 pixels × 2 bytes = 40 MB (38.1 MiB); the 8-bit save is 20 MB, and if it was produced by clipping, the measurement data is already gone. A 16-bit container holding sensor values beyond 8 bits is normal and intended; the 8-bit container is where the loss lives.

Live streams have no file header to read: the format arrives from the camera as its current pixel format, which you take from the device or stream object before sizing the receiving buffer. When a stream will not start because the format is not what you expected, work through unknown pixel format: diagnose missing depth streams; when a live frame arrives looking wrong, the suspect is the same mismatch seen from the stream side in colour frame comes out doubled: fix the Mat type pixel format mismatch.

Decide by source, once:

  • Single known producer: read its header once, fix the buffer type, assert on every load.
  • Mixed archives and user-supplied files: header call per file, group by reported depth, allocate per group.
  • Raw dumps with no header: no call can recover the facts; take width, height and depth from the acquisition log, and configure the writer to emit a headered format next time.

Before you write the next load call

  • The buffer type comes from the header's bit depth, never from a default. Without it, the loader converts silently.
  • Packed 12-bit files unpack to 16 bits before processing, because 1.5 bytes per pixel on disk is not a 16-bit buffer size.
  • A 0-to-4095 gradient file round-trips pixel-exactly. This is the one test that catches clamp and low-byte conversions at once.
  • Saving code writes the depth the measurement needs into a container that stores it, so the next reader never repeats this investigation.

If you are also choosing the camera that produces the files, start from the area scan cameras category, filter by resolution and interface in the camera finder, or let the camera step of the configurator put a matched set together. The machine-vision cameras topic page collects the rest of the camera-side guides, and the applications team answers format-and-buffer questions through the contact page.

Questions

Does a header-only read have to open and scan the whole file?

No. The file-information call seeks to the header fields near the start of the file, reads a few hundred bytes and returns without decoding a pixel. For a 24,000,000-byte 16-bit frame that is roughly 100,000 times less data moved, which is why the header read stays fast even on network shares and slow links, and why running it over a whole directory before a batch costs almost nothing.

What exactly happens to a 16-bit pixel when it lands in an 8-bit buffer?

The library converts each 16-bit sample to the buffer's 8-bit type during the load. Clamping sends 3000 to 255; keeping the low byte sends 3000 to 184 (3000 − 11 × 256). Which one you get depends on the library's conversion routine, and a two-pixel test file with values 300 and 3000 identifies it in one run. Either way the original value is gone for good.

Should I just allocate 16-bit buffers for every file to be safe?

You can, and it is safe; the cost is memory and copy bandwidth, not correctness. A batch of 100 frames at 4000 × 3000 needs 2.4 GB of 16-bit buffers where 8-bit buffers would take 1.2 GB. If the library exposes a header call, reading the stored depth per file is the lighter habit; keep the always-16-bit rule for small, mixed archives where the extra memory does not matter.

Does the same header trick work for live camera streams?

No. A stream has no file header; its format arrives in the acquisition API as the camera's current pixel format, which you read from the device or stream object before sizing the receiving buffer. The failure looks similar — a frame with more bits per pixel than the buffer holds — so format checks belong on both paths.

What about raw camera files with no header at all?

A raw dump is pixels only, so there is nothing to parse: width, height and bit depth must come from the acquisition log or the camera's save settings. Header-only calls work on container formats — BMP stores the depth in its DIB header, TIFF in the BitsPerSample tag, PNG in the IHDR chunk — so configure the writer to emit one of those and the next reader can check before loading.