Hướng dẫn python jpeg - jpeg trăn

Nguồn

Hướng dẫn python jpeg - jpeg trăn

Hiểu và giải mã hình ảnh JPEG bằng Python

Ngày 14 tháng 7 năm 2020

Chào mọi người! Hôm nay chúng ta sẽ hiểu thuật toán nén JPEG. Một điều mà rất nhiều người không biết là JPEG không phải là một định dạng mà là một thuật toán. Các hình ảnh JPEG bạn thấy chủ yếu ở định dạng JFIF (định dạng trao đổi tệp JPEG) sử dụng bên trong thuật toán nén JPEG. Đến cuối bài viết này, bạn sẽ hiểu rõ hơn về cách thuật toán JPEG nén dữ liệu và cách bạn có thể viết một số mã Python tùy chỉnh để giải nén nó. Chúng tôi sẽ không bao gồm tất cả các sắc thái của định dạng JPEG (như quét tiến triển) mà chỉ là định dạng cơ bản cơ bản trong khi viết trình giải mã của chúng tôi.

Giới thiệu

Tại sao viết một bài viết khác trên JPEG khi đã có hàng trăm bài viết trên Internet? Chà, thông thường khi bạn đọc các bài viết trên JPEG, tác giả chỉ cung cấp cho bạn chi tiết về định dạng trông như thế nào. Bạn không thực hiện bất kỳ mã nào để thực hiện giải nén và giải mã thực tế. Ngay cả khi bạn viết mã, nó vẫn ở C/C ++ và không thể truy cập được với nhiều nhóm người. Tôi có kế hoạch thay đổi điều đó bằng cách chỉ cho bạn cách trình giải mã JPEG cơ bản hoạt động bằng Python 3. Tôi sẽ dựa trên bộ giải mã của mình trên mã được cấp phép MIT này nhưng sẽ sửa đổi rất nhiều để tăng khả năng đọc và dễ hiểu. Bạn có thể tìm thấy mã sửa đổi cho bài viết này trên repo GitHub của tôi.

Các phần khác nhau của JPEG

Hãy bắt đầu với hình ảnh đẹp này của Ange Albertini. Nó liệt kê tất cả các phần khác nhau của tệp JPEG đơn giản. Hãy xem nó. Chúng tôi sẽ khám phá từng phân khúc. Bạn có thể phải tham khảo hình ảnh này khá nhiều lần trong khi đọc hướng dẫn này.

Hướng dẫn python jpeg - jpeg trăn

Ở cấp độ rất cơ bản, hầu hết mọi tệp nhị phân đều chứa một vài điểm đánh dấu (hoặc tiêu đề). Bạn có thể nghĩ về những điểm đánh dấu này như là giống như dấu trang. Chúng rất quan trọng để hiểu được một tệp và được sử dụng bởi các chương trình như

10 10 10 10 10 10 10
9 (trên MAC/Linux) để cho chúng tôi biết chi tiết về một tệp. Các điểm đánh dấu này xác định nơi lưu trữ một số thông tin cụ thể trong một tệp. Hầu hết các điểm đánh dấu được theo sau bởi thông tin
7 10
0 cho phân đoạn đánh dấu cụ thể. Điều này cho chúng ta biết phân khúc cụ thể đó là bao lâu.

Tệp bắt đầu & kết thúc tập tin

Điểm đánh dấu đầu tiên mà chúng tôi quan tâm là

7 10
1. Nó cho chúng ta biết rằng đây là sự khởi đầu của hình ảnh. Nếu chúng ta không thấy nó, chúng ta có thể cho rằng đây là một số tệp khác. Một dấu hiệu quan trọng khác không kém là
7 10
2. Nó cho chúng tôi biết rằng chúng tôi đã đạt đến cuối tệp hình ảnh. Mỗi điểm đánh dấu, ngoại trừ
7 10
3 đến
7 10
4 và
7 10
5, ngay lập tức được theo sau bởi một bộ xác định độ dài sẽ cung cấp cho bạn độ dài của phân đoạn đánh dấu đó. Đối với các điểm kết thúc tệp và tệp hình ảnh, chúng sẽ luôn dài hai byte mỗi byte.

Trong suốt hướng dẫn này, chúng tôi sẽ làm việc với hình ảnh này:

Hướng dẫn python jpeg - jpeg trăn

Hãy để viết một số mã để xác định các điểm đánh dấu này.

from struct import unpack


marker_mapping = {
    0xffd8: "Start of Image",
    0xffe0: "Application Default Header",
    0xffdb: "Quantization Table",
    0xffc0: "Start of Frame",
    0xffc4: "Define Huffman Table",
    0xffda: "Start of Scan",
    0xffd9: "End of Image"
}


class JPEG:
    def __init__(self, image_file):
        with open(image_file, 'rb') as f:
            self.img_data = f.read()
    
    def decode(self):
        data = self.img_data
        while(True):
            marker, = unpack(">H", data[0:2])
            print(marker_mapping.get(marker))
            if marker == 0xffd8:
                data = data[2:]
            elif marker == 0xffd9:
                return
            elif marker == 0xffda:
                data = data[-2:]
            else:
                lenchunk, = unpack(">H", data[2:4])
                data = data[2+lenchunk:]            
            if len(data)==0:
                break        

