Reading an image sounds trivial. Load file, show window, done.

That's what I thought too — until an empty Mat crashed my program in production, and a VideoWriter silently produced a 0-byte file for a whole weekend of recording. It is trivial, with few tips, everyone will master this topic. 

In this article, I'll show you the I/O habits that now save me hours of debugging: imread, imshow, imwrite, VideoCapture and VideoWriter in OpenCV 5.

1. Loading Images — And the Check I Never Skip

#include <opencv2/opencv.hpp>
using namespace cv;

Mat img = imread("photo.jpg");
if (img.empty()) {
    std::cerr << "Could not read photo.jpg" << std::endl;
    return -1;
}

imread never throws. Wrong path, corrupted file, unsupported format — you just get an empty Mat, and the crash happens later, somewhere else. Always check empty().

Need grayscale directly? Pass the flag and skip a conversion:

Mat gray = imread("photo.jpg", IMREAD_GRAYSCALE);

2. Displaying — Why waitKey Is Not Optional

imshow("My photo", img);
waitKey(0);

I used to forget waitKey() and wonder why the window flashed and disappeared. The truth: imshow only queues the drawing — waitKey is what actually pumps the GUI events. No waitKey, no window.

3. Saving with imwrite

imwrite("output.png", img);   // lossless
imwrite("output.jpg", img, {IMWRITE_JPEG_QUALITY, 90});
  • .png → lossless, bigger files, my default for intermediate results
  • .jpg → compressed, quality flag 0–100

The codec is chosen purely by the file extension. Simple, but easy to forget.

4. Video Capture — Files and Cameras, Same API

VideoCapture cap(0);            // webcam
// VideoCapture cap("movie.mp4");  // or a file — identical code below

if (!cap.isOpened()) {
    std::cerr << "Cannot open video source" << std::endl;
    return -1;
}

Mat frame;
while (cap.read(frame)) {
    imshow("Video", frame);
    if (waitKey(1) == 'q') break;
}

What clicked for me: video is just a loop over Mats. cap.read() returns false at end-of-file or on camera failure — that's your natural exit condition.

5. The Trap I Fell Into: VideoWriter Frame Size

Here is the weekend-ruining bug I mentioned:

int w = (int)cap.get(CAP_PROP_FRAME_WIDTH);
int h = (int)cap.get(CAP_PROP_FRAME_HEIGHT);
double fps = cap.get(CAP_PROP_FPS);
if (fps <= 0) fps = 30.0;   // webcams often report 0!

VideoWriter out("rec.mp4",
                VideoWriter::fourcc('m','p','4','v'),
                fps, Size(w, h));

while (cap.read(frame)) {
    out.write(frame);       // frame size MUST match Size(w, h)
}

If the frame you write has a different size than the one you promised in the constructor, VideoWriter writes nothing — no error, no warning, just an empty file. Now I always assert the sizes match before a long recording.

6. Full code Example + build instructions

My CMakeLists.txt 

(note set directory where opencv is installed C:/opencv5/opencv-5.x/build/install)

cmake_minimum_required(VERSION 3.22)
project(cv5test)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

set(OPENCV_DIR "C:/projects/opencv5/opencv-5.x/build/install")
list(APPEND CMAKE_PREFIX_PATH "${OPENCV_DIR}")
find_package(OpenCV 5.0.0 REQUIRED)

add_executable(cv5test main.cpp)
target_link_libraries(cv5test PRIVATE ${OpenCV_LIBS})

target_include_directories(${PROJECT_NAME} PRIVATE 
    ${OpenCV_INCLUDE_DIRS}
)

add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
    # Copy OpenCV runtime DLLs
    COMMAND ${CMAKE_COMMAND} -E copy_directory_if_newer
        "${OPENCV_DIR}/x64/vc18/bin"   # Note: VS2026 uses the vc18 designation
        $<TARGET_FILE_DIR:${PROJECT_NAME}>
        
        
    COMMENT "Verifying and copying required OpenCV and FFmpeg runtime DLLs..."
)

Configure and build command for Visual Studio 18 2026

cmake -S . -B build -G "Visual Studio 18 2026" -A x64
cmake --build build --config Release --parallel

Full source code

load ooo.jpg image and town0.avi video.

#include <opencv2/core.hpp>
#include <opencv2/opencv.hpp>
#include <opencv2/highgui.hpp>
#include <string>
#include <iostream>
#include <stdio.h>
using namespace cv;
using namespace std;

int main()
{
    std::cout << "OpenCV version: " << CV_VERSION << std::endl;
    Mat img = imread("ooo.jpg");
    if (img.empty())
    {
        std::cerr << "Could not read photo.jpg" << std::endl;
        return -1;
    }

    Mat gray = imread("ooo.jpg", IMREAD_GRAYSCALE);

    imshow("My photo", img);
    waitKey(0);
    imshow("My photo", gray);
    waitKey(0);

    imwrite("output.png", img); // lossless
    imwrite("output.jpg", img, {IMWRITE_JPEG_QUALITY, 90});

    VideoCapture cap("town0.avi"); // webcam
    // VideoCapture cap("movie.mp4");  // or a file — identical code below

    if (!cap.isOpened())
    {
        std::cerr << "Cannot open video source" << std::endl;
        return -1;
    }

    Mat frame;
    while (cap.read(frame))
    {
        imshow("Video", frame);
        if (waitKey(1) == 'q')
            break;
    }

    int w = (int)cap.get(CAP_PROP_FRAME_WIDTH);
    int h = (int)cap.get(CAP_PROP_FRAME_HEIGHT);
    double fps = cap.get(CAP_PROP_FPS);
    if (fps <= 0)
        fps = 30.0; // webcams often report 0!

    VideoWriter out("rec.mp4",
                    VideoWriter::fourcc('m', 'p', '4', 'v'),
                    fps, Size(w, h));

    while (cap.read(frame))
    {
        out.write(frame); // frame size MUST match Size(w, h)
    }
}

7. My Key Takeaways

  • imread fails silently — check empty() every single time
  • waitKey drives the GUI, not just the keyboard
  • File extension selects the codec in imwrite
  • VideoWriter frame size mismatch = silent empty file
  • Webcams may report FPS as 0 — always have a fallback

Conclusion

I/O is boring until it fails silently. These small habits made my capture code boring in the best way — it just works.

In the next article, we'll transform those loaded images: resize, crop, rotate and color spaces — including the HSV trick that makes color detection actually robust.

Tip:

Need help to install, compile opencv C++ program? Use Agents in Antigravity, Cursor, Gemini CLI, Claude to do this for you.