AEGLOSLAB Arduino UNO Q 완전 가이드

제9부 AI·엣지 응용

9.5 카메라 비전 예제와 추론 성능 측정

이 장은 9.1~9.4에서 다룬 하드웨어·소프트웨어 지식을 실제 카메라 앱으로 묶는다. Arduino 공식 예제 저장소(arduino/app-bricks-examples)의 실제 소스를 그대로 가져와 구조를 설명하고, 독자가 자신의 보드에서 지연시간·FPS를 직접 측정할 수 있는 코드를 제시한다. 이 책은 구체적인 ms·FPS 수치를 단정하지 않는다 — 이유는 9.5.6에서 밝힌다.

9.5.1 두 가지 시작점 — 정지 이미지 vs 실시간 비디오

App Lab 예제 갤러리에는 컴퓨터 비전 관련 예제가 여럿 있다. 이 장은 그중 구조가 가장 단순한 두 개를 다룬다.

예제입력특징
Simple Object Detection정지 이미지 파일지연시간 측정 코드가 이미 내장돼 있음
Detect Objects on Camera(내부적으로 Video Object Detection Brick 사용)USB/CSI 카메라 실시간 스트림콜백 기반, 코드가 약 20줄 수준으로 짧음(직접 확인 — 아래 참고)

두 예제 모두 MPL-2.0 라이선스로 공개된 arduino/app-bricks-examples 저장소에서 가져왔다.

9.5.2 코드로 보는 구조 — 정지 이미지 객체 탐지

core-and-foundational/09-computer-vision/01-simple-object-detection/python/main.py(전문, 집필 시점 저장소 원문 그대로):

# SPDX-FileCopyrightText: Copyright (C) Arduino s.r.l. and/or its affiliated companies
# SPDX-License-Identifier: MPL-2.0

from arduino.app_utils import App
from arduino.app_utils import Logger
from arduino.app_bricks.object_detection import ObjectDetection
from PIL import Image
import time

PATH_IMAGE = "assets"
IMAGE_FILE = "input_image.png"
CONFIDENCE_THRESHOLD = 0.5

logger = Logger("ObjectDetectionApp")

object_detection = ObjectDetection()
pil_image = Image.open(f"{PATH_IMAGE}/{IMAGE_FILE}")

def loop():
    start_time = time.time() * 1000
    results = object_detection.detect(pil_image, confidence=CONFIDENCE_THRESHOLD)
    diff = time.time() * 1000 - start_time

    if results is None:
        logger.info("No objects detected in the image.")
    else:
        logger.info(f"Detected {len(results.get('detection', []))} objects in the image in {diff:.2f} ms.")
        for idx, detection in enumerate(results.get("detection", [])):
            label = detection.get("class_name", "unknown")
            confidence = detection.get("confidence", "0")
            logger.info(f"Object {idx}: Label={label}, Confidence={confidence}")

        img_with_boxes = object_detection.draw_bounding_boxes(pil_image, results)
        if img_with_boxes is not None:
            img_with_boxes.save("output_image.png", format="PNG")

    time.sleep(60)

App.run(user_loop=loop)

주목할 점: 이 예제는 이미 time.time() 기반 지연시간 측정 코드를 포함하고 있다(start_time, diff). Arduino 팀이 예제 단계에서부터 "추론에 시간이 걸린다"는 것을 사용자에게 보여주려 한 의도로 읽힌다. loop()가 60초마다 한 번씩만 반복되므로, 이 예제 자체는 실시간 스트림이 아니라 정지 이미지를 반복 재추론하는 데모다.

9.5.3 실시간 비디오 객체 탐지 — 약 20줄의 실체

블로그·커뮤니티 글에서 "Detect Objects on Camera 예제는 실제 코드가 약 20줄뿐"이라는 표현을 흔히 볼 수 있다. core-and-foundational/09-computer-vision/02-video-object-detection/python/main.py(전문, 집필 시점 저장소 원문)를 직접 세어 보면 대체로 맞는 말이다 — 라이선스 헤더·주석·빈 줄을 빼면 실제 실행 코드는 20줄 안팎이다.

# SPDX-FileCopyrightText: Copyright (C) Arduino s.r.l. and/or its affiliated companies
# SPDX-License-Identifier: MPL-2.0

from arduino.app_utils import App, Logger
from arduino.app_bricks.video_objectdetection import VideoObjectDetection
from arduino.app_peripherals.camera import Camera

CONFIDENCE_THRESHOLD = 0.5
logger = Logger("VideoObjectDetectionApp")

