در حال بارگذاری 0%
// منو.exe
خانه رزومه بلاگ تماس سفارش
English فارسی
~/blog / robotics / slam

ربات چگونه می‌فهمد کجاست، وقتی GPS ندارد؟ آشنایی عملی با SLAM

فرض کن یک ربات وارد انباری می‌شود که قبلاً آن را ندیده است. چند ردیف قفسه، یک راهرو باریک و یک ستون وسط سالن قرار دارد. GPS در این محیط، اگر هم سیگنالی داشته باشد، برای حرکت دقیق میان قفسه‌ها کافی نیست. ربات باید از همین چیزهایی که می‌بیند بفهمد کجاست و راهی را که آمده به خاطر بسپارد.

اینجا سؤال جالبی پیش می‌آید: ربات برای پیدا کردن موقعیتش به نقشه نیاز دارد، اما برای ساختن نقشه هم باید بداند هنگام هر مشاهده کجا بوده است. چطور می‌شود این دو مسئله را با هم حل کرد؟

ربات چگونه می‌فهمد کجاست، وقتی GPS ندارد؟ آشنایی عملی با SLAM

# SLAM دقیقاً چه چیزی را تخمین می‌زند؟

پاسخ، خانواده‌ای از روش‌ها به نام SLAM است: ساختن نقشه و تخمین موقعیت، به‌صورت هم‌زمان. در این مقاله اول منطق آن را باز می‌کنیم، بعد یک نمونهٔ کوچک را با پایتون اجرا می‌کنیم؛ نمونه‌ای که هم مسیر ربات و هم موقعیت نشانه‌های محیط را تخمین می‌زند.
SLAM مخفف Simultaneous Localization and Mapping است؛ یعنی «مکان‌یابی و نقشه‌سازی هم‌زمان».
در ساده‌ترین حالت دوبعدی، وضعیت ربات سه مؤلفه دارد: مختصات افقی x، مختصات عمودی y و زاویهٔ جهت‌گیری θ. به این مجموعه، Pose می‌گوییم. دانستن موقعیت بدون جهت‌گیری کافی نیست؛ رباتی که وسط راهرو ایستاده باید بداند رو به کدام سمت است.
نقشه هم همیشه یک تصویر شبیه Google Maps نیست. ممکن است مجموعه‌ای از نقاط شاخص، یک شبکهٔ اشغال یا یک ابرنقاط سه‌بعدی باشد. شکل نقشه به حسگر و کاربرد بستگی دارد. در تمرین این مقاله، نقشه فقط مختصات تعدادی نشانهٔ ثابت است.
وقتی ربات کار را شروع می‌کند، می‌توانیم محل آغاز را مبدأ دستگاه مختصات قرار بدهیم. در این صورت «دو متر جلوتر از نقطهٔ شروع» معنی دارد، حتی اگر طول و عرض جغرافیایی ربات را ندانیم. SLAM بدون یک مرجع بیرونی لزوماً مختصات جغرافیایی مطلق تولید نمی‌کند؛ موقعیت را در چارچوب نقشهٔ خودش تخمین می‌زند.

# چرا شمردن دور چرخ‌ها کافی نیست؟

یک راه اولیه برای تخمین حرکت، استفاده از انکودر چرخ‌هاست. اگر قطر چرخ و تعداد دورها را بدانیم، می‌توانیم مقدار جابه‌جایی را حساب کنیم. این روش بخشی از اودومتری است: تخمین حرکت نسبی ربات در طول زمان.
اما کف زمین همیشه صاف نیست، چرخ ممکن است بلغزد و اندازه‌گیری‌ها هم بی‌خطا نیستند. اگر فقط حرکت‌های کوچک را به هم اضافه کنیم، خطاها به‌تدریج جمع می‌شوند. به این انحراف انباشته، Drift می‌گوییم.
مثلاً ربات یک دور کامل دور قفسه‌ها می‌زند و از نظر فیزیکی به نقطهٔ شروع برمی‌گردد؛ ولی تخمین اودومتری ممکن است پایان مسیر را یک متر آن‌طرف‌تر قرار دهد. خطای زاویه‌ای هم دردسرساز است: جهت اشتباه باعث می‌شود حرکت‌های بعدی در راستای اشتباه روی نقشه ثبت شوند.
SLAM برای اصلاح این تخمین از محیط کمک می‌گیرد. اگر یک ستون ثابت را دوباره ببیند، مشاهدات جدید را با اطلاعات قبلی مقایسه می‌کند و از این ارتباط برای اصلاح مسیر و نقشه استفاده می‌کند.

