⭐ Vision Engineer · Chặng 2Bài 1/6Bài học~60 phút

OpenCV cơ bản: đọc, hiển thị, ghi ảnh, video và camera

Cài OpenCV, imread/imshow/imwrite, đường dẫn tiếng Việt trên Windows, đọc ghi video, vòng lặp webcam đo FPS, waitKey và thuộc tính camera.

OpenCV cơ bản: đọc, hiển thị, ghi ảnh, video và camera

Học xong bài này, bạn sẽ

  • Cài OpenCV và đọc ảnh đúng cách, kể cả đường dẫn tiếng Việt
  • Đọc video và webcam theo thời gian thực
  • Ghi ảnh, ghi video kết quả

Nên học trước: Dự án chặng 1: báo cáo chất lượng thư mục ảnh

Nội dung bài viết
  1. 1. Cài OpenCV và kiểm tra
  2. 2. Ghi ảnh: imwrite
  3. 3. Đọc ảnh: imread
  4. 4. Hiển thị: imshow và waitKey
  5. 5. Đường dẫn tiếng Việt trên Windows
  6. 6. Đọc file video
  7. 7. Ghi video kết quả
  8. 8. Camera theo thời gian thực
  9. 9. Lỗi thường gặp
  10. Tóm tắt
  11. Bài tập

Chặng 2 bắt đầu với OpenCV, thư viện xử lý ảnh mã nguồn mở phổ biến nhất thế giới, dùng được từ Python, C++, C# (qua OpenCvSharp) và chạy trên hầu hết phần cứng. Ở chặng 1, bài "Ảnh là mảng số: NumPy cho xử lý ảnh" đã cho bạn thấy ảnh chỉ là mảng NumPy. Bài này dạy ba việc nền tảng mà mọi chương trình vision đều làm: đọc, hiển thị, ghi ảnh (kể cả đường dẫn tiếng Việt trên Windows), đọc và ghi video, và lấy hình từ camera theo thời gian thực. Bài tiếp theo, "Không gian màu BGR, HSV, Gray và tách màu", sẽ bắt đầu phân tích nội dung ảnh.

1. Cài OpenCV và kiểm tra

Trong môi trường ảo của dự án (bài "Môi trường ảo, pip và thư viện cho AI Vision"):

python -m pip install opencv-python numpy

Gói opencv-python có giao diện cửa sổ (cv2.imshow). Trên máy chủ không màn hình hoặc Docker, dùng opencv-python-headless. Chỉ cài một gói OpenCV: các gói đều cung cấp module cv2 và đè lên nhau (xem bảng 4 gói trong bài môi trường ảo). Kiểm tra:

import cv2

print("OpenCV", cv2.__version__)
for dong in cv2.getBuildInformation().splitlines():
    if dong.strip().startswith(("GUI:", "FFMPEG:")):
        print(dong.strip())
OpenCV 5.0.0
GUI:                           NONE
FFMPEG:                      YES

Kết quả trên là của máy chạy thử bài viết, cài bản headless nên GUI: NONE (không mở được cửa sổ). Với opencv-python trên Windows bạn sẽ thấy dòng GUI có WIN32UI; trên Linux là GTK hoặc QT. Dòng FFMPEG: YES nghĩa là đọc/ghi được file video MP4, AVI. Các ví dụ trong bài chạy được trên cả OpenCV 4.x và 5.x; chỗ nào hai bản khác nhau sẽ được ghi chú.

2. Ghi ảnh: imwrite

Tạo một ảnh "sản phẩm" để dùng cho cả bài, rồi ghi ra nhiều định dạng. OpenCV chọn định dạng theo đuôi file:

import os

import cv2
import numpy as np

rng = np.random.default_rng(0)
anh = rng.integers(70, 90, size=(480, 640, 3), dtype=np.uint8)       # nền băng tải có nhiễu
cv2.rectangle(anh, (200, 140), (440, 340), (190, 190, 190), -1)       # sản phẩm
cv2.circle(anh, (320, 240), 40, (40, 40, 40), -1)                     # lỗ giữa
cv2.putText(anh, "SP-01", (215, 175), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 0, 200), 2)

print(cv2.imwrite("san_pham.png", anh))                                      # PNG: không mất dữ liệu
cv2.imwrite("san_pham_q95.jpg", anh, [cv2.IMWRITE_JPEG_QUALITY, 95])
cv2.imwrite("san_pham_q50.jpg", anh, [cv2.IMWRITE_JPEG_QUALITY, 50])
cv2.imwrite("san_pham.bmp", anh)                                             # không nén

