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