← Quay lại Blog
manipulationrobot-armpythonprogramming

Inverse Kinematics robot arm 6 bậc: Lý thuyết + Python

Giải bài toán IK cho robot arm 6-DOF — từ DH parameters, forward kinematics đến numerical IK solver với Python và NumPy.

Nguyễn Anh Tuấn26 tháng 1, 202610 phút đọc
Inverse Kinematics robot arm 6 bậc: Lý thuyết + Python

Inverse Kinematics là gì và tại sao quan trọng?

Inverse Kinematics (IK) cho robot arm 6 bậc tự do là bài toán cốt lõi trong robotics: cho vị trí và hướng mong muốn của end-effector, tìm bộ góc khớp (joint angles) tương ứng. Nếu bạn từng lập trình robot gắp vật, bạn biết rằng con người nghĩ theo tọa độ Cartesian ("đưa tay đến điểm x, y, z") nhưng robot cần góc từng khớp. IK chính là cầu nối giữa hai thế giới đó.

Bài viết này sẽ đi từ lý thuyết Denavit-Hartenberg (DH), qua forward kinematics, đến numerical IK solver hoàn chỉnh bằng Python. Code chạy được ngay với NumPy — không cần thư viện robotics chuyên dụng.

Robot arm công nghiệp 6 bậc tự do trong nhà máy

Denavit-Hartenberg Convention

Tại sao cần DH Parameters?

Mỗi robot arm có cấu hình khớp (joint) và link khác nhau. DH convention cung cấp một phương pháp chuẩn hóa để mô tả mối quan hệ hình học giữa các khớp liên tiếp bằng đúng 4 tham số cho mỗi khớp:

Tham số Ký hiệu Ý nghĩa
Link length a_i Khoảng cách giữa trục z_{i-1} và z_i dọc theo trục x_i
Link twist alpha_i Góc xoay từ z_{i-1} đến z_i quanh trục x_i
Link offset d_i Khoảng cách dọc theo trục z_{i-1}
Joint angle theta_i Góc xoay quanh trục z_{i-1} (biến khớp cho revolute joint)

Ma trận biến đổi DH

Mỗi bộ DH parameters cho ra một homogeneous transformation matrix 4x4:

import numpy as np

def dh_matrix(theta, d, a, alpha):
    """
    Tạo ma trận biến đổi DH 4x4.
    theta: góc khớp (rad)
    d: link offset
    a: link length
    alpha: link twist (rad)
    """
    ct, st = np.cos(theta), np.sin(theta)
    ca, sa = np.cos(alpha), np.sin(alpha)

    return np.array([
        [ct, -st * ca,  st * sa, a * ct],
        [st,  ct * ca, -ct * sa, a * st],
        [0,   sa,       ca,      d     ],
        [0,   0,        0,       1     ]
    ])

Ma trận này encode cả rotation lẫn translation. Nhân chuỗi 6 ma trận lại ta được forward kinematics — vị trí và hướng của end-effector từ bộ góc khớp.

Forward Kinematics cho robot 6-DOF

Ví dụ: Robot dạng Puma 560

Dưới đây là bảng DH parameters mẫu cho robot 6 khớp revolute (đơn vị: mét, radian):

# DH Parameters cho robot 6-DOF dạng anthropomorphic
# Mỗi hàng: [a, alpha, d, theta_offset]
# theta thực tế = theta_variable + theta_offset
DH_PARAMS = [
    # a      alpha        d      theta_offset
    [0.0,    np.pi/2,     0.4,   0.0],   # Joint 1 (base rotation)
    [0.25,   0.0,         0.0,   0.0],   # Joint 2 (shoulder)
    [0.025,  np.pi/2,     0.0,   0.0],   # Joint 3 (elbow)
    [0.0,   -np.pi/2,     0.28,  0.0],   # Joint 4 (wrist roll)
    [0.0,    np.pi/2,     0.0,   0.0],   # Joint 5 (wrist pitch)
    [0.0,    0.0,         0.1,   0.0],   # Joint 6 (wrist yaw)
]

def forward_kinematics(joint_angles, dh_params=DH_PARAMS):
    """
    Tính vị trí và hướng end-effector từ góc khớp.
    Trả về ma trận 4x4 (rotation + position).
    """
    T = np.eye(4)
    for i, (a, alpha, d, offset) in enumerate(dh_params):
        theta = joint_angles[i] + offset
        T = T @ dh_matrix(theta, d, a, alpha)
    return T

