2015年1月21日 星期三

[Raspberry Pi] DIY 一個Raspberry Pi 上的webcam 影像分享

我自己弄了一個可以透過區網傳輸Raspberry Pi 的webcam影像觀看程式,不計較效能

以及流量問題 ,我只希望能夠用網頁觀看,在多個不同的裝置(PC ,平板,手機)。當然

也會有些許的延遲(幾秒鐘),接下來也會繼續嘗試用其他的方式,讓影像不要Delay那麼嚴

重,在此先做個筆記。


一、環境說明

(一)Raspberry Pi  B+

           os 為 wheezy ,使用python 2.7 搭配OpenCV 2.4.10 。

          網卡 Edimax EW-7811Un 

(二)WebCam 為Logitech C170

  (三)WebSocket Server 由 GlassFish 4.1 +  Java 實作。

  (四)觀看端由 Html + JavaScript 實作。


二、整體架構圖




三、架構說明:

(一) 輸入端由OpenCV開啟Webcam取得影像 ,以Jpeg作為壓縮格式後,轉成

       Base64字串,由WebSocket client端發送(為了節省頻寬,目前品質壓到30%)。

(二) websocket 由python 的websocket-client package ,安裝方式,請參考      

[Python] 用Python 做 WebSocket Client


(三)WebSocket Server在onMessage (收到圖片時)會傳給所有連線中的Client,
 除了發送圖片的Raspberry Pi


(四) 觀看端在網頁載入完成後,會連線到WebSocket Server,等待接收訊息。當觀看端的onMessage事件發生時,會解碼收到的圖片訊息,並且顯示在網頁上,並更新時間。


四、程式碼

(一) Raspberry Pi 上的發送圖片程式 Webcam.py

import sys
import cv
import cv2
import numpy
import base64
from websocket import create_connection
#connection to websocket server
ws = create_connection('ws://192.168.0.101:8080/DemoEchoServer/echo')
#JPEG encode_param
encode_param=[int(cv2.IMWRITE_JPEG_QUALITY),30]
#select first VideoCapture 
capture = cv2.VideoCapture(0)
#set WebCame Frame Width
capture.set(3,640)
#set WebCam Frame Height
capture.set(4,480)
#defind function
def repeat():
    # Capture frame-by-frame
    ret, frame = capture.read()
    #encode to jpg
    if (type(ret)!=type(None)):
        result,imgencode=cv2.imencode('.jpg',frame,encode_param )
    # to numpy array
        data = numpy.array(imgencode)
    # tostring
        stringData = data.tostring()
    #encode to base64
        encoded = base64.b64encode(stringData)
    #send
        ws.send(encoded)
    #sleep 60millisecond
        cv2.waitKey(60)
while True:
    repeat()


(二)WebSocket Server EchoServer.java
import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import javax.websocket.EndpointConfig;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint("/echo")
public class EchoServer {
  
  //用來存放Session的集合
  private static final Set sessions = Collections
                     .synchronizedSet(new HashSet());  
 
    @OnOpen
    public void onOpen(Session session,EndpointConfig config) {        
        session.setMaxBinaryMessageBufferSize(1024*1024);
        session.setMaxTextMessageBufferSize(1024*1024);
        System.out.println(session.getId()+" open");
       //把session加入集合中  
       sessions.add(session);
    }   

    @OnClose
    public void onClose(Session session) {
        System.out.println(session.getId()+" close");
        //把session從集合中移除
         sessions.remove(session);
    }

