고속 드론 레이싱 환경에서 VIO 드리프트의 비행 시간 의존성 분석 및 온라인 시간 동기화 보정 기법

TII-RATM-Drone-Racing 데이터셋 · OpenVINS 벤치마크 · 상관/편상관 분석 · English

이 페이지는 Google Colab 노트북 분석 결과를 정적 페이지로 옮긴 것입니다. 패키지 설치·런타임 재시작 셀은 생략했고, 각 셀의 코드는 "코드 보기"로 펼칠 수 있습니다. 표본 수가 n=6이므로 통계 결과는 경향성 수준으로 해석해야 합니다.

데이터셋 이름: TII-RATM-Drone-Racing

올린이: 컴퓨터 비전 전문 기업인 Voxel51

논문 연구

코드 보기 (셀 9)
import os
import fiftyone as fo
from huggingface_hub import snapshot_download

# 1. repo_type="dataset" 옵션을 추가하여 데이터셋 경로 지정 다운로드
print("데이터셋 다운로드 시작 (580MB 내외)...")
local_dir = snapshot_download(
    repo_id="Voxel51/TII-RATM-Drone-Racing",
    repo_type="dataset"
)
print(f"다운로드 완료! 로컬 경로: {local_dir}")

# 2. 로컬에 저장된 데이터셋을 FiftyOne으로 안전하게 임포트
dataset = fo.Dataset.from_dir(
    dataset_dir=local_dir,
    dataset_type=fo.types.FiftyOneDataset
)

# 3. 데이터셋 로드 성공 유무 및 구조 확인
print("\n--- 데이터셋 로드 성공 ---")
print(dataset)
데이터셋 다운로드 시작 (580MB 내외)...
--- 데이터셋 로드 성공 ---
Name:        2026.09.09.06.27.55.121976
Media type:  multimodal
Num samples: 6
Persistent:  False
Tags:        ['fiftyone', 'multimodal', 'mcap', 'drone', 'slam']
Sample fields:
    id:                       fiftyone.core.fields.ObjectIdField
    filepath:                 fiftyone.core.fields.StringField
    tags:                     fiftyone.core.fields.ListField(fiftyone.core.fields.StringField)
    metadata:                 fiftyone.core.fields.EmbeddedDocumentField(fiftyone.core.metadata.Metadata)
    created_at:               fiftyone.core.fields.DateTimeField
    last_modified_at:         fiftyone.core.fields.DateTimeField
    flight:                   fiftyone.core.fields.StringField
    track:                    fiftyone.core.fields.StringField
    duration:                 fiftyone.core.fields.FloatField
    distance_m:               fiftyone.core.fields.FloatField
    mean_speed_ms:            fiftyone.core.fields.FloatField
    max_speed_ms:             fiftyone.core.fields.FloatField
    num_camera_frames:        fiftyone.core.fields.IntField
    num_feature_track_frames: fiftyone.core.fields.IntField
    num_imu_samples:          fiftyone.core.fields.IntField
    num_vio_poses:            fiftyone.core.fields.IntField
    num_ground_truth_poses:   fiftyone.core.fields.IntField
    num_gates:                fiftyone.core.fields.IntField
    tracking_error_rmse_m:    fiftyone.core.fields.FloatField
    tracking_error_median_m:  fiftyone.core.fields.FloatField
    tracking_error_max_m:     fiftyone.core.fields.FloatField
    tracking_error_final_m:   fiftyone.core.fields.FloatField
코드 보기 (셀 11)
# 1. 데이터셋에 들어있는 샘플 하나를 골라 구조 분석하기
sample = dataset.first()

print("--- [1] 샘플의 기본 필드 목록 ---")
print(sample.field_names)

print("\n--- [2] 상세 데이터 맵 스키마 구조 ---")
# FiftyOne 내부에 정의된 실제 필드 설계도 출력
for field_name in sample.field_names:
    print(f"필드명: {field_name} -> 데이터 타입: {type(sample[field_name])}")
--- [1] 샘플의 기본 필드 목록 ---
('id', 'filepath', 'tags', 'metadata', 'created_at', 'last_modified_at', 'flight', 'track', 'duration', 'distance_m', 'mean_speed_ms', 'max_speed_ms', 'num_camera_frames', 'num_feature_track_frames', 'num_imu_samples', 'num_vio_poses', 'num_ground_truth_poses', 'num_gates', 'tracking_error_rmse_m', 'tracking_error_median_m', 'tracking_error_max_m', 'tracking_error_final_m')