# Test: tất cả khớp ở vị trí 0
q_home = np.zeros(6)
T_home = forward_kinematics(q_home)
position = T_home[:3, 3]
print(f"Home position: x={position[0]:.3f}, y={position[1]:.3f}, z={position[2]:.3f}")

Trích xuất Position và Orientation

def extract_pose(T):
    """Trích xuất position (xyz) và rotation matrix từ T 4x4."""
    position = T[:3, 3]
    rotation = T[:3, :3]
    return position, rotation

def rotation_to_euler(R):
    """Chuyển rotation matrix sang Euler angles (ZYX convention)."""
    sy = np.sqrt(R[0, 0]**2 + R[1, 0]**2)
    singular = sy < 1e-6

    if not singular:
        roll  = np.arctan2(R[2, 1], R[2, 2])
        pitch = np.arctan2(-R[2, 0], sy)
        yaw   = np.arctan2(R[1, 0], R[0, 0])
    else:
        roll  = np.arctan2(-R[1, 2], R[1, 1])
        pitch = np.arctan2(-R[2, 0], sy)
        yaw   = 0.0

    return np.array([roll, pitch, yaw])

Jacobian Matrix — Chìa khóa của Numerical IK

Jacobian là gì?

Jacobian matrix J mô tả mối quan hệ vi phân giữa vận tốc khớp và vận tốc end-effector:

v = J(q) * dq

Trong đó v là vector 6D (3 linear + 3 angular velocity), q là vector góc khớp. Đối với numerical IK, Jacobian cho phép ta tính: "nếu muốn end-effector dịch chuyển delta_x, cần thay đổi góc khớp bao nhiêu?"

Tính Jacobian bằng phương pháp số

Cách đơn giản và robust nhất là finite differences:

def compute_jacobian(joint_angles, dh_params=DH_PARAMS, delta=1e-6):
    """
    Tính numerical Jacobian (6x6) bằng finite differences.
    Hàng 0-2: linear velocity (dx, dy, dz)
    Hàng 3-5: angular velocity (drx, dry, drz)
    """
    n_joints = len(joint_angles)
    J = np.zeros((6, n_joints))

    T_current = forward_kinematics(joint_angles, dh_params)
    pos_current, R_current = extract_pose(T_current)
    euler_current = rotation_to_euler(R_current)

    for i in range(n_joints):
        q_perturbed = joint_angles.copy()
        q_perturbed[i] += delta

        T_perturbed = forward_kinematics(q_perturbed, dh_params)
        pos_perturbed, R_perturbed = extract_pose(T_perturbed)
        euler_perturbed = rotation_to_euler(R_perturbed)

        # Linear velocity columns
        J[:3, i] = (pos_perturbed - pos_current) / delta
        # Angular velocity columns
        J[3:, i] = (euler_perturbed - euler_current) / delta

    return J

Minh họa Jacobian matrix và vận tốc end-effector

Numerical IK Solver: Newton-Raphson

Thuật toán

Ý tưởng cốt lõi: lặp đi lặp lại việc tính error giữa pose hiện tại và pose mong muốn, rồi dùng pseudo-inverse Jacobian để cập nhật góc khớp.

while |error| > tolerance:
    error = target_pose - current_pose
    dq = J_pinv * error
    q = q + dq

Implementation đầy đủ

def pose_error(T_current, T_target):
    """
    Tính error 6D giữa pose hiện tại và target.
    Returns: [dx, dy, dz, drx, dry, drz]
    """
    pos_err = T_target[:3, 3] - T_current[:3, 3]

    R_current = T_current[:3, :3]
    R_target = T_target[:3, :3]
    R_err = R_target @ R_current.T

    # Trích xuất rotation error dạng axis-angle
    angle = np.arccos(np.clip((np.trace(R_err) - 1) / 2, -1, 1))
    if angle < 1e-6:
        rot_err = np.zeros(3)
    else:
        rot_err = angle / (2 * np.sin(angle)) * np.array([
            R_err[2, 1] - R_err[1, 2],
            R_err[0, 2] - R_err[2, 0],
            R_err[1, 0] - R_err[0, 1]
        ])

    return np.concatenate([pos_err, rot_err])


