Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

stough 202-

Preview: Entropy, Compression, and Huffman Variable Length Encoding
  1. Computing Entropy

  2. Huffman Trees

  3. Variable-length encoding and decoding

Images and videos can be quite large. At 60 frames per second, a 4K Ultra HD movie of 2 hours could require

(3840⋅2160)pixels⋅3bytepixel=24883200byteframe,(3840\cdot2160)pixels \cdot 3\frac{byte}{pixel} = 24883200 \frac{byte}{frame},
24883200byteframe⋅60framesecond⋅60secondminute⋅120minutes109byteGB=10749.5GB,\frac{24883200 \frac{byte}{frame}\cdot60\frac{frame}{second}\cdot60\frac{second}{minute}\cdot120 minutes}{10^9\frac{byte}{GB}} = 10749.5 GB,

or about 163 blu-ray discs to store without compression. So clearly compression is a thing.

Compression takes advantage of different kinds of redundancy in the signal. Images in particular have a lot of redundancy. Spatial and Temporal redundancy are characterized by the fact that most pixels are closely related their neighbors. This is due to the fact that any images of human interest are likely to have objects comprised of regions of similar color. Temporal redundancy is the video case, where a pixel over time changes little usually.

Coding redundancy is the one we’re dealing with here. Most images we consider are 3-channel images with 8 bits per pixel per channel. We need 8 bits to represent a value in [0,255][0, 255]. Variable-length encoding can take advantage of the differential frequency of certain pixel values over others, encoding more common pixels with fewer bits, and paying for that by using more bits to represent less common pixels.

  • Compressibility: The degree to which the information in an image or video can be represented using fewer bits. In the example above of the highly compressible 4K Ultra HD, we might say the compression ratio is ~160 (display size over storage size).

  • Entropy: a measure of the information content in a signal.

    • signal: here, think of this as seeing each individual pixel in the image in order.

    • Think of entropy as the surprise, or “unpredictability” of the signal. Each new pixel value kk in the signal gives you some information,

    I(k)=−log⁡2(p(k)),I(k) = -\log_2(p(k)),

    where p(k)p(k) is the probability of observing kk in the signal. For example, if the probability of observing kk is 1, as in 100% of all the pixels have the value kk, then there is no information/surpise, or 0 bits, associated with observing it. If p(k)=12p(k) = \frac{1}{2}, then that’s 1 bit of information. (We won’t consider the symbols κ\kappa that never show up, since observing such a symbol would be infinitely surprising :-).

    • For our images in the range [0,255][0, 255], we can measure the entropy or information content of the image as

    H~=−∑k=0255p(k)log⁡2p(k)\tilde{H} = -\sum_{k=0}^{255} p(k)\log_{2}p(k)

    This is just an average of how many bits of information each newly observed pixel gives us, weighted by how likely that pixel value is. p(k)p(k) is just the normalized histogram values.

In this notebook we’ll explore Huffman variable-length encoding and its effectiveness in some images.

Imports

Notice I’ve added our own custom Huffman utilities huff_utils and scipy.stats.entropy. Study build_huff_tree in particular.

Computing Entropy

loadHuffableImage: input is multi-channel, using grayscale.
loadHuffableImage: Setting range to [0, 255]
((128, 128), dtype('uint8'), np.uint8(0), np.uint8(255))
<Figure size 400x300 with 1 Axes>
<Figure size 900x300 with 2 Axes>
np.int64(131072)
np.float64(3.749747928998133)

We store this image with 8 bits per pixel, but on average each pixel carries only about 3.75 bits of information. The histogram shows why: a handful of gray levels account for almost all the pixels. So, at least in principle, we should be able to store this image in less than half the space. Entropy is the lower bound on the average bits per pixel that any code can achieve, as long as it treats each pixel independently. Let’s see how close Huffman coding gets.

Huffman Trees

A Huffman tree is a binary tree structure whose leaf nodes represent the symbols in a signal (the pixel values in an image). The path from the root of the tree to a leaf node represents the bit encoding of the associated symbol (0 for left child, 1 for right child, for example). Let’s see an example

Before heapify...
[node: freq 136: A]
[node: freq 690: B]
[node: freq 754: C]
[node: freq 134: D]

