Thị giác Video và 3D¶
Thị giác video và 3D mở rộng hiểu biết ảnh sang miền thời gian và không gian. File này đề cập đến optical flow, phân loại video (3D CNN, TimeSformer), theo dõi đối tượng (SORT, DeepSORT), nhận dạng hành động, ước lượng độ sâu (monocular và stereo), đám điểm (point cloud), NeRF, và 3D Gaussian splatting.
-
Các file 01-04 coi ảnh như những bức ảnh chụp tĩnh độc lập. Nhưng thế giới thị giác là liên tục: vật thể chuyển động, cảnh thay đổi, và độ sâu tồn tại. File này mở rộng thị giác máy tính sang miền thời gian (video) và miền không gian (3D), bao quát cách các mô hình hiểu chuyển động, theo dõi đối tượng, ước lượng độ sâu, và tái tạo cảnh.
-
Một video là một chuỗi các ảnh (khung hình - frames) được chụp theo thời gian. Ở 30 khung hình mỗi giây, một đoạn clip 10 giây chứa 300 khung hình. Thách thức cốt lõi là mô hình hoá chiều thời gian: các vật thể di chuyển như thế nào, các cảnh tiến hoá ra sao, và chúng ta có thể liên kết thông tin giữa các khung hình với nhau bằng cách nào?
-
Optical flow ước lượng chuyển động biểu kiến của các pixel giữa hai khung hình liên tiếp. Với mỗi pixel trong khung hình \(t\), optical flow tạo ra một vector dịch chuyển 2D \((u, v)\) chỉ đến nơi pixel đó di chuyển đến trong khung hình \(t+1\). Kết quả là một trường chuyển động dày đặc có cùng kích thước với ảnh.
- Optical flow được tính toán dựa trên giả định không đổi độ sáng (brightness constancy assumption): cường độ của một pixel không đổi khi nó di chuyển. Nếu một pixel tại vị trí \((x, y)\) trong khung hình \(t\) có cường độ \(I(x, y, t)\) và di chuyển một đoạn \((u, v)\) trong một khoảng thời gian nhỏ \(\delta t\):
- Lấy khai triển Taylor bậc một (chương 03) và chia cho \(\delta t\):
-
trong đó \(I_x, I_y\) là các gradient không gian (Sobel, file 01) và \(I_t\) là gradient thời gian (hiệu giữa các khung hình liên tiếp). Đây là phương trình ràng buộc optical flow. Một phương trình, hai ẩn (\(u, v\)): chúng ta cần một ràng buộc bổ sung.
-
Lucas-Kanade giả sử dòng chảy không đổi trong một cửa sổ nhỏ (ví dụ: pixel 5x5). Điều này tạo ra một hệ quá xác định (25 phương trình, 2 ẩn) được giải bằng bình phương tối thiểu (phương trình chính tắc từ chương 06):
-
Ma trận 2x2 là tensor cấu trúc từ file 01 (cùng ma trận được dùng trong phát hiện góc Harris). Lucas-Kanade hoạt động tốt cho các chuyển động nhỏ nhưng thất bại khi vật thể di chuyển hơn vài pixel giữa các khung hình.
-
Phương pháp Farneback khớp một khai triển đa thức với lân cận của mỗi pixel và ước lượng trường dịch chuyển giải thích tốt nhất sự thay đổi giữa các khung hình. Nó tạo ra dòng dày đặc (một vector cho mọi pixel) và xử lý các chuyển động lớn hơn Lucas-Kanade.
-
Các phương pháp optical flow học sâu hiện đại (FlowNet, RAFT) học dự đoán optical flow end-to-end từ các cặp khung hình. RAFT (Recurrent All-Pairs Field Transforms, Teed và Deng, 2020) tính toán một thể tích tương quan 4D giữa tất cả các cặp pixel trong hai khung hình và tinh chỉnh dần dự đoán dòng chảy bằng một toán tử cập nhật dựa trên GRU. RAFT đạt độ chính xác hàng đầu (state-of-the-art) và đã trở thành backbone dòng chảy tiêu chuẩn.
-
Mạng hai luồng (two-stream networks) (Simonyan và Zisserman, 2014) là một cách tiếp cận sớm để hiểu video. Một luồng xử lý một khung hình RGB đơn (bề ngoài), luồng kia xử lý một chồng các khung hình optical flow (chuyển động). Hai luồng được hợp nhất ở cuối (bằng cách lấy trung bình hoặc nối). Kiến trúc này tách biệt rõ ràng "vật trông như thế nào" và "chúng di chuyển ra sao".
-
Mạng Tích Chập 3D (3D Convolutional Networks) mở rộng tích chập 2D sang chiều thời gian. Một tích chập 3D áp dụng một bộ lọc kích thước \(k \times k \times k_t\) trải dài cả chiều không gian và thời gian, trực tiếp học các đặc trưng không-thời gian.
-
C3D (Tran và cộng sự, 2015) xếp chồng các tích chập 3D với các bộ lọc 3x3x3, cho thấy rằng các tích chập thời gian có thể học các đặc trưng chuyển động mà không cần optical flow tường minh. Chi phí rất cao: tích chập 3D có \(k_t\) lần tham số và tính toán nhiều hơn so với tích chập 2D tương ứng.
-
I3D (Inflated 3D, Carreira và Zisserman, 2017) thực hiện một cách tiếp cận thực tế hơn: bắt đầu với một CNN 2D đã tiền huấn luyện (như Inception hoặc ResNet) và "phình" (inflate) tất cả các bộ lọc 2D thành 3D bằng cách lặp lại trọng số dọc theo chiều thời gian và chia cho \(k_t\). Điều này chuyển giao tiền huấn luyện ImageNet sang video trong khi thêm mô hình hoá thời gian. Một bộ lọc 2D \(k \times k\) trở thành bộ lọc \(k \times k \times k_t\) được khởi tạo là \(W_{\text{3D}}[:,:,j] = W_{\text{2D}} / k_t\) cho mọi vị trí thời gian \(j\).
-
Mạng SlowFast (Feichtenhofer và cộng sự, 2019) sử dụng hai đường dẫn song song hoạt động ở các độ phân giải thời gian khác nhau:
- Đường dẫn chậm (Slow pathway) xử lý các khung hình ở tốc độ khung hình thấp (ví dụ: mỗi khung hình thứ 16) với độ phân giải không gian cao và nhiều kênh, nắm bắt chi tiết không gian tinh tế.
- Đường dẫn nhanh (Fast pathway) xử lý các khung hình ở tốc độ khung hình cao (mỗi khung hình thứ 2) với độ phân giải không gian giảm và ít kênh hơn (thường là \(1/8\) của đường dẫn Chậm), nắm bắt các thay đổi thời gian nhanh.
- Các kết nối ngang hợp nhất thông tin từ Fast sang Slow qua các tích chập có stride.
-
Cái nhìn sâu sắc là thông tin không gian và thời gian có các yêu cầu băng thông khác nhau: bề ngoài vật thể thay đổi chậm, nhưng chuyển động có thể nhanh. SlowFast phù hợp với sự bất đối xứng này theo thiết kế.
-
TimeSformer (Bertasius và cộng sự, 2021) áp dụng Vision Transformer cho video. Nó phân tách sự chú ý không-thời gian đầy đủ (sẽ cấm kỵ vì quá đắt: \(O((T \times N)^2)\) cho \(T\) khung hình và \(N\) mảnh mỗi khung hình) thành chú ý phân chia (divided attention): mỗi khối luân phiên giữa chú ý thời gian (mỗi mảnh chú ý qua thời gian tại cùng một vị trí không gian) và chú ý không gian (mỗi mảnh chú ý qua không gian trong cùng một khung hình). Điều này giảm chi phí từ \(O(T^2 N^2)\) xuống \(O(T^2 + N^2)\).
-
VideoMAE (Tong và cộng sự, 2022) mở rộng ý tưởng masked autoencoder (file 04) sang video. Một tỷ lệ che cực cao (90-95%) được sử dụng vì video có độ dư thừa thời gian cao: các khung hình lân cận trông gần như giống hệt nhau, nên việc che hầu hết các mảnh vẫn để lại đủ thông tin cho việc tái tạo. VideoMAE tiền huấn luyện một backbone ViT trên video không gán nhãn và chuyển giao sang các tác vụ hạ nguồn.
-
Nhận dạng hành động (action recognition) phân loại một đoạn video vào một trong nhiều danh mục hành động (ví dụ: "chạy", "nấu ăn", "chơi guitar"). Nó là tương đương video của phân loại ảnh. Các tiêu chuẩn bao gồm Kinetics-400 (400 lớp hành động, ~300K đoạn), Something-Something (174 hành động chi tiết đòi hỏi lập luận thời gian), và ActivityNet (200 lớp với các video dài, chưa được cắt).
-
Phát hiện hành động theo thời gian (temporal action detection) vượt xa phân loại: cho một video dài chưa cắt, tìm thời gian bắt đầu, kết thúc, và lớp của mỗi hành động. Đây là tương đương thời gian của phát hiện đối tượng. Các phương pháp như ActionFormer sử dụng một Transformer để xử lý các đặc trưng thời gian và dự đoán ranh giới hành động.
-
Theo dõi đối tượng video (video object tracking) theo dõi một đối tượng cụ thể qua các khung hình sau khi nó được xác định trong khung hình đầu tiên.
-
SORT (Simple Online and Realtime Tracking, Bewley và cộng sự, 2016) kết hợp một mô hình phát hiện (phát hiện các đối tượng trong mỗi khung hình độc lập) với bộ lọc Kalman (Kalman filter) để dự đoán chuyển động và thuật toán Hungary (Hungarian algorithm) để gán.
-
Bộ lọc Kalman duy trì một ước lượng trạng thái (vị trí, vận tốc, kích thước) cho mỗi đối tượng được theo dõi và dự đoán nó sẽ ở đâu trong khung hình tiếp theo bằng một mô hình chuyển động tuyến tính. Khi một phát hiện mới đến, bộ lọc Kalman cập nhật ước lượng của nó bằng cách kết hợp dự đoán với quan sát, được trọng số hoá bởi độ bất định tương ứng của chúng. Đây là cập nhật Bayes (chương 05) được áp dụng cho theo dõi.
-
Thuật toán Hungary giải quyết bài toán gán song tuyến tính (bilinear assignment): cho \(M\) đối tượng được theo dõi và \(N\) phát hiện mới, tìm sự khớp một-một tối ưu hoá tổng chi phí (sử dụng khoảng cách IoU từ file 03). Các phát hiện không khớp bắt đầu các track mới; các track không khớp bị chấm dứt sau một thời gian ân hạn.
-
DeepSORT mở rộng SORT bằng cách thêm một đặc trưng bề ngoài sâu (deep appearance feature): mỗi đối tượng được phát hiện được đưa qua một CNN nhỏ tạo ra một embedding bề ngoài (một vector mô tả). Chi phí khớp kết hợp khoảng cách IoU với khoảng cách cosine (chương 01) trong không gian embedding. Điều này xử lý sự che khuất (occlusion) và nhận dạng lại (re-identification): ngay cả khi một đối tượng biến mất sau một đối tượng khác trong vài khung hình, embedding bề ngoài của nó cho phép khớp lại khi nó xuất hiện trở lại.
-
ByteTrack (Zhang và cộng sự, 2022) cải thiện việc theo dõi bằng cách sử dụng mọi phát hiện, bao gồm cả những phát hiện có độ tin cậy thấp. Hầu hết các bộ theo dõi loại bỏ các phát hiện dưới một ngưỡng tin cậy. ByteTrack đầu tiên khớp các phát hiện độ tin cậy cao với các track hiện có, sau đó khớp các phát hiện độ tin cậy thấp còn lại với các track không khớp. Điều này phục hồi các đối tượng bị che khuất tạm thời hoặc mờ (và do đó có độ tin cậy phát hiện thấp).
-
Thị giác 3D khôi phục chiều không gian thứ ba bị mất trong phép chiếu ảnh 2D (file 01).
-
Ước lượng độ sâu (depth estimation) dự đoán khoảng cách từ camera đến mỗi điểm trong cảnh.
-
Độ sâu stereo sử dụng hai camera được tách ra bởi một đường cơ sở đã biết \(b\). Cùng một điểm xuất hiện ở các vị trí ngang khác nhau trong ảnh trái và phải (độ lệch này được gọi là disparity \(d\)). Độ sâu tỷ lệ nghịch với disparity:
-
trong đó \(f\) là tiêu cự và \(b\) là khoảng cách đường cơ sở. Tính toán disparity đòi hỏi tìm các điểm tương ứng giữa hai ảnh (stereo matching), là một tìm kiếm 1D dọc theo đường quét ngang (vì các camera được căn chỉnh theo phương ngang, một điểm cùng độ cao trong 3D chiếu vào cùng một hàng trong cả hai ảnh).
-
Ước lượng độ sâu đơn nhãn (monocular depth estimation) dự đoán độ sâu từ một ảnh đơn, đây về cơ bản là một bài toán không xác định (vô số cảnh 3D có thể tạo ra cùng một ảnh 2D). Thế nhưng con người làm điều này một cách dễ dàng bằng cách sử dụng các manh mối như kích thước tương đối, gradient kết cấu, sự che khuất, và sương mù khí quyển. Các mạng sâu học các manh mối này từ dữ liệu huấn luyện.
-
Các mô hình như MiDaS và Depth Anything dự đoán các bản đồ độ sâu tương đối (xếp hạng vật thể nào gần hơn) từ các ảnh đơn. Chúng được huấn luyện trên các tập dữ liệu đa dạng với một hàm mất mát bất biến tỷ lệ (scale-invariant) và tạo ra kết quả chính xác đáng kinh ngạc bất chấp tính mơ hồ về mặt lý thuyết.
-
Đám điểm (point clouds) là các tập hợp các điểm 3D \((x, y, z)\), có thể kèm theo màu sắc hoặc các thuộc tính khác, được thu bởi các cảm biến LiDAR hoặc tái tạo stereo. Không giống ảnh, đám điểm là không có thứ tự và được phân bố không đều.
-
PointNet (Qi và cộng sự, 2017) xử lý đám điểm trực tiếp bằng cách áp dụng các MLP dùng chung cho mỗi điểm độc lập, sau đó tổng hợp bằng max pooling (vốn bất biến với hoán vị, giải quyết vấn đề thứ tự). PointNet++ thêm nhóm phân cấp để nắm bắt cấu trúc cục bộ ở nhiều tỷ lệ.
-
Trường Bức Xạ Thần Kinh (Neural Radiance Fields - NeRF) (Mildenhall và cộng sự, 2020) biểu diễn một cảnh 3D như một hàm liên tục ánh xạ một vị trí 3D \((x, y, z)\) và hướng nhìn \((\theta, \phi)\) thành một màu sắc \((r, g, b)\) và mật độ \(\sigma\). Hàm này được tham số hoá bởi một MLP:
- Để kết xuất (render) một pixel, một tia (ray) được bắn từ camera qua pixel đó vào cảnh. Các điểm được lấy mẫu dọc theo tia, và MLP dự đoán màu sắc và mật độ tại mỗi điểm. Màu pixel được tính bằng kết xuất thể tích (volume rendering): tích phân màu được trọng số hoá bởi mật độ dọc theo tia:
- trong đó \(T(t) = \exp(-\int_{t_n}^{t} \sigma(\mathbf{r}(s)) \, ds)\) là độ truyền qua tích luỹ (transmittance - lượng ánh sáng đã bị hấp thụ cho đến nay). Trong thực tế, tích phân này được xấp xỉ bằng cách lấy mẫu \(N\) điểm dọc theo tia và cộng lại:
-
NeRF được huấn luyện bằng cách cực tiểu hoá MSE giữa các pixel được kết xuất và pixel thực tế từ một tập hợp các bức ảnh đã biết pose. Sau khi huấn luyện, NeRF có thể kết xuất các góc nhìn mới chân thực từ bất kỳ vị trí camera nào. Hạn chế là tốc độ: kết xuất đòi hỏi đánh giá MLP hàng triệu lần (một lần cho mỗi điểm lấy mẫu trên mỗi pixel), khiến việc kết xuất thời gian thực khó khăn.
-
3D Gaussian Splatting (Kerbl và cộng sự, 2023) giải quyết hạn chế về tốc độ của NeRF bằng cách biểu diễn cảnh như một tập hợp các nguyên hàm Gaussian 3D thay vì một hàm thể tích liên tục. Mỗi Gaussian có một vị trí 3D (trung bình), một ma trận hiệp phương sai 3D (kiểm soát hình dạng và hướng), độ mờ (opacity), và màu sắc (được biểu diễn dưới dạng harmonic cầu cho các hiệu ứng phụ thuộc góc nhìn).
-
Kết xuất chiếu mỗi Gaussian 3D lên mặt phẳng ảnh (tạo ra một "vết bắn" Gaussian 2D - splat), sắp xếp theo độ sâu, và tổng hợp từ trước ra sau bằng alpha blending. Đây là một quá trình raster hoá chạy trên GPU theo thời gian thực (100+ FPS), nhanh hơn nhiều so với ray marching của NeRF. Gaussian splatting ngang bằng hoặc vượt quá chất lượng NeRF trong khi cho phép kết xuất thời gian thực.
-
SLAM (Simultaneous Localisation and Mapping - Định vị và Lập bản đồ Đồng thời) là bài toán xây dựng một bản đồ của môi trường chưa biết trong khi đồng thời theo dõi vị trí của camera bên trong nó. Nó là nền tảng của robot, lái xe tự động, và AR.
-
Quang học tự động (Visual odometry) ước lượng chuyển động camera từ khung hình này sang khung hình khác bằng cách theo dõi các đặc trưng qua các ảnh. Các điểm đặc trưng (SIFT, ORB từ file 01) được khớp giữa các khung hình liên tiếp, và sự xoay và tịnh tiến của camera được ước lượng từ các tương ứng bằng ma trận thiết yếu (essential matrix) (mã hoá mối quan hệ hình học giữa hai góc nhìn, được suy ra từ các tham số nội và ngoại của file 01).
-
SLAM dựa trên đặc trưng (feature-based SLAM) mở rộng quang học tự động bằng cách duy trì một bản đồ bền vững. ORB-SLAM (Mur-Artal và cộng sự, 2015) là hệ thống SLAM dựa trên đặc trưng được sử dụng rộng rãi nhất. Nó có ba luồng song song:
- Theo dõi (Tracking): khớp các đặc trưng ORB trong mỗi khung hình mới với bản đồ, ước lượng pose camera bằng PnP (Perspective-n-Point) và RANSAC
- Lập bản đồ cục bộ (Local mapping): tam giác hoá các điểm bản đồ mới từ các đặc trưng đã khớp, tối ưu hoá vị trí của chúng bằng bundle adjustment (cực tiểu hoá sai số chiếu lại trên tất cả các góc nhìn thấy mỗi điểm)
- Đóng vòng (Loop closure): phát hiện khi camera quay lại một khu vực đã lập bản đồ trước đó (dùng bag-of-visual-words), sau đó sửa độ trôi tích lũy (drift) bằng cách tối ưu hoá toàn cục bản đồ
-
SLAM LiDAR sử dụng đám điểm 3D từ các cảm biến LiDAR thay vì (hoặc bổ sung cho) ảnh camera. LiDAR cung cấp các phép đo độ sâu trực tiếp, làm ước lượng hình học mạnh mẽ hơn nhưng với chi phí phần cứng cao hơn. Các phương pháp như LOAM (LiDAR Odometry and Mapping) đăng ký (register) các đám điểm giữa các lần quét liên tiếp bằng căn chỉnh iterative closest point (ICP).
-
SLAM Thị giác - Quán tính (Visual-Inertial SLAM) hợp nhất dữ liệu camera với các phép đo từ một IMU (gia tốc kế + con quay hồi chuyển). IMU cung cấp các ước lượng tần số cao về sự xoay và gia tốc để lấp đầy khoảng trống giữa các khung hình camera và xử lý chuyển động nhanh hoặc mất đặc trưng thị giác tạm thời.
-
Các ứng dụng VR/AR là một trong những người tiêu thụ thị giác máy tính đòi hỏi khắt khe nhất.
-
Ước lượng pose xác định vị trí và hướng của cơ thể người (hoặc khuôn mặt, hoặc bàn tay) từ ảnh. Pose cơ thể thường được biểu diễn như một tập hợp các vị trí keypoint 2D hoặc 3D (các khớp: vai, khuỷu tay, cổ tay, hông, đầu gối, mắt cá chân). Các mô hình như OpenPose và MediaPipe dự đoán các keypoint này bằng hồi quy bản đồ nhiệt (heatmap): với mỗi khớp, mô hình xuất ra một bản đồ nhiệt nơi đỉnh chỉ ra vị trí của khớp.
-
Các phương pháp từ trên xuống (top-down) trước tiên phát hiện người bằng một bộ phát hiện khung bao (file 03), sau đó ước lượng pose trong mỗi khung bao. Các phương pháp từ dưới lên (bottom-up) phát hiện tất cả các keypoint trong ảnh trước, sau đó nhóm chúng thành từng cá nhân bằng các trường ái lực phần (part affinity Fields - các trường vector mã hoá sự liên kết giữa các khớp được kết nối).
-
Tái tạo cảnh (scene reconstruction) xây dựng một mô hình 3D của môi trường từ dữ liệu cảm biến. Trong AR, điều này cho phép đặt các vật thể ảo trên các bề mặt thật, che khuất các vật thể ảo đằng sau các vật thể thật, và đổ bóng ảo. Các phương pháp tái tạo cảnh thời gian thực (như các hệ thống dựa trên cảm biến độ sâu trong ARKit và ARCore) xây dựng một lưới thưa (sparse mesh) của môi trường cập nhật khi người dùng di chuyển.
-
Các ràng buộc kết xuất thời gian thực trong VR là cực đoan: cả hai mắt cần các kết xuất riêng biệt ở 90+ FPS (để tránh say xe), với độ trễ dưới 20 mili giây từ chuyển động đầu đến cập nhật màn hình. Các kỹ thuật như kết xuất tập trung vào điểm nhìn (foveated rendering) (kết xuất ở độ phân giải cao chỉ nơi người dùng đang nhìn, dùng eye tracking) và tái chiếu (reprojection) (làm cong khung hình trước dựa trên pose đầu mới để lấp đầy khoảng trống trong khi khung tiếp theo được kết xuất) là cần thiết để đáp ứng các ràng buộc này.
-
Sự hội tụ của kết xuất neural thời gian thực (3D Gaussian splatting), theo dõi mạnh mẽ (visual-inertial SLAM), và ước lượng pose hiệu quả đang làm cho các trải nghiệm AR/VR chân thực, tương tác trở nên khả thi hơn.
Bài tập lập trình (dùng CoLab hoặc notebook)¶
-
Cài đặt thuật toán optical flow Lucas-Kanade từ đầu. Tính toán dòng chảy giữa hai khung hình tổng hợp nơi một hình vuông di chuyển sang phải.
import jax.numpy as jnp import matplotlib.pyplot as plt def lucas_kanade(frame1, frame2, window_size=5): """Lucas-Kanade optical flow.""" # Compute gradients Ix = jnp.zeros_like(frame1) Iy = jnp.zeros_like(frame1) It = frame2 - frame1 # Sobel-like gradients Ix = Ix.at[1:-1, :].set((frame1[2:, :] - frame1[:-2, :]) / 2) Iy = Iy.at[:, 1:-1].set((frame1[:, 2:] - frame1[:, :-2]) / 2) H, W = frame1.shape half_w = window_size // 2 u = jnp.zeros_like(frame1) v = jnp.zeros_like(frame1) for i in range(half_w, H - half_w): for j in range(half_w, W - half_w): Ix_win = Ix[i-half_w:i+half_w+1, j-half_w:j+half_w+1].ravel() Iy_win = Iy[i-half_w:i+half_w+1, j-half_w:j+half_w+1].ravel() It_win = It[i-half_w:i+half_w+1, j-half_w:j+half_w+1].ravel() A = jnp.stack([Ix_win, Iy_win], axis=1) ATA = A.T @ A ATb = -A.T @ It_win # Check if the system is well-conditioned det = ATA[0,0] * ATA[1,1] - ATA[0,1] * ATA[1,0] if jnp.abs(det) > 1e-6: flow = jnp.linalg.solve(ATA, ATb) u = u.at[i, j].set(flow[0]) v = v.at[i, j].set(flow[1]) return u, v # Create two frames: a white square that moves right frame1 = jnp.zeros((64, 64)) frame1 = frame1.at[20:40, 15:35].set(1.0) frame2 = jnp.zeros((64, 64)) frame2 = frame2.at[20:40, 20:40].set(1.0) # shifted 5 pixels right u, v = lucas_kanade(frame1, frame2, window_size=7) # Visualise fig, axes = plt.subplots(1, 3, figsize=(14, 4)) axes[0].imshow(frame1, cmap='gray'); axes[0].set_title('Frame 1'); axes[0].axis('off') axes[1].imshow(frame2, cmap='gray'); axes[1].set_title('Frame 2'); axes[1].axis('off') # Quiver plot of flow (subsample for clarity) step = 4 Y, X = jnp.mgrid[0:64:step, 0:64:step] axes[2].imshow(frame1, cmap='gray', alpha=0.5) axes[2].quiver(X, Y, u[::step, ::step], v[::step, ::step], color='#e74c3c', scale=50, width=0.005) axes[2].set_title('Optical Flow'); axes[2].axis('off') plt.tight_layout(); plt.show() # Check average flow in the moving region region_u = u[20:40, 15:35] print(f"Average horizontal flow in object region: {region_u[region_u != 0].mean():.2f} pixels") -
Cài đặt một bộ lọc Kalman đơn giản cho theo dõi đối tượng 2D. Mô phỏng một quỹ đạo nhiễu và cho thấy bộ lọc Kalman làm mượt các ước lượng.
import jax import jax.numpy as jnp import matplotlib.pyplot as plt def kalman_predict(x, P, F, Q): """Kalman filter prediction step.""" x_pred = F @ x P_pred = F @ P @ F.T + Q return x_pred, P_pred def kalman_update(x_pred, P_pred, z, H, R): """Kalman filter update step.""" y = z - H @ x_pred # innovation S = H @ P_pred @ H.T + R # innovation covariance K = P_pred @ H.T @ jnp.linalg.inv(S) # Kalman gain x_updated = x_pred + K @ y P_updated = (jnp.eye(len(x_pred)) - K @ H) @ P_pred return x_updated, P_updated # State: [x, y, vx, vy] dt = 1.0 F = jnp.array([[1, 0, dt, 0], # state transition [0, 1, 0, dt], [0, 0, 1, 0], [0, 0, 0, 1]]) H = jnp.array([[1, 0, 0, 0], # observation: we measure x, y [0, 1, 0, 0]]) Q = jnp.eye(4) * 0.01 # process noise R = jnp.eye(2) * 4.0 # measurement noise (noisy detector) # Simulate ground truth: circular motion n_steps = 50 t = jnp.linspace(0, 2 * jnp.pi, n_steps) true_x = 10 * jnp.cos(t) + 20 true_y = 10 * jnp.sin(t) + 20 # Noisy observations key = jax.random.PRNGKey(42) noise = jax.random.normal(key, (n_steps, 2)) * 2.0 obs_x = true_x + noise[:, 0] obs_y = true_y + noise[:, 1] # Run Kalman filter x = jnp.array([obs_x[0], obs_y[0], 0.0, 0.0]) # initial state P = jnp.eye(4) * 10.0 # initial uncertainty kalman_x, kalman_y = [], [] for i in range(n_steps): x, P = kalman_predict(x, P, F, Q) z = jnp.array([obs_x[i], obs_y[i]]) x, P = kalman_update(x, P, z, H, R) kalman_x.append(x[0]) kalman_y.append(x[1]) kalman_x = jnp.array(kalman_x) kalman_y = jnp.array(kalman_y) # Visualise plt.figure(figsize=(8, 8)) plt.plot(true_x, true_y, 'k-', linewidth=2, label='Ground Truth') plt.scatter(obs_x, obs_y, c='#e74c3c', s=20, alpha=0.5, label='Noisy Observations') plt.plot(kalman_x, kalman_y, '#3498db', linewidth=2, label='Kalman Filter') plt.legend(); plt.grid(alpha=0.3) plt.title('Kalman Filter Tracking') plt.xlabel('x'); plt.ylabel('y') plt.axis('equal'); plt.show() obs_error = jnp.mean(jnp.sqrt((obs_x - true_x)**2 + (obs_y - true_y)**2)) kalman_error = jnp.mean(jnp.sqrt((kalman_x - true_x)**2 + (kalman_y - true_y)**2)) print(f"Observation RMSE: {obs_error:.2f}") print(f"Kalman filter RMSE: {kalman_error:.2f}") print(f"Error reduction: {(1 - kalman_error/obs_error) * 100:.1f}%") -
Cài đặt một đường ống kết xuất thể tích kiểu NeRF đơn giản. Bắn các tia qua một cảnh 3D đơn giản (các hình cầu với màu và mật độ đã biết) và kết xuất một ảnh bằng cách tích phân dọc theo mỗi tia.
import jax import jax.numpy as jnp import matplotlib.pyplot as plt def render_ray(origin, direction, spheres, n_samples=64, t_near=1.0, t_far=6.0): """Volume render a single ray through a scene of spheres.""" t_vals = jnp.linspace(t_near, t_far, n_samples) deltas = jnp.concatenate([jnp.diff(t_vals), jnp.array([1e-3])]) colour = jnp.zeros(3) transmittance = 1.0 for i in range(n_samples): point = origin + t_vals[i] * direction # Compute density and colour at this point density = 0.0 point_colour = jnp.zeros(3) for center, radius, col, sigma in spheres: dist = jnp.linalg.norm(point - center) # Soft sphere: density falls off with distance from surface d = jnp.exp(-jnp.maximum(0, dist - radius) * sigma) * sigma density += d point_colour += d * jnp.array(col) # Normalise colour by total density point_colour = jnp.where(density > 1e-6, point_colour / density, point_colour) # Volume rendering equation alpha = 1.0 - jnp.exp(-density * deltas[i]) colour += transmittance * alpha * point_colour transmittance *= (1.0 - alpha) return colour # Scene: three coloured spheres spheres = [ (jnp.array([0.0, 0.0, 4.0]), 0.8, [1.0, 0.2, 0.2], 5.0), # red (jnp.array([1.5, 0.5, 5.0]), 0.6, [0.2, 1.0, 0.2], 5.0), # green (jnp.array([-1.0, -0.5, 3.5]), 0.5, [0.2, 0.2, 1.0], 5.0), # blue ] # Camera setup img_h, img_w = 64, 64 focal = 60.0 origin = jnp.array([0.0, 0.0, 0.0]) image = jnp.zeros((img_h, img_w, 3)) for i in range(img_h): for j in range(img_w): # Compute ray direction px = (j - img_w / 2) / focal py = -(i - img_h / 2) / focal direction = jnp.array([px, py, 1.0]) direction = direction / jnp.linalg.norm(direction) colour = render_ray(origin, direction, spheres) image = image.at[i, j].set(jnp.clip(colour, 0, 1)) plt.figure(figsize=(6, 6)) plt.imshow(image) plt.title('NeRF-style Volume Rendering\n(3 spheres)') plt.axis('off') plt.tight_layout(); plt.show() print(f"Image shape: {image.shape}") print(f"Rendered {img_h * img_w} rays with 64 samples each")