Преобразование значения цвета из 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);
}