--- [2] 상세 데이터 맵 스키마 구조 ---
필드명: id -> 데이터 타입: <class 'str'>
필드명: filepath -> 데이터 타입: <class 'str'>
필드명: tags -> 데이터 타입: <class 'mongoengine.base.datastructures.BaseList'>
필드명: metadata -> 데이터 타입: <class 'NoneType'>
필드명: created_at -> 데이터 타입: <class 'datetime.datetime'>
필드명: last_modified_at -> 데이터 타입: <class 'datetime.datetime'>
필드명: flight -> 데이터 타입: <class 'str'>
필드명: track -> 데이터 타입: <class 'str'>
필드명: duration -> 데이터 타입: <class 'float'>
필드명: distance_m -> 데이터 타입: <class 'float'>
필드명: mean_speed_ms -> 데이터 타입: <class 'float'>
필드명: max_speed_ms -> 데이터 타입: <class 'float'>
필드명: num_camera_frames -> 데이터 타입: <class 'int'>
필드명: num_feature_track_frames -> 데이터 타입: <class 'int'>
필드명: num_imu_samples -> 데이터 타입: <class 'int'>
필드명: num_vio_poses -> 데이터 타입: <class 'int'>
필드명: num_ground_truth_poses -> 데이터 타입: <class 'int'>
필드명: num_gates -> 데이터 타입: <class 'int'>
필드명: tracking_error_rmse_m -> 데이터 타입: <class 'float'>
필드명: tracking_error_median_m -> 데이터 타입: <class 'float'>
필드명: tracking_error_max_m -> 데이터 타입: <class 'float'>
필드명: tracking_error_final_m -> 데이터 타입: <class 'float'>
코드 보기 (셀 12)
import pandas as pd

# 데이터셋의 모든 에피소드를 리스트로 추출하여 테이블(DataFrame)화 준비
summary_data = []

print("🚀 TII-RATM-Drone-Racing 데이터셋 기반 알고리즘 벤치마킹 리포트 생성 중...\n")

for i, sample in enumerate(dataset):
    # 각 비행 샘플에서 검증 수치 및 특징 메타데이터 가져오기
    track_type = getattr(sample, "track", "N/A")
    flight_id = getattr(sample, "flight", f"Episode {i+1}")
    distance = getattr(sample, "distance_m", 0.0)
    duration = getattr(sample, "duration", 0.0)
    mean_speed = getattr(sample, "mean_speed_ms", 0.0)

    # 이 데이터셋의 핵심: 미리 계산된 OpenVINS/SLAM 추정 오차(RMSE 등) 지표 추출
    rmse_err = getattr(sample, "tracking_error_rmse_m", None)
    max_err = getattr(sample, "tracking_error_max_m", None)
    median_err = getattr(sample, "tracking_error_median_m", None)
    final_err = getattr(sample, "tracking_error_final_m", None)

    summary_data.append({
        "비행 ID": flight_id,
        "트랙 종류": track_type,
        "비행 거리 (m)": round(distance, 2) if distance else "N/A",
        "비행 시간 (초)": round(duration, 2) if duration else "N/A",
        "평균 속도 (m/s)": round(mean_speed, 2) if mean_speed else "N/A",
        "평균 오차 (RMSE, m)": round(rmse_err, 4) if rmse_err is not None else "N/A",
        "최대 오차 (Max, m)": round(max_err, 4) if max_err is not None else "N/A",
        "중앙값 오차 (m)": round(median_err, 4) if median_err is not None else "N/A",
        "최종 지점 오차 (m)": round(final_err, 4) if final_err is not None else "N/A"
    })

# Pandas 데이터프레임으로 변환하여 표 형태로 가독성 있게 출력
df_report = pd.DataFrame(summary_data)

# 데이터프레임 예쁘게 인쇄하기
from IPython.display import display, HTML
print("📊 [SLAM / OpenVINS 알고리즘 정밀도 검증 결과 테이블]")
display(df_report)

# 분석 요약 멘트 출력
print("\n💡 분석 의견:")
if not df_report.empty and df_report["평균 오차 (RMSE, m)"].iloc[0] != "N/A":
    avg_rmse = df_report["평균 오차 (RMSE, m)"].mean()
    print(f"1. 로드된 비행 세션 전반에서 OpenVINS 알고리즘은 안정적인 추정 성능을 보여주고 있습니다.")
    print(f"2. 전체 에피소드의 평균적인 위치 추정 오차(RMSE)는 약 {avg_rmse:.4f}m 수준입니다.")
else:
    print("데이터셋 메타데이터 테이블 로드 완료. FiftyOne 대시보드 화면과 교차 검증을 진행해 주세요.")
🚀 TII-RATM-Drone-Racing 데이터셋 기반 알고리즘 벤치마킹 리포트 생성 중...

📊 [SLAM / OpenVINS 알고리즘 정밀도 검증 결과 테이블]
비행 ID 트랙 종류 비행 거리 (m) 비행 시간 (초) 평균 속도 (m/s) 평균 오차 (RMSE, m) 최대 오차 (Max, m) 중앙값 오차 (m) 최종 지점 오차 (m)
0 flight-01p-ellipse ellipse 479.27 118.85 4.03 1.0719 3.4820 0.9018 3.4820
1 flight-02p-ellipse ellipse 446.16 106.84 4.18 0.6343 3.4555 0.4333 3.4555
2 flight-03p-ellipse ellipse 379.71 102.72 3.70 0.6123 2.3495 0.3983 0.6712
3 flight-07p-lemniscate lemniscate 444.32 125.67 3.54 1.6563 3.7836 1.4195 3.4664
4 flight-08p-lemniscate lemniscate 367.14 96.08 3.82 0.6742 2.1939 0.5008 2.1939
5 flight-09p-lemniscate lemniscate 447.09 101.63 4.40 0.6417 1.6992 0.5586 1.6992
💡 분석 의견:
1. 로드된 비행 세션 전반에서 OpenVINS 알고리즘은 안정적인 추정 성능을 보여주고 있습니다.
2. 전체 에피소드의 평균적인 위치 추정 오차(RMSE)는 약 0.8818m 수준입니다.
코드 보기 (셀 14)
import os
import pandas as pd
import numpy as np
import fiftyone as fo
from huggingface_hub import snapshot_download
from IPython.display import display, HTML