# ربات با چه حسگرهایی محیط را می‌بیند؟

یک سامانهٔ SLAM می‌تواند از ترکیب‌های مختلف حسگر استفاده کند:
حسگر چه اطلاعاتی می‌دهد؟ محدودیت مهم
انکودر چرخ حرکت تقریبی چرخ‌ها و اودومتری لغزش، خطای مدل حرکت و تجمع خطا
LiDAR فاصله تا سطح‌های اطراف در جهت‌های مختلف کیفیت بازتاب، هندسهٔ محیط و اجسام متحرک
دوربین ویژگی‌های تصویری و تغییر آن‌ها بین فریم‌ها نور، تاری حرکت و سطح‌های کم‌بافت
دوربین عمق یا استریو اطلاعات تصویری همراه با تخمین عمق برد، کالیبراسیون و شرایط تصویربرداری
IMU سرعت زاویه‌ای و شتاب ویژه بایاس و خطای انتگرال‌گیری
هیچ‌کدام از این حسگرها در همهٔ شرایط بی‌نقص نیستند. ارزش ترکیب حسگرها این است که هرکدام بخشی از ضعف دیگری را جبران می‌کند. یک دوربین تک‌چشمیِ تنها، بدون اطلاعات اضافی برای تعیین مقیاس، معمولاً مقیاس متری مسیر و نقشه را مشخص نمی‌کند. وجود عمق معتبر یا یک ترکیب مناسب از حسگرها می‌تواند این ابهام را برطرف کند.
در تمرین ما، حسگر فرضی مستقیماً فاصله و زاویه تا نشانه‌ها را اندازه می‌گیرد. این ساده‌سازی اجازه می‌دهد اصل مسئله را ببینیم، بدون اینکه فعلاً وارد استخراج ویژگی از تصویر یا پردازش اسکن لیزری شویم.

# مسئلهٔ مرغ و تخم‌مرغ چگونه حل می‌شود؟

فرض کن ربات یک ستون را در فاصلهٔ سه متری سمت راست خودش دیده است. برای گذاشتن این ستون روی نقشه باید بداند خودش هنگام مشاهده کجا بوده و رو به کدام سمت قرار داشته است. اگر تخمین وضعیت ربات اشتباه باشد، مختصات ستون هم اشتباه ثبت می‌شود.
از طرف دیگر، همین ستون می‌تواند کمک کند وضعیت ربات را اصلاح کنیم. اگر ربات کمی جلو برود و آن را دوباره ببیند، دو مشاهده باید با یک ستون ثابت سازگار باشند.
پس به‌جای قطعی فرض کردن یکی از این دو، آن‌ها را با هم تخمین می‌زنیم. در یک روش مبتنی بر بهینه‌سازی، مجموعه‌ای از وضعیت‌های ربات و نشانه‌های نقشه را طوری تغییر می‌دهیم که با اندازه‌گیری‌های حرکت و محیط بیشترین سازگاری را داشته باشند.
برای نشانه‌ای با مختصات (lx, ly) و رباتی با وضعیت (x, y, θ)، فاصلهٔ پیش‌بینی‌شده برابر است با:
r = sqrt((lx − x)² + (ly − y)²)
فاصلهٔ پیش‌بینی‌شدهٔ ربات تا نشانه، از روی وضعیت فعلی و مختصات نقشه
و زاویهٔ نسبی پیش‌بینی‌شده از این رابطه به دست می‌آید:
β = atan2(ly − y, lx − x) − θ
زاویهٔ نشانه نسبت به جهتِ رو به جلوی ربات
اختلاف بین این پیش‌بینی‌ها و اندازه‌گیری حسگر، خطای مشاهده است:
ⓘ
چرا اختلاف زاویه‌ها را به بازهٔ مناسب برمی‌گردانیم؟ برای زاویه باید اختلاف را به بازهٔ مناسب برگردانیم؛ وگرنه دو جهت نزدیک به مرز منفی و مثبت π ممکن است به‌اشتباه خیلی دور از هم به نظر برسند.
مشاهدات هم وزن یکسانی ندارند. اندازه‌گیری‌ای که عدم‌قطعیت کمتری دارد باید اثر بیشتری روی پاسخ بگذارد. در کد، هر مؤلفهٔ خطا را بر انحراف معیار همان مؤلفه تقسیم می‌کنیم؛ بنابراین خطای فاصله برحسب متر و خطای زاویه برحسب رادیان مستقیماً با هم جمع نمی‌شوند.

