[Raspberry Pi 5] DepthAi와 PyQt5를 이용하여 RGB/Depth 이미지 영상표시 앱 만들기

2024. 3. 28. 20:20OAK-D Lite 활용 프로젝트

<완성된 애플리케이션 모습 스크린샷>

 

Luxonis사 제품 OAK-D Lite를 연동하여 사용자 선택에 따라 RGB 영상과 Depth 영상을 보여주는 앱을 PyQt5를 이용하여 제작하겠습니다.

 

DepthAi는 Luxnois사에서 판매하고 있는 제품들과의 통신을 위한 Spatial AI 플랫폼입니다. 이를 통해 영상 속의 대상 인식 및 추적, 영상판독 기능 등을 구현할 수 있습니다. Depth AI의 단순 데모 앱을 이용하는 것 이외에, SDK 또는 API를 활용하여 개발자 입맛에 맞는 앱을 구현할 수 있습니다. 공식 문서에 따르면, SDK보다 API를 활용하는 것이 Customizability 측면에서 유리하다고 합니다.

 

DepthAI’s Documentation — DepthAI documentation | Luxonis

 

DepthAI’s Documentation — DepthAI documentation | Luxonis

License Plates & Car Attributes Recognition This experiment allows you to run multiple neural networks at once to collect car attributes and license plates (only Chinese)

docs.luxonis.com

 

DepthAi API는 Python API와 C++ API를 모두 제공하지만 PyQt5를 활용하기 위해 Python API를 이용하여 앱을 제작하겠습니다. 최종적으로 앱이 구현될 환경은 2024년 3월 기준, Raspberry Pi 5에 최신 OS(bookwarm)을 설치한 상태입니다. 또한 추후 제품 개발에 이용될 목적이기 때문에 7'' Touchscreen을 연동하여 앱을 실행시킬 것입니다. 

 

<라즈베리파이 정보>

 

OAK-D Lite를 사용하기 위해 라이브러리를 설치해 주어야 합니다. 아래 링크를 참고하되 막히는 부분이 생길 것입니다. 이는 적절한 구글링을 통해 문제를 해결할 수 있습니다. (OpenSSL 관련 에러, EXTERNALLY-MANAGED 에러, opencv-python 관련 에러 등, 에러 해결과 관련된 링크는 게시글 하단에 있습니다.)

 

Integrated Computer Vision Package - OAK-D Lite With Raspberry Pi Set Up - Tutorial Australia (core-electronics.com.au)

 

Integrated Computer Vision Package - OAK-D Lite With Raspberry Pi Set Up - Tutorial Australia

If you ever needed a performance boost when running Machine Learnt AI Systems (like facial recognition) with a Raspberry Pi Single Board Computer then I have a solution for you. The OAK-D Lite. The Oak-D Lite is the Leatherman Multi-tool of the machine lea

core-electronics.com.au

 

PyQt5 또한 설치되어 있어야 합니다. 추가로, 실행파일을 만들기 위해 PyInstaller도 설치해 줍니다.

 

다음은 애플리케이션의 전체 코드입니다. 

import sys
import numpy as np
import cv2
from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel, QVBoxLayout, QWidget, QPushButton, QHBoxLayout
from PyQt5.QtGui import QImage, QPixmap
from PyQt5.QtCore import Qt, QTimer
import depthai as dai