if __name__ == "__main__":
    img = JPEG('profile.jpg')
    img.decode()    

# OUTPUT:
# Start of Image
# Application Default Header
# Quantization Table
# Quantization Table
# Start of Frame
# Huffman Table
# Huffman Table
# Huffman Table
# Huffman Table
# Start of Scan
# End of Image

Chúng tôi đang sử dụng struct để giải nén các byte của dữ liệu hình ảnh.

7 10
6 bảo
7 10
7 coi dữ liệu là lớn và loại
7 10
8. Dữ liệu trong JPEG được lưu trữ ở định dạng lớn. Chỉ có dữ liệu EXIF ​​có thể ở Little-endian (mặc dù nó không phổ biến). Và một đoạn ngắn có kích thước 2 vì vậy chúng tôi cung cấp
7 10
9 hai byte từ
10 11 12 13 10 9
0 của chúng tôi. Bạn có thể tự hỏi làm thế nào chúng tôi biết đó là một
10 11 12 13 10 9
1. Chà, chúng tôi biết rằng các điểm đánh dấu trong JPEG là 4 chữ số hex:
10 11 12 13 10 9
2. Một chữ số hex bằng 4 bit (1⁄2 byte) vì vậy 4 chữ số hex sẽ bằng 2 byte và ngắn bằng 2 byte.

Phần bắt đầu của phần quét ngay lập tức được theo sau bởi dữ liệu quét hình ảnh và dữ liệu quét hình ảnh không có độ dài được chỉ định. Nó tiếp tục cho đến khi kết thúc của các điểm đánh dấu của tập tin được tìm thấy, vì vậy bây giờ chúng tôi đang tìm kiếm thủ công trên mạng đối với điểm đánh dấu EOF bất cứ khi nào chúng tôi thấy điểm đánh dấu SOC.

Bây giờ chúng ta có khung cơ bản tại chỗ, hãy để tiếp tục và tìm ra những gì phần còn lại của dữ liệu hình ảnh chứa. Chúng tôi sẽ trải qua một số lý thuyết cần thiết trước và sau đó xuống mã hóa.

Mã hóa một JPEG

Trước tiên tôi sẽ giải thích một số khái niệm cơ bản và các kỹ thuật mã hóa được JPEG sử dụng và sau đó giải mã sẽ tự nhiên làm theo điều đó như là một điều ngược lại của nó. Theo kinh nghiệm của tôi, trực tiếp cố gắng để có ý nghĩa giải mã là một chút khó khăn.

Mặc dù hình ảnh bên dưới won có ý nghĩa rất lớn đối với bạn ngay bây giờ, nhưng nó sẽ cung cấp cho bạn một số neo để giữ trong khi chúng tôi trải qua toàn bộ quá trình mã hóa/giải mã. Nó cho thấy các bước liên quan đến quy trình mã hóa JPEG: (SRC)

Hướng dẫn python jpeg - jpeg trăn

Không gian màu jpeg

Theo thông số kỹ thuật của JPEG (ISO/IEC 10918-6: 2013 (e), phần 6.1):

  • Hình ảnh được mã hóa chỉ có một thành phần được coi là dữ liệu thang độ xám trong đó 0 là màu đen và 255 có màu trắng.
  • Hình ảnh được mã hóa với ba thành phần được coi là dữ liệu RGB được mã hóa dưới dạng YCBCR trừ khi hình ảnh chứa phân đoạn đánh dấu App14 như được quy định trong 6.5.3, trong trường hợp mã hóa màu được coi là RGB hoặc YCBCR theo dữ liệu ứng dụng của dấu hiệu App14 bộ phận. Mối quan hệ giữa RGB và YCBCR được định nghĩa là quy định trong Rec. ITU-T T.871 | ISO/IEC 10918-5.
  • Hình ảnh được mã hóa với bốn thành phần được coi là CMYK, với (0,0,0,0) cho biết màu trắng trừ khi hình ảnh chứa một đoạn đánh dấu App14 như được chỉ định trong 6.5.3, trong trường hợp mã hóa màu được coi là CMYK hoặc YCCK Theo dữ liệu ứng dụng của phân đoạn đánh dấu App14. Mối quan hệ giữa CMYK và YCCK được định nghĩa là quy định trong khoản 7.CMYK, with (0,0,0,0) indicating white unless the image contains an APP14 marker segment as specified in 6.5.3, in which case the color encoding is considered either CMYK or YCCK according to the application data of the APP14 marker segment. The relationship between CMYK and YCCK is defined as specified in clause 7.

Hầu hết các triển khai thuật toán JPEG sử dụng độ chói và sắc tố (mã hóa YUV) thay vì RGB. Điều này cực kỳ hữu ích trong JPEG vì mắt người khá tệ khi thấy độ sáng tần số cao thay đổi trên một khu vực nhỏ để về cơ bản chúng ta có thể giảm lượng tần số và mắt người giành được có thể nói sự khác biệt. Kết quả? Một hình ảnh nén cao mà hầu như không có giảm chất lượng.

