Как я скрестил JPEG, GIF и получил VP9

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Disclaimer: статья эта написана по просьбе почтеннейшей публики. Сам я не хотел выкладывать результаты, которые недостаточно «отполированы» для передачи их широкому кругу, и вот почему.

Неверная технология может увлечь людей по неверному пути, принеся вред, превышающий её пользу. Если пользоваться аналогиями, то это поданное нищему яблочко, оказавшееся ядовитым и убившее его. Бесплатность никогда не бывает оправданием для плохого софта, в конце концов, то яблоко тоже было бесплатным. «Ядовитые решения» не только бьют по пользователю, но и обесценивают труд остальных людей, вкладывавших его в тот же проект (зачастую они работают на энтузиазме, а их награда — востребованность решения; более распространённый случай, когда человек выкладывает решение бесплатно, чтобы кто‑то в него что‑то добавил в его интересах, впрочем, тоже находится под ударом, потому что для этого всё‑таки необходима популярность, а «отравивший» его проект мимопроходил этой популярности мешает). Сейчас, пока я это набираю, я смотрю на пятую попытку поставить Firefox через новенький‑моднявенький Snap на слабом, постоянно рвущемся соединении, и мне очень хочется, чтобы апофигеты Snap'а оказались сейчас передо мной, привязанными к дереву.

Сделав двухмерную версию JPEG и глянув его обычным вьюером, я обнаружил дичайшие артефакты. Как оказалось, я неверно выбрал знаменатель, к которому нужно приводить веса гармоник. Оно вроде бы как работает — но стандарт работает «в миллиметрах», а моё поделие — «в дюймах». Если всё нижеперечисленное расползётся по проектам и приведёт к появлению двух несовместимых систем кодирования — я никогда не прощу себе, что пошёл на поводу у этой кучи плюсиков под комментариями и выложил то, что выкладываю сейчас. Пожалуйста, не подводите старпёра и добейтесь максимального реюза существующих стандартов, и не только его. «Дьявол кроется в мелочах», убить проект — проще простого, а ещё проще сделать его «ядовитым». Ну, и психологический момент — теперь я чувствую, что переложил работу на плечи желающих продолжить и дальше как будто бы можно уже и не продолжать... это тоже некоторое время сдерживало меня от написания этой статьи.

Я себе вижу продолжение как‑то так:

  1. Обкорнать третье измерение и сделать полностью совместимый со стандартом компрессор JPEG.

  2. Посмотреть, что в нём сделано не так, как сделано у меня.

  3. Подумать, чем руководствовался Joint Photographic Experts Group и осознанно понять, что из этого относится к 3D‑JPEG, а что — таки нет.

  4. Правильно выбрать способ хранения гармоник. Вероятно, учесть вылезшие за годы недостатки JPEG. Например (ни к чему не подталкиваю!) — сначала хранить все DC‑компоненты, а затем — гармоники. Если не успели подгрузить гармоники — перескакиваем на следующий чанк (длина известна из его тэга, потому что в GIF каждый чанк данных предваряется тэгом), на экране мелькнут артефакты 8×8, но это лучше, чем ничего. Повторюсь, это только «мысли вслух» (это было бы круто с JPEG «по умолчанию», без специальных костылей в виде «Progressive JPEG», но не факт, что будет круто в 3D‑JPEG).

  5. Правильно выбрать варианты хранения цветности. Понижать ли разрешение по оси времени? Что при этом делать с наличием двух чанков яркости на чанк цветности? А может быть, хранить в одном чанке больше 8 кадров? А как в JPEG стандартизирован канал яркости — как среднее арифметическое или как визуально корректное чёрно‑белое (с учётом чувствительности глаза)?

  6. Правильно выбрать расширения для GIF (в том числе кастомные номера для тэгов, которыми мы снабдим чанки 3D‑JPEG). Я себе это вижу как чанк таблицы Хаффмана, чанк таблицы квантования и чанки, собственно, самих данных, в тэгах которых мы не забыли упомянуть, сколько миллисекунд на кадр у нас отводится (может быть, взять совместимую с AVI простую дробь?) Если характер данных резко поменялся — снова вставляем чанки Хаффмана и/или квантования, дальше чанки данных идут соответствующие (ну, как палитру на лету меняют в обычном AniGIF).

  7. JPEG умеет работать с 10 битами на канал, если я не ошибаюсь. Одноканальный 10-битный — это уже, в принципе, вещь относительно самодостаточная, потому что палитру из 1024 цветов уже реально сделать «непрерывно перетекающей между тонами» (в 256 мне это впихнуть не удалось). Да, только для ограниченного круга рисованной анимации с определёнными стилями, но реально. А тэг для чанка с палитрой у GIF и так уже есть готовенький.

  8. А тащить ли вообще в GIF такую часть JPEG, как сжатие Хаффмана? У GIF уже есть своё, LZW. Как оно себя покажет на квантованных коэффициентах, в основном сводящихся к нулям?

  9. Этот пункт я в любом случае сам бы за всю жизнь не сделал, поэтому и не собирался изначально: подобрать оптимальную таблицу квантования! К счастью, она, в лучших традициях JPEG, прямо в файле и хранится. Тут мы не делаем ничего необратимого — всегда можно улучшать сложившиеся на практике таблицы ещё и ещё.

