Пролог


Доброго времени суток! Однажды ко мне на работу пришёл друг, и я ему показал свой свеженаписанный шейдер, на тот момент это была первый серьёзный опыт с ними. Данная микропрограмма преобразовывала изображение с камеры в изображение вязаной кофты.



На момент начала разработки изображение бралось с камеры телефона и выводилось в вязанном виде, эффект был необычным. И друг подкинул идею сделать фильтр для камеры в виде квадратичного дерева.



В общем с этого момента я сел за изучения методов быстрого построения квадратичного дерева, и остановился на идеи HistoPyramid.

Реализация


Изначально нужно подготовить входные параметры для шейдера. Я использовал Unity3D, так что примеры кода на C#.

Процесс работы алгоритма разбит на два этапа:

  1. Построение дерева
  2. а. Подготовка параметров шейдера
    б. Реализация шейдера
  3. Создание изображения из дерева

Построение дерева


Подготовка параметров шейдера


Все расчёты будут вестись на условиях, что максимальный размер квадрата должен быть равен 256 пикселям.

int quadSize = 256;

Изначально приведу пример построенного дерева:



Количество уровней в дереве высчитывается следующим образом:

float levelsAmount = Mathf.Log(quadSize, 2f);

Каждый следующий уровень в два раза меньше предыдущего. Так как элементы дерева будут расположены справа, то размер изображения дерева будет высчитываться следующим:

Vector2 histoSize = new Vector2((float)quadSize + (float)quadSize / 2f, (float)quadSize);

В параметры шейдера потребуется передать координаты начала блока, координаты конца блока и размер блока. Так же потребуются такие же параметры для родительского блока, т.е. блока большего чем текущий.

float _cellSize = Mathf.Pow (2f, levelsLog - (float)currentLevel);
Vector2 size = new Vector2 (_cellSize / histoSize.x, _cellSize / histoSize.y);
Vector2 start = new Vector2 (0.6666666667f, size.y);
Vector2 end = new Vector2 (0.6666666667f + size.x, 2f *size.y);

builderMaterial [i].SetVector ("_startBlock", start);
builderMaterial [i].SetVector ("_endBlock", end);
builderMaterial [i].SetVector ("_sizeBlock", size);

Указанный кусок кода рассчитывает координаты начала, конца и размер блока, при условии что currentLevel больше 0, т.е. все элементы дерева, которые находится справа от исходного изображения. От этого в координатах начала и конца стоят «магические цифры» 0.6666666667f (2/3 занимает исходное изображение). _cellSize будет хранить размер блока в пикселях, после чего, для начала и конца потребуется перевести в относительные единицы.

if (i == 1) {
	parentStart = Vector4.zero;
	parentSize = new Vector4 (0.6666666667f, 1f, 0, 0);
}
else {
	_cellSize = Mathf.Pow (2f, levelsLog - (float)currentLevel + 1f);
	parentSize = new Vector4 (_cellSize / histoSize.x, _cellSize / histoSize.y, 0, 0);
	parentStart = new Vector4 (0.6666666667f, parentSize.y, 0, 0);
}

В случае с родительским блоком всё идентично.

Реализация шейдера


Шейдер будет запускаться levelsAmount раз. Для каждого из уровня дерева будут передаваться параметры, соответсвующие текущему уровню.

Параметрами на вход шейдера следующие:

Properties
{	
 	_MainTex ("Base (RGB)", 2D) = "white" {}
 	_BaseStep ("Pixel size parent block", Vector) = (0, 0, 0, 0)
 	_Level ("Current level", Float) = 1
	 	
 	_startBlock («Start current block", Vector) = (0, 0, 0, 0)
 	_endBlock («End current block", Vector) = (0, 0, 0, 0)
 	_sizeBlock ("Size current block", Vector) = (0, 0, 0, 0)
	 	
 	_parentEndBlock ("Start parent block", Vector) = (0, 0, 0, 0)
 	_parentSizeBlock ("Size parent block", Vector) = (0, 0, 0, 0)
}

  • _MainTex — исходная текстура, в нашем случае размером 256х256 пикселей;
  • _BaseStep — относительный размер пикселя (1/histoSize);
  • _Level — текущий уровень дерева;
  • _startBlock — начало текущего блока;
  • _endBlock — конец текущего блока;
  • _sizeBlock — размер текущего блока;
  • _parentEndBlock — конец родительского блока;
  • _parentSizeBlock — размер родительского блока;

Исходное изображение передающееся в _MainTex:



Задачей данного шейдера состоит в следующем:

  • Определить, попадают ли координаты обработки изображения в текущий блок
  • Найти среднее значение из 4х родительских пикселей и высчитать разницу цвета относительно родительский и нового цвета
  • Если мы не попали в текущий блок, возвращаем цвет, который уже есть на текстуре

Для нулевого уровня текстура создастся следующим образом:

if (_Level == 0) {
	if (vo.uv.x < _sizeBlock.x) {
		float2 uv = float2(vo.uv.x / _sizeBlock.x, vo.uv.y);
		return tex2D(_MainTex, uv);
	}
	else {
		return fixed4(0,0,0,1);
	}
}

Пропорционально конечному размеру, мы просто вставляем её и остальные пиксели красим в чёрный цвет.

Для остальных уровней необходимо проверить попадание в текущий блок:

if (vo.uv.y >= _startBlock.y && vo.uv.y <= _endBlock.y && vo.uv.x >= _startBlock.x && vo.uv.x <= _endBlock.x) {

Для упрощения вычислений, отличия цвета от исходного будем считать только на первом уровне. Относительные координаты блока рассчитаем следующим образом:

float2 uv = (vo.uv - _startBlock)/_szeBlock;
float2 suv = float2(0.6666666667 * uv.x, uv.y);

Теперь необходимо взять четыре цвета родительского блока и вычислить среднее значение плюс ошибку.

fixed4 col1 = tex2D(_MainTex, suv);
fixed4 col2 = tex2D(_MainTex, float2(min(suv.x + delta.x, _endBlock.x), suv.y));
fixed4 col3 = tex2D(_MainTex, float2(suv.x, min(suv.y - delta.y, _startBlock.y)));
fixed4 col4 = tex2D(_MainTex, float2(suv.x, max(suv.y - delta.y, _startBlock.y)));
col = (col1 + col2 + col3 + col4)/4;

Ошибка вычисляется простым способом: если пиксели отличаются по цвету, то будет добавлять (1/n) к ошибке, где n — количество комбинаций сравнения цветов.

Приведу в пример одну комбинацию:

float rgbError = 0;
if (abs(DecodeFloatRGBA(col2) - DecodeFloatRGBA(col1)) > 0) {
	rgbError += 0.1666666667;
}

Результатом функции получается среднее значение цвета пикселя, где альфа-канал — это ошибка. Это главное упрощение в моем алгоритме, так как на расчёт отклонения цвета через гистограмму меня не хватило.

return fixed4(col.rgb, errorColor);

На остальных уровнях принцип схож, только ошибка будет считаться из суммы пикселей родительского блока.

float errorColor = min((col1.a + col2.a + col3.a + col4.a), 1.0);

В итоге, пройдясь по всем уровням, наше дерево будет сформировано. Следующим этапом следует вывод дерева на экран, масштабируя его под размер экрана.

Создание изображение из дерева


Задачей данного шейдера следующая: относительно каждого уровня определить ошибку, и если она не превышает заданный уровень, отрисовать необходимый уровень с цветом пикселя в дереве. Параметрами шейдера:

Properties
{	
	_MainTex ("Base (RGB)", 2D) = "white" {}
	_TileTex ("Tile (RGB)", 2D) = "white" {}
}

  • _MainTex — текстура созданного дерева;
  • _TileTex — текстура с уровнями дерева, для изменения стилистики визуализации;

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

Основная функция вывода изображения выглядит следующим образом:

fixed4 getLevelColor(float level, float2 uv, float2 cellSize, float outSize) {
	float2 suv = float2(0.6666666667 + uv.x*cellSize.x,  cellSize.y+uv.y*cellSize.y);

	fixed4 color = tex2D(_MainTex, suv);

	if (color.a > 0.59-(0.08*(9-level))) {
		return fixed4(0,0,0,0);
	}
				
	float2 scaledUV = frac(uv/outSize);
	float2 patternUV = float2(2*scaledUV.x*outSize, outSize+scaledUV.y*outSize);
				
	fixed4 tileColor = tex2D(_TileTex, patternUV) * fixed4(color.rgb, 1.0);
	fixed4 outcolor = lerp(tileColor, fixed4(0,0,0,1), 1 - tileColor.a);
	return fixed4(outcolor.rgb, 1);
}

Входные параметры:

  • cellSize — относительный размер блока;
  • outSize — относительный размер на конечном изображении;

Идея следующая, для каждого пикселя конечного изображения необходимо проходит от самого маленького блока дерева к большому (снизу вверх).

Если ошибка не больше заданного значения (threshold):

if (color.a > 0.59-(0.08*(9-level))) // Прошу прощения за магические наборы цифр.

То выводим нужное изображение с текстуры _TileTex.

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

fixed4 value = float4(0,0,0,0);

value = getLevelColor(7, vo.uv, float2(0.005208333333, 0.0078125), 0.5);
if (value.a > 0) { return value; }
		  		
value = getLevelColor(6, vo.uv, float2(0.01041666667, 0.015625), 0.25);
if (value.a > 0) { return value; }
		  		
value = getLevelColor(5, vo.uv, float2(0.02083333333, 0.03125), 0.125);
if (value.a > 0) { return value; }
		  		
value = getLevelColor(4, vo.uv, float2(0.04166666667, 0.0625), 0.0625);
if (value.a > 0) { return value; }
		  		
value = getLevelColor(3, vo.uv, float2(0.08333333333, 0.125), 0.03125);
if (value.a > 0) { return value; }
		  		
value = getLevelColor(2, vo.uv, float2(0.1666666667, 0.25), 0.015625);
if (value.a > 0) { return value; }
		  		
value = getLevelColor(1, vo.uv, float2(0.3333333333, 0.5), 0.0078125);
if (value.a > 0) { return value; }

return value;

На выходе получим желанный результат:



Эпилог


В итоге мы получили шейдер работающий на слабых мобильных устройствах с частотой кадров 25-30 и больше. В данной статье я затронул обработку изображения с «чистыми» цветами, но можно пойти дальше, и сделать расчёт ошибки отклонения цвета боле сложной, тогда можно использовать данный скрипт в «шумных» изображениях со сложной текстурой.

Пример (с 44 секунды):



Хотел извиниться за множество магических чисел в статье. Спасибо за внимание!

Ссылки на используемые материалы:

  • Effcient region segmentation on compressed gray images using quadtree and shading representation. Kuo-Liang Chunga, Hsu-Lien Huanga, Hsueh-I Lu
  • RealTime QuadTree Analysis using HistoPyramids. Gernot Ziegler, Rouslan Dimitrovb, Christian Theobalta, Hans-Peter Seidela
    a Max-Planck-Institut fur Informatik, Stuhlsatzenhausweg 85, D-66123 Saarbrucken, Germany. bInternational University Bremen GmbH, Postfach 750561, D-28725 Bremen, Germany
  • HistoPyramid stream compaction and expansion. Christopher Dyken and Gernot Ziegler
  • Quadtrees: Implementation. Herman Tulleken

Комментарии (12)


  1. gonzzza
    01.04.2016 16:31
    +1

    Статью с первого раза не перевалил, придется перечитывать. Но "эффект свитера" — прекрасен!


    1. NikitaSMorozov
      01.04.2016 17:41
      +5

      Если интересно, могу написать про кофту, алгоритм правда до ужаса прост.


      1. midday
        01.04.2016 18:08
        +5

        =) Давайте кофту! Все-равно интересно.


      1. LincolnFaradey
        02.04.2016 03:45

        Все же хотелось бы увидеть этот простой алгоритм.


      1. nse
        02.04.2016 03:45

        Ждем :)


      1. cy_job
        02.04.2016 13:55

        Однозначно пишите, спасибо за статью и приложение.


  1. ScratchBoom
    01.04.2016 20:55

    А приложение для камеры можно посмотреть?


    1. ScratchBoom
      01.04.2016 21:03

      Уже нашёл в описании к YouTube ролику


  1. inwardik
    01.04.2016 22:22

    Спасибо, удивили. Классное приложение, успехов в его развитии!


  1. NikitaSMorozov
    02.04.2016 03:47

    Спасибо за проявленный интерес!
    Думал что будет критика в области математики и общих методах написания шейдера. Со следующей статьёй постараюсь разобраться быстро.


  1. fear724
    03.04.2016 04:42

    Простите что вопрос не по теме, искал давно данную музыку. Подскажите кто автор песни на видео «Live Filter Camera — live sweater, real time mobile quad-tree, mobile tiled image „


    1. NikitaSMorozov
      03.04.2016 04:44

      The 126ers — As We Go, бесплатно можно использовать на YouTube.