졸업 캡스톤 프로젝트에서 사용해보기 위해 라즈베리파이 4B 모델을 구하게 되었다.
그리고 이 라즈베리파이에 카메라 모듈도 연결해 영상 스트림을 생성해야했었다.
이번에는 라즈베리파이의 초기 설정과 카메라 모듈까지 연결하는 과정을 알아보도록 한다.
라즈베리파이 초기 설정
먼저 제품 박스를 열면 손바닥 크기도 채 안되는 사이즈의 메인보드 기판이 나오게 된다.
번외로 라즈베리파이 4B의 단자 구성은 아래와 같다.
- USB 2.0 포트 X 2
- USB 3.0 포트 X 2
- 기가비트 이더넷 포트
- micro HDMI 포트 X 2
- USB-C 파워 포트
먼저 파이에 들어갈 OS를 설치해주어야 한다.
기본적으로 파이는 SD카드를 스토리지로 삼는다. SD카드 슬롯은 좌측 (USB 포트 반대편) 하단에 위치한다.
라즈베리파이 OS Imager 설치 페이지에서 Imager를 다운받는다.
다음으로 SD카드를 리더기 등을 통해 PC에 연결한 뒤, Imager에서 OS 설치 디바이스와 라즈베리파이 OS, 설치할 저장소 위치를 지정해준다.
이어서 OS 커스텀 설정 구간에서는 사용자 / WIFI / 지역 설정과 더불어 SSH 설정을 진행할 수 있다.
SD카드에 OS를 심은 뒤 파이에 장착 후, 전원 케이블 연결 시키면 LED에 불이 들어오며 파이에 전원이 켜지게 된다.
Micro HDMI 케이블이나 젠더가 있으면 디스플레이와 연결해 화면을 볼 수 있으나, Micro HDMI 케이블이나 젠더는 찾기 어려웠다. (다이소에도 잘 없는듯..)
그래서 공유기에 랜포트를 통해서 물린 뒤에 SSH로 접근했다.
파이에 연결되면 초기 업데이트를 진행해준다.
apt-get update -y && apt-get upgrade -y
카메라 모듈 설정
다음으로 파이에 카메라 모듈을 달기 위해 파이의 전원을 꺼준다. 파이 자체적으로 전원버튼이 따로 없어서 커맨드로 종료시켜준다.
shutdown now
파이의 중앙 하단부(micro HDMI 오른쪽)를 보면 'camera'라고 적힌 슬롯이 하나 보인다.
이 슬롯의 검정 브라켓을 양쪽을 잡고 들어올린뒤에 카메라 모듈 케이블을 넣고 브라켓을 내려서 고정시켜준다.
라즈베리파이 공식 페이지에 연결방법이 상세하게 소개되어있다.
카메라 모듈을 연결하고 파이 전원을 켜준다.
다음으로 실제 모듈이 정상적으로 연결되었는지 확인하기 위해 카메라 스트림을 받아보도록한다.
VNC를 통해 파이에 접속한 뒤, Bullseye 버전에 기본적으로 설치된 libcamera 모듈을 이용해 카메라 동작을 확인할 수 있다.
아래 커맨드로 카메라 스트림을 짧게 5초간 확인할 수 있다.
libcamera-hello
이 캡스톤 프로젝트에서 파이가 사용되는 부분은 카메라 스트림을 받아서 모델(YOLO, BLIP) 서버로 그대로 쏴주는 곳이였다. 따라서 카메라 스트림을 파이에서 스트리밍해줄 수 있는 수단이 필요했는데, 알아본 결과 gstreamer, mjpg-streamer를 먼저 찾을 수 있었다.
다만, mjpg-streamer의 경우 최신 파이 OS에서는 사용할 수 없다고 한다. 실제로 mjpg-streamer 레포를 받아서 빌드해본 결과 input_raspicam.so 파일이 빌드되지 않았다.
그 뒤로 찾아본 것이 PiCamera라는 Python 모듈이 있다. 이 모듈도 OS에 따라 PiCamera와 PiCamera2가 따로 존재하는데, 공식 Repo에 따르면 2022년 9월 이후의 OS 이미지에는 Pre-install되어있다고 한다. 따라서 글이 작성되는 시점에서는 PiCamera2를 사용하는 것이 맞다.
공식 Repo에서 제공해주는 mjpg 스트리밍 서버 구축 코드를 이용해 카메라 스트림을 mjpg 형태로 스트리밍할 수 있다.
#!/usr/bin/python3
# Mostly copied from https://picamera.readthedocs.io/en/release-1.13/recipes2.html
# Run this script, then point a web browser at http:<this-ip-address>:8000
# Note: needs simplejpeg to be installed (pip3 install simplejpeg).
import io
import logging
import socketserver
from http import server
from threading import Condition
from picamera2 import Picamera2
from picamera2.encoders import JpegEncoder
from picamera2.outputs import FileOutput
PAGE = """\
<html>
<head>
<title>picamera2 MJPEG streaming demo</title>
</head>
<body>
<h1>Picamera2 MJPEG Streaming Demo</h1>
<img src="stream.mjpg" width="640" height="480" />
</body>
</html>
"""
class StreamingOutput(io.BufferedIOBase):
def __init__(self):
self.frame = None
self.condition = Condition()
def write(self, buf):
with self.condition:
self.frame = buf
self.condition.notify_all()
class StreamingHandler(server.BaseHTTPRequestHandler):
def do_GET(self):
if self.path == '/':
self.send_response(301)
self.send_header('Location', '/index.html')
self.end_headers()
elif self.path == '/index.html':
content = PAGE.encode('utf-8')
self.send_response(200)
self.send_header('Content-Type', 'text/html')
self.send_header('Content-Length', len(content))
self.end_headers()
self.wfile.write(content)
elif self.path == '/stream.mjpg':
self.send_response(200)
self.send_header('Age', 0)
self.send_header('Cache-Control', 'no-cache, private')
self.send_header('Pragma', 'no-cache')
self.send_header('Content-Type', 'multipart/x-mixed-replace; boundary=FRAME')
self.end_headers()
try:
while True:
with output.condition:
output.condition.wait()
frame = output.frame
self.wfile.write(b'--FRAME\r\n')
self.send_header('Content-Type', 'image/jpeg')
self.send_header('Content-Length', len(frame))
self.end_headers()
self.wfile.write(frame)
self.wfile.write(b'\r\n')
except Exception as e:
logging.warning(
'Removed streaming client %s: %s',
self.client_address, str(e))
else:
self.send_error(404)
self.end_headers()
class StreamingServer(socketserver.ThreadingMixIn, server.HTTPServer):
allow_reuse_address = True
daemon_threads = True
picam2 = Picamera2()
picam2.configure(picam2.create_video_configuration(main={"size": (640, 480)}))
output = StreamingOutput()
picam2.start_recording(JpegEncoder(), FileOutput(output))
try:
address = ('', 8000)
server = StreamingServer(address, StreamingHandler)
server.serve_forever()
finally:
picam2.stop_recording()
일단 이렇게 라즈베리파이에 카메라 모듈을 연결하고 mjpg 스트리밍하는 과정을 알아보았다. 이 카메라 스트림 주소를 opencv를 사용하는 모델서버에서 받아올 수 있다. 실제 이 라즈베리파이를 전시장에 가져가서 모델서버로 스트림을 쏴주는 것까지에는 네트워크적인 작업이 더 필요하게 될 것이다. (터널링을 뚫어준다거나 등)