Documente Academic
Documente Profesional
Documente Cultură
Introduction
According to Wikipedia:
Arithmetic coding is a method for lossless data compression. Normally, a string of
characters such as the words hello there is represented using a fixed number of bits
per character, as in the ASCII code. Like Huffman coding, arithmetic coding is a form
of variable-length entropy encoding that converts a string into another form that
represents frequently used characters using more bits, with the goal of using fewer
bits in total. As opposed to other entropy encoding techniques that separate the
input message into its component symbols and replace each symbol with a code
word, arithmetic coding encodes the entire message into a single number, a fraction
ne, where (0.0 n < 1.0).
Some of the properties of arithmetic coding are:
1.
2.
3.
4.
5.
It is suited for use as a compression black-box by those who are not coding
experts or do not want to implement the coding algorithm themselves.
Description
The coder object in the source code attached to this article can work as a black box
implementation of an arithmetic encoder/decoder. However, to use it, we need to
understand the basics of statistical modeling for arithmetic coding and a few of the
basic concepts of arithmetic coding.
In general, using arithmetic coding depends on creating a statistical model of the
data. In this example, I will assume that we are trying to encode the words HELLO
WORLD.
ii.
iii.
iv.
v.
vi.
vii.
The total number of characters to be encoded is 10. For convenience and clarity, I
have ignored the space between the words HELLO and WORLD.
2.
3.
1/10
0.0 0.1
1/10
0.1 0.2
1/10
0.2 0.3
3/10
0.3 0.6
2/10
0.6 0.8
1/10
0.8 0.9
1/10
0.9 1.0
4.
Copy Code
High
1/10
0.2 0.30.2
0.3
1/10
0.1 0.20.21
0.22
3/10
0.3 0.60.213
0.216
3/10
0.3 0.60.2139
0.2148
3/10
0.6 0.80.21444
0.21462
1/10
0.9 1.00.214602
0.214620
2/10
1/10
3/10
1/10
0.0 0.1
High
6
0.21461578 0.21461580
8
6
So the final low value 0.214615788 will encode the string HELLOWORLD (without
the space, which was omitted for clarity).
5.
The algorithm for decoding the number and retrieving the encoded string/data
is as below:
Find the symbol represented by the range that the number is in, output it. Remove
the effects of encoding and repeat. In pseudo-code:
Hide Copy Code
This algorithm would give us the working below, taking the output from the example
above:
Hide Shrink
Copy Code
subtract current symbols low value from number = 0.538596 0.3 = 0.238596
divide the number by the current range = 0.238596/0.3 = 0.79532
1/10
0.2 0.3
1/10
0.1 0.20.14615788
3/10
0.3 0.60.4615788
3/10
0.3 0.60.538596
3/10
0.6 0.80.79532
1/10
0.9 1.00.9766
2/10
0.6 0.80.766
1/10
0.8 0.90.83
3/10
0.3 0.60.3
1/10
0.0 0.10.0
Implementation
The above algorithms give us the basis of a working implementation of arithmetic
coding.
In a practical implementation of arithmetic coding/decoding, note that:
i.
The range assigned to a symbol includes the lower limit but not the upper
limit of that range. In other words, in our example, the symbol H owns the
range 0.2 to 0.29999...but not 0.3. Mathematically, this can be written as H
owns the range (0.2 n < 0.3).
ii.
1/10
0000
0999
1/10
1000
1999
1/10
2000
2999
3/10
3000
5999
2/10
6000
7999
1/10
8000
8999
1/10
9000
9999
Basically, this means that the symbol D owns the range 0.0000 to 0.0999...which is
the range (0.0 n < 0.1) written in mathematical notation.
Keep in mind that the number 99999... should be thought of as 0.9999..., with an
infinity of 9s after the decimal point. In the limit, there is an infinitesimally small
difference between this upper limit 1.
iii.
High
1/10
0.2 0.30.2
0.3
1/10
0.1 0.20.21
0.22
3/10
0.3 0.60.213
0.216
3/10
0.3 0.60.2139
0.2148
3/10
0.6 0.80.21444
0.21462
1/10
0.9 1.00.214602
0.214620
High
2/10
1/10
3/10
0.3 0.6
0.21461578 0.21461589
8
6
1/10
0.0 0.1
0.21461578 0.21461580
8
6
Notice that as the encoding proceeds, the high and low numbers significant digits
tend to converge.
In the second iteration (while encoding E), the digit 2 converges for both the high
and low numbers, and will never change again regardless of how many more
characters we encode thereafter. This is a property of the encoding algorithm which
continually narrows the encoding range.
As encoding proceeds, we obtain the sequence of low numbers, 0.2, 0.21, 0.213, and
the high numbers, 0.3, 0.22, 0.216 (output when encoding L). At this point, the
significant digits 2 and 1 have converged for both the high and low numbers.
Again, at the fifth iteration (while encoding 0), we have the number 0.21444 in the
current low number, and 0.21462 in the current high number. At this point, the most
significant numbers 2, 1, and 4 have converged.
In a practical implementation, once the significant digits of the high and low
numbers have converged, they can be considered to have no further effect on the
calculation. They act to simply narrow the encoding range, retaining the decimal
place of the encoding, but have no further significant effect on subsequent
calculations.
If we imagine our upper and lower numbers as being in an infinitely large array (or a
very large array), in our practical calculation, we may safely ship out any significant
digits that have converged, and simply shift the entire array one more place to the
right.
iv.
We may set the range between high and low to 00000 and 99999, with an imaginary
decimal point in front of these numbers. Furthermore, we will assume that the
number of 0s in the low number after the decimal point stretches to infinity, and the
number of 9s in the high number also stretches to infinity.
For the purposes of our calculation, while the range between 00000 (0.00000...) and
99999 (0.99999...) is actually 99999 (0.99999...), we increment it by 1 (0.000...1).
This is because the number of 9s after the decimal point is taken to be infinite.
Therefore, the difference between the high and low numbers in the limit is actually 1.
Keep in mind that the imaginary decimal point is in front of each of the high or low
range numbers below. The numbers 0.000000... and 0.99999... should be assumed
to continue indefinitely. Since our bounds are now 0.00000 and 0.99999, and not 0.0
and 1.0, when calculating the range, we will add 1 to compensate. I.e., the high
value should be considered to be 1.
In the pseudo code below, MSD stands for the Most Significant Digit or the Left Most
Digit in the number.
Also, at this point, 2 has converged, so shifting out 2 and furthermore shifting in
another 9 into the high array gives us the new high and low, as below:
Hide Copy Code
output 2
At this point, 1 has converged, so shifting out 1 and furthermore shifting in another 9
into the high array gives us the new high and low, as below:
Hide Shrink
Copy Code
output 21
no output
no output
At this point, 4 has converged, so shifting out 4 and furthermore shifting in another
digit into both the high and low array gives us the new high and low, as below:
Hide Copy Code
output 214
At this point, 6 has converged, so shifting out 6 and furthermore shifting in another
digit into both the high and low array gives us the new high and low, as below:
Hide Copy Code
output 2146
At this point, 1 has converged, so shifting it out and shifting in another digit into both
the high and low array gives us the new high and low, as below:
Hide Copy Code
output 21461
no output
At this point, 5 has converged, so shifting it out and shifting in another digit into both
the high and low array gives us the new high and low, as below:
Hide Copy Code
output 214615
At this point, 7 and 8 have converged, so shifting out 7 and 8 and further shifting
another digit into both the output array gives us the coded string:
Hide Copy Code
output 21461578
END ENCODING
...
High Output
00000 99999
1/10
1/10
3/10
3/10
Charact Probabilit
Range Low
er
y
High Output
3/10
1/10
2/10
1/10
3/10
1/10
Underflow
It is possible that while encoding symbols, a situation could arise in which the high
and low cannot converge. In the event that the encoded word has a string of 0s or 9s
in it, the high and low values will slowly converge on a value, but may not see their
most significant digits match immediately. For example, high and low may look like
this:
Hide Copy Code
High:
700003
Low:
699994
The calculated range is only a single digit long, which means the encoder does not
have enough precision to be accurate.
In effect, the range between high and low has become so small that any calculation
will always return the same values. Moreover, since the most significant digits of
both high and low are not equal, the algorithm can't output the digit and shift.
The way to avoid underflow is to prevent it altogether. This is done by modifying the
algorithm slightly. If the two MSDs don't match, but are now on adjacent numbers, a
second test is done. If high and low are one apart, we test to see if the second most
significant digit in high is a 0, and the second digit in low is a 9. If so, it means that
the underflow is threatening.
When there is potential for underflow, the encoder does a slightly different shift
operation. It deletes the second digits from high and low, and shifts the rest of the
digits left. The most significant digit, however, stays in place. Also, an underflow
counter is set to mark the digit that was discarded. The operation looks like this:
Hide Copy Code
Before
------
After
-----
High:
40344
43449
Low:
39810
38100
Underflow:
Decoding
Previously, in the decoding process, we could use the entire input number. This,
however, may not be possible in practice since we can't perform an operation like
that on a number that could potentially be millions or billions of bytes long. Just as in
the encoding process, the decoder can operate using simple finite integer
calculations.
The decoder maintains three integers. The first two, high and low, correspond
exactly to the high and low values maintained by the encoder. The third number,
code, contains the current bits being read in from the input bits stream.
Important: The current probability is determined by where the present code value
falls along that range. If you divide the value-low by high-low+1, you get the actual
probability for the present symbol.
vi.
The C# implementation
The encoding procedure written in C# is included in the downloadable source code
for this article. The code for the encoder as well as the decoder were first published
(in C) in an article entitled "Arithmetic Coding for Data Compression" in the February
1987 issue of "Communications of the ACM", by Ian H. Witten, Radford Neal, and John
Cleary, published again by Mark Nelson as C code, and then ported to C# by myself
and is being published here with the author's permission.
I have modified the code slightly so as to further isolate statistical modeling and
arithmetic coding. The coder (coder.cs in the source code) class is an object that
implements arithmetic coding. This class can then be used by any statistical model
of the data. I have included a test project and a few examples of testing the coding
and decoding methods of the class.
There are two major differences between the algorithms shown earlier and the code
included in this article.
The first difference is in the way probabilities are transmitted. In the algorithms
shown above, the probabilities were kept as a pair of floating point numbers on the
0.0 to 1.0 range. Each symbol had its own section of that range. In the C# class
included here, a symbol has a slightly different definition. Instead of two floating
point numbers, the symbol's range is defined as two integers, which are counts along
a scale.
The scale is also included as part of the coder class definition: as the property ' scale'
of
the
class
(coder.scale).
This
scale
is
used
in
the
methods encode_symbol and decode_symbol to convert the probability integer into
its floating point equivalent, or to decode an encoded symbol into its char
equivalent. This also means that a user of the class can input the probability of a
symbol as integers instead of floating point numbers. See the included test solution
for an example.
So, for instance in the "HELLO WORLD" example, the letter H was defined previously
as the high/low pair of 0.2 and 0.3. In the code being used here, "H" would be
defined as the low and high counts of 2 and 3, with the symbol scale being 10.
The second difference in this algorithm is that all of the comparison and shifting
operations are being done in base 2, rather than base 10. The illustrations given
previously were done on base 10 numbers to make the algorithms a little more
comprehensible. The algorithms work properly in base 10, but masking off digits and
shifting in base 10 on most computers is expensive and slow. Instead of comparing
the two MSD digits, we now compare the two MSD bits.
There are two things missing that are needed in order to use the encoding and
decoding algorithms. The first is a set of bit oriented input and output routines.
These are shown in the code listing and are presented here as the bit IO routines.
The coder.symbol structure is responsible for storing the probabilities of each
character, and performing two different transformations.
During the encoding process, the coder.symbol structure has to take a character to
be encoded and convert it to a probability range. The probability range is defined as
a low count, a high count in the structure.
During the decoding process, the coder.symbol structure has to take a count derived
from the input bit stream and convert it into a character for output.
The coder object is created with
these coder.symbol structures.
'dictionary'
or
'alphabet'
made
up
of
Test code
An example program is shown in the included solution. It implements a
compression/expansion program that uses some arbitrary models based on the
discussed examples.
The decoding method needs to know the length of the encoded string so as to know
when to stop decoding the message.
The test project encodes an arbitrarily defined input string, and writes it out to
a MemoryStream object. This is the return value of the Encode method of the class.
The MemoryStream object returned will contain an array of bytes with the
compressed data in binary.
One can then decode the stream by calling the Expand method of the class.
The Expand method of the class takes a memory stream and the length of the
encoded message as parameters. To test it, we can encode a string and then pass
the binary stream returned back to the Expand method for decoding.
During the encoding process, a routine called convert_int_to_symbol is called. This
routine gets a given input character, and converts it to a low count, high count, and
scale using the current model. Since our model is a set of fixed probabilities, this just
means looking up the probabilities in the input coder.symbol struct. Once those are
defined, the encoder can be called.
During the decoding process, there are two functions associated with modeling. In
order to determine what character is waiting to be decoded on the input stream, the
model needs to be interrogated to determine what the present scale is. In our
example, the scale (or range of counts) is fixed by the coder.scale property. When
decoding, a modeling function called convert_symbol_to_int is called. It takes the
given count, and determines what character matches the count. Finally, the decoder
is called again to process that character out of the input stream.