# جلوی صحنه و پشت صحنهٔ SLAM

در بسیاری از سامانه‌ها، کار میان دو بخش تقسیم می‌شود. Front-end دادهٔ حسگرها را پردازش می‌کند، مشاهدات متناظر را پیدا می‌کند و تخمین حرکت یا قیدهای اولیه را می‌سازد. Back-end این قیدها را کنار هم قرار می‌دهد و مسیر و نقشه را اصلاح می‌کند.
یک مثال مشخص، Cartographer است: بخش محلی آن زیرنقشه می‌سازد و بخش سراسری ارتباط بین زیرنقشه‌ها و قیدهای بازدید دوباره را برای سازگاری بیشتر نقشه بهینه می‌کند. جزئیات این تقسیم کار در مستندات رسمی Cartographer آمده است.
در نمونهٔ کوچک ما، داده‌های حسگر شبیه‌سازی شده‌اند و شناسهٔ نشانه‌ها را از قبل می‌دانیم. بخش دشوار پیدا کردن اینکه «این نقطه همان نقطهٔ قبلی است یا نه» حذف شده تا بتوانیم روی تخمین مشترک تمرکز کنیم.

# وقتی ربات می‌گوید: «اینجا را قبلاً دیده‌ام»

به تشخیص بازگشت به یک مکان قبلی، Loop Closure یا «بستن حلقه» می‌گوییم. این تشخیص یک ارتباط تازه میان بخش‌هایی از مسیر ایجاد می‌کند که ممکن است از نظر زمانی فاصلهٔ زیادی داشته باشند.
فرض کن ربات از در ورودی شروع کرده، میان قفسه‌ها دور زده و دوباره ورودی را دیده است. این مشاهده می‌تواند نشان بدهد که مسیر تخمین‌زده‌شده با نقشهٔ قبلی سازگار نیست. بهینه‌سازی از قید تازه استفاده می‌کند تا خطا را میان وضعیت‌های مختلف مسیر توزیع کند؛ بنابراین ممکن است نقاط قبلی نقشه هم جابه‌جا شوند.
بازدید دوباره همیشه به معنی رسیدن به نقطهٔ شروع نیست. دیدن همان نشانه یا همان بخش محیط از زاویه‌ای دیگر هم می‌تواند اطلاعات ارزشمندی ایجاد کند. در مقابل، تشخیص اشتباه یک مکان مشابه ممکن است قید نادرست بسازد. کیفیت تشخیص و اعتبارسنجی این ارتباط‌ها بسیار مهم است.

# تمرین عملی: یک SLAM دوبعدی کوچک بسازیم

