Обновить

Комментарии 36

Не читал, но правильный ответ же "прибавить 1 и нормировать на 256"?

Чему равно #0088FF?

Однако некоторых программистов всё равно притягивает альтернативное решение. В чём дело? Чем оно им так нравится?

Тем, что делить на 256 во много раз быстрее, чем на 255? Вы яблоко на сколько долек режете, на 8 или на 7?

делить на 256 во много раз быстрее, чем на 255

Это верно только для целочисленного результата. С плавающей точкой разницы нет. Большинство задач, которые я могу себе представить с таким делением, требуют плавающей точки.

целочисленные константы тоже сейчас заменяются компилятором на обратное умножение и выполняется в одну команду процессора.

Почему? Это же просто изменение ординаты. Где-то внутри SSE наверняка есть такая оптимизация.

Для float-вычислений на GPU или современному SIMD-процессору абсолютно монопенисуально, на 255.0 или на 256.0 делить. Выигрыш в полнаносекунды на целочисленных сдвигах тут же сгорит, когда придется делать костыльный клэмп

Диапазон [0, 256) в диапазон [0, 1)нужно переводить делением на 256. Да, у вас в результатах не будет ровно 1, это и не нужно. Так же как 10 цифр это от 0 до 9, а не от 0 до 10.

А почему вы выбрали незакрытые с одной стороны диапазоны? В целых числах диапазон [0, 256) не имеет большого смысла, т.к. совпадает с [0, 255]. А диапазон [0, 255] в диапазон [0,1] надо переводить делением на 255.

Потому что количество возможных значений в байте 256. Я же говорю, представьте, что у вас не 256 значений, а 10, от 0 до 9. В диапазон от 0 до 1 их нужно переводить делением на 10, будет 10 значений с шагом по 0.1. Незакрытые они чтобы показать связь - 256 значений, но значение 256 не входит в диапазон, потому что отсчет начинается от нуля, первый элемент это 0.

А диапазон [0, 255] в диапазон [0,1]

Зачем вам в результате круглая единица, если в исходных значениях круглого числа нет? Вам надо сделать так (числа в двоичной системе):

// было
min:   00000000
max:   11111111

// стало
min: 0.00000000
max: 0.11111111

0.11111111 это не 1.00000000. Делать круглую единицу конечно можно, только зачем?

В смысле зачем? Единица – это белый цвет, отражает 100% падающего на него света.

Уж лучше тогда круглый 0 убирать из диапазона.

Разницу между яркостью 255 и яркостью 254 в большинстве случаев никто не заметит, если специально не рисовать под нее тестовую картинку. А вот 1 вместо 0 вполне может подпортить уровень черного.

Тогда не будет цвета, который поглощает 100% падающего на него света.

В RGB значение, которое излучает максимум белого цвета это 255 из 256 возможных значений. С другим количеством значений принцип тот же самый. С 2 десятичными знаками после запятой это будет 100 значений от 0.00 до 0.99.

Зачем вам в результате круглая единица, если в исходных значениях круглого числа нет?

Ну это верно с точки зрения систем счисления, а на практике чисто белый цвет (255) - это и есть "круглое" число, максимальное значение диапазона, и он должен быть отображаться в максимальное значение вещественного диапазона (1.0).

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

а на практике чисто белый цвет (255) - это и есть “круглое” число

Нет, круглое число в двоичной системе счисления это степень двойки.

максимальное значение диапазона

Вот и 0.99 это максимальное значение диапазона. Если вы включаете 0, значит не включаете 1.

Представьте, что вы не увеличиваете точность, а уменьшаете. То есть вам нужно замапить исходные 256 значений на 2 с шагом по 1/2. С вашим подходом все значения кроме 255 превратятся в 0, когда должно быть 128 туда и 128 сюда.

Представьте, что у вас не 8 бит, а 2, то есть 4 возможных значения. Поделите отрезок 0…1 на 4 равные части. Как вы их будете обозначать на числовой прямой?

Также ваш алгоритм неустойчив к увеличению точности входных данных. Было 8 бит, стало 16. По логике надо мапить на 1 и выше значения когда старший байт ненулевой, а у вас одно значение мапится когда нулевой.

Нет, круглое число в двоичной системе счисления это степень двойки.

Не, я понимаю, что такое круглое число в двоичной системе. Мой посыл в том, что в рамках рассматриваемой задачи это неважно. Минимальная/максимальная интенсивность (0/255) должны отображаться в минимальное/максимальное вещественное значение 0.0/1.0. То, что 255 на 1 меньше круглого 256 - это низкоуровневая особенность реализации и для задач перевода цвета неважна. Если бы входной диапазон бы 0..123, делили бы на 123, а не на 124, т.е. вообще неважно, какое там число следует за максимально возможным, круглое или нет.

Также ваш алгоритм неустойчив к увеличению точности входных данных

Дак я не предлагаю новый алгоритм, я за первый, описанный в статье, который делит на 255, и, как следует из статьи, применяется в большинстве реализаций.

Я вообще не понял ваши контрпримеры. Два варианта разбиения отрезка на доли описаны в статье.

