Friday, May 8, 2015

Python + RPi Camera Module: Capturing to a network stream, display on Tkinter GUI

This example modify from Picamera tutorial: Basic Recipes - 4.9. Capturing to a network streamHere we have two scripts: a server (presumably on a fast machine) which listens for a connection from the Raspberry Pi, and a client which runs on the Raspberry Pi and sends a continual stream of images to the server.

Instead of do nothing on server side, I create Tkinter GUI to display the received image.

This video show how both client and server run on Raspberry Pi 2, remotely with xrdp/remmina.


In server side, in order to run both Tkinter GUI and server connection in parallel, I have to implement both in Thread. For this part, reference to O'Reilly Python Cookbook (2002): Combining Tkinter and Asynchronous I/O with Threads.

pyCamServer.py
#!/usr/bin/python

import time
import threading
import random
import Queue
from PIL import Image, ImageTk
import Tkinter as tk
import socket
import struct
import io
import tkMessageBox

class GuiPart:
    
    def __init__(self, master, queue, endCommand):
        self.queue = queue
        self.endCommand = endCommand
        
        self.root=master
        self.root.title('My Pictures')

        imageFile = "pi.jpg"
        self.image1 = ImageTk.PhotoImage(Image.open(imageFile))
        w = 640
        h = 480
        x = 0
        y = 0
        self.root.geometry("%dx%d+%d+%d" % (w, h, x, y))

        self.panel1 = tk.Label(self.root, image=self.image1)

        self.panel1.pack(side=tk.TOP, fill=tk.BOTH, expand=tk.YES)
        self.panel1.configure(image=self.image1)

        self.root.protocol("WM_DELETE_WINDOW", self.on_closing)

    def on_closing(self):
        if tkMessageBox.askokcancel("Quit", "Do you want to quit?"):
            self.endCommand()
            self.root.destroy()

    def processIncoming(self):
        """Handle all messages currently in the queue, if any."""
        while self.queue.qsize(  ):
            try:
                self.update_image(self.queue.get(0))
            except Queue.Empty:
                # just on general principles, although we don't
                # expect this branch to be taken in this case
                pass
    def update_image(self, newImage):
        self.image1=newImage
        self.panel1.configure(image=self.image1)

class ThreadedClient:
    def __init__(self, master):
        self.master = master
        self.queue = Queue.Queue(  )
        self.gui = GuiPart(master, self.queue, self.endApplication)

        # Set up the thread to do asynchronous I/O
        # More threads can also be created and used, if necessary
        self.running = 1
        self.thread1 = threading.Thread(target=self.workerThread1)
        self.thread1.start(  )

        # Start the periodic call in the GUI to check if the queue contains
        # anything
        self.periodicCall(  )

    def periodicCall(self):
        """
        Check every 200 ms if there is something new in the queue.
        """
        self.gui.processIncoming(  )
        if not self.running:
            # This is the brutal stop of the system. You may want to do
            # some cleanup before actually shutting it down.
            import sys
            sys.exit(1)
        self.master.after(200, self.periodicCall)

    def workerThread1(self):
        #set up server
        # Start a socket listening for connections on 0.0.0.0:8000 (0.0.0.0 means
        # all interfaces)
        self.server_socket = socket.socket()
        self.server_socket.bind(('0.0.0.0', 8000))
        self.server_socket.listen(0)

        # Accept a single connection and make a file-like object out of it
        self.connection = self.server_socket.accept()[0].makefile('rb')

        try:
            while self.running:
                # Read the length of the image as a 32-bit unsigned int. If the
                # length is zero, quit the loop
                image_len = struct.unpack('<L', self.connection.read(struct.calcsize('<L')))[0]
                if not image_len:
                    break
                # Construct a stream to hold the image data and read the image
                # data from the connection
                image_stream = io.BytesIO()
                image_stream.write(self.connection.read(image_len))

                image_stream.seek(0)
                
                newPhotoImage=ImageTk.PhotoImage(Image.open(image_stream))
                self.queue.put(newPhotoImage)
        finally:
            self.connection.close()
            self.server_socket.close()

    

    def endApplication(self):
        self.master.destroy()
        self.running = 0



root = tk.Tk(  )

client = ThreadedClient(root)
root.mainloop(  )


In client side, basically same as in Picamera tutorial: Basic Recipes - 4.9. Capturing to a network stream.

pyCamClient.py
#!/usr/bin/python

import io
import socket
import struct
import time
import picamera

# Connect a client socket to my_server:8000 (change my_server to the
# hostname of your server)
client_socket = socket.socket()
client_socket.connect(('192.168.1.111', 8000))

# Make a file-like object out of the connection
connection = client_socket.makefile('wb')
try:
    with picamera.PiCamera() as camera:
        camera.resolution = (640, 480)
        # Start a preview and let the camera warm up for 2 seconds
        camera.start_preview()
        time.sleep(2)

        # Note the start time and construct a stream to hold image data
        # temporarily (we could write it directly to connection but in this
        # case we want to find out the size of each capture first to keep
        # our protocol simple)
        start = time.time()
        stream = io.BytesIO()
        for foo in camera.capture_continuous(stream, 'jpeg'):
            # Write the length of the capture to the stream and flush to
            # ensure it actually gets sent
            connection.write(struct.pack('<L', stream.tell()))
            connection.flush()
            # Rewind the stream and send the image data over the wire
            stream.seek(0)
            connection.write(stream.read())
            # If we've been capturing for more than 30 seconds, quit
            if time.time() - start > 30:
                break
            # Reset the stream for the next capture
            stream.seek(0)
            stream.truncate()
    # Write a length of zero to the stream to signal we're done
    connection.write(struct.pack('<L', 0))
finally:
    connection.close()
    client_socket.close()