При работе с графикой в Delphi часто возникает необходимость обрабатывать изображения попиксельно — применять фильтры, конвертировать цвета, анализировать содержимое. Стандартное свойство канвы Pixels[X,Y] решает эту задачу, но работает катастрофически медленно. Свойство ScanLine предоставляет прямой доступ к памяти изображения и ускоряет обработку в десятки и сотни раз.
Статья задумывалась как прямое продолжение TBitmap.PixelFormat, но была не закончена в своё время. Поэтому их следует воспринимать как один материал. Внутри будет много отсылок к ней.
Классический ScanLine
Синтаксис ScanLine
|
1 |
property ScanLine[Row: Integer]: Pointer; |
Свойство возвращает указатель на начало строки пикселей с номером Row. Нумерация начинается с нуля. Получив указатель, мы работаем напрямую с памятью без посредников.
Что такое строка пикселей?
Изображение в памяти представляет собой непрерывный блок данных, организованный построчно. Каждая строка — это последовательность пикселей, идущих слева направо:
|
1 2 3 4 |
Строка 0: [Пиксель 0][Пиксель 1][Пиксель 2]...[Пиксель W-1] Строка 1: [Пиксель 0][Пиксель 1][Пиксель 2]...[Пиксель W-1] ... Строка H-1: [Пиксель 0][Пиксель 1][Пиксель 2]...[Пиксель W-1] |
ScanLine[Row] возвращает адрес первого пикселя в строке Row. Дальше мы можем двигаться по строке, увеличивая указатель.
Проблема нетипизированного указателя
Свойство возвращает Pointer — нетипизированный указатель. Компилятор не знает, какого размера пиксели в изображении и какую структуру они имеют. Один пиксель может занимать:
- 1 бит (pf1bit) — 8 пикселей в байте, 2 цвета из палитры
- 4 бита (pf4bit) — 2 пикселя в байте, до 16 цветов из палитры
- 1 байт (pf8bit) — до 256 цветов из палитры
- 2 байта (pf15bit, pf16bit) — цвет закодирован непосредственно в битах
- 3 байта (pf24bit) — по байту на канал R, G, B
- 4 байта (pf32bit) — R, G, B плюс альфа-канал
Если мы попытаемся работать с данными, не зная их формат, результат будет непредсказуемым:
|
1 2 3 4 5 6 7 |
var Row: PRGBTriple; // Предполагаем 24-битный формат begin // Опасно! Формат изображения может быть любым Row := Bitmap.ScanLine[0]; Row^.rgbtRed := 255; // Возможно повреждение данных end; |
Если реальный формат окажется 32-битным, мы будем читать и записывать данные со смещением, повреждая изображение.
Решение: явное указание формата
Чтобы точно знать, с какими данными работаем, перед использованием ScanLine всегда устанавливаем свойство PixelFormat:
|
1 2 3 4 5 6 7 |
var Row: PRGBTriple; begin Bitmap.PixelFormat := pf24bit; // Теперь мы точно знаем формат Row := Bitmap.ScanLine[0]; // Безопасное приведение типа Row^.rgbtRed := 255; // Корректная работа end; |
После установки PixelFormat изображение конвертируется в указанный формат (если это необходимо), и мы можем быть уверены в структуре данных.
Принцип работы со ScanLine
Обработка изображения через ScanLine строится по простой схеме: внешний цикл перебирает строки, внутренний — пиксели в каждой строке. В начале каждой итерации внешнего цикла получаем указатель на текущую строку, затем двигаемся по ней от пикселя к пикселю.
Существует два основных шаблона работы со строкой пикселей. В первом мы получаем указатель на начало строки и последовательно инкрементируем его. Во втором мы воспринимаем строку пикселей, как массив.
Шаблон со смещением указателя
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
procedure ProcessBitmap(Bitmap: TBitmap); var X, Y: Integer; Row: PRGBTriple; begin // 1. Устанавливаем известный формат пикселей Bitmap.PixelFormat := pf24bit; // 2. Внешний цикл — перебираем строки for Y := 0 to Bitmap.Height - 1 do begin // 3. Получаем указатель на начало строки Row := Bitmap.ScanLine[Y]; // 4. Внутренний цикл — перебираем пиксели в строке for X := 0 to Bitmap.Width - 1 do begin // 5. Здесь работаем с текущим пикселем: // - читаем цвет: Row^.rgbtRed, Row^.rgbtGreen, Row^.rgbtBlue // - изменяем цвет: Row^.rgbtRed := новое_значение // 6. Переходим к следующему пикселю Inc(Row); end; end; end; |
Шаблон с массивом
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
procedure ProcessBitmapArray(Bitmap: TBitmap); type TRGBTripleArray = array[0..MaxInt div SizeOf(TRGBTriple) - 1] of TRGBTriple; PRGBTripleArray = ^TRGBTripleArray; var X, Y: Integer; Row: PRGBTripleArray; begin Bitmap.PixelFormat := pf24bit; for Y := 0 to Bitmap.Height - 1 do begin Row := Bitmap.ScanLine[Y]; for X := 0 to Bitmap.Width - 1 do begin // Обращаемся к пикселю по индексу Row[X].rgbtRed := 255; // Row[X].rgbtGreen := ...; // Row[X].rgbtBlue := ...; end; end; end; |
Какой шаблон выбрать
Первый шаблон со смещением указателя предпочтительнее по нескольким причинам:
Производительность. Операция Inc(Row) — это простое прибавление константы к адресу. Обращение Row[X] требует умножения индекса на размер элемента и сложения с базовым адресом при каждом обращении к пикселю.
Чистота кода. Не нужно объявлять дополнительные типы TRGBTripleArray и PRGBTripleArray. Указатель на одиночную запись уже объявлен в системных модулях.
Однако. Шаблон с массивом оправдан, когда нужен произвольный доступ к пикселям строки — например, при реализации алгоритмов размытия, где требуется одновременно читать соседние пиксели.
Форматы пикселей
Все форматы пикселей рассмотрены в статье TBitmap.PixelFormat. На практике работа ведётся в основном с четырьмя форматами:
| Формат | Бит на пиксель | Применение |
|---|---|---|
| pf1bit | 1 | Чёрно-белые изображения, маски |
| pf8bit | 8 | Изображения с палитрой до 256 цветов |
| pf24bit | 24 | Полноцветные изображения без прозрачности |
| pf32bit | 32 | Полноцветные изображения с альфа-каналом |
Каждый формат требует своего типа указателя и своего подхода к обработке. Примеры работы с каждым из них рассмотрим далее.
Формат pf24bit: Преобразование в оттенки серого
Это самый простой и распространённый формат для обработки изображений. Каждый пиксель занимает ровно 3 байта — по одному на каждый цветовой канал. Никаких палитр, никакой упаковки битов, никакого альфа-канала. Один пиксель — одна структура.
|
1 2 3 4 5 6 7 |
type PRGBTriple = ^TRGBTriple; TRGBTriple = packed record rgbtBlue: Byte; // Синий (0..255) rgbtGreen: Byte; // Зелёный (0..255) rgbtRed: Byte; // Красный (0..255) end; |
Видим, что порядок полей — BGR, а не RGB. Это наследие формата BMP, ибо Windows исторически хранит цвета именно так.
Продублирую гифку из описания pf24bit.