Вот вам тоже пример, не про цвет, но по-моему лучше демонстрирующий суть задачи. Контроллер скорости принимает команду установки скорости в трёх разных видах: как 28 шагов скорости (0..27), 14 (0..13) и 127 (0..126), в каждом из случаев максимальное значение означает "максимальную скорость". Внутри контроллера все три варианта преобразуются в 0..1, но по вашим формулам "максимальное значение" будет не 1.0, а тремя разными числами! Чтобы проверить, а максимальная ли скорость, предлагается сравнивать с тремя значениями, вместо одной единицы? Чтобы отмасштабировать вещественную скорость в ещё какой-нибудь выходной диапазон команд мотора (скажем, 0..255), придется тоже учитывать, из какого входного диапазона скорость была получена? Абсолютно непонятно, какая польза от такого подхода. (Это, если что, реальный стандарт управления модельными железными дорогами)

Дак я не предлагаю новый алгоритм, я за первый

“Ваш” в разговоре означает тот, который вы поддерживаете, а не только тот, который вы придумали.

Контроллер скорости принимает команду установки скорости в трёх разных видах: как 28 шагов скорости (0…27), 14 (0…13) и 127 (0…126)

Да, это хороший пример. Берем половину скорости. Значения 14 и 7 для первых двух ведь показывают ровно половину от максимальной?

14 / 28 = 0.5
7 / 14 = 0.5
63 / 127 = 0.496
64 / 127 = 0.504

У вас получается:

14 / 27 = 0.519
7 / 13 = 0.538
63 / 126 = 0.5
64 / 126 = 0.508

3 разных значения, а скорость одна и та же.

но по вашим формулам “максимальное значение” будет не 1.0

А почему оно должно быть именно 1.0? Для двух десятичных разрядов ведь максимальное значение 99, а не 100. Почему тут должно быть по-другому?

Чтобы проверить, а максимальная ли скорость, предлагается сравнивать с тремя значениями, вместо одной единицы?

Ну у вас же на входе могут быть 3 разных значения. Прямое сравнение с float единицей вообще делать неправильно, нужно считать дельту и сравнивать что она меньше заданной. И вот как раз заданная дельта тут зависит от точности датчика, она равна 1 деленное на количество значений в датчике.

проблема “белый больше не белый”

Она идет из того, что мы добавили новые разряды, а чем их заполнять неизвестно. Тут нет одного правильного решения, заполнить можно по-разному. Представьте, что это не RGB, а другой датчик, и выходные значения могут быть значения больше 1, например от 0 до 4. Тогда 1023 / 255 = 4.012, когда должно быть 3.996. 256, 512, 768 должны переходить в 1.0, 2.0, 3.0, а они переходят в 1.004, 2.008, 3.011.

Более того, если мы считаем 255 чистым белым, то идеально серым надо считать 127.5.

Хм, тут соглашусь. Если у нас 2 разряда, 00 черный, 11 белый, то 10 будет не идеально серый, иначе бы тогда на одно значение между ними приходилась половина диапазона цвета. В общем, как я сказал ниже, для RGB это подходящий вариант, но нужно понимать, что он может подходить не для всех случаев.

В диапазоне 0..256 257 значений, а не 256.

Я не говорил про диапазон 0…256.

Твой аналог с 10 значениями ломается на том, что 9 это уже максимальное значение шкалы. Если 9 делить на 10, ты никогда не получишь 100% мощности канала, что в графике означает серый вместо чистого белого

9 для счетчика из одного десятичного разряда это и есть 100% мощности канала. Так же как 255 для счетчика из 8 двоичных разрядов.

[0, 255] это неправильно. 255 может кодировать не только то что светлее 255 но и то что после, то что до деления дало бы скажем 255.555. А вот меньше нуля точно не может быть так что верный интервал [0, 256)

Мы же рассматриваем конкретную задачу перевода RGB из uint8 во float, там не может быть 255.555

Очень интересно, но немного запутанно :).

2⁸ == 256. От 0 до 255 — 256 значений, 256 кратно 2, что удобнее для двоичных преобразований. Возможно, я ошибаюсь, но мне кажется, что это наиболее логичный вариант.

Давайте переведем белый (один компонент для простоты) из 8бит в 16бит с транзитом во float для наглядности.
`255 / 256 = 0,99609375 --> 0,99609375 * 65536 -> 65280`. Нет, какая-то ерунда получается, если делить на число значений. И во float уже не белый, и в 16бит потемнел.

А если по уму, делить на максимальное значение?
`255 / 255 = 1.0 -> 1.0 * 65535 --> 65535`. Вот теперь нормально. Везде максимально белый.

Теперь попробуйте так же для строго серого 128 0x80. По логике это должно быть 0.5 = 128/256 = 32768 / 65536.

128 / 255 = 0.501961
0.501961 * 65535 = 32896
32896 - 32768 = 128
На 128 единиц съехало.