class DepthAIManager:
    def __init__(self):
        self.init_depthai()
        
    def init_depthai(self):
        # depth 영상을 조작하는 환경설정 변수
        extended_disparity = False
        subpixel = False
        lr_check = True
        
        # Pipeline 생성
        pipeline = dai.Pipeline()
        
        # 영상 소스와 출력용 Node 생성
        camRgb = pipeline.create(dai.node.ColorCamera)
        xoutRgb = pipeline.create(dai.node.XLinkOut)
        xoutRgb.setStreamName("rgb")
        
        monoLeft = pipeline.create(dai.node.MonoCamera)
        monoRight = pipeline.create(dai.node.MonoCamera)
        self.depth = pipeline.create(dai.node.StereoDepth)
        xoutDepth = pipeline.create(dai.node.XLinkOut)
        xoutDepth.setStreamName("disparity")
        
        # 속성 설정하기
        camRgb.setPreviewSize(700, 400)
        camRgb.setInterleaved(False)
        camRgb.setColorOrder(dai.ColorCameraProperties.ColorOrder.RGB)
        monoLeft.setResolution(dai.MonoCameraProperties.SensorResolution.THE_400_P)
        monoLeft.setCamera("left")
        monoRight.setResolution(dai.MonoCameraProperties.SensorResolution.THE_400_P)
        monoRight.setCamera("right")
        
        # Depth 영상 환경설정
        self.depth.setDefaultProfilePreset(dai.node.StereoDepth.PresetMode.HIGH_DENSITY)
        self.depth.initialConfig.setMedianFilter(dai.MedianFilter.KERNEL_7x7)
        self.depth.setLeftRightCheck(lr_check)
        self.depth.setExtendedDisparity(extended_disparity)
        self.depth.setSubpixel(subpixel)
        
        # Linking
        camRgb.preview.link(xoutRgb.input)
        monoLeft.out.link(self.depth.left)
        monoRight.out.link(self.depth.right)
        self.depth.disparity.link(xoutDepth.input)
        
        # 장치와 pipeline 연결
        self.device = dai.Device(pipeline)
        
    def get_rgb_frame(self):
        qRgb = self.device.getOutputQueue(name="rgb", maxSize=4, blocking=False)
        inRgb = qRgb.get()

        # BGR to RGB        
        frame = inRgb.getCvFrame()
        frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        return frame_rgb
        

    def get_depth_frame(self):
        qDepth = self.device.getOutputQueue(name="disparity", maxSize=4, blocking=False)
        inDisparity = qDepth.get()
        frame = inDisparity.getFrame()
        
        # 시각화 개선처리
        frame = (frame * (255 / self.depth.initialConfig.getMaxDisparity())).astype(np.uint8)
        frame = cv2.applyColorMap(frame, cv2.COLORMAP_JET)
        frame_depth = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        return frame_depth
        
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("MYTESTAPP")
        self.setWindowState(Qt.WindowFullScreen)
        
        # DepthAIManger 초기화
        self.depthai_manager = DepthAIManager()
        
        # GUI setup
        self.setup_ui()
        
        # 프레임 업데이트를 위한 타이머 설정
        self.timerRGB = QTimer()
        self.timerRGB.timeout.connect(self.update_RGBFrame)
        self.timerDepth = QTimer()
        self.timerDepth.timeout.connect(self.update_DepthFrame)
        
    def setup_ui(self):
        central_widget = QWidget()
        self.setCentralWidget(central_widget)

        layout = QVBoxLayout()
        central_widget.setLayout(layout)

        self.label = QLabel()
        self.label.setAlignment(Qt.AlignCenter)
        layout.addWidget(self.label)

        buttonRGB = QPushButton("RGB")
        buttonDepth = QPushButton("Depth")
        buttonRGB.clicked.connect(self.show_RGBImage)
        buttonDepth.clicked.connect(self.show_DepthImage)

        hbox = QHBoxLayout()
        hbox.addWidget(buttonRGB)
        hbox.addWidget(buttonDepth)
        layout.addLayout(hbox)
        
    def update_RGBFrame(self):
        rgb_frame = self.depthai_manager.get_rgb_frame()
        self.display_frame(rgb_frame)
    
    def update_DepthFrame(self):
        depth_frame = self.depthai_manager.get_depth_frame()
        self.display_frame(depth_frame)
        
    def display_frame(self, frame):
        pixmap = self.convert_frame_to_pixmap(frame)
        self.label.setPixmap(pixmap)
        
    def convert_frame_to_pixmap(self, frame):
        height, width, channel = frame.shape
        bytesPerLine = 3 * width
        qImg = QImage(frame.data, width, height, bytesPerLine, QImage.Format_RGB888)
        pixmap = QPixmap.fromImage(qImg)
        return pixmap
    
    def show_RGBImage(self):
        self.timerDepth.stop()
        self.timerRGB.start()
        
    def show_DepthImage(self):
        self.timerRGB.stop()
        self.timerDepth.start()
        
    def closeEvent(self, event):
        self.timerRGB.stop()
        self.timerDepth.stop()
        self.device.close()
        event.accept()
        
if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec_())

 

먼저 앱을 실행하는 데 필요한 모듈을 살펴보겠습니다.

import sys
import numpy as np
import cv2
from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel, QVBoxLayout, QWidget, QPushButton, QHBoxLayout
from PyQt5.QtGui import QImage, QPixmap
from PyQt5.QtCore import Qt, QTimer
import depthai as dai

 

OpenCV 함수 기능을 사용하기 위해 cv2 모듈을 추가했습니다. 그리고 PyQt5.모듈 이하 클래스들을 추가했습니다. 이어서 DepthAi 하드웨어와 그 기능을 활용하기 위해 depthai 모듈을 추가했습니다.

 

