Thursday, December 4, 2014

การอ่านภาพขนาดใหญ่ทีละส่วน ด้วย Python และ PIL ตอนที่ 1

เรื่องนี้ค้างคามานานแล้ว คงถึงเวลาที่ต้องทำให้สำเร็จซะที 

มีโจทย์อยู่ว่าต้องการอ่านภาพขนาดใหญ่มาก เช่น ภาพพวก Remote Sensing แบบ Multi-Spectral ซึ่งปกติต้องใช้เครื่องคอมที่มีแรมเยอะๆ เพราะต้องอ่านภาพทั้งหมดไปไว้ในหน่วยความจำก่อนการประมวลผล และถ้าต้องประมวลผลแบบพวก Convolution ก็จะต้องใช้หน่วยความจำมากขึ้นไปอีก 

ทีนี้ก็เลยมาคิดว่า ถ้าเครื่องมีแรมไม่พอจะทำได้ไหม ถ้าโหลดภาพกันตรงๆก็คงเจอปัญหา Out of Memory แน่ๆ 

ทางออกนึงที่น่าจะเป็นไปได้คือ การอ่านภาพทีละส่วน เนื่องจากการประมวลผลภาพส่วนใหญ่ไม่ได้ใช้ข้อมูลจากทั้งรูปในทีเดียว ส่วนใหญ่จะใช้เป็นก้อนๆไป

 ต่อไปก็หาเทคนิคที่จะใช้ ไปค้นดูปรากฎว่า Java ก็ทำได้ ลองเขียนโค้ดไปพักใหญ่ๆ 

ต่อมา ลองค้นหากับ Python บ้าง เพื่อว่าจะมีวิธีเหมือนกัน ก็ไปพบที่เว็บนี้ Using PIL On Large Images
เค้าบอกว่า ถ้าใช้ PIL (Python Imaging Library) ในการอ่านค่ารูป มันสามารถแบ่งส่วนได้

เมื่อเราใช้คำสั่งในการอ่านรูป คือ
im = Image.open("Lenna.png")

จริงๆแล้วรูปยังไม่ถูกอ่านและโหลดเข้าหน่วยความจำทั้งหมด คำสั่งนี้เพียงแค่โหลดส่วนต้นของไฟล์ภาพ ที่เกี่ยวข้องกับข้อมูลภาพ เช่น การเข้ารหัส ขนาดของภาพ ชนิดของภาพ (ภาพสีเทา ภาพสี) ฯลฯ กระบวนการนี้ถูกเรียกว่า Lazy operation

ซึ่งหลังจากคำสั่งนี้ เราสามารถใช้คำสั่งอื่นเพื่อดูข้อมูลนี้ได้ เช่น
im.size ก็จะให้ค่าเป็น (width, height) ออกมา

ถ้าใช้คำสั่ง im.tile จะได้โครงสร้างรูปแบบการอ่านค่าแบบบล็อกออกมา (แสดงว่าจริงๆแล้ว PIL ก็อ่านค่ารูปแบบแบ่งส่วนอยู่แล้ว ไม่ได้อ่านทีเดียวทั้งหมด เพียงแต่กระบวนการนี้ต่อเนื่องกันทั้งรูป)

ตัวอย่างเช่น ถ้ามีรูป Lenna.png ที่ดาวน์โหลดมาจากที่นี่


เมื่อเราอ่านรูปโดย
im = Image.open("Lenna.png")
และดูขนาด
im.size
(512, 512)
หมายเหตุว่าเป็น (width, height) ครับ

และดูการแบ่งส่วนในการอ่านรูป
im.tile
[('zip', (0, 0, 512, 512), 54L, 'RGB')]

ก็จะเห็นว่ามีค่าอยู่สี่ชุด ได้แก่
1. การเข้ารหัส ในที่นี้คือ zip ถ้าไม่เข้ารหัสเลยก็จะเป็น raw
2. ขนาดกรอบสี่เหลี่ยมในการอ่านรูป เริ่มตั้งแต่มุมซ้ายบน (x1,y1) จนถึงขวาล่างสุด (x2,y2) ของรูป
3. ค่า offset ในการอ่านรูปแต่ละครั้ง ในที่นี้คือ 54L ซึ่งในที่นี้ไม่มีการแบ่งส่วนของรูป จึงเป็นค่าขนาดของ header ของไฟล์รูปนี้ (ไฟล์รูปจะมีส่วนหัวที่บอกถึงการเข้ารหัส ชนิดของรูป ขนาด ฯลฯ) ค่า offset นี้จะเป็นตัวบอกว่าเราจะเริ่มอ่านไฟล์เนื้อหาของรูปจริงๆที่ตรงไหน เช่น 54L ก็คือเริ่มอ่านหลังจาก 54 คูณ ขนาดของ long (ไม่รู้เท่าไหร่เหมือนกันครับ 555) ไบต์เป็นต้นไป
4. โหมดของรูป ในที่นี้คือ RGB

ในหลักการ เราสามารถเปลี่ยนค่าเหล่านี้ได้ และจะทำให้สามารถแบ่งส่วนการอ่านรูปได้

ถ้าลองเปลี่ยนมาเป็นรูปสีเทา โดยในที่นี้จะเป็นรูปเดิมที่ถูกแปลงเป็นไฟล์ tif โดยไม่เข้ารหัส (ไฟล์ raw)


i = Image.open("Lenna-gray-raw.tif")
i.size
(512, 512)
i.tile
[('raw', (0, 0, 512, 64), 8, ('L', 0, 1)), ('raw', (0, 64, 512, 128), 32776, ('L', 0, 1)), ('raw', (0, 128, 512, 192), 65544, ('L', 0, 1)), ('raw', (0, 192, 512, 256), 98312, ('L', 0, 1)), ('raw', (0, 256, 512, 320), 131080, ('L', 0, 1)), ('raw', (0, 320, 512, 384), 163848, ('L', 0, 1)), ('raw', (0, 384, 512, 448), 196616, ('L', 0, 1)), ('raw', (0, 448, 512, 512), 229384, ('L', 0, 1))]

จะเห็นว่าคราวนี้ ข้อมูลชุดแรกของค่า tile (แถบแรก) คือ
('raw', (0, 0, 512, 64), 8, ('L', 0, 1))
บอกว่าไม่มีการเข้ารหัส (raw) และ มีการอ่านค่าทุกๆ 64 บรรทัด โดยครั้งแรกที่อ่านจะต้องเลื่อนไป 8 bytes ก่อน (offset) ซึ่งเป็น header ของไฟล์รูปนี้ รูปจริงๆก็จะอยู่ต่อจาก byte ที่ 8 นั่นเอง

ข้อมูลชุดถัดมา (แถบที่สอง) คือ
('raw', (0, 64, 512, 128), 32776, ('L', 0, 1))
บอกว่าให้อ่านอีก 64 แถวต่อไป (ทุกคอลัมน์) ค่า Offset คราวนี้ก็จะเท่ากับ 64 แถว * 512 คอลัมน์ + 8 ไบต์แรก = 32776 ไบต์

เหมือนกับรูปประมาณนี้ครับ


ในตอนต่อไปเราจะลองเปลี่ยนค่า size และ tile ของรูป เพื่อแบ่งส่วนในการอ่านรูปดูครับ

หมายเหตุ รูปจะถูกอ่านจริงจากไฟล์และโหลดมาไว้ในหน่วยความจำก็ต่อเมื่อ
1) มีการใช้คำสั่งในการประมวลผลรูป หรือมีการอ่านค่าพิกเซลของรูป หรือ
2) บังคับโดยใช้คำสั่ง im.load()

No comments:

Post a Comment