Если вы расширяете точность, то у вас одно изначальное значение соответствует 256 новым значениям. То есть все значения 65280…65535 соответствуют исходному 255. Информации о значениях этих разрядов изначально не было, поэтому нужно задать для них какое-то константное значение. Если их обнулить, то получится 65280. Если сделать 1, то черный посветлеет. В ваших расчетах получается, что они заполняются единицами постепенно, то есть 0 идет к левому краю этого диапазона из 256 новых значений, а 255 к правому, для серого соответственно получилось 128. Если цель перевести максимально белый и черный в максимально белый и черный, ну согласен, подходящий вариант. Но надо понимать, что не для всех расчетов это подходит.

Ну если честно, проблема "белый больше не белый" мне кажется более серьезной, чем "серый стал чуть менее серым".

Более того, если мы считаем 255 чистым белым, то идеально серым надо считать 127.5. И поскольку 128 чуток больше, то то, что 32896 на 0.5*256 больше, чем 32767.5, вообще не выглядит ошибкой.

Логика вас подводит, 128 это не средний серый.

Точно так же как если бы у нас был 2битный цвет (значения от 0 до 3), 2 - это не середина диапазона. [ 0 | 1 | 2 | 3 ].

И точно так же, было бы ошибочно утверждать, что 2 из этого диапазона должна стать 128, а не 170.

Согласен, строго серый это неправильное название. Но математически в границах диапазона это все-таки ровно 0.5.

0.5 - это точная середина в условно-непрерывном пространстве float (и то, при допущении, что мы берем [0..1], а не [0..1), что уже не совсем корректно.

В целочисленном диапазоне от 0 до 3 середины в этом понимании нету (она лежит точно на границе между 1 и 2). Это "фишка" двоичной (и кратной) системы счисления. В троичной системе точная середина была бы (два трита, диапазон значений от 0 до 8, ровная середина - 4).

Не совсем. Тут каждому целому числу соответствует какой-то отрезок в диапазоне [0…1). 1 именно не включается, ему соответствует значение 256. Если мы включаем левый край 0, то для равномерности это правило должно соблюдаться для всех подотрезков. Иначе будет неопределенность, одна точка будет принадлежать 2 отрезкам.
0 мапится на подотрезок [0, 1/256).
128 на [128/256, 129/256).
255 на [255/256, 256/256).

Верно, вы говорите о диапазонах (для простоты, опять же, нагляднее оперировать 2битным цветом) - у вас получается 4 отрезка, [0 - 0,25 | 0,25 - 0,5 | 0,5 - 0,75 | 0.75 - 1,0]. Но здесь важный момент: 0, 1, 2, 3 - это номера этих диапазонов, а не их значения. Почти-черный попадет в диапазон номер 0, а почти-белый цвет будет находиться в диапазоне номер 3. Это квантизация. В ней действительно кажется, что номер диапазона тождественен его левой границе, а правая .. "где-то перед следующей левой". При квантизации мы теряем информацию (из какого-то плавного источника) и решаем из какого из четырёх диапазонов пришло значение. И тут возможны варианты, брать центра, края или как-то ешё.

Вопрос с дальнейшим выводом - как это транслируется в цвета. Для работы у нас есть 4 значения и мы из них можем выжать только 4 цвета. У нас должен быть самый темный (назовём его черный), очевидно это диапазон номер 0 и самый светлый (назовём его белый), очевидно это диапазон номер 3. Каким будет цвет диапазона номер 1 ... темно-серым, а 2 - светло-серым. Всё.. приехали.

То есть что важно - при работе с картинками/цветами, именно правило восстановления диктует нам каким должен был быть квантователь выше. Он должен нам "черный", "белый" и "два посередине". Что интересно, "два посередине" уже даёт пространство для маневра - это гамма (обычно 1,6 если я верно помню). Но вот от краёв никуда не уйти. Т.е. крайние коды (0 и N-1) закрепляются за крайними цветами, а промежуточные равномерно располагаются между ними. Поэтому для N бит получается деление именно на 2^N-1, а не на 2^N.

P.S. Отдельный реверанс в сторону устройства отображения - монитор (в 95%), на котором белый это тоже условность и зависит от крутилки яркости, но это уже за пределами спора о деквантизации ))

у вас получается 4 отрезка, [0 - 0,25 | 0,25 - 0,5 | 0,5 - 0,75 | 0.75 - 1,0]

Отрезки получаются [0 - 0,25) [0,25 - 0,5) [0,5 - 0,75) [0.75 - 1,0). Правый край не включается в любом отрезке, иначе одна точка будет принадлежать 2 отрезкам, поэтому и 1 тоже не включается.

Поэтому для N бит получается деление именно на 2^N-1

Согласен, просто надо понимать, что это подходит не для всех случаев. Поэтому неправильно говорить “Правильно делать только так”.

Статья хорошая, но спор вечный) На практике если ты не пишешь собственный движок трассировки лучей для НАСА, деление на 255 закрывает абсолютно все задачи и не ломает совместимость

Присоединюсь к комментарию выше. Статья хорошая но спор вечный)

Кстати а что говорят по этому поводу разные стандарты? Как правильно конвертировать форматы чисел?

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации