이더넷을 이용하여 라즈베리 파이에서 PC로 사진 전송하는 법 (How to send images captured by PiCamera to my PC using Ethernet)


(본문에서 가끔 라즈베리파이를 줄여서 RPi로 칭하기로 함)




이번에는 라즈베리파이 -> PC 로 사진전송하는 법을 알아보겠습니다.

원래는 네이버 클라우드 플랫폼을 이용하여 클라우드 서버를 구축한 뒤 그쪽으로

사진을 전송하려 하였으나 서버에 따로 파이썬, opencv 등 기타 설치해야 할게 너무 많은데 

시간은 없고 해서 일단은 필요한 프로그램들이 이미 설치된 노트북으로 사진을 전송해 보겠습니다.




제 노트북과 라즈베리 파이는 위 사진에서처럼 이더넷 케이블을 이용하여 연결된 상태이고

집에 있는 공유기를 이용하여 인터넷에 연결시켜 놓은 상태입니다.

(이 때, RPi는 노트북이 잡고있는 인터넷을 이더넷을 통해 공유하는 방식으로 인터넷에 연결되게 됩니다)




RPi에서 PC로 사진을 전송하는 단계에 들어선 분들은 이미 RPi 및 PC에 기본 셋팅을 완료했을 거란 가정하에

포스팅 하도록 하겠습니다.




