5.33. Stream ImageIO¶
save() dan to_jpeg() menangani kasus I/O satu bingkai: sebuah aplikasi menangkap bingkai, mengodekannya, dan mengirimkannya ke suatu tempat. Kelas aplikasi lainnya membutuhkan kasus urutan: merekam banyak bingkai secara berurutan pada laju penangkapan alami, menyimpannya di tempat yang dapat diambil kembali nanti, dan memutarnya pada kecepatan yang tepat. Sebuah skrip pengumpulan data pelatihan menangkap beberapa ratus bingkai contoh untuk jalur pembelajaran mesin; log stasiun inspeksi merekam setiap bagian yang ditangkap untuk keterlacakan; sebuah skrip pengembangan memutar ulang urutan yang tersimpan untuk menguji algoritma baru terhadap data yang sebelumnya ditangkap secara langsung.
Kelas ImageIO adalah perekam/pemutar modul citra. Satu stream menyimpan urutan bingkai Image -- yang mungkin memiliki ukuran dan format piksel berbeda -- bersama dengan interval antar-bingkai dari setiap bingkai, sehingga pemutaran dapat menciptakan kembali laju bingkai asli. Dua penyimpanan tersedia: file pada filesystem atau buffer berukuran tetap di RAM.
5.33.1. Dua penyimpanan¶
Sebuah file stream menyimpan rekaman secara permanen melalui siklus daya dan ukurannya hanya dibatasi oleh penyimpanan yang mendukungnya. Dimulai dengan header ajaib 16-byte OMV IMG STR Vx.y diikuti satu potongan per bingkai; penulis saat ini menghasilkan V2.0 dan pembaca masih menerima file V1.0 dan V1.1 untuk kompatibilitas mundur. Path file adalah argumen konstruktor; mode adalah mode pembukaan file ('r' untuk membaca stream yang ada, 'w' untuk memotong dan menulis baru).
# Recording to /sdcard/run.bin
stream = image.ImageIO("/sdcard/run.bin", "w")
for _ in range(120):
img = csi0.snapshot()
stream.write(img)
stream.close()
Sebuah memory stream berada di buffer RAM yang dialokasikan saat konstruksi. Konstruktor mengambil 3-tuple (w, h, pixformat) alih-alih path, dan argumen mode menjadi jumlah slot bingkai yang telah dialokasikan sebelumnya. Buffer berukuran tepat untuk sejumlah bingkai pada dimensi yang diberikan dan tidak boleh tumbuh setelah dialokasikan -- menulis melewati slot terakhir akan menimbulkan EOFError, dan menulis bingkai yang lebih besar dari buffer per-slot akan menimbulkan ValueError. Memory stream adalah alat yang tepat ketika aplikasi perlu meneruskan sebuah rekaman ke tahap berikutnya tanpa melalui filesystem (misalnya, buffer ring pendek dari bingkai terbaru untuk pola trigger-dan-putar ulang).
# 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())
Untuk format piksel terkompresi (image.JPEG, image.PNG) ukuran per-slot diperkirakan sebesar 2 bit per piksel; bingkai yang telah dikodekan lebih besar dari perkiraan akan menimbulkan ValueError saat penulisan, sehingga aplikasi yang berharap menyimpan JPEG berkualitas tinggi harus memberi alokasi berlebih pada jumlah slot atau mengodekan pada kualitas yang lebih rendah terlebih dahulu.
type() mengembalikan image.ImageIO.FILE_STREAM atau image.ImageIO.MEMORY_STREAM sehingga kode hilir dapat beradaptasi dengan penyimpanan mana pun yang diberikan.
5.33.2. Perekaman¶
write() menambahkan Image yang ditangkap ke file stream (atau menyimpannya di slot saat ini pada memory stream) dan memajukan offset sebesar satu. Panggilan yang sama merekam interval antar-bingkai sejak penulisan terakhir, sehingga bagian pemutaran dapat menjeda selama waktu yang tepat antara bingkai dan laju bingkai alami rekaman terjaga.
Bingkai heterogen diperbolehkan dalam satu file stream: rekaman dapat mencampur tangkapan RGB565, cropping skala abu-abu, dan thumbnail yang dikodekan JPEG secara bebas, dan pembaca akan mendekode setiap bingkai pada ukuran dan format aslinya. Memory stream bersifat homogen (semua slot berbagi (w, h, pixformat) yang diberikan konstruktor), sehingga rekaman memori dibatasi pada satu konfigurasi bingkai.
write() mengembalikan objek stream sehingga panggilan dapat dirangkai. Menulis pada offset non-akhir dari sebuah file stream akan memotong sisa file -- berguna untuk mengedit urutan yang tersimpan, namun berisiko jika posisi tulis berikutnya dipindahkan secara tidak sengaja oleh seek() sebelumnya.
sync() membilas penulisan tertunda ke disk untuk file stream (ini adalah no-op pada memory stream) dan harus dipanggil secara berkala ketika rekaman berjalan lama, untuk menghindari kehilangan ekor rekaman jika kamera melakukan reboot sebelum file ditutup. Destruktor menutup stream secara otomatis ketika ImageIO keluar dari cakupan, tetapi close() eksplisit adalah disiplin yang tepat.
5.33.3. Pemutaran¶
read() membaca bingkai pada offset saat ini, memajukan offset, dan mengembalikan Image baru. Penerima tetap berada di buffer bingkai ketika copy_to_fb=True (default) sehingga citra yang dikembalikan dapat digambar melalui pratinjau IDE; dengan copy_to_fb=False bingkai mendarat di 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
Dua kata kunci mengontrol perilaku pemutaran. loop=True (default untuk file stream) membalik penunjuk baca kembali ke awal ketika akhir rekaman tercapai, sehingga panggilan tidak pernah mengembalikan None; loop=False mengembalikan None setelah rekaman habis dan perulangan pemanggil berakhir. pause=True (default) memblokir panggilan hingga interval antar-bingkai yang direkam saat penulisan telah berlalu, sehingga laju bingkai pemutaran sesuai dengan laju bingkai penangkapan asli; pause=False langsung kembali, berguna untuk jalur analisis yang ingin memproses rekaman secepat mungkin tanpa memperhatikan waktu asli.
Pola perulangan yang sama berlaku untuk memory stream kecuali bahwa loop diabaikan -- membaca melewati akhir memory stream akan menimbulkan EOFError. Pola yang diharapkan untuk ring memori adalah melakukan seek() kembali ke nol secara eksplisit ketika pembungkusan diinginkan.
5.33.5. Rekaman yang dapat diputar di host¶
Stream ImageIO adalah alat yang tepat ketika rekaman akan diputar kembali di kamera -- setiap bingkai yang ditangkap disimpan dalam format piksel aslinya, interval antar-bingkai direkam secara tepat, dan skrip hilir dapat menelusuri, mencari, dan menganalisis ulang tanpa kehilangan data. Namun, ini bukan alat yang tepat ketika rekaman harus dapat diputar di host -- workstation, ponsel, atau pemutar web. Host mengharapkan kontainer video standar, bukan format header ajaib OpenMV di disk.
Dua modul terpisah menangani kasus yang dapat diputar di host. Modul mjpeg merekam Motion JPEG: urutan bingkai yang dikompresi JPEG yang dikemas dalam kontainer gaya AVI yang dapat diputar langsung oleh VLC, QuickTime, ffmpeg, dan tag video web standar. Modul gif merekam GIF animasi: urutan bingkai yang tidak terkompresi (atau terkompresi palet) dengan penundaan per-bingkai eksplisit, yang dapat diputar di browser web atau penampil citra mana pun yang menangani GIF animasi.
Modul mjpeg adalah pilihan alami untuk rekaman panjang. Kompresi JPEG menjaga ukuran file tetap terkendali -- sebanding dengan to_jpeg() pada kualitas yang dikonfigurasi, bingkai demi bingkai -- sehingga sesi penangkapan yang diperpanjang tetap dalam anggaran kartu SD. Penggunaannya sangat mirip dengan perekaman ImageIO:
import mjpeg
m = mjpeg.Mjpeg("/sdcard/run.mjpeg")
while running:
m.add_frame(csi0.snapshot(), quality=85)
m.close()
mjpeg.Mjpeg menerima kata kunci posisional dan skala bergaya gambar yang sama seperti metode citra lainnya, sehingga rekaman dapat diskalakan, dipotong, atau dipetakan palet per bingkai saat dimasukkan. Argumen width dan height konstruktor default ke dimensi buffer bingkai utama dan menetapkan resolusi keluaran; setiap bingkai yang ditambahkan diskalakan (mempertahankan rasio aspek) agar sesuai. sync() membilas file ke disk selama rekaman panjang, dan close() menyelesaikan kontainer -- file Motion JPEG yang belum ditutup dengan bersih tidak dapat diputar, sehingga disiplin ini penting.
Modul gif adalah pilihan alami untuk rekaman pendek yang dibagikan apa adanya kepada penonton non-teknis -- beberapa detik aksi yang ditangkap untuk demo, ilustrasi animasi untuk dokumentasi, klip peristiwa yang disematkan dalam pesan obrolan. Bingkai GIF disimpan tanpa kompresi (atau dengan kompresi palet pada kedalaman warna 7-bit), yang membuat file jauh lebih besar per detik dibandingkan Motion JPEG dan menghalangi format ini untuk rekaman lebih dari beberapa detik, tetapi hasilnya dapat langsung masuk ke browser mana pun:
import gif
g = gif.Gif("/sdcard/clip.gif")
while running:
g.add_frame(csi0.snapshot(), delay=10)
g.close()
Argumen delay pada add_frame() adalah waktu tampilan per-bingkai dalam centi-detik (10 adalah 100 ms per bingkai, atau 10 fps), yang merupakan kontrol pemutaran GIF standar. Kata kunci loop konstruktor menetapkan apakah klip yang dihasilkan berulang secara otomatis di penampil (defaultnya adalah True, yang sesuai dengan ekspektasi "GIF animasi" konvensional).
Ketiga jalur perekaman menangani kasus umum di antara mereka: ImageIO untuk pemrosesan ulang di kamera, Motion JPEG untuk rekaman panjang yang dapat diputar di host, GIF animasi untuk klip pendek yang dapat diputar di host. Pilihan di antara ketiganya bergantung pada siapa yang memutar rekaman. Tahap hilir yang berjalan di kamera sendiri membaca ImageIO; workstation host atau penampil web membaca MJPEG atau GIF.
5.33.6. Pola trigger-dan-putar ulang¶
Pola yang berguna menggabungkan memory stream dengan kondisi trigger. Kamera merekam secara terus-menerus ke buffer ring memori dengan slot count, menimpa slot tertua setiap kali putaran. Ketika kondisi trigger terpicu (sebuah blob memasuki bingkai, peristiwa gerakan melampaui ambang batas, tombol ditekan), aplikasi mengambil snapshot isi ring -- count bingkai terbaru -- dan menulisnya ke file stream di kartu SD. Hasilnya adalah rekaman pra-trigger yang menangkap detik-detik sebelum peristiwa yang sebenarnya diperhatikan kamera, bukan hanya detik-detik setelahnya, yang merupakan keterbatasan klasik perekam naif "tangkap-saat-dipicu".
Implementasinya mudah setelah kelas stream tersedia: memory stream berukuran tetap berfungsi sebagai ring (dengan seek() eksplisit ke nol ketika offset mencapai jumlah slot), perulangan utama menangkap ke dalamnya pada setiap iterasi, dan penangan trigger membaca memory stream bingkai demi bingkai dan menulis masing-masing ke file stream yang dinamai berdasarkan timestamp dari trigger.