6.1. ทำไมต้องใช้อาร์เรย์¶
คลาส Image คือเครื่องมือที่เหมาะสมสำหรับงานพิกเซล เนื่องจากทุกเมธอดในนั้นทำงานโดยตรงบน native pixel buffer ของกล้องในการเรียกครั้งเดียวอย่างรวดเร็ว สิ่งที่แอปพลิเคชันทำกับเฟรมส่วนใหญ่ เช่น การกำหนดค่าขีดแบ่ง การค้นหาบลอบ การตรวจจับ AprilTag ฟิลเตอร์ขอบ มีอยู่ใน library นั้นแล้ว
สิ่งที่ image library ไม่ได้ เปิดเผยคืองานตัวเลขส่วนที่เหลือที่แอปพลิเคชัน OpenMV พบเจอ:
บัฟเฟอร์ของ sensor ที่ไม่ใช่พิกเซล เช่น ตัวอย่างจาก ADC แกนจาก IMU (inertial measurement unit) เสียงจาก microphone
ตัวเลขที่ได้มาจากภาพซึ่งไม่มีเมธอดใน built-in คืนมา เช่น คอลัมน์ฮิสโตแกรม การผสมแบบกำหนดเองของสองเฟรม การแปลงต่อพิกเซลที่ไม่มีใน catalogue
พีชคณิตเชิงเส้นขนาดเล็ก เช่น เมทริกซ์ calibration ที่แก้ไขเลนส์ การหมุนที่รวม IMU
การคำนวณด้านการประมวลผลสัญญาณ เช่น เนื้อหาความถี่ของบัฟเฟอร์การสั่นสะเทือน การปรับให้เรียบของเอาต์พุต sensor เวกเตอร์ลักษณะเด่นที่ classifier ต้องการเป็นอินพุต
ทั้งหมดนี้ต้องการรูปแบบเดียวกัน ได้แก่ บัฟเฟอร์ของตัวเลขที่ใช้การดำเนินการเดียวกับทุกองค์ประกอบ for loop ของ Python เป็นวิธีเขียนที่ชัดเจน:
for i in range(len(samples)):
samples[i] = samples[i] * cal
Loop ทำงานได้ แต่ก็ช้าด้วย Python เป็นภาษาที่ตีความ และทุกรอบของ Python loop มีค่าใช้จ่ายของการรัน interpreter ครั้งหนึ่ง ได้แก่ ค้นหา samples อ่านองค์ประกอบ i คูณ เขียนกลับ เลื่อน loop counter ตรวจสอบเงื่อนไข loop บนบัฟเฟอร์ที่มีตัวอย่าง sensor พันตัว ค่าใช้จ่ายของ interpreter เหล่านี้รวมกันเป็นสิบมิลลิวินาทีสำหรับการดำเนินการที่พื้นฐานแล้วรวดเร็ว
ค่าใช้จ่ายนั้นกัดกินทุกครั้งที่สคริปต์เข้าถึงบัฟเฟอร์ เฟรม QVGA ระดับสีเทามี 76,800 พิกเซล accelerometer ที่ 100 Hz ส่งตัวอย่างสามแกนร้อยตัวต่อวินาที microphone เติมบัฟเฟอร์ 1,024 ตัวอย่างทุก 64 ms Python for loop แบบ pure ผ่านสิ่งเหล่านี้ใดๆ จะเปลี่ยนงานที่ควรใช้เวลาไม่กี่ไมโครวินาทีให้กลายเป็นงานที่ใช้เวลาสิบมิลลิวินาที และนานกว่าประมาณสิบเท่าสำหรับบัฟเฟอร์ขนาดภาพ
6.1.1. ฟังก์ชัน library เร็วกว่า loop¶
แนวทางแก้ไขคือการแสดงการดำเนินการเป็นการเรียกฟังก์ชันเดียวต่อบัฟเฟอร์ทั้งหมด แทนที่จะเป็น Python loop ผ่านองค์ประกอบ numpy คือสิ่งนั้น: library ของการคำนวณอาร์เรย์ที่ทุกการดำเนินการเป็นฟังก์ชันที่ optimized แล้วซึ่งเดินผ่านบัฟเฟอร์ครั้งเดียวตั้งแต่ต้นจนจบ np.multiply(samples, cal) คูณทุกองค์ประกอบของ samples ด้วย cal ภายในการเรียกเดียว โดยทำการคำนวณเดิมกับที่ loop ทำแต่ไม่มีค่าใช้จ่าย interpreter ต่อรอบ การคูณ 1000 องค์ประกอบเดียวกันที่ใช้เวลาสิบมิลลิวินาทีในฐานะ Python loop ใช้เวลาสิบไมโครวินาทีในฐานะการเรียก numpy
นี่คือข้อเสนอที่ numpy มีให้ทั่วไป ได้แก่ sum, mean, sin, exp, matrix multiply, primitives สำหรับประมวลผลสัญญาณ แต่ละอย่างเป็นฟังก์ชัน library เดียวที่ทำงานบนบัฟเฟอร์ทั้งหมดในคราวเดียว การแลกเปลี่ยนคือข้อมูลต้องอยู่ในชนิดอาร์เรย์ของ numpy และการดำเนินการต้องแสดงต่ออาร์เรย์นั้น ไม่ใช่ต่อองค์ประกอบทีละตัว
6.1.2. ทำไม list ถึงใช้ไม่ได้¶
Python list ไม่สามารถทดแทนได้ list สามารถเก็บออบเจกต์ผสมกันได้ เช่น จำนวนเต็ม float สตริง list อื่น และฟังก์ชัน library ที่อ่านมันยังต้องมองแต่ละ slot เพื่อหาว่ามีอะไรอยู่และดึงค่าออกมาก่อนที่จะเกิดการคำนวณ ค่าใช้จ่ายต่อ slot นั้นคือสิ่งเดียวกันกับที่ Python loop จ่าย list ไม่เหมาะกับการคำนวณอาร์เรย์ที่รวดเร็ว
6.1.3. ทำไม bytearray ถึงยังไม่พอ¶
bytearray มี รูปแบบ ที่ถูกต้อง ได้แก่ บัฟเฟอร์ชนิดเดียว หนึ่งไบต์ต่อองค์ประกอบ ทั้งหมดอยู่ในบล็อกต่อเนื่องกัน เป็นสิ่งที่ peripheral API ที่เน้นไบต์ส่วนใหญ่คืนมา แต่สิ่งที่ขาดคือ การคำนวณ bytearray * 2 จะทำซ้ำบัฟเฟอร์แทนที่จะสองเท่าค่าแต่ละตัว และ bytearray + bytearray แบบ element-by-element ไม่มีความหมายที่สมเหตุสมผล
โครงสร้างข้อมูลที่รวมบัฟเฟอร์ชนิดเข้ากับการคำนวณแบบ element-wise คือ ndarray สิ่งที่อยู่ภายในกล่องและวิธีที่แต่ละฟิลด์กำหนดพฤติกรรม fast-path คือรากฐานที่ส่วนที่เหลือของบทนี้ยึดอยู่