(Source : http://picamera.readthedocs.io/en/release-1.9/recipes1.html#capturing-to-a-network-stream)




위의 picamera document를 참고하면

두 개의 scripts가 필요합니다.

(Script란 ? 소프트웨어에 실행시키는 처리 절차를 문자(텍스트)로 기술한 것. 일종의 프로그램이라고 할 수 있다.)


server script : 라즈베리 파이로부터 연결을 수신하는 측. (여기서는 PC가 됨)

client script : 라즈베리 파이 위에서 동작하며, 서버로 연속적인 이미지 스트림을 보내는 측.


통신을 위해 매우 단순한 프로토콜을 사용합니다.

처음에 이미지 길이를 32비트 integer(Little Endian 형식으로)로 보내며, 

이후 image data bytes들이 보내집니다.

다음 그림은 우리가 사용할 프로토콜에 대한 구조도 입니다.

 


이미지 길이가 0이라는 것은 더이상 보내질 이미지가 없다는 의미로써 연결이 닫히게 됩니다.




1. Server Script


본격적으로 서버단을 먼저 살펴보겠습니다.

서버측에서는 JPEG 형식의 이미지를 reading하기 위해 PIL을 사용합니다.

PIL말고도 OpenCV나 GraphicsMagick와 같은 다른 그래픽 라이브러리들을 이용할 수도 있다고 합니다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import io
import socket
import struct
from PIL import Image
 
# Start a socket listening for connections on 0.0.0.0:8000 (0.0.0.0 means
# all interfaces)
server_socket = socket.socket()
server_socket.bind(('0.0.0.0'8000))
server_socket.listen(0)
 
# Accept a single connection and make a file-like object out of it
connection = server_socket.accept()[0].makefile('rb')
try:
    while True:
        # 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', 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(connection.read(image_len))
        # Rewind the stream, open it as an image with PIL and do some
        # processing on it
        image_stream.seek(0)
        image = Image.open(image_stream)
        print('Image is %dx%d' % image.size)
        image.verify()
        print('Image is verified')
finally:
    connection.close()
    server_socket.close()
cs


위 코드를 100% 이해한 것은 아니나 아는 선에서 코드를 살펴보도록 하겠습니다.



PC와 RPi가 통신하도록 하기 위해 각각 socket이 필요하며 socket에 대한 설명은

아주 잘 정리된 글이 있어 그것으로 대체 하겠습니다.

socket 설명


server_socket = socket.socket()


먼저 Server단에서 stream을 받기 위한 소켓 객체를 생성합니다.


server_socket.bind(('0.0.0.0'8000))


서버가 특정한 포트를 열고 입력을 기다리기 위해서 소켓을 포트에 바인드하는 과정이다.

bind() 호출시에는 호스트이름과 포트번호가 필요한데, 여기서 0.0.0.0 은 모든 인터페이스를 의미하고

8000은 포트 번호이다. 


server_socket.listen(0)


 바인드가 완료되면 포트를 듣는다. 이 메소드가 호출되면 클라이언트가 해당 포트에 접속하는 것을 기다린다.

접속한 상대방이 내가 원하는 client인지 다른 요청인지는 알 수 없고 그저 듣기만 한다. (접속이 들어오면 리턴된다.)


connection = server_socket.accept()[0].makefile('rb')


여기서 실제로 접속을 수락하게 되는데 listen()으로 접속 시도를 알아차린 뒤

서버측에서 그 요청을 받아서 접속을 시작한다. 접속의 개시는 accept()를 사용한다.

accept()는 (소켓, 주소정보)로 구성되는 튜플을 리턴한다. 

makefile('rb')은 소켓으로 들어오는 file object를 return한다.

(이 부분은 나도 잘 모르겠다)

서버는 최초 생성되어 듣는 소켓이 아닌 accept()의 리턴으로 제공되는 소켓을 사용해서 클라이언트와 정보를 주고 받는다.



image_len = struct.unpack('<L', connection.read(struct.calcsize('<L')))[0]


위의 '<L'에서 '<'은 Little-Endian을 'L'은 unsigned long을 의미한다. 자세한 사항은 아래 참조

https://docs.python.org/3/library/struct.html


즉, struct.unpack()은 connection.read(struct.calcsize('<L')) 의 값을 

little-endian으로 구성 된 unsigned long으로 unpack하겠다는 의미. 


calcsize()는 다음 설명으로 대체.

Return the size of the struct (and hence of the bytes object produced by pack(fmt, ...)) corresponding to the format string fmt.


====================

struct.unpack(fmt, buffer)

struct.calcsize(fmt)

====================


if not image_len:

    break


이후 image length를 읽다가 length가 0이면 loop를 빠져나온다.




image_stream = io.BytesIO()


image length가 0이 아니라면 image data를 보관하기위한 stream을 만든 뒤



image_stream.write(connection.read(image_len))


connection으로 부터 image data를 읽는다.


image_stream.seek(0)


이후 image stream을 되감는다. (아마 stream의 처음으로 돌아가려고 하는듯)


image = Image.open(image_stream)


image stream을 image with PIL로 연다.


위 코드 다음에 image.show() 명령어를 추가하시면 image stream을 직접 확인가능합니다.




참고자료

https://soooprmx.com/archives/8737





2. Client Script




1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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(('my_server'8000))
 
# Make a file-like object out of the connection
connection = client_socket.makefile('wb')
try:
    with picamera.PiCamera() as camera:
        camera.resolution = (640480)
        # 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()
cs


첫부분은 클라이언트 소켓을 서버로 연결하는 것부터 시작하는데

여기서 주의할 점은


client_socket.connect(('my_server'8000))


위 코드에서 my_server을 이용하고자 하는 서버의 hostname으로 바꿔줘야 하는 것이다.


나의 경우 PC의 이더넷 주소는 192.168.137.2 이고

RPi의 이더넷 주소는 192.168.137.3 인데


PC를 서버로 사용하기로 했으므로 PC의 이더넷 주소인 192.168.137.2를 입력해주었다.

그리고 포트번호는 그대로 8000번을 사용한다.




 with picamera.PiCamera() as camera:


PiCamera를 camera란 이름으로 사용한다.


camera.resolution = (640480)


해상도를 위와 같이 설정한다.


camera.start_preview()


time.sleep(2)


camera preview를 시작하고 2초간 warm up 시킨다고 한다.


start = time.time()


stream = io.BytesIO()


시작 시간을 기록하고, 일시적으로 image data를 보관하기 위한 stream을 생성한다.


(image data를 connection에 직접 write 해도 되지만 여기서는 우리가 사용할 protocol을 단순하게 유지하기 위해서

먼저 각 캡처의 사이즈를 알아내기 위해 stream을 만든다.)



for foo in camera.capture_continuous(stream, 'jpeg'):


capture_continuous()는 infinite iterator로서 카메라로부터 이미지를 계속 capture한다고 함.


JPEG frame을 가능한 빨리 in-memory stream으로 capture하기 위해 위와 같이 사용.


(자세한 내용은 다음 document에서 "continuous"를 검색하여 참조할 것.

http://picamera.readthedocs.io/en/release-1.10/api_camera.html)



 connection.write(struct.pack('<L', stream.tell()))


capture의 길이를 위에서 만든 stream에 write하여 보내고


 connection.flush()


capture 길이가 실제로 보내졌는지 확인하기 위해 flush한다.

(flush : 기억장치 부분의 내용을 비우는 것.)



stream.seek(0)


이후 ServerScript에서 했던 것처럼 stream을 되감고


connection.write(stream.read())


image data를 보낸다.



if time.time() - start > 30:

    break


이미지를 캡쳐한지 30초가 지났다면, loop를 빠져나간다.


stream.seek(0)

stream.truncate()


다음 capture를 위해 stream을 reset한다.(stream을 되감고 길이를 줄인다)


connection.write(struct.pack('<L'0))


더이상 보낼 capture가 없을 때 끝났다는 의미에서 stream에 0을 써 보낸다.



=================================================================



위 과정을 끝내고 각각 script를 실행시키면 RPi에서 찍은 이미지를 서버에서 바로 확인 가능하다.



주의할 점이 있다면 client script를 실행시키기 전에 client script로부터 연결을 accept할 준비가 된

server단의 listening socket이 있다는 것을 보장하기 위해 server script를 먼저 실행해야 한다는 것이다.



궁금한 점이나 어색한 부분 또는 잘못된 부분은 언제든지 댓글 달아주세요 :)



더보기

댓글,

jayharvey

머신러닝/딥러닝 관련 글을 포스팅할 예정입니다 :)