Tuesday, December 16, 2014

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

เมื่อเราพยายามแก้ปัญหาด้วยวิธีหนึ่ง เราอาจจะต้องยอมสูญเสียบางอย่างไปใช่ไหมครับ

เช่นเดียวกับปัญหาในการอ่านภาพขนาดใหญ่ วิธีที่ง่ายที่สุดคือเพิ่มประสิทธิภาพของคอมพิวเตอร์ เช่น เปลี่ยน CPU หรือเพิ่ม RAM ซึ่งสิ่งที่เสียไปคือค่าใช้จ่าย

หากเลือกช่องทางอื่น เช่นเหมือนกับที่เราพยายามทำ คือการแบ่งส่วนรูปเป็นส่วนย่อยๆ วิธีนี้ก็จะต้องแลกด้วยการเสียเวลาเพิ่มขึ้น เนื่องจาก
- ความซับซ้อนของ Algorithm มากขึ้น
- การอ่านข้อมูลหลายๆครั้งจาก Hard disk น่าจะเสียเวลามากกว่าการอ่านครั้งเดียว
นอกจากนี้ ยังต้องแลกด้วยการเสียหน่วยความจำเพิ่มเติมอีก ได้แก่หน่วยความจำที่ใช้เก็บส่วนของรูปในระหว่างรอประมวลผล

เราลองมาทำการทดลองวัดเฉพาะเรื่องของเวลา ที่ใช้ในการอ่านและประมวลผลรูป ระหว่างการอ่านครั้งเดียว กับการอ่านทีละส่วนกันครับ

วิธีวัดเวลาใน python ก็ตรงไปตรงมาคือ
import time
start = time.time()
#our codes
elapse = time.time()-start

อย่างไรก็ตาม มีข้อสังเกตว่า python มีการสร้าง cache ของรูปที่เคยอ่านไว้ ทำให้การอ่านรูปซ้ำใช้เวลาน้อยกว่าการอ่านรูปครั้งแรก ดังนั้น ในการวัดเวลาครั้งนี้เราจะเอาเวลาโดยประมาณ ที่ไม่รวมการอ่านรูปในรอบแรก
ในที่นี้เราใช้รูปทดสอบขนาด 7076x7001 พิกเซล เป็นรูปแบบ RGB
กรณีที่ 1 อ่านทั้งรูป เวลาที่ใช้โดยประมาณ 0.65 วินาที
from PIL import Image
import time
#starting time
start = time.time()
img = Image.open("mfu2009(chs. 1,2,3).tif")
#load image
img.load()
#time used
elapse = time.time() - start
print elapse

กรณีที่ 2 อ่านทีละส่วนของรูป เวลาที่ใช้โดยประมาณ 1.10 วินาที
from PIL import Image
import time
#starting time
start = time.time()
img = Image.open("mfu2009(chs. 1,2,3).tif")
#create new output image
out = Image.new(img.mode, img.size, None)

#try to read every 128 rows
rowRead = 128
#keep original rowRead, used when there are extra last set of rows
rowReadExtra = rowRead
#size of original image
oriW, oriH = img.size
totalRead = oriH/rowRead
#is oriH is divisible by rowRead?
rowLeft = oriH%rowRead

#image header size, please check by img.tile, it is different for images
header = img.tile[0][2];
#loop for each partial read
for rr in range(totalRead):
    #reopen image, necessary due to partial loading
    img = Image.open("mfu2009(chs. 1,2,3).tif")
    #------- tile parameters --------
    #for grayscale
    offset = rr*(rowRead*oriW)
    #if RGB
    if img.mode=='RGB':
        offset = offset*3
    #if the last set of row is additional
    if rr==totalRead-1 and rowLeft!=0:
        rowRead = rowRead + rowLeft
    #set tile size
    img.tile = [('raw', (0, 0, oriW, rowRead),  header + offset, ('RGB', 0, 1))]
    #------- size parameters --------
    img.size = (oriW, rowRead)
    #load part of image
    img.load()
    #copy part of image to new image
    #paste(source image, top-left corner)
    out.paste(img,(0,rr*rowReadExtra))    


elapse = time.time() - start
print elapse

กรณีที่ 3 อ่านทั้งรูป แล้วประมวลผลแต่ละพิกเซล เวลาที่ใช้โดยประมาณ 1.24 วินาที
from PIL import Image
import time
#starting time
start = time.time()
img = Image.open("mfu2009(chs. 1,2,3).tif")
#load image and perform point processing
out = img.point(lambda i: 255-i)
#time used
elapse = time.time() - start
print elapse

กรณีที่ 4 อ่านทีละส่วนของรูป แล้วประมวลผลแต่ละพิกเซล เวลาที่ใช้โดยประมาณ 1.83 วินาที
โค้ดเหมือนกรณีที่ 2 เพียงแค่แก้ไขในส่วนสุดท้ายของลูปนิดหน่อย
from PIL import Image
import time
#starting time
start = time.time()
img = Image.open("mfu2009(chs. 1,2,3).tif")
#create new output image
out = Image.new(img.mode, img.size, None)

#try to read every 128 rows
rowRead = 128
#keep original rowRead, used when there are extra last set of rows
rowReadExtra = rowRead
#size of original image
oriW, oriH = img.size
totalRead = oriH/rowRead
#is oriH is divisible by rowRead?
rowLeft = oriH%rowRead

#image header size, please check by img.tile, it is different for images
header = img.tile[0][2];
#loop for each partial read
for rr in range(totalRead):
    #reopen image, necessary due to partial loading
    img = Image.open("mfu2009(chs. 1,2,3).tif")
    #------- tile parameters --------
    #for grayscale
    offset = rr*(rowRead*oriW)
    #if RGB
    if img.mode=='RGB':
        offset = offset*3
    #if the last set of row is additional
    if rr==totalRead-1 and rowLeft!=0:
        rowRead = rowRead + rowLeft
    #set tile size
    img.tile = [('raw', (0, 0, oriW, rowRead),  header + offset, ('RGB', 0, 1))]
    #------- size parameters --------
    img.size = (oriW, rowRead)
    #load image part and perform point processing
    img.load()
    temp = img.point(lambda i: 255-i)    
    #copy part of image to new image
    #paste(source image, top-left corner)
    out.paste(temp,(0,rr*rowReadExtra))  

elapse = time.time() - start
print elapse

สรุปว่า
กรณีที่ 2 ใช้เวลาเป็น 1.69 เท่าของวิธีที่ 1
กรณีที่ 4 ใช้เวลาเป็น 1.48 เท่าของวิธีที่ 3
นั่นคือ พอจะบอกได้ว่า การแบ่งส่วนอ่านและประมวลผลพิกเซลรูป จะใช้เวลาเพิ่มขึ้นอีกประมาณ 1.5-1.7 เท่าของวิธีปกติ แต่ความเป็นจริงแล้วค่านี้ยังขึ้นอยู่กับหลายปัจจัย ทั้งจำนวนแถวทีอ่านในแต่ละครั้ง อัลกอริธึมในการประมวลผลรูป ขนาดและชนิดของรูป ซึ่งถ้าต้องการได้ค่าที่ถูกต้อง คงต้องทดสอบอีกหลายๆครั้งในหลายๆกรณีครับ

No comments:

Post a Comment