โครงงานวันหยุด : สร้างระบบจัดเก็บภาพพร้อมระบบจับการเคลื่อนไหวด้วย Raspberry Pi ภาคสาม


[ภาคหนึ่ง][ภาคสอง]
มาภาคนี้เป็นการปรับปรุง python code และการนำไปทดสอบครับ





วิเคราะห์

งานหลักมีสามงานคือตรวจจับการเคลื่อนไหว จับภาพนิ่งด้วยกล้อง และนำรูปภาพไปวางไว้บน GDrive





งานที่อาจจะเป็นคอขวดของระบบก็งานที่สาม เพราะมันโยงกับการสื่อสารผ่านอินเตอร์เน็ต และการ upload ข้อมูล หากเรากำหนดให้มีการทำงานเป็นแบบเป็นไปตามลำดับ (sequential) คงไม่ค่อยดีเท่าไหร่ โชคดีที่ Python มีการกล่าวถึงเรื่องการแยกการทำงานออกเป็น Thread หลักการคือการแยกงานออกเป็นงานย่อย ทำงานอิสระต่อกันไม่ต้องรอกัน แต่งานแต่ละงานอาจใช้ทรัพยากรร่วมกันได้ ในกรณีนี้คือการใช้ Queue [1,2,3] มาเก็บข้อมูลแฟ้มภาพที่กล้องจับมาได้




ดังนั้นงานจับภาพก็จะจับภาพไปเรื่อย ๆ เมื่อตรวจพบการเคลื่อนไหว และงาน upload ภาพก็จะเร่ิมทำงานเมื่อมีข้อมูลอยู่ใน Queue และทำไปเรื่อยๆ จนกว่า Queue จะว่าง ผังการทำงานจะเป็นดังรูปข้างบนครับ


งานจับภาพนิ่งด้วยกล้อง

งานนี้ผมสร้าง Class ขึ้นมาอีกหนึ่ง Class เพื่อทำงานในลักษณะของ Thread (ท่านสามารถปรับ Class เดิมให้ทำงานเป็น Thread โดยไม่ต้องสร้าง Class ใหม่ก็ได้) โดยการทำงานคือ จับภาพ บันทึกลง sd card แล้วก็ส่งข้อมูลเข้าไปใน queue


class Snapper(threading.Thread):

   def __init__(self,queue,capture):
      threading.Thread.__init__(self)
      self.queue = queue
      self.imgcap = ImageCapture()
      self.exitFlag=False
      self.imgcap=capture

   def snap(self,pin):
      if self.imgcap and self.queue :
         fname = self.imgcap.capture()
         if fname :
            #fname is null means snapping is error.
            #put file name into queueu without waiting time.
            self.queue.put(fname,False)

   def run(self):
      #you need it to make it runnable
      while not self.exitFlag :
         #do nothing
         pass

งาน upload ไฟล์ภาพไปที่ GDrive

ผมก็สร้าง Class มาอีกหนึ่ง ทำงานแบบ Thread  โดยจะทำงานก็ต่อเมื่อมีข้อมูลใน queue หรือ queue ไม่ว่าง ทำการดึงข้อมูลออกมาแล้วก็ส่งไป GDrive หลังจากทำงานสำเร็จก็ลบไฟล์ที่อยู่บน SD-Card ทิ้งไหเสีย เพราะไฟล์ไปอยู่บน GDrive แล้วนี่นา


class Uploader(threading.Thread):
  def __init__(self,queue):
    threading.Thread.__init__(self)
    self.gdata = RaspiGData()
    self.queue=queue
    self.exitFlag=False

  def run(self):
    import os
    while not (self.exitFlag and self.queue.empty()) :
      fname = self.queue.get() 
      if fname :
        if not self.gdata.ready :
          self.gdata.create_client()
        else :
          success=self.gdata.upload_image(fname)
          if success :
            os.unlink(fname)



งานตรวจจับการเคลื่อนไหว

เราใช้ประโยชน์จาก RPi.GPIO [4] ซึ่งทางผู้สร้างเขาได้ให้มันทำงานในลักษณะของ Thread อยู่ ก็เลยสบายไม่ต้องทำอะไรมากนักเพียงแก้ไข Code ของเรานิดหน่อย คือจากเดิมที่เราเคยทำเป็น Class ไว้ก็ไม่ต้องแล้วเพื่อความสะดวกเรามาเรียกใช้งานในตัวโปรแกรมหลักเลย (main.py)

pir_pin=7 # PIR Pin is 7th on board
GPIO.setmode(GPIO.BOARD)
GPIO.setup(pir_pin,GPIO.IN,pull_up_down=GPIO.PUD_UP)

และเราต้องเติมคำสั่งไปอีกสองบรรทัดเพื่อให้ RPi.GPIO ทำงานในลักษณะของ Thread คือ

GPIO.add_event_detect(pir_pin, GPIO.RISING)
GPIO.add_event_callback(pir_pin, snapper.snap)

