Hồi bé, khi xem mấy bộ phim viễn tưởng, thấy các diễn viên huơ huơ tay trong không khi và viết ra những chữ loằng ngoằng trên màn hình, mình thấy thật cool ngầu. Chắc không ít lần mình ước có thể thực hiện được như họ.
Với công nghệ ngày nay thì việc này hoàn toàn không khó. Bạn chỉ cần bỏ ra ít tiền, sắm cho mình một số thiết bị cầm tay xịn xịn như ipad, tablet, … kèm bút cảm ứng xịn xịn là có thể thực hiện được ước mơ ngày bé rồi.
Nhưng nếu bạn không có nhiều tiền thì sao? Câu trả lời sẽ có trong bài hôm nay. Mình sẽ hướng dẫn các bạn tạo ra một virtual pen, có thể viết, vẽ trong không khí bất cứ gì bạn muốn, và những thứ đó sẽ thực sự được vẽ ra trên màn hình máy tính của bạn. Điều đặc biệt là bạn không cần phải sắm thêm bất cứ phần cứng đắt tiền nào cả. Chỉ cần một máy tính (không có màn hình cảm ứng), một camera và một chiếc bút bình thường. Về mặt công nghệ, chúng ta cũng chỉ phải sử dụng đến vài kỹ thuật xử lý ảnh đơn giản trong thư viện OpenCV, không cần AI hay Deep Learning gì cao siêu gì sất.
Bố cục bài viết sẽ bao gồm 2 phần:
Ý tưởng của thuật toán như sau.
Chỉ vậy thôi là ta đã có được một chiếc Virtual Pen để xài rồi.
Bước đầu tiên và quan trọng nhất là tìm ra khoảng giá trị phù hợp của bút để có thể dựa vào đó nhận diện chính xác bút vẽ.
Để làm điều này được dễ dàng hơn, chúng ta nên chuyển hệ màu của ảnh từ BGR (Blue Green Red) sang hệ màu HSV (* Hue Saturation, Value*). Xem thêm về các *Color Space* tại đây.
Mình cũng sẽ tạo ra một giao diện đơn giản với các thanh trượt (trackbars) để các bạn có thể tùy chỉnh theo bút của các bạn. Sau khi tìm được khoảng giá trị phù hợp rồi, chúng ta sẽ lưu lại để sử dụng trong các bước tiếp theo (bấm phím s).
Code thực hiện bước này như sau:
Các bạn có thể xem video mình thực hiện bước này như dưới đây:
Tại bước này, bạn cũng không cần phải chọn được khoảng giá trị quá chuẩn chỉ, có thể vẫn còn một chút nhiễu kiểu như các chấm trắng li ti trong ảnh cũng chấp nhận được. Ở bước thứ 2, chúng ta sẽ có cách loại bỏ chúng.
Sử dụng một số phép biến đổi nâng cao như Erosion và Dilation, ta có thể loại bỏ đuợc 1 số nhiễu nhỏ còn tồn tại ở bước 1. Chi tiết về 2 phép toán này, các bạn có thể tham khảo tại đây.
Trong bài này, mình sử dụng chung một kernel matrix 5x5 cho cả 2 Erosion và Dilation. Số lượng vòng lặp (iterations) và kích thước của kernel matrix có thể phụ thuộc vào đối tượng cần phát hiện và loại nhiễu cần loại bỏ. Nếu hiểu sâu sắc về 2 phép biến đổi này thì bạn có thể thử thay đổi 2 tham số kể trên xem sao.
Code thực hiện của bước này như sau:
Kết quả:
Đến đây, nếu vẫn còn nhiễu … cũng không sao. Bước tiếp theo, chúng ta sẽ có một mẹo nhỏ để loại bỏ chúng. :D
Bước này cũng khá quan trọng và thú vị. Nó sử dụng một kỹ thuật xử lý ảnh rất phổ biến và hữu dụng, đó là Contour Detection. Hiểu một cách đơn giản thì Contour là một vùng diện tích mà một đối tượng chiếm cứ trên bức ảnh. Tìm hiểu thêm về nó tại đây.
Tại bước này, Contour được sử dụng để tìm ra chính xác vị trí của bút và theo dõi chuyển động của nó khi ta viết. Vị trí được thể hiện bằng 4 giá trị (x,y,w,h) có được thông qua gọi hàm cv2.getBoundingRect(). Ta sẽ đánh dấu nó bằng một hình chữ nhật bao quanh đối tượng để dễ bề theo dõi. Để loại bỏ bớt nhiễu (nếu có), ta chỉ lấy đối tượng nào mà Contour của nó có diện tích lớn nhất, còn lại thì bỏ đi. Diện tích này là kết quả trả về của hàm cv2.contourArea().
Code thực hiện như sau:
Kết quả thực hiện:
Mọi thứ đã được chuẩn bị cơ bản. Giờ là lúc ta có thể bắt đầu thiết lập để viết/vẽ được rồi.
Để viết/vẽ, ta chỉ cần nối toạ độ của bút ở frame trước đó ($x_1,y_1$) với tọa độ của bút trong frame hiện tại ($x_2,y_2$). Với FPS của camera vào khoảng 20 đến 24 fps thì mắt người nhìn vào sẽ giống như là đang viết/vẽ realtime vậy.
Chú ý là ở đây, mình sử dụng thêm một canvas màu đen, có kích thước đúng bằng kích thức của frame để vẽ lên nó. Sau đó mới tiến hành tích hợp canvas vào frame hiện tại. Việc tách riêng phần vẽ sang một nơi khác có tác dụng ở bước số 5 và số 6.
Code thực hiện cho bước này:
Kết quả thực hiện:
Mình có thêm chức năng xóa toàn bộ những gì đã vẽ khi bấm phím c để tiện cho việc test. Trong bước tiếp theo, ta sẽ làm cho chức năng xóa này chuyên nghiệp hơn.
Trong trường hợp viết/vẽ sai, bạn muốn làm lại từ đầu thì sao? Chức năng xóa sẽ giúp bạn thực hiện điều đó. Bước này ta sẽ thêm vào chức năng xóa toàn bộ những gì đã viết/vẽ. Bước tiếp theo, ta sẽ thêm nốt chức năng xóa từng phần theo ý muốn.
Một điều rất hiển nhiên là diện tích của Contour sẽ tăng lên khi đối tượng tiến vào gần camera. Sử dụng thuộc tính này, ta sẽ trigger môt sự kiện khi diện tích Contour của bút lớn hơn 1 giá trị ngưỡng để xóa toàn bộ nội dung mà ta đã viết/vẽ trước đó. Còn để thực hiện xóa thì chỉ cần set giá trị cho canvas bằng None là được.
Code thực hiện như sau:
Kết quả thực hiện:
Nếu bạn để ý thì mình đã thêm vào đoạn xử lý từ dòng 44-47 trong hàm draw_line() để việc vẽ trông tự nhiên hơn. Vẫn chỉ là các thao tác xử lý ảnh cơ bản thôi, nhưng mang lại hiệu quả khá tốt trong trường hợp của mình. Bạn có thể sử dụng hoặc không tùy theo tình huống cụ thể phía bạn.
Đây là bước cuối cùng để hoàn thiện sản phẩm của chúng ta. Tưởng tượng bạn đang vẽ một bức tranh rất đẹp, nhưng chẳng may sai một nét mà lại xóa đi vẽ lại từ đầu thì thật là khó chịu vào mất thời gian. Vì thế, mình sẽ thêm vào chức năng xóa từng phần, tức chỉ xóa những gì mà ta muốn.
Có 2 công việc cần thực hiện để hoàn thành chức năng này:
Đối với công việc thứ nhất, ta có thể thực hiện rất dễ dàng bằng cách bấm các phím khác nhau. Nhưng mình không muốn làm như thế, mình không muốn chạm vào bất cứ thứ gì, chỉ thao tác trong không khí mà thôi. Cách làm sẽ là yêu cầu người dùng đưa bàn tay lên góc trên bên trái màn hình. Sử dụng kỹ thuật Background Subtraction, chúng ta có thể phát hiện ra điều đó và thực hiện chuyển qua lại giữa 2 chế độ viết/vẽ và xóa.
Đối với công việc thứ 2, ta cũng có thể thực hiện dễ dàng bằng cách vẽ lên canvas màu đen đúng tại vị trí muốn xóa.
Code thực hiện đầy đủ như sau:
Và đây là thành quả cuối cùng:
Khá ấn tượng đấy chứ, :D!
Mẹo nhỏ: Để ngắt quãng các nét viết/vẽ khi di chuyển bút sang vị trí khác, mình đã che một bên bút bằng màu sắc khác ngoài dải phát hiện đối tượng mà ta chọn ở bước 1. Khi muốn ngắt, ta chỉ cần quay phần có màu khác đó hướng về phía camera. Còn khi muốn vẽ ta làm ngược lại.
Nếu theo dõi kỹ từ đầu và test thực tế thì bạn có thể nhận ra 2 vấn đề của ứng dụng này:
Với vấn đề thứ nhất, thoạt đầu mình nghĩ nó là hạn chế, nhưng chợt thấy các thiết bị có tính năng bút cảm ứng cũng chỉ cho phép sử dụng 1 loại bút duy nhất mà nó cung cấp, nên có thể nói đây cũng ko hẳn là hạn chế. Nếu muốn sử dụng được nhiều loại bút hơn thì ta có thể nghĩ đến giải pháp thay thế kỹ thuật nhận diện đối tượng bằng OpenCV bằng kỹ thuật Object Detection trong Deep Learning. Tất nhiên, cái giá phải trả là mất nhiều thời gian, công sức để thực hiện hơn so với cách làm hiện tại.`
Vấn đề thứ 2 thì đúng là hạn chế. Có thể giải quyết vấn đề này bằng cách thêm vào kỹ thuật Tracking đối tượng. Khi đó thì nét vẽ sẽ liên tục thậm chí khi người dùng di chuyển bút với tốc độ nhanh hơn.
Nếu 2 vấn đề nêu trên được giải quyết một cách triệt để thì ứng dụng này hoàn toàn có thể triển khai đến cho end-user sử dụng được. :D
Vậy là chúng ta đã hoàn thành việc xây dựng ứng dụng bút ảo bằng OpenCV. Mình nghĩ là có khá nhiều kiến thức thú vị mà bạn đã học được thông qua bài này. Cảm ơn các bạn đã theo dõi!
Toàn bộ code của bài này, các bạn có thể tham khảo tại đây.
[1] Taha Anwar, “Creating a Virtual Pen And Eraser with OpenCV”, Available online: https://learnopencv.com/creating-a-virtual-pen-and-eraser-with-opencv/ (Accessed on 15 Aug 2021).