nyaload

Журнал Пушыстого

Журнал Пушыстого

Previous Entry Share Next Entry
упаковка float 01 <-> byte
nyaload
_winnie
Часто значения float из промежутка [0, 1] хранят в виде байта или в двух байтах, для экономии места.

Когда незадумчивые программисты пишут код конвертации float2byte и byte2float, возникают следующие ошибки:

1) float2byte( byte2float (X) ) != X
2) float2byte( 1.0 ) отображается в 256, то есть в байт 0.
3) float2byte( byte2float (X) + небольшой шум ) != X
4) часто важно что бы byte2float(255) == 1.0 и byte2float(0) == 0.0
5) множества float соответствующие значению байта - имеют разный размер. Напр. 0 и 255 отображаются в промежутки длины 1/510, а 1,2,..254 - в промежутки 1/255

1,2 - тупо багло. 3,4,5 - важны, но иногда можно пожертвовать, например часто забивают на 5 без особых проблем, или даже с пользой если есть выход за пределы [0..1] в промежуточных вычислениях.

Формулы которые пишут незадумчиво с налёту - это обычно {b=int(f*255.0), f=b/255.0}, {b=min(int(f*256.0),255), f=b/256.0} и тп.

Получают небольшие изменения яркости при каждом save/load, странные шумы, превращение 255 в 254, белый цвет умноженый на белый - даёт не белый, и тп.

Что бы избежать этих ошибок, надо представлять себе такую картинку: делим отрезок [0..1] на 256 равных отрезков (полуоткрытых интервалов). Байты должны превращаться в серединки или другие внутренности этих отрезков, а эти отрезки - в байты, взаимно однозначно. Вариант: промежутки для 1,2,..254 одинаковые, а для 0 и 255 - в два раза меньше.

Примеры устойчивых упаковок:

byte2float:
   f = (b/255.0f - чуть меньше устойчиво к шуму, max-шума=1/(255*256). Зато 0 и 255 превращаются в 0.0f и 1.0f.
   или f = (b + 0.5f)/256.0f - устойчиво к шуму, max-шума=1/(2*256), но 255 отображается в 511.0/512.0 а не в 1.0. Белый цвет умноженый на белый много раз станет серым.

float2byte:
   b = min( int(f*256), 255 )
   или b = int(f*255.9999f)

И ещё вариант, классический, спасибо zeux за его комментарий (рекомендую, если сомневаетесь что выбрать):

byte2float:
   f = b/255.0f
float2byte:
   b = int(f*255 + 0.5)
Хорош всем, только интервалы для 0 и 255 в два раза меньше чем для 1,2,..,254.



Увы, многие библиотеки об этом не задумываются. Если у вас есть многократный save load с преобразованием float/byte, сделайте code review в этом месте. Это могут быть упакованые анимации, heighmap ландшафтов, очевидно текстуры, веса костей для скелетной анимации, всякие веса смешиваний и долей. В базах данных тоже любят оптимизировать такие float.


#include <algorithm> #include <assert.h> typedef unsigned char byte; float clamp(float f) { return std::max(std::min(f, 1.0f), 0.f); } byte f2b_1(float f) { f = clamp(f); return int(f*255.9999f); } byte f2b_2(float f) { f = clamp(f); return std::min(int(f*256.f), 255); } float b2f_1(byte b) { return (b+0.5f) / 256.0f; } float b2f_2(byte b) { return b / 255.0f; } //recommended int main() { for (int i = 0; i < 256; ++i) { byte b = i; assert( f2b_1(b2f_1(b)) == b ); assert( f2b_1(b2f_2(b)) == b ); assert( f2b_2(b2f_1(b)) == b ); assert( f2b_2(b2f_2(b)) == b ); float eps = 0.00001; float big_eps = 0.001; assert( f2b_1(b2f_1(b) + big_eps) == b ); assert( f2b_1(b2f_2(b) + eps) == b ); assert( f2b_2(b2f_1(b) + big_eps) == b ); assert( f2b_2(b2f_2(b) + eps) == b ); assert( f2b_1(b2f_1(b) - big_eps) == b ); assert( f2b_1(b2f_2(b) - eps) == b ); assert( f2b_2(b2f_1(b) - big_eps) == b ); assert( f2b_2(b2f_2(b) - eps) == b ); } assert(f2b_1(0.00001f) == 0); assert(f2b_1(0) == 0); assert(f2b_1(0.99999f) == 255); assert(f2b_1(1) == 255); assert(f2b_2(0.00001f) == 0); assert(f2b_2(0) == 0); assert(f2b_2(0.99999f) == 255); assert(f2b_2(1) == 255); assert(b2f_2(0) == 0); assert(b2f_2(255) == 1.0); }
_Winnie C++ Colorizer


  • 1
О да. Еще в hacker's delight (или где? не помню) было про то, что "при перемножении цветов помните, что перемножение белых цветов должно оставлять их белыми, и еще перемножение не должно искажать гамму, ибо потом замаетесь дебажить порядок перемножений"

>при перемножении цветов помните, что перемножение белых цветов должно оставлять их белыми
Ага. Тогда b2f_1, который (b+0.5) / 256 в пролёте. Если им перевести 1.0 в (1 - 511/512) и возвести в большую степень то белый станет черным.

>и еще перемножение не должно искажать гамму, ибо потом замаетесь дебажить порядок перемножений
А как это надо понимать, "сохранить гамму при перемножении"? Которая вообще не известна для абстрактной картинки в вакууме (там гамма подогнана под любимый монитор дизайнера или номинальные 2.2 и испорчена тысячей мониторов пользователей).
И почему порядок вдруг становится важен?

Это когда края интервалов (0..1 и 254..255) не равны серединке (100..101 например), то градиенты постепенно начинают плыть.

Ну, э, операция некоммутативной становится, ибо нелинейно)