코드 설명을 이어가기에 앞서 공식 문서의 DepthAI API Documentation 섹션의 이미지를 살펴보겠습니다. 

 

<DepthAI API 작동원리 도식화>

 

Device 쪽에서 Pipeline을 생성하고 그 내부에 Node를 생성하여 서로 연결할 수 있습니다. 이때 노드 간에 Message를 통해 데이터를 주고받습니다. 영상을 사용할 주체, Host는 XLinkOut을 연결하여 영상정보를 얻을 수 있습니다. 반대로 XLinkIn을 연결하여 Host에서 Device로 데이터를 전달할 수 있습니다. 이에 본 애플리케이션에서는 카메라로부터 획득한 영상을 다루는 기능을 가진 DepthAIManager 클래스를 만들어 관리했습니다. 

 

class DepthAIManager:
    def __init__(self):
        self.init_depthai()
        
    def init_depthai(self):
        # depth 영상을 조작하는 환경설정 변수
        extended_disparity = False
        subpixel = False
        lr_check = True
        
        # Pipeline 생성
        pipeline = dai.Pipeline()
        
        # 영상 소스와 출력용 Node 생성
        camRgb = pipeline.create(dai.node.ColorCamera)
        xoutRgb = pipeline.create(dai.node.XLinkOut)
        xoutRgb.setStreamName("rgb")
        
        monoLeft = pipeline.create(dai.node.MonoCamera)
        monoRight = pipeline.create(dai.node.MonoCamera)
        self.depth = pipeline.create(dai.node.StereoDepth)
        xoutDepth = pipeline.create(dai.node.XLinkOut)
        xoutDepth.setStreamName("disparity")
        
        # 속성 설정하기
        camRgb.setPreviewSize(700, 400)
        camRgb.setInterleaved(False)
        camRgb.setColorOrder(dai.ColorCameraProperties.ColorOrder.RGB)
        monoLeft.setResolution(dai.MonoCameraProperties.SensorResolution.THE_400_P)
        monoLeft.setCamera("left")
        monoRight.setResolution(dai.MonoCameraProperties.SensorResolution.THE_400_P)
        monoRight.setCamera("right")
        
        # Depth 영상 환경설정
        self.depth.setDefaultProfilePreset(dai.node.StereoDepth.PresetMode.HIGH_DENSITY)
        self.depth.initialConfig.setMedianFilter(dai.MedianFilter.KERNEL_7x7)
        self.depth.setLeftRightCheck(lr_check)
        self.depth.setExtendedDisparity(extended_disparity)
        self.depth.setSubpixel(subpixel)
        
        # Linking
        camRgb.preview.link(xoutRgb.input)
        monoLeft.out.link(self.depth.left)
        monoRight.out.link(self.depth.right)
        self.depth.disparity.link(xoutDepth.input)
        
        # 장치와 pipeline 연결
        self.device = dai.Device(pipeline)
        
    def get_rgb_frame(self):
        qRgb = self.device.getOutputQueue(name="rgb", maxSize=4, blocking=False)
        inRgb = qRgb.get()

        # BGR to RGB        
        frame = inRgb.getCvFrame()
        frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        return frame_rgb
        

    def get_depth_frame(self):
        qDepth = self.device.getOutputQueue(name="disparity", maxSize=4, blocking=False)
        inDisparity = qDepth.get()
        frame = inDisparity.getFrame()
        
        # 시각화 개선처리
        frame = (frame * (255 / self.depth.initialConfig.getMaxDisparity())).astype(np.uint8)
        frame = cv2.applyColorMap(frame, cv2.COLORMAP_JET)
        frame_depth = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        return frame_depth

 

init_depthai 함수를 순차적으로 살펴보겠습니다. 

def init_depthai(self):
        # depth 영상을 조작하는 환경설정 변수
        extended_disparity = False
        subpixel = False
        lr_check = True
        
        # Pipeline 생성
        pipeline = dai.Pipeline()
        
        # 영상 소스와 출력용 Node 생성
        camRgb = pipeline.create(dai.node.ColorCamera)
        xoutRgb = pipeline.create(dai.node.XLinkOut)
        xoutRgb.setStreamName("rgb")
        
        monoLeft = pipeline.create(dai.node.MonoCamera)
        monoRight = pipeline.create(dai.node.MonoCamera)
        self.depth = pipeline.create(dai.node.StereoDepth)
        xoutDepth = pipeline.create(dai.node.XLinkOut)
        xoutDepth.setStreamName("disparity")

 

