[하이브리드 항법 시스템: 제6편] [구현-2단계] 중간 보정 단계: OpenCV/YOLO 지형지물 대조 및 Digital Twin 테스트

안녕하세요! 자율비행 무인기 시스템의 최전선에서 밤낮없이 연구에 매진하고 계신 대학원생 및 연구원 여러분. QUAD 드론연구소입니다.

지난 5편에서는 적의 GPS 재밍이나 도심 빌딩 숲과 같은 GNSS 거부(GNSS-Denied) 환경에서 드론이 추락하지 않고 생존할 수 있도록 해주는 PX4의 Dead-Reckoning(추측 항법) 모드에 대해 알아보았습니다. 관성측정장치(IMU)와 시각적 오도메트리(VIO)를 활용한 추측 항법은 짧은 시간 동안 매우 안정적인 비행을 보장합니다. 하지만 시간이 지남에 따라 센서의 오차가 누적되어 위치가 서서히 발산하는 드리프트(Drift) 현상을 근본적으로 피할 수는 없습니다.

수백 킬로미터를 날아가야 하는 자폭 드론이나 장거리 정찰 무인기가 목표물에 정확히 도달하기 위해서는, 비행 중간중간 누적된 오차를 초기화(Reset)해 주는 ‘중간 보정(Intermediate Correction)’ 단계가 필수적입니다.

오늘 제6편에서는 과거 순항 미사일에 사용되던 TERCOM(지형 대조 항법)의 개념을 현대적으로 계승하여, OpenCV와 YOLO 딥러닝 모델을 활용한 ‘절대 시각 위치 결정(Absolute Visual Localization, AVL)’ 기술을 구현하는 방법과, 이를 Digital Twin(디지털 트윈) 환경에서 안전하게 테스트하는 기법을 심도 있게 다루어 보겠습니다.


1. 절대 시각 위치 결정(AVL)과 지형 매칭의 진화

과거 1970년대의 순항 미사일들은 레이더 고도계를 이용해 지형의 높낮이를 측정하고, 이를 기체에 저장된 디지털 고도 모델(DEM)과 대조하는 TERCOM(Terrain Contour Matching) 시스템을 사용했습니다. 하지만 현대의 무인기 작전에서는 적에게 탐지될 위험이 있는 능동형 레이더 대신, 전력 소모가 적고 전파를 방사하지 않는 수동형 카메라(Vision) 기반의 항법이 대세를 이루고 있습니다.

이렇게 온보드 카메라를 통해 촬영된 하향(Downward) 영상 내의 지형지물을 위성 지도나 항공 사진과 같은 지리 태그(Geo-tag)가 부착된 참조 맵과 직접 대조하여 항공기의 정확한 위도와 경도를 산출하는 기술을 **절대 시각 위치 결정(AVL)**이라고 부릅니다. AVL은 VIO와 같은 상대 시각 위치 결정(RVL)이 가지는 누적 오차(Drift)를 원천적으로 차단하며, 각 프레임마다 독립적인 절대 좌표를 제공하는 강력한 기술입니다.


2. 중간 보정을 위한 두 가지 핵심 기술: OpenCV vs YOLO

무인기에서 지형지물을 매칭하여 오차를 계산하는 방법은 크게 전통적인 영상 처리 방식과 최신 딥러닝 기반 방식으로 나뉩니다. 각 임무 환경과 가용 컴퓨팅 자원에 맞게 적절한 기술을 선택하거나 융합해야 합니다.

📍 Approach A: OpenCV 템플릿 매칭 (Template Matching)

템플릿 매칭은 전체 참조 위성 지도(Source Image) 위에서 UAV가 촬영한 실시간 영상(Template Image)을 슬라이딩 윈도우 방식으로 이동시키며 가장 유사한 위치를 찾는 고전적이고 직관적인 방식입니다.

  • 장점: 구현이 매우 간단하며, 컴퓨팅 자원을 적게 소모합니다. 복잡한 학습 과정 없이 즉시 적용이 가능합니다.
  • 구현 원리: OpenCV의 cv2.matchTemplate() 함수를 사용하며, 조명 변화나 약간의 노이즈에 강인한 정규화된 상관계수(Normalized Cross-Correlation, TM_CCOEFF_NORMED) 방식을 주로 채택합니다.
  • 한계: UAV의 고도 변화에 따른 영상의 스케일(Scale) 변화나, 기체 회전에 따른 뷰 앵글의 변화에 매우 취약합니다. 따라서 드론이 정확히 일정한 고도와 헤딩(Heading)을 유지하며 비행하는 고정익기의 체크포인트(Checkpoint) 통과 확인에 적합합니다.

