5.33. Luồng ImageIO

save()to_jpeg() xử lý trường hợp I/O một khung hình: ứng dụng chụp một khung hình, mã hóa và đẩy nó đến đâu đó. Một loại ứng dụng khác cần xử lý trường hợp chuỗi: ghi lại nhiều khung hình liên tiếp ở tốc độ chụp tự nhiên, lưu trữ chúng ở nơi có thể truy xuất sau, và phát lại với tốc độ phù hợp. Một tập lệnh thu thập dữ liệu huấn luyện chụp vài trăm khung hình mẫu cho pipeline học máy; nhật ký trạm kiểm tra ghi lại mọi linh kiện đã chụp để truy xuất nguồn gốc; một tập lệnh phát triển phát lại chuỗi đã lưu để kiểm tra thuật toán mới với dữ liệu đã được chụp trực tiếp trước đó.

Lớp ImageIO là bộ ghi/phát của mô-đun ảnh. Một luồng duy nhất chứa một chuỗi các khung hình Image -- có thể có kích thước và định dạng điểm ảnh khác nhau -- cùng với khoảng cách giữa các khung hình, để quá trình phát lại có thể tái tạo tốc độ khung hình gốc. Có hai kho lưu trữ: tệp trên hệ thống tập tin hoặc bộ đệm có kích thước cố định trong RAM.

5.33.1. Hai kho lưu trữ

Một luồng tệp lưu trữ bản ghi qua các chu kỳ nguồn điện và được giới hạn chỉ bởi dung lượng bộ nhớ hỗ trợ. Nó bắt đầu với tiêu đề magic 16 byte OMV IMG STR Vx.y theo sau là một chunk mỗi khung hình; bộ ghi hiện tại phát ra V2.0 và bộ đọc vẫn chấp nhận các tệp V1.0V1.1 để tương thích ngược. Đường dẫn tệp là đối số của hàm tạo; chế độ là chế độ mở tệp ('r' để đọc luồng đã có, 'w' để xóa và ghi mới).

# Recording to /sdcard/run.bin
stream = image.ImageIO("/sdcard/run.bin", "w")
for _ in range(120):
    img = csi0.snapshot()
    stream.write(img)
stream.close()

Một luồng bộ nhớ tồn tại trong bộ đệm RAM được phân bổ khi khởi tạo. Hàm tạo nhận một bộ 3 giá trị (w, h, pixformat) thay vì đường dẫn, và đối số mode trở thành số lượng slot khung hình được phân bổ trước. Bộ đệm được phân bổ đúng kích thước cho số lượng khung hình đó với các kích thước đã cung cấp và không được phép tăng thêm sau khi phân bổ -- ghi vượt quá slot cuối cùng sẽ phát sinh EOFError, và ghi một khung hình lớn hơn bộ đệm per-slot sẽ phát sinh ValueError. Luồng bộ nhớ là công cụ phù hợp khi ứng dụng cần chuyển bản ghi đến giai đoạn tiếp theo mà không cần đi qua hệ thống tập tin (chẳng hạn như vòng đệm ngắn của các khung hình gần đây cho kiểu ghi-và-phát lại).

# Pre-allocate space for 32 QVGA RGB565 frames in RAM
stream = image.ImageIO((320, 240, image.RGB565), 32)
for _ in range(32):
    stream.write(csi0.snapshot())

Đối với các định dạng điểm ảnh nén (image.JPEG, image.PNG), kích thước per-slot được ước tính là 2 bit mỗi điểm ảnh; một khung hình được mã hóa lớn hơn ước tính sẽ phát sinh ValueError tại thời điểm ghi, vì vậy ứng dụng dự kiến lưu trữ JPEG chất lượng cao cần phải phân bổ thừa số lượng slot hoặc mã hóa ở chất lượng thấp hơn trước.

type() trả về image.ImageIO.FILE_STREAM hoặc image.ImageIO.MEMORY_STREAM để code tiếp theo có thể thích ứng với kho lưu trữ nào được cung cấp.

5.33.2. Ghi lại

write() nối thêm một Image đã chụp vào luồng tệp (hoặc lưu vào slot hiện tại của luồng bộ nhớ) và tiến vị trí offset lên một. Cùng một lệnh gọi đó cũng ghi lại khoảng cách giữa các khung hình kể từ lần ghi cuối, để phần phát lại có thể tạm dừng đúng thời lượng giữa các khung hình và tốc độ khung hình tự nhiên của bản ghi được bảo tồn.