for ten in ["san_pham.bmp", "san_pham.png", "san_pham_q95.jpg", "san_pham_q50.jpg"]:
    doc = cv2.imread(ten)
    sai_lech = np.abs(doc.astype(int) - anh).max()
    print(f"{ten:18s} {os.path.getsize(ten) / 1024:7.1f} KB   sai lệch tối đa: {sai_lech}")

print("Ghi vào thư mục chưa có:", cv2.imwrite("chua_co/anh.png", anh))
True
san_pham.bmp         900.1 KB   sai lệch tối đa: 0
san_pham.png         491.6 KB   sai lệch tối đa: 0
san_pham_q95.jpg      94.1 KB   sai lệch tối đa: 81
san_pham_q50.jpg      11.0 KB   sai lệch tối đa: 92
Ghi vào thư mục chưa có: False

Dung lượng có thể chênh một chút giữa các phiên bản thư viện. Điều cần thấy: PNG và BMP đọc lại đúng từng pixel (sai lệch 0), JPEG thì không, kể cả chất lượng 95. Với ảnh nền nhiễu như trên, JPEG chất lượng thấp còn sai lệch nhiều hơn ở các cạnh và vùng chi tiết.

Định dạngMất dữ liệu?Dùng ở trạm kiểm tra
PNGKhôngLưu ảnh NG để phân tích, dataset AI, ảnh 16 bit. Tham số IMWRITE_PNG_COMPRESSION (0–9) chỉ đổi tốc độ ghi và dung lượng, không đổi chất lượng; với ảnh nhiễu mức nén cao gần như không nhỏ hơn mà chậm hơn.
BMPKhôngGhi nhanh nhất (không nén) khi cycle time gấp, nhưng tốn ổ cứng
JPEGCóLưu ảnh OK số lượng lớn để tra cứu, gửi ảnh qua mạng. Không dùng cho ảnh cần đo đạc lại
TIFFTùy cấu hìnhẢnh 16 bit, ảnh từ phần mềm của hãng camera
imwrite không tự tạo thư mục, và không ném lỗiGhi vào thư mục chưa tồn tại, imwrite trả về False (có bản in thêm cảnh báo) và chương trình chạy tiếp như không có gì. Trên dây chuyền, điều đó nghĩa là cả ca không lưu được ảnh NG nào. Luôn Path(...).mkdir(parents=True, exist_ok=True) trước và kiểm tra giá trị trả về.

3. Đọc ảnh: imread

mau = cv2.imread("san_pham.png")                          # mặc định: màu BGR, uint8
xam = cv2.imread("san_pham.png", cv2.IMREAD_GRAYSCALE)    # đọc thẳng thành ảnh xám
nguyen = cv2.imread("san_pham.png", cv2.IMREAD_UNCHANGED) # giữ nguyên: 16 bit, kênh alpha...
print(mau.shape, xam.shape, nguyen.shape, mau.dtype)

khong_co = cv2.imread("khong_ton_tai.png")
print("File không tồn tại:", khong_co)
(480, 640, 3) (480, 640) (480, 640, 3) uint8
File không tồn tại: None

cv2.imread không báo lỗi khi sai đường dẫn hay file hỏng: nó trả về None (và in một dòng cảnh báo [ WARN:...] ra luồng lỗi). Lỗi chỉ lộ ra ở dòng sau, với thông báo khó hiểu như 'NoneType' object has no attribute 'shape' hoặc (-215:Assertion failed) !_src.empty(). Hãy bọc imread trong một hàm kiểm tra ngay:

from pathlib import Path


def doc_anh(duong_dan, co=cv2.IMREAD_COLOR):
    """Đọc ảnh, báo lỗi rõ ràng thay vì trả về None."""
    anh = cv2.imread(str(duong_dan), co)
    if anh is None:
        ly_do = "không tồn tại" if not Path(duong_dan).exists() else "không giải mã được (hỏng/sai định dạng)"
        raise FileNotFoundError(f"Không đọc được ảnh {duong_dan}: {ly_do}")
    return anh


try:
    doc_anh("anh_ca_dem/ST1_000128.png")
except FileNotFoundError as loi:
    print(loi)
Không đọc được ảnh anh_ca_dem/ST1_000128.png: không tồn tại

