System designTroubleshooting

Fix a null image result in a vision pipeline

Trace a null image result in a vision pipeline to 1 of 5 causes — completion, export, type, buffer reuse, frame ID — with stride math and a 20-run test.

LumaSpec Vision11 min read Share on LinkedIn

On this page 10 sections
  1. 01Classify the null before you change anything
  2. 02Cause 1: the execution had not finished
  3. 03Cause 2: the image was never exported
  4. 04Cause 3: the returned object is not a raster
  5. 05Cause 4: the buffer was reused before you read it
  6. 06Cause 5: the image belongs to another execution
  7. 07Prove the fix with 20 fixed-input runs
  8. 08Hardware for the reproduction bench
  9. 09Prevention checklist and the next step
  10. 10Questions

A processing flow returns a clean score and then hands your image getter a null pointer. The instinct is to suspect the camera; in most integration cases the camera is fine, and the image was never exported for that execution or its buffer was reused before you read it. This guide walks the five causes in order of likelihood, gives a one-minute test for each, and shows the stride and timing arithmetic that separates a software fault from an acquisition fault. You finish with a 20-run bench proof, a log format, and a checklist that keeps the fault from coming back.

Classify the null before you change anything

Four different faults all present as a null image, and each needs a different fix. Log the result container's state before you dereference anything, then match what you see to a row.

What you observe What it means The one-minute test
Result pointer is null No image was exported for that execution Enumerate the configured outputs and compare names
Container exists, size reads 0 bytes and 0 x 0 The image branch never ran Log the status of the module that produces the image
Size reads 1280 x 1024 but pixel access fails Wrong object type or access path Query the runtime type before any cast
Valid now, garbage or a fault one frame later The buffer was reused by the next acquisition Copy stride x height bytes straight after retrieval
Pixels valid, window black Display path, not data path Separate the display fault; the data is fine

A non-null wrapper can still carry zero pixels, so the pointer alone is not evidence of an image. When the pixels are valid and only the window is black, the fault is in the display — the blank image window troubleshooting guide covers that path, and no display setting can repair a missing result object.

Cause 1: the execution had not finished

A readable score says the measurement branch completed. It says nothing about the image branch: the two outputs can sit on different paths with different timing. Work one execution at a time:

  1. Submit one known input frame and record the execution identifier you get back.
  2. Wait for the completion event — the synchronous call's return or the result callback — never a fixed sleep.
  3. Read the overall execution status and the status of the module that produces the image.
  4. Retrieve the scalar and the image from that same execution's result scope.

Set the wait to about ten times the single-frame processing time you measured on the bench: a flow that returns in 60 ms gets a 600 ms timeout, which absorbs load spikes without hanging the station. A fixed sleep gives you none of that margin. A 100 ms delay passes while one execution takes 60 ms on an idle PC and fails the day a loaded CPU pushes the same execution past 100 ms.

Check the branch conditions as well. A measurement branch can finish while an optional annotation branch is skipped, and a proper log says so plainly:

exec=42 status=done score=0.98 module=annotate skipped=1 img=null

That line is a configuration fault, not an acquisition fault.

Cause 2: the image was never exported

An image you can see in the flow editor is not an output until the configuration exports it. Editor previews, internal module images and public results are three different interfaces. Audit the export contract against six questions:

  • Is the output exported at the scope your application reads — flow scope, not module scope?
  • Is its type an image, not a filename, a byte array or a display object?
  • Does the mapping point at the source, intermediate or annotated image you intend?
  • Does the producing module run on the branch you are testing?
  • Is the configuration the application loaded the one you saved?
  • Does the getter receive the exact identifier, case included?

Then run the controlled test: export the raw source image under a simple name such as img_test and retrieve it. When that works, move the mapping one module downstream and repeat until retrieval fails again. The failure marks the exact module that breaks the export.

Re-read the names after every recipe load: application code keeps the old identifier while the new recipe maps that name to something else.

Cause 3: the returned object is not a raster

An encoded file, an editor display object and a pixel buffer are three different things, and a loose mapping hands you the wrong one. Query the runtime type before any cast, then take the size fields from the frame itself: under GigE Vision the frame leader carries the pixel format, width, height and X padding, so the stride arrives in the stream rather than in your head.