📍 Approach B: YOLO 기반 지형 랜드마크 인식 (Landmark Detection)

사전 입력된 위성 사진은 수년 전에 촬영되었거나 특정 계절의 모습만 반영하는 반면, 실시간 UAV 영상은 그림자, 날씨, 식생 변화 등에 민감하게 반응하여 두 도메인 간의 격차(Domain Gap)가 매우 큽니다. 이러한 교차 뷰(Cross-View) 환경의 한계를 극복하기 위해 객체 탐지 알고리즘인 **YOLO(You Only Look Once)**가 도입되었습니다.

  • 특징: YOLOv8, YOLO11, 그리고 최신 YOLO26에 이르는 모델들은 엣지 디바이스(NVIDIA Jetson 등)에서도 실시간(Real-time)으로 동작할 수 있도록 최적화되어 있습니다.
  • 동작 원리: 계절이나 조명에 따라 모습이 변하는 숲이나 강물 대신, 형태가 고정적인 저층/고층 건물, 교차로, 교량, 체육관 등 16가지 불변의 기하학적 랜드마크를 학습시켜 탐지합니다.
  • 장점: 크기 변화와 회전에 강인하며, 탐지된 여러 랜드마크 객체들 사이의 상대적 배치 구조(위상)를 바탕으로 그래프 매칭(Graph Matching)이나 PnP 알고리즘을 수행하면, 개별 객체의 모습이 변형되더라도 매우 정확한 6자유도(6-DoF) 위치 및 자세를 추정할 수 있습니다.

3. Digital Twin을 활용한 시뮬레이션 기반 개발 (SITL)

이러한 복잡한 AVL 시스템을 실제 비행에 바로 적용하는 것은 추락의 위험이 따릅니다. 따라서 로보틱스 개발 과정에서는 배포될 실제 작전 지역과 최대한 유사한 디지털 트윈(Digital Twin) 환경을 구축하여 사전 검증하는 것이 필수적입니다.

우리는 Gazebo 또는 FlightForge와 같은 시뮬레이터를 활용하여 대상 지역의 3D 환경을 생성할 수 있습니다.

  1. 지형 생성: 오픈 소스 디지털 표고 모델(DEM)을 사용하여 실제와 동일한 고도와 굴곡을 가진 지형을 생성합니다.
  2. 텍스처 맵핑 및 객체 배치: 위성 이미지 텍스처를 입히고, LoD2(Level of Detail 2) 수준의 건물 모델을 절차적(Procedural)으로 생성하여 배치합니다.
  3. 검증: 구축된 3D 디지털 트윈 환경 안에서 UAV를 가상으로 비행시키며, 하향 카메라 센서 플러그인을 통해 합성 데이터(Synthetic Data)를 생성합니다. 이 데이터를 바탕으로 우리의 OpenCV 템플릿 매칭이나 YOLO 모델이 제대로 작동하는지, 오차 계산 결과가 PX4의 위치 보정 명령으로 정확히 들어가는지를 검증합니다.

4. [Python 실전 예제] 중간 보정 위치 오차 계산기

다음은 ROS2 환경을 가정하여 온보드 컴퓨터(예: Jetson Orin)에서 실행될 수 있는 Python 예제 코드입니다. 이 스크립트는 UAV의 실시간 영상과 **사전 탑재된 위성 지도(체크포인트)**를 입력받아, OpenCV 템플릿 매칭 방식과 YOLO 객체 탐지 방식을 융합하여 기체의 현재 픽셀 오차를 계산하는 원리를 보여줍니다.

Python
import asyncio
import cv2
import numpy as np
from ultralytics import YOLO
from mavsdk import System
from mavsdk.offboard import VelocityBodyYawspeed, OffboardError

# YOLOv8 경량 모델 로드 (COCO 데이터셋 사전 학습)
model = YOLO('yolov8n.pt') 

# 카메라 해상도 및 중심점 설정 (Gazebo 기본 카메라 해상도 640x480 가정)
CAM_WIDTH = 640
CAM_HEIGHT = 480
CENTER_X = CAM_WIDTH // 2
CENTER_Y = CAM_HEIGHT // 2

# 서보잉 P 제어기 게인값
Kp = 0.005 

