5.17. Danh mục các kernel chuẩn¶
Xử lý ảnh cổ điển đã tích lũy một danh mục khá phong phú các mẫu trọng số kernel xuất hiện đi xuất hiện lại -- bộ phát hiện cạnh, bộ làm nét, bộ tạo hiệu ứng nổi, bộ làm mượt, bộ làm mờ chuyển động -- và tất cả chúng đều chạy thông qua morph(). Mỗi kernel ngắn gọn, thực hiện một việc duy nhất, và hầu hết đều dễ đọc khi bạn đã hiểu logic cơ bản của các trọng số.
Các kernel bên dưới đều có kích thước 3x3 trừ khi có ghi chú khác, vì vậy tất cả đều dùng size=1 trong lời gọi. Cấu trúc trọng số của mỗi kernel được mô tả kèm theo, vì việc đọc các trọng số chính là cách xây dựng trực giác để hiểu tại sao kernel này tạo hiệu ứng nổi trong khi kernel khác lại làm nét ảnh.
5.17.1. Kernel đồng nhất¶
Kernel đơn giản nhất có thể là kernel đồng nhất -- một ở tâm, không ở mọi nơi khác:
identity = [0, 0, 0,
0, 1, 0,
0, 0, 0]
img.morph(1, identity)
Mỗi điểm ảnh đầu ra lấy giá trị từ tâm của vùng lân cận, tức là điểm ảnh đầu vào ở cùng vị trí. Ảnh đi qua mà không thay đổi. Kernel đồng nhất không có ứng dụng thực tế như một bộ lọc, nhưng đây là đường cơ sở hữu ích để hiểu mọi kernel khác: bất kỳ kernel không đồng nhất nào cũng là kernel đồng nhất cộng với một vài sửa đổi.
Một kernel có trọng số tâm lớn kèm các trọng số âm nhỏ xung quanh sẽ trừ đi vùng xung quanh khỏi tâm. Một kernel có trọng số tâm bằng không sẽ bỏ qua chính điểm ảnh đó và chỉ phản hồi với sự khác biệt giữa các điểm ảnh lân cận. Đọc kernel theo cách này -- trọng số tâm làm gì với điểm ảnh, các trọng số xung quanh cộng thêm hay bớt đi điều gì -- là cách nhanh nhất để dự đoán hiệu ứng của nó.
5.17.2. Phát hiện cạnh¶
Các kernel phát hiện cạnh phản hồi mạnh với các vị trí mà độ sáng thay đổi nhanh chóng theo một hướng nhất định, và tạo ra đầu ra gần bằng không ở những vùng có độ sáng đồng đều. Đây là nhóm có tổng trọng số bằng không: một vùng phẳng (mọi điểm ảnh có cùng giá trị) tạo ra đầu ra bằng không, vì mỗi trọng số dương bị triệt tiêu chính xác bởi một trọng số âm có cùng độ lớn.
Sobel-x là ví dụ điển hình. Nó phát hiện các cạnh dọc (các chuyển tiếp độ sáng trái/phải):
sobel_x = [-1, 0, 1,
-2, 0, 2,
-1, 0, 1]
img.morph(1, sobel_x, mul=0.25, add=128)
Kernel Sobel-y tương ứng là cùng mẫu nhưng xoay 90 độ; nó phát hiện các cạnh ngang (các chuyển tiếp độ sáng lên/xuống):
sobel_y = [-1, -2, -1,
0, 0, 0,
1, 2, 1]
Hàng giữa của Sobel-x có các trọng số -2 và 2 thay vì -1 và 1. Trọng số bổ sung trên hàng giữa cung cấp cho kernel một bộ làm mượt nhỏ tích hợp theo hướng dọc theo cạnh, giúp nó chống nhiễu tốt hơn so với toán tử Prewitt đơn giản hơn không có các độ lớn bổ sung đó:
prewitt_x = [-1, 0, 1,
-1, 0, 1,
-1, 0, 1]
prewitt_y = [-1, -1, -1,
0, 0, 0,
1, 1, 1]
Prewitt cân bằng đều mọi hàng, nên phản hồi của nó sắc nét hơn một chút so với Sobel, nhưng đổi lại nhạy cảm hơn với nhiễu đơn điểm ảnh (chi phí chạy kernel là giống nhau -- phép tích chập thực hiện cùng khối lượng công việc dù các trọng số là gì). Trên một ảnh sạch với các cạnh rõ ràng, đây là một thay thế hoàn toàn có thể dùng được cho Sobel.
Scharr đi theo hướng ngược lại. Các trọng số của nó lớn hơn và được điều chỉnh để phát hiện chính xác hướng cạnh ở các góc tinh tế hơn:
scharr_x = [-3, 0, 3,
-10, 0, 10,
-3, 0, 3]
img.morph(1, scharr_x, mul=0.0625, add=128)
Hệ số chia mul=0.0625 (1/16) đưa đầu ra trở lại trong phạm vi 0 -- 255 sau khi tính tổng tích lớn hơn. Scharr là câu trả lời đúng khi ứng dụng cần phản hồi gradient trung thực nhất về mặt hình học và sẵn sàng trả thêm chi phí tính toán cho điều đó.
5.17.3. Laplacian¶
Một kernel Laplacian phản hồi với các cạnh theo bất kỳ hướng nào cùng một lúc. Trong khi mỗi kernel Sobel phát hiện sự thay đổi độ sáng dọc theo một trục, mẫu trọng số đối xứng của Laplacian phản hồi theo cùng một cách bất kể hướng cạnh là gì:
laplacian_4 = [ 0, -1, 0,
-1, 4, -1,
0, -1, 0]
img.morph(1, laplacian_4, add=128)
Cấu trúc: trọng số tâm 4, bốn điểm lân cận ngang/dọc có trọng số -1, bốn đường chéo có trọng số bằng không. Kernel có tổng bằng không, nên các vùng phẳng tạo ra đầu ra bằng không. Khi độ sáng thay đổi, giá trị tâm khác với trung bình của bốn điểm lân cận chính, và đầu ra là độ lớn của sự khác biệt đó.
Biến thể kết nối-8 bao gồm các điểm lân cận đường chéo:
laplacian_8 = [-1, -1, -1,
-1, 8, -1,
-1, -1, -1]
Mỗi kernel phát hiện những thứ hơi khác nhau. Phiên bản kết nối-4 tạo ra đầu ra sạch hơn trên các cạnh ngang và dọc; phiên bản kết nối-8 đẳng hướng hơn -- nó phản hồi đều nhau theo mọi hướng -- nhưng tạo ra đầu ra nhiễu hơn một chút. Kernel kết nối-8 cũng lưu hành dưới tên outline, theo ứng dụng trực quan hóa các cạnh của nó.
5.17.5. Hiệu ứng nổi¶
Kernel hiệu ứng nổi tạo ra hiệu ứng chiếu sáng từ một phía như trong các trình chỉnh sửa ảnh cổ điển. Kết quả trông như thể ảnh được đẩy nổi thành một phù điêu rồi chiếu sáng từ một góc:
emboss = [-2, -1, 0,
-1, 1, 1,
0, 1, 2]
img.morph(1, emboss, add=128)
Mấu chốt là sự bất đối xứng qua đường chéo. Góc trên-trái có trọng số âm lớn nhất, góc dưới-phải có trọng số dương lớn nhất, và đường chéo từ góc này sang góc kia chạy từ âm qua một đến dương. Tại mỗi điểm ảnh, kernel về cơ bản tính "độ sáng ở phía dưới-phải của tôi trừ độ sáng ở phía trên-trái của tôi," dương ở nơi ảnh sáng hơn theo hướng đó và âm ở nơi tối hơn. Cộng thêm 128 căn giữa lại đầu ra có dấu về màu xám trung bình để hiệu ứng hiển thị rõ.
Xoay sự bất đối xứng qua đường chéo kia tạo hiệu ứng nổi từ hướng ngược lại:
emboss_alt = [ 0, 1, 2,
-1, 1, 1,
-2, -1, 0]
img.morph(1, emboss_alt, add=128)
Hai hướng hiệu ứng nổi hữu ích khi kết hợp -- trừ một cái khỏi cái kia, hoặc chạy mỗi cái trên cùng một ảnh và so sánh các phản hồi -- khi ứng dụng cần phát hiện hướng.
5.17.6. Làm mượt¶
Các kernel làm mượt là nhóm có tổng trọng số bằng một (và tất cả đều không âm). Một vùng phẳng qua kernel như vậy tạo ra cùng độ sáng phẳng, vì kernel tính trung bình các giá trị điểm ảnh thay vì khuếch đại sự khác biệt của chúng.
Đơn giản nhất là làm mờ hộp, đây chính xác là những gì mean() tính toán:
box_blur = [1, 1, 1,
1, 1, 1,
1, 1, 1]
img.morph(1, box_blur)
Kernel có tổng bằng 9, vì vậy phép chia tự động theo tổng kernel biến tổng tích thành giá trị trung bình thực sự trên chín điểm ảnh lân cận. Trên thực tế mean() là cách tốt hơn để chạy kernel này -- nó tạo ra cùng đầu ra nhanh hơn, thông qua một đường dẫn được tối ưu hóa cho việc tính trung bình và không gì khác, trong khi morph chạy cỗ máy tích chập tổng quát. Làm mờ hộp nằm trong danh mục vì nó là đường cơ sở đúng để hiểu mọi kernel làm mượt khác.
Xấp xỉ 3x3 của trọng số Gaussian cho tâm và các điểm lân cận chính trọng số cao hơn các góc:
gaussian = [1, 2, 1,
2, 4, 2,
1, 2, 1]
img.morph(1, gaussian)
Các trọng số là hàng tam giác Pascal 1, 2, 1 nhân ngoài với chính nó. Trọng số tâm 4 là lớn nhất vì điểm ảnh tâm đóng góp nhiều nhất vào đầu ra của chính nó; các góc là 1 vì chúng xa tâm nhất. Kernel có tổng bằng 16, và phép chia tự động theo tổng kernel xử lý việc chuẩn hóa -- không cần đối số mul. Dạng 3x3 là xấp xỉ thô của Gaussian thực và không thể phân biệt với gaussian() ở size=1; dạng morph chủ yếu hữu ích khi ứng dụng muốn kết hợp làm mượt với một thao tác khác trong cùng một lượt.
5.17.7. Làm mờ chuyển động¶
Kernel làm mờ chuyển động tính trung bình các điểm ảnh theo một hướng, để nguyên hướng vuông góc không bị mờ. Trường hợp đơn giản nhất là theo chiều ngang:
motion_h = [0, 0, 0,
1, 1, 1,
0, 0, 0]
img.morph(1, motion_h)
Hàng giữa tính trung bình ba điểm ảnh dọc theo trục ngang; các hàng trên và dưới bằng không. Kernel có tổng bằng 3, vì vậy phép chia tự động theo tổng kernel tạo ra giá trị trung bình thực sự của ba điểm ảnh mà không cần mul. Đầu ra là bản sao bị nhòe theo chiều ngang của đầu vào -- hiệu ứng mà camera ghi lại khi đối tượng đang chuyển động ngang trong thời gian phơi sáng. Làm mờ chuyển động theo chiều dọc là cùng mẫu nhưng xoay:
motion_v = [0, 1, 0,
0, 1, 0,
0, 1, 0]
Làm mờ chuyển động theo đường chéo dùng đường chéo chính:
motion_diag = [1, 0, 0,
0, 1, 0,
0, 0, 1]
img.morph(1, motion_diag)
Các kernel làm mờ chuyển động hữu ích cả như một hiệu ứng (cố ý làm mờ một khung hình vì mục đích thị giác) và như một mẫu thử nghiệm cho các thuật toán cần chống lại các hiện tượng do chuyển động gây ra (chạy thuật toán trên đầu vào bị làm mờ chuyển động và kiểm tra xem nó vẫn tạo ra câu trả lời đúng).
5.17.8. Đọc kernel nhanh¶
Một vài quy tắc ngón tay cái giúp đọc các kernel mới dễ dàng hơn:
Tổng bằng một với trọng số không âm ⇒ làm mượt (bảo toàn độ sáng trung bình).
Tổng bằng không với cả trọng số dương và âm ⇒ phản hồi cạnh (bằng không trên các vùng phẳng).
Tổng bằng một với tâm dương lớn và vùng xung quanh âm nhỏ ⇒ làm nét (đồng nhất cộng phản hồi cạnh).
Bất đối xứng qua một đường chéo với tổng bằng một ⇒ hiệu ứng nổi (làm nổi bật một phía của mỗi chuyển tiếp độ sáng).
Tập trung dọc theo một trục với tổng bằng một ⇒ làm mờ định hướng.
Kernel khớp với quy tắc đầu tiên trong số này thường là phỏng đoán đúng về những gì nó làm. Hầu hết các kernel hữu ích đều có thể nhận ra chỉ từ bố cục mẫu trọng số của chúng.
Khi không có kernel chuẩn nào làm được những gì ứng dụng muốn, bước tiếp theo là điều chỉnh thủ công một kernel. Sự kết hợp của các quy tắc trên và các điều khiển mul / add bao gồm gần như mọi lượt tuyến tính mà một pipeline thị giác máy cổ điển từng cần; từ đó chỉ là vấn đề thử nghiệm các trọng số, quan sát đầu ra, và lặp lại.