И это только касательно GIF, к которому математика готова «здесь и сейчас». «Взрослые» кодеки могут потребовать более высокой скорости распаковки — ну, а GIF обычно имеет невысокое разрешение и предназначен для компактных, коротких видео без звука, а особенность 3D‑JPEG как раз в том, что чем выше степень сжатия, тем меньше нагрузка на процессор при распаковке (то есть компактные, сильно пережатые «анигифки» идеально подходят).

Теперь о цифрах. Я тестировал алгоритм на чёрно‑белом (о цвете вообще пока не думал) видео в 584 кадра 640×360 каждый (вполне нормальный размер для анигифки).

Сырой размер: 134 553 600.

Размер в 7-zip: 4 533 514.

Хаффман, конечно, сожмёт не так сильно, но, думаю, разительных отличий, стоящих того, чтобы LZMA туда тащить — мы не встретим. Опять же, мы ещё не видели в деле LZW.

Характер видео: различные движения как кадра целиком, так и отдельных предметов в кадре.

Таблица квантования: от фонаря, то есть доработанная напильником традиционная 2D.

Почему же столь примитивная вещь, как «MJPEG на стероидах», наступает на пятки «взрослым» кодекам? А дело в том, что косинусное преобразование по шкале времени позволяет реализовать самую сильную сторону таких кодеков, то есть выделение движущихся областей и передачу их из кадра в кадр с сохранением только тех небольших изменений, которые эти области претерпевают, плюс самого факта их движения. Основное содержимое таблицы коэффициентов по шкале времени — нули. Но только без интеллектуального анализа, всё получается «само собой», самыми примитивными средствами. Что же касается артефактов, которыми приходится платить за такое сжатие — я выбрал кадры, где движется и фон, и объект, и собрал из них две обычные гифки чудовищного веса (на одной было нагляднее, но не влезло в хабралимит). На них всё можно оценить «как есть», потому что применительно к монохромным 256-цветным изображениям обычный GIF ведь, по сути, lossless‑формат!

Боже мой, сержант, эти гифки ползут прямо на нас! Нас сейчас раздавит!!!
Оригинал
Оригинал
3D-JPEG
3D-JPEG

Ну, а теперь код, которым это было получено.

Я обещаю прочитать спецификации GIF и JPEG и подумать на шаг вперёд прежде, чем что-то делать «для широкой публики» на основе этого кода!
//Собирается OpenWatcom 1.9 как Win32 текстовое консольное приложение.
#include <fstream.h>
#include <math.h>
#include <malloc.h>