def inverse_kinematics(T_target, q_init=None, dh_params=DH_PARAMS,
                        max_iter=200, pos_tol=1e-4, rot_tol=1e-3,
                        damping=0.1):
    """
    Numerical IK solver dùng Damped Least Squares (Levenberg-Marquardt).

    Args:
        T_target: ma trận 4x4 mục tiêu
        q_init: góc khớp khởi tạo (None = zeros)
        max_iter: số vòng lặp tối đa
        pos_tol: sai số vị trí chấp nhận (mét)
        rot_tol: sai số góc chấp nhận (rad)
        damping: hệ số damping lambda (tránh singularity)

    Returns:
        q_solution: bộ góc khớp nghiệm
        success: True nếu hội tụ
        iterations: số vòng lặp thực tế
    """
    n_joints = len(dh_params)
    q = q_init.copy() if q_init is not None else np.zeros(n_joints)

    for iteration in range(max_iter):
        T_current = forward_kinematics(q, dh_params)
        error = pose_error(T_current, T_target)

        pos_err_norm = np.linalg.norm(error[:3])
        rot_err_norm = np.linalg.norm(error[3:])

        # Kiểm tra hội tụ
        if pos_err_norm < pos_tol and rot_err_norm < rot_tol:
            return q, True, iteration

        # Tính Jacobian và pseudo-inverse có damping
        J = compute_jacobian(q, dh_params)
        # Damped Least Squares: dq = J^T (J J^T + lambda^2 I)^(-1) * error
        JJT = J @ J.T
        dq = J.T @ np.linalg.solve(
            JJT + damping**2 * np.eye(6), error
        )

        q = q + dq

        # Giới hạn góc khớp trong [-pi, pi]
        q = np.mod(q + np.pi, 2 * np.pi) - np.pi

    return q, False, max_iter


# === VÍ DỤ SỬ DỤNG ===

# Bước 1: Forward kinematics để tạo target pose
q_desired = np.array([0.3, -0.5, 0.8, 0.1, -0.3, 0.6])
T_target = forward_kinematics(q_desired)

print("Target position:", T_target[:3, 3])
print("Target angles (ground truth):", np.degrees(q_desired))

# Bước 2: Giải IK từ initial guess khác
q_init = np.array([0.0, 0.0, 0.0, 0.0, 0.0, 0.0])
q_solution, success, iters = inverse_kinematics(T_target, q_init)

print(f"\nIK {'converged' if success else 'FAILED'} after {iters} iterations")
print(f"Solution angles: {np.degrees(q_solution).round(2)}")

# Verify
T_result = forward_kinematics(q_solution)
pos_error = np.linalg.norm(T_target[:3, 3] - T_result[:3, 3])
print(f"Position error: {pos_error*1000:.3f} mm")

Damped Least Squares vs Pure Pseudo-Inverse

Vấn đề Singularity

Khi robot gần singularity (ví dụ: hai khớp wrist thẳng hàng), Jacobian gần như singular, pseudo-inverse cho ra giá trị cực lớn khiến robot "bay" không kiểm soát.

Damped Least Squares (còn gọi là Levenberg-Marquardt) thêm hệ số damping lambda:

dq = J^T (J J^T + lambda^2 I)^(-1) * error

Khi lambda = 0, ta có pseudo-inverse thuần. Khi lambda lớn, chuyển động chậm hơn nhưng ổn định gần singularity. Trong thực tế, lambda = 0.01 - 0.5 hoạt động tốt cho hầu hết robot.

Adaptive Damping

Một kỹ thuật nâng cao là điều chỉnh lambda theo khoảng cách đến singularity:

def adaptive_damping(J, lambda_min=0.01, lambda_max=0.5):
    """Tăng damping khi gần singularity."""
    # Manipulability measure
    w = np.sqrt(max(np.linalg.det(J @ J.T), 0))
    # Threshold
    w_threshold = 0.05
    if w < w_threshold:
        ratio = 1.0 - (w / w_threshold)**2
        return lambda_min + ratio * (lambda_max - lambda_min)
    return lambda_min

Mẹo thực tế khi implement IK

1. Multiple Solutions

Robot 6-DOF thường có tối đa 8 nghiệm cho cùng một target pose. Initial guess quyết định bạn nhận được nghiệm nào. Trong ứng dụng thực tế, chọn nghiệm gần cấu hình hiện tại nhất để giảm chuyển động:

def best_ik_solution(T_target, q_current, n_trials=10):
    """Thử nhiều initial guess, chọn nghiệm gần nhất."""
    best_q = None
    best_dist = float('inf')

    for _ in range(n_trials):
        q_init = q_current + np.random.uniform(-0.5, 0.5, 6)
        q_sol, success, _ = inverse_kinematics(T_target, q_init)
        if success:
            dist = np.linalg.norm(q_sol - q_current)
            if dist < best_dist:
                best_dist = dist
                best_q = q_sol

    return best_q