Преобразование в оттенки серого — это классическая задача обработки изображений. Для каждого пикселя вычисляем яркость по формуле, учитывающей особенности человеческого зрения (критика формулы), и записываем её во все три канала:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
procedure ConvertToGrayscale(Bitmap: TBitmap; const Value: TValue; out Result: TBitmap); var X, Y: Integer; SrcRow, DstRow: PRGBTriple; Gray: Byte; begin Result := TBitmap.Create; Result.PixelFormat := pf24bit; Result.SetSize(Bitmap.Width, Bitmap.Height); Bitmap.PixelFormat := pf24bit; for Y := 0 to Bitmap.Height - 1 do begin SrcRow := Bitmap.ScanLine[Y]; DstRow := Result.ScanLine[Y]; for X := 0 to Bitmap.Width - 1 do begin Gray := Round( SrcRow^.rgbtRed * 0.299 + SrcRow^.rgbtGreen * 0.587 + SrcRow^.rgbtBlue * 0.114 ); DstRow^.rgbtRed := Gray; DstRow^.rgbtGreen := Gray; DstRow^.rgbtBlue := Gray; Inc(SrcRow); Inc(DstRow); end; end; end; |
Формат pf32bit: Прозрачность на основе яркости
Этот формат расширяет pf24bit одним дополнительным байтом — альфа-каналом. Каждый пиксель занимает 4 байта, что удобно для выравнивания в памяти и делает доступ чуть быстрее. Альфа-канал управляет прозрачностью: 0 — полностью прозрачный, 255 — полностью непрозрачный.
|
1 2 3 4 5 6 7 8 |
type PRGBQuad = ^TRGBQuad; TRGBQuad = packed record rgbBlue: Byte; // Синий (0..255) rgbGreen: Byte; // Зелёный (0..255) rgbRed: Byte; // Красный (0..255) rgbReserved: Byte; // Альфа-канал (0..255) end; |
Порядок цветовых каналов тот же — BGR. Поле rgbReserved исторически называлось «зарезервированным», но сегодня повсеместно используется как альфа-канал.
Тип TRGBQuad объявлен в модуле Winapi.Windows.
Подробное описание формата pf32bit.

В примере вычисляем яркость каждого пикселя по уже знакомой формуле и записываем её в альфа-канал. Цвет пикселя сохраняется, но добавляется прозрачность: тёмные области становятся прозрачными, светлые — непрозрачными.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
procedure ApplyAlphaByBrightness(Bitmap: TBitmap; const Value: TValue; out Result: TBitmap); var X, Y: Integer; Gray: Byte; SrcRow, DstRow: PRGBQuad; begin Result := TBitmap.Create; Result.PixelFormat := pf32bit; Result.SetSize(Bitmap.Width, Bitmap.Height); Bitmap.PixelFormat := pf32bit; for Y := 0 to Bitmap.Height - 1 do begin SrcRow := Bitmap.ScanLine[Y]; DstRow := Result.ScanLine[Y]; for X := 0 to Bitmap.Width - 1 do begin // Вычисляем яркость Gray := Round( SrcRow^.rgbRed * 0.299 + SrcRow^.rgbGreen * 0.587 + SrcRow^.rgbBlue * 0.114 ); // Копируем цвет без изменений Move(SrcRow^, DstRow^, 4); // Яркость становится прозрачностью DstRow^.rgbReserved := Gray; // Переходим к следующему 4-байтному блоку Inc(SrcRow); Inc(DstRow); end; end; end; |
Ключевое отличие от остальных примеров в том, что источник также переводится в pf32bit. Тут это не особо оправдано, но сделано для максимальной идентичности аналогичному примеру для быстрого ScanLine, который будет ниже.
Также, здесь используется Move вместо покомпонентного копирования. Вместо трёх присваиваний:
|
1 2 3 |
DstPtr^.rgbRed := SrcPtr^.rgbRed; DstPtr^.rgbGreen := SrcPtr^.rgbGreen; DstPtr^.rgbBlue := SrcPtr^.rgbBlue; |
одна операция копирует все 4 байта разом, а затем перезаписывается только альфа-канал. Короче и быстрее.
После применения этой процедуры изображение сохраняет исходные цвета, но получает прозрачность:
- Тёмные пиксели (яркость близка к 0) — почти прозрачные
- Светлые пиксели (яркость близка к 255) — почти непрозрачные
- Промежуточные оттенки — частично прозрачные
Такой эффект часто используется для создания масок, виньеток или плавного перехода между изображениями.
Примечание об альфа-канале: При сохранении в BMP альфа-канал сохраняется, но не все программы корректно его отображают. Для полноценной работы с прозрачностью результат лучше сохранять в PNG.
Формат pf8bit: Grayscale-палитра и постеризация
В отличие от pf24bit и pf32bit, где цвет хранится непосредственно в пикселе, формат pf8bit использует палитру. Каждый пиксель — это один байт, индекс в таблице из 256 цветов. Сама палитра хранится отдельно в заголовке изображения.
Такой подход экономит память: 1 байт на пиксель вместо 3-4. Но ограничивает изображение 256 цветами.
Доступ к пикселям осуществляется очень просто — это указатель на байт:
|
1 2 3 4 5 6 7 8 9 10 |
type PByte = ^Byte; var Row: PByte; Index: Byte; begin Row := Bitmap.ScanLine[Y]; Index := Row^; // Индекс цвета в палитре (0..255) end; |
Подробно про pf8bit.

