Ubuntu EdgeX 실습(2)

zeusqoi·2026년 9월 11일

개인공부

목록 보기
16/42

이번 실습에서는 EdgeX에서 USB 카메라를 어떻게 관리하고 스트리밍하는지, 그리고 여기서 한 단계 더 나아가 카메라에서 얻은 이미지 데이터를 EdgeX 내부에 저장하고 조회하는 방법을 실습했다.

앞선 실습에서는 EdgeX의 기본적인 서비스 구조와 Compose를 이용한 실행 방법을 확인했다.

이번에는 실제 USB 카메라를 EdgeX에 연결해본다.

단순히 카메라를 연결하는 것에서 끝나는 것이 아니라,

USB Camera
    ↓
USB Camera Device Service
    ↓
RTSP Stream
    ↓
외부 영상 처리

라는 기존 구조가 어떻게 동작하는지 확인하고,

USB Camera
    ↓
USB Camera Device Service
    ↓
이미지 데이터
    ↓
EdgeX Core Services
    ↓
Redis

와 같이 이미지 데이터 자체를 EdgeX에서 관리할 수 있는 방법까지 확인한다.


1. 동작 환경 및 동작 순서

1.1. USB Camera Device Service 실행

먼저 EdgeX의 USB Camera Device Service를 실행했다.

cd ~/edgex-compose/compose-builder

make run no-secty ds-usb-camera

실행 결과에서 다음과 같이 컨테이너가 실행되는 것을 확인할 수 있었다.

✔ Container edgex-device-usb-camera Started

여기서 ds-usb-camera는 Device Service이다.

EdgeX에서 Device Service는 실제 장치와 EdgeX 사이를 연결하는 역할을 한다.

즉, 우리가 연결한 USB 카메라를 EdgeX가 직접 다룰 수 있도록 해주는 서비스라고 생각하면 된다.

1.2. USB Camera Device Service 확인

Device Service가 정상적으로 실행되었는지 확인했다.

curl -s \
http://localhost:59881/api/v2/deviceservice/name/device-usb-camera | jq .

정상적으로 실행 중이라면 device-usb-camera 서비스의 정보가 출력된다.

또한 EdgeX CLI를 설치한 후 Device Service 목록도 확인했다.

sudo snap install edgex-cli
edgex-cli deviceservice list

결과:

Name               BaseAddress
device-usb-camera  http://edgex-device-usb-camera:59983

이 단계에서 확인한 것은 카메라 자체가 아직 등록된 것이 아니라, 카메라를 관리할 Device Service가 먼저 실행되어 있다는 것이다.

1.3. USB 카메라 장치 확인

이제 실제 Ubuntu에서 USB 카메라가 어떤 장치 파일로 인식되었는지 확인했다.

v4l2-ctl --list-devices

처음에는 실제 웹캠이 연결되어 있지 않아 다음과 같은 가상 장치만 확인되었다.

Dummy video device (0x0000):
    /dev/video0

이 상태에서는 EdgeX USB Camera Device Service에 카메라를 등록하더라도 실제 카메라 스트리밍을 정상적으로 사용할 수 없다.

이후 실제 C270 HD WEBCAM을 연결했다.

다시 장치를 확인하면:

Dummy video device:
    /dev/video0

C270 HD WEBCAM:
    /dev/video1
    /dev/video2
    /dev/media0

와 같이 실제 카메라가 추가된 것을 확인할 수 있었다.

여기서 중요한 것은 카메라가 연결되었다고 해서 /dev/video0이라고 무조건 가정하면 안 된다는 것이다.

실제 환경에서는 /dev/video1, /dev/video2 등으로 할당될 수 있기 때문에 v4l2-ctl --list-devices를 이용해 실제 경로를 확인해야 한다.

1.4. 카메라의 상세 정보 확인

실제 카메라가 /dev/video1에 연결되어 있는 것을 확인한 뒤 상세 정보를 확인했다.

v4l2-ctl --all --device /dev/video1

주요 정보는 다음과 같았다.

Driver name      : uvcvideo
Card type        : C270 HD WEBCAM
Bus info         : usb-0000:00:14.0-6

Model            : C270 HD WEBCAM
Serial           : 200901010001

Width/Height     : 640/480
Pixel Format     : YUYV
Frames per second: 30.000

이 과정을 통해 단순히 /dev/video1이라는 파일이 존재하는 것이 아니라, 실제 Logitech C270 웹캠이 해당 장치로 인식되고 있다는 것을 확인할 수 있었다.

1.5. EdgeX에 카메라 등록