The pixel format fixes the bytes per pixel. The GenICam PFNC convention defines Mono8 at 8 bits per pixel and RGB8 at 24 bits per pixel, which makes the layout arithmetic:

stride = width x bytes_per_pixel + padding_x

where width and padding_x come from the frame metadata and bytes_per_pixel from the format definition.

bytes = stride x height

where height is the frame's row count.

Work the numbers for a 1280 x 1024 Mono8 frame: stride = 1280 x 1 + 0 = 1280 bytes, and the buffer holds 1280 x 1024 = 1,310,720 bytes, or 1.25 MiB. Align each row to 64 bytes and padding_x rises to 64: stride 1344 bytes, total 1,376,256 bytes. The same frame in RGB8 carries 24 bits per pixel, so the stride is 1280 x 3 = 3840 bytes and the buffer holds 3,932,160 bytes, or 3.75 MiB.

Two adjacent faults live here. An encoded payload is not a raster until it is decoded — the image bytes and pixel data guide separates the two. And a converted image can keep its destination layout rather than the source one, so check the converted object's pixel format before processing. For anything beyond a single plane, size the destination with the multi-channel image buffer sizing guide.

Cause 4: the buffer was reused before you read it

Acquisition rings recycle buffers, and the clock starts the moment you retrieve. At 30 fps the next frame wants its memory back in 33 ms; at 60 fps, 17 ms. The bench camera in this guide runs at 9 fps, which buys 111 ms (1000 / 9) — comfortable for a single-trigger test, and a trap the moment you restore production rates.

The signature is an image that reads correctly right after retrieval and turns to garbage or throws one frame later. Copy before anything else runs: stride x height bytes, which is 1,310,720 bytes for the 1280 x 1024 Mono8 case above and about 0.13 ms of work on a 10 GB/s memory bus. Copy the metadata — pixel format, stride, timestamp — with the pixels; a shallow assignment copies only the address.

The same lifetime rules apply at the acquisition boundary: the camera frame memory lifetime guide works through them for managed code. In a processing flow, repeat the analysis for every intermediate image and exported result.

Cause 5: the image belongs to another execution

A score from execution 42 paired with an image from execution 43 is an invalid inspection record, even though both objects are individually valid. Stamp an identifier at both ends. GigE Vision numbers every frame with a block ID that increments by one, so capture it when the frame arrives and attach it to every result you read back. Where the processing engine issues its own execution identifier, log the two side by side.

The block ID doubles as an acquisition check. A trace that jumps from 1042 to 1044 lost a frame before any processing ran, which points at the link, cable or trigger rather than at your output mapping; the frame corruption after a power cycle guide covers that family. For pairing results with frames under load, the asynchronous result and frame matching guide gives the timestamp discipline.

Prove the fix with 20 fixed-input runs

Return to one known input and one execution at a time. That removes camera timing and overlapping runs from the test and makes the log readable. Twenty runs is the smallest useful sample, and the sampling arithmetic says why:

p = 1 - (1 - q)^n

where q is the per-run failure rate and n is the number of runs. With q = 0.1, twenty runs surface the fault at least once with p = 1 - 0.9^20, about 0.88; five runs manage about 0.41.

Log every run, passes included:

run 17: exec=61 img=valid Mono8 1280x1024 stride=1280 bytes=1310720

Then run the negative test. A configuration that intentionally produces no image must report an absent state distinct from a failed retrieval; downstream code that cannot tell the two apart raises acquisition alarms for configuration decisions.

Test What it isolates Evidence to log
Source image exported as img_test Retrieval path and naming Output name, runtime type, validity
Mapping moved one module downstream The module that breaks the export Module status, dimensions
Immediate copy after retrieval Ownership and reuse Copy size, destination validity
20 sequential executions Buffer reuse and stale outputs Execution ID beside each image ID
Queued consumer on another thread Cross-thread lifetime Retain, queue and release events

Hardware for the reproduction bench

The bench camera needs one thing above all: a stream that carries the fields these tests read — pixel format, width, height, padding and block ID. For the fixed-frame work here, the LS-CMR-0053 is that camera: a 12 MP GigE model with a C-mount and a 9 fps rolling shutter.