Мы хотим получить изображение в оттенках серого. Для этого установим палитру из 256 градаций.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
procedure SetGrayscalePalette(Bitmap: TBitmap); var I: Integer; Palette: array[0..255] of TRGBQuad; begin // Заполняем палитру оттенками серого for I := 0 to 255 do begin Palette[I].rgbRed := I; Palette[I].rgbGreen := I; Palette[I].rgbBlue := I; Palette[I].rgbReserved := 0; end; SetDIBColorTable(Bitmap.Canvas.Handle, 0, 256, Palette); end; |
Пример 1: Grayscale-палитра. Конвертируем цветное изображение в 8-битное с палитрой из 256 оттенков серого. Вычисляем яркость и записываем её как индекс — индекс 128 соответствует цвету (128, 128, 128) в палитре.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
procedure ConvertToGrayscale8bit(Bitmap: TBitmap; const Value: TValue; out Result: TBitmap); var X, Y: Integer; SrcRow: PRGBTriple; DstRow: PByte; Gray: Byte; begin Result := TBitmap.Create; Result.PixelFormat := pf8bit; Result.SetSize(Bitmap.Width, Bitmap.Height); // Устанавливаем grayscale-палитру SetGrayscalePalette(Result); Bitmap.PixelFormat := pf24bit; for Y := 0 to Bitmap.Height - 1 do begin SrcRow := Bitmap.ScanLine[Y]; DstRow := Result.ScanLine[Y]; for X := 0 to Bitmap.Width - 1 do begin // Вычисляем яркость — она же индекс в палитре Gray := Round( SrcRow^.rgbtRed * 0.299 + SrcRow^.rgbtGreen * 0.587 + SrcRow^.rgbtBlue * 0.114 ); DstRow^ := Gray; Inc(SrcRow); Inc(DstRow); end; end; end; |
Результат визуально идентичен 24-битному grayscale из первого раздела — те же 256 градаций яркости. Но размер данных втрое меньше: 1 байт на пиксель вместо 3. Для изображения 1000×1000 это приблизительно 1 МБ (≈ 0.95 МБ, помним, что в километре у нас 1024 метра) вместо 3 МБ. Плюс 1 КБ на палитру — это пренебрежимо мало.
Именно так работает большинство программ при сохранении grayscale-изображений — используют 8-битный формат с серой палитрой.
Пример 2: Постеризация. Сокращаем 256 оттенков до меньшего количества — например, до 8. Получаем плакатный эффект с резкими переходами между тонами.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
procedure Posterize8bit(Bitmap: TBitmap; const Value: TValue; out Result: TBitmap); var X, Y: Integer; SrcRow: PRGBTriple; DstRow: PByte; Gray, Levels, Step: Byte; begin Levels := Value.AsType<Byte>; if Levels < 2 then Levels := 2; Step := 256 div Levels; Result := TBitmap.Create; Result.PixelFormat := pf8bit; Result.SetSize(Bitmap.Width, Bitmap.Height); SetGrayscalePalette(Result); Bitmap.PixelFormat := pf24bit; for Y := 0 to Bitmap.Height - 1 do begin SrcRow := Bitmap.ScanLine[Y]; DstRow := Result.ScanLine[Y]; for X := 0 to Bitmap.Width - 1 do begin // Вычисляем яркость Gray := Round( SrcRow^.rgbtRed * 0.299 + SrcRow^.rgbtGreen * 0.587 + SrcRow^.rgbtBlue * 0.114 ); // Квантуем до нужного количества уровней DstRow^ := (Gray div Step) * Step; Inc(SrcRow); Inc(DstRow); end; end; end; |
При Levels = 8 получаем изображение с резкими переходами: чёрный, тёмно-серый, серый и так далее до белого. Чем меньше уровней, тем грубее и «плакатнее» результат.
Формат pf1bit: Монохромное изображение
Самый компактный формат — один бит на пиксель, всего два цвета. Восемь пикселей упакованы в один байт, что делает доступ чуть сложнее — нужны битовые операции.
Как и в pf8bit, цвета определяются палитрой — просто в ней всего две записи. Обычно это чёрный и белый, но можно задать любые два цвета.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
var Row: PByte; X: Integer; ByteIndex, BitIndex: Integer; PixelValue: Boolean; begin Row := Bitmap.ScanLine[Y]; ByteIndex := X div 8; BitIndex := 7 - (X mod 8); // Биты нумеруются справа налево // Чтение пикселя PixelValue := (Row[ByteIndex] and (1 shl BitIndex)) <> 0; // Установка белого Row[ByteIndex] := Row[ByteIndex] or (1 shl BitIndex); // Установка чёрного Row[ByteIndex] := Row[ByteIndex] and not (1 shl BitIndex); end; |
Внимание: старший бит (7) соответствует левому пикселю в байте, младший (0) — правому.
Смотрим описание формата pf1bit.