برای این تمرین به ربات واقعی، GPS یا ROS نیاز نداریم. یک ربات را در مسیر مربعی شبیه‌سازی می‌کنیم و داده‌های اندازه‌گیری را به آن می‌دهیم.
این آزمایش شامل ۲۵ وضعیت ربات، ۱۲ نشانهٔ ثابت و ۹۵ مشاهدهٔ فاصله و زاویه است. اندازه‌گیری‌های اودومتری و محیط نویز دارند. مختصات واقعی مسیر و نشانه‌ها فقط برای ساختن دادهٔ شبیه‌سازی و ارزیابی نتیجه استفاده می‌شوند؛ تابع حل‌کننده آن‌ها را دریافت نمی‌کند.
حل‌کننده، وضعیت نخست را در مبدأ ثابت می‌کند و بقیهٔ وضعیت‌ها و مختصات همهٔ نشانه‌ها را با هم تخمین می‌زند. ثابت کردن مبدأ یک انتخاب برای دستگاه مختصات است؛ مختصات GPS به مسئله اضافه نمی‌کند.
فایل slam_demo.py از همان کدی که پایین می‌بینی ساخته شده است. کتابخانه‌های موردنیاز را نصب کن:
terminal
python -m pip install numpy scipy matplotlib
بعد فایل را اجرا کن:
terminal
python slam_demo.py
کد از least_squares در SciPy برای حل مسئلهٔ کمترین مربعات غیرخطی استفاده می‌کند. الگوی وابستگی خطاها به متغیرها هم مشخص شده تا حل‌کننده از ساختار تنک مسئله استفاده کند؛ مستندات رسمی SciPy این گزینه را با نام jac_sparsity توضیح می‌دهد.
slam_demo.py
"""Educational joint 2D landmark SLAM; no GPS or known landmark coordinates.
 
Run: python slam_demo.py
Writes: slam_result.png and results.json beside this script.
"""
import json
from pathlib import Path
 
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
from scipy.optimize import least_squares
from scipy.sparse import lil_matrix
 
 
def wrap(a):
    return (a + np.pi) % (2 * np.pi) - np.pi
 
 
def rotation(a):
    return np.array([[np.cos(a), -np.sin(a)],
                     [np.sin(a), np.cos(a)]])
 
 
def relative_motion(a, b):
    delta = rotation(a[2]).T @ (b[:2] - a[:2])
    return np.r_[delta, wrap(b[2] - a[2])]
 
 
def make_data():
    rng = np.random.default_rng(1616)
    corners = np.array([[0., 0.], [4., 0.], [4., 4.], [0., 4.], [0., 0.]])
    xy = np.vstack([np.linspace(corners[k], corners[k+1], 6, endpoint=False)
                    for k in range(4)] + [corners[-1:]])
    yaw = np.r_[np.repeat([0., np.pi/2, np.pi, -np.pi/2], 6), 0.]
    truth = np.c_[xy, yaw]
    landmarks = np.array([[-1., -1.], [2., -1.], [5., -1.], [5., 2.],
                          [5., 5.], [2., 5.], [-1., 5.], [-1., 2.],
                          [1., 1.], [3., 1.], [3., 3.], [1., 3.]])
    odom_sigma = np.array([0.10, 0.10, 0.04])
    sensor_sigma = np.array([0.06, np.deg2rad(0.8)])
    odom = np.array([relative_motion(a, b) for a, b in zip(truth[:-1], truth[1:])])
    odom += rng.normal(size=odom.shape) * odom_sigma
    observations = []
    for i, pose in enumerate(truth):
        count = 3 if i in (0, 5, 10, 15, 20) else 4
        nearest = np.argsort(np.linalg.norm(landmarks - pose[:2], axis=1))[:count]
        for j in nearest:
            d = landmarks[j] - pose[:2]
            measurement = np.array([np.linalg.norm(d),
                                    wrap(np.arctan2(d[1], d[0]) - pose[2])])
            measurement += rng.normal(size=2) * sensor_sigma
            measurement[1] = wrap(measurement[1])
            observations.append((i, int(j), *measurement))
    return truth, landmarks, odom, observations, odom_sigma, sensor_sigma
 
 
