TÓM TẮT
Mở Đầu
Trong lĩnh vực xử lý ảnh và thị giác máy tính, độ dời ảnh (image displacement) là một khái niệm quan trọng, giúp chúng ta mô tả sự chuyển đổi, biến dạng hoặc dịch chuyển của các điểm ảnh trong một hình ảnh so với một hình ảnh tham chiếu. Việc vẽ và biểu diễn độ dời ảnh không chỉ hữu ích trong nghiên cứu học máy (machine learning) mà còn trong các ứng dụng thực tiễn như ghép ảnh (image stitching), bù trễ (motion compensation), chỉnh sửa video, và phân tích chuyển động.
Bài viết này sẽ cung cấp một hướng dẫn chi tiết, gồm:
- Giới thiệu lý thuyết về độ dời ảnh và các mô hình phổ biến.
- Chuẩn bị môi trường và các công cụ cần thiết.
- Thu thập và tiền xử lý dữ liệu.
- Các phương pháp tính toán độ dời (optical flow, block matching, feature-based).
- Cách vẽ độ dời dưới dạng vector field, heatmap và các biểu diễn trực quan khác.
- Triển khai thực tế bằng Python (OpenCV, NumPy, Matplotlib, PyTorch/TensorFlow).
- Đánh giá và tối ưu kết quả.
- Ứng dụng thực tiễn và một số lưu ý khi làm việc với độ dời ảnh.
Tất cả các phần sẽ được mở rộng chi tiết, kèm ví dụ code, hình minh hoạ và lời khuyên thực tế, để bạn có thể tự tin thực hiện dự án của mình.
1. Kiến Thức Cơ Bản Về Độ Dời Ảnh
1.1. Định Nghĩa
Độ dời ảnh (displacement field) là một hàm D(x, y) = (u(x, y), v(x, y)) mô tả chuyển động của mỗi pixel tại tọa độ (x, y) từ ảnh gốc sang ảnh mục tiêu. Trong đó:
- u(x, y): độ dời theo trục hoành (horizontal).
- v(x, y): độ dời theo trục tung (vertical).
Nếu độ dời dương, pixel di chuyển sang phải (hoặc xuống); nếu âm, di chuyển sang trái (hoặc lên).
1.2. Các Mô Hình Độ Dời Phổ Biến
| Mô hình | Đặc điểm | Ứng dụng |
|---|---|---|
| Optical Flow | Dựa trên giả thuyết độ sáng không đổi, tính toán chuyển động liên tục | Phân tích chuyển động video, tracking |
| Block Matching | Chia ảnh thành các khối, tìm khối tương ứng trong ảnh khác | Nén video, bù trễ |
| Feature-Based Matching (SIFT, SURF, ORB) | Dùng các điểm đặc trưng để ước lượng độ dời toàn cục | Ghép ảnh, panorama |
| Deep Learning Based (FlowNet, PWC-Net) | Mạng nơ-ron học độ dời trực tiếp | Ứng dụng thời gian thực, độ chính xác cao |
1.3. Tại Sao Cần Vẽ Độ Dời?

