Как использовать ScanLine
свойство для обработки 24-битных растровых пикселей? Почему я предпочитаю использовать его, а не довольно часто используемое свойство Pixels
?
Как использовать свойство ScanLine для 24-битных растровых изображений?
Ответ 1
1. Введение
В этом посте я попытаюсь объяснить использование свойства ScanLine
только для формата 24-битного растрового пикселя, и если вам действительно нужно использовать его. Как сначала взгляните, что делает это свойство настолько важным.
2. ScanLine или нет...?
Вы можете спросить себя, почему использовать такую сложную технику, как использование ScanLine
свойство, похоже, когда вы можете просто использовать Pixels
для доступа к вашим растровым пикселям. Ответ - большая разница в производительности, заметная при выполнении модификаций пикселей даже на относительно небольшой площади пикселей.
Свойство Pixels
внутренне использует функции Windows API - GetPixel
и SetPixel
, для получения и установки значений цвета контекста устройства. Недостаток производительности в Pixels
заключается в том, что вам обычно нужно получать значения цвета пикселя, прежде чем изменять их, что внутренне означает вызов обоих упомянутых Функции Windows API. Свойство ScanLine
выигрывает эту гонку, потому что обеспечивает прямой доступ к памяти, в которой хранятся данные растрового пикселя. Прямой доступ к памяти происходит быстрее, чем два вызова функций Windows API.
Но это не значит, что свойство Pixels
абсолютно плохое, и вам следует избегать его использования во всех случаях. Когда вы собираетесь изменять только несколько пикселей (не больших областей), например, тогда Pixels
может быть достаточно для вас. Но не используйте его, когда вы собираетесь манипулировать областью пикселей.
3. Глубоко внутри пикселей
3.1 Исходные данные
Пиксельные данные растрового изображения (назовем их необработанными данными на данный момент), вы можете представить себе как одномерный массив байтов, содержащий последовательность значений интенсивности цветовых компонентов для каждого пикселя. Каждый пиксель в битовой карте состоит из фиксированного количества байтов в зависимости от используемого формата пикселей.
Например, формат 24-битного пикселя имеет 1 байт для каждого из его цветовых компонентов - для красного, зеленого и синего каналов. На следующем рисунке показано, как представить исходный массив байтов данных для такого 24-битного растрового изображения. Каждый цветной прямоугольник здесь представляет собой один байт:
3.2 Пример из практики
Представьте, что у вас есть 24-битное растровое изображение размером 3x2 (ширина 3px, высота 2px) и держите его в голове, потому что я попытаюсь объяснить некоторые внутренние элементы и показать принцип ScanLine
. Он настолько мал только из-за пространства, необходимого для глубокого просмотра внутри (для тех, кто имеет яркое зрение, это зеленый пример такого изображения в формате png здесь ↘ ↙: -)
3,3 Пиксельная композиция
Как сначала давайте посмотрим, как данные пикселя нашего растрового изображения внутренне хранятся; посмотрите на необработанные данные. На следующем рисунке показан массив байтов необработанных данных, где вы можете увидеть каждый байт нашего крошечного растрового изображения с его индексом в этом массиве. Вы также можете заметить, как группы из 3 байтов формируют отдельные пиксели и на каких координатах находятся эти пиксели, расположенные на нашем растровом изображении:
В другом виде показано следующее изображение. Каждый ящик представляет собой один пиксель нашего воображаемого растрового изображения. В каждом пикселе вы можете видеть его координаты и группу из 3 байтов с их индексами из массива байтов необработанных данных:
4. Жизнь с цветами
4,1. Начальные значения
Как мы уже знаем, пиксели в нашем воображаемом 24-битном растровом файле состоят из 3 байт - 1 байт для каждого цветового канала. Когда вы создали это растровое изображение в своем воображении, все эти байты во всех пикселях были против вашей воли, инициализированной значением максимального байта - до 255. Это означает, что все каналы имеют максимальную интенсивность цвета:
Когда мы посмотрим, какой цвет будет смешаться с этими начальными значениями канала для каждого пикселя, мы увидим, что наше растровое изображение entirely white
. Итак, когда вы создаете 24-битную растровую карту в Delphi, она изначально белая. Ну, по умолчанию белые будут растровыми в каждом пиксельном формате, но они могут отличаться от начальных значений байтов исходных данных.
5. Тайная жизнь ScanLine
Из приведенного выше чтения я надеюсь, что вы поняли, как данные растрового изображения хранятся в массиве байтов необработанных данных и как формируются отдельные пиксели из этих данных. Теперь перейдем к собственному свойству ScanLine
и как он может быть полезен при прямой обработке необработанных данных.
5,1. Назначение ScanLine
Основное блюдо этого сообщения, свойство ScanLine
, является индексированным свойством только для чтения, которое возвращает указатель на первый байт массив необработанных байтов данных, который принадлежит определенной строке в растровом изображении. Другими словами, мы запрашиваем доступ к массиву байтов необработанных данных для данной строки, и получаемый результат является указателем на первый байт этого массива. Индексный параметр этого свойства указывает индекс на основе 0 строки, для которой мы хотим получить эти данные.
Следующее изображение иллюстрирует наше воображаемое растровое изображение и указатели, которые мы получаем с помощью свойства ScanLine
, используя различные индексы строк:
5.2. Преимущество ScanLine
Итак, из того, что мы знаем, мы можем заключить, что ScanLine
дает нам указатель на некоторый массив байтов данных строки. И с этим массивом строк исходных данных мы можем работать - мы можем читать или перезаписывать его байты, но только в диапазоне границ массива определенной строки:
Ну, у нас есть массив интенсивностей цвета для каждого пикселя определенной строки. Рассмотрение итерации такого массива; было бы не очень удобно перебирать этот массив по одному байту и настраивать только одну из трех цветных частей пикселя. Лучше будет проходить через пиксели и каждый три цветовых байта корректировать с каждой итерацией - как и с Pixels
, как мы это делали.
5.3. Прыжки через пиксели
Чтобы упростить цикл массива строк, нам нужна структура, соответствующая нашим данным пикселя. К счастью, для 24-битных растровых изображений существует структура RGBTRIPLE
; в Delphi, переведенный как TRGBTriple
. Эта структура, короче, выглядит так (каждый элемент представляет интенсивность одного цветового канала):
type
TRGBTriple = packed record
rgbtBlue: Byte;
rgbtGreen: Byte;
rgbtRed: Byte;
end;
Поскольку я старался быть терпимым к тем, у кого есть версия Delphi ниже 2009 года, и потому что это делает код более понятным, я не буду использовать арифметику указателей для итерации, но массив с фиксированной длиной с указателем на него в следующем Примеры (арифметика указателя была бы менее читаема в Delphi 2009 ниже).
Итак, у нас есть структура TRGBTriple
для пикселя, и теперь мы определяем тип массива строк. Это упростит итерацию пикселей растровых строк. Этот я только что заимствовал из подразделения ShadowWnd.pas(в любом случае, дома одного интересного класса). Вот он:
type
PRGBTripleArray = ^TRGBTripleArray;
TRGBTripleArray = array[0..4095] of TRGBTriple;
Как вы можете видеть, он имеет ограничение в 4096 пикселей для строки, чего должно быть достаточно для обычно широких изображений. Если этого недостаточно, просто увеличьте верхнюю границу.
6. ScanLine на практике
6.1. Сделайте второй ряд черным
Начнем с первого примера. В этом случае мы объективизируем наше воображаемое растровое изображение, задаем его правильную ширину, высоту и формат пикселей (или, если хотите, бит глубины). Затем мы используем ScanLine
с параметром строки 1, чтобы получить указатель на массив байтов исходных данных второй строки. Указатель, который мы получаем, присваиваем переменной RowPixels
, которая указывает на массив TRGBTriple
, поэтому с этого времени мы можем взять его как массив пикселов строк. Затем мы перебираем этот массив по всей ширине растрового изображения и устанавливаем все значения цвета каждого пикселя равным 0, что приводит к растровому изображению с первой белой строкой (по умолчанию используется белый цвет, как указано выше), и что делает вторую строку черной, Это растровое изображение затем сохраняется в файле, но не удивляйтесь, когда вы его видите, это действительно очень мало:
type
PRGBTripleArray = ^TRGBTripleArray;
TRGBTripleArray = array[0..4095] of TRGBTriple;
procedure TForm1.Button1Click(Sender: TObject);
var
I: Integer;
Bitmap: TBitmap;
Pixels: PRGBTripleArray;
begin
Bitmap := TBitmap.Create;
try
Bitmap.Width := 3;
Bitmap.Height := 2;
Bitmap.PixelFormat := pf24bit;
// get pointer to the second row raw data
Pixels := Bitmap.ScanLine[1];
// iterate our row pixel data array in a whole width
for I := 0 to Bitmap.Width - 1 do
begin
Pixels[I].rgbtBlue := 0;
Pixels[I].rgbtGreen := 0;
Pixels[I].rgbtRed := 0;
end;
Bitmap.SaveToFile('c:\Image.bmp');
finally
Bitmap.Free;
end;
end;
6.2. Графическое изображение в оттенках серого с использованием яркости
В качестве своего рода значимого примера я размещаю здесь процедуру для масштабирования растровых изображений с использованием яркости. Он использует итерацию всех строк растровых изображений сверху вниз. Для каждой строки затем получается указатель на необработанные данные и, как и раньше, рассматривается как массив пикселей. Для каждого пикселя этого массива затем вычисляется значение яркости по этой формуле:
Luminance = 0.299 R + 0.587 G + 0.114 B
Это значение яркости затем присваивается каждой цветовой составляющей итерированного пикселя:
type
PRGBTripleArray = ^TRGBTripleArray;
TRGBTripleArray = array[0..4095] of TRGBTriple;
procedure GrayscaleBitmap(ABitmap: TBitmap);
var
X: Integer;
Y: Integer;
Gray: Byte;
Pixels: PRGBTripleArray;
begin
// iterate bitmap from top to bottom to get access to each row raw data
for Y := 0 to ABitmap.Height - 1 do
begin
// get pointer to the currently iterated row raw data
Pixels := ABitmap.ScanLine[Y];
// iterate the row pixels from left to right in the whole bitmap width
for X := 0 to ABitmap.Width - 1 do
begin
// calculate luminance for the current pixel by the mentioned formula
Gray := Round((0.299 * Pixels[X].rgbtRed) +
(0.587 * Pixels[X].rgbtGreen) + (0.114 * Pixels[X].rgbtBlue));
// and assign the luminance to each color component of the current pixel
Pixels[X].rgbtRed := Gray;
Pixels[X].rgbtGreen := Gray;
Pixels[X].rgbtBlue := Gray;
end;
end;
end;
И возможное использование вышеуказанной процедуры. Обратите внимание, что эту процедуру можно использовать только для 24-битных растровых изображений:
procedure TForm1.Button1Click(Sender: TObject);
var
Bitmap: TBitmap;
begin
Bitmap := TBitmap.Create;
try
Bitmap.LoadFromFile('c:\ColorImage.bmp');
if Bitmap.PixelFormat <> pf24bit then
raise Exception.Create('Incorrect bit depth, bitmap must be 24-bit!');
GrayscaleBitmap(Bitmap);
Bitmap.SaveToFile('c:\GrayscaleImage.bmp');
finally
Bitmap.Free;
end;
end;