#define max(a,b)  (((a) > (b)) ? (a) : (b))
#define min(a,b)  (((a) < (b)) ? (a) : (b))

//double Stat2D[8][8]={0}, Stat3D[8][8][8]={0};
//int Count2D=0, Count3D=0;

double CosFreq[8][8] = {
	1, 1, 1, 1, 1, 1, 1, 1, 
	0.980785, 0.83147, 0.55557, 0.19509, -0.19509, -0.55557, -0.83147, -0.980785, 
	0.92388, 0.382683, -0.382683, -0.92388, -0.92388, -0.382683, 0.382683, 0.92388, 
	0.83147, -0.19509, -0.980785, -0.55557, 0.55557, 0.980785, 0.19509, -0.83147, 
	0.707107, -0.707107, -0.707107, 0.707107, 0.707107, -0.707107, -0.707107, 0.707107, 
	0.55557, -0.980785, 0.19509, 0.83147, -0.83147, -0.19509, 0.980785, -0.55557, 
	0.382683, -0.92388, 0.92388, -0.382683, -0.382683, 0.92388, -0.92388, 0.382683, 
	0.19509, -0.55557, 0.83147, -0.980785, 0.980785, -0.83147, 0.55557, -0.19509 };

double AlphaNorm[8] = {1.4142135623730950488016887242097, 1, 1, 1, 1, 1, 1, 1};

static double PreCalcCoeffs[512][512];

double round (double FuckinWatcomLibrary)
{
	if (FuckinWatcomLibrary<0) return ceil (FuckinWatcomLibrary - .5);
	return floor (FuckinWatcomLibrary + .5);
}

char* ReadBMP (char *Name, int *XSize, int *YSize, void *Colormap, int Restrictions)
{
	#pragma pack(push)
	#pragma pack(0)
		struct
		{
			short Id;
			long FSize, Ver, DataOffset, Unused, Width, Height;
			short WTF, BPP;
			long Compr; //0 for uncompressed, 1,2 for RLE
			long DataSize,   XRes, YRes, PalSize, Unused2;
		//	char Palette[256][4];
			char Palette[2][4];
		} BmpHeader;
	#pragma pack(pop)
	fstream f;
	char *Raw, *P;
	int line, netto, brutto;

	f.open (Name, ios::in|ios::binary);
	if (f.fail()) return NULL;
	f.read ((char*)&BmpHeader, sizeof (BmpHeader));
	if (f.fail()) {f.close(); return NULL;}
	if (BmpHeader.Id!=0x4D42) {f.close(); return NULL;}	//Wrong BMP id!
	if (BmpHeader.Compr) {f.close(); return NULL;}	//Compressed files are not supported
	*XSize = BmpHeader.Width;
	*YSize = BmpHeader.Height;
	f.seekg(BmpHeader.DataOffset);

	switch (BmpHeader.BPP)
	{
		case 24:
			netto = BmpHeader.Width*3;
			brutto = (netto + 3) & ~3;
			Raw = (char*)malloc(BmpHeader.Height*netto);
			if (!Raw) {f.close(); return NULL;}
			for (line=0,P=Raw; line<BmpHeader.Height; line++,P+=netto)
			{
				f.read (P, netto);
				if (f.fail()) {f.close(); free(Raw); return NULL;}
				f.seekg (brutto-netto, ios::cur);
			}
		break;

		default:
			f.close();
		return NULL;	//ToDo
	}

	f.close();
	return Raw;
}
/*
void Get2DStat (double Flat[8][8])
{
	int u, v, x, y;
	double DCT[8][8];
	for (v=0; v<8; v++) for (u=0; u<8; u++)
	{
		DCT[v][u]=0;
		for (y=0; y<8; y++) for (x=0; x<8; x++) DCT[v][u]+=CosFreq[u][x] * CosFreq[v][y] * (Flat[y][x] - 128.0);
		DCT[v][u]/=4.0;	// *=sqrt(2/8)*sqrt(2/8)
		if (!u) DCT[v][u]/=sqrt(2.0);
		if (!v) DCT[v][u]/=sqrt(2.0);
	}

	for (v=0; v<8; v++) for (u=0; u<8; u++) Stat2D[v][u]+=fabs(DCT[v][u]);
	Count2D++;
}
*/
void CompressCube (double Cube[8][8][8])
{
	int u, v, w, x, y, z;
	double DCT[8][8][8];
	for (z=0; z<8; z++) for (y=0; y<8; y++) for (x=0; x<8; x++) Cube[z][y][x]-=128;
	for (w=0; w<8; w++) for (v=0; v<8; v++) for (u=0; u<8; u++)
	{
		DCT[w][v][u]=0;
//		for (z=0; z<8; z++) for (y=0; y<8; y++) for (x=0; x<8; x++) DCT[w][v][u]+=cos( (2*x+1)*u*3.1415926536/16.0 ) * cos( (2*y+1)*v*3.1415926536/16.0 ) * cos( (2*z+1)*w*3.1415926536/16.0 ) * Cube[z][y][x];
		for (z=0; z<8; z++) for (y=0; y<8; y++) for (x=0; x<8; x++) DCT[w][v][u]+=CosFreq[u][x] * CosFreq[v][y] * CosFreq[w][z] * Cube[z][y][x];
		DCT[w][v][u]/=8.0;	// *=sqrt(2/8)*sqrt(2/8)*sqrt(2/8)
		if (!u) DCT[w][v][u]/=sqrt(2.0);
		if (!v) DCT[w][v][u]/=sqrt(2.0);
		if (!w) DCT[w][v][u]/=sqrt(2.0);
	}

	memcpy (Cube, DCT, sizeof(double)*8*8*8);
}