Có thể bạn quan tâm: Cách Vẽ Đồng Tiền Bạc: Hướng Dẫn Chi Tiết Từ Cơ Bản Đến Nâng Cao
- Trực quan hoá: Giúp người dùng nhanh chóng hiểu được mô hình chuyển động.
- Kiểm tra chất lượng: Đánh giá độ chính xác của thuật toán tính độ dời.
- Giao diện người-máy: Trong các công cụ chỉnh sửa ảnh/video, người dùng có thể thấy “đường mũi tên” chỉ vị trí dịch chuyển.
2. Chuẩn Bị Môi Trường Làm Việc
2.1. Yêu Cầu Phần Cứng
| Thành phần | Cấu hình tối thiểu | Ghi chú |
|---|---|---|
| CPU | Intel i5 / AMD Ryzen 5 | Đủ cho OpenCV truyền thống |
| GPU | NVIDIA GTX 1060 (Compute Capability ≥ 5.0) | Cần cho mô hình deep learning |
| RAM | 8 GB | Đề nghị 16 GB cho dữ liệu lớn |
| Lưu trữ | SSD 256 GB | Tốc độ đọc/ghi nhanh |
2.2. Cài Đặt Phần Mềm
Sử dụng Python 3.10+ và các thư viện:
# Tạo môi trường ảo
python -m venv venv_disp
source venv_disp/bin/activate # Linux/macOS
venv_disp\Scripts\activate # Windows # Cài đặt các gói cần thiết
pip install numpy opencv-python matplotlib seaborn tqdm
pip install torch torchvision # nếu dùng PyTorch
pip install tensorflow # nếu dùng TensorFlow
pip install scikit-image
Lưu ý: Đối với OpenCV, nếu muốn hỗ trợ CUDA, cài phiên bản
opencv-python-headlesskèmopencv-contrib-python.
2.3. Kiểm Tra Cài Đặt
import cv2, torch, numpy as np, matplotlib.pyplot as plt print("OpenCV version:", cv2.__version__)
print("NumPy version:", np.__version__)
print("PyTorch GPU available:", torch.cuda.is_available())
Nếu mọi thứ hiển thị đúng, môi trường đã sẵn sàng.
3. Thu Thập Và Tiền Xử Lý Dữ Liệu
3.1. Nguồn Dữ Liệu
| Tên dữ liệu | Mô tả | Link |
|---|---|---|
| KITTI Flow 2026 | Dữ liệu video xe hơi, kèm ground-truth optical flow | http://www.cvlibs.net/datasets/kitti/eval_flow.php |
| MPI Sintel | Dữ liệu synthetic, đa dạng chuyển động | http://sintel.is.tue.mpg.de/ |
| FlyingChairs | Dữ liệu synthetic cho học sâu | https://github.com/NVIDIA/flownet2-pytorch |
Bạn có thể tải bất kỳ bộ dữ liệu nào phù hợp với mục tiêu. Đối với ví dụ, chúng ta sẽ dùng KITTI.

