Преобразование значения цвета из float 0..1 в байт 0..255
Каким будет правильный способ преобразования значения цвета из float в байт? Сначала я думал, что b=f*255.0
должен это сделать, но теперь я думаю, что в этом случае только точный 1.0
будет преобразован в 255
, но 0.9999
уже будет 254
, что, вероятно, не что я хочу...
Кажется, что b=f*256.0
было бы лучше, за исключением того, что у него был бы нежелательный случай создания 256
в случае точного 1.0
.
В конце я использую это:
#define F2B(f) ((f) >= 1.0 ? 255 : (int)((f)*256.0))
Ответы
Ответ 1
1.0 - единственный случай, который может пойти не так, поэтому обрабатывайте этот случай отдельно:
b = floor(f >= 1.0 ? 255 : f * 256.0)
Кроме того, может оказаться целесообразным, чтобы f действительно был 0 <= f <= 1, чтобы избежать неправильного поведения из-за ошибок округления (например, f = 1.0000001).
f2 = max(0.0, min(1.0, f))
b = floor(f2 == 1.0 ? 255 : f2 * 256.0)
Альтернативные безопасные решения:
b = (f >= 1.0 ? 255 : (f <= 0.0 ? 0 : (int)floor(f * 256.0)))
или
b = max(0, min(255, (int)floor(f * 256.0)))
Ответ 2
Я всегда делал round(f * 255.0)
.
Нет необходимости в тестировании (специальный случай для 1) и/или зажатие в других ответах. Является ли это желательным ответом для ваших целей, зависит от того, насколько ваша цель должна соответствовать входным значениям как можно ближе [моя формула] или разделить каждый компонент на 256 равных интервалов [другие формулы].
Возможный недостаток моей формулы заключается в том, что интервалы 0 и 255 имеют только половину ширины других интервалов. За годы использования я еще не видел никаких наглядных доказательств того, что это плохо. Напротив, я счел предпочтительным, чтобы он не попадал ни в крайность, пока вход не приблизился к нему, но это вопрос вкуса.
Возможный потенциал роста заключается в том, что [я полагаю] относительные значения компонентов R-G-B (немного) более точны для более широкого диапазона входных значений.
Хотя я не пытался это доказать, это мой интуитивный смысл, учитывая, что для каждого компонента я раунд, чтобы получить максимально доступное целое число. (Например, я считаю, что если цвет имеет G ~ = 2 x R, эта формула будет чаще оставаться близкой к этому соотношению, хотя разница довольно мала, и есть много других цветов, которые лучше подходят формуле 256
. Так что это может быть стирка.)
На практике подходы, основанные на 256
или 255
, по-видимому, дают хорошие результаты.
Еще один способ оценить 255
vs 256
- это изучение другого направления -
преобразование с 0..255 байт в 0.0..1.0 float.
Формула, которая преобразует целочисленные значения 0..255 в равноотстоящие значения в диапазоне 0.0.1.0:
f = b / 255.0
В этом направлении нет вопроса о том, следует ли использовать 255
или 256
: приведенная выше формула является формулой, которая дает равномерно распределенные результаты. Обратите внимание, что он использует 255
.
Чтобы понять взаимосвязь между формулами 255
в двух направлениях, рассмотрите эту диаграмму, если у вас всего 2 бита, поэтому значения целочисленные значения 0..3:
Диаграмма с использованием 3
для двух бит, аналогичная 255
для 8 бит. Конверсия может быть сверху вниз или снизу вверх:
0 --|-- 1 --|-- 2 --|-- 3
0 --|--1/3--|--2/3--|-- 0
1/6 1/2 5/6
|
- границы между четырьмя диапазонами. Обратите внимание на то, что во внутренней части значения поплавка и целочисленные значения находятся в середине их диапазонов. Обратите внимание, что расстояние между всеми значениями является постоянным в обоих представлениях.
Если вы поймете эти диаграммы, вы поймете, почему я одобряю формулы на основе 255
по формулам на основе 256
.
Претензия. Если вы используете / 255.0
при переходе от байта к float, но вы не используете round(f * 255.0)
при переходе к байту из float, , тогда "ошибка поездки" . Подробности следуют.
Это наиболее легко измеряется, начиная с float, переходя к байту, затем обратно в float. Для простого анализа используйте 2-битные диаграммы "0..3".
Начните с большого количества значений float, равномерно расположенных от 0.0 до 1.0.
Группировка всех этих значений в значениях 4
будет округлена.
Диаграмма имеет 6 диапазонов длины с половиной интервала:
0..1/6, 1/6..1/3,.., 5/6..1
Для каждого диапазона средняя ошибка округления равна половине диапазона, поэтому 1/12
(минимальная ошибка равна нулю, максимальная ошибка 1/6 равномерно распределена).
Все диапазоны дают ту же ошибку; 1/12
- общая средняя ошибка при поездке в оба конца.
Если вы используете какие-либо из формул * 256
или * 255.999
, большинство результатов округления одинаковы, но некоторые из них перемещаются в соседний диапазон.
Любое изменение на другой диапазон увеличивает ошибку; например, если ошибка для одного входа с поплавком ранее была немного меньше 1/6, возврат центра смежного диапазона приводит к ошибке чуть более 1/6. Например. 0,18 в оптимальной формуле = > байт 1 = > плавать 1/3 ~ = 0,333, для ошибки | 0.33-0.18|
= 0.147
; используя формулу 256
formula = > byte 0 = > float 0, для ошибки 0.18
, что является увеличением от оптимальной ошибки 0.147
.
Диаграммы с использованием * 4
с / 3
. Преобразование происходит от одной строки до следующей.
Обратите внимание на неравномерное расстояние первой строки: 0..3/8, 3/8..5/8, 5/8..1. Эти расстояния составляют 3/8, 2/8, 3/8.
Обратите внимание, что границы интервала последней строки отличаются от первой строки.
0------|--3/8--|--5/8--|------0
1/4 1/2 3/4
=> 0------|-- 1 --|-- 2 --|------3
=> 0----|---1/3---|---2/3---|----0
1/6 1/2 5/6
Единственный способ избежать этой повышенной ошибки - использовать другую формулу при переходе от байта к float. Если вы твердо верите в одну из формул 256
, тогда я оставлю ее вам, чтобы определить оптимальную обратную формулу.
(Значение по байту должно возвращать среднюю точку значений float, которая стала значением этого байта. За исключением 0 до 0 и от 3 до 1. Или, возможно, от 0 до 1/8, от 3 до 7/8! На приведенной выше диаграмме должен взять вас из средней линии обратно в верхнюю строку.)
Но теперь у вас будет ситуация с трудной защитой, в которой вы принимаете значения байтов с одинаковым интервалом и преобразуете их в значения с плавающей точкой неравномерно.
Это ваши варианты, если вы используете любое значение, отличное от 255
, для целых чисел 0..255: либо увеличение средней ошибки округления, либо неравномерно разнесенных значений в области с плавающей запятой.
Ответ 3
Почему бы не попробовать что-то вроде
b=f*255.999
Получает освобождение от частного случая f==1
, но 0.999 все еще 255
Ответ 4
Принятое решение не удалось, когда он сравнивал float как целое.
Этот код работает нормально:
float f;
uint8_t i;
//byte to float
f =CLAMP(((float)((i &0x0000ff))) /255.0, 0.0, 1.0);
//float to byte
i =((uint8_t)(255.0f *CLAMP(f, 0.0, 1.0)));
если у вас нет CLAMP:
#define CLAMP(value, min, max) (((value) >(max)) ? (max) : (((value) <(min)) ? (min) : (value)))
Или для полного RGB:
integer_color =((uint8_t)(255.0f *CLAMP(float_color.r, 0.0, 1.0)) <<16) |
((uint8_t)(255.0f *CLAMP(float_color.g, 0.0, 1.0)) <<8) |
((uint8_t)(255.0f *CLAMP(float_color.b, 0.0, 1.0))) & 0xffffff;
float_color.r =CLAMP(((float)((integer_color &0xff0000) >>16)) /255.0, 0.0, 1.0);
float_color.g =CLAMP(((float)((integer_color &0x00ff00) >>8)) /255.0, 0.0, 1.0);
float_color.b =CLAMP(((float)((integer_color &0x0000ff))) /255.0, 0.0, 1.0);
Ответ 5
Я считаю, что правильным является пол (f * 256), а не раунд. Это отобразит интервал 0..1 в ровно 256 зон равной длины.
[EDIT] и проверите 256 как частный случай.
Ответ 6
public static void floatToByte(float f)
{
return (byte)(f * 255 % 256)
}
Значения < 1 точно преобразуются.
Значения, которые после преобразования падают между 255 и 256, перекрываются до 255 при преобразовании в байт.
Значения > 1 зацикливаются на 0 с помощью оператора %
.
Ответ 7
Что вы подразумеваете под правильным способом преобразования значения цвета из числа с плавающей запятой в байт? Вы имеете в виду, что если вы выберете единообразные случайные действительные числа из диапазона [0,1[
что они будут однозначно распределены между 256
ячейками от 0
до 255
?
Чтобы упростить задачу, мы предполагаем, что вместо значения с float
у нас есть действительное число, а вместо int
мы хотим преобразовать в двухбитовое целое число, что-то вроде uint_2
- представление целого числа, которое состоит ровно из двух битов. Это будет означать, что наш unit2_t
может иметь значения 00b
, 01b
, 10b
и 11b
(b обозначает, что у нас здесь есть двоичное число. Это также известно как соглашение Intel). Затем нам нужно придумать, какие интервалы действительных чисел следует сопоставлять каким целым значениям. Если вы хотите отобразить [0,0.25[
в 0
, [0.25,0.5[
в 1
, [0.5,0.75[
в 2
и [0.75,1.0]
в 3
, преобразование можно выполнить с помощью b = std::floor(f * 4.0)
(floor берет только целую часть числа и игнорирует дробную часть). Это работает для всех чисел, кроме f=1
. Простое изменение b = floor(f >= 1.0? 255: f * 256.0)
может решить эту проблему. Это уравнение гарантирует, что интервалы расположены на одинаковом расстоянии.
Если вы предполагаете, что наше действительное значение задано как число с плавающей точкой IEEE 754 одинарной точности, то в пределах интервала [0,1]
существует конечное число возможных представлений с плавающей запятой. Вы должны решить, какие представления этих вещественных чисел принадлежат какому целому представлению. Затем вы можете придумать некоторый исходный код, который преобразует ваше число с плавающей точкой в целое число и проверить, соответствует ли оно вашему отображению. Может быть, int ig = int(255.99 * g);
это то, что вам нужно, или, может быть, b = floor(f >= 1.0? 255: f * 256.0)
. Это зависит от того, какое представление действительного числа вы хотите отобразить на какое представление целого числа.
Посмотрите на следующую программу. Это показывает, что разные конверсии делают разные вещи:
#include <iostream>
constexpr int realToIntegerPeterShirley(const double value) {
return int(255.99 * value);
}
#define F2B(f) ((f) >= 1.0 ? 255 : (int)((f)*256.0))
constexpr int realToIntegerInkredibl(const double value) {
return F2B(value);
}
const int realToIntegerMarkByers(const double value) {
return std::floor(value >= 1.0 ? 255 : value * 256.0);
}
constexpr int realToIntegerToolmakerSteve(const double value) {
return std::round(value * 255.0);
}
constexpr int realToIntegerErichKitzmueller(const double value) {
return value*255.999;
}
constexpr int realToInteger(const float value) {
return realToIntegerInkredibl(value);
}
int main() {
{
double value = 0.906285;
std::cout << realToIntegerMarkByers(value) << std::endl; // output '232'
std::cout << realToIntegerPeterShirley(value) << std::endl; // output '231'
}
{
double value = 0.18345;
std::cout << realToIntegerInkredibl(value) << std::endl; // output '46'
std::cout << realToIntegerToolmakerSteve(value) << std::endl; // output '47'
}
{
double value = 0.761719;
std::cout << realToIntegerVertexwahn(value) << std::endl; // output '195'
std::cout << realToIntegerErichKitzmueller(value) << std::endl; // output '194'
}
}
Вы можете использовать этот маленький стенд для экспериментов:
int main() {
std::mt19937_64 rng;
// initialize the random number generator with time-dependent seed
uint64_t timeSeed = std::chrono::high_resolution_clock::now().time_since_epoch().count();
std::seed_seq ss{uint32_t(timeSeed & 0xffffffff), uint32_t(timeSeed>>32)};
rng.seed(ss);
// initialize a uniform distribution between 0 and 1
std::uniform_real_distribution<double> unif(0, 1);
// ready to generate random numbers
const int nSimulations = 1000000000;
for (int i = 0; i < nSimulations; i++)
{
double currentRandomNumber = unif(rng);
int firstProposal = realToIntegerMarkByers(currentRandomNumber);
int secondProposal = realToIntegerErichKitzmueller(currentRandomNumber);
if(firstProposal != secondProposal) {
std::cout << "Different conversion with real " << currentRandomNumber << std::endl;
return -1;
}
}
}
В конце я бы посоветовал не переводить из числа с плавающей точкой в целое число. Сохраните свое изображение как данные с большим динамическим диапазоном и выберите инструмент (например, http://djv.sourceforge.net/), который преобразует ваши данные в низкий динамический диапазон. Tone mapping - это собственная область исследований, и есть несколько инструментов, которые имеют приятный пользовательский интерфейс и предлагают вам все виды операторов тональных карт.
Ответ 8
Если вы хотите иметь одинаковые по размеру куски, лучшим решением будет следующее. Он преобразует диапазон от [0,1]
до [0,256[
.
#include <cstdint>
#include <limits>
// Greatest double predecessor of 256:
constexpr double MAXCOLOR = 256.0 - std::numeric_limits<double>::epsilon() * 128;
inline uint32_t float_to_int_color(const double color){
return static_cast<uint32_t>(color * MAXCOLOR);
}