For the acquisition side of the bench:

Area scan cameras12 MP GigE area scan camera, IMX226, 1/1.7" rolling shutter, 9 fps (LS-CMR-0053)LS-CMR-0053
Sensor format
1/1.7"
Lens mount
C-mount
Interface
GigE
Resolution
12 MP
$560.00View partBuild a system with it

A 12 MP frame at Mono8 — 4000 x 3000 pixels — is 12,000,000 bytes of pixel data. A 1 Gbit/s GigE link moves about 125 MB/s at its theoretical limit, so one frame needs 12,000,000 / 125,000,000, about 0.096 s, or 96 ms on the wire. At 9 fps there are 111 ms between frames, so single-trigger frames arrive whole; keep the trigger spacing above the transfer time as frame rates climb. For the wider choice, compare area scan cameras in the camera finder.

When the station has no PC, run the flow on a vision controller: a box with camera ports and onboard processing, so the pipeline and its image outputs stay on one machine beside the line. For that station:

Vision controllersHikrobot MV-SVC2506-16G60-128G 4 camera GigE ports, 1 configuration GigE port, 1 communication GigE port, 6100pro encryption supportedMV-SVC2506-16G60-128G
LAN ports
5+ LAN ports
USB 3 ports
2 USB 3 ports
Light outputs
4 built-in channels
Memory
16 GB
Price on requestView and quoteBuild a system with it

Prevention checklist and the next step

Fold these into the integration acceptance test:

  • Completion: retrieve outputs only after the completion event and a status read, with the wait set to ten times the measured single-frame time.
  • Mapping: re-read the output definition after every recipe load; identifiers live in configuration, not in application code.
  • Type: query the runtime type and the pixel format before any pixel access.
  • Layout: compute with the stride the frame reports, never the width you expect.
  • Ownership: copy stride x height bytes immediately after retrieval; release only what you allocated.
  • Correlation: stamp the block ID or execution ID on every image and every scalar.
  • Absence: treat an intentional skip as a state, not an error.

When the trace ends at the processing interface, take the minimal configuration and the run log to that vendor's support channel. When acquisition sizing is still open, size the station in the camera configurator or contact the applications team with your interface, frame size, frame rate and retention requirements.

Questions

Why does the score come back while the image result is null?

Score and image are separate outputs of one execution. The measurement branch completed, so the scalar is readable, while the image branch was skipped or never exported. The run log shows it directly: a done execution with the annotate module skipped and the image null. Fix the branch condition or the export mapping; the camera is not involved. Exporting the source image under a temporary name confirms the retrieval path in under a minute.

Is an image that displays in the editor available through the API?

Not until the configuration exports it. Editor previews, internal module images and public outputs are separate interfaces, and a preview can be a composite overlay the result API never carries. Add an image export mapped to the source image, retrieve it under a simple name such as img_test, then move the mapping one module downstream at a time until retrieval fails again. The failure marks the module that breaks the export.

How long can I keep the returned image pointer?

Until the next buffer reuse. At 30 fps that is 33 ms, at 60 fps it is 17 ms, and at a 9 fps bench rate it is 111 ms. Copy before the next execution: stride times height, which is 1,310,720 bytes for a 1280 x 1024 Mono8 image, about 0.13 ms of work on a 10 GB/s memory bus. If the wrapper does not own its pixels, a shallow copy carries only the address.

Will a delay before image retrieval fix a null result?

No, it hides the race instead of closing it. A 100 ms sleep passes while one execution takes 60 ms on an idle bench, then fails the day a loaded CPU pushes that execution past 100 ms. Wait for the completion event or the synchronous return, read the status, then retrieve. Set the wait to about ten times the single-frame time you measured, so a 60 ms flow gets a 600 ms timeout.

When is the camera actually at fault?

When the pipeline's input image is itself missing. Check the acquisition trace for gaps: GigE Vision numbers every frame with a block ID that increments by one, so a jump from 1042 to 1044 means a frame was dropped before any processing ran. That is a link, cable or trigger problem, not an output-mapping problem, and the fix belongs on the network side.

Corrections

  • Rewritten for usefulness: specific values with their basis, the named standards, a worked example, no hedging.