5.4. Đọc và ghi điểm ảnh

Hầu hết các thao tác trên một ảnh đều ẩn công việc xử lý từng điểm ảnh bên trong một lệnh gọi phương thức duy nhất, nơi các vòng lặp chạm vào từng điểm ảnh diễn ra ở tốc độ gốc. Tuy nhiên, có những trường hợp mã ứng dụng cần truy cập trực tiếp vào một điểm ảnh cụ thể: để đọc giá trị tại một vị trí cụ thể, để ghi giá trị mới vào đó, để lấy mẫu tại một điểm cho bước hiệu chỉnh, hoặc để gỡ lỗi một giá trị tại vị trí đã biết. Module image cung cấp mức độ truy cập đó thông qua hai dạng địa chỉ, mỗi dạng phù hợp với một cách tư duy khác nhau về vị trí của điểm ảnh.

5.4.1. Địa chỉ theo tọa độ

Dạng tự nhiên nhất là dạng mà phần Tọa độ đã phát triển từ vựng cho: xác định điểm ảnh bằng tọa độ Descartes (x, y). get_pixel() nhận (x, y) và trả về giá trị tại vị trí đó; set_pixel() nhận cùng (x, y) cùng với một giá trị và ghi vào đó.

Những gì các lệnh gọi này trả về hoặc chấp nhận phụ thuộc vào định dạng của ảnh. Ảnh thang xám, ảnh nhị phân và ảnh Bayer mang một giá trị duy nhất trên mỗi điểm ảnh -- độ sáng cho thang xám, 0 hoặc 1 cho nhị phân, một mẫu kênh màu đơn cho Bayer -- vì vậy get_pixel() trả về một số nguyên duy nhất. RGB565 mang ba kênh màu đóng gói trong 16 bit, và get_pixel giải nén chúng thành bộ (r, g, b) theo mặc định, với mỗi kênh được ánh xạ vào phạm vi 0 -- 255.

Hành vi mặc định có thể được thay đổi ở cả hai đầu. Truyền rgbtuple=False vào get_pixel trên ảnh RGB565 sẽ quay lại từ đóng gói 16 bit thô -- cùng dạng mà chỉ số tuyến tính trả về, và là dạng hiệu quả khi ứng dụng sẽ ghi lại giá trị đóng gói đó trực tiếp. Truyền rgbtuple=True trên ảnh một kênh thực hiện điều ngược lại: giá trị được lưu trữ được chuyển đổi thành bộ RGB888 trước khi trả về, với ảnh Bayer trải qua bước debayer tại chỗ. Tham số này tồn tại để mã gọi có thể yêu cầu điểm ảnh trong một không gian màu đồng nhất bất kể ảnh bên dưới lưu trữ như thế nào.

Ảnh nén -- JPEG và PNG -- không được hỗ trợ bởi get_pixel hoặc set_pixel. Các byte của chúng không đại diện cho điểm ảnh tại các vị trí đã biết, và các phương thức này sẽ báo lỗi thay vì trả về giá trị không có ý nghĩa.

Trong thực tế, các mẫu trông như sau:

v = img.get_pixel(40, 30)            # grayscale: int 0..255
img.set_pixel(40, 30, 255)           # write white

r, g, b = img.get_pixel(40, 30)      # RGB565: defaults to (r, g, b) tuple
img.set_pixel(40, 30, (255, 0, 0))   # write red

Nếu (x, y) được yêu cầu nằm ngoài ảnh, get_pixel trả về Noneset_pixel không làm gì. Điều đó được thiết kế một cách khoan dung: nhiều thuật toán đi dọc gần các cạnh của ảnh và đôi khi truy cập các vị trí nằm ngoài phạm vi, và một thao tác im lặng không gây ra lỗi ít gây rối hơn so với một ngoại lệ mỗi khi điều đó xảy ra.

5.4.2. Địa chỉ theo chỉ số tuyến tính

Dạng còn lại là địa chỉ điểm ảnh theo vị trí của chúng trong bộ đệm bên dưới. Hãy nhớ lại bố cục của bộ đệm: các điểm ảnh được lưu trữ theo từng hàng, tất cả các điểm ảnh của hàng trên cùng trước, sau đó là tất cả các điểm ảnh của hàng tiếp theo, và cứ tiếp tục như vậy xuống đến hàng dưới cùng. Cách sắp xếp đó có nghĩa là mỗi điểm ảnh có một chỉ số nguyên duy nhất đếm từ 0 ở góc trên bên trái và tăng dần theo từng hàng. Điểm ảnh tại tọa độ (x, y) có chỉ số tuyến tính y * width + x.

