지난 주는 고민으로 가득한 시도뿐이라 기록할 것이 별로 없었습니다. 그래도 21주차에는 눈에 보이는 성과가 있어서 너무나도 도파민이 뿜뿜했던 기억이 있습니다.

그러면 바로 구현 내용으로 들어가보도록 하겠습니다.

RFID 부정승차 감지 레이저 트래킹 시스템 개발기

카메라 캘리브레이션부터 RBF 매핑, Z=1200mm 평면 설계, DeepSORT 트래킹까지


목차

  1. Z=1200mm 타겟 평면 설계
  2. 캘리브레이션 및 RBF 매핑
  3. bbox 오차 문제와 DeepSORT 도입 배경
  4. 앞으로 구현할 내용

💡 핵심 흐름
RFID 태깅 감지 → 해당 인물의 신체 좌표 추출 → 레이저로 1200mm 높이 조준 → 관리자 확인

하드웨어 구성

구성 요소모델/사양역할
IP 카메라ONVIF 지원 (1920×1080)RTSP 영상 + 메타데이터
RFID 리더기TCP 소켓 연결태깅 감지 → person_id 전달
PCIntel Xe 내장 GPU영상처리, RBF 연산, 관리자 UI
ESP8266WiFi 모듈UDP 수신 → UART → STM32
STM32ARM Cortex-M4PWM 생성 → 팬틸트 서보 제어
서보 모터HDS-2288 × 2팬(좌우) + 틸트(상하) 2축
레이저포인터 모듈1200mm 평면 조준

전체 시스템 흐름

RFID 태깅
  → TCP 수신 → person_id 확정
  → RFID 리더기 위치 픽셀(rfid_v) 기준 bbox ratio 계산

매 프레임
  → ONVIF bbox 수신 → 칼만 필터 노이즈 제거
  → target_u = bbox_cx
  → target_v = bbox_top + bbox_height × ratio
  → RBF eval(u, v) → pan_pwm, tilt_pwm
  → UDP → ESP8266 → UART → STM32 → 서보 구동

1. Z=1200mm 타겟 평면 설계

1.1 깊이 추정 문제

레이저가 실제로 가리켜야 할 3D 좌표를 구하려면 깊이(Z) 정보가 필요하다. 그런데 단안 카메라만으로는 깊이를 직접 측정할 수 없다. 처음에는 다음과 같은 방법들을 검토했다.

방법문제점
사람 키 + 픽셀 비율로 추정키가 다르고, 가려지면 bbox가 잘려 비율이 틀어짐
바닥면(Z=0) 교차카메라 틸트각 정밀 측정 필요, 교실 환경 내에서 책상이나 의자에 가려지면 추정 실패
스테레오 카메라 추가하드웨어 추가 비용 및 설치 복잡도
Z=1200mm 고정 평면키/거리 무관, 깊이 추정 불필요 ✅

1.2 1200mm 고정 평면의 핵심 아이디어

💡 사람의 최소 키를 120cm로 가정하면, 1200mm 높이는 어떤 사람이든 몸통에 해당한다. 이 평면을 고정 타겟으로 삼으면 거리/키에 관계없이 일관된 조준이 가능하다.

카메라가 천장 벽면 끝에 비스듬히 설치되어 있기 때문에, 실세계의 수평 평면(Z=1200mm)은 카메라 영상에서 직선이 아닌 곡선으로 보인다. RBF 캘리브레이션을 통해 이 평면이 영상에서 어떤 픽셀 위치에 해당하는지 역산할 수 있다.

1.3 RFID 태깅 기반 ratio 초기화

1200mm 평면의 영상 픽셀을 구하더라도, 사람의 bbox에서 그 위치가 상단 몇 %인지는 사람마다, 거리마다 다르다. 이를 해결하기 위해 RFID 태깅 순간을 활용했다.

# RFID 리더기 위치 = 실세계 정확히 Z=1200mm
# 태깅 순간 그 픽셀의 v 좌표 = rfid_v (사전 측정, 고정값)

# 태깅 순간 bbox에서의 비율 계산
ratio = (rfid_v - bbox_top) / bbox_height

