5.11. So sánh khung hình¶
So sánh khung hình (frame differencing) đối chiếu từng khung hình mới với một khung hình tham chiếu đã lưu để tìm ra những phần của cảnh đã thay đổi. Đây là nền tảng của các ứng dụng camera theo dõi sự kiện -- chụp ảnh khi phát hiện chuyển động, cảnh báo xâm nhập, "lưu video khi có gì đó di chuyển" -- và được xây dựng hoàn toàn từ các phép toán theo từng điểm ảnh đã đề cập trước đó: hiệu giá trị tuyệt đối, ngưỡng và tìm kiếm vùng, chạy trên mỗi khung hình.
5.11.1. Pipeline cơ bản¶
Giai đoạn đầu tiên là thu thập tham chiếu. Vào thời điểm nào đó gần lúc khởi động -- lý tưởng nhất là khi cảnh ở trạng thái "không có thay đổi" -- ứng dụng chụp một khung hình và giữ lại. Khung hình đó trở thành đường cơ sở để so sánh với mọi lần chụp tiếp theo.
reference = csi0.snapshot().copy()
Lệnh .copy() rất quan trọng. csi0.snapshot() nếu chỉ dùng một mình sẽ trả về một Image có bộ đệm nằm trong bộ đệm khung hình, nơi lần gọi snapshot tiếp theo sẽ ghi đè lên nó. .copy() cấp phát một bộ đệm riêng cho tham chiếu và giúp các điểm ảnh của khung hình này tồn tại qua lần chụp tiếp theo.
Giai đoạn thứ hai chạy trên mỗi khung hình: chụp một ảnh mới, sau đó tính hiệu giá trị tuyệt đối giữa nó và tham chiếu. Đó chính xác là những gì difference() thực hiện:
current = csi0.snapshot()
current.difference(reference)
Sau lần gọi này, current chứa một ảnh mà các điểm ảnh khác không đánh dấu mọi vị trí nơi cảnh đã thay đổi kể từ khi lấy tham chiếu, với độ lớn của từng điểm ảnh tỉ lệ thuận với mức độ thay đổi tại vị trí đó.
Giai đoạn thứ ba ngưỡng hóa ảnh hiệu. Hiệu thô luôn chứa một số nhiễu: các biến đổi độ sáng nhỏ từ nhiễu shot của cảm biến, thay đổi gradient từ độ trôi ánh sáng, rung sub-pixel do chuyển động nhỏ của camera. Một lần ngưỡng hóa -- binary() với ngưỡng đặt trên mức nhiễu đó -- chỉ giữ lại những thay đổi đủ lớn để tính là chuyển động thực và bỏ đi phần còn lại, tạo ra ảnh nhị phân mà các điểm ảnh khác không là các vị trí thực sự đã thay đổi.
Giai đoạn thứ tư trích xuất các vùng liên thông của mặt nạ nhị phân đó -- các nhóm điểm ảnh khác không liền kề nhau tạo thành các mảng liên tục. find_blobs() thực hiện điều đó trong một lần gọi, trả về danh sách các vùng chuyển động, mỗi vùng có hộp giới hạn và số điểm ảnh, mà phần còn lại của ứng dụng có thể xử lý.
Pipeline frame-differencing: một khung hình tham chiếu cộng với khung hình hiện tại trở thành ảnh hiệu; ngưỡng hóa biến hiệu thành mặt nạ nhị phân của các vị trí đã thay đổi; bước vùng liên thông biến mặt nạ thành danh sách các vùng chuyển động.¶
5.11.2. Tham chiếu trong bộ nhớ và trên đĩa¶
Pipeline cơ bản giữ khung hình tham chiếu trong RAM. Đó là lựa chọn đúng khi tham chiếu được chụp trong lần chạy này của tập lệnh và chỉ cần tồn tại trong khi tập lệnh đang chạy.
Đối với một ứng dụng chạy dài -- một camera cần tiếp tục phát hiện thay đổi sau khi bật nguồn lại, một tập lệnh chạy không liên tục cần phát hiện bất kỳ thay đổi nào kể từ một thời điểm trước đó -- khung hình tham chiếu phải tồn tại lâu hơn tập lệnh đang chạy. Mẫu là lưu tham chiếu ra đĩa:
csi0.snapshot().save("/sdcard/reference.bmp")
và tải lại vào đầu mỗi lần chạy:
reference = image.Image("/sdcard/reference.bmp")
Logic so sánh không thay đổi; chỉ có nơi lưu tham chiếu giữa các lần chụp là khác. Một vài cải tiến tự nhiên mở rộng biến thể trên đĩa này -- tự động chụp lại tham chiếu theo bộ định thời, tùy chọn trung bình trượt để theo dõi độ trôi ánh sáng chậm -- nhưng sự thay thế ở trung tâm vẫn giống nhau.
5.11.3. Cô lập nguồn sáng¶
Cùng mẫu phép trừ xuất hiện trong một bối cảnh hơi khác: cô lập một nguồn sáng so với phần còn lại của cảnh. Thủ thuật là chụp một tham chiếu "đèn tắt" -- một khung hình được chụp khi thứ đang được phát hiện (đèn hiệu IR, điểm ảnh màn hình, đèn chỉ thị trạng thái) không được chiếu sáng -- và trừ tham chiếu đó khỏi mỗi khung hình tiếp theo. Kết quả có độ sáng bằng không ở khắp nơi cảnh giống nhau trong cả hai lần chụp, và độ sáng khác không chỉ ở nơi nguồn sáng thực sự bật sáng.
5.11.4. Chọn difference hay sub¶
Một lưu ý thực tế về việc chọn phép toán số học nào. difference() trả về giá trị tuyệt đối của thay đổi -- không có dấu -- khiến nó nhạy cảm với thay đổi theo cả hai chiều (sáng hơn hoặc tối hơn) với cái giá là không cho ứng dụng biết chiều thay đổi theo hướng nào. Đối với phát hiện chuyển động thuần túy, đó là lựa chọn đúng: bất cứ thứ gì di chuyển đều thú vị, bất kể độ sáng thay đổi theo hướng nào.
Đối với phát hiện nguồn sáng, điểm ảnh được chiếu sáng luôn sáng hơn so với tham chiếu khi đèn tắt, vì vậy sub() (với tính năng kẹp tại không) là lựa chọn trung thực hơn. Bất kỳ nơi nào khung hình hiện tại tối hơn tham chiếu (điều này sẽ là nhiễu cảm biến xung quanh giá trị không sáng) sẽ bị kẹp về không thay vì báo cáo tín hiệu giả "đèn đang bật".