이제 실제 카메라를 EdgeX의 Device로 등록했다.

먼저 이전에 잘못 등록했던 장치를 삭제했다.

curl -X DELETE \
'http://localhost:59881/api/v2/device/name/Ca' \
-H 'accept: application/json'

그 다음 실제 카메라의 경로인 /dev/video1을 이용해 다시 등록했다.

curl -X POST -H 'Content-Type: application/json' \
http://localhost:59881/api/v2/device \
-d '[
  {
    "apiVersion": "v2",
    "device": {
      "name":"Ca",
      "serviceName": "device-usb-camera",
      "profileName": "USB-Camera-General",
      "description": "My test camera",
      "adminState": "UNLOCKED",
      "operatingState": "UP",
      "protocols": {
        "USB": {
          "CardName": "whatever",
          "Path": "/dev/video1",
          "AutoStreaming": "false"
        }
      }
    }
  }
]'

정상적으로 등록되면:

{
  "statusCode": 201
}

이 반환된다.

여기서 Path에 입력한 /dev/video1이 앞에서 v4l2-ctl을 이용해 확인한 실제 카메라 경로이다.

즉,

Ubuntu
 └─ /dev/video1
       ↓
USB Camera Device Service
       ↓
EdgeX Device "Ca"

와 같이 연결된 것이다.

1.6. 카메라 스트리밍 시작

카메라를 Device로 등록했으므로 이제 스트리밍을 시작한다.

curl -X PUT -d '{
  "StartStreaming": {
    "InputImageSize": "640x480",
    "OutputVideoQuality": "5"
  }
}' \
http://localhost:59882/api/v2/device/name/Ca/StartStreaming

정상적으로 실행되면:

{
  "apiVersion": "v2",
  "statusCode": 200
}

이 반환된다.

이제 EdgeX에 카메라가 등록되어 있고 실제 스트리밍까지 시작된 상태이다.

1.7. RTSP Stream URI 확인

스트리밍이 시작되었으므로 실제 영상을 가져올 수 있는 주소를 확인했다.

curl -s \
http://localhost:59882/api/v2/device/name/Ca/StreamURI | jq '.'

결과에서 다음과 같은 값을 확인할 수 있었다.

rtsp://localhost:8554/stream/Ca

여기서 RTSP는 실시간 영상 스트리밍을 전달하기 위한 프로토콜이다.

중요한 점은 EdgeX가 이 단계에서 이미지 파일을 Core Data에 저장하는 것이 아니라, 카메라 영상을 외부에서 가져갈 수 있는 스트리밍 주소를 제공한다는 것이다.

1.8. 실제 영상 확인

확인한 RTSP 주소를 ffplay를 이용해 재생했다.

ffplay -rtsp_transport tcp \
rtsp://localhost:8554/stream/Ca

실제로 C270 웹캠의 영상이 출력되는 것을 확인했다.

터미널에는 영상의 해상도와 프레임 정보도 출력되었다.

Video: mpeg4
640x480
30 tbr

중간에

warning: first frame is no keyframe

이라는 경고도 출력되었지만, 실제 영상이 정상적으로 출력되고 있었기 때문에 스트리밍 자체에는 문제가 없었다.

여기까지의 실습을 통해 다음 구조가 실제로 동작하는 것을 확인했다.

C270 Webcam
      ↓
/dev/video1
      ↓
USB Camera Device Service
      ↓
StartStreaming
      ↓
RTSP URI
      ↓
ffplay
      ↓
실시간 영상

2. 기존 동작의 문제점

여기서 한 가지 의문이 생긴다.

카메라 영상이 EdgeX를 통해 스트리밍되고 있다면, 그 영상 자체도 EdgeX 내부에 저장되고 있는 것일까?

이번 실습에서 가장 중요한 부분 중 하나가 바로 이 질문이다.

PDF에서는 USB Camera Device Service의 소스 코드와 Device Profile을 분석하여 이 부분을 확인한다.

2.1. USB Camera Device Service가 저장하는 데이터

USB Camera Device Service의 metadata.go를 살펴보면 이미지와 관련된 구조체들이 정의되어 있다.

하지만 이 구조체들은 실제로 촬영된 이미지의 픽셀 데이터 자체를 저장하는 구조체가 아니다.

예를 들어 이미지의:

  • Width
  • Height
  • Pixel Format
  • Bytes Per Line
  • Frame Size

등과 같은 카메라와 영상의 특성 및 메타데이터를 나타낸다.

즉,

이미지 자체
[픽셀][픽셀][픽셀]...

       ≠

