5.33. สตรีม ImageIO#

save() และ to_jpeg() ครอบคลุมกรณี I/O แบบ เฟรมเดียว: แอปพลิเคชันจับเฟรม เข้ารหัส แล้วส่งไปยังที่ใดที่หนึ่ง แอปพลิเคชันอีกประเภทหนึ่งต้องการกรณี ลำดับ: บันทึกเฟรมหลายเฟรมติดต่อกันในอัตราการจับภาพที่เป็นธรรมชาติ จัดเก็บไว้เพื่อดึงข้อมูลภายหลัง และเล่นกลับในความเร็วที่ถูกต้อง สคริปต์รวบรวมข้อมูลสำหรับการฝึก ML จะจับเฟรมตัวอย่างไม่กี่ร้อยเฟรมสำหรับ machine learning pipeline; บันทึกสถานีตรวจสอบบันทึกชิ้นส่วนที่จับได้ทุกชิ้นเพื่อการตามรอย; สคริปต์พัฒนาเล่นซ้ำลำดับที่เก็บไว้เพื่อทดสอบอัลกอริทึมใหม่กับข้อมูลที่จับสดมาก่อนหน้านี้

คลาส ImageIO คือตัวบันทึก/เล่นของโมดูล image สตรีมเดียวเก็บลำดับของเฟรม Image -- ซึ่งอาจมีขนาดและรูปแบบพิกเซลต่างกัน -- พร้อมกับช่วงเวลาระหว่างเฟรมของแต่ละเฟรม เพื่อให้การเล่นกลับสร้างอัตราเฟรมต้นฉบับได้อีกครั้ง มีแหล่งจัดเก็บข้อมูลสองแบบ: ไฟล์ในระบบไฟล์หรือบัฟเฟอร์ขนาดคงที่ใน RAM

5.33.1. แหล่งจัดเก็บข้อมูลทั้งสองแบบ#

สตรีมไฟล์ จะคงการบันทึกไว้แม้เมื่อปิดเครื่องและมีขนาดจำกัดเฉพาะโดยพื้นที่เก็บข้อมูลที่รองรับ โดยเริ่มต้นด้วย magic header ขนาด 16 ไบต์ OMV IMG STR Vx.y ตามด้วยหนึ่ง chunk ต่อเฟรม; ตัวเขียนปัจจุบันส่งออก V2.0 และตัวอ่านยังคงยอมรับไฟล์ V1.0 และ V1.1 เพื่อความเข้ากันได้ย้อนหลัง อาร์กิวเมนต์ constructor คือ path ของไฟล์; mode คือโหมดเปิดไฟล์ ('r' เพื่ออ่านสตรีมที่มีอยู่, 'w' เพื่อตัดทิ้งและเขียนใหม่)

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

สตรีมหน่วยความจำ อาศัยอยู่ในบัฟเฟอร์ RAM ที่จัดสรรเมื่อสร้าง constructor รับ tuple 3 ค่า (w, h, pixformat) แทน path และอาร์กิวเมนต์ mode กลายเป็น จำนวน slot เฟรมที่จัดสรรล่วงหน้า บัฟเฟอร์มีขนาดพอดีสำหรับเฟรมจำนวนนั้นตามขนาดที่ระบุ และ ไม่อนุญาตให้ขยาย หลังจากจัดสรรแล้ว -- การเขียนเกิน slot สุดท้ายจะ raise EOFError และการเขียนเฟรมที่ใหญ่กว่าบัฟเฟอร์ต่อ slot จะ raise ValueError สตรีมหน่วยความจำเป็นเครื่องมือที่เหมาะสมเมื่อแอปพลิเคชันต้องการส่ง การบันทึก ไปยังขั้นตอนถัดไปโดยไม่ผ่านระบบไฟล์ (เช่น ring buffer เฟรมล่าสุดสั้นๆ สำหรับรูปแบบ trigger-and-replay)

# 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())

