EthicsComputerCourses
Join training +91 90532 10052

Reading & Writing Images with OpenCV

Computer Vision · Lesson 4 of 28 Intermediate 7 min read Updated

By the end of this lesson you will be able to install OpenCV, load a photo from disk, check that it really loaded, draw on it, and save it as JPEG or PNG with the right settings. You will also see how JPEG compression changes your pixels.

What is OpenCV?

OpenCV (Open Source Computer Vision Library) is the most widely used library for classical computer vision. It is written in C++ for speed and has a Python interface called cv2. Under the hood, every image it gives you is a plain NumPy array, so everything from Images as NumPy Arrays still applies.

Think of OpenCV as a well-stocked toolbox in a mechanic's shop in Lucknow. You could make every spanner yourself with NumPy, and in this course you often will, to understand them. But for daily work you pick up the ready-made tool.

The current major version is OpenCV 5. OpenCV does not run in this site's browser editor, so the OpenCV examples here are plain code. Their outputs were captured by running them with OpenCV 5.0.0 on a computer.

Installing OpenCV

Step 1: create and activate a virtual environment. Step 2: install the package. Step 3: check the version.

Terminal
python -m venv cv-env
cv-env\Scripts\activate          # Windows; on Linux or macOS: source cv-env/bin/activate
pip install opencv-python numpy matplotlib
python -c "import cv2; print(cv2.__version__)"
Output
5.0.0

Your version number may be newer. There are four packages on PyPI; install exactly one of them:

PackageUse it when
opencv-pythonNormal desktop use, with windows (cv2.imshow)
opencv-python-headlessServers, Docker and cloud, where there is no screen
opencv-contrib-pythonYou need the extra "contrib" modules too
opencv-contrib-python-headlessContrib modules on a server

Installing two of them at once makes them overwrite each other's files. If import cv2 behaves strangely, uninstall all four and install one again.

Reading and saving images

Here is a complete script. It makes a test "shop sign" in code, saves it, and reads it back in two ways. Remember that OpenCV colours are in BGR order, so (0, 0, 255) is red.

Python
import cv2
import numpy as np

print("OpenCV version:", cv2.__version__)

# Step 1: make a test image in code (BGR order!)
img = np.full((120, 200, 3), 255, dtype=np.uint8)
cv2.rectangle(img, (20, 20), (90, 100), (0, 0, 255), thickness=-1)   # filled red box
cv2.circle(img, (150, 60), 30, (255, 0, 0), thickness=3)             # blue ring
cv2.putText(img, "Pune", (100, 110), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 0), 2)

# Step 2: save it
ok = cv2.imwrite("shop_sign.png", img)
print("Saved:", ok)

# Step 3: read it back two ways
colour = cv2.imread("shop_sign.png")                       # default: 3-channel BGR
grey = cv2.imread("shop_sign.png", cv2.IMREAD_GRAYSCALE)
print("Colour:", colour.shape, colour.dtype)
print("Grey:  ", grey.shape, grey.dtype)
print("Pixel inside red box (BGR):", colour[50, 50])
Output
OpenCV version: 5.0.0
Saved: True
Colour: (120, 200, 3) uint8
Grey:   (120, 200) uint8
Pixel inside red box (BGR): [  0   0 255]

Notice one big difference from NumPy: OpenCV drawing functions take points as (x, y), which is (column, row). NumPy indexing takes [row, column]. So the rectangle from (20, 20) to (90, 100) covers columns 20 to 90 and rows 20 to 100, and colour[50, 50] is inside it.

The main imread flags are:

  • cv2.IMREAD_COLOR (the default): always 3 channels, BGR.
  • cv2.IMREAD_GRAYSCALE: one channel.
  • cv2.IMREAD_UNCHANGED: keep the file as it is, including a PNG alpha channel.

The silent failure: imread returns None

If the file is missing, or the path is wrong, cv2.imread does not raise an error. It prints a warning line and returns None. Your program crashes later, with a confusing message such as 'NoneType' object has no attribute 'shape'. Always check straight away.

Python
import cv2

img = cv2.imread("does_not_exist.jpg")
print(img)
if img is None:
    raise SystemExit("Could not read the image: check the path and file name")
Output
None
Could not read the image: check the path and file name