Giống như mỗi pixel trong không gian màu RGB được tạo thành từ 3 byte dữ liệu màu (đỏ, xanh lá cây, xanh dương), mỗi pixel trong yuv cũng sử dụng 3 byte nhưng những gì mỗi byte đại diện là hơi khác nhau. Thành phần Y xác định độ sáng của màu (còn được gọi là độ chói hoặc luma), trong khi các thành phần U và V xác định màu (còn được gọi là Chroma). Thành phần U đề cập đến lượng màu xanh và thành phần V đề cập đến lượng màu đỏ.

Định dạng màu này được phát minh khi tivi màu weren siêu phổ biến và các kỹ sư muốn sử dụng một định dạng mã hóa một hình ảnh cho cả tivi màu và đen và trắng. Yuv có thể được hiển thị một cách an toàn trên TV đen trắng nếu màu không có sẵn. Bạn có thể đọc thêm về lịch sử của nó trên Wikipedia.

Biến đổi cosin rời rạc & định lượng hóa

JPEG chuyển đổi một hình ảnh thành các khối của các khối pixel 8x8 (được gọi kết quả khối. Hãy để có được một sự hiểu biết cấp cao về tất cả các thuật ngữ này có nghĩa là gì.

Một biến đổi cosin rời rạc là một phương pháp để chuyển đổi các điểm dữ liệu riêng biệt thành sự kết hợp của sóng cosine. Có vẻ khá vô dụng khi dành thời gian chuyển đổi một hình ảnh thành một loạt các cosines nhưng thật hợp lý khi chúng ta hiểu DCT kết hợp với cách hoạt động của bước tiếp theo. Trong JPEG, DCT sẽ lấy một khối hình ảnh 8x8 và cho chúng tôi biết cách tái tạo nó bằng ma trận 8x8 của các hàm cosine. Đọc thêm tại đây)

Ma trận 8x8 của các hàm cosin trông như thế này:

Hướng dẫn python jpeg - jpeg trăn

Chúng tôi áp dụng DCT cho từng thành phần của một pixel riêng biệt. Đầu ra của việc áp dụng DCT là ma trận hệ số 8x8 cho chúng ta biết mỗi hàm cosin (trong số 64 chức năng) đóng góp bao nhiêu cho ma trận đầu vào 8x8. Ma trận hệ số của DCT thường chứa các giá trị lớn hơn ở góc trên cùng bên trái của ma trận hệ số và các giá trị nhỏ hơn ở góc dưới bên phải. Góc trên cùng bên trái biểu thị hàm cosin tần số thấp nhất và phía dưới bên phải biểu thị hàm cosin tần số cao nhất.

Điều này cho chúng ta biết là hầu hết các hình ảnh đều chứa một lượng lớn thông tin tần số thấp và một lượng nhỏ thông tin tần số cao. Nếu chúng ta biến các thành phần phía dưới bên phải của mỗi ma trận DCT thành 0, hình ảnh kết quả vẫn sẽ giống nhau bởi vì, như tôi đã đề cập, con người rất xấu trong việc quan sát các thay đổi tần số cao. Đây chính xác là những gì chúng tôi làm trong bước tiếp theo.

Tôi đã tìm thấy một video tuyệt vời về chủ đề này. Xem nó nếu DCT không có ý nghĩa quá nhiều.

Tất cả chúng ta đã nghe nói rằng JPEG là một thuật toán nén mất mát nhưng cho đến nay chúng ta đã làm bất cứ điều gì mất mát. Chúng tôi chỉ biến các khối 8x8 của các thành phần YUV thành các khối cosine 8x8 mà không bị mất thông tin. Phần mất đi trong bước lượng tử hóa.

Quant hóa là một quá trình trong đó chúng ta lấy một vài giá trị trong một phạm vi cụ thể và biến chúng thành một giá trị riêng biệt. Đối với trường hợp của chúng tôi, đây chỉ là một tên lạ mắt để chuyển đổi các hệ số tần số cao hơn trong ma trận đầu ra DCT thành 0. Khi bạn lưu hình ảnh bằng JPEG, hầu hết các chương trình chỉnh sửa hình ảnh đều hỏi bạn bạn cần bao nhiêu lần nén. Tỷ lệ bạn cung cấp ở đó ảnh hưởng đến số lượng lượng tử hóa được áp dụng và mất bao nhiêu thông tin tần số cao hơn. Đây là nơi áp dụng nén tổn thất. Khi bạn mất thông tin tần số cao, bạn có thể tạo lại hình ảnh gốc chính xác từ hình ảnh JPEG thu được.

Tùy thuộc vào mức độ nén cần thiết, một số ma trận lượng tử hóa phổ biến được sử dụng (thực tế thú vị: hầu hết các nhà cung cấp có bằng sáng chế về xây dựng bảng lượng tử hóa). Chúng tôi chia phần tử ma trận hệ số DCT khôn ngoan với ma trận lượng tử hóa, làm tròn kết quả thành một số nguyên và nhận ma trận lượng tử hóa. Hãy để đi qua một ví dụ.

Nếu bạn có ma trận DCT này:

Hướng dẫn python jpeg - jpeg trăn

Ma trận lượng tử hóa (phổ biến) này:

Hướng dẫn python jpeg - jpeg trăn

Sau đó, ma trận lượng tử kết quả sẽ là thế này:

Hướng dẫn python jpeg - jpeg trăn