extended_disparity, subpixel, lr_check의 부울 변수는 모두 depth 영상과 관련된 환경설정 변수로서 카메라로 부터 물체까지의 거리 및 그에 따른 화질과 관련된 속성입니다. 이어 Pipeline을 생성하고 RGB 영상, Depth 영상용 Node를 만들었습니다. XLinkOut Node에는 각각 이름을 붙여 이후에 접근할 수 있도록 설정했습니다.

 

        # 속성 설정하기
        camRgb.setPreviewSize(700, 400)
        camRgb.setInterleaved(False)
        camRgb.setColorOrder(dai.ColorCameraProperties.ColorOrder.RGB)
        monoLeft.setResolution(dai.MonoCameraProperties.SensorResolution.THE_400_P)
        monoLeft.setCamera("left")
        monoRight.setResolution(dai.MonoCameraProperties.SensorResolution.THE_400_P)
        monoRight.setCamera("right")
        
        # Depth 영상 환경설정
        self.depth.setDefaultProfilePreset(dai.node.StereoDepth.PresetMode.HIGH_DENSITY)
        self.depth.initialConfig.setMedianFilter(dai.MedianFilter.KERNEL_7x7)
        self.depth.setLeftRightCheck(lr_check)
        self.depth.setExtendedDisparity(extended_disparity)
        self.depth.setSubpixel(subpixel)
        
        # Linking
        camRgb.preview.link(xoutRgb.input)
        monoLeft.out.link(self.depth.left)
        monoRight.out.link(self.depth.right)
        self.depth.disparity.link(xoutDepth.input)
        
        # 장치와 pipeline 연결
        self.device = dai.Device(pipeline)

 

다음은 각 카메라 변수에 영상 속성을 설정합니다. 7인치 디스플레이에 알맞은 적당한 크기, 700x400의 사이즈로 RGB 영상을 설정했으며 depth 영상은 적당한 출력을 위한 값을 설정했습니다. 이어서 카메라 소스 Node와 XLinkOut Node를 연결하고 마지막엔 완성된 Pipeline을 장치와 연동시켰습니다.

 

다음은 XOutLink Node로부터 Queue를 얻어서 이로부터 이미지 정보를 RGB 형태로 저장하고 있는 frame을 추출하겠습니다. 

 

    def get_rgb_frame(self):
        qRgb = self.device.getOutputQueue(name="rgb", maxSize=4, blocking=False)
        inRgb = qRgb.get()

        # BGR to RGB        
        frame = inRgb.getCvFrame()
        frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        return frame_rgb
        

    def get_depth_frame(self):
        qDepth = self.device.getOutputQueue(name="disparity", maxSize=4, blocking=False)
        inDisparity = qDepth.get()
        frame = inDisparity.getFrame()
        
        # 시각화 개선처리
        frame = (frame * (255 / self.depth.initialConfig.getMaxDisparity())).astype(np.uint8)
        frame = cv2.applyColorMap(frame, cv2.COLORMAP_JET)
        frame_depth = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        return frame_depth

 

Device를 초기화하고 나면 getOutputQueue()를 통해 출력 Queue를 얻을 수 있습니다. 이때 생성된 Queue는 Host의 컴퓨터, RAM에 위치하게 됩니다. qRgb.get()을 통해 Queue에서 데이터를 읽습니다. get() 함수는 메세지가 도착할 때까지 blocked 된 상태입니다. 그렇게 메세지가 도착하고 데이터를 읽어 들이게 되면 OpenCV 형식의 frame으로 변환하고 후처리를 위해 영상을 RGB 형식으로 변환한 뒤 frame을 반환해 줍니다. RGB frame과 달리 depth frame의 경우 depth 정보를 담고 있는 영상의 시각화를 위해 추가적인 과정을 거쳐 반환해야 합니다. 

 

장치와의 연동 및 일부 기능추가는 마무리했으니 실질적인 애플리케이션 작성에 관한 내용을 설명하겠습니다. MainWindow 클래스 초기화 부분을 살펴보겠습니다.

 

    def __init__(self):
        super().__init__()
        self.setWindowTitle("MYTESTAPP")
        self.setWindowState(Qt.WindowFullScreen)
        
        # DepthAIManger 초기화
        self.depthai_manager = DepthAIManager()
        
        # GUI setup
        self.setup_ui()
        
        # 프레임 업데이트를 위한 타이머 설정
        self.timerRGB = QTimer()
        self.timerRGB.timeout.connect(self.update_RGBFrame)
        self.timerDepth = QTimer()
        self.timerDepth.timeout.connect(self.update_DepthFrame)

 