After heapify...
[node: freq 134: D]
[node: freq 136: A]
[node: freq 754: C]
[node: freq 690: B]
Final Huffman tree:

  --[node: freq 754: C]
--[node: freq 1714: ]
      --[node: freq 134: D]
    --[node: freq 270: ]
      --[node: freq 136: A]
  --[node: freq 960: ]
    --[node: freq 690: B]

In the example above we start with pixel values in [‘A’, ‘B’, ‘C’, ‘D’]. The Huffman Tree construction algorithm basically sorts these symbols (image intensities for example) from least to most frequent as a collection of leaf nodes (a minheap is useful for this).

  • We then take the two least frequent nodes and combine them into one Huffman tree with the two symbols as leaf nodes and the frequency associated with this tree as the sum of the two individual frequencies.

  • Then insert this tree back into the collection. This is where a heap comes in useful, as a self-maintaining structure always providing access to the least frequent node.

  • Repeat. Each iteration we end up with one tree fewer in the collection/heap, until there is only one tree left.

The structure of that final tree represents an optimal encoding given the relative frequencies provided.

Variable-length encoding and decoding

See the code in huff_utils. One thing you’ll notice about the encoder is that a lot of pixel values require many more than even 8 bits!!! How could that possibly save space?

{np.int64(217): '11', np.int64(0): '101', np.int64(23): '10011111111', np.int64(51): '10011111110', np.int64(188): '10011111101', np.int64(158): '100111111001', np.int64(166): '100111111000', np.int64(183): '100111110111', np.int64(92): '100111110110', np.int64(185): '100111110101', np.int64(126): '100111110100', np.int64(176): '10011111001', np.int64(177): '10011111000', np.int64(171): '100111101111', np.int64(175): '100111101110', np.int64(99): '10011110110', np.int64(103): '100111101011', np.int64(218): '100111101010', np.int64(189): '10011110100', np.int64(228): '100111100111', np.int64(117): '100111100110', np.int64(109): '10011110010', np.int64(54): '10011110001', np.int64(250): '10011110000', np.int64(12): '1001110111', np.int64(65): '10011101101', np.int64(66): '10011101100', np.int64(202): '1001110101', np.int64(178): '10011101001', np.int64(90): '100111010001', np.int64(45): '100111010000', np.int64(14): '1001110011', np.int64(219): '100111001011', np.int64(110): '100111001010', np.int64(111): '100111001001', np.int64(224): '100111001000', np.int64(128): '100111000', np.int64(215): '100110111', np.int64(142): '100110110111', np.int64(155): '100110110110', np.int64(245): '10011011010', np.int64(26): '1001101100', np.int64(172): '10011010', np.int64(187): '10011001111', np.int64(191): '10011001110', np.int64(55): '10011001101', np.int64(56): '10011001100', np.int64(41): '10011001011', np.int64(84): '10011001010', np.int64(16): '1001100100', np.int64(153): '10011000111', np.int64(78): '10011000110', np.int64(200): '10011000101', np.int64(102): '10011000100', np.int64(248): '10011000011', np.int64(118): '100110000101', np.int64(129): '100110000100', np.int64(197): '10011000001', np.int64(24): '10011000000', np.int64(170): '10010111111', np.int64(86): '10010111110', np.int64(181): '10010111101', np.int64(91): '10010111100', np.int64(194): '10010111011', np.int64(29): '10010111010', np.int64(107): '10010111001', np.int64(53): '10010111000', np.int64(160): '10010110111', np.int64(80): '10010110110', np.int64(67): '1001011010', np.int64(130): '10010110011', np.int64(122): '10010110010', np.int64(40): '1001011000', np.int64(3): '10010101', np.int64(216): '10010100', np.int64(4): '100100111', np.int64(25): '1001001101', np.int64(68): '10010011001', np.int64(139): '10010011000', np.int64(50): '1001001011', np.int64(105): '10010010101', np.int64(113): '100100101001', np.int64(233): '100100101000', np.int64(17): '1001001001', np.int64(43): '10010010001', np.int64(98): '10010010000', np.int64(173): '1001000111', np.int64(79): '100100011011', np.int64(161): '100100011010', np.int64(157): '10010001100', np.int64(212): '100100010', np.int64(162): '10010000111', np.int64(96): '100100001101', np.int64(112): '100100001100', np.int64(27): '1001000010', np.int64(62): '1001000001', np.int64(131): '10010000001', np.int64(133): '10010000000', np.int64(114): '10001111111', np.int64(116): '10001111110', np.int64(100): '1000111110111', np.int64(222): '1000111110110', np.int64(74): '100011111010', np.int64(232): '1000111110011', np.int64(236): '1000111110010', np.int64(120): '1000111110001', np.int64(123): '1000111110000', np.int64(235): '10001111011', np.int64(85): '1000111101011', np.int64(97): '1000111101010', np.int64(223): '1000111101001', np.int64(229): '1000111101000', np.int64(193): '1000111100', np.int64(21): '1000111011', np.int64(205): '1000111010', np.int64(134): '10001110011', np.int64(135): '10001110010', np.int64(59): '10001110001', np.int64(124): '10001110000', np.int64(46): '10001101', np.int64(143): '10001100111', np.int64(184): '100011001101', np.int64(95): '100011001100', np.int64(36): '10001100101', np.int64(151): '100011001001', np.int64(152): '100011001000', np.int64(198): '1000110001', np.int64(186): '1000110000', np.int64(136): '10001011', np.int64(253): '1000101011', np.int64(32): '1000101010', np.int64(213): '100010100', np.int64(246): '10001001111', np.int64(156): '10001001110', np.int64(35): '1000100110', np.int64(141): '10001001011', np.int64(144): '10001001010', np.int64(94): '10001001001', np.int64(192): '10001001000', np.int64(101): '10001000111', np.int64(52): '10001000110', np.int64(127): '10001000101', np.int64(33): '10001000100', np.int64(154): '10001000011', np.int64(77): '10001000010', np.int64(19): '10001000001', np.int64(164): '10001000000', np.int64(214): '10000', np.int64(255): '01', np.int64(63): '001', np.int64(249): '000111111111', np.int64(132): '000111111110', np.int64(244): '00011111110', np.int64(13): '0001111110', np.int64(57): '00011111011', np.int64(231): '00011111010', np.int64(106): '00011111001', np.int64(226): '00011111000', np.int64(179): '00011110111', np.int64(180): '00011110110', np.int64(89): '0001111010', np.int64(83): '00011110011', np.int64(87): '00011110010', np.int64(38): '0001111000', np.int64(140): '00011101', np.int64(72): '00011100111', np.int64(146): '00011100110', np.int64(148): '00011100101', np.int64(150): '00011100100', np.int64(20): '000111000', np.int64(1): '0001101', np.int64(7): '000110011', np.int64(61): '0001100101', np.int64(243): '00011001001', np.int64(70): '00011001000', np.int64(6): '000110001', np.int64(49): '0001100001', np.int64(209): '0001100000', np.int64(8): '000101111', np.int64(242): '00010111011', np.int64(227): '000101110101', np.int64(234): '000101110100', np.int64(119): '000101110011', np.int64(121): '000101110010', np.int64(221): '000101110001', np.int64(225): '000101110000', np.int64(230): '00010110111', np.int64(237): '00010110110', np.int64(206): '0001011010', np.int64(167): '00010110011', np.int64(174): '00010110010', np.int64(241): '00010110001', np.int64(145): '000101100001', np.int64(149): '000101100000', np.int64(39): '00010101111', np.int64(81): '00010101110', np.int64(88): '00010101101', np.int64(190): '00010101100', np.int64(58): '0001010101', np.int64(147): '0001010100', np.int64(208): '000101001', np.int64(238): '00010100011', np.int64(240): '00010100010', np.int64(104): '00010100001', np.int64(239): '00010100000', np.int64(28): '0001001111', np.int64(22): '0001001110', np.int64(64): '0001001101', np.int64(137): '0001001100', np.int64(5): '00010010', np.int64(44): '0001000111', np.int64(203): '0001000110', np.int64(163): '00010001011', np.int64(165): '00010001010', np.int64(182): '0001000100', np.int64(37): '000100001111', np.int64(75): '000100001110', np.int64(108): '000100001101', np.int64(220): '000100001100', np.int64(247): '0001000010', np.int64(11): '000100000', np.int64(2): '000011', np.int64(30): '0000101111', np.int64(76): '0000101110', np.int64(196): '00001011011', np.int64(204): '00001011010', np.int64(60): '0000101100', np.int64(254): '000010101', np.int64(10): '000010100', np.int64(252): '000010011', np.int64(48): '000010010', np.int64(73): '00001000111', np.int64(159): '00001000110', np.int64(31): '0000100010', np.int64(71): '0000100001', np.int64(199): '0000100000', np.int64(15): '0000011', np.int64(47): '0000010111', np.int64(201): '0000010110', np.int64(195): '0000010101', np.int64(125): '0000010100', np.int64(82): '0000010011', np.int64(168): '0000010010', np.int64(69): '0000010001', np.int64(34): '0000010000', np.int64(169): '0000001111', np.int64(42): '0000001110', np.int64(115): '0000001101', np.int64(251): '0000001100', np.int64(93): '0000001011', np.int64(138): '0000001010', np.int64(18): '000000100', np.int64(211): '000000011', np.int64(9): '000000010', np.int64(210): '000000001', np.int64(207): '000000000'}
{'11': np.int64(217), '101': np.int64(0), '10011111111': np.int64(23), '10011111110': np.int64(51), '10011111101': np.int64(188), '100111111001': np.int64(158), '100111111000': np.int64(166), '100111110111': np.int64(183), '100111110110': np.int64(92), '100111110101': np.int64(185), '100111110100': np.int64(126), '10011111001': np.int64(176), '10011111000': np.int64(177), '100111101111': np.int64(171), '100111101110': np.int64(175), '10011110110': np.int64(99), '100111101011': np.int64(103), '100111101010': np.int64(218), '10011110100': np.int64(189), '100111100111': np.int64(228), '100111100110': np.int64(117), '10011110010': np.int64(109), '10011110001': np.int64(54), '10011110000': np.int64(250), '1001110111': np.int64(12), '10011101101': np.int64(65), '10011101100': np.int64(66), '1001110101': np.int64(202), '10011101001': np.int64(178), '100111010001': np.int64(90), '100111010000': np.int64(45), '1001110011': np.int64(14), '100111001011': np.int64(219), '100111001010': np.int64(110), '100111001001': np.int64(111), '100111001000': np.int64(224), '100111000': np.int64(128), '100110111': np.int64(215), '100110110111': np.int64(142), '100110110110': np.int64(155), '10011011010': np.int64(245), '1001101100': np.int64(26), '10011010': np.int64(172), '10011001111': np.int64(187), '10011001110': np.int64(191), '10011001101': np.int64(55), '10011001100': np.int64(56), '10011001011': np.int64(41), '10011001010': np.int64(84), '1001100100': np.int64(16), '10011000111': np.int64(153), '10011000110': np.int64(78), '10011000101': np.int64(200), '10011000100': np.int64(102), '10011000011': np.int64(248), '100110000101': np.int64(118), '100110000100': np.int64(129), '10011000001': np.int64(197), '10011000000': np.int64(24), '10010111111': np.int64(170), '10010111110': np.int64(86), '10010111101': np.int64(181), '10010111100': np.int64(91), '10010111011': np.int64(194), '10010111010': np.int64(29), '10010111001': np.int64(107), '10010111000': np.int64(53), '10010110111': np.int64(160), '10010110110': np.int64(80), '1001011010': np.int64(67), '10010110011': np.int64(130), '10010110010': np.int64(122), '1001011000': np.int64(40), '10010101': np.int64(3), '10010100': np.int64(216), '100100111': np.int64(4), '1001001101': np.int64(25), '10010011001': np.int64(68), '10010011000': np.int64(139), '1001001011': np.int64(50), '10010010101': np.int64(105), '100100101001': np.int64(113), '100100101000': np.int64(233), '1001001001': np.int64(17), '10010010001': np.int64(43), '10010010000': np.int64(98), '1001000111': np.int64(173), '100100011011': np.int64(79), '100100011010': np.int64(161), '10010001100': np.int64(157), '100100010': np.int64(212), '10010000111': np.int64(162), '100100001101': np.int64(96), '100100001100': np.int64(112), '1001000010': np.int64(27), '1001000001': np.int64(62), '10010000001': np.int64(131), '10010000000': np.int64(133), '10001111111': np.int64(114), '10001111110': np.int64(116), '1000111110111': np.int64(100), '1000111110110': np.int64(222), '100011111010': np.int64(74), '1000111110011': np.int64(232), '1000111110010': np.int64(236), '1000111110001': np.int64(120), '1000111110000': np.int64(123), '10001111011': np.int64(235), '1000111101011': np.int64(85), '1000111101010': np.int64(97), '1000111101001': np.int64(223), '1000111101000': np.int64(229), '1000111100': np.int64(193), '1000111011': np.int64(21), '1000111010': np.int64(205), '10001110011': np.int64(134), '10001110010': np.int64(135), '10001110001': np.int64(59), '10001110000': np.int64(124), '10001101': np.int64(46), '10001100111': np.int64(143), '100011001101': np.int64(184), '100011001100': np.int64(95), '10001100101': np.int64(36), '100011001001': np.int64(151), '100011001000': np.int64(152), '1000110001': np.int64(198), '1000110000': np.int64(186), '10001011': np.int64(136), '1000101011': np.int64(253), '1000101010': np.int64(32), '100010100': np.int64(213), '10001001111': np.int64(246), '10001001110': np.int64(156), '1000100110': np.int64(35), '10001001011': np.int64(141), '10001001010': np.int64(144), '10001001001': np.int64(94), '10001001000': np.int64(192), '10001000111': np.int64(101), '10001000110': np.int64(52), '10001000101': np.int64(127), '10001000100': np.int64(33), '10001000011': np.int64(154), '10001000010': np.int64(77), '10001000001': np.int64(19), '10001000000': np.int64(164), '10000': np.int64(214), '01': np.int64(255), '001': np.int64(63), '000111111111': np.int64(249), '000111111110': np.int64(132), '00011111110': np.int64(244), '0001111110': np.int64(13), '00011111011': np.int64(57), '00011111010': np.int64(231), '00011111001': np.int64(106), '00011111000': np.int64(226), '00011110111': np.int64(179), '00011110110': np.int64(180), '0001111010': np.int64(89), '00011110011': np.int64(83), '00011110010': np.int64(87), '0001111000': np.int64(38), '00011101': np.int64(140), '00011100111': np.int64(72), '00011100110': np.int64(146), '00011100101': np.int64(148), '00011100100': np.int64(150), '000111000': np.int64(20), '0001101': np.int64(1), '000110011': np.int64(7), '0001100101': np.int64(61), '00011001001': np.int64(243), '00011001000': np.int64(70), '000110001': np.int64(6), '0001100001': np.int64(49), '0001100000': np.int64(209), '000101111': np.int64(8), '00010111011': np.int64(242), '000101110101': np.int64(227), '000101110100': np.int64(234), '000101110011': np.int64(119), '000101110010': np.int64(121), '000101110001': np.int64(221), '000101110000': np.int64(225), '00010110111': np.int64(230), '00010110110': np.int64(237), '0001011010': np.int64(206), '00010110011': np.int64(167), '00010110010': np.int64(174), '00010110001': np.int64(241), '000101100001': np.int64(145), '000101100000': np.int64(149), '00010101111': np.int64(39), '00010101110': np.int64(81), '00010101101': np.int64(88), '00010101100': np.int64(190), '0001010101': np.int64(58), '0001010100': np.int64(147), '000101001': np.int64(208), '00010100011': np.int64(238), '00010100010': np.int64(240), '00010100001': np.int64(104), '00010100000': np.int64(239), '0001001111': np.int64(28), '0001001110': np.int64(22), '0001001101': np.int64(64), '0001001100': np.int64(137), '00010010': np.int64(5), '0001000111': np.int64(44), '0001000110': np.int64(203), '00010001011': np.int64(163), '00010001010': np.int64(165), '0001000100': np.int64(182), '000100001111': np.int64(37), '000100001110': np.int64(75), '000100001101': np.int64(108), '000100001100': np.int64(220), '0001000010': np.int64(247), '000100000': np.int64(11), '000011': np.int64(2), '0000101111': np.int64(30), '0000101110': np.int64(76), '00001011011': np.int64(196), '00001011010': np.int64(204), '0000101100': np.int64(60), '000010101': np.int64(254), '000010100': np.int64(10), '000010011': np.int64(252), '000010010': np.int64(48), '00001000111': np.int64(73), '00001000110': np.int64(159), '0000100010': np.int64(31), '0000100001': np.int64(71), '0000100000': np.int64(199), '0000011': np.int64(15), '0000010111': np.int64(47), '0000010110': np.int64(201), '0000010101': np.int64(195), '0000010100': np.int64(125), '0000010011': np.int64(82), '0000010010': np.int64(168), '0000010001': np.int64(69), '0000010000': np.int64(34), '0000001111': np.int64(169), '0000001110': np.int64(42), '0000001101': np.int64(115), '0000001100': np.int64(251), '0000001011': np.int64(93), '0000001010': np.int64(138), '000000100': np.int64(18), '000000011': np.int64(211), '000000010': np.int64(9), '000000001': np.int64(210), '000000000': np.int64(207)}
61771
np.int64(131072)
np.float64(2.121901863333927)
np.float64(3.77020263671875)

Huffman coding uses about 3.77 bits per pixel, within a few hundredths of a bit of the entropy, for a compression ratio of better than 2. This is no accident. Huffman codes are optimal among all codes that assign each symbol its own whole number of bits, and they always come within 1 bit per symbol of the entropy. Notice also that the rare pixel values that got codes much longer than 8 bits barely matter: they’re so rare that they hardly contribute to the total.

 

The next three sections are yours. Together they complete the round trip: an image goes in, bits come out, and the same image comes back.

🔨 Your Turn: Decoding

So in the above we see how we can take an image--an array of pixel values--and encode it using the Huffman variable-length encoding. In short:

I = load_huffable_image('../dip_pics/happy128.png')
encoder, decoder = build_huff_pair(I)
enI = ''.join(encoder[pix] for pix in I.ravel())

After the above section executes, enI is the encoded image (a sequence of bits), which can be stored or transmitted more efficiently. However, when display of the image is needed again, we’re going to need to decode the bit sequence back into an array of pixel values. We can use the decoder dictionary to do this.

Task: Write a decode_image function that accepts such an encoded string (like enI) and the decoder dictionary, and outputs a list of intensities equal to the uncompressed image I.ravel(). Show a validation test of this function: is the reconstructed array of intensities equal to the original? See array_equal.

Guidance:

  • Look at decoder above: its keys are bit strings and its values are pixel intensities.

  • The encoded string has no separators between codewords, so the hard part is knowing where one codeword ends and the next begins. Huffman codes are prefix-free: no codeword is the beginning of any other codeword. Think about why that property makes the decoding unambiguous, and use it to decide when you’ve read a complete codeword.

  • Test on a tiny example you can check by hand first, such as encoding just Ih.ravel()[:5], before decoding the whole image.

  • Decoding the whole 128×128128 \times 128 image in plain Python should take at most a few seconds. If yours takes much longer, look for work you repeat for every bit.

 

🔨 Your Turn: Reconstructing

Task: Now take the output of decode_image to reconstruct the original image I. Show your reconstruction code and a figure supporting its success.

Guidance:

  • Even though our encoded stream does not include the image shape, you may cheat there and use the original image shape “out of band” so to speak. In practice we could send along the image shape with the encoded stream without too much trouble. See numpy.reshape.

  • Your decoded list holds Python integers; give the reconstructed array the same datatype as the original (arr_info on both).

  • A figure from vis_pair shows they look the same; np.array_equal shows they are the same. Show both, since lossless compression promises the second.

 

🔨 Your Turn: In Color

Task: Do this with a color image (that is not happyFace). Perform Huffman compression separately on each of the three channels and show that you can encode, and then decode and reconstruct such a color image. Report the compression ratio for each channel and for the whole image.

Guidance:

  • Don’t load the image with load_huffable_image: it converts color images to grayscale (read it in huff_utils). Use plt.imread, and note that .png files load as floats in [0,1][0,1] while .jpg files load as uint8 in [0,255][0,255]. Huffman coding needs the integer values.

  • Each channel gets its own encoder/decoder pair, built from that channel alone. Your decode_image from above should work unchanged on each channel.

  • Pick a modest image size, or crop one, so encoding and decoding stay quick: every pixel becomes several characters of a Python string.

  • Which channel compresses best? Compare each channel’s bits per pixel with its entropy, as we did for the gray image.