Các khung hình không đồng nhất được cho phép trong một luồng tệp: một bản ghi có thể kết hợp tự do các ảnh chụp RGB565, các ảnh cắt xén thang xám và các hình thu nhỏ mã hóa JPEG, và bộ đọc sẽ giải mã mỗi cái theo kích thước và định dạng gốc của nó. Luồng bộ nhớ là đồng nhất (tất cả các slot chia sẻ (w, h, pixformat) được cung cấp cho hàm tạo), do đó bản ghi bộ nhớ bị giới hạn ở một cấu hình khung hình.

write() trả về đối tượng luồng để các lệnh gọi có thể xâu chuỗi. Ghi tại offset không phải cuối tệp của luồng tệp sẽ cắt bớt phần còn lại của tệp -- hữu ích cho việc chỉnh sửa chuỗi đã lưu, nhưng rủi ro nếu vị trí ghi tiếp theo đã bị di chuyển ngoài ý muốn bởi seek() trước đó.

sync() xả các lần ghi đang chờ ra đĩa cho các luồng tệp (đây là no-op trên luồng bộ nhớ) và nên được gọi định kỳ khi bản ghi chạy lâu, để tránh mất phần cuối của bản ghi nếu cam khởi động lại trước khi tệp được đóng. Hàm hủy đóng luồng tự động khi ImageIO ra ngoài phạm vi, nhưng gọi close() tường minh là phương pháp đúng đắn.

5.33.3. Phát lại

read() đọc khung hình tại offset hiện tại, tiến offset lên, và trả về Image mới. Khung hình nằm trong bộ đệm khung hình khi copy_to_fb=True (mặc định) để ảnh trả về có thể vẽ qua IDE preview; với copy_to_fb=False khung hình sẽ được đặt trên heap MicroPython.

# Loop a recorded stream at its natural frame rate
stream = image.ImageIO("/sdcard/run.bin", "r")
while True:
    img = stream.read()
    # img is now in the frame buffer; the IDE shows it
    # and the script can run any analysis it likes

Hai từ khóa điều khiển hành vi phát lại. loop=True (mặc định cho luồng tệp) đưa con trỏ đọc về đầu khi đến cuối bản ghi, vì vậy lệnh gọi không bao giờ trả về None; loop=False trả về None khi bản ghi đã hết và vòng lặp của bên gọi kết thúc. pause=True (mặc định) chặn lệnh gọi cho đến khi khoảng cách giữa các khung hình được ghi tại thời điểm ghi đã trôi qua, để tốc độ khung hình phát lại khớp với tốc độ chụp gốc; pause=False trả về ngay lập tức, hữu ích cho các pipeline phân tích muốn xử lý nhanh bản ghi mà không tuân theo thời gian gốc.

Kiểu vòng lặp tương tự hoạt động cho luồng bộ nhớ ngoại trừ loop bị bỏ qua -- đọc vượt quá cuối luồng bộ nhớ sẽ phát sinh EOFError. Kiểu dự kiến cho vòng bộ nhớ là dùng seek() về không một cách tường minh khi muốn quay vòng.

5.33.5. Bản ghi phát được trên host

Luồng ImageIO là công cụ phù hợp khi bản ghi sẽ được phát lại trên cam -- chúng bảo tồn mọi khung hình đã chụp ở định dạng điểm ảnh gốc, khoảng cách giữa các khung hình được ghi chính xác, và một tập lệnh tiếp theo có thể duyệt qua chúng, seek, và phân tích lại mà không mất dữ liệu. Tuy nhiên, chúng không phải là công cụ phù hợp khi bản ghi phải phát được trên host -- máy trạm, điện thoại, trình phát web. Host mong đợi một container video tiêu chuẩn, không phải định dạng magic-header trên đĩa của OpenMV.

Hai mô-đun riêng biệt xử lý trường hợp phát được trên host. Mô-đun mjpeg ghi Motion JPEG: một chuỗi các khung hình nén JPEG được đóng gói vào một container kiểu AVI mà VLC, QuickTime, ffmpeg và thẻ video web tiêu chuẩn đều phát trực tiếp. Mô-đun gif ghi một GIF động: một chuỗi các khung hình không nén (hoặc nén bảng màu) với độ trễ per-frame tường minh, phát được trên mọi trình duyệt web hoặc trình xem ảnh hỗ trợ GIF động.