DepthAIManager를 생성한 뒤 UI를 구성했습니다. 이어서 Queue에서 반환된 frame을 실시간 업데이트 하기 위해 타이머를 설정했습니다. setup_ui 함수를 살펴보겠습니다.

 

    def setup_ui(self):
        central_widget = QWidget()
        self.setCentralWidget(central_widget)

        layout = QVBoxLayout()
        central_widget.setLayout(layout)

        self.label = QLabel()
        self.label.setAlignment(Qt.AlignCenter)
        layout.addWidget(self.label)

        buttonRGB = QPushButton("RGB")
        buttonDepth = QPushButton("Depth")
        buttonRGB.clicked.connect(self.show_RGBImage)
        buttonDepth.clicked.connect(self.show_DepthImage)

        hbox = QHBoxLayout()
        hbox.addWidget(buttonRGB)
        hbox.addWidget(buttonDepth)
        layout.addLayout(hbox)

 

실시간 영상이 표시될 QLabel을 배치하고 그 밑을 좌우로 양분하여 버튼 두 개를 배치했습니다. 각 버튼에는 사용자 선택에 따라 알맞은 영상을 표시하는 함수를 연결했습니다. 다음은 Mainwindow 클래스에 정의된 영상 출력과 관련된 함수들입니다. 

 

    def update_RGBFrame(self):
        rgb_frame = self.depthai_manager.get_rgb_frame()
        self.display_frame(rgb_frame)
    
    def update_DepthFrame(self):
        depth_frame = self.depthai_manager.get_depth_frame()
        self.display_frame(depth_frame)
        
    def display_frame(self, frame):
        pixmap = self.convert_frame_to_pixmap(frame)
        self.label.setPixmap(pixmap)
        
    def convert_frame_to_pixmap(self, frame):
        height, width, channel = frame.shape
        bytesPerLine = 3 * width
        qImg = QImage(frame.data, width, height, bytesPerLine, QImage.Format_RGB888)
        pixmap = QPixmap.fromImage(qImg)
        return pixmap
    
    def show_RGBImage(self):
        self.timerDepth.stop()
        self.timerRGB.start()
        
    def show_DepthImage(self):
        self.timerRGB.stop()
        self.timerDepth.start()
        
    def closeEvent(self, event):
        self.timerRGB.stop()
        self.timerDepth.stop()
        self.device.close()
        event.accept()

 

반환된 프레임을 QImage 형식으로 변환하여 적절한 포맷으로 변환한 후 마지막엔 QPixmap형태로 반환합니다. 이어 각 버튼에 알맞은 타이머를 실행하는 함수를 작성했습니다. 

 

이로써 스크립트 실행을 위한 준비가 모두 끝났습니다. 배포 및 소스코드 보안을 위해 PyInstaller로 실행파일을 만들어 실행해 주면 됩니다. 

 

<Raspberry Pi 5의 7인치 디스플레이와 OAK-D Lite가 연동되어 작동하고 있는 모습>

 

 

=============에러 대처 관련 링크 ============

A. Open SSL 관련에러

https://www.kimnjang.com/121

 

라즈베리파이4 + V4L2RTSPServer 설치 방법 정리(with EASYCAP)

라즈베리파이4에 V4L2RTSPSERVER 설치방법을 간단히 정리하였다. 또한 V4L2에서 사용하는 카메라는 라즈베리파이 카메라가 아닌 EASYCAP 기준으로 라즈베리파이 카메라 사용시 일부 추가되야될 부분이

www.kimnjang.com

 

B. EXTERNALLY-MANAGED 에러

https://velog.io/@dongju101/error-externally-managed-environment-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0

 

error: externally-managed-environment 문제 해결

다음 명령어 입력시 해결 가능

velog.io

 

C. opencv-python 관련 에러(재설치)

https://stackoverflow.com/questions/46449850/how-to-fix-the-error-qobjectmovetothread-in-opencv-in-python

 

How to fix the error "QObject::moveToThread:" in opencv in python?

I am using opencv2 in python with the code import cv2 cv2.namedWindow("output", cv2.WINDOW_NORMAL) cv2.imshow("output",im) cv2.resizeWindow('output', 400,400) cv2.waitKey(0) cv2.destroyAllW...

stackoverflow.com