Пример: пороговое преобразование. Классическое преобразование в чёрно-белое: пиксели с яркостью выше порога становятся белыми, остальные — чёрными.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
procedure ConvertToMonochrome(Bitmap: TBitmap; const Value: TValue; out Result: TBitmap); var X, Y: Integer; SrcRow: PRGBTriple; DstRow: PByte; Gray: Byte; Threshold: Byte; ByteIndex, BitIndex: Integer; begin Threshold := Value.AsType<Byte>; Result := TBitmap.Create; Result.PixelFormat := pf1bit; Result.SetSize(Bitmap.Width, Bitmap.Height); Bitmap.PixelFormat := pf24bit; for Y := 0 to Bitmap.Height - 1 do begin SrcRow := Bitmap.ScanLine[Y]; DstRow := Result.ScanLine[Y]; for X := 0 to Bitmap.Width - 1 do begin // Вычисляем яркость Gray := Round( SrcRow^.rgbtRed * 0.299 + SrcRow^.rgbtGreen * 0.587 + SrcRow^.rgbtBlue * 0.114 ); ByteIndex := X div 8; BitIndex := 7 - (X mod 8); if Gray >= Threshold then DstRow[ByteIndex] := DstRow[ByteIndex] or (1 shl BitIndex) // Белый else DstRow[ByteIndex] := DstRow[ByteIndex] and not (1 shl BitIndex); // Чёрный Inc(SrcRow); end; end; end; |
Выбор порога:
- 128 — стандартный средний порог, делит диапазон пополам
- Ниже 128 — больше белого, изображение светлее
- Выше 128 — больше чёрного, изображение темнее
Для документов с текстом обычно подбирают порог экспериментально — где-то между 100 и 180 в зависимости от качества скана.
Изображение в формате pf1bit занимает в 24 раза меньше, чем pf24bit. Именно этот формат используется для факсов, сканов документов и штрих-кодов.
Быстрый ScanLine
В предыдущих примерах мы вызывали ScanLine[Y] для каждой строки. Это свойство не просто возвращает указатель — оно выполняет проверки и вычисления при каждом обращении. Этого можно избежать и существенно ускорить обработку больших битмапов.
Немного теории
DIB-секция (а TBitmap по умолчанию хранит данные именно так) держит пиксели в непрерывном блоке памяти. Строки идут снизу вверх (bottom-up) с выравниванием каждой строки до границы 4 байт (DWORD-aligned). Изменить порядок для VCL.TBitmap возможности нет, поэтому считаем, что это всегда так.
Поэтому ScanLine[Height-1] — это указатель на самый младший адрес массива (первая строка в памяти = последняя строка изображения). Таким образом, можно получить указатель на начало один раз и дальше перемещаться арифметикой указателей.
Для битмапа формата pf32bit (и только для него) применим такой базовый шаблон:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
procedure FastProcess(Bitmap: TBitmap; const Value: TValue; out Result: TBitmap); var I: Integer; SrcPtr, DstPtr: PRGBQuad; Total: Integer; begin Result := TBitmap.Create; Result.PixelFormat := pf32bit; Result.SetSize(Bitmap.Width, Bitmap.Height); Bitmap.PixelFormat := pf32bit; // Встаём на начало блока данных — один раз SrcPtr := Bitmap.ScanLine[Bitmap.Height - 1]; DstPtr := Result.ScanLine[Result.Height - 1]; Total := Bitmap.Width * Bitmap.Height; for I := 0 to Total - 1 do begin // Здесь обработка пикселя — аналогично примерам выше // SrcPtr^ — исходный пиксель, DstPtr^ — результат Inc(SrcPtr); Inc(DstPtr); end; end; |

Как ранее говорилось, размер строки пикселей выравнивается по границе 4 байт. Из-за выравнивания в 4 байта в конце каждой строки могут быть padding-байты. Если гнать линейно через Width * Height — указатель съедет и мы начнём читать мусор из padding’а как пиксели. Для pf32bit это работает (4 байта на пиксель, строка всегда кратна 4), но для pf24bit, pf8bit, pf1bit — катастрофа.

