Flight-Duration Dependence of VIO Drift in High-Speed Drone Racing and Online Time-Offset Calibration

TII-RATM-Drone-Racing dataset · OpenVINS benchmark · correlation & partial correlation · 한국어

This page is a static rendering of a Google Colab analysis notebook. Package-installation and runtime-restart cells are omitted; expand "Show code" to see each cell's source (code comments are in Korean). With only n = 6 flights, statistical results should be read as trends, not firm conclusions.

Dataset: TII-RATM-Drone-Racing

Published by: Voxel51, a computer vision company

Research outline

Show code (cell 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)
Starting dataset download (~580 MB)...
--- Dataset loaded successfully ---
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
Show code (cell 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] Basic field list of a sample ---
('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] Detailed schema of data fields ---
Field: id -> type: <class 'str'>
Field: filepath -> type: <class 'str'>
Field: tags -> type: <class 'mongoengine.base.datastructures.BaseList'>
Field: metadata -> type: <class 'NoneType'>
Field: created_at -> type: <class 'datetime.datetime'>
Field: last_modified_at -> type: <class 'datetime.datetime'>
Field: flight -> type: <class 'str'>
Field: track -> type: <class 'str'>
Field: duration -> type: <class 'float'>
Field: distance_m -> type: <class 'float'>
Field: mean_speed_ms -> type: <class 'float'>
Field: max_speed_ms -> type: <class 'float'>
Field: num_camera_frames -> type: <class 'int'>
Field: num_feature_track_frames -> type: <class 'int'>
Field: num_imu_samples -> type: <class 'int'>
Field: num_vio_poses -> type: <class 'int'>
Field: num_ground_truth_poses -> type: <class 'int'>
Field: num_gates -> type: <class 'int'>
Field: tracking_error_rmse_m -> type: <class 'float'>
Field: tracking_error_median_m -> type: <class 'float'>
Field: tracking_error_max_m -> type: <class 'float'>
Field: tracking_error_final_m -> type: <class 'float'>
Show code (cell 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 대시보드 화면과 교차 검증을 진행해 주세요.")
🚀 Generating algorithm benchmark report on the TII-RATM-Drone-Racing dataset...

📊 [SLAM / OpenVINS accuracy validation results]
Flight ID Track Type Flight Distance (m) Flight Duration (s) Mean Speed (m/s) Mean Error (RMSE, m) Max Error (m) Median Error (m) Final Position Error (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
💡 Analysis notes:
1. Across the loaded flight sessions, OpenVINS shows stable estimation performance.
2. The average position estimation error (RMSE) across all episodes is about 0.8818 m.
Show code (cell 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. Downloading the dataset directly from the Hugging Face Hub (~580 MB)...
2. Loading into FiftyOne...
- Dataset loaded successfully!

3. 🚀 Generating OpenVINS validation report...

📊 [SLAM / OpenVINS accuracy validation results]
Flight ID Track Type Flight Distance (m) Flight Duration (s) Mean Speed (m/s) Mean Error (RMSE, m) Max Error (m) Median Error (m) Final Position Error (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
Show code (cell 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. 📊 [Grouped bar chart: mean error (RMSE) vs. max error per flight]
Figure 1
2. 🔍 [Error cause analysis — correlation table]
Flight Distance (m) Flight Duration (s) Mean Speed (m/s) Mean Error (RMSE, m) Max Error (m)
Flight Distance (m) 1.0000 0.6794 0.4306 0.4256 0.5379
Flight Duration (s) 0.6794 1.0000 -0.3670 0.9096 0.8401
Mean Speed (m/s) 0.4306 -0.3670 1.0000 -0.5602 -0.3681
Mean Error (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
💡 Correlation interpretation guide:
• Correlation between mean error (RMSE) and flight duration: 0.9096
• Correlation between mean error (RMSE) and mean speed: -0.5602
• Correlation between mean error (RMSE) and flight distance: 0.4256

※ Tip: values near 1 indicate a positive correlation, values near −1 a negative correlation, and values near 0 no linear relationship.

Reading the bar chart

Reading the correlation matrix

Partial Correlation Analysis — TII-RATM Drone Racing Data

Goal: separate and test two effects found in the correlation table.

  1. Whether the strong correlation between flight duration ↔ RMSE (0.9096) is genuinely a drift-accumulation effect.
  2. Whether the negative correlation between mean speed ↔ Max Error (−0.3681) is an effect of speed itself, or confounding because shorter flights happened to be faster.

Partial correlation statistically controls for a third variable (here, flight duration) and examines the remaining "pure" relationship between the other two.

⚠️ Caution: with n = 6 the statistical power is very low. Treat these results as indications of a trend, not firm conclusions, and state this explicitly in any paper.

Show code (cell 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. Simple (uncontrolled) correlation

As a baseline, we first recompute the simple Pearson correlation without any control variable.

Show code (cell 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. Partial correlation — controlling for flight duration (duration_s)

Key question: with flight duration held fixed, does the relationship "faster flight → larger/smaller error" still hold?

Show code (cell 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 Simple r Partial r (controlling 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. Interpretation guide

How to read the results table:

Example sentence for a Discussion section:

"Simple correlation analysis showed a negative correlation between mean speed and maximum error (r = −0.37); however, after controlling for flight duration, the partial correlation was [value], indicating that the independent effect of speed was [maintained/weakened]. This suggests [interpretation], although further validation is needed given the limited sample size (n = 6)."

4. (Optional) Partial correlation controlling for trajectory type (ellipse vs. lemniscate)

Some flights, such as flight-07p-lemniscate, had unusually large errors. The trajectory shape itself may be a confounder, so the categorical variable is dummy-coded and controlled for in the same way.

Show code (cell 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
Flight duration–RMSE correlation (controlling for trajectory type):
n r CI95 p_val
pearson 6 0.976735 [0.68, 1.0] 0.004245
Show code (cell 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. Downloading the dataset directly from the Hugging Face Hub...
2. Loading into FiftyOne...
3. Building the validation data table...
📊 [SLAM / OpenVINS accuracy validation results]
Flight ID Track Type Flight Distance (m) Flight Duration (s) Mean Speed (m/s) Mean Error (RMSE, m) Max Error (m) Median Error (m) Final Position Error (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. 📈 [Scatter plots with regression lines for key variables]
Figure 2
5. 🌡️ [Correlation matrix heatmap]
Show code (cell 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
Show code (cell 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