Bắt hình của IP Camera từ ứng dụng Python

Thỉnh thoảng tôi bắt gặp câu hỏi "Làm thế nào để lấy ảnh chụp của IP Camera bằng code Python". Hầu hết câu trả lời, của cả Tây lẫn Việt, đều là kêu người ta dùng OpenCV.

Camera

Quan điểm của tôi là, OpenCV tốt đấy, nhưng nếu chỉ để lấy ảnh chụp của camera thôi mà dùng OpenCV thì chuối quá. Thứ nhất, OpenCV là thư viện dành cho computer vision, nên nếu không khai thác gì đến tính năng đó mà ôm OpenCV vào thì nặng nề, dư thừa không cần thiết. Đằng nào thì tự OpenCV cũng không có khả năng xử lý giao thức RTSP mà nó phải gọi đến FFmpeg , nên để cho gọn ghẽ, tốt hơn là dùng thẳng FFmpeg để bắt hình camera đi. Thứ hai, tệ hơn nữa, mặc dù OpenCV sử dụng FFmpeg bên dưới, nó cũng không khai thác FFmpeg đúng cách nên có nhiều IP Camera cũng truyền video qua giao thức RTSP mà nó không lấy được. Lí do là RTSP có thể truyền trên TCP hoặc UDP, nhưng OpenCV được hardcode để gọi FFmpeg với chỉ mỗi TCP, thành ra nếu IP Camera truyền với "RTSP over UDP" thì OpenCV không lấy được.

Vậy giải pháp là gì?

Do phong cách của tôi là ưu tiên những giải pháp "mỏng tang", vừa đủ nhu cầu, nên khi cần bắt ảnh chụp camera, tôi dùng PyAV, là một thư viện để Python gọi trực tiếp đến FFmpg, không thông qua lớp OpenCV. Đây là ví dụ cách dùng:

from typing import Optional

import av
from PIL.Image import Image


def take_photo_from_video_source(video_url: str, rtsp_transport: str) -> Optional[Image]:
    video_options = {
        'rtsp_transport': rtsp_transport,
        'pix_fmts': 'yuv420p|rgb24|yuv444p',
    }
    try:
        container = av.open(video_url, options=video_options)
    except av.AVError as e:
        logger.error("IP Camera is not online. Error {}", e)
        return
    frame = next(container.decode(video=0))
    return frame.to_image()

Trong ví dụ này thì biến rtsp_transport có thể nhận giá trị tcp, udp hay các giá trị khác như liệt kê ở đây: https://ffmpeg.org/ffmpeg-protocols.html#rtsp. Hàm này tôi cho trả về giá trị kiểu from PIL.Image.Image để sau đó tôi có thể biến đổi thêm nữa (như resize hay biến thành trắng đen chẳng hạn).

Ghi chú:

  1. Tên của thư viện "PyAV" trông không có gì gợi liên tưởng đến FFmpeg. Thực ra, "AV" bắt nguồn từ "LibAV", là một tên khác của FFmpeg. Trong lịch sử phát triển của FFmpeg, có thời điểm dự án gặp sự bất đồng nội bộ, khiến một nhóm tác giả tách ra, lập dự án mới đặt tên là LibAV. Sau đó bất đồng được giải quyết, "LibAV" và "FFmpeg" được hợp nhất lại.

  2. Nhìn vào đoạn code sau của OpenCV:

    #if LIBAVFORMAT_BUILD >= CALC_FFMPEG_VERSION(52, 111, 0)
    #ifndef NO_GETENV
        char* options = getenv("OPENCV_FFMPEG_CAPTURE_OPTIONS");
        if(options == NULL)
        {
            av_dict_set(&dict, "rtsp_transport", "tcp", 0);
        }
        else
        {
    

    thì có vẻ OpenCV có thể nhận vào option để gọi FFmpeg thông qua biến môi trường OPENCV_FFMPEG_CAPTURE_OPTIONS, nhưng tôi chưa thử. Đó không phải là cách làm thuận tiện, vì biến môi trường thường được dùng để cấu hình ứng dụng cho suốt vòng đời của nó, từ lúc khởi chạy đến lúc tắt đi, trong khi trong một ứng dụng, tôi có nhu cầu bắt hình từ nhiều camera khác nhau và muốn có option riêng cho mỗi camera. Tất nhiên bạn có thể dùng code để tự làm thay đổi biến môi trường, nhưng đó ko phải là cách hay khi trong suốt vòng đời của ứng dụng mà biến môi trường cứ thay đổi xoành xoạch.