สำหรับรูปแบบพิกเซลที่บีบอัด (image.JPEG, image.PNG) ขนาดต่อ slot ประมาณที่ 2 บิตต่อพิกเซล; เฟรมที่เข้ารหัสแล้วมีขนาดใหญ่กว่าค่าประมาณจะ raise ValueError ในเวลาเขียน ดังนั้นแอปพลิเคชันที่คาดว่าจะจัดเก็บ JPEG คุณภาพสูงต้องจัดสรร slot มากเกินไปหรือเข้ารหัสที่คุณภาพต่ำกว่าก่อน

type() คืนค่า image.ImageIO.FILE_STREAM หรือ image.ImageIO.MEMORY_STREAM เพื่อให้โค้ดในขั้นตอนถัดไปสามารถปรับตัวตามแหล่งจัดเก็บที่ได้รับ

5.33.2. การบันทึก#

write() เพิ่ม Image ที่จับได้ต่อท้ายสตรีมไฟล์ (หรือเก็บไว้ที่ slot ปัจจุบันของสตรีมหน่วยความจำ) และเลื่อน offset ขึ้นทีละหนึ่ง การเรียกเดียวกันนี้ยังบันทึก ช่วงเวลาระหว่างเฟรม นับจากการเขียนครั้งล่าสุด เพื่อให้ฝั่งเล่นกลับสามารถหยุดรอเวลาที่ถูกต้องระหว่างเฟรม และรักษาอัตราเฟรมธรรมชาติของการบันทึกไว้

เฟรมที่ต่างกันได้รับอนุญาตภายในสตรีมไฟล์เดียว: การบันทึกสามารถผสม RGB565, ภาพระดับสีเทา, และ JPEG thumbnails ได้อย่างอิสระ และตัวอ่านจะถอดรหัสแต่ละเฟรมตามขนาดและรูปแบบต้นฉบับ สตรีมหน่วยความจำเป็นแบบ homogeneous (ทุก slot ใช้ (w, h, pixformat) จาก constructor ร่วมกัน) ดังนั้นการบันทึกในหน่วยความจำจำกัดอยู่กับการกำหนดค่าเฟรมเดียว

write() คืนค่า stream object เพื่อให้สามารถเชื่อมโยงการเรียกต่อเนื่องได้ การเขียนที่ offset ที่ไม่ใช่ท้ายไฟล์ของสตรีมไฟล์จะตัดทิ้งส่วนที่เหลือ -- มีประโยชน์สำหรับแก้ไขลำดับที่เก็บไว้ แต่มีความเสี่ยงหาก next-write position ถูกเลื่อนโดยไม่ได้ตั้งใจโดย seek() ก่อนหน้า

sync() flush การเขียนที่รอดำเนินการลงดิสก์สำหรับสตรีมไฟล์ (ไม่มีผลกับสตรีมหน่วยความจำ) และควรเรียกเป็นระยะเมื่อการบันทึกใช้เวลานาน เพื่อหลีกเลี่ยงการสูญเสียส่วนท้ายหาก cam รีบูตก่อนปิดไฟล์ destructor ปิดสตรีมโดยอัตโนมัติเมื่อ ImageIO อยู่นอกขอบเขต แต่การเรียก close() อย่างชัดเจนเป็นวินัยที่ถูกต้อง

5.33.3. การเล่นกลับ#

read() อ่านเฟรมที่ offset ปัจจุบัน เลื่อน offset และคืนค่า Image ใหม่ รูปภาพจะถูกเก็บไว้ในบัฟเฟอร์เฟรมเมื่อ copy_to_fb=True (ค่าเริ่มต้น) เพื่อให้ภาพที่คืนค่ามาสามารถวาดผ่านการแสดงตัวอย่างใน IDE ได้; เมื่อ copy_to_fb=False เฟรมจะไปอยู่บน MicroPython heap

# 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