이미지 정보
width
height
pixel format
...

이다.

2.2. capture라는 이름 때문에 생길 수 있는 오해

driver.go, device.go를 보면 capture라는 단어가 포함된 함수들이 존재한다.

처음 보면

"capture 함수가 있으니까 카메라로 사진을 찍어서 EdgeX에 저장하는구나."

라고 생각할 수 있다.

하지만 실제 구현을 확인하면 해당 함수는 장치가 녹화 또는 스트리밍 가능한 장치인지 확인하는 역할을 한다.

즉, capture라는 이름만 보고 이미지 데이터가 EdgeX에 저장된다고 판단하면 안 된다.

2.3. Device Profile에서 제공하는 명령 확인

USB Camera Device Service에서 사용하는 general.usb.camera.yaml의 command도 확인했다.

여기에는 카메라의:

  • Device Capability
  • Current Video Input
  • Camera Status
  • Data Format
  • Cropping Ability
  • Streaming Parameters
  • Image Formats
  • Video Start Streaming
  • Video Stop Streaming
  • Stream URI
  • Streaming Status

등을 다루는 명령이 정의되어 있다.

하지만 촬영된 이미지 또는 동영상 데이터를 EdgeX 내부에 저장하는 command는 존재하지 않는다.

따라서 USB Camera Device Service의 역할을 정확하게 표현하면 카메라 장치를 관리하고 RTSP 스트리밍 주소를 제공하는 서비스라고 할 수 있다.

2.4. 기존 구조 이해

PDF에서 설명하는 기존 구조는 다음과 같다.

USB Camera
    ↓
USB Camera Microservice
    ↓
RTSP Stream URI
    ↓
외부 Pipeline Server
    ↓
Object Detection
    ↓
Detection Result
    ↓
MQTT / REST
    ↓
EdgeX Core Services

예를 들어 Object Detection을 수행한다고 하면 카메라 영상 자체를 EdgeX Core Data에 저장하는 것이 아니라,

카메라 영상
   ↓
RTSP
   ↓
외부 Object Detection 서버
   ↓
사람 / 자동차 / 강아지 ...

와 같이 외부 서버가 영상을 받아 분석한다.

그 후 detection class, score, bounding box 등의 분석 결과만 EdgeX로 다시 전달할 수 있다.

따라서 기존 방식에서는 이미지 자체 데이터가 EdgeX 내부에 저장되지 않는다.


3. 해결 방법 제안

그렇다면 이미지 자체를 EdgeX에서 관리하고 싶다면 어떻게 해야 할까?

PDF에서는 USB Camera Device Service와 EdgeX Core Services 사이에 이미지 데이터를 저장하는 별도의 Microservice를 추가하는 방법을 제안한다.

USB Camera
     ↓
USB Camera Device Service
     ↓
Image → Array 변환
     ↓
Device REST
     ↓
EdgeX Core Services
     ↓
Core Data / Redis

3.1. Device REST를 이용한 Binary 이미지 저장

먼저 Device REST가 제공하는 endpoint를 이용해 이미지 파일 자체를 Binary 형태로 저장해봤다.

Postman을 이용해 다음 endpoint로 요청했다.

POST
http://localhost:59986/api/v2/resource/sample-image/png

Body는 binary로 설정하고 PNG 파일을 선택했다.

요청 결과:

200 OK

정상적으로 이미지가 전달되었다.

PDF에서도 동일하게 Device REST endpoint에 Binary 형태의 PNG/JPEG 이미지를 POST하고 Core Data에서 확인하는 과정을 설명한다.

3.2. Binary 방식의 한계

그렇다면 Binary 방식이면 문제가 해결된 것처럼 보인다.

하지만 Core Data에서 Reading을 조회해보면:

{
  "valueType": "Binary",
  "mediaType": "image/png",
  "value": ""
}

와 같이 value를 직접 확인할 수 없다.

즉,

"이미지가 저장되어 있다."

라는 사실은 확인할 수 있지만,

"저장된 이미지의 실제 데이터가 무엇인가?"

를 Core Service의 일반적인 Reading 값으로 직접 확인하거나 활용하기 어렵다.

PDF에서도 Binary 방식은 Core Service에서 실제 value를 확인할 수 없다는 점을 문제점으로 제시한다.

3.3. 이미지 데이터를 배열로 변환

그래서 다른 방법을 사용했다.

이미지는 결국 픽셀들의 집합이고, 이를 배열로 표현할 수 있다.

이번 실습에서는 Python의 Pillow와 NumPy를 이용해 이미지를 배열로 변환했다.