На рисунке массив байт битмапа 31×32 формата pf24bit. При ширине в 31 пиксел, в конце каждой строки есть три неиспользуемых байта, которые присутствуют, чтобы добить длину строки до количества, кратного 4. Поэтому, при перемещении по непрерывному массиву байт надо их учитывать.
Для вычисления длины строки с учётом выравнивания есть штатная функция BytesPerScanline.
Примерный шаблон для работы с битмапом формата pf24bit:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
procedure FastProcess(Bitmap: TBitmap; const Value: TValue; out Result: TBitmap); var X, Y: Integer; SrcPtr, DstPtr: PRGBTriple; SrcBytesPerLine, DstBytesPerLine: Integer; SrcPadding, DstPadding: Integer; begin Result := TBitmap.Create; Result.PixelFormat := pf24bit; Result.SetSize(Bitmap.Width, Bitmap.Height); Bitmap.PixelFormat := pf24bit; // Длина строки с учётом выравнивания SrcBytesPerLine := BytesPerScanline(Bitmap.Width, 24, 32); DstBytesPerLine := BytesPerScanline(Result.Width, 24, 32); // Количество padding-байтов в конце строки SrcPadding := SrcBytesPerLine - Bitmap.Width * SizeOf(TRGBTriple); DstPadding := DstBytesPerLine - Result.Width * SizeOf(TRGBTriple); // Встаём на начало блока данных — один раз SrcPtr := Bitmap.ScanLine[Bitmap.Height - 1]; DstPtr := Result.ScanLine[Result.Height - 1]; for Y := 0 to Bitmap.Height - 1 do begin for X := 0 to Bitmap.Width - 1 do begin // Здесь обработка пикселя — аналогично примерам выше // SrcPtr^ — исходный пиксель, DstPtr^ — результат Inc(SrcPtr); Inc(DstPtr); end; // Перешагиваем padding в конце строки Inc(PByte(SrcPtr), SrcPadding); Inc(PByte(DstPtr), DstPadding); end; end; |
Это шаблон с «перешагиванием» зоны незначащих выравнивающих байт с целью выйти на начало следующей строки. Это далеко не единственный способ работать с массивом пикселей битмапа. В следующих примерах будут показаны разные способы и прокомментированы различия.
Быстрый Grayscale (pf24bit)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
procedure ConvertToGrayscaleFast(Bitmap: TBitmap; const Value: TValue; out Result: TBitmap); var X, Y: Integer; SrcPtr, DstPtr: PRGBTriple; SrcPtrStart, DstPtrStart: PRGBTriple; SrcBytesPerLine, DstBytesPerLine: Integer; Gray: Byte; begin Result := TBitmap.Create; Result.PixelFormat := pf24bit; Result.SetSize(Bitmap.Width, Bitmap.Height); Bitmap.PixelFormat := pf24bit; SrcBytesPerLine := BytesPerScanline(Bitmap.Width, 24, 32); DstBytesPerLine := BytesPerScanline(Result.Width, 24, 32); SrcPtrStart := Bitmap.ScanLine[Bitmap.Height - 1]; DstPtrStart := Result.ScanLine[Result.Height - 1]; for Y := 0 to Bitmap.Height - 1 do begin SrcPtr := SrcPtrStart; DstPtr := DstPtrStart; for X := 0 to Bitmap.Width - 1 do begin Gray := Round( SrcPtr^.rgbtRed * 0.299 + SrcPtr^.rgbtGreen * 0.587 + SrcPtr^.rgbtBlue * 0.114 ); DstPtr^.rgbtRed := Gray; DstPtr^.rgbtGreen := Gray; DstPtr^.rgbtBlue := Gray; Inc(SrcPtr); Inc(DstPtr); end; Inc(PByte(SrcPtrStart), SrcBytesPerLine); Inc(PByte(DstPtrStart), DstBytesPerLine); end; end; |
Этот код отличается от шаблона в подходе к навигации по строкам. Шаблон вычисляет padding (разницу между длиной строки и полезными данными) и после каждой строки перешагивает именно padding.
Этот код не вычисляет padding вообще. Вместо этого он использует два уровня указателей:
- SrcPtrStart, DstPtrStart — указатели на начало текущей строки, сдвигаются на полную BytesPerScanline после каждой строки
- SrcPtr, DstPtr — рабочие указатели, каждую строку сбрасываются на PtrStart и бегут по пикселям
Рабочие указатели ничего не знают о padding — они честно проходят Width пикселей и выбрасываются. А PtrStart прыгает ровно на BytesPerScanline, автоматически перешагивая и данные, и padding одним действием.
По сути, это эквивалент классического ScanLine[Y] в цикле, только адрес следующей строки вычисляется сложением, а не через свойство. Это в любом случае быстрее классического варианта, потому что ScanLine при каждом вызове делает ряд проверок, а здесь — одно сложение указателя с константой.
Оба подхода корректны. Шаблон чуть экономнее на переменных. Этот вариант чуть нагляднее — видно разделение на «где мы стоим» и «куда мы бежим».
Быстрый Alpha by Brightness (pf32bit)
Для pf32bit выравнивание не играет роли — 4 байта на пиксель всегда кратны 4. Можно пройти весь блок линейно:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
procedure ApplyAlphaByBrightnessFast(Bitmap: TBitmap; const Value: TValue; out Result: TBitmap); var I, Total: Integer; SrcPtr, DstPtr: PRGBQuad; Gray: Byte; begin Result := TBitmap.Create; Result.PixelFormat := pf32bit; Result.SetSize(Bitmap.Width, Bitmap.Height); Bitmap.PixelFormat := pf32bit; SrcPtr := Bitmap.ScanLine[Bitmap.Height - 1]; DstPtr := Result.ScanLine[Result.Height - 1]; Total := Result.Width * Result.Height; for I := 0 to Total - 1 do begin Gray := Round( SrcPtr^.rgbRed * 0.299 + SrcPtr^.rgbGreen * 0.587 + SrcPtr^.rgbBlue * 0.114 ); Move(SrcPtr^, DstPtr^, 4); DstPtr^.rgbReserved := Gray; Inc(SrcPtr); Inc(DstPtr); end; end; |
Ключевое отличие от шаблона состоит в том, здесь полностью линейный проход. Для pf32bit padding невозможен — 4 байта на пиксель всегда кратны границе выравнивания 4 байта. Поэтому двойной цикл Y, X не нужен. Один цикл по Total, никаких вычислений padding, никаких BytesPerScanline. Самый чистый случай быстрого Scanline.
Быстрый Grayscale 8-bit (pf8bit)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
procedure ConvertToGrayscale8bitFast(Bitmap: TBitmap; const Value: TValue; out Result: TBitmap); var X, Y: Integer; SrcPtr: PRGBTriple; DstPtr: PByte; SrcPtrStartAddr, DstPtrStartAddr: NativeInt; SrcBytesPerLine, DstBytesPerLine: Integer; Gray: Byte; begin Result := TBitmap.Create; Result.PixelFormat := pf8bit; Result.SetSize(Bitmap.Width, Bitmap.Height); SetGrayscalePalette(Result); Bitmap.PixelFormat := pf24bit; SrcBytesPerLine := BytesPerScanline(Bitmap.Width, 24, 32); DstBytesPerLine := BytesPerScanline(Result.Width, 8, 32); SrcPtrStartAddr := NativeInt(Bitmap.ScanLine[Bitmap.Height - 1]); DstPtrStartAddr := NativeInt(Result.ScanLine[Result.Height - 1]); for Y := 0 to Bitmap.Height - 1 do begin SrcPtr := PRGBTriple(SrcPtrStartAddr + Y*SrcBytesPerLine); DstPtr := PByte(DstPtrStartAddr + Y*DstBytesPerLine); for X := 0 to Bitmap.Width - 1 do begin Gray := Round( SrcPtr^.rgbtRed * 0.299 + SrcPtr^.rgbtGreen * 0.587 + SrcPtr^.rgbtBlue * 0.114 ); DstPtr^ := Gray; Inc(SrcPtr); Inc(DstPtr); end; end; end; |
Ещё один вариант навигации по строкам — через вычисление адреса.
Начальные адреса хранятся как NativeInt, а указатель на начало каждой строки вычисляется прямым умножением:
|
1 2 |
SrcPtr := PRGBTriple(SrcPtrStartAddr + Y * SrcBytesPerLine); DstPtr := PByte(DstPtrStartAddr + Y * DstBytesPerLine); |
Padding не вычисляется — он автоматически учтён внутри BytesPerScanline. Рабочие указатели SrcPtr и DstPtr не переживают итерацию внешнего цикла — каждую строку пересоздаются из базового адреса.
Сравнение трёх подходов:
- Шаблон с padding — идём указателем, в конце строки перешагиваем padding
- Два уровня указателей — PtrStart шагает на BytesPerScanline, рабочий указатель переназначается с каждой новой строкой
- Этот вариант — адрес строки вычисляется каждый раз через Base + Y * Stride
Третий подход допускает произвольный доступ — можно обойти строки в любом порядке, начать с середины, пропустить строки. Два предыдущих привязаны к последовательному проходу. На практике для попиксельной обработки разница в производительности незначительна — одно умножение на строку.
Так и просится пример: для зеркального отражения по вертикали такой подход с произвольной адресацией строк очень удобен — читаем строку Y, пишем в строку Height — 1 — Y:
|
1 2 |
SrcPtr := PRGBTriple(SrcPtrStartAddr + Y * SrcBytesPerLine); DstPtr := PRGBTriple(DstPtrStartAddr + (Bitmap.Height - 1 - Y) * DstBytesPerLine); |
С последовательным проходом пришлось бы заводить два независимых указателя, один идущий вперёд, другой — назад. Здесь же произвольная адресация получается естественно.
Быстрая постеризация (pf8bit)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
procedure Posterize8bitFast(Bitmap: TBitmap; const Value: TValue; out Result: TBitmap); var X, Y: Integer; SrcPtr: PRGBTriple; DstPtr: PByte; SrcBytesPerLine, DstBytesPerLine: Integer; SrcPadding, DstPadding: Integer; Gray, Levels, Step: Byte; begin Levels := Value.AsType<Byte>; if Levels < 2 then Levels := 2; Step := 256 div Levels; Result := TBitmap.Create; Result.PixelFormat := pf8bit; Result.SetSize(Bitmap.Width, Bitmap.Height); SetGrayscalePalette(Result); Bitmap.PixelFormat := pf24bit; SrcBytesPerLine := BytesPerScanline(Bitmap.Width, 24, 32); DstBytesPerLine := BytesPerScanline(Result.Width, 8, 32); SrcPadding := SrcBytesPerLine - Bitmap.Width * SizeOf(TRGBTriple); DstPadding := DstBytesPerLine - Result.Width * SizeOf(Byte); SrcPtr := Bitmap.ScanLine[Bitmap.Height - 1]; DstPtr := Result.ScanLine[Result.Height - 1]; for Y := 0 to Bitmap.Height - 1 do begin for X := 0 to Bitmap.Width - 1 do begin Gray := Round( SrcPtr^.rgbtRed * 0.299 + SrcPtr^.rgbtGreen * 0.587 + SrcPtr^.rgbtBlue * 0.114 ); DstPtr^ := (Gray div Step) * Step; Inc(SrcPtr); Inc(DstPtr); end; Inc(PByte(SrcPtr), SrcPadding); Inc(DstPtr, DstPadding); end; end; |
Этот код — чистое применение базового шаблона без каких-либо вариаций. Вычисляем padding, встаём на начало блока, двойной цикл, перешагиваем padding в конце строки. Вся специфика постеризации — только в одной строке внутри цикла:
|
1 2 |
DstPtr^ := (Gray div Step) * Step; |
Быстрый Threshold (pf1bit)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
procedure ConvertToMonochromeFast(Bitmap: TBitmap; const Value: TValue; out Result: TBitmap); var X, Y: Integer; SrcPtr: PRGBTriple; DstRow: PByte; SrcBytesPerLine, DstBytesPerLine: Integer; SrcPadding: Integer; Gray, Threshold: Byte; ByteIndex, BitIndex: Integer; begin Threshold := Value.AsType<Byte>; Result := TBitmap.Create; Result.PixelFormat := pf1bit; Result.SetSize(Bitmap.Width, Bitmap.Height); Bitmap.PixelFormat := pf24bit; SrcBytesPerLine := BytesPerScanline(Bitmap.Width, 24, 32); DstBytesPerLine := BytesPerScanline(Result.Width, 1, 32); SrcPadding := SrcBytesPerLine - Bitmap.Width * SizeOf(TRGBTriple); SrcPtr := Bitmap.ScanLine[Bitmap.Height - 1]; DstRow := Result.ScanLine[Result.Height - 1]; for Y := 0 to Bitmap.Height - 1 do begin for X := 0 to Bitmap.Width - 1 do begin Gray := Round( SrcPtr^.rgbtRed * 0.299 + SrcPtr^.rgbtGreen * 0.587 + SrcPtr^.rgbtBlue * 0.114 ); ByteIndex := X div 8; BitIndex := 7 - (X mod 8); if Gray >= Threshold then DstRow[ByteIndex] := DstRow[ByteIndex] or (1 shl BitIndex) else DstRow[ByteIndex] := DstRow[ByteIndex] and not (1 shl BitIndex); Inc(SrcPtr); end; Inc(PByte(SrcPtr), SrcPadding); Inc(DstRow, DstBytesPerLine); end; end; |
Формат pf1bit ломает шаблон в двух местах.
Нет Inc(DstPtr) во внутреннем цикле. В шаблоне оба указателя синхронно шагают по пикселям. Здесь источник идёт по PRGBTriple как обычно, а приёмник — нет. Один байт приёмника содержит 8 пикселей, поэтому вместо инкремента указателя — адресация через DstRow[ByteIndex] с битовыми операциями.
Нет DstPadding. В шаблоне приёмник перешагивает padding после каждой строки. Здесь рабочий указатель DstRow не двигается во внутреннем цикле вообще, поэтому перешагивать нечего — сразу шагаем на полную DstBytesPerLine:
|
1 |
Inc(DstRow, DstBytesPerLine); |
По сути DstRow ведёт себя как PtrStart из варианта с двумя уровнями указателей — указывает на начало строки, а доступ внутри строки — через индекс.
Итоговые выводы
Быстрый Scanline — не отдельная техника, а простая идея: получить начальный адрес данных один раз и дальше перемещаться арифметикой указателей, не обращаясь к свойству ScanLine[Y] повторно.
Мы разобрали несколько вариантов реализации этой идеи:
- Padding — вычисляем неиспользуемые байты в конце строки и перешагиваем их
- Два уровня указателей — PtrStart шагает на BytesPerScanline, рабочий указатель сбрасывается каждую строку
- Вычисление адреса — Base + Y * Stride, допускает произвольный порядок обхода строк
- Линейный проход — для pf32bit padding невозможен, двойной цикл не нужен
Все подходы корректны. Выбор между ними — дело вкуса и конкретной задачи. Общее одно: обращение к ScanLine происходит один раз при инициализации, а не на каждой строке.
Бенчмарк: Классический vs Быстрый