# [단계 1] 원격 호환성 버그 우회를 위한 로컬 직접 다운로드
print("1. Hugging Face 허브에서 데이터셋 직접 안전 다운로드 중 (580MB 내외)...")
local_dir = snapshot_download(
    repo_id="Voxel51/TII-RATM-Drone-Racing",
    repo_type="dataset"
)
print(f"   - 다운로드 완료! 경로: {local_dir}\n")

# [단계 2] FiftyOne 로컬 임포트 (버그 단계 패스)
print("2. FiftyOne 데이터셋 엔진에 로드 중...")
dataset = fo.Dataset.from_dir(
    dataset_dir=local_dir,
    dataset_type=fo.types.FiftyOneDataset
)
print("   - 데이터셋 로드 성공!\n")

# [단계 3] 알고리즘 벤치마킹 데이터 추출 및 데이터프레임 생성
print("3. 🚀 OpenVINS 자율주행 알고리즘 검증 리포트 생성 중...\n")
summary_data = []

for i, sample in enumerate(dataset):
    track_type = getattr(sample, "track", "N/A")
    flight_id = getattr(sample, "flight", f"Episode {i+1}")
    distance = getattr(sample, "distance_m", 0.0)
    duration = getattr(sample, "duration", 0.0)
    mean_speed = getattr(sample, "mean_speed_ms", 0.0)

    # 데이터셋 내부에 이미 완벽하게 연산되어 저장된 OpenVINS 추정 오차 지표 추출
    rmse_err = getattr(sample, "tracking_error_rmse_m", None)
    max_err = getattr(sample, "tracking_error_max_m", None)
    median_err = getattr(sample, "tracking_error_median_m", None)
    final_err = getattr(sample, "tracking_error_final_m", None)

    summary_data.append({
        "비행 ID": flight_id,
        "트랙 종류": track_type,
        "비행 거리 (m)": round(distance, 2) if distance else "N/A",
        "비행 시간 (초)": round(duration, 2) if duration else "N/A",
        "평균 속도 (m/s)": round(mean_speed, 2) if mean_speed else "N/A",
        "평균 오차 (RMSE, m)": round(rmse_err, 4) if rmse_err is not None else "N/A",
        "최대 오차 (Max, m)": round(max_err, 4) if max_err is not None else "N/A",
        "중앙값 오차 (m)": round(median_err, 4) if median_err is not None else "N/A",
        "최종 지점 오차 (m)": round(final_err, 4) if final_err is not None else "N/A"
    })

df_report = pd.DataFrame(summary_data)

# [단계 4] 주피터 노트북 형식으로 표 출력
print("📊 [SLAM / OpenVINS 알고리즘 정밀도 검증 결과 테이블]")
display(df_report)
1. Hugging Face 허브에서 데이터셋 직접 안전 다운로드 중 (580MB 내외)...
2. FiftyOne 데이터셋 엔진에 로드 중...
- 데이터셋 로드 성공!

3. 🚀 OpenVINS 자율주행 알고리즘 검증 리포트 생성 중...

📊 [SLAM / OpenVINS 알고리즘 정밀도 검증 결과 테이블]
비행 ID 트랙 종류 비행 거리 (m) 비행 시간 (초) 평균 속도 (m/s) 평균 오차 (RMSE, m) 최대 오차 (Max, m) 중앙값 오차 (m) 최종 지점 오차 (m)
0 flight-01p-ellipse ellipse 479.27 118.85 4.03 1.0719 3.4820 0.9018 3.4820
1 flight-02p-ellipse ellipse 446.16 106.84 4.18 0.6343 3.4555 0.4333 3.4555
2 flight-03p-ellipse ellipse 379.71 102.72 3.70 0.6123 2.3495 0.3983 0.6712
3 flight-07p-lemniscate lemniscate 444.32 125.67 3.54 1.6563 3.7836 1.4195 3.4664
4 flight-08p-lemniscate lemniscate 367.14 96.08 3.82 0.6742 2.1939 0.5008 2.1939
5 flight-09p-lemniscate lemniscate 447.09 101.63 4.40 0.6417 1.6992 0.5586 1.6992
코드 보기 (셀 15)
import seaborn as sns
import numpy as np
import matplotlib.pyplot as plt

# 데이터 정제: 'N/A' 문태 데이터나 결측치가 있다면 계산을 위해 숫자로 변환
analysis_df = df_report.copy()
for col in analysis_df.columns:
    if col != "비행 ID" and col != "트랙 종류":
        analysis_df[col] = pd.to_numeric(analysis_df[col], errors='coerce')

# --- [분석 1] Matplotlib를 이용한 오차 대조 그룹 바 차트 시각화 ---
print("1. 📊 [트랙별 평균 오차(RMSE) vs 최대 오차(Max) 그룹 바 차트]")

x = np.arange(len(analysis_df["비행 ID"]))  # 각 에피소드 위치
width = 0.35  # 바 너비

fig, ax = plt.subplots(figsize=(12, 6))