OpenCV 5 có thêm cờ cv2.IMREAD_COLOR_RGB để đọc thẳng ra RGB; nếu code cần chạy cả trên OpenCV 4.x, hãy đọc BGR rồi dùng cv2.cvtColor như bài trước.

4. Hiển thị: imshow và waitKey

import cv2

anh = cv2.imread("san_pham.png")
cv2.namedWindow("San pham", cv2.WINDOW_NORMAL)   # cửa sổ kéo giãn được (ảnh 5 MP không vừa màn hình)
cv2.imshow("San pham", anh)
phim = cv2.waitKey(0) & 0xFF                     # 0: chờ đến khi bấm phím
cv2.destroyAllWindows()
print("Bạn đã bấm phím có mã", phim)
  • Tên cửa sổ là định danh: gọi imshow lần nữa với cùng tên thì ảnh mới thay ảnh cũ trong cùng cửa sổ. Nên đặt tên không dấu: thanh tiêu đề trên một số hệ thống hiện sai tiếng Việt.
  • cv2.waitKey(ms) vừa chờ phím vừa cho cửa sổ thời gian để vẽ. Không gọi waitKey thì cửa sổ trắng xóa hoặc "Not Responding". waitKey(0) chờ vô hạn; waitKey(1) chờ khoảng 1 ms, dùng trong vòng lặp video.
  • waitKey trả về mã phím, hoặc -1 nếu hết thời gian mà không có phím nào. Trên một số hệ thống, mã trả về có thêm bit trạng thái ở phần cao, nên ta viết & 0xFF để chỉ giữ 8 bit thấp rồi so với ord("q"). Cần phím mũi tên thì dùng cv2.waitKeyEx.
  • Phím chỉ được nhận khi cửa sổ ảnh đang được chọn (focus), không phải cửa sổ terminal.
Không có màn hình: headless và JupyterVới gói -headless, cv2.imshow báo lỗi The function is not implemented. Khi đó lưu ảnh kết quả bằng imwrite rồi mở bằng trình xem ảnh. Trong notebook Jupyter, tránh imshow (dễ treo kernel); hiển thị bằng matplotlib: plt.imshow(cv2.cvtColor(anh, cv2.COLOR_BGR2RGB)), nhớ đổi BGR sang RGB.

5. Đường dẫn tiếng Việt trên Windows

Trên Windows, cv2.imread và cv2.imwrite của nhiều phiên bản OpenCV không xử lý được đường dẫn có ký tự ngoài bảng mã của hệ thống, ví dụ D:\Ảnh mẫu\lỗi-01.png: imread trả về None, imwrite trả về False, không có lỗi rõ ràng. Thư mục do người vận hành đặt tên có dấu là chuyện rất thường ở nhà máy Việt Nam. Cách chắc chắn chạy được trên mọi hệ điều hành và mọi phiên bản: để Python/NumPy đọc ghi bytes của file, còn OpenCV chỉ mã hóa/giải mã trong bộ nhớ.

from pathlib import Path

import cv2
import numpy as np


def doc_anh_unicode(duong_dan, co=cv2.IMREAD_COLOR):
    """Đọc ảnh từ đường dẫn bất kỳ (kể cả tiếng Việt trên Windows)."""
    du_lieu = np.fromfile(str(duong_dan), dtype=np.uint8)   # file không có → FileNotFoundError
    anh = cv2.imdecode(du_lieu, co)
    if anh is None:
        raise ValueError(f"Không giải mã được ảnh: {duong_dan}")
    return anh


def ghi_anh_unicode(duong_dan, anh, tham_so=()):
    """Ghi ảnh ra đường dẫn bất kỳ; định dạng theo đuôi file; tự tạo thư mục."""
    duong_dan = Path(duong_dan)
    duong_dan.parent.mkdir(parents=True, exist_ok=True)
    ok, buf = cv2.imencode(duong_dan.suffix, anh, list(tham_so))
    if not ok:
        raise ValueError(f"Không mã hóa được ảnh theo định dạng {duong_dan.suffix}")
    buf.tofile(str(duong_dan))


anh = cv2.imread("san_pham.png")
dich = Path("Ảnh mẫu") / "Ca đêm" / "lỗi-xước-01.png"
ghi_anh_unicode(dich, anh)
doc = doc_anh_unicode(dich)
print(dich.as_posix(), doc.shape, "giống ảnh gốc:", np.array_equal(doc, anh))

