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.