angle-left

Преобразование RGB888 <-> RGB565 и защита грибов от выцветания

Недавно я занимался быстрым сжатием изображений и по этому поводу перегонял картинки с грибами в разные форматы. И все бы ничего, но грибы при перегонке теряли точность и вообще вели себя неподобающим образом. А виноват во всем оказался участок кода, отвечающий за преобразование цвета между форматами RGB с разной точностью (24, 16, 12 бит на точку). Тривиальная казалось бы задача, но из-за этой простоты ее очень легко можно решить неправильно.

Итак, рассмотрим разные способы преобразования на примере RGB888 -> RGB565. RGB888 имеет 8 бит на каждый цветовой канал, а RGB565 - по 5 бит для красного и синего и 6 бит для зеленого канала. Соответствующие переменные в псевдокоде обозначаются r8, g8, b8 и r5, g6, b5.

Наиболее простым в реализации методом является обычный сдвиг.

//incorrect RGB888 -> RGB565
r5 = r8 >> 3;
g6 = g8 >> 2;
b5 = b8 >> 3;

//incorrect RGB565 -> RGB888
r8 = r5 << 3;
g8 = g6 << 2;
b8 = b5 << 3;

Но это преобразование некорректно. Белый цвет с максимальной интенсивностью всех каналов RGB565(31, 62, 31) будет преобразован в RGB888(248, 252, 248), что, очевидно, неправильно. То есть, после преобразования RGB565 -> RGB888 итоговое изображение будет более зеленым и немного тусклее, чем должно быть.

Правильная формула преобразования для канала с из формата X в Y выглядит следующим образом: cY = cX * max(cY)/max(cX). Тем не менее, следующий код также не совсем корректен:

//incorrect RGB565 -> RGB888
r8 = (BYTE)( (float) r5 * 255.0f / 31.0f );
g8 = (BYTE)( (float) g6 * 255.0f / 63.0f );
b8 = (BYTE)( (float) b5 * 255.0f / 31.0f );

//incorrect RGB888 -> RGB565
r5 = (BYTE)( (float) r8 * 31.0f / 255.0f );
g6 = (BYTE)( (float) g8 * 63.0f / 255.0f );
b5 = (BYTE)( (float) b8 * 31.0f / 255.0f );

В зависимости от языка и настроек компилятора при приведении числа с плавающей запятой к целому может отбрасываться целая часть, то есть числа не будут правильно округляться. Это приводит к тому, что при циклическом преобразовании RGB565 <-> RGB888, цвета каждый раз будут темнеть. Например, цвет RGB888(230, 230, 40) после первого цикла станет равным RGB888(222, 226, 32), а после пятого RGB888(197, 214, 0):

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

//RGB565 -> RGB888
r8 = (BYTE)( (float) r5 * 255.0f / 31.0f + 0.5f );
g8 = (BYTE)( (float) g6 * 255.0f / 63.0f + 0.5f );
b8 = (BYTE)( (float) b5 * 255.0f / 31.0f + 0.5f );

//RGB888 -> RGB565
r5 = (BYTE)( (float) r8 * 31.0f / 255.0f + 0.5f );
g6 = (BYTE)( (float) g8 * 63.0f / 255.0f + 0.5f );
b5 = (BYTE)( (float) b8 * 31.0f / 255.0f + 0.5f );

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

//RGB565 -> RGB888 using tables
Table5 = {0, 8, 16, 25, 33, 41, 49, 58, 66, 74, 82, 90, 99, 107, 115, 123, 132,
 140, 148, 156, 165, 173, 181, 189, 197, 206, 214, 222, 230, 239, 247, 255};

Table6 = {0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 45, 49, 53, 57, 61, 65, 69,
 73, 77, 81, 85, 89, 93, 97, 101, 105, 109, 113, 117, 121, 125, 130, 134, 138,
 142, 146, 150, 154, 158, 162, 166, 170, 174, 178, 182, 186, 190, 194, 198,
 202, 206, 210, 215, 219, 223, 227, 231, 235, 239, 243, 247, 251, 255};

r8 = Table5[r5];
g8 = Table6[g6];
b8 = Table5[b5];

Зачастую этот способ является наиболее подходящим, так как он обладает хорошей скоростью и корректен математически. Но в моем случае специфика аппаратуры и ограничения на память не позволяли им воспользоваться. В таких случаях можно использовать приближенные методы. Хорошей целочисленной аппроксимацией для RGB565 -> RGB888, является копирование старших разрядов в младшие после сдвига.

//RGB565 -> RGB888 using integer approximation
r8 = ( r5 << 3 ) | (r5 >> 2);
g8 = ( g6 << 2 ) | (g6 >> 4);
b8 = ( b5 << 3 ) | (b5 >> 2);

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