# 평균 오차(RMSE)와 최대 오차(Max) 바 그래프 그리기
rects1 = ax.bar(x - width/2, analysis_df["평균 오차 (RMSE, m)"], width, label='Mean Error (RMSE)', color='#2ca02c')
rects2 = ax.bar(x + width/2, analysis_df["최대 오차 (Max, m)"], width, label='Max Error (Max)', color='#d62728')

# 그래프 스타일 및 라벨 설정
ax.set_title('OpenVINS Estimation Errors by Flight Episode', fontsize=14, pad=15)
ax.set_ylabel('Error Distance (meters)', fontsize=12)
ax.set_xlabel('Flight Episode ID', fontsize=12)
ax.set_xticks(x)
ax.set_xticklabels(analysis_df["비행 ID"], rotation=15)
ax.legend(fontsize=11)
ax.grid(True, axis='y', linestyle=':', alpha=0.6)

# 바 상단에 수치 텍스트 표시 기능
def autolabel(rects):
    for rect in rects:
        height = rect.get_height()
        if not np.isnan(height):
            ax.annotate(f'{height:.2f}m',
                        xy=(rect.get_x() + rect.get_width() / 2, height),
                        xytext=(0, 3),  # 3포인트 위로 오프셋
                        textcoords="offset points",
                        ha='center', va='bottom', fontsize=9)

autolabel(rects1)
autolabel(rects2)

fig.tight_layout()
plt.show()


# --- [분석 2] Pandas를 활용한 상관계수(Correlation) 계산 및 시각화 ---
print("\n2. 🔍 [오차 원인 정밀 분석 - 상관계수 테이블]")

# 분석할 대상 열 선택 (비행 특성 요소 vs 결과 오차)
target_cols = ["비행 거리 (m)", "비행 시간 (초)", "평균 속도 (m/s)", "평균 오차 (RMSE, m)", "최대 오차 (Max, m)"]
corr_matrix = analysis_df[target_cols].corr()

# 상관계수 데이터프레임 예쁘게 출력
display(corr_matrix.round(4))

print("\n💡 상관계수 결과 해석 가이드:")
# 가장 강한 상관관계를 가진 변수 추적용 멘트
rmse_corr = corr_matrix["평균 오차 (RMSE, m)"]
print(f"• '평균 오차(RMSE)'와 '비행 시간'의 상관계수: {rmse_corr['비행 시간 (초)']:.4f}")
print(f"• '평균 오차(RMSE)'와 '평균 속도'의 상관계수: {rmse_corr['평균 속도 (m/s)']:.4f}")
print(f"• '평균 오차(RMSE)'와 '비행 거리'의 상관계수: {rmse_corr['비행 거리 (m)']:.4f}")
print("\n※ 해석 팁: 수치가 1에 가까울수록 양의 상관관계(비례), -1에 가까울수록 음의 상관관계(반비례)를 뜻하며, 0에 가까우면 서로 아무런 연관이 없음을 의미합니다.")
1. 📊 [트랙별 평균 오차(RMSE) vs 최대 오차(Max) 그룹 바 차트]
Figure 1
2. 🔍 [오차 원인 정밀 분석 - 상관계수 테이블]
비행 거리 (m) 비행 시간 (초) 평균 속도 (m/s) 평균 오차 (RMSE, m) 최대 오차 (Max, m)
비행 거리 (m) 1.0000 0.6794 0.4306 0.4256 0.5379
비행 시간 (초) 0.6794 1.0000 -0.3670 0.9096 0.8401
평균 속도 (m/s) 0.4306 -0.3670 1.0000 -0.5602 -0.3681
평균 오차 (RMSE, m) 0.4256 0.9096 -0.5602 1.0000 0.6970
최대 오차 (Max, m) 0.5379 0.8401 -0.3681 0.6970 1.0000
💡 상관계수 결과 해석 가이드:
• '평균 오차(RMSE)'와 '비행 시간'의 상관계수: 0.9096
• '평균 오차(RMSE)'와 '평균 속도'의 상관계수: -0.5602
• '평균 오차(RMSE)'와 '비행 거리'의 상관계수: 0.4256

※ 해석 팁: 수치가 1에 가까울수록 양의 상관관계(비례), -1에 가까울수록 음의 상관관계(반비례)를 뜻하며, 0에 가까우면 서로 아무런 연관이 없음을 의미합니다.

바 차트 그래프 해석

편상관분석 (Partial Correlation) — TII-RATM 드론 레이싱 데이터

목적: 상관계수 표에서 발견된 현상 두 가지를 분리해서 검증합니다.

  1. 비행 시간 ↔ RMSE의 강한 상관(0.9096)이 진짜 '드리프트 누적' 효과인지
  2. 평균 속도 ↔ Max Error의 음의 상관(-0.3681)이 '속도 자체의 효과'인지, 아니면 '비행 시간이 짧은 비행일수록 속도가 빨랐다'는 교란(confounding) 때문인지

편상관분석은 세 번째 변수(여기서는 비행 시간)의 영향을 통계적으로 통제한 뒤, 나머지 두 변수 사이의 '순수한' 관계를 봅니다.

⚠️ 주의: 표본이 n=6이라 통계적 검정력이 매우 낮습니다. 여기서 나오는 결과는 확정적 결론이 아니라 '경향성 확인' 수준으로만 해석하고, 논문에도 그렇게 명시해야 합니다.