def solve(odom, observations, n_landmarks, odom_sigma, sensor_sigma):
    n_poses = len(odom) + 1
    initial_poses = np.zeros((n_poses, 3))
    for i, motion in enumerate(odom):
        previous = initial_poses[i]
        initial_poses[i+1, :2] = previous[:2] + rotation(previous[2]) @ motion[:2]
        initial_poses[i+1, 2] = wrap(previous[2] + motion[2])
    initial_map = np.zeros((n_landmarks, 2))
    seen = set()
    for i, j, distance, bearing in observations:
        if j not in seen:
            angle = initial_poses[i, 2] + bearing
            initial_map[j] = initial_poses[i, :2] + distance * np.array([np.cos(angle), np.sin(angle)])
            seen.add(j)
    if len(seen) != n_landmarks:
        raise ValueError("Every landmark must be observed at least once.")
    pose_size = 3 * (n_poses - 1)
    x0 = np.r_[initial_poses[1:].ravel(), initial_map.ravel()]
 
    def unpack(x):
        # Fixed origin removes global translation/rotation ambiguity.
        poses = np.vstack([np.zeros(3), x[:pose_size].reshape(-1, 3)])
        return poses, x[pose_size:].reshape(-1, 2)
 
    def residuals(x):
        poses, map_xy = unpack(x)
        errors = []
        for i, measurement in enumerate(odom):
            error = relative_motion(poses[i], poses[i+1]) - measurement
            error[2] = wrap(error[2])
            errors.extend(error / odom_sigma)
        for i, j, distance, bearing in observations:
            d = map_xy[j] - poses[i, :2]
            expected_bearing = wrap(np.arctan2(d[1], d[0]) - poses[i, 2])
            error = np.array([np.linalg.norm(d) - distance,
                              wrap(expected_bearing - bearing)])
            errors.extend(error / sensor_sigma)
        return np.array(errors)
 
    pattern = lil_matrix((3 * len(odom) + 2 * len(observations), len(x0)), dtype=int)
    for i in range(len(odom)):
        rows = slice(3*i, 3*i+3)
        if i > 0:
            pattern[rows, 3*(i-1):3*i] = 1
        pattern[rows, 3*i:3*i+3] = 1
    offset = 3 * len(odom)
    for k, (i, j, _, _) in enumerate(observations):
        rows = slice(offset+2*k, offset+2*k+2)
        if i > 0:
            pattern[rows, 3*(i-1):3*i] = 1
        pattern[rows, pose_size+2*j:pose_size+2*j+2] = 1
    result = least_squares(residuals, x0, jac_sparsity=pattern.tocsr(),
                           x_scale="jac", max_nfev=500,
                           ftol=1e-9, xtol=1e-9, gtol=1e-9)
    if not result.success:
        raise RuntimeError(result.message)
    poses, map_xy = unpack(result.x)
    return initial_poses, poses, map_xy
 
 
def rmse(estimate, truth):
    return float(np.sqrt(np.mean(np.sum((estimate[:, :2] - truth[:, :2])**2, axis=1))))
 
 
def main():
    truth, landmarks, odom, obs, odom_sigma, sensor_sigma = make_data()
    initial, optimized, map_xy = solve(odom, obs, len(landmarks), odom_sigma, sensor_sigma)
    seen = set()
    first_only = []
    for observation in obs:
        if observation[1] not in seen:
            seen.add(observation[1])
            first_only.append(observation)
    _, no_revisit, _ = solve(odom, first_only, len(landmarks), odom_sigma, sensor_sigma)
    results = {
        "poses": len(truth), "landmarks": len(landmarks), "observations": len(obs),
        "odometry_rmse_m": rmse(initial, truth),
        "slam_rmse_m": rmse(optimized, truth),
        "first_observation_only_rmse_m": rmse(no_revisit, truth),
        "odometry_endpoint_gap_m": float(np.linalg.norm(initial[-1, :2] - initial[0, :2])),
        "slam_endpoint_gap_m": float(np.linalg.norm(optimized[-1, :2] - optimized[0, :2])),
    }
    output = Path(__file__).resolve().parent
    (output / "results.json").write_text(json.dumps(results, indent=2), encoding="utf-8")
    print(json.dumps(results, indent=2))
    fig, ax = plt.subplots(figsize=(9, 6), constrained_layout=True)
    fig.patch.set_facecolor("#050505")
    ax.set_facecolor("#0a0f0a")
    ax.plot(*truth[:, :2].T, "--", color="#c8ffd4", linewidth=1.6, label="True trajectory")
    ax.plot(*initial[:, :2].T, "o-", color="#ffb000", markersize=3, label="Odometry")
    ax.plot(*optimized[:, :2].T, "o-", color="#00ff41", markersize=3, label="Joint SLAM")
    ax.scatter(*landmarks.T, marker="x", color="#7d8f7d", s=60, label="True landmarks")
    ax.scatter(*map_xy.T, marker="+", color="#00fff9", s=70, label="Estimated landmarks")
    ax.set(xlabel="x (m)", ylabel="y (m)", title="2D landmark SLAM: poses and map estimated together")
    ax.set_aspect("equal")
    ax.grid(alpha=0.15, color="#c8ffd4", linewidth=0.5)
    ax.tick_params(colors="#9fcdaa", labelsize=9)
    for side in ax.spines.values():
        side.set_color("#234530")
    ax.xaxis.label.set_color("#c8ffd4")
    ax.yaxis.label.set_color("#c8ffd4")
    ax.title.set_color("#c8ffd4")
    legend = ax.legend(loc="upper right", fontsize=8)
    legend.get_frame().set_facecolor("#0a0f0a")
    legend.get_frame().set_edgecolor("#234530")
    for text in legend.get_texts():
        text.set_color("#c8ffd4")
    fig.savefig(output / "slam_result.png", dpi=180)
    plt.close(fig)
 
 