คำสำคัญสองคำควบคุมพฤติกรรมการเล่นกลับ loop=True (ค่าเริ่มต้นสำหรับสตรีมไฟล์) จะวน read pointer กลับไปที่จุดเริ่มต้นเมื่อถึงจุดสิ้นสุดของการบันทึก ดังนั้นการเรียกจะไม่คืนค่า None เลย; loop=False คืนค่า None เมื่อการบันทึกหมดและ loop ของผู้เรียกจะสิ้นสุด pause=True (ค่าเริ่มต้น) จะบล็อกการเรียกจนกว่าช่วงเวลาระหว่างเฟรมที่บันทึกในขณะเขียนจะผ่านไป ทำให้อัตราเฟรมการเล่นกลับตรงกับอัตราการจับภาพต้นฉบับ; pause=False คืนค่าทันที มีประโยชน์สำหรับ pipeline การวิเคราะห์ที่ต้องการประมวลผลการบันทึกให้เร็วที่สุดโดยไม่คำนึงถึงระยะเวลาต้นฉบับ

รูปแบบ loop เดียวกันใช้ได้กับสตรีมหน่วยความจำ ยกเว้นว่า loop จะถูกเพิกเฉย -- การอ่านเกินจุดสิ้นสุดของสตรีมหน่วยความจำจะ raise EOFError รูปแบบที่คาดหวังสำหรับ memory ring คือการ seek() กลับไปที่ศูนย์อย่างชัดเจนเมื่อต้องการวน

5.33.5. การบันทึกที่เล่นได้บน host#

สตรีม ImageIO เป็นเครื่องมือที่เหมาะสมเมื่อการบันทึกจะถูกเล่นกลับ บน cam -- สตรีมเหล่านี้เก็บทุกเฟรมที่จับได้ในรูปแบบพิกเซลดั้งเดิม ช่วงเวลาระหว่างเฟรมถูกบันทึกอย่างแม่นยำ และสคริปต์ในขั้นตอนถัดไปสามารถผ่านเฟรมทีละเฟรม seek และวิเคราะห์ซ้ำโดยไม่สูญเสียข้อมูล อย่างไรก็ตาม เครื่องมือนี้ไม่เหมาะสมเมื่อการบันทึกต้องเล่นได้บน host -- workstation, โทรศัพท์, เว็บเพลเยอร์ host คาดหวังคอนเทนเนอร์วิดีโอ มาตรฐาน ไม่ใช่รูปแบบ magic-header บนดิสก์ของ OpenMV

สองโมดูลแยกต่างหากครอบคลุมกรณีที่เล่นได้บน host โมดูล mjpeg บันทึก Motion JPEG: ลำดับของเฟรมที่บีบอัดด้วย JPEG บรรจุในคอนเทนเนอร์แบบ AVI-style ที่ VLC, QuickTime, ffmpeg และแท็กวิดีโอเว็บมาตรฐานเล่นได้โดยตรง โมดูล gif บันทึก animated GIF: ลำดับของเฟรมที่ไม่บีบอัด (หรือบีบอัดด้วย palette) พร้อม delay ต่อเฟรมอย่างชัดเจน เล่นได้ในเว็บเบราว์เซอร์หรือ image viewer ที่รองรับ animated GIF

โมดูล mjpeg เป็นตัวเลือกที่เหมาะสมสำหรับการบันทึก ยาว การบีบอัด JPEG ช่วยให้ขนาดไฟล์จัดการได้ -- เทียบได้กับ to_jpeg() ที่คุณภาพที่กำหนด ทีละเฟรม -- ดังนั้นเซสชันการจับภาพแบบขยายยังคงอยู่ในงบประมาณของ SD card การใช้งานสอดคล้องกับการบันทึก ImageIO อย่างใกล้ชิด:

import mjpeg

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

mjpeg.Mjpeg ยอมรับ keyword แบบ positional และ scale สไตล์การวาดเดียวกับที่เมธอด image อื่นๆ รับ ดังนั้นการบันทึกสามารถปรับขนาด, crop, หรือ palette-map ต่อเฟรมในขณะบันทึกได้ อาร์กิวเมนต์ width และ height ของ constructor ค่าเริ่มต้นเป็นขนาดของบัฟเฟอร์เฟรมหลัก และกำหนดความละเอียดเอาต์พุต; ทุกเฟรมที่เพิ่มจะถูกปรับขนาด (โดยรักษาสัดส่วน) ให้พอดี sync() flush ไฟล์ลงดิสก์ระหว่างการบันทึกยาว และ close() จัดรูปคอนเทนเนอร์ให้สมบูรณ์ -- ไฟล์ Motion JPEG ที่ไม่ได้ปิดอย่างถูกต้องจะไม่สามารถเล่นได้ ดังนั้นวินัยนี้จึงสำคัญ