코드 보기 (셀 20)
import pandas as pd
import pingouin as pg

# 6개 비행 에피소드 데이터 (앞서 산출한 벤치마킹 결과 표 그대로 입력)
data = {
    "flight_id": [
        "flight-01p-ellipse", "flight-02p-ellipse", "flight-03p-ellipse",
        "flight-07p-lemniscate", "flight-08p-lemniscate", "flight-09p-lemniscate",
    ],
    "track_type": ["ellipse", "ellipse", "ellipse", "lemniscate", "lemniscate", "lemniscate"],
    "distance_m": [479.27, 446.16, 379.71, 444.32, 367.14, 447.09],
    "duration_s": [118.85, 106.84, 102.72, 125.67, 96.08, 101.63],
    "avg_speed_ms": [4.03, 4.18, 3.70, 3.54, 3.82, 4.40],
    "rmse_m": [1.0719, 0.6343, 0.6123, 1.6563, 0.6742, 0.6417],
    "max_error_m": [3.4820, 3.4555, 2.3495, 3.7836, 2.1939, 1.6992],
}
df = pd.DataFrame(data)
df
flight_id track_type distance_m duration_s avg_speed_ms rmse_m max_error_m
0 flight-01p-ellipse ellipse 479.27 118.85 4.03 1.0719 3.4820
1 flight-02p-ellipse ellipse 446.16 106.84 4.18 0.6343 3.4555
2 flight-03p-ellipse ellipse 379.71 102.72 3.70 0.6123 2.3495
3 flight-07p-lemniscate lemniscate 444.32 125.67 3.54 1.6563 3.7836
4 flight-08p-lemniscate lemniscate 367.14 96.08 3.82 0.6742 2.1939
5 flight-09p-lemniscate lemniscate 447.09 101.63 4.40 0.6417 1.6992

1. 원래(단순) 상관계수 — 통제 없음

먼저 비교 기준으로, 통제하지 않은 단순 피어슨 상관을 다시 확인합니다.

코드 보기 (셀 23)
cols = ["distance_m", "duration_s", "avg_speed_ms", "rmse_m", "max_error_m"]
simple_corr = df[cols].corr(method="pearson")
simple_corr.round(4)
distance_m duration_s avg_speed_ms rmse_m max_error_m
distance_m 1.0000 0.6794 0.4306 0.4256 0.5379
duration_s 0.6794 1.0000 -0.3670 0.9096 0.8401
avg_speed_ms 0.4306 -0.3670 1.0000 -0.5602 -0.3681
rmse_m 0.4256 0.9096 -0.5602 1.0000 0.6970
max_error_m 0.5379 0.8401 -0.3681 0.6970 1.0000

2. 편상관분석 — 비행 시간(duration_s)을 통제한 뒤의 순수 관계

핵심 질문: 비행 시간을 고정했을 때도 '속도가 빠를수록 오차가 크다/작다'는 관계가 유지되는가?

코드 보기 (셀 26)
pairs_to_test = [
    ("avg_speed_ms", "rmse_m"),
    ("avg_speed_ms", "max_error_m"),
    ("distance_m", "rmse_m"),
    ("distance_m", "max_error_m"),
]

results = []
for x, y in pairs_to_test:
    # 단순 상관
    simple_r = df[x].corr(df[y])
    # duration_s를 통제한 편상관
    pc = pg.partial_corr(data=df, x=x, y=y, covar="duration_s", method="pearson")

    # pingouin 버전에 따라 p-value 컬럼명이 'p-val' 또는 'p-unc' 등으로 다를 수 있어 자동 탐색
    p_col = next((c for c in pc.columns if c.lower() in ("p-val", "p-unc", "pval", "p_val")), None)
    p_value = round(pc[p_col].values[0], 4) if p_col else None

    results.append({
        "x": x,
        "y": y,
        "단순상관(r)": round(simple_r, 4),
        "편상관(r, duration 통제)": round(pc["r"].values[0], 4),
        "p-value": p_value,
        "n": int(pc["n"].values[0]),
    })

results_df = pd.DataFrame(results)
results_df
x y 단순상관(r) 편상관(r, duration 통제) p-value n
0 avg_speed_ms rmse_m -0.5602 -0.5857 0.2994 6
1 avg_speed_ms max_error_m -0.3681 -0.1185 0.8495 6
2 distance_m rmse_m 0.4256 -0.6308 0.2538 6
3 distance_m max_error_m 0.5379 -0.0825 0.8951 6

3. 해석 가이드

결과표를 읽는 방법:

논문 Discussion 섹션에 쓸 문장 예시:

"단순 상관분석에서는 평균 속도와 최대 오차 간 음의 상관(r=-0.37)이 관찰되었으나, 비행 시간을 통제한 편상관분석 결과 [해당 값]으로 나타나 [속도의 독립적 효과가 유지/약화]되었다. 이는 [해석]을 시사하나, 표본 크기(n=6)의 한계로 추가 검증이 필요하다."

4. (선택) 궤적 유형(ellipse vs lemniscate)을 통제한 편상관

flight-07p-lemniscate처럼 유독 오차가 큰 비행이 있었죠. 궤적 형태 자체가 교란변수일 수도 있으니, 범주형 변수는 더미코딩 후 같은 방식으로 통제할 수 있습니다.

