Многим знакомо цветовое пространство 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);
}
В общем, это на память, и ссылки по теме:
- http://www.erazer.org/how-to-convert-rgb-to-yuv420p/
- http://www.gamedev.ru/code/forum/?id=84700
- http://www.fourcc.org/yuv.php
- http://www.fourcc.org/fccyvrgb.php
- http://www.f4.fhtw-berlin.de/~barthel/ImageJ/ColorInspector*HTMLHelp/farbraumJava.htm
- http://en.wikipedia.org/wiki/YUV
- http://www.mikekohn.net/file_formats/yuv_rgb_converter.php - удобный конвертер для проверки себя
- http://www.equasys.de/colorconversion.html - матричные формулы
- http://www.javamex.com/java_equivalents/unsigned.shtml - про unsigned в Java
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
Как уменьшается количество цветоразностных компонент? На самом деле есть много более хитрых алгоритмов, но для собственных потребностей часто хватает двух:
- считаем среднее
- или находим медиану
Всё основано на том, что человеческий глаз слабо замечает переходы цветов на смежных пикселях, на чём и основана игра.
Выше для понижения битности. Для повышения битности просто делаем дублирование уже существующих 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, не считая медиану или среднего. Тоже вариант, плюс ускорение, но потеря информации больше.