Mặc dù con người có thể nhìn thấy thông tin tần số cao, nếu bạn xóa quá nhiều thông tin khỏi các khối hình ảnh 8x8, hình ảnh tổng thể sẽ trông có vẻ bị chặn. Trong ma trận số lượng này, giá trị đầu tiên được gọi là giá trị DC và phần còn lại của các giá trị là giá trị AC. Nếu chúng ta lấy các giá trị DC từ tất cả các ma trận được định lượng và tạo ra một hình ảnh mới, về cơ bản chúng ta sẽ kết thúc với một hình thu nhỏ với độ phân giải 1/8 của hình ảnh gốc.

Cũng cần lưu ý rằng vì chúng tôi áp dụng lượng tử hóa trong khi giải mã, chúng tôi sẽ phải đảm bảo màu sắc rơi trong phạm vi [0,255]. Nếu chúng nằm ngoài phạm vi này, chúng ta sẽ phải kẹp chúng theo cách thủ công vào phạm vi này.

Zig-zag

Sau khi định lượng hóa, JPEG sử dụng mã hóa ZIG-ZAG để chuyển đổi ma trận thành 1D (IMG SRC):

Hướng dẫn python jpeg - jpeg trăn

Hãy để tưởng tượng chúng ta có ma trận số lượng này:

Hướng dẫn python jpeg - jpeg trăn

Đầu ra của mã hóa Zig-Zag sẽ là:

[15 14 13 12 11 10 9 8 0  ...  0]

Mã hóa này được ưa thích vì hầu hết các thông tin tần số thấp (quan trọng nhất) được lưu trữ khi bắt đầu ma trận sau khi định lượng hóa và mã hóa Zig-ZAG lưu trữ tất cả những thứ đó vào đầu ma trận 1D. Điều này rất hữu ích cho việc nén xảy ra trong bước tiếp theo.

Độ dài chạy và mã hóa đồng bằng

Mã hóa độ dài chạy được sử dụng để nén dữ liệu lặp lại. Vào cuối mã hóa Zig-Zag, chúng tôi đã thấy hầu hết các mảng 1D được mã hóa Zig-Zag có rất nhiều 0 ở cuối. Mã hóa có độ dài chạy cho phép chúng tôi đòi lại tất cả không gian lãng phí và sử dụng ít byte hơn để thể hiện tất cả các 0 đó. Hãy tưởng tượng bạn có một số dữ liệu như thế này:

10 10 10 10 10 10 10

Mã hóa độ dài chạy sẽ chuyển đổi nó thành:

7 10

Chúng tôi đã có thể nén thành công 7 byte dữ liệu vào chỉ 2 byte.

Mã hóa Delta là một kỹ thuật được sử dụng để thể hiện byte so với byte trước nó. Nó dễ hiểu điều này với một ví dụ. Hãy nói rằng bạn có dữ liệu sau:

10 11 12 13 10 9

Bạn có thể sử dụng mã hóa Delta để lưu trữ nó như thế này:

10 1  2  3  0 -1

Trong JPEG, mọi giá trị DC trong ma trận hệ số DCT đều được mã hóa Delta so với giá trị DC trước nó. Điều này có nghĩa là nếu bạn thay đổi hệ số DCT đầu tiên của hình ảnh của mình, toàn bộ hình ảnh sẽ bị hỏng nhưng nếu bạn sửa đổi giá trị đầu tiên của ma trận DCT cuối cùng, chỉ có một phần rất nhỏ trong hình ảnh của bạn sẽ bị ảnh hưởng. Điều này rất hữu ích vì giá trị DC đầu tiên trong hình ảnh của bạn thường đa dạng nhất và bằng cách áp dụng mã hóa delta, chúng tôi mang phần còn lại của các giá trị DC gần 0 và kết quả là nén tốt hơn trong bước tiếp theo của mã hóa Huffman.

Huffman mã hóa

Mã hóa Huffman là một phương pháp nén thông tin không mất mát. Huffman đã từng tự hỏi mình, những gì mà số lượng nhỏ nhất mà tôi có thể sử dụng để lưu trữ một đoạn văn bản tùy ý? Định dạng mã hóa này là câu trả lời của anh ấy. Hãy tưởng tượng bạn phải lưu trữ văn bản này:

a b c d e

Trong một kịch bản bình thường, mỗi nhân vật sẽ chiếm 1 byte không gian:

a: 01100001
b: 01100010
c: 01100011
d: 01100100
e: 01100101

Điều này dựa trên ASCII để lập bản đồ nhị phân. Nhưng nếu chúng ta có thể nghĩ ra một bản đồ tùy chỉnh thì sao?

# Mapping

000: 01100001
001: 01100010
010: 01100011
100: 01100100
011: 01100101

Bây giờ chúng ta có thể lưu trữ cùng một văn bản bằng cách sử dụng ít bit hơn:

a: 000
b: 001
c: 010
d: 100
e: 011

Đây là tất cả tốt và tốt nhưng nếu chúng ta muốn chiếm ít không gian hơn? Điều gì sẽ xảy ra nếu chúng ta có thể làm điều gì đó như thế này:

[15 14 13 12 11 10 9 8 0  ...  0]
0

