Chúng ta thường nghe thuật ngữ JPEG. Vậy JPEG thực sự là gì? Nó hoạt động như thế nào?
Tôi sẽ cố gắng cung cấp một cái nhìn tổng quan ngắn gọn về JPEG là gì và cách thức hoạt động của nó. Đây là bài viết dành cho người mới bắt đầu, vì vậy chúng ta sẽ không đi sâu vào chi tiết.
Đầu tiên, JPEG không phải là một định dạng tệp. Đây là một phương pháp nén.
Thuật ngữ JPEG là từ viết tắt của Joint Photographic Experts Group, nhóm đã tạo ra tiêu chuẩn này. Hầu hết thời gian bạn nói về tệp của mình ở định dạng JPEG, tức là bạn đang nói đến trình bao bọc JFIF (Định dạng trao đổi tệp JPEG).
Hãy bắt đầu từ khi hình ảnh bắt đầu tồn tại.
Bạn nhấp vào một bức ảnh bằng máy ảnh của mình.
Cảm biến của máy ảnh được phủ một mảng bộ lọc màu (CFA), thường là bộ lọc Bayer, bao gồm khảm ma trận 2x2 gồm các bộ lọc màu đỏ, xanh lá cây, xanh lam và (một lần nữa) xanh lá cây. Cảm biến ảnh màu xanh lá cây là các thành phần nhạy sáng và cảm biến màu đỏ và xanh lam là các thành phần nhạy sắc độ. Bayer sử dụng gấp đôi số thành phần màu xanh lá cây so với màu đỏ hoặc xanh lam để mô phỏng sinh lý học của mắt người. Từ CFA, chúng ta nhận được dữ liệu hình ảnh là một màu cho mỗi pixel. Điều này không phù hợp để nén JPEG. Dữ liệu hình ảnh này được tái tạo để tạo ra bộ ba màu RGB trên mỗi pixel ảnh.
Tệp hình ảnh thô thu được do đó là một bitmap (mảng 2D). Kích thước của nó rất lớn. Vì vậy, chúng ta cần nén tệp này và đó là lúc JPEG phát huy tác dụng. JPEG là một kỹ thuật nén có mất dữ liệu, nghĩa là nó sử dụng các phép tính gần đúng và loại bỏ dữ liệu một phần để nén nội dung. Do đó, nó không thể đảo ngược.
Nén JPEG dựa trên 2 quan sát sau:
Quan sát #1: Mắt người không nhìn thấy màu sắc (sắc độ) tốt bằng chúng ta nhìn thấy độ sáng (độ chói).
Quan sát #2: Mắt người không thể phân biệt được những thay đổi tần số cao trong cường độ hình ảnh.
Bước 1: Chuyển đổi không gian màu RGB sang YCbCr
Mỗi pixel trong hình ảnh của bạn được lưu trữ dưới dạng kết hợp cộng của các giá trị Đỏ, Xanh lam và Xanh lục. Mỗi giá trị này có thể nằm trong khoảng từ 0 đến 255. Mô hình màu này được gọi là mô hình RGB. Hãy xem xét một pixel có màu kaki. Nó sẽ được lưu trữ dưới dạng (240, 230, 140).
Hãy nhớ Quan sát số 1 - Độ sáng quan trọng hơn màu sắc đối với chất lượng nhận thức cuối cùng của hình ảnh. Vì vậy, chúng ta chuyển đổi từ không gian màu RGB sang không gian mà độ sáng bị giới hạn trong một kênh duy nhất. Không gian màu này được gọi là YCbCr.
Ở đây, Y là thành phần độ sáng và Cb, Cr là các thành phần sắc độ. Chúng lần lượt là sự khác biệt của màu xanh lam và đỏ. Giá trị của chúng sẽ nằm trong khoảng từ 0 đến 255.
Giá trị YCbCr có thể được tính trực tiếp từ RGB như sau:[1]
Y = 0.299 R + 0.587 G + 0.114 B
Cb = - 0.1687 R - 0.3313 G + 0.5 B + 128
Cr = 0.5 R - 0.4187 G - 0.0813 B + 128
Bước 2: Giảm mẫu
Vì sắc độ không quá quan trọng, chúng ta có thể hạ mẫu và giảm lượng màu (thành phần CbCr).
Nhìn chung, màu sắc giảm theo hệ số 2 theo cả hai hướng (dọc và ngang) - nghĩa là Y được lấy mẫu tại mỗi pixel, trong khi Cb và Cr được lấy mẫu tại mọi khối pixel 2x2. Bây giờ, cứ mỗi 4 pixel Y, sẽ chỉ tồn tại 1 pixel CbCr. Bạn sẽ không nhận thấy nhiều thay đổi trong hình ảnh, nhưng một lượng lớn kích thước tệp sẽ giảm đi.
Trong phần mềm chỉnh sửa hình ảnh, bạn thường được hỏi chất lượng bạn muốn lưu hình ảnh. Trên thực tế, đây là phần mềm hỏi bạn muốn hạ mẫu bao nhiêu trên hình ảnh.
Bước 3: Sử dụng Biến đổi Cosin Rời rạc (DCT)
Mỗi thành phần YCbCr trong ba thành phần được nén và mã hóa riêng sử dụng cùng một phương pháp được mô tả ở đây. Hiện tại, chỉ xem xét một trong những thành phần này. 2 thành phần còn lại được xử lý theo cùng một cách chính xác.
3 a. hình ảnh cơ sở
DCT là một phương pháp thể hiện một chuỗi hữu hạn các điểm dữ liệu theo tổng các hàm cosin dao động ở các tần số khác nhau.
Đối với nén, các hàm cosin được sử dụng thay vì các hàm sin vì có sự khác biệt cụ thể trong hành vi ranh giới của chúng. Một hàm như độ sáng của hình ảnh không cần phải lấy giá trị bằng không trên ranh giới như hàm sin. Vì vậy, rất khó để xấp xỉ một tín hiệu như vậy bằng tổ hợp tuyến tính của các hàm sin.
Xem hình ảnh sau[2]
Đây là 64 hình ảnh cơ sở, được xây dựng từ các hàm cosin ở các tần số khác nhau trên trục X và Y.
Hình ảnh cơ sở đầu tiên, tức là baseimg[0][0] sẽ có màu trắng hoàn toàn, đối với baseimg[0][1] đến baseimage[0][7], bạn có thể thấy tần số tăng dần theo trục x. đối với baseimg[1][0] đến baseimage[7][0], bạn có thể thấy tần số tăng dần theo trục y. baseimg[7][7] sẽ được kiểm tra hoàn toàn.
3 b. ảnh phụ
Toàn bộ hình ảnh chúng ta muốn nén được chia thành các ảnh phụ, mỗi ảnh gồm 8x8 pixel. Chúng ta hãy gọi mỗi ảnh là một ảnh phụ.
Ảnh phụ này có thể được hình dung như một ma trận 8x8. Chúng ta sẽ nén toàn bộ hình ảnh theo từng ảnh phụ một.
Hãy xem xét một ví dụ. Các giá trị của thành phần đang xem xét được đưa ra trong ma trận sau:
Vì chúng ta sẽ sử dụng DCT và sóng cosin đi từ 1 đến -1, chúng ta sẽ căn giữa các giá trị của mình quanh số không. Điều này có nghĩa là chúng ta dịch chuyển phạm vi từ [0..255] đến [-128..128]. Vì vậy, chúng ta trừ 128 khỏi mọi giá trị.
Bây giờ, hình ảnh phụ của chúng ta được dịch chuyển thành:
Bây giờ, chúng ta có 2 thứ trong tay:
1. Ảnh phụ 8x8 cần nén
2. 64 ảnh cơ sở
Nhiệm vụ của chúng ta ở đây là biến đổi ảnh phụ thành tổ hợp tuyến tính của 64 ảnh cơ sở này.
Ảnh phụ có thể được chuyển đổi thành biểu diễn miền tần số này bằng cách sử dụng Biến đổi Cosin rời rạc loại II hai chiều đã chuẩn hóa (DCT).
Chúng ta có thể coi ảnh phụ bao gồm một tập hợp có trọng số của 64 ảnh cơ sở này được hợp nhất với nhau.
Do đó, ảnh phụ = C1f1 + C2f2 + C3f3 + … C64f64
Trong đó Ci là một hằng số và fi là các ảnh cơ sở.
Chúng ta có thể tìm từng hệ số này (Ci) bằng cách sử dụng DCT (loại II). Nếu bạn muốn biết thêm về cách thức hoạt động của DCT, vui lòng tham khảo tại đây.
Tôi sẽ sử dụng mã python sau để tìm ma trận DCT của ảnh phụ của mình:
import numpy as np
from scipy.fftpack import dct
def dct2D(x):
tmp = dct(x, type=2 ,norm='ortho').transpose()
return dct(tmp, type=2 ,norm='ortho').transpose()
print dct2D([
[-64., -68., -71., -72., -80., -81., -81., -85.],
[-67., -70., -75., -76., -80., -79., -76., -75.],
[-61., -68., -75., -75., -79., -81., -80., -74.],
[-60., -67., -65., -65., -66., -63., -63., -64.],
[-57., -67., -58., -65., -59., -54., -40., -40.],
[-45., -36., -26., -23., -21., -17., -18., -13.],
[-33., -20., -20., -4., -6., 2., 0., 0.],
[-21., -10., -3., 6., 9., 14., 13., 9.]
])
Ở đây, tôi đang sử dụng gói SciPy cung cấp hàm scipy.fftpack.dct để tính DCT của một mảng 1 chiều. Để tính DCT cho một mảng 2 chiều, trước tiên chúng ta áp dụng dct theo cột, chuyển vị, sau đó theo hàng, rồi hoàn tác chuyển vị.
Và, tôi nhận được:
Đây là bảng hệ số 8x8, biểu diễn sự đóng góp của từng hình ảnh cơ sở vào hình ảnh con.
Bước 4: Lượng tử hóa
Bây giờ chúng ta sẽ lượng tử hóa bảng hệ số thu được bằng DCT. Đây là phần thực sự mất mát của quy trình.
Trong bảng hệ số thu được thông qua DCT, các ô trên cùng bên trái tham chiếu đến phần tần số thấp và các ô dưới cùng bên phải tham chiếu đến phần tần số cao.
Chúng ta biết rằng phần tần số cao có thể bị loại bỏ mà không làm mất nhiều diện mạo của hình ảnh. (Nhớ lại Quan sát số 2)
Vì vậy, bây giờ chúng ta chuẩn bị một bảng lượng tử hóa 8x8. Bảng này sẽ có các giá trị rất nhỏ ở phần trên cùng bên trái và các giá trị rất cao về phía phần dưới cùng bên phải. Mỗi giá trị trong bảng hệ số được chia cho giá trị tương ứng trong bảng lượng tử hóa và làm tròn đến số nguyên gần nhất. Bây giờ, do có số chia cao ở phần dưới cùng bên phải, các giá trị chia ở đây trở thành số không - do đó loại bỏ dữ liệu tần số cao.
Bảng lượng tử hóa này tùy thuộc vào bộ mã hóa và do đó, bảng được lưu trong tiêu đề hình ảnh để có thể giải mã hình ảnh sau này.
Dưới đây là bảng lượng tử hóa JPEG chuẩn:
Và đây là hình ảnh phụ của chúng ta sau khi lượng tử hóa. (sau khi chia mỗi giá trị trong bảng hệ số của chúng ta với giá trị tương ứng trong bảng lượng tử hóa)
Lưu ý rằng trong bảng đầu ra được lượng tử hóa, tất cả các giá trị ngoại trừ khối 3x3 trên cùng bên trái đều là số không. Đây là dữ liệu tần số cao mà chúng tôi đã loại bỏ. JPEG nổi tiếng là chỉ với 9 giá trị này, chúng ta có thể lấy lại gần như cùng một hình ảnh.
Bước 5: Mã hóa
Bây giờ chúng ta đã có đầu ra được nén dưới dạng một mảng 2D. Chúng ta cũng biết rằng rất nhiều trong số chúng là số không. Vì vậy, chúng ta sẽ tìm một cách tốt hơn để lưu trữ hình ảnh phụ thay vì lưu trữ nó dưới dạng một mảng 2D.
Chúng ta sẽ lưu trữ các giá trị theo thứ tự ngoằn ngoèo. Vì vậy, dữ liệu sẽ là:
-24, -23, 19, 5, 4, 0, 0, 1, 0, 0, 0, 0, 1 theo sau là 53 số không.
Dữ liệu của mẫu này có thể dễ dàng được nén bằng thuật toán Run-Length Encoding (RLE). Đầu ra cuối cùng được mã hóa bằng cách kết hợp RLE và mã hóa Huffman.
Bước 6: Thêm Tiêu đề
Mặc dù các tệp JPEG/JFIF không có tiêu đề được định nghĩa chính thức, nhưng chúng thường chứa các mục sau:
* Dấu hiệu JPEG Start Of Image (SOI) (0xFFD8)
* Dấu hiệu ứng dụng
* Chiều rộng tính bằng pixel
* Chiều cao tính bằng pixel
* Số lượng thành phần (ví dụ: 3 cho RGB)
Tệp nén của bạn đã sẵn sàng !!!
Để giải nén tệp nén JPEG, chỉ cần thực hiện ngược lại.
Sử dụng Discrete Cosine Transform-III để đảo ngược DCT-II.
Tiếp theo, trong JPEG 102, chúng ta sẽ viết từ đầu một bộ mã hóa JPEG bằng C, nhưng đó là chuyện của ngày khác.