void ExpandCube (double DCT[8][8][8])
{
	int u, v, w, x, y, z;
	double Cube[8][8][8];
	for (z=0; z<8; z++) for (y=0; y<8; y++) for (x=0; x<8; x++)
	{
		Cube[z][y][x]=0;
		for (w=0; w<8; w++) for (v=0; v<8; v++) for (u=0; u<8; u++) Cube[z][y][x] += CosFreq[u][x] * CosFreq[v][y] * CosFreq[w][z] * DCT[w][v][u] / AlphaNorm[u] / AlphaNorm[v] / AlphaNorm[w];
		Cube[z][y][x]/=8.0;	// *=sqrt(2/8)*sqrt(2/8)*sqrt(2/8)
	}

	for (z=0; z<8; z++) for (y=0; y<8; y++) for (x=0; x<8; x++) DCT[z][y][x]=min(255.0,max(Cube[z][y][x]+128,0.0));
}

void InitFastExpand()
{
	int u, v, w, x, y, z;
	for (z=0; z<8; z++) for (y=0; y<8; y++) for (x=0; x<8; x++)
		for (w=0; w<8; w++) for (v=0; v<8; v++) for (u=0; u<8; u++) PreCalcCoeffs[w*64+v*8+u][z*64+y*8+x] = CosFreq[u][x] * CosFreq[v][y] * CosFreq[w][z] / AlphaNorm[u] / AlphaNorm[v] / AlphaNorm[w] / 8.0;
}

void ExpandCubeFast (double DCT[512])
{
	int Coeff, Pixel;
	double Cube[512] = {0};
	
	for (Coeff=0; Coeff<512; Coeff++)
		if (DCT[Coeff])
			for (Pixel=0; Pixel<512; Pixel++)
				Cube[Pixel] += PreCalcCoeffs[Coeff][Pixel] * DCT[Coeff];	//Still a bottleneck!!!

	for (Pixel=0; Pixel<512; Pixel++) DCT[Pixel]=min(255.0,max(Cube[Pixel]+128,0.0));
}