Mã hóa Huffman cho phép chúng tôi sử dụng loại ánh xạ có độ dài thay đổi này. Phải mất một số dữ liệu đầu vào, ánh xạ các ký tự thường xuyên nhất đến các mẫu bit nhỏ hơn và các ký tự thường xuyên nhất đến các mẫu bit lớn hơn và cuối cùng tổ chức ánh xạ vào một cây nhị phân. Trong một JPEG, chúng tôi lưu trữ thông tin DCT (biến đổi cosin rời rạc) bằng cách sử dụng mã hóa Huffman. Hãy nhớ rằng tôi đã nói với bạn rằng sử dụng mã hóa Delta cho các giá trị DC giúp mã hóa Huffman? Tôi hy vọng bạn có thể hiểu tại sao bây giờ. Sau khi mã hóa Delta, chúng tôi kết thúc với ít ký tự hơn để lập bản đồ và tổng kích thước của cây Huffman của chúng tôi bị giảm.

Tom Scott có một video tuyệt vời với hình ảnh động về cách Huffman mã hóa hoạt động nói chung. Hãy xem nó trước khi tiếp tục.

Một JPEG chứa tối đa 4 bảng Huffman và chúng được lưu trữ trong phần Xác định Huffman Bảng (bắt đầu bằng

10 11 12 13 10 9
3). Các hệ số DCT được lưu trữ trong 2 bảng Huffman khác nhau. Một chỉ chứa các giá trị DC từ các bảng Zig-ZAG và các giá trị còn lại chứa các giá trị AC từ các bảng Zig-ZAG. Điều này có nghĩa là trong việc giải mã của chúng tôi, chúng tôi sẽ phải hợp nhất các giá trị DC và AC từ hai ma trận riêng biệt. Thông tin DCT cho kênh độ chói và sắc tố được lưu trữ riêng biệt vì vậy chúng tôi có 2 bộ DC và 2 bộ thông tin AC cho chúng tôi tổng cộng 4 bảng Huffman.

Trong một hình ảnh màu xám, chúng tôi sẽ chỉ có 2 bảng Huffman (1 cho DC và 1 cho AC) vì chúng tôi không quan tâm đến màu sắc. Như bạn đã có thể tưởng tượng, 2 hình ảnh có thể có các bảng Huffman rất khác nhau, vì vậy điều quan trọng là lưu trữ các bảng này bên trong mỗi JPEG.

Vì vậy, chúng tôi biết các chi tiết cơ bản về những gì một hình ảnh JPEG chứa. Hãy bắt đầu với việc giải mã!

Giải mã JPEG

Chúng ta có thể chia phần giải mã thành một loạt các bước:

  1. Trích xuất các bảng Huffman và giải mã các bit
  2. Trích xuất các hệ số DCT bằng cách hoàn tác các mã hóa dài và Delta
  3. Sử dụng các hệ số DCT để kết hợp sóng cosine và tái tạo giá trị pixel cho mỗi khối 8x8
  4. Chuyển đổi YCBCR thành RGB cho mỗi pixel
  5. Hiển thị hình ảnh RGB kết quả

JPEG Standard hỗ trợ 4 định dạng nén:

  • Đường cơ sở
  • Giới thiệu tuần tự
  • Cấp tiến
  • Không mất mát

Chúng tôi sẽ làm việc với nén đường cơ sở và theo tiêu chuẩn, đường cơ sở sẽ chứa chuỗi các khối 8x8 ngay cạnh nhau. Các định dạng nén khác bố trí dữ liệu khác nhau một chút. Chỉ để tham khảo, tôi có các phân đoạn khác nhau trong nội dung hex của hình ảnh chúng tôi đang sử dụng. Cái này nó thì trông như thế nào:

Hướng dẫn python jpeg - jpeg trăn

Chúng tôi đã biết rằng một JPEG chứa 4 bảng Huffman. Đây là bước cuối cùng trong quy trình mã hóa nên nó phải là bước đầu tiên trong quy trình giải mã. Mỗi phần DHT chứa các thông tin sau:

Đồng ruộngKích thướcSự mô tả
Định danh đánh dấu2 byte0xff, 0xc4 để xác định điểm đánh dấu DHT
Chiều dài2 byte0xff, 0xc4 để xác định điểm đánh dấu DHT
Chiều dàiĐiều này chỉ định độ dài của bảng HuffmanThông tin HT
bit 4: type of HT, 0 = DC table, 1 = AC table
bit 5..7: not used, must be 0
1 bytebit 0..3: số HT (0..3, nếu không có lỗi) bit 4: loại HT, 0 = bảng DC, 1 = AC BIT BIT 5..7: Không được sử dụng, phải là 0Số lượng biểu tượng
16 byteSố lượng ký hiệu có mã có độ dài 1..16, tổng (n) của các byte này là tổng số mã, phải làBiểu tượng

n byte

Bảng chứa các ký hiệu theo thứ tự tăng độ dài mã (n = tổng số mã).Giả sử bạn có một bảng DH tương tự như thế này (SRC):Biểu tượng
Mã Huffman00 2
Độ dài mã010 3
một011 3
b100 3
c101 3
d110 3
e1110 4
f11110 5
g111110 6
h1111110 7
tôi11111110 8
j111111110 9

k

[15 14 13 12 11 10 9 8 0  ...  0]
1

l

[15 14 13 12 11 10 9 8 0  ...  0]
2