Абсолютные значения зависят от процессора, размера изображения и фоновой нагрузки. Диаграмма показывает соотношение, а не конкретные миллисекунды. Бенчмарк встроен в демо-приложение — загрузите своё изображение и проверьте сами.
Диаграмма подтверждает тезис статьи: быстрый ScanLine стабильно опережает классический по всем форматам.
Alpha 32 — самый быстрый из всех. Формат pf32bit идеально выровнен: 4 байта на пиксель, строка всегда кратна 4, padding отсутствует. Процессор работает с выровненными данными без штрафов. При этом разница Classic/Fast здесь тоже заметна — накладные расходы на вызов ScanLine[Y] никуда не деваются.
Mono 1-bit — самый медленный. Это ожидаемо: битовая адресация, упаковка 8 пикселей в байт, операции сдвига и маскирования на каждый пиксель.
Тяжесть вычислений внутри цикла. Если на каждый пиксель приходится Round() с тремя умножениями на Double, то время вызова ScanLine[Y] растворяется на фоне вычислений. Быстрый вариант всё равно быстрее, но разница скромнее.
Практический вывод: быстрый ScanLine убирает накладные расходы на доступ к строкам. Если обработка пикселя тяжёлая, оптимизировать нужно её, а не способ навигации по строкам.
Кэшированный ScanLine
Быстрый Scanline хорош для последовательного прохода — сверху вниз (последовательно по непрерывному массиву), строка за строкой. Но есть задачи, где доступ к строкам непоследователен и непредсказуем:
- Фильтры свёртки — для каждого пикселя нужны соседние строки: текущая, верхняя, нижняя;
- Масштабирование — строка-источник вычисляется по коэффициенту, порядок произвольный;
- Повороты на произвольный угол — координаты источника определяются тригонометрией;
- Алгоритмы диффузии ошибок — ошибка квантования распространяется на соседние строки.
В таких случаях быстрый Scanline с последовательным Inc() не подходит — мы прыгаем по строкам хаотично. Вычисление адреса через Base + Y * Stride работает, но при многократном обращении к одной и той же строке мы каждый раз выполняем умножение заново.
Идея кэшированного ScanLine проста: завести массив указателей размером с высоту изображения, изначально заполненный nil. При первом обращении к строке — вызвать ScanLine[Y] и сохранить результат. При повторном — вернуть сохранённый указатель без каких-либо вычислений:
|
1 2 3 |
if Cache[Y] = nil then Cache[Y] := Bitmap.ScanLine[Y]; Result := Cache[Y]; |
Каждая строка запрашивается у VCL.Bitmap не более одного раза. При повторных обращениях — чтение из массива, то есть одна операция индексации без каких-либо проверок, умножений и вызовов свойств.
Подход особенно выгоден, когда одна и та же строка запрашивается многократно — а для фильтров свёртки это именно так: строка является «нижним соседом» для одного пикселя, «текущей» для другого и «верхним соседом» для третьего.
Пример
Переписывать предыдущие примеры с кэшем не имеет смысла. Все они — последовательный проход строка за строкой, каждая строка читается ровно один раз. Кэш там не ускорит ничего, а только добавит накладные расходы: массив, проверку на nil, присваивание.
Кэш оправдан там, где одна строка запрашивается многократно. Нужны примеры, где это происходит естественно:
- Фильтр свёртки (размытие, резкость) — для каждого пикселя читаем 3, 5 или более строк, и соседние пиксели делят большинство этих строк;
- Масштабирование методом ближайшего соседа — несколько строк результата могут ссылаться на одну строку источника;
- Диффузия ошибок (Флойд — Стейнберг) — ошибка распространяется на текущую и следующую строку, обе нужны одновременно.
Поэтому возьмём в качестве примера какой-нибудь эффект, где происходит непоследовательное обращение к строкам битмапа. Например, эффект стекла.
Эффект стекла: Базовая версия