(OpenCV also prints a [ WARN ... ] can't open/read file line before this.) The most common causes are a typo in the file name, running the script from a different folder than you think, and a file that is not really an image.

Showing images

On a desktop, cv2.imshow opens a window. You must call cv2.waitKey to let the window draw itself and wait for a key press.

Python
import cv2

img = cv2.imread("shop_sign.png")
cv2.imshow("Shop sign", img)
cv2.waitKey(0)            # 0 = wait until any key is pressed
cv2.destroyAllWindows()

imshow does not work in the headless package, on servers, or in most notebooks. There, convert to RGB and use matplotlib, as you saw in Pixels, Channels & Colour Spaces.

JPEG or PNG?

PNG is lossless: it saves every pixel exactly. JPEG is lossy: it throws away detail your eye is unlikely to notice, to make the file smaller. It is like summarising a long WhatsApp voice note in a few lines: smaller, but some words are gone forever.

You can try this in the browser with Pillow, an image library that does load in the editor. Step 1: make an image. Step 2: save it as PNG and as JPEG at three quality levels. Step 3: read each file back and measure how far the pixels moved.

PythonRun it yourself
import os
import numpy as np
from PIL import Image

# Step 1: a white card with a red box and a dark line, in RGB
img = np.full((120, 200, 3), 255, dtype=np.uint8)
img[20:100, 20:90] = [220, 30, 30]
img[60:63, 100:190] = [20, 20, 20]

# Step 2 and 3: save, reload, compare
Image.fromarray(img).save("card.png")
back = np.asarray(Image.open("card.png"))
print(f"PNG       : {os.path.getsize('card.png'):5} bytes, identical: {np.array_equal(back, img)}")

for q in (95, 50, 10):
    name = f"card_q{q}.jpg"
    Image.fromarray(img).save(name, quality=q)
    back = np.asarray(Image.open(name)).astype(int)
    err = np.abs(back - img.astype(int)).mean()
    print(f"JPEG q={q:<3}: {os.path.getsize(name):5} bytes, mean error {err:.2f}")
Output
PNG       :   443 bytes, identical: True
JPEG q=95 :  2780 bytes, mean error 0.83
JPEG q=50 :  1937 bytes, mean error 1.83
JPEG q=10 :  1445 bytes, mean error 3.75

PNG comes back identical. JPEG files change pixels, and the error grows as quality drops. For this simple drawing PNG is even smaller, because PNG compresses flat areas very well. For real camera photos JPEG is usually much smaller.

The OpenCV way to set JPEG quality is a list of flag and value pairs:

Python
cv2.imwrite("sign.jpg", img, [cv2.IMWRITE_JPEG_QUALITY, 90])
cv2.imwrite("sign.png", img, [cv2.IMWRITE_PNG_COMPRESSION, 6])   # 0-9, still lossless

Rule of thumb: save masks, scanned documents and anything you will process again as PNG. Use JPEG for photos you only show to people.

Reading image bytes from an upload

Web apps receive images as bytes, not file paths. cv2.imdecode turns bytes into an image, and cv2.imencode does the reverse.

Python
import cv2
import numpy as np

data = open("shop_sign.png", "rb").read()        # in a web app: the uploaded bytes
img = cv2.imdecode(np.frombuffer(data, np.uint8), cv2.IMREAD_COLOR)
if img is None:
    raise ValueError("Upload is not a valid image")
ok, png_bytes = cv2.imencode(".png", img)
print(img.shape, ok, type(png_bytes))
Output
(120, 200, 3) True <class 'numpy.ndarray'>

Common mistakes

  • Not checking for None. Check right after every imread.
  • Confusing (x, y) and [row, col]. Drawing uses (x, y); indexing uses [row, col].
  • Saving masks as JPEG. JPEG adds grey edge pixels; a 0/255 mask stops being 0/255.
  • Installing two OpenCV packages. Keep exactly one of the four.

Key points

  • cv2 images are NumPy arrays in BGR order with dtype uint8.
  • cv2.imread returns None on failure instead of raising an error.
  • Drawing functions take (x, y) points; NumPy indexing takes [row, col].
  • PNG is lossless; JPEG is smaller for photos but changes pixel values.
  • cv2.imdecode and cv2.imencode convert between bytes and images.

Exercise

In the browser editor, make a 100 by 100 RGB image with a green square in the middle. Save it as square.jpg with quality 30 and as square.png. Read both back with Pillow and print how many pixels differ from the original in each file.

Show solution
Python
import numpy as np
from PIL import Image

img = np.zeros((100, 100, 3), dtype=np.uint8)
img[30:70, 30:70] = [0, 200, 0]
Image.fromarray(img).save("square.jpg", quality=30)
Image.fromarray(img).save("square.png")
for name in ("square.jpg", "square.png"):
    back = np.asarray(Image.open(name))
    changed = np.any(back != img, axis=2).sum()
    print(name, "changed pixels:", changed)

np.any(..., axis=2) marks a pixel as changed if any of its three channels differs. The PNG reports 0 changed pixels; the JPEG reports many, mostly near the square's edges.

Quick Quiz

1. What does cv2.imread("missing.jpg") return when the file does not exist?

2. In cv2.rectangle(img, (40, 10), (80, 30), ...), what is 40?

3. Which format should you use to save a black-and-white mask you will process again?

4. Which package should you install on a cloud server with no screen?

Learn Computer Vision with a mentor

Free lessons get you started. Our Data Science & Machine Learning training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call