Mô-đun mjpeg là lựa chọn tự nhiên cho các bản ghi dài. Nén JPEG giữ kích thước tệp ở mức có thể quản lý -- tương đương với to_jpeg() ở chất lượng đã cấu hình, khung hình sau khung hình -- vì vậy một phiên chụp kéo dài vẫn trong ngân sách thẻ SD. Cách sử dụng phản chiếu chặt chẽ bản ghi ImageIO:

import mjpeg

m = mjpeg.Mjpeg("/sdcard/run.mjpeg")
while running:
    m.add_frame(csi0.snapshot(), quality=85)
m.close()

mjpeg.Mjpeg chấp nhận các từ khóa vị trí và tỷ lệ theo kiểu vẽ tương tự các phương thức ảnh khác, vì vậy một bản ghi có thể được thu nhỏ, cắt xén hoặc ánh xạ bảng màu mỗi khung hình trong quá trình ghi. Các đối số widthheight của hàm tạo mặc định theo kích thước của bộ đệm khung hình chính và cố định độ phân giải đầu ra; mọi khung hình được nối thêm đều được thu nhỏ (giữ tỷ lệ khung hình) để vừa. sync() xả tệp ra đĩa trong quá trình ghi dài, và close() hoàn tất container -- một tệp Motion JPEG chưa được đóng đúng cách sẽ không phát được, vì vậy phương pháp này rất quan trọng.

Mô-đun gif là lựa chọn tự nhiên cho các bản ghi ngắn được chia sẻ trực tiếp với người xem không có kiến thức kỹ thuật -- vài giây hành động chụp cho demo, hình minh họa động cho tài liệu, đoạn sự kiện nhúng trong tin nhắn chat. Các khung hình GIF được lưu không nén (hoặc nén bảng màu ở độ sâu màu 7-bit), làm cho các tệp lớn hơn nhiều mỗi giây so với Motion JPEG và loại định dạng này ra khỏi các bản ghi dài hơn vài giây, nhưng kết quả đặt trực tiếp vào bất kỳ trình duyệt nào:

import gif

g = gif.Gif("/sdcard/clip.gif")
while running:
    g.add_frame(csi0.snapshot(), delay=10)
g.close()

Đối số delay trên add_frame() là thời gian hiển thị per-frame theo centi-giây (10 là 100 ms mỗi khung hình, hoặc 10 fps), đây là điều khiển phát lại GIF tiêu chuẩn. Từ khóa loop của hàm tạo thiết lập liệu clip kết quả có tự động lặp lại trong các trình xem không (mặc định là True, phù hợp với kỳ vọng thông thường về "GIF động").

Ba đường ghi lại bao gồm các trường hợp phổ biến giữa chúng: ImageIO cho xử lý lại trên cam, Motion JPEG cho các bản ghi dài phát được trên host, GIF động cho các đoạn ngắn phát được trên host. Sự lựa chọn giữa chúng phụ thuộc vào ai phát lại bản ghi. Một giai đoạn tiếp theo chạy trên cam đọc ImageIO; một máy trạm host hoặc trình xem web đọc MJPEG hoặc GIF.

5.33.6. Kiểu ghi-và-phát lại

Một kiểu hữu ích kết hợp luồng bộ nhớ với điều kiện kích hoạt. Cam ghi liên tục vào vòng đệm bộ nhớ count slot, ghi đè slot cũ nhất mỗi lần quay vòng. Khi điều kiện kích hoạt được kích hoạt (một vùng màu (blob) vào khung hình, một sự kiện chuyển động vượt quá ngưỡng, một nút được nhấn), ứng dụng chụp ảnh nhanh nội dung của vòng -- count khung hình gần đây nhất -- và ghi chúng vào luồng tệp trên thẻ SD. Kết quả là một bản ghi trước khi kích hoạt chụp các giây trước sự kiện mà cam thực sự nhận thấy, chứ không chỉ các giây sau, đây là giới hạn cổ điển của bộ ghi ngây thơ "chụp khi kích hoạt".

Việc triển khai rất đơn giản khi đã có các lớp luồng trong tay: một luồng bộ nhớ có kích thước cố định phục vụ như vòng (với seek() tường minh về không khi offset đạt đến số lượng slot), vòng lặp chính chụp vào nó ở mỗi lần lặp, và trình xử lý kích hoạt đọc từng khung hình từ luồng bộ nhớ và ghi mỗi cái vào luồng tệp được đặt tên theo dấu thời gian của kích hoạt.