async def run():
    drone = System()
    await drone.connect(system_address="udp://:14540")
    print("드론 연결 대기 중...")
    async for state in drone.core.connection_state():
        if state.is_connected:
            print("드론 연결 완료!")
            break

    # 이륙 준비
    await drone.action.arm()
    await drone.action.takeoff()
    await asyncio.sleep(8) # 고도 상승 대기

    # Offboard 모드 초기화 (반드시 시작 전에 셋포인트를 한 번 보내야 함)
    print("Offboard 모드 전환 준비...")
    await drone.offboard.set_velocity_body(
        VelocityBodyYawspeed(0.0, 0.0, 0.0, 0.0)
    )
    try:
        await drone.offboard.start()
        print("🚀 Offboard 모드 활성화! 타겟 탐색 시작.")
    except OffboardError as error:
        print(f"Offboard 모드 시작 실패: {error}")
        return

    # Gazebo UDP 비디오 스트림 수신 (GStreamer 파이프라인 사용)
    # 환경에 따라 "udp://127.0.0.1:5600" 로 직접 열릴 수도 있습니다.
    gst_pipeline = 'udpsrc port=5600 ! application/x-rtp, payload=96 ! rtpjitterbuffer ! rtph264depay ! avdec_h264 ! videoconvert ! appsink'
    cap = cv2.VideoCapture(gst_pipeline, cv2.CAP_GSTREAMER)

    while True:
        ret, frame = cap.read()
        if not ret:
            print("비디오 스트림을 받을 수 없습니다.")
            await asyncio.sleep(0.1)
            continue

        # YOLO 객체 탐지 수행
        results = model.predict(frame, conf=0.5, verbose=False)
        target_found = False

        for result in results:
            for box in result.boxes:
                # class 47이 사과(apple), 혹은 스포츠공(32) 등으로 인식될 수 있음. 
                # 테스트 시 가장 잘 잡히는 클래스 ID나 전체 객체를 대상으로 할 수 있습니다.
                cls_id = int(box.cls)
                
                # 테스트 편의상 화면에서 탐지된 가장 큰 객체를 타겟으로 지정
                x1, y1, x2, y2 = map(int, box.xyxy)
                obj_center_x = (x1 + x2) // 2
                obj_center_y = (y1 + y2) // 2
                
                # 픽셀 오차 계산 (화면 중심 - 객체 중심)
                err_x = obj_center_x - CENTER_X
                err_y = obj_center_y - CENTER_Y

                # 비주얼 서보잉 제어 법칙 (FRD 프레임: Forward, Right, Down)
                v_forward = 3.0  # 타겟을 향해 3m/s로 지속 전진
                v_right = err_x * Kp  # 오차가 양수(오른쪽)면 오른쪽으로 이동
                v_down = err_y * Kp   # 오차가 양수(아래쪽)면 고도 하강

                # 속도 제한 (클램핑)
                v_right = np.clip(v_right, -1.5, 1.5)
                v_down = np.clip(v_down, -1.0, 1.0)

                # PX4로 속도 명령 전송
                await drone.offboard.set_velocity_body(
                    VelocityBodyYawspeed(v_forward, v_right, v_down, 0.0)
                )
                
                # 시각화 박스 및 조준선 그리기
                cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2)
                cv2.line(frame, (CENTER_X, CENTER_Y), (obj_center_x, obj_center_y), (0, 0, 255), 2)
                target_found = True
                break # 첫 번째 타겟만 추적
            
            if target_found:
                break

        if not target_found:
            # 타겟을 놓쳤을 경우 제자리 호버링
            await drone.offboard.set_velocity_body(
                VelocityBodyYawspeed(0.0, 0.0, 0.0, 0.0)
            )

        cv2.imshow("Visual Servoing - Target Tracking", frame)
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break

    # 종료 시 착륙
    await drone.offboard.stop()
    await drone.action.land()
    cap.release()
    cv2.destroyAllWindows()

if __name__ == "__main__":
    asyncio.run(run())

[코드 설명]

  1. match_by_template: 실시간 드론 영상(uav_frame)을 위성 지도 내에서 슬라이딩하며 가장 유사도가 높은 지점을 cv2.TM_CCOEFF_NORMED로 찾아냅니다.
  2. detect_landmarks: 계절이나 조명 변화로 템플릿 매칭이 실패할 경우를 대비하여, 구조적 불변성을 지닌 건물 등의 랜드마크를 YOLO로 탐지합니다.
  3. calculate_position_error: 기체가 스스로 추정하던 예상 좌표(expected)와 실제 영상으로 매칭된 좌표(matched) 간의 편차(Error)를 구합니다. 이 픽셀 오차에 카메라의 GSD(Ground Sample Distance)를 곱하면 실제 미터(m) 단위의 물리적 오차를 도출할 수 있으며, 이 값을 ROS 2 브릿지를 통해 PX4로 전달하여 궤적을 수정합니다.

저희 QUAD 드론 연구소 📺YouTube 채널 멤버쉽 [💎다이아몬드 회원]으로 가입하시면 풀 테스트를 위한 모든 교재와 소스를 받아 보실 수 있습니다.


5. Gazebo 시뮬레이터 테스트