    @OnMessage    
   public void onMessage(String message , Session session) throws IOException {       
      
       try
       {
         //sent Image 
        for(Session session2 : sessions)
       {
           if (!session2.getId().equals(session.getId()))
           {
            session2.getAsyncRemote().sendText(message);
           }
            //session2.getBasicRemote().sendText(message);
          
        }
       catch (Exception e)
        {
           e.printStackTrace();;
        }      
    }
   
    @OnError
    public void onError(Throwable t) {
        t.printStackTrace();
    }
}

(三)觀看端 display.html
1:  <html>  
2:    <head>  
3:      <title>TODO supply a title</title>  
4:      <meta charset="UTF-8">  
5:      <meta name="viewport" content="width=device-width, initial-scale=1.0">  
6:       <script language="javascript" type="text/javascript">   
7:      //echo server的網址   
8:      var wsUri = "ws://192.168.0.101:8080/DemoEchoServer/echo";   
9:      //定義元件   
10:      var outputElement;   
11:      //初始化function    
12:      function init()   
13:      {   
14:       outputElement = document.getElementById("output");   
15:       live();   
16:      }   
17:      //執行websocket
18:      function live() {   
19:       //連線到echo server   
20:       websocket = new WebSocket(wsUri);   
21:       //連線成功要執行的function   
22:       websocket.onopen = function (evt) {   
23:        onOpen(evt)   
24:       };   
25:       //連結結速要執行的function   
26:       websocket.onclose = function (evt) {   
27:        onClose(evt)   
28:       };   
29:       //收到echo server傳來的訊息要做的function   
30:       websocket.onmessage = function (evt) {   
31:        onMessage(evt)   
32:       };   
33:       //當發生錯誤時要執行的function   
34:       websocket.onerror = function (evt) {   
35:        onError(evt)   
36:       };   
37:      }   
38:      //連線成功後,在output Element顯示時間  
39:      function onOpen(evt) {   
40:        var currentdate = new Date();   
41:        var datetime = "Last Sync: " + currentdate.getDate() + "/"  
42:          + (currentdate.getMonth()+1) + "/"   
43:          + currentdate.getFullYear() + " @ "   
44:          + currentdate.getHours() + ":"   
45:          + currentdate.getMinutes() + ":"   
46:          + currentdate.getSeconds();  
47:       writeToScreen(datetime+" onOpen")  
48:      }   
49:      //當中斷連線時輸出訊息在outputElement上      
50:      function onClose(evt) {   
51:        var currentdate = new Date();   
52:        var datetime = "Last Sync: " + currentdate.getDate() + "/"  
53:          + (currentdate.getMonth()+1) + "/"   
54:          + currentdate.getFullYear() + " @ "   
55:          + currentdate.getHours() + ":"   
56:          + currentdate.getMinutes() + ":"   
57:          + currentdate.getSeconds();  
58:       writeToScreen(datetime+" onClose")  
59:      }   
60:      //收到訊息時顯示在螢幕上, 並且解密base64圖片資料   
61:      function onMessage(evt) {   
62:        var currentdate = new Date();   
63:        var datetime = "Last Sync: " + currentdate.getDate() + "/"  
64:          + (currentdate.getMonth()+1) + "/"   
65:          + currentdate.getFullYear() + " @ "   
66:          + currentdate.getHours() + ":"   
67:          + currentdate.getMinutes() + ":"   
68:          + currentdate.getSeconds()+" "  
69:          + currentdate.getTime();  
70:       writeToScreen(datetime+" onMessage")  
71:      document.getElementById("ItemPreview").src = "data:image/png;base64," + evt.data;   
72:      }   
73:      //發生錯誤時顯示訊息在outputElement上   
74:      function onError(evt) {     
75:         var currentdate = new Date();   
76:        var datetime = "Last Sync: " + currentdate.getDate() + "/"  
77:          + (currentdate.getMonth()+1) + "/"   
78:          + currentdate.getFullYear() + " @ "   
79:          + currentdate.getHours() + ":"   
80:          + currentdate.getMinutes() + ":"   
81:          + currentdate.getSeconds();  
82:       writeToScreen(datetime+" onError")  
83:      }   
84:      //發送訊息   
85:      function doSend(message) {   
86:       writeToScreen("己傳送 " + message);   
87:       websocket.send(message);   
88:      }   
89:      //輸出訊息在outputElement上   
90:      function writeToScreen(message) {   
91:       var pre = document.getElementById("myp");   
92:       pre.style.wordWrap = "break-word";   
93:       pre.innerHTML = message;   
94:       output.appendChild(pre);   
95:      }   
96:      //加入事件,當載入完成後執行init()   
97:      window.addEventListener("load", init, false);   
98:     </script>   
99:    </head>   
100:    <body>  
101:      <div id="output"> <p id="myp"></div>  
102:      <div>  
103:        <img id="ItemPreview" src="" />  
104:      </div>  
105:    </body>  
106:  </html>  

五、執行程式
 (一)先開啟WebSocket Server。
 (二)在Raspberry Pi上輸入 python Webcam.py
 (三)開啟display.html觀看。

 六、執行結果



七、待解決問題

 (一) 畫面延遲問題, 以目前看來至少會慢個約5秒,慢的主要原因是資料量,每一次
  都發送一張完整的圖片 (640 *480)  ,樹梅派只有單核心, 要一直取影像再壓成JPEG傳送,
可能會有點吃力。

(二) 透過websocket Server 再做一次分派,好處是可以讓多個裝置同時間做觀看。雖然每一個之間還是會有Delay。把傳送影像用的session2.getBasicRemote().sendText(message); 改成用session2.getAsyncRemote().sendText(message); 效能看起來些許的提昇。



沒有留言:

張貼留言