Có thể bạn quan tâm: Cách Vẽ Đồng Hồ Omnitrix Chi Tiết Từ A‑z: Hướng Dẫn Dành Cho Người Mới Bắt Đầu Và Các Fan Hâm Mộ
3.2. Đọc Ảnh Và Flow Ground Truth
import os, cv2
import numpy as np def read_image(path): img = cv2.cvtColor(cv2.imread(path), cv2.COLOR_BGR2RGB) return img def read_flow_kitti(path): """KITTI flow lưu dưới dạng 3-channel PNG, trong đó: R = horizontal, G = vertical, B = validity mask """ flow_png = cv2.imread(path, cv2.IMREAD_UNCHANGED) flow = flow_png:, :, :2.astype(np.float32) # Giá trị lưu ở dạng 16-bit, chuyển sang float và chuẩn hoá flow = (flow - 215) / 64.0 valid = flow_png:, :, 2 == 1 return flow, valid # Ví dụ
img1 = read_image('kitti/testing/image_2/000000_10.png')
img2 = read_image('kitti/testing/image_2/000000_11.png')
flow_gt, mask_gt = read_flow_kitti('kitti/testing/flow_noc/000000_10.png')
3.3. Chuẩn Hóa Kích Thước
Để vẽ độ dời, các ảnh và flow field nên có cùng kích thước. Nếu cần, dùng cv2.resize.
def resize_to(img, size): return cv2.resize(img, size, interpolation=cv2.INTER_LINEAR) target_size = (512, 256) # width, height
img1_res = resize_to(img1, target_size)
img2_res = resize_to(img2, target_size)
flow_res, mask_res = resize_to(flow_gt, target_size), cv2.resize(mask_gt.astype(np.uint8), target_size, interpolation=cv2.INTER_NEAREST).astype(bool)
4. Các Phương Pháp Tính Toán Độ Dời
4.1. Optical Flow Cơ Bản – Farneback
def farneback_flow(img1, img2): gray1 = cv2.cvtColor(img1, cv2.COLOR_RGB2GRAY) gray2 = cv2.cvtColor(img2, cv2.COLOR_RGB2GRAY) flow = cv2.calcOpticalFlowFarneback( gray1, gray2, None, pyr_scale=0.5, levels=3, winsize=15, iterations=3, poly_n=5, poly_sigma=1.2, flags=0 ) return flow
4.2. Deep Learning – PWC‑Net (PyTorch)
Chuẩn bị mô hình: Tải pretrained weights từ repo chính thức.
import torch
from pwcnet import PWCNet # Giả sử bạn đã clone repo và có module device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = PWCNet().to(device)
model.eval()
# Load pretrained checkpoint
checkpoint = torch.load('pwcnet-default.pth.tar')
model.load_state_dict(checkpoint'state_dict') def pwcnet_flow(img1, img2): # Chuyển thành tensor, chuẩn hoá 0,1 img1_t = torch.from_numpy(img1.transpose(2,0,1)).unsqueeze(0).float() / 255.0 img2_t = torch.from_numpy(img2.transpose(2,0,1)).unsqueeze(0).float() / 255.0 img1_t, img2_t = img1_t.to(device), img2_t.to(device) with torch.no_grad(): flow = model(img1_t, img2_t)0 # output shape: 2, H, W flow_np = flow.cpu().numpy().transpose(1,2,0) # H, W, 2 return flow_np
4.3. Block Matching – Simple Implementation
def block_matching(img1, img2, block_size=16, search_range=8): h, w = img1.shape:2 flow = np.zeros((h, w, 2), dtype=np.float32) gray1 = cv2.cvtColor(img1, cv2.COLOR_RGB2GRAY) gray2 = cv2.cvtColor(img2, cv2.COLOR_RGB2GRAY) for y in range(0, h - block_size, block_size): for x in range(0, w - block_size, block_size): ref_block = gray1y:y+block_size, x:x+block_size min_sad = 1e9 best_dx, best_dy = 0, 0 for dy in range(-search_range, search_range+1): for dx in range(-search_range, search_range+1): nx, ny = x+dx, y+dy if nx < 0 or ny < 0 or nx+block_size > w or ny+block_size > h: continue cand_block = gray2ny:ny+block_size, nx:nx+block_size sad = np.sum(np.abs(ref_block.astype(np.int32) - cand_block.astype(np.int32))) if sad < min_sad: min_sad = sad best_dx, best_dy = dx, dy flowy:y+block_size, x:x+block_size, 0 = best_dx flowy:y+block_size, x:x+block_size, 1 = best_dy return flow
Lưu ý: Block matching chậm và không mượt, nhưng rất dễ hiểu cho người mới.
5. Cách Vẽ Độ Dời Ảnh