from PIL import Image
import numpy as np

image = Image.open(filepath)
image_array = np.array(image)

실제로 변환한 결과:

(701, 844, 4)
uint8

였다.

이는 다음과 같은 의미이다.

701   → 이미지 높이
844   → 이미지 너비
4     → RGBA 4개 채널
uint8 → 각 픽셀 값의 자료형

즉 이미지 한 장이:

[높이][너비][RGBA]

형태의 3차원 배열로 표현된 것이다.

PDF에서도 이미지가 결국 3차원 배열로 표현될 수 있다는 점을 이용하여 배열 데이터를 Core Service에 전달하는 방법을 제안한다.

3.4. 이미지 배열을 EdgeX에 POST

PDF에서는 이 과정을 수행하는 image to array Dockerfile을 제시한다고 되어 있지만, 제공된 실습 자료에는 실제 Dockerfile 코드가 포함되어 있지 않았다.

따라서 이번 실습에서는 같은 목적을 수행하는 Python 코드를 직접 작성했다.

image = Image.open(filepath)
image_array = np.array(image)

data = {
    "image": image_array.tolist()
}

response = requests.post(
    POST_URL,
    headers={"Content-Type": "application/json"},
    data=json.dumps(data)
)

여기서

image_array.tolist()

를 이용해 NumPy 배열을 일반 Python 리스트로 변환한다.

그 후 JSON 형태로 만들어 Device REST의 sample-json endpoint에 전달했다.

http://localhost:59986/api/v2/resource/sample-json/json

실행 결과:

basic.png
status: 200
response:

즉 이미지 배열 데이터가 정상적으로 EdgeX에 전달되었다.


4. 제안한 방법 동작 결과

이제 정말 EdgeX에 이미지 배열이 저장되었는지 확인해야 한다.

4.1. Core Data에서 확인

다음 API를 이용해 sample-json Device의 Reading을 조회했다.

curl -s \
"http://localhost:59880/api/v2/reading/device/name/sample-json?offset=0&limit=20" \
| jq .

그 결과:

{
  "resourceName": "json",
  "valueType": "Object",
  "objectValue": {
    "image": [
      ...
    ]
  }
}

형태로 저장된 것을 확인할 수 있었다.

즉 Binary 방식과 달리 이번에는 EdgeX Core Data에서 실제 이미지 데이터가 objectValue.image에 들어가 있는 것을 확인할 수 있었다.

데이터가 매우 크기 때문에 터미널에서 전부 출력하면 화면을 가득 채울 정도로 많은 배열이 출력된다.

예를 들어 데이터의 마지막 부분을 확인하면:

0],[255,255,255,0],[255,255,255,0],...

처럼 배열이 정상적으로 끝까지 이어지는 것을 확인할 수 있었다.

4.2. Redis에서 확인

EdgeX Core Data는 Redis를 사용하므로, 실제 Redis에 해당 Reading이 어떻게 저장되었는지도 확인했다.

먼저 Redis 컨테이너를 확인했다.

docker ps --format "{{.Names}}" | grep redis

결과:

edgex-redis

Redis 내부의 key도 확인할 수 있었다.

docker exec -it edgex-redis redis-cli --scan

여기서 Core Data Reading과 관련된:

cd|rd:...

형태의 key를 확인할 수 있었다.

4.3. Redis Insight로 데이터 확인

PDF에서는 Redis 데이터를 확인하기 위해 RESP.app을 사용했다.

이번 Ubuntu 환경에서는 RESP.app을 현재 사용할 수 없어 Redis Insight를 사용했다.

Redis Insight에서 EdgeX Redis에 연결했다.

127.0.0.1:6379

그리고 이미지 배열이 저장된 Reading의 Redis key를 검색했다.

검색 결과:

cd|rd:268a0772-9f99-4ddb-ad26-456f3ff93cf4

가 확인되었고, 데이터 크기도 약 8MB로 표시되었다.

해당 key를 열어보니 실제 데이터 안에:

DeviceName: "sample-json"
ResourceName: "json"
ValueType: "Object"
ObjectValue:
{
    "image": [
        [
            [255,255,255,0],
            ...
        ]
    ]
}

형태의 데이터가 들어 있는 것을 확인할 수 있었다.

즉 단순히 "Core Data API에서 보인다" 정도가 아니라, 실제로 EdgeX가 사용하는 Redis 내부에도 이미지 배열 데이터가 저장되어 있는 것을 확인했다.

4.4. 저장된 데이터를 다시 이미지로 복원

지금까지는

