Методы контекстного моделирования
Пример реализации 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.