# 이후 추적에서 동일 비율 적용
target_v = bbox_top + bbox_height * ratio

장점: 키가 달라도, 거리가 달라도 태깅 순간의 실측값으로 ratio가 결정되므로 추정 오차가 없다. RFID 리더기가 Re-ID보다 훨씬 정확한 '높이 캘리브레이션 도구'가 된다.


2. 캘리브레이션 및 RBF 매핑

2.1 실세계 캘리브레이션 포인트 측정

카메라 내부 파라미터(fx, fy, cx, cy)는 체커보드 캘리브레이션으로 사전에 구했다. 카메라 틸트각, 레이저 오프셋 등 나머지 미지수는 모두 현장 측정으로 한번에 흡수했다.

총 16개 막대를 바닥의 격자 위치에 세우고, 막대 1200mm 높이에 스티커를 붙인 뒤 다음 두 가지를 기록했다.

  • 카메라 화면에서 스티커의 픽셀 좌표 (u, v)
  • 키보드로 레이저를 수동 조준했을 때의 서보 PWM 값 (pan, tilt)
포인트X(cm)Y(cm)u(px)v(px)pan PWMtilt PWM
p0-37065804058901320
p70221160755016051235
p10-234056328701170
p150100153589815701065
.....................
(총 16개)

2.2 RBF (Thin-Plate Spline) 원리

16개 캘리브 포인트를 이용해 두 가지 RBF 보간 함수를 학습했다.

  • pixel(u, v)pan PWM
  • pixel(u, v)tilt PWM

RBF(Radial Basis Function)는 알고 있는 점들로부터의 거리를 기반으로 모르는 점을 추정하는 방법이다. Thin-Plate Spline 커널 φ(r) = r²·log(r) 을 사용하면 2D 픽셀 공간에서 가장 부드럽고 정확한 보간이 가능하다.

# 학습 (1회)
from scipy.interpolate import RBFInterpolator

rbf_pan  = RBFInterpolator(pixels, pwms[:,0],
                            kernel='thin_plate_spline')
rbf_tilt = RBFInterpolator(pixels, pwms[:,1],
                            kernel='thin_plate_spline')

# 추론 (매 프레임)
pan  = int(rbf_pan([[u, v]])[0])
tilt = int(rbf_tilt([[u, v]])[0])

카메라 파라미터, 서보 스펙, 레이저 오프셋을 아무것도 몰라도 된다. 그냥 "이 픽셀이면 이 PWM이더라"는 실측 데이터 16개만 있으면 끝이다.

2.3 교차 검증 결과

Leave-One-Out 교차검증으로 16개 포인트 각각을 제외하고 나머지 15개로 보간한 뒤 예측 오차를 측정했다.

항목pan 오차tilt 오차
평균 오차6.1 PWM7.4 PWM
최대 오차23.0 PWM34.0 PWM
각도 환산≈ 0.6°≈ 0.7°
실용 판정✅ 충분✅ 충분

📌 p15 포인트(X=0, Y=100) 추가 후 기존 p7의 tilt 오차가 146 PWM → 9 PWM으로 94% 감소했다. 데이터 분포가 고른 것이 정확도에 결정적 영향을 준다.

2.4 영상 내 1200mm 평면 격자 투영

세계좌표(X, Y) → 픽셀(u, v) 역방향 RBF도 함께 학습하여, 실세계 격자가 카메라 영상에서 어떻게 보이는지 실시간으로 오버레이했다.

# 실세계 격자 → 영상 투영
for X in range(-500, 10, 100):     # X 등간격 (cm)
    for Y in range(0, 1050, 100):  # Y 등간격 (cm)
        u = rbf_u.eval(X, Y)       # 세계→픽셀 RBF
        v = rbf_v.eval(X, Y)
        cv2.circle(frame, (int(u), int(v)), 3, (80, 220, 80), -1)

투영 결과, 실세계에서 수평인 격자가 영상에서는 카메라 시점에 따라 곡선으로 휘어져 보였다. 카메라가 비스듬히 설치되어 있기 때문에 당연한 현상이며, RBF가 이 왜곡을 자연스럽게 학습했음을 확인했다.

실제 평면 구현도