코드 보기 (셀 30)
df["is_lemniscate"] = (df["track_type"] == "lemniscate").astype(int)

pc_track = pg.partial_corr(
    data=df, x="duration_s", y="rmse_m", covar="is_lemniscate", method="pearson"
)
print("비행시간-RMSE 상관 (궤적 유형 통제):")
pc_track
비행시간-RMSE 상관 (궤적 유형 통제):
n r CI95 p_val
pearson 6 0.976735 [0.68, 1.0] 0.004245
코드 보기 (셀 34)
import os
import pandas as pd
import numpy as np
import fiftyone as fo
from huggingface_hub import snapshot_download
import seaborn as sns
import matplotlib.pyplot as plt
from IPython.display import display

# --- [단계 1] 데이터셋 안전 다운로드 및 FiftyOne 로드 ---
print("1. Hugging Face 허브에서 데이터셋 직접 안전 다운로드 중...")
local_dir = snapshot_download(
    repo_id="Voxel51/TII-RATM-Drone-Racing",
    repo_type="dataset"
)

print("2. FiftyOne 데이터셋 엔진에 로드 중...")
dataset = fo.Dataset.from_dir(
    dataset_dir=local_dir,
    dataset_type=fo.types.FiftyOneDataset
)

# --- [단계 2] 벤치마킹 데이터 파싱 및 df_report 테이블 생성 ---
print("3. 알고리즘 검증 데이터 테이블 생성 중...")
summary_data = []

for i, sample in enumerate(dataset):
    track_type = getattr(sample, "track", "N/A")
    flight_id = getattr(sample, "flight", f"Episode {i+1}")
    distance = getattr(sample, "distance_m", 0.0)
    duration = getattr(sample, "duration", 0.0)
    mean_speed = getattr(sample, "mean_speed_ms", 0.0)

    rmse_err = getattr(sample, "tracking_error_rmse_m", None)
    max_err = getattr(sample, "tracking_error_max_m", None)
    median_err = getattr(sample, "tracking_error_median_m", None)
    final_err = getattr(sample, "tracking_error_final_m", None)

    summary_data.append({
        "비행 ID": flight_id,
        "트랙 종류": track_type,
        "비행 거리 (m)": round(distance, 2) if distance else "N/A",
        "비행 시간 (초)": round(duration, 2) if duration else "N/A",
        "평균 속도 (m/s)": round(mean_speed, 2) if mean_speed else "N/A",
        "평균 오차 (RMSE, m)": round(rmse_err, 4) if rmse_err is not None else "N/A",
        "최대 오차 (Max, m)": round(max_err, 4) if max_err is not None else "N/A",
        "중앙값 오차 (m)": round(median_err, 4) if median_err is not None else "N/A",
        "최종 지점 오차 (m)": round(final_err, 4) if final_err is not None else "N/A"
    })

df_report = pd.DataFrame(summary_data)
print("📊 [SLAM / OpenVINS 알고리즘 정밀도 검증 결과 테이블]")
display(df_report)


# --- [단계 3] 그래프 시각화를 위한 데이터 전처리 ---
plot_df = df_report.copy()
target_cols = ["비행 거리 (m)", "비행 시간 (초)", "평균 속도 (m/s)", "평균 오차 (RMSE, m)", "최대 오차 (Max, m)"]
for col in target_cols:
    plot_df[col] = pd.to_numeric(plot_df[col], errors='coerce')
plot_df = plot_df.dropna(subset=target_cols)


# --- [단계 4] 그래프 1: 변수별 산점도 및 회귀 추세선 (2x1 서브플롯) ---
print("\n4. 📈 [핵심 변수별 산점도 및 회귀 추세선 시각화 수립]")
fig, axes = plt.subplots(1, 2, figsize=(15, 6))

# 4-1. 비행 시간 vs 평균 오차
sns.regplot(
    data=plot_df,
    x="비행 시간 (초)",
    y="평균 오차 (RMSE, m)",
    ax=axes[0],
    color="#d62728",
    scatter_kws={'s': 80, 'alpha': 0.8},
    line_kws={'color': 'black', 'linestyle': '--', 'linewidth': 2}
)
axes[0].set_title("Flight Duration vs Mean Error (RMSE)\n[Positive Correlation: 0.9096]", fontsize=12, pad=10)
axes[0].set_xlabel("Flight Duration (seconds)", fontsize=11)
axes[0].set_ylabel("Mean Error (meters)", fontsize=11)
axes[0].grid(True, linestyle=':', alpha=0.6)

# 4-2. 평균 속도 vs 평균 오차
sns.regplot(
    data=plot_df,
    x="평균 속도 (m/s)",
    y="평균 오차 (RMSE, m)",
    ax=axes[1],
    color="#1f77b4",
    scatter_kws={'s': 80, 'alpha': 0.8},
    line_kws={'color': 'black', 'linestyle': '--', 'linewidth': 2}
)
axes[1].set_title("Mean Speed vs Mean Error (RMSE)\n[Negative Correlation: -0.5602]", fontsize=12, pad=10)
axes[1].set_xlabel("Mean Speed (m/s)", fontsize=11)
axes[1].set_ylabel("Mean Error (meters)", fontsize=11)
axes[1].grid(True, linestyle=':', alpha=0.6)