camera = Camera(resolution=(640, 480), fps=30)
# camera = Camera("csi:0", resolution=(640, 480), fps=30)   # CSI 카메라
# camera = Camera("usb:0", resolution=(640, 480), fps=30)   # USB 카메라(포지션 지정)
# camera = Camera("rtsp://<RTSP_URL>", username="<USERNAME>", password="<PASSWORD>")  # RTSP 스트림

detection_stream = VideoObjectDetection(camera=camera, confidence=CONFIDENCE_THRESHOLD, debounce_sec=0.0)
# detection_stream = VideoObjectDetection(camera=camera, confidence=0.7, debounce_sec=0.0)      # 더 엄격한 임계값
# detection_stream = VideoObjectDetection(camera=camera, confidence=CONFIDENCE_THRESHOLD, debounce_sec=1.0)  # 중복 탐지 디바운스

def send_detections_to_console(detections: dict):
    for key, values in detections.items():
        for value in values:
            logger.info(f"Detected {key} with confidence {value.get('confidence', 'N/A')}")

detection_stream.on_detect_all(send_detections_to_console)

App.run()

구조를 분석하면:

  1. Camera(...)로 입력원을 선언한다 — 기본 카메라, CSI 위치 지정, USB 위치 지정, RTSP 스트림 네 가지 옵션이 주석으로 함께 제공된다.
  2. VideoObjectDetection(...)이 카메라 프레임을 받아 내부적으로 계속 추론을 실행하는 Brick이다. confidence(신뢰도 임계값)와 debounce_sec(같은 객체의 중복 탐지를 얼마나 걸러낼지)가 주요 파라미터다.
  3. on_detect_all(콜백)로 탐지 결과를 이벤트 기반으로 받는다 — 폴링이 아니라 콜백이라는 점이 프레임 루프를 직접 작성할 필요가 없게 해 준다.
  4. App.run()이 전체 이벤트 루프를 시작한다.
USB/CSI 카메라 VideoObjectDetection Brick on_detect_all 콜백 Logger 콘솔 출력 WebUI :7000 바운딩박스 표시

