System designTroubleshooting

OpenCV segmentation fault on a CSI camera: fix the GStreamer caps

Why a CSI camera segfaults in OpenCV at first read: the sensor mode list is the contract, and here is the caps string that matches it, proven in gst-launch.

LumaSpec Vision9 min read Share on LinkedIn

On this page 9 sections
  1. 01The crash signature already names the cause
  2. 02Likely causes, most probable first
  3. 03Prove the camera outside OpenCV first
  4. 04The fix: a listed row at the source, scaling at the converter
  5. 05What the pipeline moves: two worked examples
  6. 06If a listed mode still crashes: sort by signature
  7. 07When the CSI link is not worth another day
  8. 08Prevention: the pattern that survives
  9. 09Questions

A MIPI CSI camera that starts cleanly and then takes OpenCV down with Segmentation fault (core dumped) on the very first read is almost never a broken camera. The pipeline string asked for a width, height and framerate combination that appears nowhere in the sensor's advertised mode list, the camera stack quietly picked a mode of its own, and the first buffer arrived in a shape the sink never agreed to. Work through the log, the gst-launch-1.0 test and the caps rewrite below, and you will have BGR frames in NumPy instead of a core dump.

The crash signature already names the cause

Read the console output as three facts rather than noise: the camera started, the stack printed every mode it can run, and it selected one that you never asked for. The request that produced this log asked for 640 x 480 at framerate 30/1, untyped. Here is the relevant part of the output:

GST_ARGUS: Available Sensor modes :
GST_ARGUS: 2592 x 1944 FR = 29.999999 fps Duration = 33333334
GST_ARGUS: 2592 x 1458 FR = 29.999999 fps Duration = 33333334
GST_ARGUS: 1280 x 720  FR = 120.000005 fps Duration = 8333333
GST_ARGUS: Running with following settings:
GST_ARGUS: Camera index = 0 Camera mode = 2
GST_ARGUS: Output Stream W = 1280 H = 720 Frame Rate = 120.000005
CONSUMER: Producer has connected; continuing.
Segmentation fault (core dumped)

No row in that list offers 640 x 480 at 30 fps, so the stack chose row 2, the third entry, which is 1280 x 720 at 120 fps, and started it. The camera did its job. The negotiation did not end in a shape the rest of the chain could deliver, and the first buffer into the sink ended the process.

The odd-looking frame rates are printed from frame durations, and they matter for your caps. One second divided by 33,333,334 ns gives 29.999999 fps, which is the same 30/1 you write in caps, and 1 s / 8,333,333 ns = 120.000005 fps. Write framerate=(fraction)30/1 and framerate=(fraction)120/1, not the decimal values.

Every capture interface works from a table like this one. The USB version of the same logic, where a camera refuses sizes it never advertised, is covered in USB camera stuck at 4K: how sensor resolution modes work.

Likely causes, most probable first

  1. The caps name no mode row. Width, height and framerate must match one printed row together. This causes most of these crashes.
  2. Right size, wrong framerate. In the list above, 1280 x 720 exists only at 120 fps; a 30/1 request at that size is as dead as 640 x 480 was.
  3. Missing type casts. The running examples write width=(int), height=(int), format=(string) and framerate=(fraction); untyped values are where silent misparses live.
  4. The crash happens only at exit. The frames were fine and the teardown order was not; that is a different fix, covered further down.
  5. The camera never negotiated at all. Sensor driver, device-tree overlay or carrier wiring; dmesg decides this one, not the pipeline string.

Treat causes 1 and 2 as the default assumption and work down the list only after the gst-launch-1.0 test clears them.

Prove the camera outside OpenCV first

