Hatred's Log Place

DON'T PANIC!

Oct 2, 2010 - 9 minute read - programming

Цветовое пространство YUV

Многим знакомо цветовое пространство RGB (Red/Green/Blue), мне потребовалось же работать с входными данными пространства YUV, которое широко используется в семействе кодеков MPEG.

Кратко о…

Составляющие пространства:

  • Y - яркостная компонента, если оставить только её получим изображение в оттенках серого, компонента получается из исходного RGB сигнала, каждая составляющая множится на свой вес (сумма весов - 1)
  • U - разностная компонента для голубого цвета (B’ - Y')
  • V - разностная компонента для красного цвета (R’ - Y')

В общем подробности можно почитать тут: http://en.wikipedia.org/wiki/YUV

Основная прелесть этого пространства в том, что для телевизионщиков можно использовать ЧБ инфраструктуру, а кроме того, для хранения информации о цвете для одного пикселя требуется меньший объем памяти (при различных организациях хранения, коих множество).

Беда в том, что существует много алгоритмов, обработки изображения, которые ориентированы на RGB, стоит вопрос преобразования. Но с преобразованием тоже не всё гладно: RGB-to-YUV и YUV-to-RGB приводит к потере и искажению информации о цвете, так что стоит свести оные к минимуму.

Кроме того, существует множество способов паковки YUV информации (стоит сказать и у RGB тоже не мало), о коих можно почитать тут: http://www.fourcc.org/yuv.php, там же можно найти формулы преобразования: http://www.fourcc.org/fccyvrgb.php

YUV ↔ RGB на Java

У меня задача обработки стояла на Java, там Я словил одну особенность: формулы предполагают работу с unsigned char, тогда как в Java unsigned типов нет. В результате у меня получился примерно такой алгоритм для YUV420p (планарный формат, составляющие YUV в кадре идут полосами, друг за другом, причем на 4 составляющие Y приходится по 1 UV, чем достигается компрессия по сравнению с RGB в 1.5 раза):

public static void processBufferYUV420P(byte[] in_buffer, byte[] out_buffer, int w, int h)
{
  int y_size  = w * h;        // Размер блока Y
  int uv_size = y_size / 4;   // Размер блоков U и V

  int x, y;
  int Ri, Gi, Bi;
  int Ro, Go, Bo;
  int Yi, Ui, Vi;
  int Yo, Uo, Vo;

  int[] rgb = new int[3];
  int[] yuv = new int[3];

  for (x = 0; x < w; x++)
  {
    for (y = 0; y < h; y++)
    {
      // Считаем индексы
      int y_idx = y * w + x;
      int u_idx = (y/2) * (w/2) + (x/2) + y_size;            // Составляющая U идет сразу за Y
      int v_idx = (y/2) * (w/2) + (x/2) + y_size + uv_size;  // Составляющая V идет сразу за U

      // Читаем компоненты цвета, делаем преобразование к "беззнаковому" виду, функции ниже
      Yi = byte2unsigned(in_buffer[y_idx]);
      Ui = byte2unsigned(in_buffer[u_idx]);
      Vi = byte2unsigned(in_buffer[v_idx]);

      // Тут преобразование в RGB, по cути, выше, мы преобразовали YUV420p в YUV444
      yuv2rgb(Yi, Ui, Vi, rgb);
      Ri = rgb[0];
      Gi = rgb[1];
      Bi = rgb[2];

      // Обработка

      ... //совершаем работу над Ri/Gi/Bi получаем на выходе Ro/Go/Bo

      // Обратное преобразование в YUV
      rgb2yuv(Ro, Go, Bo, yuv);
      Yo = yuv[0];
      Uo = yuv[1];
      Vo = yuv[2];

      // Запись, тут мы возвращаем "знаковость" компонентам
      out_buffer[y_idx] = byte2signed(Yo);
      out_buffer[u_idx] = byte2signed(Uo);
      out_buffer[v_idx] = byte2signed(Vo);
    }
  }
}

/**
 * Обрезает значение до [0..255]
 * @param value
 * @return
 */
public static int clip(int value)
{
  int return_value = value;

  if (value < 0)
    return_value = 0;

  if (value > 255)
    return_value = 255;

  return return_value;
}

/**
 * Конвертирует yuv2rgb
 * @param y [0..255]
 * @param u [0..255]
 * @param v [0..255]
 * @param rgb массив трех элементов RGB [0..255]
 */
public static void yuv2rgb(int y, int u, int v, int[] rgb)
{
  int r, g, b;

  // Я умножал на 1024, округлял, а в конце делал смещение вправо на 10, потеря точности не большая
  // с целыми числами работа быстрее
  r = (int)(y + (1441*(v - 128) >> 10));                    // R
  g = (int)(y - ((354*(u - 128) + 734*(v - 128)) >> 10));   // G
  b = (int)(y + (1822*(u - 128) >> 10));                    // B

  rgb[0] = clip(r);
  rgb[1] = clip(g);
  rgb[2] = clip(b);
}

/**
 * Конвертирует RGB и YUV
 * @param r [0..255]
 * @param g [0..255]
 * @param b [0..255]
 * @param yuv массив результатов YUV [0..255]
 */
public static void rgb2yuv(int r, int g, int b, int[] yuv)
{
  int y, u, v;

  // аналогично со смещениями и целыми числами
  y = (int)((r *  39191 + g *  76939 + b *  14942) >> 17);
  u = (int)(((r * -22117 + g * -43419 + b *  65536) >> 17) + 128);
  v = (int)(((r *  65536 + g * -54878 + b * -10658) >> 17) + 128);

  yuv[0] = y;
  yuv[1] = u;
  yuv[2] = v;
}

/**
 * В Java нет unsigned типов, поэтому обходим так
 * Преобразует signed byte в unsigned форму, повышая битность до int
 * @param b
 * @return
 */
public static int byte2unsigned(byte b)
{
  return (b & 0xFF);
}

/**
 * В Java нет unsigned типов, поэтому обходим так
 * Преобразуем unsigned форму байта (в виде int) в signed форму
 * @param b
 * @return
 */
public static byte byte2signed(int b)
{
  //return (b > 127) ? (byte)(b - 0x100) : (byte)(b);
   return (byte)(b); 
}

В общем, это на память, и ссылки по теме:

RGB ↔ YUV в Octave

И в довесок функции для Octave для выполнения данных преобразований:

function ret = clip3(value, min_range, max_range)
	ret = value;
	if value < min_range
		ret = min_range;
	endif
	
	if value > max_range
		ret = max_range;
	endif
endfunction

function YUV = rgb2yuv(R, G, B)
	Wu = 0.436;
	Wv = 0.615;
	
	KOEF = [0.299, 0.587, 0.114; <br/>
	             -0.147, -0.289, 0.436; <br/>
	             0.615, -0.515, -0.100];
	
	RGB = [R; G; B] ./ 255; # to [0..1] form
	YUV = KOEF * RGB;
	
	Y = YUV(1)
	U = YUV(2)
	V = YUV(3)
	
	Y = floor(Y * 255);
	U = floor((U + Wu) * 255 / (Wu*2));
	V = floor((V + Wv) * 255 / (Wv*2));
	
	YUV = [Y ; U; V];
endfunction

function RGB = yuv2rgb (Y, U, V)
	Wu = 0.436;
	Wv = 0.615;
	
	Y = Y / 255
	U = U * (Wu*2) / 255 - Wu
	V = V * (Wv*2) / 255 - Wv
	YUV = [Y; U; V] ;
	KOEF = [1, 0, 1.14; <br/>
	              1, -0.395, -0.581; <br/>
	              1, 2.032, 0];
	
	RGB = KOEF * YUV .* 255;
	
	RGB(1) = clip3(RGB(1), 0, 255);
	RGB(2) = clip3(RGB(2), 0, 255);
	RGB(3) = clip3(RGB(3), 0, 255);
	
endfunction

Использовать можно примерно так:

T = rgb2yuv(156, 255, 0)
T = yuv2rgb(T(1), T(2), T(3))

Варианты паковки в картинках

YUV444 - один пиксель - 24 бит, т.е. на каждый пиксель приходится одна составляющая Y, U и V. Самый неэкономный, но самый простой для манипуляций, к примеру, для изменения размеров. Поэтому иногда к нему переходят.

В виде данных может представляться в двух видах: packed и planar (обычно если это planar после полного названия ставится буква p: YUV444p, YUV420p).

Packed - байты яркости (чёрно-белая картинка, Y) и цветоразности (UV) - идут вперемешку.

Planar - в памяти идёт сначала Y компонента (одним сплошным куском), затем отдельно U и V.

Обращу внимание, последовательно U и V может меняться у разных подформатов (см: http://www.fourcc.org/yuv.php)

YUV444 packed:

YUV422 - на один пиксель, в среднем - 16 бит. Из-за того, что на два яркостных пикселя (YY) используется по одному цветоразностных пикселя: UV. Т.е. на два пикселя: p2_bits = 2*8 + 8 + 8 = 32 бит ⇒ на один пиксель p_bits = p2_bits/2 = 32/2 = 16. На первой картинке в этом разделе видно как объединяются пиксели. Так же может существовать в двух видах: packed и planar.

YUV422 packed:

YUV420 - на один пиксель, в среднем - 12 бит: на 4 смежные яркостных пикселя:

YY
YY

используется по одному цветоразностному UV. Т.е. p4_bits = 4*8 + 8 + 8 = 48 ⇒ p_bits = p4_bits/4 = 48/4 = 12. Информацию об использовании этого формата в packed виде я никогда не видел. Кроме того, в некоторых источниках говорят, что букву p (planar) часто опускают, и говорят, что YUV420 == YUV420p.

YUV420p - planar:

Схожая схема паковки и у других форматов в планарном представлении.

YUV411 - на один пиксель, в среднем - 12 бит. Отличие от YUV420 только в том, общие цветоразностные пиксели (UV) не для четёрых смежных яркостных (Y), а для четырёх последовательных. На первой картинке раздела это очень хорошо показано. Может быть представлен как в планарном, так и в пакованном виде.

YUV411 - packed:

Planar или packed?

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

С другой стороны, у нас есть много вариантов YUV форматов, в которых отличаются последовательности YUV (UYV, UVY и т.п.) и когда возникает необходимость в конвертировании приходится пробегать для каждого пикселя, что бы установить нужный порядок (к примеру: некоторое оборудование может работать ну только с этим конкретным форматом, к примеру YV12, а у вас YUV420p). В случае с планарами мы просто перенастраиваем три указателя на нужные области.

Более того, YUV420p очень популярен в телевидении (это, имхо, и причина, почему его в пакованном виде вообще нет), и его особенность, что сначала идёт полный яркостный кадр (читай - полная ЧБ картинка), а потом цветоразностные, использовалась на заре появления цветного телевидения: старые телевизоры игнорировали расширенные цветоразностные данные (они как-то хитро там посылались, интернет в помощь) и могли нормально показывать ЧБ картинку. Новые цветные телевизоры уже могли читать полную картинку и показывать уже цветное изображение. И всё - для одного сигнала!

Переходы между YUV

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

  1. считаем среднее
  2. или находим медиану

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

Выше для понижения битности. Для повышения битности просто делаем дублирование уже существующих UV для смежных пикселей.

Пример: YUV444 -> YUV422

Y0 U0 V0   Y1 U1 V1          Y0 U0` V0`   Y1 ... ...
                      -->    
Y2 U2 V2   Y3 U3 V3          Y2 U1` V1`   Y3 ... ...

Расчёт:

U0` = (U0 + U1) / 2       V0` = (V0 + V1) / 2
U1` = (U2 + U3) / 2       V1` = (V2 + V3) / 2

При обратном переходе (YUV422 -> YUV444):

U0 = U0`     V0 = V0`
U1 = U0`     V1 = V0`

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