Nó sẽ được lưu trữ trong tệp JFIF gần như thế này (chúng sẽ được lưu trữ trong nhị phân. Tôi đang sử dụng ASCII chỉ cho mục đích minh họa):

[15 14 13 12 11 10 9 8 0  ...  0]
3

0 có nghĩa là không có mã Huffman có độ dài 1. 1 có nghĩa là có 1 mã Huffman có độ dài 2., v.v. Luôn có 16 byte dữ liệu độ dài trong phần DHT ngay sau thông tin lớp và ID. Hãy để viết một số mã để trích xuất các độ dài và các yếu tố trong DHT.

[15 14 13 12 11 10 9 8 0  ...  0]
4

Nếu bạn chạy mã, nó sẽ tạo ra đầu ra sau:

[15 14 13 12 11 10 9 8 0  ...  0]
5

Ngọt! Chúng tôi có độ dài và các yếu tố. Bây giờ chúng ta cần tạo một lớp bảng Huffman tùy chỉnh để chúng ta có thể tạo lại một cây nhị phân từ các yếu tố và độ dài này. Tôi đang xấu hổ sao chép mã này từ đây:

[15 14 13 12 11 10 9 8 0  ...  0]
6

10 11 12 13 10 9
4 có độ dài và phần tử, lặp lại tất cả các yếu tố và đưa chúng vào danh sách
10 11 12 13 10 9
5. Danh sách này chứa danh sách lồng nhau và đại diện cho một cây nhị phân. Bạn có thể đọc trực tuyến cách một cây Huffman hoạt động và cách tạo cấu trúc dữ liệu cây Huffman của riêng bạn trong Python. Đối với DHT đầu tiên của chúng tôi (sử dụng hình ảnh tôi đã liên kết khi bắt đầu hướng dẫn này), chúng tôi có các dữ liệu, độ dài và các yếu tố sau:

[15 14 13 12 11 10 9 8 0  ...  0]
7

Sau khi gọi

10 11 12 13 10 9
4 về điều này, danh sách
10 11 12 13 10 9
5 sẽ chứa dữ liệu này:

10 11 12 13 10 9 8 cũng chứa phương pháp 10 11 12 13 10 9 9 đi qua cây cho chúng tôi và cung cấp cho chúng tôi các bit được giải mã bằng cách sử dụng bảng Huffman. Phương pháp này mong đợi một BITSTREAM như một đầu vào. Một dòng điện chỉ là biểu diễn nhị phân của dữ liệu. Ví dụ, một dòng điện điển hình của 10 1 2 3 0 -1 0 sẽ là 10 1 2 3 0 -1 1. Trước tiên, chúng tôi chuyển đổi từng ký tự thành mã ASCII của nó và sau đó chuyển đổi mã ASCII đó thành nhị phân. Hãy để tạo ra một lớp tùy chỉnh cho phép chúng tôi chuyển đổi một chuỗi thành các bit và đọc từng bit một. Đây là cách chúng tôi sẽ thực hiện nó:

Chúng tôi cung cấp cho lớp này một số dữ liệu nhị phân trong khi khởi tạo nó và sau đó sử dụng các phương thức

10 1  2  3  0 -1
2 và
10 1  2  3  0 -1
3 để đọc nó.

Đồng ruộngKích thướcSự mô tả
Định danh đánh dấu2 byte0xff, 0xc4 để xác định điểm đánh dấu DHT
Chiều dài2 byte0xff, 0xc4 để xác định điểm đánh dấu DHT
Chiều dàiĐiều này chỉ định độ dài của bảng HuffmanThông tin HT
1 byteSố lượng ký hiệu có mã có độ dài 1..16, tổng (n) của các byte này là tổng số mã, phải làBiểu tượng

n byte

[15 14 13 12 11 10 9 8 0  ...  0]
8

Chúng tôi đã làm một vài điều ở đây. Đầu tiên, tôi đã xác định một phương thức

10 1  2  3  0 -1
6. Nó chỉ là một phương pháp tiện dụng để giải mã một số byte biến từ dữ liệu nhị phân. Tôi đã thay thế một số mã trong phương thức
10 1  2  3  0 -1
7 để sử dụng chức năng mới này. Sau đó, tôi đã xác định phương pháp
10 1  2  3  0 -1
8. Phương pháp này chỉ cần đọc tiêu đề của phần bảng lượng tử hóa và sau đó nối dữ liệu lượng tử hóa trong một từ điển với giá trị tiêu đề làm khóa. Giá trị tiêu đề sẽ là 0 cho độ chói và 1 cho sắc tố. Mỗi phần bảng lượng tử hóa trong JFIF chứa 64 byte dữ liệu QT (đối với ma trận lượng tử hóa 8x8 của chúng tôi).

Nếu chúng ta in các ma trận lượng tử hóa cho hình ảnh của chúng ta. Họ sẽ trông như thế này:

[15 14 13 12 11 10 9 8 0  ...  0]
9

Giải mã bắt đầu khung hình

Phần bắt đầu của khung chứa thông tin sau (SRC):

