5.4. การอ่านและเขียนพิกเซล¶
การดำเนินการส่วนใหญ่กับภาพจะซ่อนงานต่อพิกเซลไว้ภายในการเรียกเมธอดครั้งเดียว โดยที่ลูปที่สัมผัสกับทุกพิกเซลจะทำงานด้วยความเร็วเนทีฟ อย่างไรก็ตาม มีบางกรณีที่โค้ดของแอปพลิเคชันต้องการเข้าถึงพิกเซลเฉพาะหนึ่งพิกเซลโดยตรง ไม่ว่าจะเพื่ออ่านค่าที่ตำแหน่งใดตำแหน่งหนึ่ง เขียนค่าใหม่ลงในพิกเซลนั้น สุ่มตัวอย่างจุดเดียวเพื่อการปรับเทียบ หรือดีบักค่าที่ตำแหน่งที่ทราบแน่ชัด โมดูล image เปิดเผยการเข้าถึงระดับนั้นผ่านรูปแบบการระบุตำแหน่งสองแบบ ซึ่งแต่ละแบบเหมาะกับวิธีคิดที่ต่างกันเกี่ยวกับตำแหน่งของพิกเซล
5.4.1. การระบุตำแหน่งด้วยพิกัด¶
รูปแบบที่เป็นธรรมชาติที่สุดคือรูปแบบที่ Coordinates ได้สร้างคำศัพท์ไว้แล้ว นั่นคือการระบุพิกเซลด้วยพิกัดคาร์ทีเซียน (x, y) get_pixel() รับค่า (x, y) และคืนค่าที่ตำแหน่งนั้น ส่วน set_pixel() รับค่า (x, y) เดียวกันพร้อมกับค่าที่ต้องการเขียนลงไป
สิ่งที่การเรียกเมธอดเหล่านี้คืนค่าหรือรับค่าขึ้นอยู่กับรูปแบบของภาพ ภาพแบบ Grayscale, binary และ Bayer จะมีค่าเดียวต่อพิกเซล ได้แก่ ความสว่างสำหรับ grayscale ค่า 0 หรือ 1 สำหรับ binary และตัวอย่างช่องสีเดียวสำหรับ Bayer ดังนั้น get_pixel() จะคืนค่าเป็นจำนวนเต็มเดียว ส่วน RGB565 จะมีสามช่องสีบรรจุอยู่ใน 16 บิต และ get_pixel จะแกะค่าเหล่านั้นออกเป็น tuple (r, g, b) โดยค่าเริ่มต้น โดยแต่ละช่องจะถูกแมปไปยังช่วง 0 ถึง 255
พฤติกรรมเริ่มต้นนี้สามารถเปลี่ยนแปลงได้ที่ทั้งสองด้าน การส่ง rgbtuple=False ไปยัง get_pixel บนภาพ RGB565 จะทำให้ได้ค่าคำ 16 บิตดิบ ซึ่งเป็นรูปแบบเดียวกับที่ linear index คืนค่า และเป็นรูปแบบที่มีประสิทธิภาพเมื่อแอปพลิเคชันจะเขียนค่าที่บรรจุไว้กลับลงไปโดยตรง การส่ง rgbtuple=True บนภาพช่องเดียวจะทำตรงข้าม คือค่าที่เก็บไว้จะถูกแปลงเป็น tuple RGB888 ก่อนคืนค่า โดยภาพ Bayer จะผ่านขั้นตอน debayer ทันที อาร์กิวเมนต์นี้มีไว้เพื่อให้โค้ดที่เรียกใช้สามารถขอพิกเซลในพื้นที่สีที่เป็นหนึ่งเดียวโดยไม่คำนึงถึงวิธีที่ภาพต้นฉบับเก็บข้อมูล
ภาพที่ถูกบีบอัด ได้แก่ JPEG และ PNG ไม่รองรับโดย get_pixel หรือ set_pixel เนื่องจากไบต์ของภาพเหล่านั้นไม่ได้แทนพิกเซลที่ตำแหน่งที่ทราบ และเมธอดเหล่านี้จะโยนข้อผิดพลาดแทนที่จะคืนค่าที่ไม่มีความหมาย
ในทางปฏิบัติ รูปแบบการใช้งานมีดังนี้:
v = img.get_pixel(40, 30) # grayscale: int 0..255
img.set_pixel(40, 30, 255) # write white
r, g, b = img.get_pixel(40, 30) # RGB565: defaults to (r, g, b) tuple
img.set_pixel(40, 30, (255, 0, 0)) # write red
หากค่า (x, y) ที่ร้องขออยู่นอกขอบเขตของภาพ get_pixel จะคืนค่า None และ set_pixel จะไม่ทำอะไร พฤติกรรมนี้ออกแบบให้ผ่อนผันโดยตั้งใจ เนื่องจากอัลกอริทึมหลายอย่างทำงานใกล้ขอบของภาพและอาจระบุตำแหน่งนอกช่วงชั่วคราว การไม่ทำอะไรอย่างเงียบๆ นั้นก่อความรบกวนน้อยกว่าการโยนข้อยกเว้นทุกครั้งที่เกิดขึ้น
5.4.2. การระบุตำแหน่งด้วย linear index¶
อีกรูปแบบหนึ่งคือการระบุตำแหน่งพิกเซลตามตำแหน่งในบัฟเฟอร์ต้นแบบ ระลึกถึงเลย์เอาต์ของบัฟเฟอร์ พิกเซลจะถูกเก็บเรียงทีละแถว โดยพิกเซลทั้งหมดของแถวบนสุดก่อน ตามด้วยแถวถัดไป และต่อเนื่องลงไปจนถึงแถวล่างสุด การจัดเรียงนี้หมายความว่าทุกพิกเซลมีดัชนีจำนวนเต็มเดียวที่นับจาก 0 ที่ด้านบนซ้ายและเพิ่มขึ้นตลอดแต่ละแถว พิกเซลที่พิกัด (x, y) มี linear index เท่ากับ y * width + x
พิกเซลสามารถระบุตำแหน่งได้ทั้งด้วยพิกัดคาร์ทีเซียน (x, y) และด้วย linear index ที่เดินผ่านบัฟเฟอร์ทีละแถวจากซ้ายไปขวา¶
โมดูล image เปิดเผย index นั้นผ่านสัญลักษณ์ subscript ปกติของ Python ดังนี้ img[i] อ่านพิกเซลที่ linear index i และ img[i] = value เขียนค่าหนึ่งค่า สิ่งที่รูปแบบ index คืนค่ามาคือ ค่าดิบที่เก็บไว้ สำหรับรูปแบบนั้น ไม่ใช่ tuple ที่แกะออกมาที่ get_pixel() คืนค่าโดยค่าเริ่มต้น ความแตกต่างนี้สำคัญเพราะรูปแบบที่เลือกไว้ก่อนหน้านี้จะเป็นตัวกำหนดว่าค่าดิบนั้นมีหน้าตาอย่างไร:
พิกเซล Grayscale และ Bayer จะคืนค่าเป็นจำนวนเต็ม 8 บิต
พิกเซล RGB565 และ YUV422 จะคืนค่าเป็นจำนวนเต็ม 16 บิต ซึ่งเป็นคำที่บรรจุไว้
พิกเซล Binary จะคืนค่าเป็น
0หรือ1พิกเซล JPEG และ PNG จะคืนค่าเป็นจำนวนเต็ม 8 บิต ทีละหนึ่งไบต์ของสตรีมที่บีบอัด ค่าเหล่านั้นไม่โปร่งใส เนื่องจากเป็นส่วนหนึ่งของการเข้ารหัสที่บีบอัดแล้ว ไม่ใช่พิกเซลในความหมายทั่วไป
รูปแบบ index เหมาะกับโค้ดที่คิดในแง่ของ buffer offset อยู่แล้ว เช่น ลูปที่เดินผ่านทุกพิกเซลครั้งเดียว อัลกอริทึมที่ต้องกระโดดทีละแถว หรือโค้ดที่แปลงระหว่างเลย์เอาต์ของบัฟเฟอร์ โค้ดที่คิดในแง่ของพิกัด x และ y จะได้รับประโยชน์มากกว่าจาก get_pixel และ set_pixel โดยสองรูปแบบนี้ระบุตำแหน่งพิกเซลเดียวกันผ่านโมเดลความคิดที่แตกต่างกัน
Image ยังสามารถทำ iteration ได้ด้วย for v in img: จะเดินผ่านบัฟเฟอร์ในลำดับ row-major เดียวกัน โดยให้ค่าดิบทีละพิกเซล และ len(img) คือจำนวนพิกเซลสำหรับรูปแบบที่ไม่ได้บีบอัด หรือจำนวนไบต์สำหรับสตรีมที่บีบอัด
5.4.3. เหตุใด Python ต่อพิกเซลจึงเป็นเส้นทางที่ช้า¶
หมายเหตุเชิงปฏิบัติที่ควรพูดตรงๆ การเดินผ่านภาพทีละพิกเซลจาก Python นั้น ช้ามาก ภาพ Grayscale ขนาด 320 × 240 มีพิกเซล 76,800 พิกเซล การเรียก get_pixel() บนแต่ละพิกเซลใน for loop จะรัน MicroPython bytecode หลายล้านคำสั่งเพื่อทำงานที่เมธอดเนทีฟที่เทียบเท่าสามารถเสร็จสิ้นได้ในไม่กี่ร้อยไมโครวินาที นี่ไม่ใช่ปัจจัยเล็กน้อย มันคือความแตกต่างระหว่างสคริปต์ที่ประมวลผลเฟรมแบบเรียลไทม์กับสคริปต์ที่คลานช้าอยู่ต่ำกว่าอัตราเฟรมของกล้องมาก
เกือบทุกเมธอดบนพื้นผิวของ Image มีอยู่เนื่องจากมีเวอร์ชันเนทีฟที่เร็วกว่าสำหรับรูปแบบต่อพิกเซลทั่วไป ลูปที่บวกภาพสองภาพเข้าด้วยกันกลายเป็นการเรียกเนทีฟครั้งเดียว ลูปที่ทำให้แต่ละพิกเซลเรียบขึ้นโดยการเฉลี่ยกับพิกเซลข้างเคียงก็กลายเป็นอีกการเรียกหนึ่ง ลูปที่จำแนกแต่ละพิกเซลเทียบกับค่าขีดแบ่งก็กลายเป็นอีกการเรียกหนึ่ง งานของแอปพลิเคชันส่วนใหญ่คือการรับรู้ว่าเมธอดระดับภาพทั้งหมดใดที่ตรงกับงานที่ลูปจะทำ และเลือกใช้เมธอดนั้นแทนที่จะเขียนลูปด้วยมือ
การอ่านและเขียนระดับพิกเซลยังคงเป็นเครื่องมือที่เหมาะสมเมื่อไม่มีทางเลือกอื่น เช่น การแก้ไขการวัดเฉพาะกลับเข้าไปในบัฟเฟอร์ การสุ่มตัวอย่างตำแหน่งหนึ่งสำหรับขั้นตอนการปรับเทียบ หรือการดีบักค่าที่ตำแหน่งที่ทราบแน่ชัด ประเด็นสำคัญคือนี่คือเส้นทางที่ช้า ใช้เมื่อเมธอดระดับภาพทั้งหมดไม่มีรูปแบบที่แอปพลิเคชันต้องการ ไม่ใช่เป็นวิธีเริ่มต้นในการดำเนินการกับพิกเซล