วันเสาร์ที่ 16 กรกฎาคม พ.ศ. 2559

การส่งภาพถ่ายจาก Raspiberry PI ผ่าน NetPie

ต่อเนื่องจากเรื่องที่เขียนไว้ก่อนหน้านี้เรื่อง "การถ่ายภาพการสังเคราะห์แสงพืชด้วยกล้อง Raspberry Pi NoIR" หลังจากเราถ่ายภาพเสร็จหากเราต้องการส่งภาพนั้นไปยังที่อื่นเพื่อวัตถุประสงค์ใดก็แล้วแต่ เช่น ส่งไปจัดเก็บบน Cloud Server หรือจะส่งให้ผู้เชี่ยวชาญช่วยวิเคราะห์ หรือแม้แต่จะส่งให้เพื่อนก็สามารถทำได้หลายวิธี วิธีการหนึ่งคือการส่งข้อมูลภาพด้วยเทคโนโลยี Internet of Things (IoT) ครับ ซึ่งผมก็เลือกใช้บริการ NETPIE ซึ่งเป็นบริการของ NECTEC ในเมืองไทยเรา ครับ ความจริงแล้วบริการแบบนี้ในต่างประเทศก็มีเยอะอยู่ แต่ใช้ของไทยเราได้เปรียบเรื่องความเร็วครับ หากต้นทางและปลายทางของการสื่อสารอยู่ภายในประเทศไทย และอุปกรณ์ในบทความนี้จะเป็น Raspberry Pi นะครับ

รู้จักและสมัครเป็นสมาชิก NETPIE

ก่อนจะใช้บริการต้องผ่านขั้นตอนการลงทะเบียนก่อน ข้อมูลตรงนี้ผมจะไม่กล่าวถึง แต่จะละให้ผู้สนใจและยังไม่ทราบข้อมูลไปดูกันเองที่ https://netpie.io/ ครับ การลงทะเบียนขอใช้บริการมีทั้งเสียงเงินและไม่เสียเงินครับ เลือกเอาตามชอบ 

สร้าง Application 

หลังจากผ่านขั้นตอนการลงทะเบียนแล้ว ก่อนจะเริ่มต้นทำอะไร เราต้องกำหนดพื้นที่ทำงานสมมุติขึ้นมาก่อนใน NETPIE เขาเรียกว่า Application สิ่งที่ได้จากขั้นตอนนี้คือ 
  1. Application key และ
  2. Secret Key
และเช่นกัน ผมก็จะละขั้นตอนตรงนี้ไว้ให้อ่านเองที่ https://netpie.io/tutorials เพราะเขาอธิบายไว้ละเอียดดีอยู่แล้ว

ภาพตัวอย่างที่เราจะได้เห็นหลังการสร้าง Application แล้ว
keys ที่เราต้องนำไปใช้ในการสื่อสารอุปกรณ์ของเรากับ NETPIE

ติดตั้ง Application Interface