ghi_anh_unicode(dich.with_suffix(".jpg"), anh, [cv2.IMWRITE_JPEG_QUALITY, 90])
print(sorted(p.name for p in dich.parent.iterdir()))
Ảnh mẫu/Ca đêm/lỗi-xước-01.png (480, 640, 3) giống ảnh gốc: True
['lỗi-xước-01.jpg', 'lỗi-xước-01.png']
  • np.fromfile đọc toàn bộ file thành mảng byte; cv2.imdecode giải mã PNG/JPEG/BMP… từ mảng đó, nhận cùng các cờ IMREAD_* như imread.
  • cv2.imencode(".png", anh) trả về (ok, buf), buf là mảng byte của file PNG; buf.tofile() ghi ra đĩa. Cũng chính imencode dùng khi gửi ảnh qua mạng hay lưu vào cơ sở dữ liệu.
  • Hai hàm này nhanh tương đương imread/imwrite. Nhiều nhóm dùng chúng mặc định trong mọi dự án để khỏi phải nghĩ.

6. Đọc file video

Để có video thử mà không cần camera, ta tự tạo một đoạn "băng tải" 1 giây: một sản phẩm chạy từ trái sang phải. cv2.VideoWriter sẽ được giải thích kỹ ở mục 7.

import cv2
import numpy as np

RONG, CAO, FPS, SO_KHUNG = 640, 480, 30, 30
ghi = cv2.VideoWriter("bang_tai.mp4", cv2.VideoWriter_fourcc(*"mp4v"), FPS, (RONG, CAO))
print("Mở được bộ ghi:", ghi.isOpened())
for i in range(SO_KHUNG):
    khung = np.full((CAO, RONG, 3), 70, np.uint8)
    x = 40 + i * 18                                         # sản phẩm dịch 18 pixel mỗi khung
    cv2.rectangle(khung, (x, 180), (x + 100, 300), (200, 200, 200), -1)
    ghi.write(khung)
ghi.release()
print("Đã ghi", SO_KHUNG, "khung vào bang_tai.mp4")
Mở được bộ ghi: True
Đã ghi 30 khung vào bang_tai.mp4

Đọc video: mở bằng cv2.VideoCapture, gọi read() lặp lại cho đến khi hết. read() trả về cặp (ok, khung); ok là False khi hết video hoặc lỗi.

import time

import cv2

cap = cv2.VideoCapture("bang_tai.mp4")
if not cap.isOpened():
    raise SystemExit("Không mở được video")

fps = cap.get(cv2.CAP_PROP_FPS)
rong = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
cao = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
tong = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))     # với một số định dạng chỉ là ước lượng
print(f"{rong}x{cao}, {fps:.0f} FPS, khoảng {tong} khung = {tong / fps:.1f} s")

dem = 0
t0 = time.perf_counter()
try:
    while True:
        ok, khung = cap.read()
        if not ok:                                # hết video
            break
        dem += 1
        xam = cv2.cvtColor(khung, cv2.COLOR_BGR2GRAY)
        # ... xử lý từng khung ở đây ...
finally:
    cap.release()                                 # luôn giải phóng, kể cả khi có lỗi
print("Đọc được", dem, "khung | khung cuối là None:", khung is None, "| ảnh xám:", xam.shape)
print(f"Tốc độ xử lý: {dem / (time.perf_counter() - t0):.0f} khung/s")
640x480, 30 FPS, khoảng 30 khung = 1.0 s
Đọc được 30 khung | khung cuối là None: True | ảnh xám: (480, 640)
Tốc độ xử lý: 2345 khung/s

Dòng tốc độ phụ thuộc máy. Để ý: sau vòng lặp, khung là None (lần đọc cuối thất bại), vì vậy luôn kiểm tra ok trước khi dùng khung. Đọc file video chạy nhanh hết mức có thể, không theo FPS của video: muốn phát đúng tốc độ thì phải tự chờ (ví dụ waitKey(int(1000 / fps))).

Nhảy tới một khung bất kỳ: cap.set(cv2.CAP_PROP_POS_FRAMES, 100) trước khi read(). Với video nén, việc nhảy có thể không chính xác tuyệt đối đến từng khung tùy codec; khi cần chính xác (ví dụ trích ảnh lỗi để làm dataset), đọc tuần tự và đếm.

7. Ghi video kết quả

cv2.VideoWriter(ten_file, fourcc, fps, (rộng, cao), isColor=True). fourcc là mã 4 ký tự chọn codec:

fourccĐuôi fileGhi chú
mp4v.mp4Có sẵn trong gói pip, file nhỏ, xem được bằng hầu hết trình phát
MJPG.aviMỗi khung là một ảnh JPEG: tương thích rộng, file lớn hơn, cắt khung dễ
XVID.aviPhổ biến trên Windows
avc1 / H264.mp4Nén tốt nhất nhưng tùy bản build, thường không có trong gói pip. Kiểm tra isOpened()

Ví dụ: đọc video băng tải, vẽ số thứ tự khung và vị trí sản phẩm, ghi ra video kết quả để gửi cho đồng nghiệp xem:

import cv2

cap = cv2.VideoCapture("bang_tai.mp4")
fps = cap.get(cv2.CAP_PROP_FPS) or 30            # một số nguồn trả về 0: dùng giá trị dự phòng
rong = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
cao = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))

ghi = cv2.VideoWriter("ket_qua.avi", cv2.VideoWriter_fourcc(*"MJPG"), fps, (rong, cao))
if not ghi.isOpened():
    raise SystemExit("Không tạo được file video (sai codec/đuôi file?)")

so_khung = 0
while True:
    ok, khung = cap.read()
    if not ok:
        break
    so_khung += 1
    cv2.putText(khung, f"Khung {so_khung}", (20, 40), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
    ghi.write(khung)                              # kích thước PHẢI đúng (rong, cao) đã khai báo
cap.release()
ghi.release()                                     # quên release: file hỏng hoặc thiếu đoạn cuối
print("Đã ghi", so_khung, "khung vào ket_qua.avi")
Đã ghi 30 khung vào ket_qua.avi
Ghi sai kích thước khung: video rỗngNếu khung đưa vào write() khác kích thước (rộng, cao) đã khai báo (ví dụ bạn resize hoặc cắt ROI rồi quên sửa bộ ghi), OpenCV không ném lỗi: các khung bị bỏ qua (OpenCV 5 in cảnh báo "Failed to write frame", nhiều bản 4.x im lặng) và bạn nhận được file chỉ vài trăm byte, không mở được. Tương tự, ghi ảnh xám 2 chiều vào bộ ghi màu cũng hỏng: hoặc đổi ảnh về BGR bằng cv2.COLOR_GRAY2BGR, hoặc tạo bộ ghi với isColor=False (bài tập 2).

8. Camera theo thời gian thực

Với webcam USB, chỉ cần thay tên file bằng số thứ tự camera: 0 là camera đầu tiên, 1 là camera thứ hai. Khung chương trình chuẩn: đọc → xử lý → hiển thị → kiểm tra phím, và giải phóng camera ở finally.

import time

import cv2

cap = cv2.VideoCapture(0)                 # Windows: cv2.VideoCapture(0, cv2.CAP_DSHOW) thường mở nhanh hơn
if not cap.isOpened():
    raise SystemExit("Không mở được camera 0 (đang bị phần mềm khác dùng? sai số thứ tự?)")

fps_tb = 0.0
t_truoc = time.perf_counter()
try:
    while True:
        ok, khung = cap.read()
        if not ok:
            print("Mất khung hình, dừng")
            break

        xam = cv2.cvtColor(khung, cv2.COLOR_BGR2GRAY)     # "xử lý" tạm thời

        t_nay = time.perf_counter()
        fps_tuc_thoi = 1.0 / max(t_nay - t_truoc, 1e-6)
        t_truoc = t_nay
        fps_tb = fps_tuc_thoi if fps_tb == 0 else 0.9 * fps_tb + 0.1 * fps_tuc_thoi   # làm mượt

        cv2.putText(khung, f"FPS {fps_tb:.1f}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2)
        cv2.imshow("Camera", khung)
        cv2.imshow("Xam", xam)

        phim = cv2.waitKey(1) & 0xFF                     # gọi waitKey MỘT lần mỗi vòng
        if phim == ord("q") or phim == 27:               # q hoặc Esc
            break
finally:
    cap.release()
    cv2.destroyAllWindows()

Chạy lên, bạn thấy hai cửa sổ ảnh màu và ảnh xám, góc trên hiện FPS (webcam laptop thường khoảng 15–30), bấm q hoặc Esc để thoát. Những điểm đáng học:

  • FPS thực đo bằng time.perf_counter() giữa hai khung, làm mượt bằng trung bình trượt (mỗi khung chỉ đóng góp 10%) để số không nhảy loạn. Đây là FPS của cả vòng lặp: bị giới hạn bởi camera hoặc bởi xử lý của bạn, cái nào chậm hơn. Muốn biết riêng thời gian xử lý, đo quanh đoạn xử lý.
  • waitKey chỉ gọi một lần mỗi vòng rồi lưu vào biến. Viết if cv2.waitKey(1) == ord("q") rồi lại if cv2.waitKey(1) == ord("s") sẽ làm "mất" phím bấm.
  • cap.release() trong finally: nếu chương trình lỗi giữa chừng mà không giải phóng, camera có thể bị "giữ" và lần chạy sau báo không mở được.
  • Với camera, một lần read() thất bại có thể chỉ là trục trặc tạm thời (cáp USB lỏng). Ở trạm thật, nên đếm số lần lỗi liên tiếp, ghi log, thử mở lại camera thay vì dừng ngay.

Thuộc tính camera: set rồi phải get lại

import cv2

cap = cv2.VideoCapture(0)
yeu_cau = {cv2.CAP_PROP_FRAME_WIDTH: 1280, cv2.CAP_PROP_FRAME_HEIGHT: 720, cv2.CAP_PROP_FPS: 30}
for thuoc_tinh, gia_tri in yeu_cau.items():
    cap.set(thuoc_tinh, gia_tri)

for _ in range(10):                  # bỏ vài khung đầu: tự động phơi sáng/cân bằng trắng đang ổn định
    cap.read()

rong = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
cao = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
fps = cap.get(cv2.CAP_PROP_FPS)
print(f"Camera thực sự chạy: {rong}x{cao} @ {fps:.0f} FPS")
cap.release()

cap.set() chỉ là yêu cầu: camera không hỗ trợ độ phân giải đó thì driver chọn mức gần nhất, và set vẫn có thể trả về True. Vì vậy luôn get lại (hoặc xem khung.shape) để biết giá trị thật. Webcam laptop thường hỗ trợ 640×480 và 1280×720; nếu yêu cầu 1920×1080 mà nhận về 640×480, đó là giới hạn của camera.

Thuộc tínhÝ nghĩaLưu ý
CAP_PROP_FRAME_WIDTH / HEIGHTĐộ phân giảiChỉ nhận các mức camera hỗ trợ
CAP_PROP_FPSTốc độ khungPhơi sáng dài trong phòng tối sẽ kéo FPS xuống
CAP_PROP_AUTO_EXPOSURE, CAP_PROP_EXPOSURETự động/thủ công thời gian phơi sángGiá trị và đơn vị khác nhau tùy backend và driver (DirectShow, Media Foundation, V4L2…); thử rồi get lại
CAP_PROP_AUTOFOCUS, CAP_PROP_FOCUSLấy nétỞ trạm kiểm tra nên tắt tự lấy nét để ảnh ổn định
CAP_PROP_BUFFERSIZESố khung trong hàng đợiĐặt 1 để giảm độ trễ; chỉ một số backend hỗ trợ
Webcam khác camera công nghiệpVideoCapture dùng tốt cho webcam USB và camera IP qua RTSP (cv2.VideoCapture("rtsp://...")). Camera công nghiệp chuẩn GigE Vision/USB3 Vision thường cần SDK của hãng (hoặc thư viện GenICam như Harvesters) để điều khiển trigger, exposure, gain đúng cách; SDK trả về khung hình, bạn chuyển thành mảng NumPy và xử lý bằng OpenCV như bình thường. Các chặng về camera và tích hợp sẽ học kỹ phần này. Ngoài ra, webcam tự động phơi sáng và cân bằng trắng: ánh sáng thay đổi thì ảnh thay đổi, nên kết quả kiểm tra bằng webcam luôn kém ổn định hơn hệ thống có đèn và camera chuyên dụng.

9. Lỗi thường gặp

Hiện tượngNguyên nhânCách xử lý
(-215:Assertion failed) !_src.empty()Ảnh là None do imread thất bại hoặc read() hết khungKiểm tra None/ok ngay sau khi đọc
imread trả về None dù file có thật (Windows)Đường dẫn có dấu tiếng Việtnp.fromfile + cv2.imdecode
Cửa sổ trắng, "Not Responding"Không gọi waitKeyGọi cv2.waitKey(1) mỗi vòng
The function is not implemented khi imshowCài bản headless hoặc cài lẫn hai góiGỡ hết, cài lại opencv-python
Camera không mở đượcPhần mềm khác (Zoom, Teams, lần chạy trước chưa release) đang giữ camera; sai số thứ tựĐóng phần mềm kia; thử 0, 1, 2; luôn release()
File video vài trăm byte, không mở đượcKhung sai kích thước, ảnh xám vào bộ ghi màu, codec không có, quên release()Kiểm tra isOpened(), kích thước khung, isColor
Vài khung đầu tối hoặc ám màuWebcam đang tự chỉnh phơi sángBỏ 5–10 khung đầu

Tóm tắt

  • Cài một gói OpenCV; opencv-python nếu cần cửa sổ, -headless cho máy chủ.
  • imread trả về None chứ không ném lỗi; imwrite chọn định dạng theo đuôi, trả về False khi thất bại và không tự tạo thư mục. Ảnh cần đo lại: PNG/BMP, không JPEG.
  • Đường dẫn tiếng Việt trên Windows: np.fromfile + imdecode, imencode + tofile.
  • Video/camera: VideoCapture → vòng read() kiểm tra ok → release() trong finally; waitKey(1) & 0xFF một lần mỗi vòng.
  • VideoWriter: fourcc khớp đuôi file, khung đúng kích thước, isColor=False cho ảnh xám, nhớ release().
  • Thuộc tính camera: set chỉ là yêu cầu, luôn get lại; đo FPS thực bằng time.perf_counter().

Bài tập

Bài 1. Chụp ảnh từ webcam bằng phím

Viết chương trình mở webcam, hiển thị hình liên tục. Nhấn s để lưu khung hình hiện tại vào thư mục anh_chup với tên file chứa thời gian (đến mili giây, để chụp nhanh hai lần trong một giây không bị ghi đè). Nhấn q để thoát và in tổng số ảnh đã lưu.

Xem đáp án
from datetime import datetime
from pathlib import Path

import cv2

THU_MUC = Path("anh_chup")
THU_MUC.mkdir(exist_ok=True)

cap = cv2.VideoCapture(0)
if not cap.isOpened():
    raise SystemExit("Không mở được camera")

so_anh = 0
try:
    while True:
        ok, khung = cap.read()
        if not ok:
            break
        hien_thi = khung.copy()                      # vẽ lên bản sao, LƯU ảnh gốc
        cv2.putText(hien_thi, f"s: chup | q: thoat | da luu: {so_anh}", (10, 25),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)
        cv2.imshow("Webcam", hien_thi)

        phim = cv2.waitKey(1) & 0xFF
        if phim == ord("s"):
            ten = datetime.now().strftime("anh_%Y%m%d_%H%M%S_%f")[:-3] + ".png"   # %f là micro giây
            duong_dan = THU_MUC / ten
            if cv2.imwrite(str(duong_dan), khung):
                so_anh += 1
                print("Đã lưu", duong_dan.as_posix())
            else:
                print("LỖI: không lưu được", duong_dan)
        elif phim == ord("q"):
            break
finally:
    cap.release()
    cv2.destroyAllWindows()
print("Tổng số ảnh đã lưu:", so_anh)

Ví dụ kết quả khi bấm s một lần rồi q (thời gian trong tên file sẽ khác):

Đã lưu anh_chup/anh_20261009_222840_103.png
Tổng số ảnh đã lưu: 1

Ba chi tiết quan trọng: lưu khung gốc chứ không lưu ảnh đã vẽ chữ (ảnh dataset không được có chữ đè lên); %f cho micro giây, cắt bớt 3 chữ số còn mili giây; kiểm tra giá trị trả về của imwrite. Tên file dạng năm-tháng-ngày-giờ còn có lợi là sắp xếp theo tên cũng chính là theo thời gian. Muốn lưu vào thư mục tiếng Việt, thay cv2.imwrite bằng hàm ghi_anh_unicode ở mục 5.

Bài 2. Chuyển video sang ảnh xám

Đọc video bang_tai.mp4 (mục 6), chuyển từng khung sang ảnh xám và ghi ra video mới bang_tai_xam.mp4. Sau đó mở lại video mới, đếm số khung và in kích thước khung đọc được.

Xem đáp án
import cv2

cap = cv2.VideoCapture("bang_tai.mp4")
fps = cap.get(cv2.CAP_PROP_FPS) or 30
rong = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
cao = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
ghi = cv2.VideoWriter("bang_tai_xam.mp4", cv2.VideoWriter_fourcc(*"mp4v"), fps, (rong, cao),
                      isColor=False)                     # bộ ghi nhận ảnh xám 2 chiều
if not ghi.isOpened():
    raise SystemExit("Không tạo được bộ ghi video")

so_khung = 0
try:
    while True:
        ok, khung = cap.read()
        if not ok:
            break
        xam = cv2.cvtColor(khung, cv2.COLOR_BGR2GRAY)    # shape (cao, rong)
        ghi.write(xam)
        so_khung += 1
finally:
    cap.release()
    ghi.release()
print("Đã ghi", so_khung, "khung xám")

kiem_tra = cv2.VideoCapture("bang_tai_xam.mp4")
dem, kich_thuoc = 0, None
while True:
    ok, khung = kiem_tra.read()
    if not ok:
        break
    dem += 1
    kich_thuoc = khung.shape
kiem_tra.release()
print("Đọc lại:", dem, "khung, kích thước", kich_thuoc)
Đã ghi 30 khung xám
Đọc lại: 30 khung, kích thước (480, 640, 3)

Để ý dòng cuối: video ghi bằng ảnh xám nhưng đọc lại vẫn ra 3 kênh. VideoCapture mặc định trả khung BGR (ba kênh giống nhau), nên chương trình đọc video xám vẫn phải cvtColor sang xám nếu cần mảng 2 chiều. Nếu quên isColor=False mà vẫn ghi ảnh xám 2 chiều, file sẽ rỗng như cảnh báo ở mục 7.

Bài 3. Lưu ảnh NG vào thư mục tiếng Việt

Viết hàm luu_anh_ng(anh, ma_loi) lưu ảnh vào Ảnh lỗi/<ngày dạng YYYY-MM-DD>/<mã lỗi>/ với tên file là số thứ tự tăng dần 0001.png, 0002.png… (không ghi đè ảnh cũ), chạy được với đường dẫn có dấu trên Windows. Trả về đường dẫn đã lưu. Thử lưu 3 ảnh với mã lỗi "xước" và 1 ảnh "móp".

Xem đáp án
from datetime import date
from pathlib import Path

import cv2
import numpy as np


def luu_anh_ng(anh, ma_loi, goc=Path("Ảnh lỗi")):
    thu_muc = goc / date.today().isoformat() / ma_loi
    thu_muc.mkdir(parents=True, exist_ok=True)
    stt = len(list(thu_muc.glob("*.png"))) + 1
    duong_dan = thu_muc / f"{stt:04d}.png"
    while duong_dan.exists():                      # phòng khi có file bị xóa ở giữa
        stt += 1
        duong_dan = thu_muc / f"{stt:04d}.png"
    ok, buf = cv2.imencode(".png", anh)
    if not ok:
        raise ValueError("Không mã hóa được ảnh")
    buf.tofile(str(duong_dan))
    return duong_dan


anh = np.zeros((60, 80, 3), np.uint8)
for ma in ["xước", "xước", "xước", "móp"]:
    p = luu_anh_ng(anh, ma)
    print(p.parent.name, p.name, cv2.imdecode(np.fromfile(str(p), np.uint8), cv2.IMREAD_COLOR).shape)
xước 0001.png (60, 80, 3)
xước 0002.png (60, 80, 3)
xước 0003.png (60, 80, 3)
móp 0001.png (60, 80, 3)

Đếm file rồi cộng 1 là cách đơn giản để đánh số; vòng while đảm bảo không ghi đè nếu số thứ tự bị lủng. Ở trạm thật nhiều luồng cùng lưu ảnh, dùng tên theo thời gian như bài 1 an toàn hơn.

Bài tiếp theo: Không gian màu BGR, HSV, Gray và tách màu, bạn sẽ bắt đầu phân tích nội dung khung hình vừa đọc được: tách vật theo màu sắc một cách ổn định dưới ánh sáng thay đổi.

Bài tập thực hành

  1. Viết chương trình mở webcam, nhấn phím s để lưu ảnh kèm thời gian vào tên file, nhấn q để thoát.
  2. Đọc một video, chuyển sang ảnh xám và ghi ra video mới.

Làm xong bài tập mới chuyển sang bài tiếp theo: tự viết code là cách duy nhất để nhớ lâu.

Bài tiếp theo
Ban biên tập HọcFree

Chúng tôi viết những bài hướng dẫn dễ hiểu, thực tế để ai cũng có thể tự học lập trình và ngoại ngữ, hoàn toàn miễn phí.