Two commands separate a caps problem from a camera problem, and they take a minute. Run them before touching the Python side again.

  1. Start with no caps at all: gst-launch-1.0 nvarguscamerasrc ! nvoverlaysink. If live video does not appear, the fault sits in the driver, the overlay or the wiring; begin at CSI camera carrier board compatibility: what to verify and camera loads the wrong sensor driver: read dmesg, fix the overlay.
  2. Add your exact caps and a passive end point: gst-launch-1.0 nvarguscamerasrc ! 'video/x-raw(memory:NVMM), width=(int)1920, height=(int)1080, format=(string)NV12, framerate=(fraction)30/1' ! fakesink. On the release behind the log above, a 1920 x 1080 request failed there with Failed to create CaptureSession — the source saying, plainly, that no mode matches.
  3. Swap in a printed row: gst-launch-1.0 nvarguscamerasrc ! 'video/x-raw(memory:NVMM), width=(int)2592, height=(int)1944, format=(string)NV12, framerate=(fraction)30/1' ! fakesink. This one runs without error.
  4. Only now go back to cv2.VideoCapture with the identical string.

If step 3 runs clean and the identical string still segfaults inside OpenCV, the caps are exonerated; skip to the signature-specific fixes further down.

The fix: a listed row at the source, scaling at the converter

Name one printed row in the caps right after the source, and ask for your working resolution in a second caps element after the converter. The converter scales; the sensor mode never changes.

import cv2

gst_str = (
    'nvarguscamerasrc ! '
    'video/x-raw(memory:NVMM), width=(int)2592, height=(int)1944, '
    'format=(string)NV12, framerate=(fraction)30/1 ! '
    'nvvidconv ! '
    'video/x-raw, width=(int)640, height=(int)480, format=(string)BGRx ! '
    'videoconvert ! '
    'video/x-raw, format=(string)BGR ! '
    'appsink'
)

cap = cv2.VideoCapture(gst_str, cv2.CAP_GSTREAMER)
assert cap.isOpened(), 'pipeline failed to open'
success, image = cap.read()
print(success, image.shape)  # True (480, 640, 3)
  • The source caps copy row 0 exactly: 2592 x 1944, NV12, 30/1, with the memory:NVMM feature that keeps frames where the converter works on them.
  • nvvidconv scales 2592 x 1944 down to 640 x 480 — both 4:3, so nothing distorts — and produces BGRx for the CPU-side chain.
  • videoconvert finishes the BGRx to BGR conversion, and appsink hands NumPy-ready frames to cap.read().
  • The print at the end is the test: success comes back True and image.shape reads (480, 640, 3).

For the fast row, only the source caps change: width=(int)1280, height=(int)720, format=(string)NV12, framerate=(fraction)120/1.

What the pipeline moves: two worked examples

Where to scale is arithmetic, not taste. Frame size is width times height times bytes per pixel, and the rate multiplies by frame rate and by eight for bits:

bytes per frame = width × height × bytes per pixel rate (bit/s) = bytes per frame × frame rate × 8 NV12 = 1.5 bytes/px, BGRx = 4, BGR = 3

At the source, row 0 moves 2592 × 1944 × 1.5 = 7,558,272 bytes ≈ 7.2 MiB per frame. At 30 fps that is 7,558,272 × 30 = 226,748,160 B/s ≈ 226.7 MB/s, or 226,748,160 × 8 = 1,813,985,280 bit/s ≈ 1.81 Gbps. All of it stays on the board's memory side; none of it should reach your Python loop.

Your process receives the small stream: 640 × 480 × 3 = 921,600 bytes ≈ 0.9 MiB per frame, and 921,600 × 30 = 27,648,000 B/s ≈ 27.6 MB/s. That is 7,558,272 / 921,600 ≈ 8.2 times less data per frame than pulling full frames and resizing them in NumPy. The 120 fps row works out at 1280 × 720 × 1.5 = 1,382,400 bytes ≈ 1.3 MiB per frame, so 1,382,400 × 120 × 8 = 1,327,104,000 bit/s ≈ 1.33 Gbps — the busiest mode in the list.

The rule that falls out: match the sensor mode at the source, scale at the converter, and carry the smallest format you actually need into Python.

If a listed mode still crashes: sort by signature

When the caps copy a printed row and gst-launch-1.0 runs clean, the remaining faults split by where and when they fire:

When you compare runs or ask for help, include the first line of /etc/nv_tegra_release: the way a mismatched request fails has shifted between stack releases, so notes from different releases do not mix.

Sometimes the right engineering call is to stop debugging the receiver chain. A USB3 or GigE board camera replaces the CSI receiver, the sensor driver and the device-tree overlay with a link the host already knows how to enumerate, and OpenCV then talks to a standard device. The trade-offs between the two connections are laid out in CSI versus USB camera connections on an industrial controller.

If you re-specify the camera itself, the board camera evaluation checklist settles resolution, shutter and interface in one pass.

Two modules from the board-level cameras range show the pattern: the LS-BCM-0003 streams 120 fps over USB3 from a 3.1 MP global-shutter sensor, and the MV-CB016-10GM-M-W carries a 1.6 MP global-shutter sensor over GigE on an M12 mount.

Board-level cameras3.1 MP USB3 board-level camera, IMX252, 1/1.8" global shutter, 120 fps (LS-BCM-0003)LS-BCM-0003
Sensor format
1/1.8"
Lens mount
C-mount
$690.00View part
Board-level camerasHikrobot MV-CB016-10GM-M-W 1.6 MP GigE board-level camera, IMX296, Mono, M12 lens mount, WTB GigE portMV-CB016-10GM-M-W
Sensor format
1/2.9"
Lens mount
M12
$559.00View part

Both sit in the camera finder alongside the rest of the range, with filters for interface, resolution and shutter.

Prevention: the pattern that survives

  • Paste the printed mode rows into your code as named constants, with the row index in a comment. The log is the contract.
  • Keep the casts on every caps value: width=(int), height=(int), format=(string), framerate=(fraction).
  • Guard the open and every read: check isOpened() once, then the boolean from each cap.read() in the loop.
  • Measure the delivered rate instead of trusting the caps; video FPS returns zero in OpenCV shows the timing loop that still works when the metadata reports zero.
  • Record head -1 /etc/nv_tegra_release in your run notes, because mismatch behavior shifts between stack releases.

For a new build, the camera configurator walks the chain from module to mount; the systems topic page collects the other capture-path guides, and the contact page reaches the applications team when a fault is still open.

Questions

Why does gst-launch-1.0 run cleanly but OpenCV segfaults on the same caps?

gst-launch-1.0 proves only that the source and the end point negotiate. OpenCV adds the appsink hand-off into your process, and that is where a shape nobody agreed on finally bites. Keep the caps identical on both sides, with the (int), (fraction) and (string) casts, and check cv2.getBuildInformation() lists GStreamer support. If gst-launch-1.0 itself fails, fix the caps there before touching Python.

Can I capture 1920 x 1080 from a sensor whose full mode is 2592 x 1944?

Only if the mode list prints that row; the example list does not, so the request fails. The 2592 x 1458 row is the exact 16:9 crop of the same sensor, since 2592 divided by 1458 equals 1.78, so request it with framerate=(fraction)30/1 and scale to 1920 x 1080 at the converter. The full 4:3 row scales cleanly to sizes such as 640 x 480.

Why did the log say Camera mode = 2 when the pipeline asked for 640 x 480?

640 x 480 at 30 fps matches no row, so the stack picked its own nearest row; the log shows it selected 1280 x 720 at 120 fps. You do not control that pick. Name a listed row yourself in the source caps and do any resizing at the converter, and the selection stops being a surprise.

Does the same mode-matching fix apply to USB3 or GigE cameras?

No. USB3 and GigE cameras negotiate formats through their own drivers and never print a mode list like this one; read the enumerated formats from the camera's SDK or viewer instead. When the format is wrong there, the capture usually refuses to open or delivers wrong-sized buffers rather than crashing. The typed caps and scale-at-the-converter habits still pay off once the format is right.