โมดูล gif เป็นตัวเลือกที่เหมาะสมสำหรับการบันทึก สั้น ที่แชร์ตรงๆ กับผู้ชมที่ไม่เชี่ยวชาญเทคนิค -- ไม่กี่วินาทีของการกระทำที่จับได้สำหรับ demo, ภาพประกอบเคลื่อนไหวสำหรับเอกสาร, คลิปเหตุการณ์ที่ฝังในข้อความ chat เฟรม GIF ถูกเก็บแบบไม่บีบอัด (หรือบีบอัดด้วย palette ที่ความลึกสี 7 บิต) ซึ่งทำให้ไฟล์มีขนาดใหญ่กว่าต่อวินาทีมากกว่า Motion JPEG และทำให้รูปแบบนี้ไม่เหมาะกับการบันทึกที่ยาวกว่าไม่กี่วินาที แต่ผลลัพธ์สามารถใส่ในเบราว์เซอร์ใดก็ได้โดยตรง:

import gif

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

อาร์กิวเมนต์ delay บน add_frame() คือเวลาแสดงต่อเฟรมในหน่วย centi-second (10 คือ 100 ms ต่อเฟรม หรือ 10 fps) ซึ่งเป็นตัวควบคุมการเล่นกลับ GIF มาตรฐาน keyword loop ของ constructor กำหนดว่าคลิปที่ได้จะวนอัตโนมัติในโปรแกรมดูหรือไม่ (ค่าเริ่มต้นคือ True ซึ่งตรงกับความคาดหวังของ "animated GIF" ทั่วไป)

เส้นทางการบันทึกสามเส้นทางครอบคลุมกรณีทั่วไปร่วมกัน: ImageIO สำหรับการประมวลผลซ้ำบน cam, Motion JPEG สำหรับการบันทึกที่เล่นได้บน host แบบยาว, animated GIF สำหรับคลิปที่เล่นได้บน host แบบสั้น การเลือกระหว่างเส้นทางเหล่านี้ขึ้นอยู่กับ ใครเล่นการบันทึก ขั้นตอนในขั้นตอนถัดไปที่รันบน cam เองอ่าน ImageIO; host workstation หรือ web viewer อ่าน MJPEG หรือ GIF

5.33.6. รูปแบบ trigger-and-replay#

รูปแบบที่มีประโยชน์รวม memory stream กับเงื่อนไข trigger cam บันทึกอย่างต่อเนื่องลงใน ring buffer หน่วยความจำขนาด count slot โดยเขียนทับ slot เก่าสุดในแต่ละรอบ เมื่อเงื่อนไข trigger เกิดขึ้น (บลอบเข้าสู่เฟรม, เหตุการณ์การเคลื่อนไหวเกินค่าขีดแบ่ง, กดปุ่ม) แอปพลิเคชันจะสแนปช็อตเนื้อหาของ ring -- เฟรมล่าสุด count เฟรม -- และเขียนลงสตรีมไฟล์บน SD card ผลลัพธ์คือ การบันทึกก่อน trigger ที่จับวินาทีก่อน เหตุการณ์ที่ cam สังเกตเห็นจริงๆ ไม่ใช่แค่วินาทีหลัง ซึ่งเป็นข้อจำกัดแบบดั้งเดิมของตัวบันทึกแบบ naive "capture-when-triggered"

การนำไปปฏิบัตินั้นตรงไปตรงมาเมื่อมีคลาส stream ในมือ: memory stream ขนาดคงที่ทำหน้าที่เป็น ring (พร้อมการ seek() ชัดเจนกลับไปที่ศูนย์เมื่อ offset ถึงจำนวน slot), loop หลักจับภาพลงไปในนั้นในทุก iteration และ trigger handler อ่าน memory stream ออกทีละเฟรมและเขียนแต่ละเฟรมลงในสตรีมไฟล์ที่ตั้งชื่อตาม timestamp ของ trigger