plt.suptitle("Statistical Variable Analysis for OpenVINS Accuracy", fontsize=15, y=1.02)
plt.tight_layout()
plt.show()


# --- [단계 5] 그래프 2: 상관계수 매트릭스 Heatmap 시각화 ---
print("\n5. 🌡️ [상관계수 매트릭스 Heatmap 시각화 수립]")
plt.figure(figsize=(8, 6))
corr_matrix = plot_df[target_cols].corr()

sns.heatmap(
    corr_matrix,
    annot=True,
    cmap="coolwarm",
    fmt=".4f",
    linewidths=0.5,
    vmin=-1, vmax=1,
    cbar_kws={'label': 'Pearson Correlation Coefficient'}
)

plt.title("Correlation Matrix Heatmap of Flight Variables", fontsize=13, pad=15)
plt.tight_layout()
plt.show()
1. Hugging Face 허브에서 데이터셋 직접 안전 다운로드 중...
2. FiftyOne 데이터셋 엔진에 로드 중...
3. 알고리즘 검증 데이터 테이블 생성 중...
📊 [SLAM / OpenVINS 알고리즘 정밀도 검증 결과 테이블]
비행 ID 트랙 종류 비행 거리 (m) 비행 시간 (초) 평균 속도 (m/s) 평균 오차 (RMSE, m) 최대 오차 (Max, m) 중앙값 오차 (m) 최종 지점 오차 (m)
0 flight-01p-ellipse ellipse 479.27 118.85 4.03 1.0719 3.4820 0.9018 3.4820
1 flight-02p-ellipse ellipse 446.16 106.84 4.18 0.6343 3.4555 0.4333 3.4555
2 flight-03p-ellipse ellipse 379.71 102.72 3.70 0.6123 2.3495 0.3983 0.6712
3 flight-07p-lemniscate lemniscate 444.32 125.67 3.54 1.6563 3.7836 1.4195 3.4664
4 flight-08p-lemniscate lemniscate 367.14 96.08 3.82 0.6742 2.1939 0.5008 2.1939
5 flight-09p-lemniscate lemniscate 447.09 101.63 4.40 0.6417 1.6992 0.5586 1.6992
4. 📈 [핵심 변수별 산점도 및 회귀 추세선 시각화 수립]
Figure 2
5. 🌡️ [상관계수 매트릭스 Heatmap 시각화 수립]
코드 보기 (셀 36)
import seaborn as sns
import matplotlib.pyplot as plt

# 1. 기존 데이터프레임의 열 이름을 논문용 영문 명칭으로 일괄 변경
rename_dict = {
    "비행 거리 (m)": "Flight Distance (m)",
    "비행 시간 (초)": "Flight Duration (s)",
    "평균 속도 (m/s)": "Mean Speed (m/s)",
    "평균 오차 (RMSE, m)": "Mean Error (RMSE, m)",
    "최대 오차 (Max, m)": "Max Error (Max, m)"
}

# 열 이름 영문 변환 적용
eng_plot_df = plot_df.rename(columns=rename_dict)
eng_target_cols = list(rename_dict.values())

# 2. 영문 기준 상관계수 매트릭스 재연산
eng_corr_matrix = eng_plot_df[eng_target_cols].corr()

# 3. 영문 히트맵 시각화 출력 (깨짐 없는 기본 폰트 사용)
plt.figure(figsize=(10, 8))

# Seaborn 히트맵 스타일 및 폰트 크기 조정
sns.set_theme(style="white")
sns.heatmap(
    eng_corr_matrix,
    annot=True,
    cmap="coolwarm",
    fmt=".4f",
    linewidths=0.7,
    vmin=-1, vmax=1,
    annot_kws={"size": 11, "weight": "bold"},
    cbar_kws={'label': 'Pearson Correlation Coefficient'}
)

# 타이틀 및 라벨 레이아웃 설정
plt.title("Correlation Matrix Heatmap of Drone Flight Variables", fontsize=14, pad=20, weight='bold')
plt.xticks(rotation=25, ha='right', fontsize=11)
plt.yticks(rotation=0, fontsize=11)
plt.tight_layout()
plt.show()
Figure 3
코드 보기 (셀 37)
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

# ==========================================
# 1. 환경 변수 및 하이퍼파라미터 설정
# ==========================================
np.random.seed(42)
flight_duration = 150.0  # 총 비행 시간 (100초 이상 테스트)
dt = 0.1                 # EKF 업데이트 주기 (초)
time_steps = int(flight_duration / dt)
timestamps = np.linspace(0, flight_duration, time_steps)

# 논문 기준 파라미터
q0 = 1e-4                # 기본 프로세스 노이즈 밀도 (OpenVINS default)
T_ref = 60.0             # Reference duration
true_initial_td = 0.02   # 실제 초기 타임오프셋 (20ms)

# 시간에 따라 변화하는 실제 td 생성 (Thermal drift, clock skew 모사)
# 5.3절 내용 반영: 시간이 지날수록 느리게 변하는 효과(Random Walk + Drift)
true_td = true_initial_td + 0.0001 * timestamps + np.cumsum(np.random.normal(0, 0.0002, time_steps))