App Lab 예제 갤러리의 "Detect Objects on Camera"는 이 Brick 위에 WebUI Brick을 추가로 연결해, 브라우저(http://<보드주소>:7000)에서 실시간으로 바운딩박스가 그려진 영상을 볼 수 있게 만든 버전이다.

9.5.4 실행 요구사항과 흔한 함정

  • USB 카메라 + 전원 있는 USB 허브 필요: UNO Q는 USB-C 포트 하나가 전원과 데이터를 겸하므로, USB 카메라를 물리려면 전원 공급이 되는 허브가 있어야 한다는 것이 커뮤니티 자료(App Lab 문서·튜토리얼)의 공통된 설명이다.
  • Network Mode 필요: 이 예제는 App Lab의 Network Mode(USB-C 허브 경유 네트워크 연결)로 실행해야 한다고 안내된다.
  • Brick을 먼저 선택해야 한다: 포럼 스레드 "Detect Objects on Camera using Arduino UNO Q"(forum.arduino.cc/t/1427993, 집필 시점 확인)에서 한 사용자가 웹 UI가 열리지 않는 문제로 어려움을 겪었는데, 원인은 Run을 누르기 전에 Bricks에서 "Video Object Detection"을 선택하지 않았던 것이었다. 선택 후 Python 서비스가 정상적으로 기동되면서 해결됐다.
  • WiFi보다 USB 연결이 안정적이었다는 보고: 같은 스레드에서 다른 사용자는 "App Lab을 호스트 PC에 WiFi로 연결했을 때는 web-ui가 안 됐는데, USB로 바꾸니 바로 됐다"고 보고했다. 재현성이 검증된 공식 설명은 아니므로 참고용 정보로 남긴다.
  • .local 호스트명으로 접속되지 않으면 IP를 직접 쓴다: 같은 스레드에서 <보드이름>.local:7000 접속이 안 될 때 보드의 IP 주소로 http://<IP>:7000을 직접 입력해 우회한 사례가 있다.
증상시도해 볼 것근거
브라우저가 자동으로 안 열림http://<IP>:7000 수동 접속포럼 1427993
<보드>.local 접속 안 됨IP 주소로 직접 접속포럼 1427993
웹 UI가 전혀 응답 없음Bricks에서 Video Object Detection 선택 후 Run포럼 1427993
WiFi 연결에서 불안정USB로 App Lab 연결 전환 후 재시도(비공식 보고)포럼 1427993
카메라 인식 안 됨전원 공급 USB 허브 사용, Network Mode 확인App Lab 문서

9.5.5 추론 지연 직접 측정하기

정지 이미지 예제는 이미 측정 코드를 갖고 있다(9.5.2). 이를 9.3.5의 벤치마크 패턴과 결합하면 제대로 된 통계를 산출할 수 있다.

import time
import statistics
from arduino.app_bricks.object_detection import ObjectDetection
from PIL import Image

object_detection = ObjectDetection()
pil_image = Image.open("assets/input_image.png")

# 워밍업 — 첫 추론은 모델 로드 비용이 섞인다
object_detection.detect(pil_image, confidence=0.5)

N = 30
latencies_ms = []
for _ in range(N):
    start = time.time() * 1000
    object_detection.detect(pil_image, confidence=0.5)
    latencies_ms.append(time.time() * 1000 - start)

latencies_ms.sort()
print(f"평균 {statistics.mean(latencies_ms):.1f}ms  "
      f"중앙값 {statistics.median(latencies_ms):.1f}ms  "
      f"p95 {latencies_ms[int(N*0.95)-1]:.1f}ms  "
      f"환산 FPS(중앙값 기준) {1000/statistics.median(latencies_ms):.1f}")

실시간 비디오 스트림은 on_detect_all 콜백이 호출되는 시점 사이의 간격을 측정하면 사실상의 FPS를 추정할 수 있다.

import time
import collections
from arduino.app_utils import App, Logger
from arduino.app_bricks.video_objectdetection import VideoObjectDetection
from arduino.app_peripherals.camera import Camera

logger = Logger("BenchmarkApp")
camera = Camera(resolution=(640, 480), fps=30)
detection_stream = VideoObjectDetection(camera=camera, confidence=0.5, debounce_sec=0.0)

timestamps = collections.deque(maxlen=30)  # 최근 30회 콜백 시각

def on_detect(detections: dict):
    now = time.time()
    timestamps.append(now)
    if len(timestamps) >= 2:
        window = timestamps[-1] - timestamps[0]
        fps_est = (len(timestamps) - 1) / window if window > 0 else 0.0
        logger.info(f"최근 {len(timestamps)}회 콜백 기준 추정 FPS: {fps_est:.1f}")

detection_stream.on_detect_all(on_detect)
App.run()

⚠️ 이 FPS 추정치는 "콜백이 불린 빈도"이지 "카메라가 실제로 몇 프레임을 캡처했는지"와는 다를 수 있다 — debounce_sec을 0보다 크게 두면 콜백 빈도가 낮아지므로, 순수 추론 속도를 재려면 debounce_sec=0.0으로 맞춰야 한다.

9.5.6 정직하게 — 이 책이 구체적 수치를 제시하지 않는 이유

이 장을 준비하며 "UNO Q에서 객체 탐지가 몇 ms/몇 FPS로 동작한다"는 벤더 공식 수치나 재현 가능한 커뮤니티 벤치마크를 찾으려 했으나, 다음 이유로 신뢰할 만한 단일 수치를 제시하지 않기로 했다.

  • 9.3에서 확인했듯 GPU/QNN 가속 경로의 가용성 자체가 불확실하다 — 같은 모델이라도 CPU로만 실행되는지, 일부 가속이 적용되는지에 따라 결과가 크게 달라진다.
  • 모델 종류(YOLOX-Nano vs 커스텀 모델), 입력 해상도, confidence·debounce_sec 설정, 보드 개체(2GB/4GB 모델, 1.1 참고)와 열 상태(2.4, 5.5)에 따라 수치가 변동한다.
  • 조사 과정에서 접한 포럼 스레드들은 대부분 연결·설정 문제를 다뤘을 뿐, 정량적 FPS·지연시간을 보고한 사례를 찾지 못했다.

대신 이 장은 독자가 자기 보드·자기 모델·자기 조건에서 직접 측정할 수 있는 코드(9.5.5)를 제공하는 쪽을 택했다. 측정 후 조건(모델, 해상도, provider, 보드 리비전)을 함께 기록해 두면, 나중에 소프트웨어 업데이트 전후 비교나 다른 사용자와의 비교에도 쓸 수 있다. 측정 결과를 Arduino 포럼(9.4에서 인용한 스레드 등)에 공유하면 커뮤니티 전체에 도움이 된다.

9.5.7 다음 장으로

  • 10.1: 제10부 서드파티 생태계 — 라즈베리파이 호환 카메라·HAT을 UNO Q에 연결하는 방법.
  • 9.3 / 9.4: 이 장의 성능 측정치가 왜 갈릴 수 있는지에 대한 배경.
  • 12.2: 제12부 실전 프로젝트 — 카메라 비전 프로젝트에서 이 장의 코드를 실전 규모로 확장한다.