Московский государственный университет имени М.В.Ломоносова
Опубликован: 10.10.2007 | Доступ: свободный | Студентов: 1480 / 160 | Оценка: 4.36 / 4.18 | Длительность: 14:22:00
Специальности: Программист
Лекция 1:

Методы сжатия без потерь

Интервальное кодирование

В отличие от классического алгоритма, интервальное кодирование предполагает, что мы имеем дело с целыми дискретными величинами, которые могут принимать ограниченное число значений. Как уже было отмечено, начальный интервал в целочисленной арифметике записывается в виде [0,N) или [0,N-1], где N - число возможных значений переменной, используемой для хранения границ интервала.

Чтобы наиболее эффективно сжать данные, мы должны закодировать каждый символ s посредством - \log _2 (f_s ) битов, где f_s - частота символа s. Конечно, на практике такая точность недостижима, но мы можем для каждого символа s отвести в интервале диапазон значений [N(F_s ),N(F_s  + f_s )), где F_s - накопленная частота символов, предшествующих символу s в алфавите, N(f) - значение, соответствующее частоте f в интервале из N возможных значений. И, чем больше будет N(f_s ), тем точнее будет представлен символ s в интервале. Следует отметить, что для всех символов алфавита должно соблюдаться неравенство f_s  > 0.

Задачу увеличения размера интервала выполняет процедура, называемая нормализацией. Практика показывает, что можно отложить выполнение нормализации на некоторое время, пока размер интервала обеспечивает приемлемую точность. Микаэль Шиндлер (Michael Schindler) предложил в работе [1.3] рассматривать выходной поток как последовательность байтов, а не битов, что избавило от битовых операций и позволило производить нормализацию заметно реже. И чаще всего нормализация обходится без выполнения переноса, возникающего при сложении значений нижней границы интервала и размера интервала. В результате скорость кодирования возросла в полтора раза при крайне незначительной потери в степени сжатия (размер сжатого файла обычно увеличивается лишь на сотые доли процента).

Выходные данные арифметического кодера можно представить в виде четырех составляющих:

  1. Составляющая, записанная в выходной файл, которая уже не может измениться.
  2. Один элемент (бит или байт), который может быть изменен переносом, если последний возникнет при сложении значений нижней границы интервала и размера интервала.
  3. Блок элементов, имеющих максимальное значение, через которые по цепочке может пройти перенос.
  4. Текущее состояние кодера, представленное нижней границей интервала.

Пример представлен в табл. 1.4

Таблица 1.4.
Составляющая, записанная в файл Элемент, который может быть изменен переносом Блок элементов, имеющих максимальное значение Нижняя граница интервала
D7 03 56 E4 3A FF FF 35 38 B1
      +
Размер интервала     EA 12 1A
      =
Перенос 3B 00 00 1F 4A CB

При выполнении нормализации возможны следующие действия:

  1. Если интервал имеет приемлемый для обеспечения заданной точности размер, нормализация не нужна.
  2. Если при сложении значений нижней границы интервала и размера интервала не возникает переноса, составляющие 2 и 3 могут быть записаны в выходной файл без изменений.
  3. В случае возникновения переноса он выполняется в составляющих 2 и 3, после чего они также записываются в выходной файл.
  4. Если элемент, претендующий на запись в выходной файл, имеет максимальное значение (в случае бита - 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

Таблица 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:

Таблица 1.6.
символ symbol_freq prev_freq low range результат
      0 0x7FFFFFFF  
K 2 3 0x26666664 0x19999998  
О 3 0 0x26666664 0x051EB850  
В 2 5 0x28F5C28A 0x010624DC  
. 1 9 0x29E1B07C 0x001A36E2  
нормализация 0x61B07C00 0x1A36E200 0x53
К 2 3 0x698DC232 0x053E2ECC 0x53
О 3 0 0x698DC232 0x0192A7A3 0x53
Р 1 7 0x6AA79DEC 0x002843F6 0x53
нормализация 0x279DEC00 0x2843F600 0x53D5
О 3 0 0x279DEC00 0x0C146364 0x53D5
В 2 5 0x2DA81DAF 0x026A7A46 0x53D5
А 1 8 0x2F96E5E7 0x003DD907 0x53D5
нормализация 0x16E5E700 0x3DD90700 0x53D55F

Как уже было отмечено, чаще всего при нормализации не происходит переноса. Исходя из этого, Дмитрий Субботин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; 
  }
}
Упражнение: Применить интервальное кодирование без переноса для строки 'КОВ.КОРОВА'.