# ==========================================
# 2. EKF 필터 시뮬레이션 함수
# ==========================================
def run_ekf_simulation(config_name, calib_cam_timeoffset, kappa):
    # 상태 변수 초기화 (초기 추정치 0으로 시작)
    td_hat = 0.0
    P_td = 1e-3  # td의 초기 공분산

    td_estimates = []
    P_history = []

    for i, t in enumerate(timestamps):
        # --- 1. [Propagation Step] 공분산 전파 ---
        if calib_cam_timeoffset:
            if config_name == "OpenVINS-default":
                q_current = q0
            elif config_name == "Duration-scaled (proposed)":
                # 5.4절 제안 공식: q_td(tau) = q0 * (1 + kappa * (tau / T_ref))
                # 여기서는 IMU 초기화 이후 경과 시간인 t를 tau로 사용
                q_current = q0 * (1.0 + kappa * (t / T_ref))
        else:
            # Baseline: 온라인 캘리브레이션을 끔 (시간에 따른 전파 및 업데이트 없음)
            q_current = 0.0

        # P_block 업데이트 (Process noise 반영)
        if calib_cam_timeoffset:
            P_td = P_td + q_current * dt

        # --- 2. [Measurement Step] EKF 업데이트 ---
        if calib_cam_timeoffset:
            # 측정치 잔차 및 야코비안 (H_td = dz/dt 모사, 여기서는 일정한 상수 효과로 가정)
            H_td = 1.2
            R = 0.01   # 측정 노이즈 공분산

            # 칼만 이득 계산: K = P*H' * (H*P*H' + R)^-1
            S = H_td * P_td * H_td + R
            K = (P_td * H_td) / S

            # 가상 측정값 잔차 (Residual)
            residual = true_td[i] - td_hat + np.random.normal(0, 0.05)

            # 상태 및 공분산 업데이트
            td_hat = td_hat + K * residual
            P_td = (1.0 - K * H_td) * P_td
        else:
            # Baseline (fixed td = 0)
            td_hat = 0.0
            P_td = 0.0

        td_estimates.append(td_hat)
        P_history.append(P_td)

    return np.array(td_estimates), np.array(P_history)

# ==========================================
# 3. 세 가지 설정별 실험 수행
# ==========================================
configs = {
    "Baseline": {"calib_cam_timeoffset": False, "kappa": 0},
    "OpenVINS-default": {"calib_cam_timeoffset": True, "kappa": 0},
    "Duration-scaled (proposed)": {"calib_cam_timeoffset": True, "kappa": 1.5} # grid search로 튜닝 가능 {0.5, 1, 2}
}

results = {}
for name, param in configs.items():
    td_est, p_hist = run_ekf_simulation(name, param["calib_cam_timeoffset"], param["kappa"])
    errors = true_td - td_est

    # 평가지표 계산 (RMSE, Max Error)
    rmse = np.sqrt(np.mean(errors**2))
    max_err = np.max(np.abs(errors))

    results[name] = {
        "estimates": td_est,
        "covariances": p_hist,
        "errors": errors,
        "RMSE": rmse,
        "Max_Error": max_err
    }

# ==========================================
# 4. 결과 출력 및 정량적 비교 (Table 5.5 모사)
# ==========================================
summary_data = []
for name in configs.keys():
    summary_data.append({
        "Configuration": name,
        "RMSE (ms)": round(results[name]["RMSE"] * 1000, 3),
        "Max Error (ms)": round(results[name]["Max_Error"] * 1000, 3)
    })

print("\n=== Experimental Protocol Validation Results ===")
df_res = pd.DataFrame(summary_data)
print(df_res.to_string(index=False))

# ==========================================
# 5. 시각화 그래프 그리기
# ==========================================
plt.figure(figsize=(14, 10))

# [Graph 1] True vs Estimated td 추이 비교
plt.subplot(2, 1, 1)
plt.plot(timestamps, true_td * 1000, 'k--', label='True $t_d$ (Drifting)', linewidth=2)
for name in configs.keys():
    plt.plot(timestamps, results[name]["estimates"] * 1000, label=f'{name}')
plt.title("Camera-IMU Time Offset ($t_d$) Estimation Trajectory")
plt.xlabel("Elapsed Flight Time $\\tau$ (seconds)")
plt.ylabel("Time Offset (ms)")
plt.legend()
plt.grid(True)

# [Graph 2] 필터의 불확실성 (공분산 P_td) 비교 -> 5.3절의 Limitation 증명
plt.subplot(2, 1, 2)
for name in configs.keys():
    if name != "Baseline":
        plt.plot(timestamps, results[name]["covariances"], label=f'{name} ($P_{{t_d}}$)')
plt.title("Filter Uncertainty ($P_{{t_d}}$) Comparison")
plt.xlabel("Elapsed Flight Time $\\tau$ (seconds)")
plt.ylabel("Covariance Value")
plt.yscale('log') # 값이 급격히 작아지므로 로그 스케일 추천
plt.legend()
plt.grid(True)

plt.tight_layout()
plt.show()
=== Experimental Protocol Validation Results ===
             Configuration  RMSE (ms)  Max Error (ms)
                  Baseline     31.788          49.715
          OpenVINS-default      5.811          17.566
Duration-scaled (proposed)      9.246          29.468
Figure 4