2. Joint Limits

Robot thật có giới hạn góc khớp. Thêm clamping sau mỗi iteration:

JOINT_LIMITS = [
    (-np.pi, np.pi),       # Joint 1
    (-np.pi/2, np.pi/2),   # Joint 2
    (-np.pi*3/4, np.pi*3/4), # Joint 3
    (-np.pi, np.pi),       # Joint 4
    (-np.pi/2, np.pi/2),   # Joint 5
    (-np.pi, np.pi),       # Joint 6
]

def clamp_joints(q, limits=JOINT_LIMITS):
    return np.array([
        np.clip(q[i], limits[i][0], limits[i][1])
        for i in range(len(q))
    ])

3. Workspace Check

Trước khi chạy IK, kiểm tra target có nằm trong workspace:

def in_workspace(T_target, dh_params=DH_PARAMS):
    """Kiểm tra nhanh target có trong tầm với không."""
    pos = T_target[:3, 3]
    dist = np.linalg.norm(pos)
    # Tổng chiều dài tất cả links
    max_reach = sum(
        np.sqrt(p[0]**2 + p[2]**2) for p in dh_params
    )
    return dist <= max_reach * 0.95  # 95% safety margin

Visualization với Matplotlib

import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

def plot_robot(joint_angles, dh_params=DH_PARAMS, ax=None):
    """Vẽ robot arm 3D từ bộ góc khớp."""
    if ax is None:
        fig = plt.figure(figsize=(10, 8))
        ax = fig.add_subplot(111, projection='3d')

    positions = [np.array([0, 0, 0])]
    T = np.eye(4)

    for i, (a, alpha, d, offset) in enumerate(dh_params):
        theta = joint_angles[i] + offset
        T = T @ dh_matrix(theta, d, a, alpha)
        positions.append(T[:3, 3].copy())

    positions = np.array(positions)
    ax.plot(positions[:, 0], positions[:, 1], positions[:, 2],
            'o-', linewidth=3, markersize=8, color='steelblue')
    ax.scatter(*positions[-1], color='red', s=100, zorder=5,
               label='End-effector')
    ax.scatter(*positions[0], color='green', s=100, zorder=5,
               label='Base')

    ax.set_xlabel('X (m)')
    ax.set_ylabel('Y (m)')
    ax.set_zlabel('Z (m)')
    ax.legend()
    ax.set_title('Robot Arm Configuration')
    plt.tight_layout()
    return ax

# Vẽ cấu hình home và cấu hình IK solution
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 7),
                                subplot_kw={'projection': '3d'})
plot_robot(np.zeros(6), ax=ax1)
ax1.set_title('Home Configuration')

plot_robot(q_solution, ax=ax2)
ax2.set_title('IK Solution')
plt.savefig('robot_ik_comparison.png', dpi=150)
plt.show()

Visualization 3D robot arm với Python Matplotlib

Bước tiếp theo

Code trong bài viết này là self-contained — chỉ cần NumPy và Matplotlib. Bạn có thể mở rộng thêm:

Nếu bạn mới bắt đầu với Python cho robotics, hãy đọc bài Lập trình điều khiển robot với Python để nắm nền tảng GPIO, PID, và computer vision.

Bài viết liên quan

Bài viết liên quan

ISO 10218 thực hành: Risk Assessment cho robot hàn
safetyrobot-armstandards

ISO 10218 thực hành: Risk Assessment cho robot hàn

Hướng dẫn thực hiện risk assessment theo ISO 10218 cho cell robot hàn — từ hazard identification đến safety measures.

28/3/202613 phút đọc
ROS 2 từ A đến Z (Phần 4): ros2_control và Hardware
ros2tutorialrobot-armPhần 4

ROS 2 từ A đến Z (Phần 4): ros2_control và Hardware

Kết nối ROS 2 với phần cứng thực — viết hardware interface cho motor driver và đọc encoder với ros2_control framework.

26/3/202611 phút đọc
Multi-robot Coordination: Thuật toán phân công task
fleetamrprogramming

Multi-robot Coordination: Thuật toán phân công task

Các thuật toán phân công nhiệm vụ cho đội robot — từ Hungarian algorithm, auction-based đến RL-based task allocation.

20/3/202612 phút đọc