A 4-by-3 grid of cells. Each cell carries a large linear index from 0 in the top-left through 11 in the bottom-right, plus a small (x, y) tuple underneath. Columns are labelled x equals 0, 1, 2, 3 across the top; rows are labelled y equals 0, 1, 2 along the left edge. A caption underneath gives the relation: linear index equals y times width plus x.

Các điểm ảnh được địa chỉ cả bằng tọa độ Descartes (x, y) và bằng chỉ số tuyến tính đi dọc theo bộ đệm từng hàng, từ trái sang phải.

Module image cung cấp chỉ số đó thông qua ký hiệu chỉ số Python thông thường: img[i] đọc điểm ảnh tại chỉ số tuyến tính i, img[i] = value ghi một giá trị. Dạng chỉ số trả về là giá trị thô được lưu trữ theo định dạng, không phải bộ giải nén mà get_pixel() trả về theo mặc định. Sự phân biệt đó quan trọng vì định dạng được chọn trước đó quyết định giá trị thô trông như thế nào:

  • Các điểm ảnh thang xám và Bayer trả về dưới dạng số nguyên 8 bit.

  • Các điểm ảnh RGB565 và YUV422 trả về dưới dạng số nguyên 16 bit -- từ đã đóng gói.

  • Các điểm ảnh nhị phân trả về là 0 hoặc 1.

  • Các điểm ảnh JPEG và PNG trả về dưới dạng số nguyên 8 bit, một byte mỗi lần từ luồng nén. Các giá trị đó mờ đục -- chúng là các mảnh của một mã hóa nén chứ không phải điểm ảnh theo bất kỳ nghĩa thông thường nào.

Dạng chỉ số phù hợp với mã đang nghĩ theo các độ lệch bộ đệm: một vòng lặp đi qua mỗi điểm ảnh một lần, một thuật toán cần nhảy theo từng hàng một lần, hoặc một đoạn mã dịch giữa các bố cục bộ đệm. Mã đang nghĩ theo tọa độ x và y phục vụ tốt hơn bởi get_pixelset_pixel; hai dạng này địa chỉ cùng các điểm ảnh thông qua các mô hình tư duy khác nhau.

Image cũng có thể lặp được. for v in img: đi qua bộ đệm theo cùng thứ tự hàng chính, trả về các giá trị thô từng điểm ảnh một, và len(img) là số lượng điểm ảnh đối với các định dạng không nén hoặc số byte đối với các luồng nén.

5.4.3. Tại sao Python từng điểm ảnh là con đường chậm

Một lưu ý thực tế đáng được thành thật. Đi qua một ảnh từng điểm ảnh một từ Python là chậm. Một ảnh thang xám 320 × 240 chứa 76.800 điểm ảnh; gọi get_pixel() trên từng điểm ảnh trong vòng lặp for chạy hàng triệu lệnh bytecode MicroPython để thực hiện công việc mà một phương thức gốc tương đương có thể hoàn thành trong vài trăm microsecond. Đó không phải là một hệ số nhỏ. Đó là sự khác biệt giữa một tập lệnh xử lý khung hình theo thời gian thực và một tập lệnh chạy chậm dưới tốc độ khung hình của camera.

Hầu hết mọi phương thức trên bề mặt Image đều tồn tại vì có một phiên bản gốc nhanh hơn của một mẫu từng điểm ảnh phổ biến. Một vòng lặp cộng hai ảnh lại với nhau trở thành một lệnh gọi gốc đơn. Một vòng lặp làm mịn mỗi điểm ảnh bằng cách lấy trung bình với các điểm lân cận của nó trở thành một vòng lặp khác. Một vòng lặp phân loại mỗi điểm ảnh theo một ngưỡng trở thành vòng lặp thứ ba. Công việc của ứng dụng, trong hầu hết các trường hợp, là nhận ra phương thức toàn ảnh nào phù hợp với công việc mà vòng lặp sẽ thực hiện, và sử dụng phương thức đó thay vì tự viết vòng lặp.

Đọc và ghi ở mức điểm ảnh vẫn là công cụ phù hợp khi không có gì khác phù hợp -- vá một phép đo cụ thể trở lại bộ đệm, lấy mẫu tại một vị trí cho bước hiệu chỉnh, gỡ lỗi một giá trị tại vị trí đã biết. Điểm mấu chốt là chúng là con đường chậm, được sử dụng khi các phương thức toàn ảnh không có dạng mà ứng dụng cần, chứ không phải là cách mặc định để thao tác trên các điểm ảnh.