|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
procedure GlassEffectBasic(Bitmap: TBitmap; const Value: TValue; out Result: TBitmap); var X, Y, DstX, DstY: Integer; Width, Height: Integer; Radius: Integer; SrcRow, DstRow: PRGBTriple; SrcPixel, DstPixel: PRGBTriple; begin Radius := Value.AsType<Byte>; Bitmap.PixelFormat := pf24bit; Result := TBitmap.Create; Result.Assign(Bitmap); Width := Bitmap.Width; Height := Bitmap.Height; for Y := 0 to Height - 1 do begin SrcRow := Bitmap.ScanLine[Y]; for X := 0 to Width - 1 do begin DstX := X + Round((Random(101) / 100 - 0.5) * Radius); DstY := Y + Round((Random(101) / 100 - 0.5) * Radius); if (DstX < 0) or (DstX >= Width) or (DstY < 0) or (DstY >= Height) then continue; SrcPixel := SrcRow; Inc(SrcPixel, X); DstRow := Result.ScanLine[DstY]; DstPixel := DstRow; Inc(DstPixel, DstX); DstPixel^ := SrcPixel^; end; end; end; |
Источник читается последовательно — Bitmap.ScanLine[Y] вызывается один раз на строку во внешнем цикле, это классический ScanLine.
Проблема — в записи результата. DstY вычисляется случайно, поэтому Result.ScanLine[DstY] вызывается внутри двойного цикла для каждого пикселя. Для изображения 1000×1000 — до миллиона вызовов свойства, при том что уникальных строк всего тысяча. Именно эту проблему решает кэшированная версия.
Эффект стекла: Версия с кэшем

