Допустим, вы пишете программу для обработки изображений. Программа получает изображение, преобразует его в значения с плавающей запятой, выполняет обработку и сохраняет изменённые пиксели на диск в виде 8-битных цветов. Сегодня я хочу рассмотреть вопрос преобразования целых значений в значения с плавающей запятой. Существует два решения, которые на Python и NumPy выглядят так:
Стандартное деление на 255
pixels = img / 255.0 result = process(pixels) output = np.trunc(result * 255 + 0.5)
Альтернативное деление на 256
pixels = (img + 0.5) / 256.0 result = process(pixels) output = np.trunc(result * 256)
Я предполагаю, что в обоих случаях выходные значения ограничиваются перед окончательным преобразованием типов:
# Ограничение и преобразование в 8 бит output_8bit = output.clip(0, 255).astype(np.uint8)
В стандартном случае целочисленный 0 соответствует 0.0, а 255 соответствует 1.0. Это работает абсолютно нормально и именно так всё реализовано в GPU. В альтернативном случае прибавляется смещение на 0,5 и деление происходит на 256, поэтому целочисленный 0 соответствует 0.5/256=0.001953125. Это неудобно, потому что код обработки изображений, например, без знания константы не сможет обнаруживать чёрные пиксели. Из‑за этого мы привязываем логику к 8-битным значениям, даже если вычисления выполняются с плавающей запятой. В стандартном решении всегда можно предполагать, что чёрный соответствует 0.0.
Однако некоторых программистов всё равно притягивает альтернативное решение. В чём дело? Чем оно им так нравится?
Аргумент против 255.0
На числовой прямой стандартное решение выглядит довольно странно. Ниже показана более наглядная версия с 3-битными числами в интервале [0..7], преобразованными в [0,1]:

На оси X находится числовая прямая, а коричневыми кружками обозначены декодированные значения с плавающей запятой. Числа внутри — это целочисленные входные значения. На каждое целое число указывают стрелки, определяющие интервал значений с плавающей запятой, округляемых до него. Ниже я буду называть эти интервалы бинами.
На краях интервала бины меньше
Первая проблема, сразу заметная на схеме — это выход крайних бинов стандартной формулы за пределы интервала [0,1]. Наверно, эта визуализация не совсем справедлива — в обоих решениях выходные значения ограничиваются, поэтому крайние бины должны уходить в бесконечность, но она чётко показывает, насколько «растянут» стандартный интервал. Растянутый интервал шире, чем предполагаемый при обработке изображений рабочий интервал [0,1].
Это означает, что при преобразовании значений с плавающей запятой в интервале [0,1] обратно в целые крайние бины, по сути, в два раза у́же остальных бинов. Из‑за этого нашему алгоритму будет «сложнее» возвращать крайние значения. Например, если мы генерируем равномерный шум [0,1] и округляем его по стандартной формуле, то значения 0 и 255 будут встречаться вдвое реже, чем другие целые.
Можно проверить это утверждение эмпирически, сгенерировав миллион равномерно распределённых случайных чисел и составив из них гистограмму. На ней будет видно, что 0 и 255 в самом деле вдвое ниже остальных бинов:


Код гистограммы
import numpy as np import matplotlib.pyplot as plt result = np.random.uniform(0, 1, 1000000) final_values = np.trunc(result * 255 + 0.5).clip(0, 255).astype(np.uint8) plt.hist(final_values, bins=256, range=(0, 255)) plt.show()
Тем не менее, я не могу придумать пример ситуации, в которой этот перекос способен вызвать какие‑то проблемы. Да, числа с плавающей запятой в стандартном решении распределены по более широкому интервалу, но преобразование исходного изображения туда и обратно всё равно происходит без потерь (uint8 → float → uint8).
Кроме того, все получающиеся значения, отличающиеся от 0.0 и 1.0, всё равно будут округляться до нужного бина, выравнивая распределение выходных значений. Вот пример: допустим, наша обработка вычитает 0.005 из цветов с плавающей запятой. При стандартном решении чёрный опускается ниже нуля, выходя за пределы интервала [0,1], а при альтернативном значения остаются положительными. Впрочем, в конечном итоге оба решения возвращают целочисленный 0:
Стандартное решение: trunc(255 * (-0.005) + 0.5) = 0 Альтернативное решение: trunc(256 * (0.5 / 256 - 0.005)) = 0
Совершенно не важно, что при стандартном решении бин нуля имеет «половинный размер».
Неточность
Вторая проблема заключается в том, что значения с плавающей запятой в стандартном решении неточные. Например, 128/255.0≈0.501961, но 128/256.0=0.5. Из‑за этой погрешности при округлении расстояния между значениями с плавающей запятой немного варьируются. Но это несерьёзная проблема, потому что погрешность оказывается крошечной. 32-битное число с плавающей запятой имеет 23-битную дробную значащую часть. Погрешность округления затрагивает самый младший бит; колебания происходят с величиной меньше 2−23. Разумеется, относительная погрешность в 0,00001% несущественна даже для самой изощрённой задачи обработки изображений. В данном случае неточность — это вопрос эстетики, а не техническая проблема.
Значения не находятся между целыми
При альтернативном решении каждое значение с плавающей запятой располагается ровно посередине между двумя целыми. Посмотрите на структуру вертикальных столбцов на схеме с числовой прямой. Позицию посередине можно воспринимать как компромисс; мы не знаем, каким конкретно было исходное квантуемое значение, поэтому средняя точка между двумя порядковыми целыми числами будет достаточно точным предположением.
Уверен, что существуют области применения, в которых это свойство полезно, хоть и не могу сам подобрать примеры. По крайней мере, в посте «Converting Color Depth» Эндрю Кеслера (известного своим трассировщиком лучей на визитке) говорится, что такая система повышает удобство дизеринга. Он объясняет это так: шум можно складывать, не беспокоясь о пограничных случаях. Неудобные же крайние значения стандартной формулы требуют внимательной обработки для согласования распределения шума.
Два вида квантования
Пока стандартная формула с делением на 255 по‑прежнему выглядит надёжной или хотя бы достаточно оправдывающей своё применение. Можно посмотреть на этот вопрос под другим углом: два решения — это равномерные скалярные квантователи. Открыв страницу Википедии о квантовании, мы поймём, что существует два основных вида квантования:
Большинство равномерных квантователей входных данных со знаком можно разделить на два типа: квантователи с нулевой (mid‑tread) и с ненулевой ступенью (mid‑riser). Названия связаны с тем, что происходит в окрестности значения 0; функция ввода‑вывода квантователя рассматривается в виде лестницы. Квантователи с нулевой ступенью имеют уровень восстановления с нулевым значением (соответствующий ступеньке лестницы), а квантователи с ненулевой ступенью имеют порок классификации с нулевым значением (соответствующий подъёму ступеньки).
В качестве источника Википедия указывает статью 1977 года с настолько невероятным сочетанием названия и абстракта, что я приведу их здесь:

На графике квантователи с нулевой и ненулевой ступенью различаются в том, где они пересекают ноль:

Квантователь с нулевой ступенью и в самом деле соотносит ноль с нулём, а квантователь с ненулевой ступенью соотносит ноль с средней точкой между двумя целыми (звучит знакомо?). В выбранной Википедией записи вещественное число на входе обозначено x, его закодированное («классифицированное») целое значение — k, а восстановленное вещественное число — yk. Формулы соответствующих квантователей выглядят так:
Тип |
Классификация (кодирование) |
Восстановление (декодирование) |
|---|---|---|
Квантователь с нулевой ступенью |
k=trunc(xL+0.5) |
yk=k/L |
Квантователь с ненулевой ступенью |
k=trunc(xL) |
yk=(k+0.5)/L |
L обозначает количество уровней вывода (например, 256).
Если мы применим эти определения к нашим конкурирующим решениям, то можем назвать стандартную формулу квантователем с ненулевой ступенью и L=255, а альтернативную — квантователем с нулевой ступенью и L=256. Я даже ещё раз покажу их код с новыми обозначениями, чтобы связь стала понятнее. Сами блоки кода остались теми же.
Квантователь с ненулевой ступенью (L=255)
pixels = img / 255.0 result = process(pixels) output = np.trunc(result * 255 + 0.5)
Квантователь с ненулевой ступенью (L=256)
pixels = (img + 0.5) / 256.0 result = process(pixels) output = np.trunc(result * 256)
С этой точки зрения можно сказать, что стандартное решение представляет собой странное сочетание квантователя с ненулевой ступенью для беззнаковых входных значений (в цитате говорится про «входные данные со знаком») и L=255. Очевидно, что это неоптимальный выбор для 8-битных входных значений. И всё это ради удобства привязки крайних значений к 0.0 и 1.0. Это подводит нас к ещё одной проблеме стандартной формулы.
Более высокая погрешность квантования
Если бы мы проектировали систему, которая получает вещественное число с равномерным распределением x∈[0,1], кодирует его в 8-битное целое k и воссоздаёт его как ещё одно вещественное число yk, то стандартная формула была бы пустой тратой ресурсов. Помните, что бины 0 и 255 слегка выходят за края интервала [0,1]? В стандартном решении интервал представляемых значений на самом деле равен [−0.5/255,255.5/255], то есть бины разнесены чуть дальше, чем строго необходимо для входных значений [0,1], что приводит к увеличению погрешности при восстановлении. Впрочем погрешность увеличивается незначительно. Согласно расчётам Питера Мудриевски на StackOverflow, средние абсолютные погрешности для делителей 255 и 256 равны 1/1020 и 1/1024. То есть, теоретически, деление на 256 точнее.
Тонкость в том, что восстановление мы выполняем не так. Сначала мы загружаем 8-битные RGB‑изображения, выполняем их обработку и снова их сохраняем. Мы не можем управлять их квантованием при сохранении; вся потерянная информация утеряна навечно. Иными словами, если цвет изображения был умножен на 255 и округлён, то деление его на 256 во время загрузки не вернёт точность. Только когда мы контролируем и сохранение, и загрузку, имеет смысл стремление к снижению погрешности при восстановлении.
На самом деле, применение альтернативной формулы при загрузке сторонних изображений увеличивает погрешность. Вероятнее всего, изображения квантовались при помощи стандартной формулы, поэтому, теоретически, декодирование их с другим коэффициентом будет некорректным. На практике же, цвета цвета измеряются не в абсолютных значениях (пусть даже это утверждается в спецификациях sRGB), и мы всего лишь выполняем обработку в чуть меньшем интервале с меньшим смещением.
Кроме того, никогда не следует сочетать этапы кодирования и декодирования двух квантователей, так вы просто поломаете код. Но такую ошибку очень легко совершить.
В заключение
Ответ на вопрос в заголовке статьи будет таким: если вы обрабатываете изображения, взятые из сторонних источников, то значения RGB следует нормализовать на 255. Ни снижение точности значений с плавающей запятой, ни абстрактное представление о более высокой погрешности при восстановлении не будут веской причиной для выбора альтернативного решения. Но если вы контролируете и сохранение, и загрузку изображения, и вам не нужно, чтобы ноль соответствовал нулю, то для повышения точности можно подумать о делении на 256. Только не вините меня, когда ваши коллеги будут загружать ваши изображения при помощи стандартной формулы.
Другие мнения
В статье Джонатана Блоу за 2002 год говорится о квантователях с нулевой и ненулевой ступенью, хоть эти названия в статье и не упоминаются. Идею схемы я взял оттуда.
В уже упомянутом посте Эндрю Кеслера за 2015 год рассказывается о плюсах альтернативной формулы. К сожалению, там приведено сравнение со стандартной формулой, но без округления, что лишает ценности основную часть анализа.
Комментарии (37)

CitizenOfDreams
11.08.2026 12:19Однако некоторых программистов всё равно притягивает альтернативное решение. В чём дело? Чем оно им так нравится?
Тем, что делить на 256 во много раз быстрее, чем на 255? Вы яблоко на сколько долек режете, на 8 или на 7?

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

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

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

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

michael_v89
11.08.2026 12:19Диапазон
[0, 256)в диапазон[0, 1)нужно переводить делением на 256. Да, у вас в результатах не будет ровно1, это и не нужно. Так же как10цифр это от0до9, а не от0до10.
osmanpasha
11.08.2026 12:19А почему вы выбрали незакрытые с одной стороны диапазоны? В целых числах диапазон [0, 256) не имеет большого смысла, т.к. совпадает с [0, 255]. А диапазон [0, 255] в диапазон [0,1] надо переводить делением на 255.

michael_v89
11.08.2026 12:19Потому что количество возможных значений в байте 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.111111110.11111111 это не 1.00000000. Делать круглую единицу конечно можно, только зачем?

Sixshaman
11.08.2026 12:19В смысле зачем? Единица – это белый цвет, отражает 100% падающего на него света.
Уж лучше тогда круглый 0 убирать из диапазона.

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

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

osmanpasha
11.08.2026 12:19Зачем вам в результате круглая единица, если в исходных значениях круглого числа нет?
Ну это верно с точки зрения систем счисления, а на практике чисто белый цвет (255) - это и есть "круглое" число, максимальное значение диапазона, и он должен быть отображаться в максимальное значение вещественного диапазона (1.0).
Эта же проблема указана и в самой статье, только с чисто черным цветом, который по альтернативной формуле из статьи перестает быть таковым, так что алгоритму, оперирующему вещественными числами, для определения "черный ли цвет?", надо знать, из какой глубины цвета он конвертирован.

michael_v89
11.08.2026 12:19а на практике чисто белый цвет (255) - это и есть “круглое” число
Нет, круглое число в двоичной системе счисления это степень двойки.
максимальное значение диапазона
Вот и 0.99 это максимальное значение диапазона. Если вы включаете 0, значит не включаете 1.
Представьте, что вы не увеличиваете точность, а уменьшаете. То есть вам нужно замапить исходные 256 значений на 2 с шагом по 1/2. С вашим подходом все значения кроме 255 превратятся в 0, когда должно быть 128 туда и 128 сюда.
Представьте, что у вас не 8 бит, а 2, то есть 4 возможных значения. Поделите отрезок 0…1 на 4 равные части. Как вы их будете обозначать на числовой прямой?
Также ваш алгоритм неустойчив к увеличению точности входных данных. Было 8 бит, стало 16. По логике надо мапить на 1 и выше значения когда старший байт ненулевой, а у вас одно значение мапится когда нулевой.

osmanpasha
11.08.2026 12:19Нет, круглое число в двоичной системе счисления это степень двойки.
Не, я понимаю, что такое круглое число в двоичной системе. Мой посыл в том, что в рамках рассматриваемой задачи это неважно. Минимальная/максимальная интенсивность (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), придется тоже учитывать, из какого входного диапазона скорость была получена? Абсолютно непонятно, какая польза от такого подхода. (Это, если что, реальный стандарт управления модельными железными дорогами)

michael_v89
11.08.2026 12:19Дак я не предлагаю новый алгоритм, я за первый
“Ваш” в разговоре означает тот, который вы поддерживаете, а не только тот, который вы придумали.
Контроллер скорости принимает команду установки скорости в трёх разных видах: как 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.5083 разных значения, а скорость одна и та же.
но по вашим формулам “максимальное значение” будет не 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 это подходящий вариант, но нужно понимать, что он может подходить не для всех случаев.

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

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

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

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

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

Kromster80
11.08.2026 12:19Давайте переведем белый (один компонент для простоты) из 8бит в 16бит с транзитом во float для наглядности.
`255 / 256 = 0,99609375 --> 0,99609375 * 65536 -> 65280`. Нет, какая-то ерунда получается, если делить на число значений. И во float уже не белый, и в 16бит потемнел.А если по уму, делить на максимальное значение?
`255 / 255 = 1.0 -> 1.0 * 65535 --> 65535`. Вот теперь нормально. Везде максимально белый.
michael_v89
11.08.2026 12:19Теперь попробуйте так же для строго серого 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. Если цель перевести максимально белый и черный в максимально белый и черный, ну согласен, подходящий вариант. Но надо понимать, что не для всех расчетов это подходит.

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

Kromster80
11.08.2026 12:19Логика вас подводит, 128 это не средний серый.
Точно так же как если бы у нас был 2битный цвет (значения от 0 до 3), 2 - это не середина диапазона. [ 0 | 1 | 2 | 3 ].
И точно так же, было бы ошибочно утверждать, что 2 из этого диапазона должна стать 128, а не 170.

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

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

michael_v89
11.08.2026 12:19Не совсем. Тут каждому целому числу соответствует какой-то отрезок в диапазоне [0…1). 1 именно не включается, ему соответствует значение 256. Если мы включаем левый край 0, то для равномерности это правило должно соблюдаться для всех подотрезков. Иначе будет неопределенность, одна точка будет принадлежать 2 отрезкам.
0 мапится на подотрезок[0, 1/256).
128 на[128/256, 129/256).
255 на[255/256, 256/256).
Kromster80
11.08.2026 12:19Верно, вы говорите о диапазонах (для простоты, опять же, нагляднее оперировать 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%), на котором белый это тоже условность и зависит от крутилки яркости, но это уже за пределами спора о деквантизации ))
michael_v89
11.08.2026 12:19у вас получается 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
Согласен, просто надо понимать, что это подходит не для всех случаев. Поэтому неправильно говорить “Правильно делать только так”.

Kromster80
11.08.2026 12:19Для стандартных RGB значений это весь диапазон и есть - верхняя граница в него входит - [0.0 - 1,0], а если бы не входила, то мы бы его просто перенормировали, чтобы входила, т.к. по факту она есть, а за ней ничего нет :-)

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

yamifa_1234
11.08.2026 12:19Присоединюсь к комментарию выше. Статья хорошая но спор вечный)
Кстати а что говорят по этому поводу разные стандарты? Как правильно конвертировать форматы чисел?
kuraga333
Не читал, но правильный ответ же "прибавить 1 и нормировать на 256"?
thiefsy
Чему равно #0088FF?