System designTroubleshooting

cv2 imwrite assertion failed empty image: fix the frame first

cv2.imwrite raises (215:Assertion failed) !_img.empty() when the image argument is empty. Find the failed cap.read() behind it and check ret before saving.

LumaSpec Vision10 min read Share on LinkedIn

On this page 9 sections
  1. 01Why does cv2.imwrite raise (215:Assertion failed) !_img.empty()?
  2. 02What makes cap.read() return False?
  3. 03How do I check the frame before saving it?
  4. 04Does the file path or extension cause this error?
  5. 05How do I save timestamped images without overwriting them?
  6. 06Why does the loop work on a webcam but fail on a machine-vision camera?
  7. 07How many frames per second can I save to disk?
  8. 08How do I keep the error from coming back on the line?
  9. 09Questions

OpenCV stops the script with (-215:Assertion failed) !_img.empty() in function 'cv::imwrite' the moment you hand it a frame that holds no pixels. The message points at the save line, but the failure happened one line earlier: cap.read() returned ret = False, the frame variable stayed empty, and the loop wrote it anyway. This guide is for anyone capturing camera frames in Python and saving stills for later inspection, measurement or OCR. You will trace the failed read, restructure the loop so an empty frame can never reach imwrite, and size the save rate so the disk keeps up.

Why does cv2.imwrite raise (215:Assertion failed) !_img.empty()?

imwrite asserts that the image argument holds pixels before it touches the disk, and !_img.empty() is that assertion failing: the frame variable you passed in is an empty NumPy array or None, because the previous cap.read() returned ret = False and the loop saved the dead result anyway.

cap.read() returns a tuple on every call: ret is a bool, and frame is a numpy.ndarray with pixel data when the grab worked, or an empty result when it did not — None on some backends, a zero-element array on others. An empty array reaches the assertion inside OpenCV's image codec module (imgcodecs); None is rejected one step earlier as a type error. Either way the stack points at imwrite, and the real fault sits in the capture.

Treat the message as a capture diagnosis, not a file diagnosis: the codec never ran, so the folder, the file name and the extension cannot be the cause. Two details make the failure repeat. A failed read does not raise; it returns ret = False and lets the loop continue. And the condition while cap.isOpened: without parentheses tests the method object itself, which is always true, so a dead camera still spins the loop and re-saves the dead frame on every pass. Write the call form: while cap.isOpened():.

What makes cap.read() return False?

Rank the causes by how often they turn up on the bench: a wrong device index first, the camera held by another program second, a camera that is unplugged, powered down or still booting third, and end of file fourth when the source is a recorded video instead of a live device.

Each cause has a one-minute test:

  • Open check. Print cap.isOpened() immediately after cv2.VideoCapture(). False means the device never opened, so every read will fail too; the start-sequence fix for a camera that fails to initialize covers that failure in detail.
  • Index check. Step the index from 0 upward and print which value opens. If several open but the reads still fail, force the backend for your operating system: cv2.CAP_V4L2 on Linux, cv2.CAP_DSHOW or cv2.CAP_MSMF on Windows, cv2.CAP_AVFOUNDATION on macOS.
  • Busy check. Close every other program that touches the camera, including a previous Python process that never called cap.release(); most capture stacks open a device exclusively.
  • Boot check. Wait 2-3 s after plugging in before the first read, because a camera that is still enumerating returns failed reads.
  • End-of-file check. For a video file rather than a live camera, read returns False at the last frame by design; rewind with cap.set(cv2.CAP_PROP_POS_FRAMES, 0) if the loop must keep running.

One more trap hides in the loop condition itself: a missing pair of parentheses turns every iteration into a save attempt, and thirty saves a second turn one failure into a screenful of assertions.

How do I check the frame before saving it?

Branch on ret the moment read returns: when it is False, count the failure and continue; when it is True, reset the counter, increment a frame index and only then touch imwrite. The assertion disappears because the save can no longer see an empty array.

The guard costs three lines and one threshold. Stop at 30 consecutive failed reads: at 30 fps that is one second of dead stream, long enough to conclude the camera is gone rather than busy.

import os
import cv2
from datetime import datetime

cap = cv2.VideoCapture(1)       # int device index, try 0 first
fail_count = 0                  # int, consecutive failed reads
frame_index = 0                 # int, successful reads so far

while cap.isOpened():           # parentheses required: method call
    ret, frame = cap.read()     # ret: bool, frame: ndarray or None
    if not ret:
        fail_count += 1
        print(f'read failed, {fail_count} in a row')
        if fail_count >= 30:    # 30 misses at 30 fps = 1 s dead
            break
        continue
    fail_count = 0
    frame_index += 1

    if frame_index % 150 == 0:  # one file every 5 s at 30 fps
        name = datetime.now().strftime('cap_%Y%m%d_%H%M%S') + f'_{frame_index}.png'
        saved = cv2.imwrite(name, frame)   # saved: bool
        if not saved:
            print(f'write failed: {name}')
        else:
            print(name, os.path.getsize(name), 'bytes')

cap.release()

Two runs prove the guard. With the camera unplugged, the script prints the failure line thirty times and exits cleanly — no traceback, no assertion. With the camera running, it prints each file name next to its size in bytes, and a size greater than zero is the proof that the codec wrote real pixels. Keep that getsize line in early builds; it is the cheapest end-to-end test a capture script can have.

Does the file path or extension cause this error?

No: the assertion fires before OpenCV touches the disk, so no folder, file name or extension can produce !_img.empty(). A bad path makes imwrite return False after the codec refuses the write, and an unknown extension fails the same way.

imwrite selects the codec from the extension — .png, .jpg, .bmp, .tif — and its return value is the only diagnostic the save line gives you. Check it on every call and log a False. Save .png when the images feed measurement or OCR: PNG is lossless, so pixel values survive unchanged, while JPEG discards detail by design.

When the text instead reads (-2:Unspecified error) The function is not implemented, the fault is the OpenCV build, not your arguments. A build without the window backend cannot run imshow, and one compiled without image codecs cannot write files; the message itself tells you to rebuild the library with desktop support, and installing the standard opencv-python wheel replaces the stripped build on a PC.

How do I save timestamped images without overwriting them?

Name each file from datetime.now() with the strftime pattern %Y%m%d_%H%M%S plus the frame index, and save only on a trigger — a key press, a hardware trigger pulse or a fixed interval — never unconditionally at the bottom of the loop.

A name that resolves to the second collides when you save twice within one second, and the first image vanishes. Add %f for microseconds or append frame_index, and every file is unique. Put the window call inside the same ret branch as the save so a dead frame cannot reach imshow either.

while cap.isOpened():
    ret, frame = cap.read()          # ret: bool, frame: ndarray or None
    if not ret:
        break
    key = cv2.waitKey(1) & 0xFF      # int, 255 when no key was pressed
    if key == ord('p'):              # save one still on demand
        name = datetime.now().strftime('cap_%Y%m%d_%H%M%S_%f.png')
        if not cv2.imwrite(name, frame):
            print('write failed:', name)
    elif key == ord('q'):
        break
    cv2.imshow('camera', frame)      # reached only for valid frames
cap.release()
cv2.destroyAllWindows()

For periodic snapshots instead of key presses, keep the modulo line from the guard loop: 150 frames at 30 fps is one file every 5 s, a rate the arithmetic in the next section sizes properly. For production, replace the key press with a hardware trigger; the hardware trigger grab loop in Python shows the acquisition side of that pattern.

Why does the loop work on a webcam but fail on a machine-vision camera?

Machine-vision cameras — the area scan cameras range — speak GigE Vision or USB3 Vision, not the webcam class the default VideoCapture backends enumerate, so VideoCapture(0) never opens them and every read returns False from the first frame while the code itself is fine.

Open these cameras through the vendor SDK or a GenTL producer, acquire the buffer there, then hand the NumPy array to OpenCV for the image work. The OpenCV VideoCapture and GigE Vision article walks through the working paths and why plain enumeration misses the camera.

For bench code like the guard loop, a USB3 camera feeds a normal PC over one cable: the LS-CMR-0166 outputs 4000 × 3000 frames at 30 fps over USB3.

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

If you would rather drop the capture code entirely, a smart camera runs acquisition and inspection on the device: the LS-SMC-0063 pairs a 16 mm lens with a 1.6 MP sensor over GigE. Compare the rest of the smart cameras range if you need a different resolution or field of view.

Smart cameras & vision sensors1.6 MP smart camera, 60 fps, 16 mm lens, white light (polarized, transparent light) (LS-SMC-0063)LS-SMC-0063
Interface
GigE
Resolution
Up to 3 MP
$1,110.00View part

How many frames per second can I save to disk?

Work it out from the raw frame: a 4000 × 3000 colour frame is 4000 × 3000 × 3 = 36,000,000 bytes ≈ 34.3 MiB, so 30 fps of uncompressed BMP demands about 1030 MiB/s — more than any single drive sustains — while saving every 150th frame lands at a comfortable 6.9 MiB/s.

bytes per frame = width × height × channels × bytes per pixel write rate (MiB/s) = bytes per frame × files per second / 1,048,576

The arithmetic is exact for uncompressed BMP, whose file size is fixed at 36,000,000 bytes plus a header of about 54 bytes. PNG and JPEG compress by content, so measure one file from a test run before you trust an interval built around them.

Save pattern at 30 fps Files per second Write rate Volume per hour
Every frame 30 ≈ 1030 MiB/s ≈ 3.5 TiB
Every 15th frame 2 ≈ 68.7 MiB/s ≈ 241 GiB
Every 150th frame 0.2 ≈ 6.9 MiB/s ≈ 24 GiB

Set the interval against the drive: a SATA drive tops out near 500 MB/s of sequential writes and a mechanical drive near 150 MB/s, so the every-15th pattern fits either with room left for the pipeline, while the every-frame pattern needs an NVMe drive plus a queued writer.

The decision: for inspection stills and fault snapshots, save on trigger or every 150th frame at 6.9 MiB/s; for continuous evidence, use a queued writer thread with PNG; never write BMP every frame on SATA or mechanical hardware.

How do I keep the error from coming back on the line?

Make four checks permanent in the loop: branch on ret at every read, check the boolean imwrite returns, stop after 30 consecutive failed reads, and verify the first saved file is larger than zero bytes before you call the run healthy.

Then fold them into habit:

  1. Guard every consumer of frame — save, imshow, crop, resize — with the same ret branch, because a crop on an empty frame fails just as dead.
  2. Log failed reads with a timestamp and the counter; a gap in the saved files tells you exactly when the link dropped.
  3. Self-test at start-up: require 10 successful reads before declaring the camera ready, so wiring and index mistakes surface before the first shift, not during it.
  4. Separate code faults from image faults: a file that saves but looks black is exposure or lighting, and the camera black image checks work that side.
  5. Measure the real frame rate before trusting fps math, because cap.get(cv2.CAP_PROP_FPS) returns 0 on some backends; the frame-rate recovery guide shows how to read it reliably.
  6. If the job is many images with no code at all, the no-code consecutive-image method handles capture and naming for you.
  7. Run unattended loops on a vision controller next to the camera, with the camera on its own link and nothing else sharing it.

The decision for each case:

  • Bench webcam stills: guard on ret, save on key press or interval, PNG for anything you will measure.
  • Machine-vision camera: open it through the SDK or GenTL producer, never VideoCapture(index); the guarded loop then works unchanged on the acquired buffers.
  • Continuous recording: queued writer thread, interval picked from the table, drive with measured headroom.
  • Unattended line: 30-read failure break, 10-read start-up self-test, file-size check on the first save.

To size the camera and controller pair for a permanent station, start at the camera finder or the camera step of the configurator; free expert support with every order covers capture questions like this one.

Questions

Does cv2.imwrite return False instead of raising when the write fails?

Yes. imwrite returns a bool: True when the codec wrote the file, False when it refused the path, the extension or the destination disk. The empty-image assertion is different: it raises before any disk work because the array has no pixels. Capture the return value with saved = cv2.imwrite(path, frame) and treat False as a stop condition, because a missing file discovered downstream is far harder to trace than an exception at the write.

What does the same empty frame do to cv2.imshow?

It raises its own assertion about the frame being empty and kills the window call, exactly as imwrite dies at the save. Put imshow inside the same ret branch as the save, so one guard protects both. On a machine with no display, such as a headless vision box, imshow raises the not-implemented error no matter what the frame holds; remove the window calls there.

Which device index should cv2.VideoCapture start from?

Start at 0 and step upward: the index is the order the operating system enumerates camera-class devices, so a built-in webcam usually claims 0 and a USB camera lands at 1 or 2. Enumeration order can change after a reboot or a hub swap, which is why a hard-coded index is the most common reason a read returns False from the first frame. Confirm what opened with cap.getBackendName() before blaming the capture code.

Does the ret check also catch black or torn frames?

No: ret is False only when the capture delivered nothing at all. A black or torn frame arrives with ret set to True and passes every guard, so add a pixel check where it matters: reject frames whose mean intensity sits at 0 or 255 across the whole array, and compare each frame with the previous one to catch repeated buffers. Work exposure and lighting first when the saved files come out black.