>Это когда края интервалов (0..1 и 254..255) не равны серединке (100..101 например)
Не смог расшифровать обозначения и смысл :(

3) множества float соответствующие значению байта - имеют разный размер. Напр. 0 и 255 отображаются в промежутки длины 1/510, а 1,2,..254 - в промежутки 1/255

byte f2b_1(float f) { f = clamp(f); return int(f*255.9999f); }
честней и детерминистичней было бы, иначе шкала сдвинута на значение .4999f вверх по значению (черные цвета могут стать чуть светлее):
byte f2b_1(float f) { f = clamp(f); return int(f*255.5f); }

возможно ошибаюсь

да, кстати, zeux, прав

Странный пост :)
Т.е. нет, все вроде правильно!
За исключением того, что канонически правильный подход такой - encode = int(f * 255 + 0.5), decode = b / 255.

encode делает разные интервалы, ужимая в два раза краевые точки - это как раз никому нахрен не нужно.

А вот и более другой тест: http://www.everfall.com/paste/id.php?z14ea7ntlmj4
Важные качества:
- f2b(b2f(i)) == i
- ошибка от b2f(f2b(i)) минимизируется - это собственно и важно при квантизации!
- влияние шума минимизируется
- endpoints сохраняются (кстати, бывает важно чтобы сохранялись не только endpoints - например, чтобы 0.5 без погрешности кодировалось в 128 - тогда надо брать умножение на 256).

Для пары *255+0.5 и /255 значения такие:
encode/decode error: 1.268933
noise threshold: 0.001961

Для пары *255.999f и /255 значения такие:
encode/decode error: 2.530851
noise threshold: 0.000016

Итого, как по мне - *256 (НЕ *255.9999) если надо сохранить 0.5, и *255+0.5 во ВСЕХ остальных случаях.

YMMV? :)

Спасибо, добавил в пост!

А потому, что какой-то мудак при разработке C решил, что приведение float/double к int - это круто, причём округление должно делаться к меньшему по модулю (это вообще неиллюзорный пиздец и источник ошибок).

В Паскале, например, отродясь были функции round и trunc, а использование real вместо integer недопустимо. И я считаю, это правильно.

И чуть ли не первым делом в любой проге, где есть такие приведения, втыкаю inline int round(double) - ну или подключаю хедер.

Да, float 2 int и int 2 float оба опасны... Первый опасен гораздо чаще, второй реже но зато непредсказуемей

А int 2 float чем опасен? Использованием float вместо double или последующим приведением float 2 int?

да, если вместо double - float, или если вместо int - 64bit.

#include <iostream>

int main()
{
    int i = 100000005;
    float f = i;
    int j = f;
    
    std::cout << j << '\n'; //suddenly, output 100000008
}