void main (void)
{
	int FrNum, XSize, YSize;
	char Name[]="FramXXXX.bmp";
	char OutName[]="TestXXXX.raw";
	char *Frame[8]={0};
	int x, y, z, xx, yy;
	double CoeffsQ2D[8][8]={
		16, 11, 10, 16, 24, 40, 51, 61,
		12, 12, 14, 19, 26, 58, 60, 55,
		14, 13, 16, 24, 40, 57, 69, 56,
		14, 17, 22, 29, 51, 87, 80, 62,
		18, 22, 37, 56, 68,109,103, 77,
		24, 35, 55, 64, 81,104,113, 92,
		49, 64, 78, 87,103,121,120,101,
		72, 92, 95, 98,112,100,103, 99},
	CoeffFactor,	//pityful attempts (until I have no actual Coeffs3D).
	Pixels3D[8][8][8],
	Waves3D[8][8][8];
	fstream Log;
	Log.open ("Log.hex",ios::out|ios::binary);

for (int i=0; i<64; i++) CoeffsQ2D[0][i]*=2;

	InitFastExpand();

	for (FrNum=0;FrNum<10000;FrNum++)
	{
		Name[4]='0'+FrNum/1000;
		Name[5]='0'+FrNum/100%10;
		Name[6]='0'+FrNum/10%10;
		Name[7]='0'+FrNum%10;

		cout<<"Reading "<<Name<<endl;

		Frame[FrNum%8]=ReadBMP(Name, &XSize, &YSize, NULL, 0/*BMP_OPEN_RGB_ONLY*/);
		if (!Frame[FrNum%8]) {cout<<"Couldn't open!"<<endl; break;}	//No more BMPs!
		if (XSize%8 || YSize%8) break;	//Wrong size!
		if (FrNum%8 && (xx!=XSize || yy!=YSize)) break;	//Sizes differ from first!
		xx=XSize;
		yy=YSize;

		if (FrNum%8 == 7)
		{
			cout<<"Encoding eight "<<XSize<<" x "<<YSize<<endl;
			for (yy=0; yy<YSize; yy+=8) for (xx=0; xx<XSize; xx+=8)
			{				
				for (z=0; z<8; z++) for (y=0; y<8; y++) for (x=0; x<8; x++) Pixels3D[z][y][x]=(Frame[z][ (yy+y)*XSize*3 + (xx+x)*3 ]+Frame[z][ (yy+y)*XSize*3 + (xx+x)*3 + 1]+Frame[z][ (yy+y)*XSize*3 + (xx+x)*3 + 2])/3.0;
//				for (z=0; z<8; z++) Get2DStat(Pixels3D[z]);
				CompressCube(Pixels3D);

//				for (z=0; z<8; z++) for (y=0; y<8; y++) for (x=0; x<8; x++) Stat3D[z][y][x]+=fabs(Pixels3D[z][y][x]);
//				Count3D++;

				for (z=0,CoeffFactor=1; z<8; z++,CoeffFactor*=1.1) for (y=0; y<8; y++) for (x=0; x<8; x++) Pixels3D[z][y][x] = round(Pixels3D[z][y][x]/(CoeffsQ2D[y][x]*CoeffFactor))+128.001;
				for (z=0; z<8; z++) for (y=0; y<8; y++) for (x=0; x<8; x++) if ((int)(Pixels3D[z][y][x])<0 || (int)(Pixels3D[z][y][x])>255) cout<<"Quant. error: "<<Pixels3D[z][y][x]<<" is out of range!"<<endl;
				for (z=0; z<8; z++) for (y=0; y<8; y++) for (x=0; x<8; x++) Frame[z][ (yy+y)*XSize*3 + (xx+x)*3 ] = Frame[z][ (yy+y)*XSize*3 + (xx+x)*3 + 1] = Frame[z][ (yy+y)*XSize*3 + (xx+x)*3 + 2] = Pixels3D[z][y][x];
			}
		
			cout<<"Saving integer data"<<endl;
			for (yy=0; yy<YSize; yy+=8) for (xx=0; xx<XSize; xx+=8)
			{				
				for (z=0; z<8; z++) for (y=0; y<8; y++) for (x=0; x<8; x++) Log.write (&(Frame[z][ (yy+y)*XSize*3 + (xx+x)*3 ]), 1);
			}
		
			cout<<"Decoding"<<endl;
			for (yy=0; yy<YSize; yy+=8) for (xx=0; xx<XSize; xx+=8)
			{
				for (z=0; z<8; z++) for (y=0; y<8; y++) for (x=0; x<8; x++) Pixels3D[z][y][x]=(double)( Frame[z][ (yy+y)*XSize*3 + (xx+x)*3 ] )-128.0;
				for (z=0,CoeffFactor=1; z<8; z++,CoeffFactor*=1.1) for (y=0; y<8; y++) for (x=0; x<8; x++) Pixels3D[z][y][x] *= CoeffsQ2D[y][x]*CoeffFactor;
//				ExpandCube(Pixels3D);
				ExpandCubeFast(Pixels3D[0][0]);
				for (z=0; z<8; z++) for (y=0; y<8; y++) for (x=0; x<8; x++) Frame[z][ (yy+y)*XSize*3 + (xx+x)*3 ] = Frame[z][ (yy+y)*XSize*3 + (xx+x)*3 + 1] = Frame[z][ (yy+y)*XSize*3 + (xx+x)*3 + 2] = Pixels3D[z][y][x];
			}

			for (z=0; z<8; z++)
			{
				OutName[4]='0'+((FrNum&~7)+z)/1000;
				OutName[5]='0'+((FrNum&~7)+z)/100%10;
				OutName[6]='0'+((FrNum&~7)+z)/10%10;
				OutName[7]='0'+((FrNum&~7)+z)%10;

				cout<<"Writing "<<OutName<<endl;

				fstream f;
				f.open(OutName, ios::out|ios::binary);
				f.write(Frame[z],XSize*YSize*3);
				f.close();
				
				free (Frame[z]);
			}
/*
			cout<<"Gathered so far (2D):"<<endl;
			for (y=0; y<8; y++,cout<<endl) for (x=0; x<8; x++) cout<<Stat2D[y][x]/Count2D<<"	";
			cout<<"Gathered so far (3D):"<<endl;
			for (z=0; z<8; z++,cout<<endl) for (y=0; y<8; y++,cout<<endl) for (x=0; x<8; x++) cout<<Stat3D[z][y][x]/Count3D<<"	";
*/
		}
	}

	Log.close();
}

