앞선 글에서 다뤘던 모니터링 웹앱에 객체 탐지(object detection) 기능을 간단히 추가해 봤습니다.
객체 탐지는 이미지내에서 객체들을 찾는 것입니다. 예를 들어 이미지내에 TV라는 객체가 있다면 해당 객체를 TV라고 찾는 것입니다. 이를 처음부터 구현하려면 이미지 처리부터 머신러닝까지 다양하고 복잡한 단계가 필요하지만, OpenCV와 TensorFlow의 Object Detection API 덕분에 쉽게 구현해 볼 수 있습니다.
Tensorflow Object Detection API
사용할 객체 탐지 API는 다음과 같습니다.
- Tensorflow Object Detection API: https://github.com/tensorflow/models/tree/master/research/object_detection
이 API는 지원 중단(deprecation) 되기는 했지만, 간단히 사용해 보기에는 여전히 충분한 기능을 발휘합니다.
이 API의 머신 러닝 모델(모델의 네트워크)을 라즈베리파이에서 사용하는 방법은 2가지가 있습니다.
- Tensorflow를 사용하는 방법
- OpenCV를 사용하는 방법
※ 아래쪽에 참고 부분에도 나오지만 이 방법들은 https://github.com/opencv/opencv/wiki/TensorFlow-Object-Detection-API 에 상세히 설명돼 있습니다.
첫번째 방법은 라즈베리파이에서 돌려보면 하드웨어 성능의 한계로 매우 느려서 사용하기가 어렵습니다. 고맙게도 OpenCV가 Tensorflow 기반 머신 러닝 모델의 네트워크를 읽어 들이고 사용할 수 있는 기능을 제공하여 두 번째 방법을 사용하는 경우 라즈베리파이 4 이상에서는 실시간으로 사용할 만큼의 빠른 속도를 보여 줍니다. 두 번째 방법으로도 라즈베리파이 3에서는 (못 쓸 정도는 아니지만) 실시간으로 사용할 만큼 빠르게 동작하지는 않습니다.
OpenCV를 사용한 방법
OpenCV에서 Tensorflow의 모델 파일을 읽어 들여 사용하기 위해서는 Tensforflow의 모델(및 가중치) 파일인 pb 파일과 모델 네트워크를 기술하는 텍스트 파일인 pbtxt 파일이 필요합니다. pb 파일을 기반으로 pbtx 파일을 생성하는 방법은 여기에서 설명해 주고 있지만 좀 번거롭습니다. 그런데, 고맙게도 OpenCV의 Tensorflow Object Detection API 설명 페이지에서는 pb 파일과 pbtxt 파일을 모두 제공해 주고 있습니다.
모델 선택
먼저 https://github.com/opencv/opencv/wiki/TensorFlow-Object-Detection-API#use-existing-config-file-for-your-model 에서 모델을 선택합니다. 여기에서는 첫 번째인 MobileNet-SSD v1을 사용하도록 하겠습니다.
weights에 링크돼 있는 tar.gz 파일이 pb 파일이 포함된 압축 파일이고, config에 링크된 파일이 pbtxt 파일입니다. pb 파일은 링크를 통해 바로 다운로드 가능하지만, pbtxt 파일의 경우 config에 걸린 링크가 해당 파일의 github 페이지이므로 그 페이지로 이동하여 raw 링크를 통해 다운로드 해야 합니다.
다음과 같은 절차를 통해 pb 파일과 pbtxt 파일을 다운로드 합니다.
cd picam
wget http://download.tensorflow.org/models/object_detection/ssd_mobilenet_v1_coco_2017_11_17.tar.gz
tar xvfz ssd_mobilenet_v1_coco_2017_11_17.tar.gz
rm -f ssd_mobilenet_v1_coco_2017_11_17.tar.gz
cd ssd_mobilenet_v1_coco_2017_11_17
wget https://raw.githubusercontent.com/opencv/opencv_extra/4.x/testdata/dnn/ssd_mobilenet_v1_coco_2017_11_17.pbtxt
※ ssd_mobilenet_v1_coco_2017_11_17.tar.gz 파일의 압축을 풀면 pb 파일 외에도 checkpoint 파일 등 불필요한 여러 파일들이 존재합니다. 이 글에서는 별도로 해당 파일들에 대한 삭제를 언급하지는 않았습니다.
웹앱에 기능 추가
이제 웹앱에 기능을 추가해 보겠습니다. 객체 탐지는 /detection 이라는 경로로 추가하도록 하겠습니다. 먼저 detection 디렉토리를 만듭니다(picam/picam/detection).
ObjectDetector 클래스
detection 디렉토리에 obejct_detector.py 파일을 만들고 다음의 코드를 추가합니다. 코드는 객체 인식을 하는 클래스입니다. 필요한 설명은 코드에 주석으로 추가해 놨습니다. 상단에는 object_class_map이라는 변수명으로 모델이 탐지한 객체의 클래스에 대한 이름을 담고 있는데, 이 정보는 여기에서 확인할 수 있습니다.
import os
import time
import cv2
import numpy as np
from picamera2 import Picamera2
from summer_toolkit.utility.singleton import Singleton
# 모델이 객체를 탐지하면 해당 객체가 무엇인지 숫자로된 클래스 코드 값을 알려줍니다.
# 해당 코드의 실제 클래스 이름입니다.
object_class_map = {
"0": "background",
"1": "person",
"2": "bicycle",
"3": "car",
"4": "motorcycle",
"5": "airplane",
"6": "bus",
"7": "train",
"8": "truck",
"9": "boat",
"10": "traffic light",
"11": "fire hydrant",
"12": "12",
"13": "stop sign",
"14": "parking meter",
"15": "bench",
"16": "bird",
"17": "cat",
"18": "dog",
"19": "horse",
"20": "sheep",
"21": "cow",
"22": "elephant",
"23": "bear",
"24": "zebra",
"25": "giraffe",
"26": "26",
"27": "backpack",
"28": "umbrella",
"29": "29",
"30": "30",
"31": "handbag",
"32": "tie",
"33": "suitcase",
"34": "frisbee",
"35": "skis",
"36": "snowboard",
"37": "sports ball",
"38": "kite",
"39": "baseball bat",
"40": "baseball glove",
"41": "skateboard",
"42": "surfboard",
"43": "tennis racket",
"44": "bottle",
"45": "45",
"46": "wine glass",
"47": "cup",
"48": "fork",
"49": "knife",
"50": "spoon",
"51": "bowl",
"52": "banana",
"53": "apple",
"54": "sandwich",
"55": "orange",
"56": "broccoli",
"57": "carrot",
"58": "hot dog",
"59": "pizza",
"60": "donut",
"61": "cake",
"62": "chair",
"63": "couch",
"64": "potted plant",
"65": "bed",
"66": "66",
"67": "dining table",
"68": "68",
"69": "69",
"70": "toilet",
"71": "71",
"72": "tv",
"73": "laptop",
"74": "mouse",
"75": "remote",
"76": "keyboard",
"77": "cell phone",
"78": "microwave",
"79": "oven",
"80": "toaster",
"81": "sink",
"82": "refrigerator",
"83": "83",
"84": "book",
"85": "clock",
"86": "vase",
"87": "scissors",
"88": "teddy bear",
"89": "hair drier",
"90": "toothbrush",
}
class ObjectDetector(metaclass=Singleton):
def __init__(self):
self.current_dir = os.getcwd()
model_path = os.path.join(self.current_dir, 'ssd_mobilenet_v1_coco_2017_11_17')
# Tensorflow로 만들어진 모델을 읽어 오는 부분입니다.
self.cv_net = cv2.dnn.readNetFromTensorflow(
os.path.join(model_path, 'frozen_inference_graph.pb'),
os.path.join(model_path, 'ssd_mobilenet_v1_coco_2017_11_17.pbtxt'),
)
def detect(self, pil_image):
# PIL 형식의 이미지를 받아서, OpenCV에서 사용할 수 있는 형태로 변환합니다.
img = np.array(pil_image)
img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR) # RGB를 OpenCV에서 사용하는 형식인 BGR로 변경합니다.
rows = img.shape[0]
cols = img.shape[1]
# 이미지를 모델 입력으로 지정합니다.
self.cv_net.setInput(cv2.dnn.blobFromImage(img, size=(300, 300), swapRB=True, crop=False))
# 입력 내용을 통해 모델을 통과 시킵니다.
cv_out = self.cv_net.forward()
# cv_out 형태
# [[[[0, class_id, score, left, top, right, bottom]]]]
for detection in cv_out[0,0,:,:]:
score = float(detection[2])
if score > 0.3:
left = detection[3] * cols
top = detection[4] * rows
right = detection[5] * cols
bottom = detection[6] * rows
left_top = (int(left), int(top))
text_org = (left_top[0] + 10, left_top[1] + 20)
right_bottom = (int(right), int(bottom))
class_name = object_class_map[str(int(detection[1]))]
text = f'{class_name}({int(score * 100)}%)'
# 모델 탐지 결과대로 사각형을 그려줍니다.
cv2.rectangle(img, left_top, right_bottom, (23, 230, 210), thickness=2)
# 객체 분류 이름을 그려줍니다.
cv2.putText(img, text, text_org, cv2.FONT_HERSHEY_SIMPLEX, 0.8, (23, 230, 210), 2)
return cv2.imencode(".jpg", img)[1].tobytes()
/detection 경로 추가
detection 디렉토리를 만들고(/picam/picam/detection), detection_router.py 파일을 생성합니다.
from fastapi import APIRouter, Request
from fastapi.responses import StreamingResponse
from picamera2 import Picamera2
from picam.detection.object_detector import ObjectDetector
from picam.monitor.camera_agent import CameraAgent
detection_router = APIRouter(tags=['detection'], prefix='/detection')
camera_agent = CameraAgent()
object_detector = ObjectDetector()
def generate_image():
while True:
# CameraAgent#capture를 통해 캡처된 PIL 형식의 이미지를 대상으로
# 객체 인식을 실행하여 결과를 반환 받습니다.
pil_img = camera_agent.capture(is_bytearray=False)
img_bytes = object_detector.detect(pil_img)
yield (
b'--frame\r\n'
b'Content-Type: image/jpeg\r\n\r\n' + img_bytes + b'\r\n\r\n'
)
@detection_router.get('', include_in_schema=False)
def respond_root(request: Request):
return StreamingResponse(
generate_image(),
media_type="multipart/x-mixed-replace; boundary=frame",
)
실행 및 확인
모든 코드가 준비됐으면 python local_server.py를 실행하고, 브라우저로 /detection 경로에 접속합니다.
전체 코드
전체 코드를 가지고 바로 실행해 보려는 경우 다음과 같이 하면 됩니다.
git clone https://github.com/intotherealworld/picam.git
cd picam
python3 -m venv .venv --system-site-packages
source .venv/bin/activate
pip install -r requirements.txt
wget http://download.tensorflow.org/models/object_detection/ssd_mobilenet_v1_coco_2017_11_17.tar.gz
tar xvfz ssd_mobilenet_v1_coco_2017_11_17.tar.gz
rm -f ssd_mobilenet_v1_coco_2017_11_17.tar.gz
cd ssd_mobilenet_v1_coco_2017_11_17
wget https://raw.githubusercontent.com/opencv/opencv_extra/4.x/testdata/dnn/ssd_mobilenet_v1_coco_2017_11_17.pbtxt
cd ..
python local_server.py
참고
'생활코딩' 카테고리의 다른 글
라즈베리파이 카메라 모듈 - 모니터링 웹앱 만들기 (7) | 2024.11.01 |
---|---|
라즈베리파이 - 홈서버 구성 (1) | 2024.10.31 |
라즈베리파이에 docker 설치하기 (2) | 2024.10.27 |
GoAccess - nginx 접속 로그 분석 (0) | 2024.10.26 |
라즈베리파이 온습도 센서 사용하기 (0) | 2024.10.25 |