ปัจจุบันทาง  NETPIE ได้เตรียม API การสื่อสารไว้ 2 รูปแบบคือ (https://netpie.io/#services) REST, Pub-Sub pattern ผมเลือกใช้ Pub-Sub pattern ครับ เพราะใช้งานกับ Python ได้สะดวก ซึ่งต้องทำการติดตั้งก่อนโดย
  1. ใช้คำสั่ง $sudo pip install microgear หรือ
  2. ดาวน์โหลด source code มาไว้ก่อนแล้วค่อยติดตั้ง ก็ได้ เขาก็อธิบายขั้นตอนไว้อีกเช่นเคย

เขียน Python Code 

หลังจากลงทะเบียนเข้าใช้งาน  สร้าง Application ได้ Key มาและติดตั้ง API แล้ว ขั้นตอนต่อไปก็คือการสร้าง Python code ครับ  หลักการในการส่งข้อมูลภาพนั้นไม่มีอะไรซับซ้อน หากท่านดูจากเอกสารประกอบการใช้งานภาษา Python จะพบว่าทาง NETPIE อนุญาตให้ส่งข้อมูลระหว่างอุปกรณ์ผ่าน Pub-Sub Pattern ในรูปแบบของ String เท่านั้น แต่ไม่ได้กำหนดขนาดของข้อมูลไว้ ดังนั้นหากเราเปลี่ยนข้อมูล Binary ของภาพถ่ายให้เป็น String ก่อนแล้วทำการส่งออกไปก็ย่อมจะทำได้ ซึ่งในภาษา Python มี library ชื่อ base64 ช่วยเรื่องนี้อยู่แล้ว





ส่วนที่ 1



import microgear.client as netpie
import base64, zlib, time

ส่วนนี้เป็นการนำเข้า Library ที่จำเป็นต้องใช้งาน จะเห็นว่านอกจาก microgear แล้วก็มีการนำ base64 เข้ามาร่วมใช้งานด้วย

ส่วนที่ 2


key = '[your key]'
secret = '[your secret key]'
app = '[your application name]'

netpie.create(key,secret,app,{'debugmode': True})

กำหนด Key ต่าง ๆ ที่ NETPIE กำหนดให้มี

netpie.create(key,secret,app,{'debugmode': True})

สร้างตัวแปรที่รองรับการเชื่อมต่อระหว่างอุปกรณ์ของเรากับ NETPIE

ส่วนที่ 3


def connection():
 print("Connected")
 
def subscription(topic,msg):
 global running
 #print(topic+" : "+msg)
 decode_base64(msg)
 running = False

def callback_error(msg) : 
 print(msg)  

def callback_reject(msg) :
 print msg
 print "Script exited"
 exit(0)

เป็นการประกาศ callback function ตามคู่มือของ NETPIE ในโครงสร้างของระบบงานนี้จะแบ่ง party ในระบบออกเป็นสองส่วนคือ Sender และ Receiver จึงมีการเพิ่มเติม code ใน subscription เป็น

Sender :

def subscription(topic,msg):
    global ready_to_send
    if not ready_to_send :
        if msg =='iamok':
     ready_to_send = True
     print "Reciever is ready"

จะมีการตรวจสอบความพร้อมของฝั่งผู้รับก่อนหากยังไม่มีคำตอบก็จะยังคงสถานะ False ไว้กับตัวแปร ready_to_send

 ส่วนที่ 4

Sender :


def encode_base64(img_data):
 encoded = None

 try:
  #compress it first.
  compressed_data = zlib.compress(img_data.getvalue(),9)
  
  #encode it to base64 string
  encoded = base64.b64encode(compressed_data)  
 except:
  pass 
  
 return encoded

หน้าที่ของฟังก์ชั่น encode_base64() คือการเปลี่ยนข้อมูลภาพที่ได้จากกล้องให้อยู่ในรูปแบบ base64 string เพื่อให้สามารถส่งข้อมูลผ่าน NETPIE ได้ แต่เนื่องจากการเปลี่ยนจากข้อมูล binary ไปเป็น string จะทำให้ขนาดของข้อมูลโตขึ้นมาหลายเท่าตัว ดังนั้นการเพิ่มเรื่อง compression data เข้าไปก็จะช่วยลดเรื่องขนาดของ bandwidth ลงไปได้เยอะ ที่นี้ขอให้สังเกตุขั้นตอนนะครับ ว่าผมใช้การ compress ข้อมูลภาพก่อนทำการ encode ให้เป็น base64 string ทั้งนี้ก็เพราะข้อกำหนดของ NETPIE ที่ให้เราต้องส่งข้อมูลแบบ string เราจึงต้องสร้างข้อมูล string ในขั้นตอนสุดท้ายก่อนส่งไปยัง NETPIE นั่นเอง

Receiver :


def decode_base64(compressed_b64str=None,save_to_file=None): 
 try :
  #firstly, decode it
  decoded = base64.decodestring(compressed_b64str)
  decompr = zlib.decompress(decoded)
  #save it if is needed.
  if save_to_file is not None:
   with open(save_to_file,"wb") as fh:
    fh.write(decompr)
  else:
   #just display on screen
   w,h = 640,480
   image = Image.open(BytesIO(decompr))
   image.show()
 except:
  pass 

หน้าที่ของฟังก์ชั่น dencode_base64() ทำงานตรงข้ามกับ encode_base64() เพื่อนำ base64 string กลับมาอยู่ในรูปแบบ binary เพื่อจะได้นำไปประมวลผลต่อได้

ส่วนที่ 5

เป็นส่วนที่ทำหน้าที่ถ่ายภาพนิ่งด้วย Raspberry Pi Camera Module ซึ่งจะมีอยู่เฉพาะทาง Sender เท่านั้น


def snap():
 global camera
 
 str_img = BytesIO()
 camera.start_preview()
 time.sleep(2)
 camera.capture(str_img,format='jpeg')
 camera.stop_preview()
 str_img.seek(0) 
 
 return str_img

code ในส่วนนี้ได้มีการนำเอา BytesIO ซึ่งเป็น in memory buffer แบบหนึ่ง มีวิธีการใช้งานคล้ายกับ File ทั่วไปเพียงแต่จะอยู่ในหน่วยความจำแทนที่จะอยู่ใน storage media ในชุดคำสั่ง camera.capture(str_img,format='jpeg')  เป็นการสั่งให้นำข้อมูลภาพจากกล้องไปเก็บไว้ในตัวแปร str_img ซึ่งเป็น BytesIO ในรูปแบบการ encode แบบ Jpeg ครับ และคำสั่ง str_img.seek(0) เป็นการสั่งให้เลื่อน pointer กลับไปที่ Byte แรก ตรงนี้ก็เพื่อให้มั่นใจว่าข้อมูลที่เราจะจัดส่งออกไปนั้นได้ถูกส่งไปออกไปตั้งแต่ Byte แรกนั่นเอง

ฉบับสมบูรณ์

Sender : (Raspberry Pi)


import microgear.client as netpie
import time
import base64
from picamera import PiCamera
from io import BytesIO
import zlib

key = 'xxxxxxxx'
secret = 'xxxxxxxxxxxx'
app = 'N3AFarm01'

netpie.create(key,secret,app,{'debugmode': True})
connected = False

def connection():
 global connected
 connected = True
 print("Connected")
 
def subscription(topic,msg):
 global this_role,ready_to_send
 if this_role == 'reciever' :
  decode_base64(msg,None) # don't need to save on disk
  running=False
 else :
  if not ready_to_send :
   if msg =='iamok':
    ready_to_send = True
    print "Reciever is ready"
  
   
 

def callback_error(msg) :
    print(msg)

def callback_reject(msg) :
    print (msg)
    print ("Script exited")
    exit(0)

def encode_base64(img_data):
 encoded = None

 try:
  #compress it first.
  compressed_data = zlib.compress(img_data.getvalue(),9)
  
  #encode it to base64 string
  encoded = base64.b64encode(compressed_data)  
 except:
  pass 
  
 return encoded
  
def decode_base64(compressed_b64str=None,save_to_file=None): 
 try :
  #firstly, decode it
  decoded = base64.decodestring(compressed_b64str)
  decompr = zlib.decompress(decoded)
  #save it if is needed.
  if save_to_file is not None:
   with open(save_to_file,"wb") as fh:
    fh.write(decompr)
  else:
   #just display on screen
   w,h = 640,480
   image = Image.fromstring('RGB',(w,h),decompr)
   image.show()
 except:
  pass   

def snap():
 global camera
 
 str_img = BytesIO()
 camera.start_preview()
 time.sleep(2)
 camera.capture(str_img,format='jpeg')
 camera.stop_preview()
 str_img.seek(0) 
 
 return str_img
 

camera = PiCamera()
camera.resolution=(640,480)


this_name = 'n3a2'     
those_name = 'n3a1'
this_role = 'sender'
running = True
ready_to_send = False

netpie.setname(this_name)
netpie.on_reject = callback_reject
netpie.on_connect = connection
netpie.on_message = subscription
netpie.on_error = callback_error
netpie.subscribe("/test")
netpie.connect(False) 



if this_role=='sender':
 while not ready_to_send :
  netpie.chat(those_name,'ruok')
  time.sleep(2)
   
 snap_shot = snap()
 b64 = encode_base64(snap_shot)
 netpie.chat(those_name,b64)
 
 time.sleep(2)
else :
 
 while running:
  pass

Receiver :(ใครก็ได้)


import microgear.client as netpie
import time
import base64
from PIL import Image
from io import BytesIO
import zlib

key = 'xxxxxxxx'
secret = 'xxxxxxxxxxx'
app = 'N3AFarm01'

netpie.create(key,secret,app,{'debugmode': True})
connected = False

def connection():
 global connected
 connected = True
 print("Connected")
 
def subscription(topic,msg):
 global this_role,ready_to_receive 
 if this_role == 'receiver' :
  if not ready_to_receive :
   if msg=='ruok' :
      netpie.chat(those_name,'iamok')
      ready_to_receive = True    
   else : 
      print "recieving image data."
      decode_base64(msg,None) # don't need to save on disk
      print "process is done"
 else :
   print(topic+":"+msg)

def callback_error(msg) :
    print(msg)

def callback_reject(msg) :
    print (msg)
    print ("Script exited")
    exit(0)

def encode_base64(img_data):
 encoded = None
 try:
  #compress it first.
  compressed_data = zlib.compress(img_data.getvalue(),9)
  
  #encode it to base64 string
  encoded = base64.b64encode(compressed_data)  
 except:
  pass 
  
 return encoded
  
def decode_base64(compressed_b64str=None,save_to_file=None): 
 try :
  #firstly, decode it
  decoded = base64.decodestring(compressed_b64str)
  decompr = zlib.decompress(decoded)
  #save it if is needed.
  if save_to_file is not None:
   with open(save_to_file,"wb") as fh:
    fh.write(decompr)
  else:
   #just display on screen
   w,h = 640,480
   image = Image.open(BytesIO(decompr))
   image.show()
 except:
  pass   




this_name = 'n3a1'     
those_name = 'n3a2'
this_role = 'receiver'
running = True
ready_to_receive = False 

netpie.setname(this_name)
netpie.on_reject = callback_reject
netpie.on_connect = connection
netpie.on_message = subscription
netpie.on_error = callback_error
netpie.subscribe("/test")
netpie.connect(False)


try :
 while running:
  pass
except KeyboardInterrupt :
 running=False 


Download Code at Github 



ทดสอบกัน

ผลการทดสอบออกมาเป็นที่น่าพอใจครับ ผมใช้การส่งภาพจาก Raspberry Pi B3 ไปยังเครื่องคอมพิวเตอร์ Notebook โดยให้ทั้งสองใช้บริการ internet provider คนละเจ้ากัน รวมเวลาที่ใช้ตั้งแต่เริ่มต้นจนเห็นภาพทั้งหมดก็ประมาณ 3 วินาที ถือว่าดีมากทีเดียวครับ


ภาพฝั่งผู้รับซึ่งใช้คอมพิวเตอร์ Notebook รับภาพจากผู้ส่ง (Raspberry Pi)  ขนาดของภาพคือ 640 x 480 pixels

แนวทางการพัฒนาต่อไป

เท่าที่ทำมานี่ก็เพียงการเริ่มต้นและเพื่อทดสอบว่าทำได้หรือไม่ การพัฒนาต่อยอดต้องมีต่อไป เช่น การทำให้ระบบทำงานโต้ตอบกันระหว่างอุปกรณ์แบบอัตโนมัติ, ใช้สร้าง data log ที่เป็น image หรือแม้แต่เรื่องทางสันทนาการ ฯล สำหรับแนวคิดที่จะทำเป็นภาพต่อเนื่องแบบวิดีโอนั้นอาจไม่ค่อยจะดีสักเท่าไหร่ เพราะจะมี overhead ในระบบสูงมาก ครับ

เอกสารอ่านเพิ่มเติม

1. https://picamera.readthedocs.io/en/release-1.12/recipes1.html
2. https://docs.python.org/3/library/zlib.html
3. https://docs.python.org/3/library/base64.html?highlight=base64#module-base64
4. http://effbot.org/imagingbook/image.htm

17 ความคิดเห็น:

  1. ต้องแบ่ง เป็น 2ไฟล์ หรอ ครับ

    ตอบลบ
    คำตอบ
    1. ไม่แน่ใจในคำถามที่ว่า "ต้องแบ่งเป็น 2 ไฟล์" ถ้าหมายถึง Python code แล้วที่ต้องแบ่งเป็นสองไฟล์ เพื่อให้ทำงานแยกกันระหว่าง ผู้ส่ง กับ ผู้รับ ในกรณีที่ต้องการให้ทำงานแบบสองทาง ก็สามารถประยกุกต์นำทั้งสองไฟล์มารวมกันได้

      ลบ
  2. แล้วโค้ด ตรงผู้รับอะ ครับใช้ ยังงัย คครับ

    ตอบลบ
    คำตอบ
    1. บันทึก code ไว้ในไฟล์ ที่มีนามสกุลเป็น .py แล้วใช้คำสั่ง python [ชื่อไฟล์ที่ตั้งไว้].py

      ลบ
  3. ใน receiver.py ของ เครื่องรับผมรันจาก Windows แล้วขึ้น Error ว่า
    Traceback (most recent call last):
    File "receiver.py", line 4, in
    from PIL import Image
    ImportError: No module named 'PIL'

    ผมต้องทำอย่า่งไรครับ ขอบคุณครับ

    ตอบลบ
    คำตอบ
    1. การใช้งาน PIL หรือ Pillow บน Windows คงต้องออกแรงนิดหน่อย เพราะไม่มีlibrary ที่ออกอย่างเป็นทางการ แต่ก็มีข้อมูลให้ทำตามได้ เช่น https://pillow.readthedocs.io/en/latest/installation.html หรือ http://christianakesson.com/compiling-pil/

      ลบ
    2. ในกรณีที่ไม่ต้องการติดตั้ง PIL ก็มีทางเลือกให้ทำการเขียนข้อมูลเก็บไว้เป็นไฟล์ก่อนได้ เมื่อมีไฟล์แล้วแล้วค่อยใช้เครื่องมืออื่นที่มีอยู่แล้วทำการต่อก็ได้ ในการทำงานเกี่ยวกับรูปภาพในภาษา Python ก็มีอีกหลายตัวนอกจาก PIL เช่น OpenCV, Matplotlib,ฯล ลองพิจารณาดู ครับ

      ลบ
  4. RPi3(ส่ง) ->>>>> Computer(ubuntu)รับ

    มันแปลงรูปไม่ออกครับ

    ตอบลบ
  5. ผมลองทำตามแล้วครับ คือภาพมัน ขึ้นแสดงที่ด้านผู้ส่ง ประมาณ 2วิ จากนั้นด้านผู่รับ จะขึ้นข้อความ
    recieving image data
    process is done
    คือมันไม่แสดงภาพเลยครับ เกิดจากอะไรหรือครับ

    ตอบลบ
    คำตอบ
    1. ลองกำหนดให้ฝั่งผู้รับบันทึกข้อมูลลงไฟล์ โดยกำหนด save_to_file = True ใน

      def decode_base64(compressed_b64str=None,save_to_file=None)
      ....

      เพราะเป็นไปได้ว่าในบางสภาพแวดล้อมการแสดงภาพด้วย PIL ไม่สามารถทำได้
      การบันทึกลงไฟล์จะทำให้เราสามารถนำภาพไปแสดงในสภาพแวดล้อมหรือ Image Viewer อื่นได้

      ลบ
    2. ขอบคุณคับ ตอบกลับไวมากคับ ผมลองทำตามที่พี่เเนะนำ ก็ยังคงเป็นเหมือนเดิมคับ

      ลบ
    3. เก็บข้อมูลภาพไม่ได้ ?
      เปลี่ยน code ใน def decode_base64(...)
      except:
      pass

      เป็น
      except Exception as err:
      print(err)

      เพื่อให้แสดง error message ออกมา อาจจะรู้ว่าต้นเหตุของปัญหา ครับ

      ลบ
  6. ผมทดลองทำตามโดยให้ฝ่ายส่งเป็นบอร์ด Raspberry Pi บอร์ดที่ 1 และฝ่ายรับเป็นบอร์ด Raspberry Pi บอร์ดที่ 2 ได้ผลเหมือนของคุณเกรียงไกรเลยครับ

    ตอบลบ
  7. ความคิดเห็นนี้ถูกผู้เขียนลบ

    ตอบลบ
  8. ลองทำตามแล้วค่ะ รันpython ผ่านแต่ภาพไม่ขึ้นเลยค่ะแล้วเราจะดูไฟล์ภาพได้จากไหนเหรอค่ะ
    >>ขอบคุณค่ะ

    ตอบลบ
  9. ของผมมันขึ้นว่า
    recieving image data.
    process is done
    สลับกันไปเรื่อยๆ ไม่แสดงภาพมาเลย เป็นเพราะอะไรหรอครับ

    ตอบลบ