3. bbox 오차 문제와 DeepSORT 도입 배경

3.1 ONVIF bbox 노이즈

카메라 ONVIF 메타데이터로 받는 bbox는 카메라 내부 AI가 매 프레임 독립적으로 계산하기 때문에, 사람이 가만히 있어도 bbox가 흔들린다. 실측 결과는 충격적이었다.

구분측정값영향
pan 노이즈30 PWM레이저 좌우 떨림
tilt 노이즈52 PWM레이저 상하 떨림 (더 심각)
bbox 픽셀 오차약 104pxtilt 방향 RBF 민감도가 높음

3.2 Visual Tracker(CSRT/KCF)를 시도했지만...

bbox 노이즈 문제를 해결하기 위해 OpenCV의 CSRT, KCF 트래커를 시도했다. 초기화 이후 bbox 노이즈가 크게 줄어드는 장점이 있었지만 치명적인 문제들이 있었다.

  • 겹침 처리 불가: 두 사람이 겹치면 다른 사람으로 붙어버림
  • CSRT FPS 저하: 1920×1080 풀 해상도에서 겨우 6fps
  • 장시간 드리프트: 외형 변화에 따라 서서히 위치가 틀어짐

⚠️ Visual Tracker는 단기 추적에는 효과적이지만, 사람이 겹치거나 일시적으로 가려지는 실제 환경에서는 신뢰할 수 없다는 결론에 도달했다.

실제 추적 결과

😭

3.3 DeepSORT 경량 구현 방향

현업에서 가장 널리 쓰이는 DeepSORT(Detection + 칼만 필터 + Re-ID) 를 도입하기로 결정했다. 우리 시스템의 특성상 전체 DeepSORT를 구현할 필요는 없다.
DeepSORT 에 대해 잘 설명된 블로그를 첨부하며 DeepSORT에 대한 설명을 생략하도록 하겠다.

DeepSORT 구성요소필요 여부이유
Detection (ONVIF)✅ 이미 있음카메라 AI가 처리
칼만 필터✅ 필요bbox 노이즈 흡수
헝가리안 매칭✅ 필요겹칠 때 올바른 ID 유지
CNN Re-ID△ 선택색상 히스토그램으로 대체 가능
다중 트랙 관리△ 단순화RFID가 타겟 ID 확정

RFID가 이미 타겟 ID를 100% 정확하게 확정해주기 때문에, DeepSORT의 Re-ID 역할 대부분을 RFID가 대체한다. 결과적으로 칼만 필터 + 색상 기반 Re-ID + 헝가리안 매칭의 경량 구현으로 충분하다.


4. 앞으로 구현할 내용

4.1 MediaPipe 어깨 중심점 기반 몸 중심 탐지

현재 bbox 중앙을 타겟으로 사용하면 팔을 벌리거나 돌아서는 경우 bbox 중심이 실제 몸 중심과 달라지는 문제가 있다. MediaPipe Pose를 도입하여 어깨 keypoint를 직접 추출할 예정이다.

# MediaPipe 어깨 중심점 추출
mp_pose = mp.solutions.pose
pose = mp_pose.Pose(min_detection_confidence=0.5)

result = pose.process(roi_rgb)
lm = result.pose_landmarks.landmark

L_SH = lm[mp_pose.PoseLandmark.LEFT_SHOULDER]
R_SH = lm[mp_pose.PoseLandmark.RIGHT_SHOULDER]

# 가시성 기반 fallback 전략
if L_SH.visibility > 0.5 and R_SH.visibility > 0.5:
    # 1순위: 양쪽 어깨 중점
    u = (L_SH.x + R_SH.x) / 2 * img_w
    v = (L_SH.y + R_SH.y) / 2 * img_h
elif L_SH.visibility > 0.5:
    # 2순위: 한쪽 어깨
    u, v = L_SH.x * img_w, L_SH.y * img_h
else:
    # 3순위: bbox 상단 26% fallback
    u = (x1 + x2) / 2
    v = y1 + (y2 - y1) * 0.26

어깨 keypoint는 팔을 벌려도 변하지 않고, bbox 가로 크기 변화에 영향받지 않는다. 또한 RFID 태깅 순간 어깨 픽셀 위치와 1200mm 기준점의 오프셋 비율을 계산해두면, 이후 어깨가 가려져도 안정적으로 타겟을 유지할 수 있다.

4.2 DeepSORT 적용으로 트래킹 진동 제거

경량 DeepSORT를 구현하여 사람이 겹치거나 일시적으로 화면을 벗어나는 상황에서도 ID를 안정적으로 유지할 계획이다.

  • 칼만 필터: bbox 노이즈 흡수 및 가려짐 시 위치 예측 유지
  • 색상 히스토그램 Re-ID: HSV 히스토그램 기반 외형 매칭 (CPU만으로 578fps 가능)
  • 헝가리안 알고리즘: 다중 사람 겹침 시 최적 ID 매칭
  • RFID 트리거: 태깅 순간 트랙 ID와 person_id 연결

🔧 실용적 접근: deep-sort-realtime Python 라이브러리로 먼저 검증한 뒤, 검증이 완료되면 C++로 이식한다. ONVIF Detection이 이미 구현되어 있으므로 Re-ID + 칼만 + 헝가리안만 추가하면 된다.

4.3 STM32 코드 포팅 (RBF eval)

현재 PC에서 수행하는 RBF 연산(pixel → PWM)을 STM32로 이식하면, PC로부터 픽셀 좌표(u, v)만 받아도 서보를 직접 제어할 수 있다. 통신 부하가 줄어들고, PC 연결이 끊겨도 마지막 픽셀 기준으로 자체 동작이 가능해진다.

항목비고
RBF eval 연산량~262회 부동소수점pan + tilt 각 1회
예상 실행 시간~13μsSTM32F4 @ 168MHz
30fps 예산33,333μs여유 2,500배
메모리 사용량~432 bytesSRAM 192KB 중 0.2%
필요 구현eval() 함수만fit()은 PC에서 1회 계산
// STM32 RBF eval (C)
// 학습된 가중치는 PC에서 추출 후 const 배열로 저장
float rbf_eval(float u, float v,
               const float* W,
               float a0, float a1, float a2) {
    float s = a0 + a1*u + a2*v;
    for (int i = 0; i < 16; i++) {
        float du = u - CALIB_U[i];
        float dv = v - CALIB_V[i];
        float r  = sqrtf(du*du + dv*dv);
        s += W[i] * r * r * logf(r + 1e-6f);
    }
    return s;
}

// 사용: UART로 (u, v) 수신 후
float pan  = rbf_eval(u, v, PAN_W,  PAN_A0,  PAN_A1,  PAN_A2);
float tilt = rbf_eval(u, v, TILT_W, TILT_A0, TILT_A1, TILT_A2);
servo_set_pwm((int)pan, (int)tilt);

마치며

이 프로젝트에서 가장 어려웠던 부분은 "레이저가 실제로 어디를 가리켜야 하는가" 라는 좌표 설계 문제였다. 단안 카메라만으로 3D 좌표를 정확히 추정하는 것은 본질적으로 어렵다.

Z=1200mm 고정 평면 + RFID 태깅 기반 ratio 초기화 라는 조합은 이 문제를 하드웨어 제약을 활용해 우아하게 해결한 방법이었다. 카메라를 추가하거나 복잡한 알고리즘을 쓰지 않아도, 이미 존재하는 RFID 인프라가 3D 좌표 캘리브레이션 도구가 되었습니다.

앞으로 MediaPipe 어깨 탐지와 DeepSORT, STM32 포팅까지 완료되면, 하드웨어부터 임베디드까지 전 스택이 완성된 실시간 레이저 트래킹 시스템이 구축될 것 같습니다 .
두구두구두구 ///!!


Tags: Computer Vision Calibration RBF DeepSORT STM32 레이저 트래킹 RFID

profile
세상의 어려운 문제를 해결하자

4개의 댓글

comment-user-thumbnail
2026년 3월 23일

실시간 레이저 트래킹 시스템 정말 보고 싶네요

1개의 답글
comment-user-thumbnail
2026년 3월 23일

시각 자료가 아주 깔끔하네요

1개의 답글