บรรทัดแรกเรากำหนดให้ RPi.GPIO รับฟังเหตุการณ์ที่เกิดขึ้นที่ PIR Pin ของเรา (Pin 7) ด้วยประเภทของเหตุการณ์คือ GPIO.RISING หรือเหตุการณ์ที่ค่าความต่างศักดิ์เริ่มเปลี่ยนจาก 0 ไป 1 เราไม่เอา จาก 1 ไป 0 เพราะเป็นเหตุการณ์ที่วัตถุเคลื่อนที่ผ่าน Sensor ไปแล้ว

บรรทัดที่สอง เราให้ RPi.GPIO ไปเรียกใช้งาน snapper.snap ซึ่งก็คือ function หนึ่งใน Class Snapper ที่สร้างไว้ตอนต้น

แต่งเติม

ผมเติมงานไปสองงานคือ cleanup และ  shutdown เพื่อล้างข้อมูลทั้งหมดและ shutdown เพื่อให้ Rasberry Pi ปิดตัวเองหลังการทำงานสมบูรณ์แล้ว จะได้ไม่เปลืองพลังงาน


def cleanup():
 global imgcap
 imgcap.quit()
 GPIO.cleanup()

def shutdown():
 import subprocess
 cmd = "/usr/bin/sudo /sbin/shutdown -h now"
 process = subprocess.Popen(cmd.split(),stdout=subprocess.PIPE)
 output=process.communicate()[0]



Code ทั้งหมด


main.py
#!/usr/bin/python

##################################
### Author : Somchai Somphadung ###
### Date : 2014-09-20                          ###
### N3A Media                                     ###
#################################

from ImageCapture import  ImageCapture
from RaspiGdata import RaspiGData
import time
import threading
import Queue
import RPi.GPIO as GPIO
import ConfigParser
import sys

conf_file="gdrive.conf"

#################################################
class Uploader(threading.Thread):
 def __init__(self,queue):
  threading.Thread.__init__(self)
  self.gdata = RaspiGData()
  self.queue=queue
  self.exitFlag=False

 def run(self):
  import os
  
  while not (self.exitFlag and self.queue.empty()) :
   fname = self.queue.get() 
   
   if fname :
    if not self.gdata.ready :
      self.gdata.create_client()
    else :
     success=self.gdata.upload_image(fname)
     if success :
      os.unlink(fname)
      pass
   
   

#################################################
class Snapper(threading.Thread):

 def __init__(self,queue,capture):
  threading.Thread.__init__(self)
  self.queue = queue
  self.imgcap = ImageCapture()
  self.exitFlag=False
  self.imgcap=capture

 def snap(self,pin):
  if self.imgcap and self.queue :
   fname = self.imgcap.capture()
   if fname :
    self.queue.put(fname,False)
 
 def run(self):
  while not self.exitFlag :
   pass

    
   
#################################################

def cleanup():
 global imgcap
 imgcap.quit()
 GPIO.cleanup()

def shutdown():
 import subprocess
 cmd = "/usr/bin/sudo /sbin/shutdown -h now"
 process = subprocess.Popen(cmd.split(),stdout=subprocess.PIPE)
 output=process.communicate()[0]
 

#################################################
imgcap = ImageCapture()
imgqueue = Queue.Queue()

if __name__ == "__main__" :
 if len(sys.argv) < 2 :
  dur = 20
 else :
  dur = int(sys.argv[1])

 config  = ConfigParser.ConfigParser()
 config.read(conf_file)
 pir_pin=int(config.get('gpio','pin'))
 if config.get('gpio','mode') == "board" :
  GPIO.setmode(GPIO.BOARD)
 else :
  GPIO.setmode(GPIO.BCM)

 GPIO.setup(pir_pin,GPIO.IN,pull_up_down=GPIO.PUD_UP)
 uploader = Uploader(imgqueue)
 snapper = Snapper(imgqueue,imgcap)

 try:
  GPIO.add_event_detect(pir_pin, GPIO.RISING)
  GPIO.add_event_callback(pir_pin, snapper.snap)
  uploader.start()
  snapper.start()
  s_time=time.time()
  e_time=s_time+dur
  while s_time < e_time :
   s_time=time.time()
   print str(e_time - s_time)
   time.sleep(1)

 except KeyboardInterrupt:
  print "Exit"

 GPIO.remove_event_detect(pir_pin)
 uploader.exitFlag=True 
 snapper.exitFlag=True      
 uploader.join()#wait until Thread job is done.
 cleanup() 
 shutdown()


ImageCapture.py
###################################
###  Author : Somchai Somphadung   ###
###  Date : 2014-09-20                             ###
### N3A Media                                         ###
##################################

import pygame
import pygame.camera
import pygame.image
import time
import ConfigParser

conf_file="gdrive.conf"