if __name__ == "__main__":
    main()

# نتیجهٔ اجرای همین کد

با بذر تصادفی ثابت 1616، این نسخه هنگام اجرا نتیجه‌های زیر را تولید کرد. اعداد گرد شده‌اند؛ اختلاف‌های کوچک میان نسخه‌های کتابخانه‌ها ممکن است رخ دهد.
معیار فقط اودومتری SLAM با مشاهدات تکراری
RMSE موقعیت در کل مسیر ۱٫۳۳۸ متر ۰٫۰۸۲ متر
فاصلهٔ تخمینی پایان مسیر تا شروع ۱٫۸۷۰ متر ۰٫۰۶۰ متر
RMSE موقعیت را این‌طور محاسبه می‌کنیم: برای هر وضعیت، مربع فاصلهٔ دوبعدی تخمین تا موقعیت واقعی را می‌گیریم؛ میانگین این مقادیر را حساب می‌کنیم و در پایان جذر می‌گیریم. مقایسه در همان دستگاه مختصات محلیِ ثابت‌شده انجام می‌شود.
مقایسهٔ مسیر واقعی، اودومتری و تخمین مشترک SLAM
مسیر واقعی (خط‌چین)، تخمین اودومتری (نارنجی) و تخمین مشترک SLAM (سبز)؛ نشانه‌های واقعی (×) و تخمین‌زده‌شده (+)
در نمودار، خط نقطه‌چین مسیر واقعی است، خط نارنجی تخمین اودومتری و خط سبز مسیر اصلاح‌شده. نشانه‌های واقعی و تخمین‌زده‌شده هم نمایش داده شده‌اند. فایل results.json مقادیر دقیق را نگه می‌دارد.
این اعداد نتیجهٔ یک آزمایش مصنوعی با نویز و هندسهٔ مشخص‌اند. نمی‌شود از آن‌ها نتیجه گرفت هر ربات واقعی دقیقاً همین مقدار بهبود خواهد داشت. هدف این است که اثر اطلاعات محیط روی خطای مسیر را با یک آزمایش قابل‌تکرار ببینیم.

# یک آزمایش مهم‌تر: مشاهدات تکراری را حذف کنیم

در انتهای کد، یک بار دیگر مسئله را حل می‌کنیم؛ اما این‌بار از هر نشانه فقط اولین مشاهده را نگه می‌داریم. نتیجهٔ RMSE مسیر در این حالت ۱٫۳۳۸ متر است؛ یعنی عملاً همان خطای اودومتری.
دلیلش را می‌توان در مدل دید: نشانه‌ای که فقط یک بار مشاهده می‌شود می‌تواند در نقشه جایی قرار بگیرد که با همان یک اندازه‌گیری و وضعیت فعلی ربات سازگار باشد. چون مختصات نشانه را هم نمی‌دانیم، این مشاهدهٔ تنها ربات را به بخش دیگری از مسیر وصل نمی‌کند.
وقتی همان نشانه دوباره دیده می‌شود، دیگر نمی‌توان برای هر مشاهده یک جای مستقل انتخاب کرد؛ همهٔ مشاهده‌ها باید با مختصات یک نشانهٔ ثابت سازگار باشند. این اتصال است که به اصلاح مسیر کمک می‌کند.
این آزمایش تمام مشاهدات تکراری را حذف می‌کند؛ بنابراین آزمون جداگانهٔ تنها یک قید بستن حلقه در انتهای مسیر نیست. اثر مشاهدهٔ چندبارهٔ محیط را بررسی می‌کند.