Также напоминаю, что в помощи народа всё ещё нуждается вот этот проект. Нужно найти хорошую таблицу частотности букв и буквосочетаний и, главное, большим количеством CUDA-карт оптимизировать цепочку — по косвенным признакам, я к оптимуму и на пушечный выстрел не подошёл.

Источник: https://habr.com/ru/articles/819155/


Интересные статьи

Интересные статьи

Управление по санитарному надзору за качеством пищевых продуктов и медикаментов США (FDA) разрешило компании Neuralink Илона Маска имплантировать мозговой чип ещё одному пациенту. Для этого компания п...
По информации СМИ, американская компания Microsoft не планирует ликвидировать свои три юридических лица в России, хотя в 2023 году эти компании получили суммарную выручку в 217 млн рублей. В 2022 году...
С февраля 2022 года клиника пересадки волос HFE, как и многие другие, столкнулась с падением количества заявок, обусловленным временным снижением спроса на фоне политических событий. Конверсия в прове...
Пока под Бобби Котиком шатается стул, Activision Blizzard мотает в разные стороны, хочется вспомнить еще один интересный и неприятный эпизод, связанный с этой компанией. Это практически рейдерский зах...
Группе учёных из Института Хюбрехта в Нидерландах удалось вырастить из стволовых клеток слёзные железы и заставить их плакать. Слёзные железы являются частью обширного слёзного аппара...