|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
procedure GlassEffectCached(Bitmap: TBitmap; const Value: TValue; out Result: TBitmap); var X, Y, DstX, DstY: Integer; Width, Height: Integer; Radius: Integer; SrcRow: PRGBTriple; SrcPixel, DstPixel: PRGBTriple; DstCache: array of PRGBTriple; begin Radius := Value.AsType<Byte>; Bitmap.PixelFormat := pf24bit; Result := TBitmap.Create; Result.Assign(Bitmap); Width := Bitmap.Width; Height := Bitmap.Height; // Кэш для результата, заполнен nil // SetLength для управляемого типа гарантирует обнуление SetLength(DstCache, Height); for Y := 0 to Height - 1 do begin SrcRow := Bitmap.ScanLine[Y]; for X := 0 to Width - 1 do begin DstX := X + Round((Random(101) / 100 - 0.5) * Radius); DstY := Y + Round((Random(101) / 100 - 0.5) * Radius); if (DstX < 0) or (DstX >= Width) or (DstY < 0) or (DstY >= Height) then continue; SrcPixel := SrcRow; Inc(SrcPixel, X); // Ленивая инициализация // ScanLine вызывается только при первом обращении к строке if DstCache[DstY] = nil then DstCache[DstY] := Result.ScanLine[DstY]; DstPixel := DstCache[DstY]; Inc(DstPixel, DstX); DstPixel^ := SrcPixel^; end; end; end; |
Ленивый кэш — строка запрашивается у VCL.Bitmap только при первом попадании в неё. Повторные обращения — чтение указателя из массива. Источник идёт последовательно, кэшировать его незачем.
Комментарии по коду
Формула яркости повторяется 10 раз в идентичном виде. Формула повторяется намеренно — каждый пример компилируется самостоятельно. Никуда не надо смотреть дополнительно, каждая процедура самодостаточна.
Value.AsType<Byte> — используется в постеризации и монохроме, но не в grayscale и alpha. Это следствие архитектуры демо-приложения: все процедуры имеют единую сигнатуру для унификации вызовов. Процедуры, которым параметр не нужен, просто его игнорируют.
Возможные ошибки
Не установлен PixelFormat
Самая частая ошибка. Без явной установки формат может оказаться любым — pfDevice, pf16bit, что угодно. Код будет компилироваться, но данные окажутся не той структуры, которую ожидает указатель.
|
1 2 3 4 5 6 |
// Опасно Row := Bitmap.ScanLine[0]; // Безопасно Bitmap.PixelFormat := pf24bit; Row := Bitmap.ScanLine[0]; |
Несоответствие типа указателя и формата
Установили pf32bit, а работаем через PRGBTriple. Или наоборот. Указатель шагает не с тем размером — каждый Inc смещается на 3 байта вместо 4 (или наоборот), данные «плывут» с накоплением ошибки от пикселя к пикселю.
|
1 2 3 4 |
Bitmap.PixelFormat := pf32bit; // Ошибка: нужен PRGBQuad Row := PRGBTriple(Bitmap.ScanLine[0]); Inc(Row); // Шагнули на 3 байта вместо 4 |
Обращение к Canvas между вызовами ScanLine
ScanLine работает с DIB-данными напрямую. Обращение к Canvas (рисование, вызов Canvas.Handle) может вызвать пересоздание внутреннего GDI-объекта, и ранее полученные указатели станут невалидными.
|
1 2 3 4 5 |
Ptr := Bitmap.ScanLine[0]; // GDI может пересоздать буфер Bitmap.Canvas.TextOut(0, 0, 'Test'); // Указатель уже невалиден Ptr^.rgbtRed := 255; |
Правило: получил ScanLine — работай только с ним. Всё рисование через Canvas — до или после.
Игнорирование padding в быстром ScanLine
Для pf32bit padding невозможен. Для всех остальных форматов — возможен. Линейный проход через Width * Height по pf24bit изображению со строкой не кратной 4 байтам приведёт к смещению указателя в область выравнивающих байтов.
|
1 2 3 4 5 6 7 |
// Опасно Total := Width * Height; for I := 0 to Total - 1 do begin // На границе строки указатель попадёт в padding Inc(Ptr); end; |
Забыли про bottom-up порядок строк
ScanLine[0] — верхняя строка изображения, но в памяти она расположена по старшему адресу. ScanLine[Height-1] — самый младший адрес. При быстром ScanLine мы стартуем с ScanLine[Height-1] и идём по адресам в сторону увеличения. Если перепутать, будет нарушение доступа или другая малоприятная фигня.
Integer вместо NativeInt для адресной арифметики
В 32-битных приложениях Integer и указатель одного размера — 4 байта. В 64-битных указатель — 8 байт, а Integer по-прежнему 4. Адрес обрежется, программа упадёт.
Заключение
Мы рассмотрели три способа работы с пикселями через ScanLine:
Классический ScanLine — вызов свойства ScanLine[Y] на каждой строке. Просто, надёжно, достаточно для большинства задач. Главное ограничение — обращение к свойству внутри цикла имеет свою цену.
Быстрый ScanLine — получаем начальный адрес данных один раз и дальше перемещаемся арифметикой указателей. Несколько вариантов реализации — через padding, два уровня указателей, вычисление адреса, линейный проход — но идея одна: не обращаться к свойству ScanLine повторно.
Кэшированный ScanLine — массив указателей на строки, заполняемый лениво при первом обращении. Оправдан там, где доступ к строкам непоследователен и одна строка запрашивается многократно.
Какой способ выбрать — зависит от задачи:
| Задача | Подход |
|---|---|
| Последовательный проход, простая логика | Классический ScanLine |
| Последовательный проход, критична скорость | Быстрый ScanLine |
| Произвольный доступ к строкам | Кэшированный ScanLine |
Все три подхода объединяет одно правило: PixelFormat нужно установить явно до первого обращения к ScanLine. Без этого — непредсказуемый формат данных и неопределённое поведение.
Скачать
Друзья, спасибо за внимание!
PixelFormat+ScanLine: тут дополненная демонстрацией массива байт демка для форматов
Исходник (zip) 271 Кб. Delphi XE 7
Исполняемый файл (zip) 1.12 Мб.
ScanLineDemo: тут описанные эффекты и бенчмарк
Исходник (zip) 846 Кб. Delphi XE 7, XE 13
Исполняемый файл (zip) 1.85 Мб.