Có thể bạn quan tâm: Cách Vẽ Đồi Trong Sketchup: Hướng Dẫn Chi Tiết Từ Cơ Bản Đến Nâng Cao
5.1. Vector Field (Quiver Plot)
import matplotlib.pyplot as plt def plot_quiver(img, flow, step=16, scale=1, title='Vector Field'): plt.figure(figsize=(10,6)) plt.imshow(img) h, w = flow.shape:2 Y, X = np.mgrid0:h:step, 0:w:step U = flow::step, ::step, 0 scale V = flow::step, ::step, 1 scale plt.quiver(X, Y, U, V, color='lime', angles='xy', scale_units='xy', scale=1) plt.title(title) plt.axis('off') plt.show()
Ví dụ:
flow_farneback = farneback_flow(img1_res, img2_res)
plot_quiver(img1_res, flow_farneback, step=12, scale=1, title='Farneback Optical Flow')
5.2. Heatmap – Độ Dời Magnitude
def plot_magnitude(img, flow, mask=None, title='Flow Magnitude'): magnitude = np.linalg.norm(flow, axis=2) plt.figure(figsize=(10,6)) plt.imshow(img, alpha=0.6) if mask is not None: magnitude = np.where(mask, magnitude, np.nan) plt.imshow(magnitude, cmap='inferno', alpha=0.5) plt.colorbar(label='Pixel displacement (px)') plt.title(title) plt.axis('off') plt.show()
5.3. Màu Sắc HSV – Visualize Direction + Magnitude
def flow_to_hsv(flow): h, w = flow.shape:2 hsv = np.zeros((h, w, 3), dtype=np.uint8) # direction angle = np.arctan2(flow...,1, flow...,0) # -pi..pi hsv...,0 = ((angle + np.pi) / (2np.pi) 179).astype(np.uint8) # Hue 0-179 # magnitude mag = np.linalg.norm(flow, axis=2) hsv...,1 = np.clip(mag10, 0, 255).astype(np.uint8) # Saturation hsv...,2 = 255 rgb = cv2.cvtColor(hsv, cv2.COLOR_HSV2RGB) return rgb def plot_hsv_flow(img, flow, title='HSV Flow Visualization'): hsv_flow = flow_to_hsv(flow) overlay = cv2.addWeighted(img, 0.5, hsv_flow, 0.5, 0) plt.figure(figsize=(10,6)) plt.imshow(overlay) plt.title(title) plt.axis('off') plt.show()
5.4. 3D Surface Plot (Matplotlib)
from mpl_toolkits.mplot3d import Axes3D def plot_3d_surface(flow, axis='u'): """axis='u' or 'v' để vẽ thành phần ngang hoặc dọc""" comp = flow...,0 if axis=='u' else flow...,1 h, w = comp.shape X, Y = np.meshgrid(np.arange(w), np.arange(h)) fig = plt.figure(figsize=(12,7)) ax = fig.add_subplot(111, projection='3d') surf = ax.plot_surface(X, Y, comp, cmap='viridis') ax.set_xlabel('X') ax.set_ylabel('Y') ax.set_zlabel(f'Displacement ({axis})') fig.colorbar(surf, shrink=0.5, aspect=5) plt.show()
6. Triển Khai Thực Tế – Dự Án Mini
6️⃣ Bước 1: Tải Dữ Liệu
mkdir data && cd data
wget http://www.cvlibs.net/download.php?file=data_odometry_gray.zip -O odometry.zip
unzip odometry.zip
6️⃣ Bước 2: Tính Độ Dời Bằng PWC‑Net

Có thể bạn quan tâm: Cách Vẽ Đồ Đã Xếp: Hướng Dẫn Chi Tiết Từ A Đến Z
flow_pwc = pwcnet_flow(img1_res, img2_res)
plot_quiver(img1_res, flow_pwc, step=10, scale=0.5, title='PWC‑Net Flow')
plot_hsv_flow(img1_res, flow_pwc, title='PWC‑Net HSV')
6️⃣ Bước 3: Đánh Giá So Với Ground‑Truth
def endpoint_error(flow_est, flow_gt, mask): err = np.linalg.norm(flow_est - flow_gt, axis=2) return np.mean(errmask) epe_farneback = endpoint_error(flow_farneback, flow_res, mask_res)
epe_pwc = endpoint_error(flow_pwc, flow_res, mask_res)
print(f"EPE Farneback: {epe_farneback:.3f} px")
print(f"EPE PWC‑Net: {epe_pwc:.3f} px")
Kết quả thường cho thấy PWC‑Net đạt EPE < 2px trên KITTI, trong khi Farneback khoảng 4‑5px.
6️⃣ Bước 4: Lưu Đồ Thị Độ Dời
def save_quiver(img, flow, path): plt.figure(figsize=(10,6)) plt.imshow(img) step = 12 Y, X = np.mgrid0:flow.shape0:step, 0:flow.shape1:step U = flow::step, ::step, 0 V = flow::step, ::step, 1 plt.quiver(X, Y, U, V, color='cyan', angles='xy', scale_units='xy', scale=1) plt.axis('off') plt.savefig(path, bbox_inches='tight', pad_inches=0, dpi=300) plt.close() save_quiver(img1_res, flow_pwc, 'output/pwc_quiver.png')
7. Đánh Giá, Tối Ưu Hóa Và Xử Lý Lỗi
7.1. Các Thước Đánh Giá
| Thước đo | Công thức | Ý nghĩa |
|---|---|---|
| EPE (Endpoint Error) | mean( sqrt( (u‑û)² + (v‑v̂)² ) ) |
Độ lệch trung bình pixel |
| AAE (Angular Error) | arccos( (u·û + v·v̂ + 1) / ( sqrt(u²+v²+1)·sqrt(û²+v̂²+1) ) ) |
Lỗi góc, hữu ích khi magnitude lớn |
| Fl‑Error | %pixels có EPE > 3px | Đánh giá mức độ “outlier” |
7.2. Thông Tin Thêm Để Cải Thiện Kết Quả
- Pyramid Scales: Tăng số lớp pyramid trong Farneback hoặc PWC‑Net giúp bắt được chuyển động lớn.
- Regularization: Áp dụng bộ lọc median hoặc TV‑L1 để làm mịn flow.
- Data Augmentation: Khi huấn luyện mô hình deep learning, dùng xoay, zoom, noise để tăng khả năng tổng quát.
- Post‑processing: Sử dụng forward‑backward consistency check để loại bỏ outlier.
7.3. Xử Lý Các Vấn Đề Thường Gặp
| Vấn đề | Nguyên nhân | Giải pháp |
|---|---|---|
| Flow bị nhiễu ở vùng tối | Độ sáng không đủ để tính gradient | Thêm tiền xử lý tăng độ tương phản (CLAHE) |
| Mất chi tiết ở biên | Block matching không có block tham chiếu | Dùng overlapping blocks hoặc padding |
| Độ dời quá lớn gây “wrap‑around” | Giá trị flow vượt giới hạn lưu trữ | Chuẩn hoá lại hoặc cắt giá trị (clipping) |
| GPU memory overflow | Mô hình quá lớn hoặc batch size > 1 | Giảm kích thước ảnh, dùng torch.cuda.empty_cache() |
8. Ứng Dụng Thực Tiễn