전체 시뮬레이션 및 비주얼 보정 알고리즘을 테스트하기 위한 단계별 방법입니다.

이 테스트는 총 4개의 터미널을 각각 띄워 실행해야 하며, 순서대로 진행해 주시면 됩니다.

1단계. Micro-XRCE-DDS-Agent 실행 (터미널 1)

PX4 Autopilot의 uORB 메시지를 ROS 2와 통신할 수 있도록 다리 역할을 해 주는 에이전트를 먼저 실행합니다.

Bash
MicroXRCEAgent udp4 -p 8888

2단계. PX4 SITL 및 Gazebo 시뮬레이터 실행 (터미널 2)

하향 카메라와 VIO 센서가 부착된 드론 모델(x500_vio_cam_down)과 비주얼 보정용 빨간 사과 랜드마크가 배치되어 있는 baylands 월드를 실행합니다.

Bash
cd ~/PX4-Autopilot-1.17.0-HybridNav
make px4_sitl gz_x500_vio_cam_down_baylands

주의: 이 노드는 /world/baylands/... 월드 이름 기반의 이미지 토픽을 구독하므로 반드시 뒤에 _baylands를 붙여 해당 월드를 실행해야 합니다.

3단계. Gazebo ➡️ ROS 2 카메라 이미지 브릿지 실행 (터미널 3)

Gazebo 시뮬레이션 내의 하향 카메라 이미지 데이터를 ROS 2 노드가 읽을 수 있는 형태로 변환(Bridge)해 줍니다.

Bash
ros2 run ros_gz_bridge parameter_bridge /world/baylands/model/x500_vio_cam_down_0/link/camera_link/sensor/imager/image@sensor_msgs/msg/Image[gz.msgs.Image

4단계. 비주얼 보정 ROS 2 노드 빌드 및 실행 (터미널 4)

ROS 2 워크스페이스를 빌드하고 제어 및 비주얼 매칭 노드를 실행합니다.

Bash
cd ~/ros2_wscolcon build --symlink-install
source install/setup.bash

# OpenCV 템플릿 매칭 방식(Approach A)으로 실행
ros2 run hybrid_nav visual_correction_node --ros-args -p approach:=A

(만약 객체 탐색용 YOLO 모델(Approach B)로 구동을 테스트하고 싶다면 뒤에 -p approach:=B 옵션을 주어 실행하면 됩니다. 단, 인터넷이 연결된 환경에서 필요한 딥러닝 가중치 파일 yolov8n.pt가 다운로드됩니다.)

5단계. 하방 카메라와 비주얼 매칭 상태 보기 (터미널 5)

Bash
ros2 run rqt_image_view rqt_image_view

💡 노드 작동 순서 및 확인 포인트

노드가 실행되면 자동으로 비행 상태 머신이 아래 순서로 작동하게 됩니다.

  1. TAKEOFF: 드론이 고도 100m까지 상승합니다.
  2. FLY_NORTH: 고도 100m에 도달하면 북쪽으로 비행(8m/s)을 시작합니다.
  3. GPS Jamming (자동): 비행 시작 7.5초 후, 노드가 MAVLink를 통해 자동으로 GPS 차단(SIM_GPS_USED=0)을 유입시킵니다. 이때 드론은 VIO 센서에만 의존해 데드 레코닝(Dead-reckoning) 모드로 비행을 유지하게 됩니다.

마무리하며

이번 6편에서는 통신이 끊긴 깊은 적진이나 도심지에서도 드론이 길을 잃지 않도록, OpenCV와 YOLO를 융합하여 영상 기반의 절대 좌표를 산출하는 ‘중간 보정(Visual Checkpoint)’ 기술에 대해 깊이 있게 알아보았습니다.

GNSS에 의존하지 않는 진정한 자율성은 단순한 비행 제어를 넘어, 기체 스스로 눈을 뜨고 세상을 인지하여 지도를 읽어내는 수준에 도달했을 때 비로소 완성됩니다. 디지털 트윈 환경에서의 철저한 시뮬레이션을 통해 연구원님의 알고리즘이 완벽히 검증되기를 바랍니다.

다음 제7편에서는 하이브리드 항법의 화룡점정이자 자폭 드론의 가장 중요한 마지막 관문, 목표물을 시각적으로 포착하여 초정밀 타격으로 유도하는 **’종말 단계: 비주얼 서보잉(Visual Servoing)’**에 대해 다루어 보겠습니다. 알고리즘 설계나 디지털 트윈 연동 과정에서 궁금한 점이 있으시다면 언제든 댓글을 남겨주세요. 감사합니다!


YouTube 강좌

Author: maponarooo, CEO of QUAD Drone Lab

Date: Jun 09, 2026

Similar Posts

답글 남기기