Методы сжатия без потерь
Интервальное кодирование
В отличие от классического алгоритма, интервальное кодирование предполагает, что мы
имеем дело с целыми дискретными величинами, которые могут принимать ограниченное число
значений. Как уже было отмечено, начальный интервал в целочисленной арифметике записывается
в виде
или
, где
- число возможных
значений переменной, используемой для хранения границ интервала.
Чтобы наиболее эффективно сжать данные, мы должны закодировать каждый символ
посредством
битов, где
- частота
символа
. Конечно, на практике такая точность недостижима, но мы можем
для каждого символа
отвести в интервале диапазон значений
, где
- накопленная частота символов,
предшествующих символу
в алфавите,
- значение,
соответствующее частоте
в интервале из
возможных значений.
И, чем больше будет
, тем точнее будет представлен символ
в интервале. Следует отметить, что для всех символов алфавита должно соблюдаться неравенство
.
Задачу увеличения размера интервала выполняет процедура, называемая нормализацией. Практика показывает, что можно отложить выполнение нормализации на некоторое время, пока размер интервала обеспечивает приемлемую точность. Микаэль Шиндлер (Michael Schindler) предложил в работе [1.3] рассматривать выходной поток как последовательность байтов, а не битов, что избавило от битовых операций и позволило производить нормализацию заметно реже. И чаще всего нормализация обходится без выполнения переноса, возникающего при сложении значений нижней границы интервала и размера интервала. В результате скорость кодирования возросла в полтора раза при крайне незначительной потери в степени сжатия (размер сжатого файла обычно увеличивается лишь на сотые доли процента).
Выходные данные арифметического кодера можно представить в виде четырех составляющих:
- Составляющая, записанная в выходной файл, которая уже не может измениться.
- Один элемент (бит или байт), который может быть изменен переносом, если последний возникнет при сложении значений нижней границы интервала и размера интервала.
- Блок элементов, имеющих максимальное значение, через которые по цепочке может пройти перенос.
- Текущее состояние кодера, представленное нижней границей интервала.
Пример представлен в табл. 1.4
При выполнении нормализации возможны следующие действия:
- Если интервал имеет приемлемый для обеспечения заданной точности размер, нормализация не нужна.
- Если при сложении значений нижней границы интервала и размера интервала не возникает переноса, составляющие 2 и 3 могут быть записаны в выходной файл без изменений.
- В случае возникновения переноса он выполняется в составляющих 2 и 3, после чего они также записываются в выходной файл.
- Если элемент, претендующий на запись в выходной файл, имеет максимальное значение (в случае бита - 1, в случае байта - 0xFF), то он может повлиять на предыдущий при возникновении переноса. Поэтому этот элемент записывается в блок, соответствующий третьей составляющей.
Ниже приведен исходный текст алгоритма, реализующего нормализацию для интервального кодирования [1.3].
// Максимальное значение, которое может принимать
// переменная. Для 32-разрядной арифметики
// CODEBITS = 31. Один бит отводится для
// определения факта переноса.
#define TOP (1<<CODEBITS)
// Минимальное значение, которое может принимать
// размер интервала. Если значение меньше,
// требуется нормализация
#define BOTTOM (TOP>>8)
// На сколько битов надо сдвинуть значение нижней
// границы интервала, чтобы остался один байт
#define SHIFTBITS (CODEBITS-8)
// Если для хранения значений используется 31 бит,
// каждый символ сдвинут на 1 байт вправо
// в выходном потоке, и при декодировании приходится
// его считывать в 2 этапа.
#define EXTRABITS ((CODEBITS-1)%8+1)
// Используемые глобальные переменные:
// next_char - символ, который может быть изменен
// переносом (составляющая 2).
// carry_counter - число символов, через которые
// может пройти перенос до символа next_char
// (составляющая 3).
// low - значение нижней границы интервала,
// начальное значение равно нулю.
// range - размер интервала,
// начальное значение равно TOP.
void encode_normalize( void ) {
while( range <= BOTTOM ) {
// перенос невозможен, поэтому возможна
// запись в выходной файл (ситуация 2)
if( low < 0xFF << SHIFTBITS ) {
output_byte( next_char );
for(;carry_counter;carry_counter--)
output_byte(0xFF);
next_char = low >> SHIFTBITS;
// возник перенос (ситуация 3)
} else if( low >= TOP ) {
output_byte( next_char+1 );
for(;carry_counter;carry_counter--)
output_byte(0x0);
next_char = low >> SHIFTBITS;
// элемент, который может повлиять на перенос
// (ситуация 4)
} else {
carry_counter++;
}
range <<= 8;
low = (low << 8) & (TOP-1);
}
}
void decode_normalize( void ) {
while( range <= BOTTOM ) {
range <<= 1;
low = low<<8 |
((next_char<<EXTRABITS) & 0xFF);
next_char = input_byte();
low |= next_char >> (8-EXTRABITS);
range <<= 8;
}
}
Пример
1.1.
Для сравнения приведем текст функции, оперирующей с битами, из работы [1.2]:
#define HALF (1<<(CODEBITS-1))
#define QUARTER (HALF>>1)
void bit_plus_follow( int bit ) {
output_bit( bit );
for(;carry_counter;carry_counter--)
output_bit(!bit);
}
void encode_normalize( void ) {
while( range <= QUARTER ) {
if( low >= HALF ) {
bit_plus_follow(1);
low -= HALF;
} else if( low + range <= HALF ) {
bit_plus_follow(0);
} else {
carry_counter++;
low -= QUARTER;
}
low <<= 1;
range <<= 1;
}
}
void decode_normalize( void ) {
while( range <= QUARTER ) {
range <<= 1;
low = low<<1 |input_bit();
}
}Процедура интервального кодирования очередного символа выглядит следующим образом:
void encode(
int symbol_freq, // частота кодируемого символа
int prev_freq, // накопленная частота символов,
// предшествующих кодируемому
// в алфавите
int total_freq // частота всех символов
) {
int r = range / total_freq;
low += r*prev_freq;
range = r*symbol_freq;
encode_normalize();
}Рассмотрим пример интервального кодирования строки "КОВ.КОРОВА". Частоты символов представлены в табл. 1.5
| Индекс | Символ | Symbol_freq | Prev_freq |
| 0 | О | 3 | 0 |
| 1 | К | 2 | 3 |
| 2 | В | 2 | 5 |
| 3 | Р | 1 | 7 |
| 4 | А | 1 | 8 |
| 5 | "" | 1 | 9 |
| Total_freq | 10 |
Для кодирования строки будем использовать функцию compress:
void compress(
DATAFILE *DataFile // файл исходных данных
) {
low = 0;
range = TOP;
next_char = 0;
carry_counter = 0;
while( !DataFile.EOF ()) {
c = DataFile.ReadSymbol() // очередной символ
encode( Symbol_freq[c], Prev_freq[c], 10 );
}
}В табл. 1.6 представлен результаты процесса кодирования функцией compress:
Как уже было отмечено, чаще всего при нормализации не происходит переноса. Исходя из этого, Дмитрий Субботин1Dmitry Subbotin. русский народный rangecoder// Сообщение в эхо-конференции FIDO RU.COMPRESS. 1 мая 1999 предложил отказаться от переноса вовсе. Оказалось, что потери в сжатии совсем незначительны, порядка нескольких байт. Впрочем, выигрыш по скорости тоже оказался не очень заметен. Главное достоинство такого подхода - в простоте и компактности кода. Вот как выглядит функция нормализации для 32-разрядной арифметики:
#define CODEBITS 24
#define TOP (1<<CODEBITS)
#define BOTTOM (TOP>>8)
#define BIGBYTE (0xFF<<(CODEBITS-8))
void encode_normalize( void ) {
while( range < BOTTOM ) {
if( low & BIGBYTE == BIGBYTE &&
range + (low & BOTTOM-1) >= BOTTOM )
range = BOTTOM - (low & BOTTOM-1);
output_byte(low>>24);
range<<=8;
low<<=8;
}
}Можно заметить, что избежать переноса нам позволяет своевременное принудительное уменьшение значение размера интервала. Оно происходит тогда, когда второй по старшинству байт low принимает значение 0xFF, а при добавлении к low значения размера интервала range возникает перенос. Так выглядит оптимизированная процедура нормализации:
void encode_normalize( void ) {
while((low ^ low+range)<TOP ||
range < BOTTOM &&
((range = -low & BOTTOM-1),1)) {
output_byte(low>>24);
range<<=8;
low<<=8;
}
}
void decode_normalize( void ) {
while((low ^ low+range)<TOP ||
range<BOTTOM &&
((range= -low & BOTTOM-1),1)) {
low = low<<8 | input_byte();
range<<=8;
}
}