Đồng ruộngKích thướcSự mô tả
Định danh đánh dấu2 byte0xff, 0xc0 để xác định điểm đánh dấu SOF0
Chiều dài2 byte0xff, 0xc0 để xác định điểm đánh dấu SOF0
Chiều dàiGiá trị này bằng với 8 + các thành phần*3 giá trịĐộ chính xác dữ liệu
1 byte2 byte0xff, 0xc0 để xác định điểm đánh dấu SOF0
Chiều dài2 byte0xff, 0xc0 để xác định điểm đánh dấu SOF0
Chiều dàiGiá trị này bằng với 8 + các thành phần*3 giá trịĐộ chính xác dữ liệu
1 byteĐây là trong bit/mẫu, thường là 8 (12 và 16 không được hầu hết các phần mềm hỗ trợ).Chiều cao hình ảnh

Cái này phải> 0

10 10 10 10 10 10 10
0

Chiều rộng hình ảnh

10 10 10 10 10 10 10
1

Số lượng thành phần

Thường là 1 = tỷ lệ màu xám, 3 = màu ycbcr hoặc yiq

Mỗi thành phần

10 10 10 10 10 10 10
2

3 byte

Đọc từng dữ liệu thành phần của 3 byte. Nó chứa, (ID thành phần (1Byte) (1 = y, 2 = cb, 3 = cr, 4 = i, 5 = q), các yếu tố lấy mẫu (1Byte) (bit 0-3 dọc., 4-7 ngang.) , Số bảng lượng tử hóa (1 byte)).

10 10 10 10 10 10 10
3

Trong dữ liệu này, chúng tôi chỉ quan tâm đến một vài điều. Chúng tôi sẽ trích xuất chiều rộng và chiều cao hình ảnh và số bảng lượng tử hóa của từng thành phần. Chiều rộng và chiều cao sẽ được sử dụng khi chúng ta bắt đầu giải mã các lần quét hình ảnh thực tế từ phần bắt đầu của phần quét. Bởi vì chúng tôi sẽ chủ yếu làm việc với hình ảnh YCBCR, chúng tôi có thể mong đợi số lượng thành phần tương ứng bằng 3 và các loại thành phần tương ứng bằng 1, 2 và 3. Hãy để viết một số mã để giải mã dữ liệu này:

The

# Mapping

000: 01100001
001: 01100010
010: 01100011
100: 01100100
011: 01100101
0 loop might seem a bit funky. The
# Mapping

000: 01100001
001: 01100010
010: 01100011
100: 01100100
011: 01100101
1 tells us how many times we can divide the height by 8. The same goes for
# Mapping

000: 01100001
001: 01100010
010: 01100011
100: 01100100
011: 01100101
2. This in short tells us how many 8x8 matrices is the image divided in.

The

# Mapping

000: 01100001
001: 01100010
010: 01100011
100: 01100100
011: 01100101
3 will take in the quantization table and some additional params, create an Inverse Discrete Cosine Transformation Matrix, and give us the Y, Cr, and Cb matrices. The actual conversion of these matrices to RGB will happen in the
# Mapping

000: 01100001
001: 01100010
010: 01100011
100: 01100100
011: 01100101
4 function.

Let’s first create our IDCT class and then we can start fleshing out the

# Mapping

000: 01100001
001: 01100010
010: 01100011
100: 01100100
011: 01100101
3 method.

10 10 10 10 10 10 10
4

Let’s try to understand this IDCT class step by step. Once we extract the MCU from a JPEG, the

# Mapping

000: 01100001
001: 01100010
010: 01100011
100: 01100100
011: 01100101
6 attribute of this class will store it. Then we will rearrange the MCU matrix by undoing the zigzag encoding via the
# Mapping

000: 01100001
001: 01100010
010: 01100011
100: 01100100
011: 01100101
7 method. Finally, we will undo the Discrete Cosine Transformation by calling the
# Mapping

000: 01100001
001: 01100010
010: 01100011
100: 01100100
011: 01100101
8 method.

If you remember, the Discrete Cosine table is fixed. How the actual calculation for a DCT works is outside the scope of this tutorial as it is more maths than programming. We can store this table as a global variable and then query that for values based on x,y pairs. I decided to put the table and its calculation in the

# Mapping

000: 01100001
001: 01100010
010: 01100011
100: 01100100
011: 01100101
9 class for readability purposes. Every single element of the rearranged MCU matrix is multiplied by the values of the
a: 000
b: 001
c: 010
d: 100
e: 011
0 and we eventually get back the Y, Cr, and Cb values.

This will make more sense once we write down the

# Mapping

000: 01100001
001: 01100010
010: 01100011
100: 01100100
011: 01100101
3 method.

If you modify the zigzag table to something like this:

10 10 10 10 10 10 10
5

You will have the following output (notice the small artifacts):

Hướng dẫn python jpeg - jpeg trăn

And if you are even brave, you can modify the zigzag table even more:

10 10 10 10 10 10 10
6

It will result in this output:

Hướng dẫn python jpeg - jpeg trăn

Now let’s finish up our

# Mapping

000: 01100001
001: 01100010
010: 01100011
100: 01100100
011: 01100101
3 method:

10 10 10 10 10 10 10
7

We start by creating an Inverse Discrete Cosine Transformation class (

a: 000
b: 001
c: 010
d: 100
e: 011
3). Then we read in the bit-stream and decode it using our Huffman table.

The

