5.1. Đối tượng Image¶
Một thuật toán xử lý ảnh duyệt qua từng điểm ảnh một. Tại mỗi vị trí, nó thực hiện một thao tác đơn giản -- đọc một giá trị, so sánh với ngưỡng, kết hợp với điểm ảnh tương ứng của ảnh thứ hai, ghi kết quả trở lại. Lặp đi lặp lại trên toàn bộ khung hình, những quyết định đơn giản theo từng điểm ảnh đó chính là nền tảng xây dựng nên phát hiện cạnh, theo dõi vùng màu (blob), giải mã mã QR và mọi kỹ thuật thị giác máy cổ điển khác. Để thực hiện công việc đó hiệu quả, thuật toán cần biết vị trí của mỗi điểm ảnh trong bộ nhớ, giá trị thực sự của mỗi điểm ảnh là gì, và phần nào của ảnh cần được xem xét. image.Image là đối tượng tổ chức toàn bộ thông tin đó.
Vision Sensors kết thúc tại thời điểm csi.CSI.snapshot() trả về. Mọi xử lý phía camera để tạo ra khung hình đã được chụp đều đã hoàn tất; ứng dụng đang cầm Image trong tay và cần biết phải làm gì với nó.
5.1.1. Bộ đệm và các thuộc tính của nó¶
Bên trong Image là một con trỏ đến một khối byte liên tiếp trong RAM cùng một tiêu đề nhỏ mang ba phần siêu dữ liệu: chiều rộng ảnh tính bằng điểm ảnh, chiều cao ảnh tính bằng điểm ảnh, và định dạng điểm ảnh của các byte đó. Các byte là chính các điểm ảnh, được lưu theo thứ tự hàng trước cột -- tất cả điểm ảnh của hàng trên cùng trước, rồi đến tất cả điểm ảnh của hàng thứ hai, và tiếp tục xuống đến hàng dưới cùng. Các thuộc tính mô tả cách đọc chúng.
Chiều rộng và chiều cao là các số nguyên đơn giản. Định dạng điểm ảnh là thuộc tính thú vị hơn, vì nó xác định số byte mỗi điểm ảnh chiếm và ý nghĩa của các byte đó. Ảnh thang xám mang một byte cho mỗi điểm ảnh lưu giá trị độ sáng. Ảnh RGB565 mang hai byte cho mỗi điểm ảnh lưu các trường đỏ, lục và lam được đóng gói trong một từ 16-bit. Ảnh Bayer mang một byte cho mỗi điểm ảnh, nhưng mỗi điểm ảnh được lấy mẫu qua một trong ba bộ lọc màu sắc được chọn theo vị trí của nó trong mảng mosaic. Vision Sensors đã liệt kê toàn bộ danh mục; điều quan trọng ở đây là đúng một trong những định dạng đó được thiết lập trên mỗi Image, và lựa chọn đó quyết định phép tính bytes-per-pixel và ý nghĩa của bất kỳ byte đơn lẻ nào trong bộ đệm.
Với một con trỏ đến bộ đệm, chiều rộng, chiều cao và định dạng, mọi thuộc tính khác mà một thuật toán có thể cần đều được tính ra từ một phép tính ngắn. Byte bắt đầu điểm ảnh (x, y) nằm tại offset (y * width + x) * bytes_per_pixel tính từ đầu bộ đệm. Tổng số byte là width * height * bytes_per_pixel. Địa chỉ của hàng tiếp theo phía dưới cách đầu hàng hiện tại đúng width * bytes_per_pixel byte. Image hiển thị ba thuộc tính qua các lệnh gọi phương thức thông thường -- width(), height(), format() -- cộng với size được tính từ size(). Các phương thức khác trong module sử dụng những giá trị đó để tự thực hiện phép tính offset; code ứng dụng hiếm khi phải làm điều đó.
Một Image là một lớp bọc Python nhỏ trỏ đến một khối bộ nhớ liên tiếp: một tiêu đề mang chiều rộng, chiều cao và định dạng điểm ảnh, tiếp theo là chính bộ đệm điểm ảnh.¶
5.1.2. Bộ đệm đến từ đâu¶
Câu chuyện mặc định xuyên suốt chương này là điều Vision Sensors đã đề cập: một khung hình được chụp đến từ snapshot, các byte nằm trong bộ đệm khung hình của camera, và Image được trả về trỏ vào chúng. Ba cách khác để lấy một đối tượng Image xuất hiện thường xuyên, và mỗi cách ngụ ý điều gì đó khác nhau về vị trí bộ đệm kết thúc.
Tải từ file trông giống như truyền một đường dẫn vào constructor: image.Image("/sdcard/saved.jpg"). Module đọc file vào một bộ đệm mới được cấp phát trên Python heap. Các file BMP, PGM và PPM được giải mã trong quá trình đọc vào và Image kết quả mang một định dạng điểm ảnh không nén. Các file JPEG và PNG giữ nguyên nén -- Image mang định dạng JPEG hoặc PNG, và bộ đệm giữ luồng byte của file hầu như không thay đổi. Để thực hiện bất kỳ công việc ở cấp độ điểm ảnh nào trên ảnh nén, ứng dụng chuyển đổi nó qua to_rgb565() hoặc to_grayscale() trước, và đó là nơi quá trình giải nén -- và sự phình to heap tương ứng, khi một file JPEG 30 KB có thể trở thành 600 KB RGB565 -- thực sự xảy ra. Tải từ file hữu ích nhất trong quá trình phát triển, khi một thuật toán cần được kiểm tra so với một khung hình tham chiếu đã biết được lưu cùng với tập lệnh.
Tạo từ đầu là trường hợp canvas: image.Image(320, 240, image.RGB565) yêu cầu module cấp phát số byte đó theo định dạng đó, xóa nội dung và trả lại lớp bọc. Các điểm ảnh chưa có ý nghĩa gì -- chúng đều là không -- nhưng ảnh rỗng là nền tảng cho một số mẫu lặp lại: khung hình tham chiếu dùng để trừ với khung hình hiện tại, canvas để vẽ lớp phủ đồ họa, bộ đệm nhị phân được điền vào và sử dụng làm mặt nạ.
Tạo từ ndarray kết nối theo hướng ngược lại, từ bất kỳ tính toán số học nào trở lại module ảnh. Truyền một ulab.numpy.ndarray kiểu float32 vào constructor tạo ra một Image có kích thước khớp với ndarray -- hình dạng hai trục (h, w) trở thành ảnh thang xám, hình dạng ba trục (h, w, 3) trở thành RGB565 -- với các giá trị float được thu phóng từ 0.0 -- 255.0 vào phạm vi điểm ảnh số nguyên. Một heatmap mạng nơ-ron, một mảng số học bất kỳ, bất cứ thứ gì được tạo ra bởi ml hoặc ulab đều trở thành thứ mà phía vẽ và kiểm tra của module ảnh có thể sử dụng.
Cả bốn nguồn đều trả về cùng một loại Image. Code sử dụng đối tượng được trả về không bao giờ phải theo dõi nó đến từ đâu.
5.1.3. Hai cách nhìn vào các byte¶
Hầu hết thời gian, code ứng dụng xử lý Image như một đối tượng ảnh có kiểu -- một thứ có các phương thức có tên. Nửa còn lại của câu chuyện là cùng đối tượng đó cũng xuất hiện, một cách minh bạch, như một chuỗi byte phẳng với bất kỳ API MicroPython nào nhận đối số bytes. Các byte không phải là bản sao của bộ đệm; chúng là một view trực tiếp vào nó.
Cách sắp xếp đó là điều làm cho việc đẩy một khung hình được chụp ra khỏi cam chỉ cần một dòng code. Băm nó, gửi qua cổng serial, chuyển tiếp đến một socket mạng -- không cái nào trong số đó cần một bước "chuyển đổi ảnh thành byte" riêng biệt:
import csi
import hashlib
csi0 = csi.CSI()
csi0.reset()
csi0.pixformat(csi.RGB565)
csi0.framesize(csi.QQVGA)
img = csi0.snapshot()
uart.write(img) # transmits the raw pixel bytes
hashlib.sha256(img) # hashes the same bytes
sock.send(img) # sends them over a socket
View dạng bytes là chỉ đọc theo mặc định, và điều đó là có chủ ý. Các bộ đệm ảnh lớn và đôi khi được chia sẻ giữa các lớp của imaging stack, do đó việc cho phép một lệnh buf[0] = 0 bình thường nào đó nằm sâu trong call stack có khả năng âm thầm làm hỏng một cái là quá nguy hiểm. Khi quyền truy cập đọc-ghi ở cấp byte là điều ứng dụng thực sự cần -- ví dụ như ghi một giá trị hiệu chỉnh vào một offset đã biết -- bytearray() trả về một view riêng biệt, rõ ràng là đọc-ghi trên cùng bộ nhớ, đánh dấu ý định tại call site.
5.1.4. Bộ đệm nằm ở đâu¶
Các bộ đệm điểm ảnh đủ lớn để vị trí của chúng trong RAM trở nên quan trọng. Một khung hình RGB565 QQVGA là 160 × 120 × 2 = 38.400 byte; một khung hình RGB565 VGA là 614.400 byte; một đầu vào RGB565 224 × 224 mà một bộ phân loại mạng nơ-ron có thể tiêu thụ là khoảng 100 KB. Python heap trên các cam nhỏ nhất có thể chỉ là vài chục kilobyte sau khi runtime đã khởi động. Giữ nhiều hơn một hoặc hai khung hình dữ liệu ảnh trên heap sẽ đẩy mọi thứ khác ra khỏi nó.
Cách thoát là các bộ đệm ảnh hầu như không nằm trên Python heap. Chúng nằm trong vùng RAM chuyên dụng mà Vision Sensors đã giới thiệu là bộ đệm khung hình -- cùng bộ nhớ mà DMA camera ghi các khung hình được chụp vào và bản xem trước IDE đọc các khung hình hoàn chỉnh ra khỏi. Hầu hết các thao tác trên Image sửa đổi nguồn của chúng tại chỗ: thuật toán đọc các điểm ảnh, quyết định, ghi các giá trị mới trở lại, và không có ảnh kết quả riêng biệt nào được cấp phát. Các thao tác tạo ra kết quả riêng biệt -- chuyển đổi định dạng và một số thao tác khác -- có thể được yêu cầu đặt kết quả đó vào bộ đệm khung hình thông qua đối số từ khóa copy_to_fb. copy_to_fb=True thực hiện hai việc cùng lúc: đặt ảnh kết quả vào bộ đệm khung hình thay vì heap (tránh áp lực heap) và làm cho kết quả trở thành khung hình tiếp theo mà bản xem trước IDE sẽ hiển thị. Thêm copy_to_fb=True vào bước cuối cùng của một pipeline, xem kết quả xuất hiện trên màn hình và lặp từ đó là một trong những thành ngữ gỡ lỗi hữu ích nhất trong xử lý ảnh.
Với một lớp bọc giữ một bộ đệm có nhãn, bốn cách để tạo ra nó, hai cách nhìn vào các byte của nó, và một công tắc quyết định nơi các bộ đệm mới nằm, Image không còn là bí ẩn nữa. Các câu hỏi nền tảng còn lại -- cách đặt tên vị trí điểm ảnh, mỗi điểm ảnh thực sự chứa gì, cách giới hạn một thao tác vào một phần của ảnh -- được xây dựng trên nền tảng đó.