Опубликован: 10.10.2007 | Уровень: профессионал | Доступ: платный | ВУЗ: Московский государственный университет имени М.В.Ломоносова
Лекция 3:

Методы контекстного моделирования

Пример реализации 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.