# این نمونه چه چیزهایی را ساده کرده است؟

این برنامه یک تمرین تخمین مشترک دوبعدی است. چند فرض آن را باید هنگام تفسیر نتیجه به خاطر داشته باشیم:
  • شناسهٔ نشانه‌ها معلوم است؛ تطبیق خودکار ویژگی‌های تصویر یا اسکن انجام نمی‌شود.
  • نشانه‌ها ثابت‌اند و اندازه‌گیری پرت یا مانع دید شبیه‌سازی نشده است.
  • نویزها مستقل و تقریباً گاوسی در نظر گرفته شده‌اند و عدم‌قطعیت‌هایشان را می‌دانیم.
  • مسئله بعد از جمع شدن داده‌ها، به‌صورت دسته‌ای حل می‌شود؛ پیاده‌سازی بلادرنگ نیست.
  • خروجی نقشهٔ کم‌تراکم نشانه‌هاست؛ یک نقشهٔ کامل از فضای آزاد و موانع برای مسیریابی تولید نمی‌کند.
در دنیای واقعی، کالیبراسیون حسگر، هماهنگی زمان داده‌ها، تشخیص مشاهدهٔ نادرست و مدیریت اشیای متحرک هم به مسئله اضافه می‌شوند. بهینه‌سازی خوب نمی‌تواند هر دادهٔ اشتباهی را خودبه‌خود درست کند.

# برای رفتن از شبیه‌سازی به ربات واقعی چه مسیری داریم؟

اگر می‌خواهی مفهوم را یاد بگیری، ابتدا همین نمونه را تغییر بده. نویز اودومتری را بیشتر کن، دقت حسگر را کاهش بده یا تعداد نشانه‌های دیده‌شده را کم کن و نتیجه را مقایسه کن. هر بار فقط یک عامل را تغییر بده تا علت تغییر خروجی مشخص بماند.
برای پروژه‌ای با اسکن لیزری دوبعدی و ROS، مستندات SLAM Toolbox یک مرجع کاربردی برای بررسی نقشه‌سازی و مکان‌یابی است. برای کار با دوربین، ORB-SLAM3 نمونه‌های رسمی برای پیکربندی‌های تک‌چشمی، استریو، RGB-D و ترکیب تصویر با IMU دارد. انتخاب سامانه باید با نوع حسگر، محیط و توان پردازشی پروژه هماهنگ باشد.
یک نکتهٔ عملی را هم فراموش نکن: تخمین موقعیت و ساختن نقشه، به‌تنهایی حرکت ایمن را حل نمی‌کند. برنامه‌ریزی مسیر، تشخیص موانع متحرک و کنترل ربات به اجزای دیگری نیاز دارند.

# پس ربات بدون GPS چگونه می‌فهمد کجاست؟

ربات حرکتش را تخمین می‌زند، محیط را مشاهده می‌کند و ارتباط بین مشاهده‌های مختلف را پیدا می‌کند. سپس وضعیت‌های ربات و نقشه را با هم اصلاح می‌کند تا با داده‌ها سازگارتر شوند.
در تمرین ما، شمردن حرکت‌ها خطا را جمع کرد. دیدن دوبارهٔ نشانه‌های ثابت، اطلاعاتی به مسئله اضافه کرد که امکان اصلاح مسیر را فراهم ساخت. همین ایدهٔ ساده، همراه با پردازش حسگر و مدیریت عدم‌قطعیت، پایهٔ بسیاری از سامانه‌های SLAM است.
takeaway.txt
سؤال «کجایم؟» و سؤال «نقشه چه شکلی است؟» هرکدام تنها جواب نمی‌گیرند؛
SLAM آن‌ها را با هم جواب می‌دهد.

# مقالات مرتبط