Методы контекстного моделирования
Пример реализации PPM-компрессора
Рассмотрим основные моменты реализации компрессора PPM для простейшего случая с порядком модели N = 1 без исключения символов. Будем также исходить из того, что статистическое кодирование выполняется арифметическим кодером.
При контекстном моделировании 1-го порядка нам не требуются сложные структуры данных, обеспечивающие эффективное хранение и доступ к информации отдельных КМ. Можно просто хранить описания КМ в одномерном массиве, размер которого равен количеству символов в алфавите входной последовательности, и находить нужную КМ, используя символ ее контекста как индекс. Мы используем байт-ориентированное моделирование, поэтому размер массива для контекстных моделей порядка 1 будет равен 256. Чтобы не плодить лишних сущностей, мы, во-первых, откажемся от КМ(-1) за счет соответствующей инициализации КМ(0), и, во-вторых, будем хранить КМ(0) в том же массиве, что и КМ(1). Считаем, что КМ(0) соответствует индекс 256.
В структуру контекстной модели ContextModel включим массив счетчиков count для всех возможных 256 символов. Для символа ухода введем в структуру КМ специальный счетчик esc, а также добавим поле TotFr, в котором будет содержаться сумма значений счетчиков всех обычных символов. Использование поля TotFr не обязательно, но позволит ускорить обработку данных.
С учетом сказанного структуры данных компрессора будут такими.
struct ContextModel{
int esc, TotFr;
int count[256];
};
ContextModel cm[257];Если размер типа int равен 4 байтам, то нам потребуется не менее 257 кбайт памяти для хранения модели.
Опишем стек, в котором будут храниться указатели на требующие модификации КМ, а также указатель стека SP и контекст context.
ContextModel *stack[2]; int SP, context [1]; //контекст вырождается в 1 символ
Больше никаких глобальных переменных и структур данных нам не нужно.
Инициализацию модели будем выполнять в общей для кодера и декодера функции init_model.
void init_model (void){
/*Так как cm является глобальной переменной, то значения
всех полей равны 0. Нам требуется только распределить
кодовое пространство в КМ(0) так, чтобы все символы,
включая символ ухода, всегда бы имели ненулевые оценки.
Пусть также символы будут равновероятными
*/
for ( int j = 0; j < 256; j++ )
cm[256].count[j] = 1;
cm[256].TotFr = 256;
/*Явно запишем, что в начале моделирования мы считаем
контекст равным 0. Число не имеет значения, лишь бы
кодер и декодер точно следовали принятым
соглашениям. Обратите на это внимание
*/
context [0] = 0;
SP = 0;
}Функции обновления модели также будут общими для кодера и декодера. В update_model производится инкремент счетчиков просмотренных КМ, а в rescale осуществляется масштабирование счетчиков. Необходимость масштабирования обусловлена особенностями типичных реализаций арифметического кодирования и заключается в делении значений счетчиков пополам при достижении суммы значений всех счетчиков TotFr+esc некоторого порога. Подробнее об этом рассказано в пункте "Обновление счетчиков символов".
const int MAX_TotFr = 0x3fff;
void rescale (ContextModel *CM){
CM->TotFr = 0;
for (int i = 0; i < 256; i++){
/*обеспечим отличие от нуля значения
счетчика после масштабирования
*/
CM->count[i] -= CM->count[i] >> 1;
CM->TotFr += CM->count[i];
}
}
void update_model (int c){
while (SP) {
SP--;
if ((stack[SP]->TotFr + stack[SP]->esc) >= MAX_TotFr)
rescale (stack[SP]);
if (!stack[SP]->count[c])
/*в этом контексте это новый символ, увеличим
счетчик уходов
*/
stack[SP]->esc += 1;
stack[SP]->count[c] += 1;
stack[SP]->TotFr += 1;
}
}Собственно кодер реализуем функцией encode. Эта функция управляет последовательностью действий при сжатии данных, вызывая вспомогательные процедуры в требуемом порядке, а также находит нужную КМ. Оценка текущего символа производится в функции encode_sym, которая передает результаты своей работы арифметическому кодеру.
int encode_sym (ContextModel *CM, int c){
// КМ потребует инкремента счетчиков, запомним ее
stack [SP++] = CM;
if (CM->count[c]){
/*счетчик сжимаемого символа не равен нулю, тогда
его можно оценить в текущей КМ; найдем
накопленную частоту предыдущего в массиве count
символа
*/
int CumFreqUnder = 0;
for (int i = 0; i < c; i++)
CumFreqUnder += CM->count[i];
/*передадим описание кодового пространства,
занимаемого символом c, арифметическому кодеру
*/
AC.encode (CumFreqUnder, CM->count[c], CM->TotFr + CM->esc);
return 1; // возвращаемся в encode с победой
}else{
/*нужно уходить на КМ(0);
если текущий контекст 1-го порядка встретился первый
раз, то заранее известно, что его КМ пуста (все
счетчики равны нулю), и кодировать уход не только не
имеет смысла, но и нельзя, т.к. TotFr+esc = 0
*/
if (CM->esc)
AC.encode (CM->TotFr, CM->esc, CM->TotFr + CM->esc);
return 0; // закодировать символ не удалось
}
}
void encode (void){
int c, // текущий символ
success; // успешность кодирования символа в КМ
init_model ();
AC.StartEncode (); // проинициализируем арифм. кодер
while (( c = DataFile.ReadSymbol() ) != EOF) {
// попробуем закодировать в КМ(1)
success = encode_sym (&cm[context[0]], c);
if (!success)
/*уходим на КМ(0), где любой символ получит
ненулевую оценку и будет закодирован
*/
encode_sym (&cm[256], c);
update_model (c);
context [0] = c; // сдвинем контекст
}
// закодируем знак конца файла символом ухода с КМ(0)
AC.encode (cm[context[0]].TotFr, cm[context[0]].esc,
cm[context[0]].TotFr + cm[context[0]].esc);
AC.encode (cm[256].TotFr, cm[256].esc,
cm[256].TotFr + cm[256].esc);
// завершим работу арифметического кодера
AC.FinishEncode();
}
Пример
3.2.
Реализация декодера выглядит аналогично. Внимания заслуживает разве что только процедура поиска символа по описанию его кодового пространства. Метод get_freq арифметического кодера возвращает число x, лежащее в диапазоне [CumFreqUnder, CumFreqUnder+CM->count[i]), т.е. CumFreqUnder <= x < CumFreqUnder+CM->count[i]. Поэтому искомым символом является i, для которого выполнится это условие.
int decode_sym (ContextModel *CM, int *c){
stack [SP++] = CM;
if (!CM->esc) return 0;
int cum_freq = AC.get_freq (CM->TotFr + CM->esc);
if (cum_freq < CM->TotFr){
/*символ был закодирован в этой КМ; найдем символ и
его точное кодовое пространство
*/
int CumFreqUnder = 0;
int i = 0;
for (;;){
if ( (CumFreqUnder + CM->count[i]) <= cum_freq)
CumFreqUnder += CM->count[i];
else break;
i++;
}
/*обновим состояние арифметического кодера на
основании точной накопленной частоты символа
*/
AC.decode_update (CumFreqUnder, CM->count[i], CM->TotFr + CM->esc);
*c = i;
return 1;
}else{
/*обновим состояние арифметического кодера на
основании точной накопленной частоты символа,
оказавшегося символом ухода
*/
AC.decode_update (CM->TotFr, CM->esc, CM->TotFr + CM->esc);
return 0;
}
}
void decode (void){
int c, success;
init_model ();
AC.StartDecode ();
for (;;){
success = decode_sym (&cm[context[0]], &c);
if (!success){
success = decode_sym (&cm[256], &c);
if (!success) break; //признак конца файла
}
update_model (c);
context [0] = c;
DataFile.WriteSymbol (c);
}
}
Пример
3.3.
Характеристики созданного компрессора, названного Dummy, приведены в пункте "Производительность на тестовом наборе Calgary Compression Corpus". Полный текст реализации Dummy оформлен в виде приложения 1.