a: 000
b: 001
c: 010
d: 100
e: 011
4 and
a: 000
b: 001
c: 010
d: 100
e: 011
5 refer to the DC tables for luminance and chrominance respectively and
a: 000
b: 001
c: 010
d: 100
e: 011
6 and
a: 000
b: 001
c: 010
d: 100
e: 011
7 refer to the AC tables for luminance and chrominance respectively.

After we decode the bit-stream, we extract the new delta encoded DC coefficient using the

a: 000
b: 001
c: 010
d: 100
e: 011
8 function and add the
a: 000
b: 001
c: 010
d: 100
e: 011
9 to it to get the delta decoded DC coefficient.

After that, we repeat the same decoding procedure but for the AC values in the quantization matrix. The code value of

[15 14 13 12 11 10 9 8 0  ...  0]
00 suggests that we have encountered an End of Block (EOB) marker and we need to stop. Moreover, the first part of the AC quant table tells us how many leading 0’s we have. Remember the run-length encoding we talked about in the first part? This is where that is coming into play. We decode the run-length encoding and skip forward that many bits. The skipped bits are all set to 0 implicitly in the
# Mapping

000: 01100001
001: 01100010
010: 01100011
100: 01100100
011: 01100101
9 class.

Once we have decoded the DC and AC values for an MCU, we rearrange the MCU and undo the zigzag encoding by calling the

# Mapping

000: 01100001
001: 01100010
010: 01100011
100: 01100100
011: 01100101
7 and then we perform inverse DCT on the decoded MCU.

The

# Mapping

000: 01100001
001: 01100010
010: 01100011
100: 01100100
011: 01100101
3 method will return the inverse DCT matrix and the value of the DC coefficient. Remember, this inverse DCT matrix is only for one tiny 8x8 MCU (Minimum Coded Unit) matrix. We will be doing this for all the individual MCUs in the whole image file.

Displaying Image on screen

Let’s modify our code a little bit and create a Tkinter Canvas and paint each MCU after decoding it in the

[15 14 13 12 11 10 9 8 0  ...  0]
04 method.

10 10 10 10 10 10 10
8

Let’s start with the

[15 14 13 12 11 10 9 8 0  ...  0]
05 and
[15 14 13 12 11 10 9 8 0  ...  0]
06 functions.
[15 14 13 12 11 10 9 8 0  ...  0]
05 takes in the Y, Cr, and Cb values, uses a formula to convert these values to their RGB counterparts, and then outputs the clamped RGB values. You might wonder why we are adding 128 to the RGB values. If you remember, before JPEG compressor applies DCT on the MCU, it subtracts 128 from the color values. If the colors were originally in the range [0,255], JPEG puts them into [-128,+128] range. So we have to undo that effect when we decode the JPEG and that is why we are adding 128 to RGB. As for the
[15 14 13 12 11 10 9 8 0  ...  0]
06, during the decompression, the output value might exceed [0,255] so we clamp them between [0,255] .

In the

# Mapping

000: 01100001
001: 01100010
010: 01100011
100: 01100100
011: 01100101
4 method, we loop over each 8x8 decoded Y, Cr, and Cb matrices and convert each element of the 8x8 matrices into RGB values. After conversion, we draw each pixel on the Tkinter
[15 14 13 12 11 10 9 8 0  ...  0]
10 using the
[15 14 13 12 11 10 9 8 0  ...  0]
11 method. You can find the complete code on GitHub. Now if you run this code, my face will show up on your screen 😄

Conclusion

Oh boy! Who would have thought it would take 6000 word+ explanation for showing my face on the screen. I am amazed by how smart some of these algorithm inventors are! I hope you enjoyed this article as much as I enjoyed writing it. I learned a ton while writing this decoder. I never realized how much fancy math goes into the encoding of a simple JPEG image. I might work on a PNG image next and try writing a decoder for a PNG image. You should also try to write a decoder for a PNG (or some other format). I am sure it will involve a lot of learning and even more hex fun 😅

Dù bằng cách nào, bây giờ tôi mệt mỏi.Tôi đã nhìn chằm chằm vào Hex quá lâu và tôi nghĩ rằng tôi đã kiếm được một kỳ nghỉ xứng đáng.Tất cả các bạn chăm sóc và nếu bạn có bất kỳ câu hỏi nào, vui lòng viết chúng trong các ý kiến dưới đây.Tôi siêu mới với cuộc phiêu lưu mã hóa jpeg này nhưng tôi sẽ cố gắng trả lời nhiều nhất có thể 😄

Từ biệt!👋

đọc thêm

Nếu bạn muốn đi sâu vào chi tiết hơn, bạn có thể xem một vài tài nguyên tôi đã sử dụng trong khi viết bài viết này.Tôi cũng đã thêm một số liên kết bổ sung cho một số công cụ liên quan đến JPEG thú vị:

  • Hướng dẫn minh họa để làm sáng tỏ JPEG
  • Một bài viết cực kỳ chi tiết về mã hóa JPEG Huffman
  • Hãy để viết một thư viện JPEG đơn giản.Sử dụng C ++
  • Tài liệu cấu trúc Python 3
  • Đọc bài viết này về cách FB sử dụng kiến thức này về JPEG
  • Bố cục và định dạng tệp JPEG
  • Một bài thuyết trình thú vị của Bộ Quốc phòng về pháp y JPEG
  • lập trình
  • Python