---
# System prepended metadata

title: 無人機模擬環境與 ORB-SLAM3、YOLO 偵測技術文件

---

# 無人機模擬環境與 ORB-SLAM3、YOLO 偵測技術文件 & 實體飛行測試文件
P.S. 程式與工研院筆電版本相同

## 目錄
- [1. Docker 使用](#1-Docker)
- [2. 模擬無人機專案建置](#2-模擬無人機專案建置)
- [3. ORB-SLAM3 指令與流程](#3-ORB-SLAM3指令與流程)
- [4. YOLO 指令與流程](#4-YOLO指令與流程)
- [5. EVO軌跡評估工具](#5-EVO軌跡評估工具)
- [6. ROS 常用除錯工具與技巧](#6-ROS常用除錯工具與技巧)
- [7. 實體飛行無人機參數設定](#7-實體無人機參數設定)


---
## 1. Docker
Docker 是一種開源軟體平台，其核心理念是「Build and Ship any Application Anywhere」，旨在讓開發者能在一個輕量、可攜的虛擬化環境中，輕鬆地開發、部署及管理任何應用程式。對於像無人機這樣涉及複雜軟體（如 ROS、SLAM 演算法、AI 模型）的專案，Docker 提供了標準化且可重現的解決方案。

### 本專案的雙容器架構

為了實現模組化，本專案將採用**雙容器**的架構。不同的核心功能將被拆分到兩個獨立的容器中，它們之間透過 ROS (Robot Operating System) 的網路通訊機制（Topics, Services）進行溝通。
p.s. 請使用ubuntu 2204 以下的版本
#### Container 1: `rtabmap_drone_example` (模擬與導航核心)

- **功能與內容**:
  - **模擬環境**: 內建 **Gazebo** 模擬器，提供一個虛擬世界讓無人機飛行與互動。
  - **飛行控制**: 包含 **PX4-SITL** (Software in the Loop)，模擬無人機的飛行控制器。
  - **通訊橋梁**: 透過 **MAVROS** 將 ROS 指令轉換為 PX4 能理解的 MAVLink 訊息。
  - **基礎導航**: 整合 **RTAB-Map** 作為視覺 SLAM 引擎，並搭配 **`move_base`** 導航堆疊，實現基本的自主路徑規劃與避障。
#### Container 2: `ORB-SLAM3 & YOLO detection` (進階感知與定位)

- **功能與內容**:
  - **高精度 SLAM**: 搭載 **ORB-SLAM3** 函式庫。ORB-SLAM3 是一個功能強大的視覺慣性 SLAM 系統，能提供比 RTAB-Map 更高精度的定位與建圖能力。將其獨立打包成容器是常見作法，因為其依賴複雜且版本穩定，便於重複使用。
  - **物件偵測**: 整合 **YOLO** 模型。此模型會接收來自模擬環境的影像串流，進行即時物件偵測，並將偵測結果（如物件類別、深度）透過 ROS Topic 發佈出去。

### 啟動前的準備 (X11 Forwarding)
為了讓容器內的圖形化介面應用（如 Gazebo, RViz）能顯示在你的主機螢幕上，需要在本機先執行以下指令授權 X server 連線：
```
xhost +local:root
XAUTH=/tmp/.docker.xauth

## 可以試試看
touch $XAUTH
xauth nlist $DISPLAY | sed -e 's/^..../ffff/' | xauth -f $XAUTH nmerge - 
```

### Docker 啟動指令 (以 NVIDIA GPU 版本為例)
```
#ubuntu 建議22.04以下 不然rviz需要使用到的OpenGL會有衝突 
docker run -it \
    --name="<給容器取個名字>" \
    --privileged \
    --net=host \
    --gpus=all \
    --env="DISPLAY=$DISPLAY" \
    --env="QT_X11_NO_MITSHM=1" \
    --volume="/tmp/.X11-unix:/tmp/.X11-unix:rw" \
    --env="XAUTHORITY=$XAUTH" \
    --volume="$XAUTH:$XAUTH" \
    --volume="<你的本機路徑>:<容器內對應路徑>" \
    <你的映像檔名稱> \
    /bin/bash                      (進入bash)
```

## 2. 模擬無人機專案建置

### 專案概述
本專案 `rtabmap_drone_example` 提供一個完整的範例，展示如何使用 `move_base`、`mavros/px4` 以及 `RTAB-Map` 視覺 SLAM 技術，在模擬環境中實現無人機的自主導航。專案的 GitHub 儲存庫與詳細安裝教學請參考：[rtabmap_drone_example](https://github.com/matlabbe/rtabmap_drone_example)。

<figure style="text-align: center;">
  <img src="https://hackmd.io/_uploads/rJCEAwVNge.png" alt="模擬環境展示圖">
  <figcaption style="font-style: italic;">圖1：rtabmap_drone_example 專案於 Gazebo 中的視覺化介面</figcaption>
</figure>




### 核心組件與執行流程
要完整運行此模擬專案，需在**四個獨立的終端機**中依序啟動以下核心節點：

1.  **啟動 Gazebo 模擬環境**
    ```
    roslaunch rtabmap_drone_example gazebo.launch
    ```
    > **作用**：此指令會啟動 Gazebo 模擬器，載入預設的世界地圖，並在其中生成一台 IRIS 四旋翼無人機模型。此模型已搭載模擬的攝影機與 IMU 感測器。

2.  **啟動 RTAB-Map SLAM**
    ```
    roslaunch rtabmap_drone_example slam.launch
    ```
    > **作用**：啟動 `rtabmap` 節點，進入 SLAM 模式。它會訂閱來自 Gazebo 無人機的影像與 IMU 數據，即時進行同步定位與建圖 (Simultaneous Localization and Mapping)，並發布地圖 (`/map`)、無人機位置 (`tf`) 與里程計 (`/rtabmap/odom`) 等資訊。

3.  **啟動 RVIZ 視覺化介面**
    ```
    roslaunch rtabmap_drone_example rviz.launch
    ```
    > **作用**：啟動 RVIZ，並載入預設的設定檔。在此介面中，你可以看到 RTAB-Map 所建立的 2D/3D 地圖、無人機的即時位置，並能透過 **"2D Nav Goal"** 工具下達自主導航的目標點。

4.  **啟動飛行控制節點**
    ```
    # 執行 C++ 版本
    rosrun rtabmap_drone_example offboard
    
    # 改寫的 Python 版本
    rosrun rtabmap_drone_example offboard_python.py
    ```
    > **作用**：它負責透過 MAVROS 與 PX4 飛行控制器溝通，將無人機切換至 `OFFBOARD` 模式，並執行解鎖與自動起飛。原專案提供的是 C++ 版本，但我們也可以使用 Python 版本進行替換。

<details>
 <summary>可以用python改寫起飛程式碼</summary>  
    
```
#!/usr/bin/env python3
# coding: utf-8
import rospy
import tf2_ros
import tf2_geometry_msgs
from geometry_msgs.msg import PoseStamped, Twist
from mavros_msgs.msg import State, PositionTarget
from mavros_msgs.srv import CommandBool, CommandBoolRequest, CommandLong, CommandLongRequest, SetMode, SetModeRequest
import math

# 定義不同控制模式的 type_mask（MAVROS 規範）
VELOCITY2D_CONTROL = 0b011111000011    # 僅控制 X, Y 速度
VELOCITY_CONTROL = 0b011111000111      # 控制 X, Y, Z 速度
POSITION_CONTROL = 0b101111111000      # 控制位置

velocity_mask = VELOCITY2D_CONTROL     # 預設為 2D 速度控制

def state_cb(msg):
    # 訂閱 mavros/state，取得無人機目前狀態（連線、解鎖、模式等）
    global current_state
    current_state = msg

def twist_cb(msg):
    # 訂閱 /cmd_vel，接收速度指令，切換為速度控制模式
    global current_goal, lastTwistReceived, velocity_mask
    if current_goal.type_mask == POSITION_CONTROL:
        rospy.loginfo("Switch to velocity control")
        current_goal.coordinate_frame = PositionTarget.FRAME_BODY_NED
        current_goal.type_mask = velocity_mask
    # 設定速度指令
    current_goal.velocity.x = msg.linear.x
    current_goal.velocity.y = msg.linear.y
    current_goal.velocity.z = 0 if velocity_mask == VELOCITY2D_CONTROL else msg.linear.z
    current_goal.position.z = 1.5
    current_goal.yaw_rate = msg.angular.z
    lastTwistReceived = rospy.Time.now()

def get_yaw_from_quaternion(q):
    # 從四元數計算出 yaw 角度
    siny_cosp = 2 * (q.w * q.z + q.x * q.y)
    cosy_cosp = 1 - 2 * (q.y * q.y + q.z * q.z)
    return math.atan2(siny_cosp, cosy_cosp)

# === 新增：目標點指令相關 ===
target_received = False     # 是否收到目標點指令
target_pose = None          # 目標點 Pose

def my_target_cb(msg):
    # 訂閱 /my_target_point，收到目標點指令時觸發
    global target_received, target_pose
    target_pose = msg
    target_received = True
    rospy.loginfo("收到新的目標點指令: x=%.2f, y=%.2f, z=%.2f" % (msg.pose.position.x, msg.pose.position.y, msg.pose.position.z))

if __name__ == "__main__":
    # 初始化 ROS 節點
    rospy.init_node("offboard_node")

    # 初始化控制目標與狀態
    current_goal = PositionTarget()
    current_state = State()
    lastTwistReceived = rospy.Time.now()
    current_pose = PoseStamped()

    # 訂閱無人機狀態
    state_sub = rospy.Subscriber("mavros/state", State, state_cb)
    # 訂閱速度指令
    twist_sub = rospy.Subscriber("/cmd_vel", Twist, twist_cb)
    # 訂閱目標點指令
    my_target_sub = rospy.Subscriber("/my_target_point", PoseStamped, my_target_cb)

    # 建立控制指令與定位資訊的 publisher
    local_pos_pub = rospy.Publisher("mavros/setpoint_raw/local", PositionTarget, queue_size=10)    # 發布位置/速度目標給 MAVROS
    vision_pos_pub = rospy.Publisher("mavros/vision_pose/pose", PoseStamped, queue_size=10)        # 發布視覺定位資訊給 MAVROS

    # 等待 MAVROS 服務（arming, command, set_mode）可用
    rospy.wait_for_service("/mavros/cmd/arming")
    arming_client = rospy.ServiceProxy("/mavros/cmd/arming", CommandBool)
    rospy.wait_for_service("/mavros/cmd/command")
    command_client = rospy.ServiceProxy("/mavros/cmd/command", CommandLong)
    rospy.wait_for_service("/mavros/set_mode")
    set_mode_client = rospy.ServiceProxy("/mavros/set_mode", SetMode)

    # 初始化 tf2 buffer 與 listener，用於查詢 /map → /base_link 的座標轉換
    tf_buffer = tf2_ros.Buffer()
    tf_listener = tf2_ros.TransformListener(tf_buffer)

    rate = rospy.Rate(50)

    # 等待與飛控連線
    while not rospy.is_shutdown() and not current_state.connected:
        rospy.loginfo_throttle(2, "wait for FCU connection...")
        rate.sleep()

    rospy.loginfo("Setting offboard mode... (5 seconds)")

    found_tf = False
    # 查詢一次 /map → /base_link 的 tf，取得初始位置
    for _ in range(50):
        try:
            trans = tf_buffer.lookup_transform("map", "base_link", rospy.Time(0), rospy.Duration(5))
            found_tf = True
            break
        except (tf2_ros.LookupException, tf2_ros.ExtrapolationException):
            rate.sleep()

    if not found_tf:
        rospy.logerr("Cannot get current position between /map and /base_link")
        exit(1)

    # 設定起飛初始點（座標、姿態）
    current_goal.coordinate_frame = PositionTarget.FRAME_LOCAL_NED
    current_goal.type_mask = POSITION_CONTROL
    current_goal.position.x = trans.transform.translation.x
    current_goal.position.y = trans.transform.translation.y
    current_goal.position.z = 1.5    # 預設起飛高度(有改過)
    q = trans.transform.rotation        
    current_goal.yaw = get_yaw_from_quaternion(q)
    current_goal.velocity.x = 0
    current_goal.velocity.y = 0
    current_goal.velocity.z = 0
    current_goal.yaw_rate = 0
    current_goal.acceleration_or_force.x = 0
    current_goal.acceleration_or_force.y = 0
    current_goal.acceleration_or_force.z = 0

    rospy.loginfo("Initial position=(%.2f,%.2f,%.2f) yaw=%.2f",
                  current_goal.position.x, current_goal.position.y, trans.transform.translation.z, current_goal.yaw)

    # 起飛前先連續發送一段時間的 setpoint，讓 PX4 進入 offboard 模式
    for _ in range(100):
        current_goal.header.stamp = rospy.Time.now()
        local_pos_pub.publish(current_goal)
        rate.sleep()

    # 準備服務請求物件
    offb_set_mode = SetModeRequest()
    offb_set_mode.custom_mode = "OFFBOARD"
    arm_cmd = CommandBoolRequest()
    arm_cmd.value = True
    disarm_cmd = CommandLongRequest()
    disarm_cmd.broadcast = False
    disarm_cmd.command = 400

    last_request = rospy.Time.now()
    lastTwistReceived = rospy.Time.now()
    current_pose = PoseStamped()
    current_pose.header.frame_id = "map"

    # 進入主迴圈
    while not rospy.is_shutdown():
        try:
            # 持續查詢 /map → /base_link 的 tf，取得無人機目前位置
            trans = tf_buffer.lookup_transform("map", "base_link", rospy.Time(0), rospy.Duration(0.05))
            current_pose.pose.position.x = trans.transform.translation.x
            current_pose.pose.position.y = trans.transform.translation.y
            current_pose.pose.position.z = trans.transform.translation.z
            current_pose.pose.orientation = trans.transform.rotation
        except (tf2_ros.LookupException, tf2_ros.ExtrapolationException):
            pass

        # 若尚未進入 OFFBOARD 模式，且間隔超過 5 秒則切換模式
        if current_state.mode != "OFFBOARD" and (rospy.Time.now() - last_request > rospy.Duration(5.0)):
            if set_mode_client.call(offb_set_mode).mode_sent:
                rospy.loginfo("Offboard enabled")
                rospy.loginfo("Vehicle arming... (5 seconds)")
                last_request = rospy.Time.now()

        # 若尚未解鎖，且間隔超過 5 秒則解鎖
        elif (not current_state.armed and
              not (current_goal.velocity.z < -0.4 and current_goal.yaw_rate < -0.4) and
              (rospy.Time.now() - last_request > rospy.Duration(5.0))):
            if arming_client.call(arm_cmd).success:
                rospy.loginfo("Vehicle armed")
                rospy.loginfo("Take off at 1.5 meter... to position=(%.2f,%.2f,%.2f) yaw=%.2f",
                              current_goal.position.x, current_goal.position.y, current_goal.position.z, current_goal.yaw)
                last_request = rospy.Time.now()

        # 若速度與 yaw_rate 為負值（如搖桿下拉），則執行解鎖
        elif (current_goal.velocity.z < -0.4 and current_goal.yaw_rate < -0.4 and
              (rospy.Time.now() - last_request > rospy.Duration(5.0))):
            if command_client.call(disarm_cmd).success:
                rospy.loginfo("Vehicle disarmed")
                rospy.signal_shutdown("Disarmed by stick command")
            else:
                rospy.loginfo("Disarming failed! Still in flight?")
                last_request = rospy.Time.now()

        # 若超過 1 秒未收到速度指令，則切回位置控制模式
        if (current_goal.header.stamp.to_sec() - lastTwistReceived.to_sec() > 1 and
            current_goal.type_mask != POSITION_CONTROL):
            current_goal.coordinate_frame = PositionTarget.FRAME_LOCAL_NED
            current_goal.type_mask = POSITION_CONTROL
            current_goal.position.x = current_pose.pose.position.x
            current_goal.position.y = current_pose.pose.position.y
            current_goal.position.z = 1.5
            q = current_pose.pose.orientation
            current_goal.yaw = get_yaw_from_quaternion(q)
            rospy.loginfo("Switch to position control (x=%.2f, y=%.2f, z=%.2f, yaw=%.2f)",
                          current_goal.position.x, current_goal.position.y, current_goal.position.z, current_goal.yaw)

        # === 新增：收到目標點指令才會飛行 ===
        if target_received and target_pose is not None:
            # 目標點模式：切換為位置控制，並設定目標點
            current_goal.coordinate_frame = PositionTarget.FRAME_LOCAL_NED
            current_goal.type_mask = POSITION_CONTROL
            current_goal.position.x = target_pose.pose.position.x
            current_goal.position.y = target_pose.pose.position.y
            current_goal.position.z = target_pose.pose.position.z
            q = target_pose.pose.orientation
            current_goal.yaw = get_yaw_from_quaternion(q)
            rospy.loginfo("執行目標點飛行: x=%.2f, y=%.2f, z=%.2f, yaw=%.2f",
                          current_goal.position.x, current_goal.position.y, current_goal.position.z, current_goal.yaw)
            target_received = False

        # 發布 setpoint 與 vision pose，供 MAVROS 及 PX4 使用
        current_goal.header.stamp = rospy.Time.now()
        local_pos_pub.publish(current_goal)
        current_pose.header.stamp = current_goal.header.stamp
        vision_pos_pub.publish(current_pose)
        rate.sleep()

```
</details>

## 3. ORB-SLAM3指令與流程
### ORB-SLAM3
ORB-SLAM3 是目前最先進的開源視覺 SLAM 方案之一，其最大的優勢在於支援多種感測器組合（單目、雙目、RGB-D，以及各自的慣性融合版本）並具備高精度的定位與地圖重建能力。它基於特徵點法，透過稀疏地圖進行定位，並整合了完整的重定位、迴圈閉環與地圖合併功能。
專案的 GitHub 儲存庫與詳細安裝教學請參考：[ORB-SLAM3](https://github.com/UZ-SLAMLab/ORB_SLAM3)
雖然 ORB-SLAM3 官方提供了 ROS 範例，但我們選用了一個由社群開發、更專注於 ROS 整合的版本：[orb_slam3_ros](https://github.com/thien94/orb_slam3_ros)

### **1. 啟動 ORB-SLAM3 節點**
使用 `roslaunch` 指令啟動對應的 SLAM 模式。
```
#舉例
roslaunch orb_slam3_ros_wrapper t265.launch
```

### **2. 播放 Rosbag 資料集**
```
#舉例
rosbag play --pause MVS/Dataset/indoor_flying1_data.bag \
/davis/left/image_raw:=/camera/left/image_raw \
/davis/right/image_raw:=/camera/right/image_raw

**指令參數詳解：**
-   `--pause`: **(關鍵參數)** 播放開始時立即暫停。
-   `MVS/Dataset/... .bag`: 指定要播放的 `.bag` 檔案路徑。
-   `/davis/left/image_raw:=/camera/left/image_raw`: 
    這是 **Topic 重對映 (Remapping)**，也可以直接寫在launch檔裡。
```
ORB-SLAM3 畫面如下，會顯示bag檔影像的畫面，以及演算法的定位軌跡。
<figure style="text-align: center;">
  <img src="https://hackmd.io/_uploads/Bkcf2mSElx.png" alt="ORB-SLAM3 Rviz 畫面">
  <figcaption style="font-style: italic;">圖2：ORB-SLAM3 移動軌跡</figcaption>
</figure>

使用dronetest launch檔可以接取模擬無人機環境中的影像，指令如下：
```
＃同時運行rtabmap_drone_example
roslaunch orb_slam3_ros_wrapper dronetest.launch
```
<figure style="text-align: center;">
  <img src="https://hackmd.io/_uploads/Hkw5TQSNgx.png" alt="ORB-SLAM3 Rviz 畫面">
  <figcaption style="font-style: italic;">圖3：模擬環境中無人機飛行軌跡</figcaption>
</figure>

## 4. YOLO指令與流程
**同步訂閱數據**：節點會同時訂閱來自模擬器的兩個關鍵 Topic：
    -   `/camera/color/image_raw`: 標準的彩色影像，用於 YOLO 進行物件辨識。
    -   `/camera/depth/image_raw`: **深度影像**。這是一張灰階圖，但每個像素點的值直接對應著該點在真實世界中的距離（單位：公尺）。
    
### **1. 安裝 Ultralytics YOLO**
透過 `pip` 即可一鍵安裝所有必要的套件。
```
pip install ultralytics
```
### **2. 啟動YOLO節點**
```
roslaunch orb_slam3_ros_wrapper yolo_detect.launch
```
運行後，會接收模擬環境中無人機的拍攝到的影像，做物件偵測以及顯示該物體與無人機的距離（深度）
<figure style="text-align: center;">
  <img src="https://hackmd.io/_uploads/H1Ici4BEel.png" alt="YOLO偵測結果">
  <figcaption style="font-style: italic;">圖4：YOLO偵測結果</figcaption>
</figure>


## 5. EVO軌跡評估工具
在完成 SLAM 任務後，我們需要一個客觀、量化的方法來評估演算法的表現。我們使用 EVO (Evaluation of Odometry and SLAM)評估工具。可參考[這篇](https://blog.csdn.net/qq_28087491/article/details/129043366)
> 評估指標：APE vs. RPE
> 
> **絕對軌跡誤差 (Absolute Pose Error - APE)**
> **評估目的**：衡量軌跡的**全域一致性 (Global Consistency)**。它直接比較估計軌跡與真實軌跡（Ground Truth）在對齊後的位置差異，最終給出一個整體的誤差值（如 RMSE - 均方根誤差）。APE 值越小，代表整個軌跡的形狀和位置越接近真實情況。
> 
> **相對位姿誤差 (Relative Pose Error - RPE)**
> **評估目的**：衡量系統的**局部精度或漂移 (Drift)**。它關注的是軌跡在短時間內的變化量，非常適合用來評估系統的漂移累積速度。例如，它可以告訴我們無人機每飛行 1 公尺，會產生多少公分的平移或角度誤差。

### evo_traj：軌跡視覺化與初步檢查
```
evo_traj tum \
Examples/ROS/ORB_SLAM3/FrameTrajectory_TUM_Format.txt \
--ref MVS_inflying1_gt_od.tum \
-as -p --plot_mode=xyz

#參數說明
tum: 指定輸入檔案的格式為 TUM。
FrameTrajectory_TUM_Format.txt: SLAM 系統輸出的估計軌跡。
--ref MVS_inflying1_gt_od.tum: 指定真實軌跡 (Ground Truth)。
-as: 表示在繪圖前，先對兩條軌跡進行對齊與尺度校正 (Sim(3) alignment)。
-p 或 --plot: 指示 EVO 產生一個繪圖視窗。
--plot_mode=xyz: 指定繪製一個 3D 軌跡圖。
```
<figure style="text-align: center;">
  <img src="https://hackmd.io/_uploads/S1Aa8OBNxx.png" alt="seq6_trajetories">
  <figcaption style="font-style: italic;">圖5：軌跡視覺化比較圖</figcaption>
</figure>


### evo_ape：計算絕對軌跡誤差
```
evo_ape tum \
MVS_inflying1_gt_od.tum \
Examples/ROS/ORB_SLAM3/FrameTrajectory_TUM_Format.txt \
--align --plot
```

### evo_rpe：計算相對位姿誤差
```
evo_rpe tum \
MVS_inflying1_gt_od.tum \
Examples/ROS/ORB_SLAM3/FrameTrajectory_TUM_Format_second.txt \
--pose_relation angle_deg --delta 1 --delta_unit m --plot
```

## 6. ROS常用除錯工具與技巧
### rostopic：檢查Topic狀況
`rostopic` 是一個多功能的指令列工具集，用於深入探查單一 Topic 的詳細資訊。
**常用指令**：
1.  **`rostopic list`**
    -   **用途**：列出當前所有正在被發布的 Topic。
2.  **`rostopic info <topic_name>`**
    -   **用途**：顯示特定 Topic 的詳細資訊，包括它的訊息類型、發布者和訂閱者。
3.  **`rostopic echo <topic_name>`**
    -   **用途**：**(最重要的除錯工具)** 直接在終端機中即時印出某個 Topic 正在傳輸的原始數據內容。
### rqt_image_view：視覺化影像數據
**用途**：對於影像類的 Topic，`rostopic echo` 會輸出一大堆無意義的數字。`rqt_image_view` 則提供了一個圖形化介面，可以直接將影像 Topic 的內容以圖片形式顯示出來。
```
#啟動
rqt_image_view
```
> 啟動後，在左上角的下拉式選單中，選擇您想查看的影像 Topic 即可。


## 7. 實體無人機參數設定及啟動流程
這裡彙整了從模擬環境轉向實體無人機時，所需的參數設定與軟體啟動流程。

### QGroundControl
<figure style="text-align: center;">
  <img src="https://hackmd.io/_uploads/Hy1wnA0kbl.png
" alt="QGC">
  <figcaption style="font-style: italic;">圖6：QGroundControl 軟體介面圖</figcaption>
</figure>

開源的地面控制站 (GCS) 軟體，主要使用到的功能如下:
- 參數配置：這是 QGC 最核心的功能之一。您可以存取飛控（如 PX4）中的所有參數。
- 硬體校準：包括校準指南針（羅盤）、加速度計、陀螺儀、遙控器 (RC) 和電變 (ESC) 等。
- 飛行模式：設定遙控器上的開關對應到哪些飛行模式（例如：Manual、Position、Offboard）。

### 7.1 PX4 飛控參數設定 (QGroundControl)

這一步是讓飛控「知道」它需要聽取外部視覺定位數據（如 ORB-SLAM3），而不是依賴 GPS。所有這些參數都可以在 QGroundControl (QGC) 中進行設定。

> 進入參數設定： 在 QGC 中，點選上方工具列的**「參數」**（Parameters）選項。

* **`EKF2_HGT_MODE`**
    * **用途**：設定高度估計的來源。
    * **設定**：在搜尋框中輸入 `EKF2_HGT_MODE`。將這個參數的值從 `GPS` 改為 **`Vision`** 。這會告訴飛控的高度估計應該來自外部視覺系統。(如果要室內飛行的話，要轉成依靠視覺定位)

* **`EKF2_EV_DELAY`**
    * **用途**：設定視覺數據從感應器傳輸到飛控的延遲時間。
    * **設定**：搜尋 `EKF2_EV_DELAY`。精確的數值非常關鍵。可以先從一個保守的值開始，例如 **100 毫秒 (100ms)**，然後根據實際情況進行微調。如果飛行時出現抖動或不穩定，這個值可能需要調整。(這是室內飛行才會用到的)

* **`MPC_XY_VEL_MAX`**
    * **用途**：最大水平速度限制。這是飛控允許的最高水平速度（單位 m/s）。
    * **設定**：在位置模式 (Position Mode) 或 Offboard 模式下，您透過搖桿或程式碼（例如 MAVROS 的 `setpoint_velocity`）發出的指令速度，最終會被限制在這個值以下。(慢速飛行比較能防止暴衝)

* **`MPC_LAND_SPEED`**
    * **用途**：修改自動降落 (Auto Land) 時的下降速度。
    * **設定**：可透過 `rosservice` 動態調整。例如，設定為 **0.3 m/s** 
    (QGC只能調到0.6m/s 可以用指令調)
        ```bash
        # 修改 Auto Land 降落速度
        rosservice call /mavros/param/set "{param_id: 'MPC_LAND_SPEED', value: {integer: 0, real: 0.3}}"
        
        # 查看目前參數
        rosservice call /mavros/param/get "{param_id: 'MPC_LAND_SPEED'}"
        ```

<figure style="text-align: center;">
  <img src="https://hackmd.io/_uploads/HJD5h0R1Zx.png
" alt="QGC">
  <figcaption style="font-style: italic;">圖7：Parameters 介面圖</figcaption>
</figure>


### 7.2 實體飛行啟動流程 (ORB-SLAM3 + Mavros)

這是在實體無人機上運行視覺定位的啟動順序。(可以啟動但尚未實機飛行)

1.  **啟動 MavROS (無人機)**
    * **用途**：建立機載電腦 (G700) 與 PX4 飛控之間的 MAVLink 通訊。
    * **指令** (範例)：
        ```bash
        # 假設飛控連接到 /dev/ttyS2，波特率為 57600
        roslaunch mavros px4.launch fcu_url:=/dev/ttyS2:57600
        ```
       

2.  **開啟相機 SDK**
    * **用途**：啟動 RealSense 相機驅動，發布影像 Topic。
    * **指令** (範例：D415)：
        ```bash
        # 啟動紅外線雙目
        roslaunch realsense2_camera rs_camera.launch \
                infra_width:=1280 infra_height:=720 infra_fps:=30 \
                enable_infra1:=true enable_infra2:=true
        ```
       

3.  **開啟 ORB-SLAM3**
    * **用途**：運行 SLAM 演算法，訂閱相機 Topic，並發布估計的攝影機姿態。
    * **指令** (範例)：
        ```bash
        # 啟動適用於 RealSense 的 SLAM 節點
        roslaunch orb_slam3_ros realsense_d415.launch
        ```
       

4.  **開啟 POSE 資訊轉換 (SLAM -> Mavros)**
    * **用途**：啟動橋接腳本（見 7.3 節），將 ORB-SLAM3 輸出的姿態 Topic (如 `/orb_slam3_stereo/camera_pose`) 轉換並發布到 MAVROS 監聽的 Topic (如 `/mavros/vision_pose/pose`)。
    * **指令** (範例)：
        ```bash
        # 假設你的橋接腳本名稱為 orb_mavros.py
        rosrun realsense2_camera orb_mavros.py
        ```
       

5.  **驗證資料流**
    * **用途**：確認 MAVROS 確實有接收到來自橋接腳本的視覺定位數據。
    * **指令**：
        ```bash
        rostopic echo mavros/vision_pose/pose
        ```
       
    * **檢查**：如果終端機持續輸出 `PoseStamped` 訊息，表示整個視覺定位管線已成功運作。

### 7.3 範例：SLAM 定位至 MavLink 橋接腳本

這個 Python 腳本（節點）的任務是訂閱 ORB-SLAM3 的姿態輸出，並將其轉發給 MAVROS。

```python=
#!/usr/bin/env python3

import rospy
from geometry_msgs.msg import PoseStamped
import time

class SlamToMavrosBridge:
    def __init__(self):
        """
        初始化 SLAM 到 Mavros 的橋接節點。
        """
        rospy.init_node('slam_to_mavros_bridge', anonymous=True)
        
        # 建立一個發布者，將 PoseStamped 訊息發布給 Mavros
        # Mavros 會監聽 /mavros/vision_pose/pose 這個 Topic 來獲取外部定位資料
        self.vision_pose_pub = rospy.Publisher('/mavros/vision_pose/pose', PoseStamped, queue_size=10)
        
        # 建立一個訂閱者，訂閱 ORB-SLAM3 發布的定位資訊
        # 根據你的 ORB-SLAM3 設置，這個 Topic 名稱可能需要調整
        rospy.Subscriber('/orb_slam3_stereo/camera_pose', PoseStamped, self.vision_pose_callback)
        
        # 等待 Mavros 服務啟動
        rospy.loginfo("Waiting for Mavros to be ready...")
        time.sleep(5) # 暫停 5 秒，確保 Mavros 已啟動

        rospy.loginfo("Bridge node started. Publishing vision poses to Mavros.")

    def vision_pose_callback(self, data):
        """
        將 ORB-SLAM3 的 PoseStamped 訊息直接轉發給 Mavros。
        此處假設 ORB-SLAM3 的輸出座標系與 Mavros 的期望座標系（例如 ENU）一致。
        """
        # 創建一個新的 PoseStamped 訊息，以確保時間戳是最新的
        px4_pose = PoseStamped()
        px4_pose.header = data.header
        px4_pose.header.stamp = rospy.Time.now()

        # 直接複製位置和姿態
        px4_pose.pose.position = data.pose.position
        px4_pose.pose.orientation = data.pose.orientation

        # 發布訊息
        self.vision_pose_pub.publish(px4_pose)

if __name__ == '__main__':
    try:
        # 創建並運行橋接器
        bridge = SlamToMavrosBridge()
        rospy.spin()
    except rospy.ROSInterruptException:
        pass
```