이미지
 ↓
배열
 ↓
EdgeX
 ↓
Redis

까지 저장되는 것을 확인했다.

그렇다면 반대로 Redis/Core Data에 저장된 배열을 다시 이미지로 만들었을 때 원본 이미지와 동일할까?

이를 확인하기 위해 Core Data에서 objectValue.image를 가져와 NumPy 배열로 변환했다.

x = next(
    v['objectValue']['image']
    for v in r['readings']
    if v.get('objectValue', {}).get('image') is not None
)

a = np.array(x, dtype=np.uint8)

Image.fromarray(a).save(
    '/home/namseungjoo/edgex-image-to-array/reconstructed.png'
)

복원 결과:

배열 shape: (701, 844, 4)
dtype: uint8
복원 이미지: reconstructed.png

원본과 동일한 크기와 데이터 형식으로 복원되었다.

4.5. 원본 이미지와 픽셀 단위 비교

마지막으로 단순히 이미지가 열리는 것만 확인하지 않고, 원본과 복원된 이미지를 픽셀 단위로 비교했다.

np.array_equal(a, b)

를 이용해 비교했다.

결과는:

shape 동일: True
픽셀 완전 동일: True
최대 픽셀 차이: 0

이었다.

이 결과는 매우 중요하다.

단순히 이미지 파일이 생성된 것이 아니라,

원본 이미지
     ↓
3차원 배열
     ↓
EdgeX Core Data
     ↓
Redis
     ↓
다시 조회
     ↓
이미지 복원

이라는 과정을 거쳤음에도 모든 픽셀 값이 원본과 완전히 동일했다는 의미이다.

따라서 이번 실습에서는 이미지 데이터가 저장되는 것뿐만 아니라 저장된 데이터를 다시 원래 이미지로 복원할 수 있을 정도로 온전하게 전달되었음을 확인했다.


5. 기존 방법과 제안한 방법의 차이점

이번 실습에서 가장 중요한 차이를 정리하면 다음과 같다.

구분Binary 방식이미지 배열 방식
이미지 전달가능가능
Core Data 저장가능가능
Core Service에서 실제 값 확인어려움가능
데이터 구조 확인제한적가능
데이터 가공제한적가능
Object Detection 등 후처리추가 처리 필요배열을 이용해 가공 가능
확장성상대적으로 낮음높음

PDF에서도 Binary 방식은 Core Service에서 value를 직접 확인할 수 없지만, 제안한 방식은 Core Service에서 데이터를 확인하고 사용할 수 있다는 점을 장점으로 설명한다. 또한 Raw 데이터 형태로 저장하거나 Object Detection API에 맞게 가공하는 등 다양한 방식으로 활용할 수 있다는 점을 제시한다.

결국 두 방식의 가장 큰 차이는 EdgeX가 이미지 데이터를 단순히 "저장되어 있는 데이터"로 취급하느냐, 아니면 실제 값을 조회하고 가공할 수 있는 데이터로 취급할 수 있느냐이다.


마무리

이번 실습을 통해 처음에는 단순히 USB 카메라를 EdgeX에 연결하는 것이라고 생각했지만, 실제로는 다음과 같은 구조를 이해할 수 있었다.

                    ┌─ RTSP → 외부 영상 처리
USB Camera
     ↓
USB Camera Device Service
     ↓
카메라 관리 / 스트리밍
     ↓
Image Data
     ↓
Image → 3D Array
     ↓
Device REST
     ↓
EdgeX Core Data
     ↓
Redis

특히 중요한 것은 USB Camera Device Service 자체가 촬영된 이미지 데이터를 EdgeX Core Data에 저장하는 서비스는 아니라는 점이었다. 이 서비스는 카메라 장치를 관리하고 RTSP 스트리밍 주소를 제공하는 역할을 한다.

따라서 이미지 데이터를 EdgeX 내부에서 관리하려면 별도의 데이터 전달 과정이 필요하다.

이번 실습에서는 그 방법으로 이미지를 3차원 배열로 변환한 뒤 Device REST를 통해 EdgeX Core Data에 전달하는 방법을 직접 구현해보았다.

그리고 마지막으로 저장된 데이터를 다시 이미지로 복원하고 원본과 비교한 결과,

shape 동일: True
픽셀 완전 동일: True
최대 픽셀 차이: 0

라는 결과를 얻었다.

이를 통해 이미지 → 배열 → EdgeX → Redis → 배열 → 이미지의 전체 과정에서 데이터가 손실되지 않았음을 직접 확인할 수 있었다.

0개의 댓글