Кажется, ни разу не наступал на такие грабли. Просто потому, что издавна привык всюду писать double (всё равно C при передаче аргументов функций норовил привести), а float - только в здоровенных массивах, где обычно хранится что-то вполне понятное с известным диапазоном.

Ага, а разработчики игр привыкли писать почти везде float, иногда делая исключение для физики. Включая сторонний middleware. Так как в железе везде float (видеокарты, SSE) ну и памяти меньше занимает. Поэтому тщательней разбираются в ошибках округления и наизусть помнят про 23 бита в мантиссе и 8 для экспоненты :)

У меня с детства такие вещи с fixed-point ассоциируются =)

Ещё реализация деления с остатком меня, математика, нереально БЕСЕД!

Да, отрицательные остатки внушают =)

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

> Часто значения float из промежутка [0, 1] хранят в виде байта или в двух байтах, для экономии места.

Хм, вот попытался представить себе ситуацию — не смог. Можно примеры необходимости такой экономии?

Например, карта нормалей (см. bump-mapping)

(тут, правда, -1..1 - но принцип тот же).
Альфа-канал (прозрачность), опять же.

В тех игрушках что я дизайнил, я готов был дорого заплатить, но старался оставить ВСЕ параметры целочисленными :-)

да-да-да! еще порой очень хочется иметь возможность описывать пользовательские типы и их трансформации. как в f# measure units. с ними писать на целочисленной арифметике очень удобно.

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

А где ещё?

это помогает читать код, статическая проверка - всего лишь бонус.

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

Строки: это строка - папка source, не destination, нельзя использовать функции которые модифицируют ФС!

Индексы: блин, как правильно? table1[table2[i+j]] или table1[table2[i]+j] или table2[table1[i]]

Типа "это блин кодировка UTF-8, нельзя её складывать с cp1251!!!"! ""

да, для этого тоже часто хочется типизации.

У тебя в посте перепутаны byte2float и foat2byte

Спасибо! Не приметил у слона перепутанный хобот и хвост :)

уважаемый винни, извините за беспокойство
не подскажите, почему промежутки разные на для 0 и 255, и для 1 - 254 ? никак не могу врубиться
и еще, как выводится, что max-шума=1/(255*256) ?

спасибо заранее

В разных вариантах по разному. Достаточно нарисовать картинку (отрезок 0-1), какие точки куда переходят (только взяв константу 4 вместо 256), что бы увидеть что происходит. То что для других целых чисел eps будут не меньше - можно доказать строго, можно сразу увидеть из картинки (эта разница растёт чем ближе к середине отрезка (около 128), и минимальная для 1, 254 )

В варианте f = (b/255.0f), b = min( int(f*256), 255 ) оценка шума появляется как минимальное eps, при котором b = 1 превращается в b = 0 при двойной конвертации int((1/255.0 - eps) * 256). то что int(x) == 0 это значит что x < 1, то надо искать минимальный eps при котором выражение (1/255.0 - eps) * 256 становится меньше единицы.

Разные промежутки - надо рисовать картинки. У меня в посте есть такая картинка (для 16-ти точек), для классического варианта f = b/255.0f , b = int(f*255 + 0.5). На ней точками обозначены куда переходят целые числа, и квадратиками показаны области действительных числах которые переходят в числа 0, 1, ... 15.

Edited at 2011-02-16 02:23 pm (UTC)

спасибо, более менее стало яснее
хотя бы понятно, что ошибка выводится опытным путем...
получается надо пробежать по 1:255 и найти минимум
причем надо как +eps, так и -eps смотреть

кстати, может имеет смысл:

inline float byte2float( unsigned char byte ) {
static float delta = 1.001f/255; // 0.001 - меньше, чем 1/255 - т.е. ошибка при умножении не превозойдет 1. Таким образом мы можем регулировать предел шума.
return min( delta * byte, 1.0f );
}

inline unsigned char float2byte( float value ) {
return int( ( value + eps ) * 255 ); // +eps - учитываем влияние шума
}

лучше +2*eps
т.к. когда 1.0f-eps - у нас возникает ошибка округления
и +eps не всегда поможет

поправка
принципе (b+0.5f) / 256.0f тоже самое - уменьшает чувствительность к шуму на 0.5/256
что мы и видим по big_eps


  • 1
?

Log in