8.1. Ghép Ảnh Panorama
- Bước 1: Tính độ dời (feature‑based hoặc optical flow) giữa các khung liên tiếp.
- Bước 2: Dùng RANSAC để lọc outlier, ước lượng ma trận homography.
- Bước 3: Áp dụng warp và blend.
8.2. Bù Trễ Video (Video Stabilization)
- Tính optical flow giữa khung hiện tại và khung tham chiếu.
- Tích hợp độ dời để có trajectory, sau đó smoothing trajectory và warp lại video.
8.3. Phân Tích Chuyển Động Y Tế
Trong y học, độ dời ảnh giúp đo biến dạng mô (e.g., cardiac MRI). Các bước:
- Thu thập series ảnh tim.
- Tính optical flow (Thường dùng Lucas‑Kanade hoặc DeepFlow).
- Vẽ vector field để đánh giá co giãn cơ tim.
8.4. Thực Tế Trong Game Engine
Engine như Unity hoặc Unreal cho phép nhập displacement maps để tạo hiệu ứng nước, gió. Độ dời ảnh được chuyển thành normal map để chiếu sáng.
9. Tổng Kết
- Độ dời ảnh là công cụ mạnh mẽ để mô tả chuyển động pixel‑cấp.
- Chúng ta đã đi qua lý thuyết, công cụ, các thuật toán (Farneback, Block Matching, PWC‑Net) và cách vẽ (quiver, heatmap, HSV, 3D).
- Python + OpenCV + PyTorch cung cấp môi trường linh hoạt, cho phép vừa học thuật vừa triển khai thực tế.
- Đánh giá chuẩn (EPE, AAE) giúp so sánh các phương pháp, còn tối ưu hoá (pyramid, regularization) nâng cao chất lượng.
- Độ dời ảnh không chỉ là khái niệm nghiên cứu mà còn ứng dụng rộng rãi: panorama, ổn định video, y tế, game.
Hy vọng bài viết chi tiết trên sẽ là cẩm nang toàn diện cho bạn trong việc hiểu, tính toán và trực quan hoá độ dời ảnh. Khi đã nắm vững các bước, bạn có thể mở rộng sang các dự án phức tạp hơn, chẳng hạn như multiview stereo, scene flow, hay neural rendering. Chúc bạn thành công!