class ImageCapture:

 def __init__(self,imgsize=(320,240),pic_format="RGB",loc="/home/pi/"):
  # pic_format could be RGB, YUV, HSV
  config  = ConfigParser.ConfigParser()
  config.read(conf_file)
  pygame.init()
  pygame.camera.init()
  cameras = pygame.camera.list_cameras()
  if not cameras :
    raise ValueError("There is not camera attached")
    self.camera=None
  
  w=config.get('captured_image','width')
  h=config.get('captured_image','height')
  picsize=(int(w),int(h))
  picformat=str(config.get('captured_image','format'))
  self.camera=pygame.camera.Camera(cameras[0],picsize,picformat)
  self.is_end=False
  self.img_loc=config.get('captured_image','folder')

 def create_img_name(self):
  exp="%Y_%m_%d-%H_%M_%S"
  return "snap_"+time.strftime(exp)+".jpeg"

 def capture(self):
  if self.camera is not None :
   self.camera.start()
   snapshot = self.camera.get_image()
   fname = self.img_loc+"/"+self.create_img_name()
   pygame.image.save(snapshot,fname)
   self.camera.stop()
   return fname
  else :
   return None

 def quit(self):
  pygame.quit()



RaspiGdata.py
##################################
### Author : Somchai Somphadung ###
### Date : 2014-09-20                          ###
### N3A Media                                     ###
#################################
import os.path
import sys
import gdata.data
import gdata.docs.data
import gdata.docs.client
import ConfigParser

conf_file="gdrive.conf"

class RaspiGData :

 def __init__(self):
         config  = ConfigParser.ConfigParser()
         config.read(conf_file)
  self.ready=False
         self.source=config.get('gdrive','source')   
         self.username=config.get('gmail','user')
         self.pwd=config.get('gmail','pwd')
         self.folder=config.get('upload_folder','folder')
         self.create_client()


 def create_client(self):
  try:
   self.client = gdata.docs.client.DocsClient(source=self.source)
   self.client.http_client.debug = False
   self.client.ClientLogin(self.username,self.pwd,service=self.client.auth_service, source=self.client.source)      
   self.ready=True
  except :
   self.ready=False

 def get_folder(self):
   col = None
   if not self.ready :
     return col
   for resource in self.client.GetAllResources(uri='/feeds/default/private/full/-/folder'):
     if resource.title.text == self.folder :
     col = resource
     break
  return col

 def upload(self, file_path, folder_resource):
  #Upload document file and return
  doc = None
  try:
   doc = gdata.docs.data.Resource(type='document', title=os.path.basename(file_path))
   media = gdata.data.MediaSource()
   media.SetFileHandle(file_path, 'image/jpeg')
   doc = self.client.CreateResource(doc, media=media, collection=folder_resource)
  except:
   pass   
  return doc

 def upload_image(self, image_file_path):
  folder_resource = self.get_folder()
  #if not folder_resource:
  # raise Exception('Could not find the %s folder' % self.folder)
  if folder_resource:
   return self.upload(image_file_path, folder_resource)
  else :
   return None




นำไปทดสอบ


ผมทำกล่องไม้ด้วยครับ ข้อดีของการไม่มี Case ก็ดีตรงที่เราเอาไปใส่ในภาชนะของเราเองได้ ถ้าต้องการพลางตัวก็ต้องทำตัวเหมือนเครื่องใช้ทั่วไปในบ้าน

รายการอุปกรณ์

ยัดลงกล่อง



พร้อมทดสอบ


ปล

เหตุผลว่าทำไมถึงต้องนำภาพไปเก็บไว้ที่ GDrive ทำไมไม่เก็บไว้ที่ SD-Card คำตอบคือเก็บไว้ได้ครับ เพียงแต่ว่าการเก็บภาพไว้ที่อื่นแบบเวลาจริงนั้นจะเป็นการสำรองข้อมูลไปในตัว หลายครั้งที่เกิดเรื่องราวขึ้นเป็นข่าวกันคืออุปกรณ์มักจะถูกทำลายหรือขโมย แล้วเราก็ตามหาอะไรไม่ได้ เพราะข้อมูลภาพมันติดไปกับอุปกรณ์หมดแล้ว การแยกส่วนกันแบบนี้ ถ้าจะเอาภาพไปด้วยก็ต้องไปถามเอาจาก Google โน้นหรือต้องผ่านเราไปก่อนหล่ะ


โปรดตรวจสอบ

โครงงานนี้ใช้ Web Camera ซึ่งในทางปฎิบัติแล้ว ท่านอาจซื้อหามาจากหลากหลายผู้ผลิต แต่ไม่ใช่ทุกรายจะได้รับการทดสอบว่าใช้งานได้กับ Raspberry Pi ดังนั้นก่อนเลือกซื้อ Web Camera มาใช้งานโปรดตรวจสอบข้อมูลกับเว็บแห่งนี้ก่อน http://elinux.org/RPi_USB_Webcams

--------------------------------
เอกสารอ้าอิง
[1] http://www.troyfawkes.com/learn-python-multithreading-queues-basics/
[2] https://docs.python.org/2/library/threading.html
[3] http://www.tutorialspoint.com/python/python_multithreading.htm
[4] http://sourceforge.net/p/raspberry-gpio-python/wiki/Inputs/

ความคิดเห็น