Тема прямого доступа к пикселям как-то сама-собой получилась из темы быстрого доступа к пикселям. Потому что не важно, в какой технологии или какой библиотеке сейчас работаем, как только возникает необходимость быстрой обработки изображения, в конечном счете, приходим к необходимости иметь указатель на массив пикселей.
Имея такой указатель, мы максимально быстро может осуществить и навигацию по массиву, и изменения в нем. Главное, чтобы эти изменения отобразились потом в результирующем изображении.
Нет секретных технологий скоростного доступа к пикселям. Есть указатель на начало массива пикселей. Получением которого сейчас и займемся.
TBitmap. ScanLine
Старый добрый TBitmap, активно руганный на всевозможных форумах. Люди до сих пор пишут свои реализации работы с DIB, в надежде получить выигрыш в скорости и секретные знания, недоступные в стандартном TBitmap.
Пример подобного подхода, когда отказываемся от готовых решений и пишем все рукми.
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 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
//********************************************************************* // Пример загрузки образа DIB из файла BMP с помощью DIB секции // "Графика в проектах Delphi" Осипов Дмитрий //********************************************************************* Function LoadBMP(DC:HDC; const FileName : PChar):HBitmap; var F: THandle; memDC: hDC; DIB_S: HBitmap; BFH: TBitmapFileHeader; BIH: TBitmapInfoHeader; pBitmapInfo: ^TBitmapInfo; Bits: array of WORD; pBits: Pointer; X,Y,HeaderSize,I: DWORD; begin Result:=0; // проверка факта существования файла if FileAge(StrPas(FileName)) < 0 then RAISE EExternal.CreateFmt('Файл "%s: не обнаружен!', [StrPas(FileName)]); F := CreateFile(FileName, GENERIC_READ,FILE_SHARE_READ, nil, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,0); TRY // читаем заголовок файла ReadFile(F, BFH, SizeOf(TBitmapFileHeader), X, nil); // Проверка на наличие сигнатуры «MB» IF BFH.bfType <> $4D42 then RAISE EExternal.Create('Неверный формат файла!'); // выясняем размер заголовка растра, по нему // мы подберем нужную структуру ReadFile(F, HeaderSize, SizeOf(DWORD), X, nil); //возвращаемся к началу заголовка SetFilePointer(F, SizeOf(TBitmapFileHeader), NIL, FILE_BEGIN); IF HeaderSize = SizeOf(TBitmapCoreHeader) then // формат OS/2 ReadFile(F, BIH, SizeOf(TBitmapCoreHeader), X, nil) else // 3-я и любая другая версия ReadFile(F, BIH, SizeOf(TBitmapInfoHeader), X, nil); // смещаемся к началу битового массива SetFilePointer(F, BFH.bfOffBits, nil, FILE_BEGIN); // устанавливаем размер массива if BIH.biSizeImage > 0 then SetLength(Bits, BIH.biSizeImage DIV SizeOf(WORD)) else // иногда поле biSizeImage=0 SetLength(Bits, (BIH.biWidth * BIH.biHeight * BIH.biBitCount DIV 8) DIV SizeOf(WORD)); // заполняем массив пикселов for i := 0 to HIGH(Bits)-1 do ReadFile(F, Bits[i], SizeOf(WORD), X, nil); // распределение памяти под структуру TBitmapInfo if BIH.biClrUsed>0 then GetMem(pBitmapInfo, SizeOf(TBitmapInfoHeader) + 12 + 256 * SizeOf(TRGBQuad)) else GetMem(pBitmapInfo, SizeOf(TBitmapInfoHeader)); pBitmapInfo^.bmiHeader := BIH; if BIH.biClrUsed > 0 then begin //если есть цветовая таблица //первый байт цветовой таблицы bfOffBits biClrUsed*4 Y := BFH.bfOffBits - BIH.biClrUsed*4; SetFilePointer(F,Y,NIL,FILE_BEGIN); //заполняем цветовую таблицу for i := Y to BFH.bfOffBits - 1 do // последний байт цветовой таблицы BFH.bfOffBits 1 ReadFile(F,pBitmapInfo^.bmiColors[i-Y], sizeof(TRGBQuad), X, nil); end; // работа с таблицей цветов завершена // создаем совместимый контекст устройства memDC := CreateCompatibleDC(DC); // создаем DIB секцию с параметрами, повторяющими // DIB образ из исходного файла DIB_S := CreateDIBSection(memDC, pBitmapInfo^, DIB_RGB_COLORS, pBits, 0, 0); SelectObject(memDC,DIB_S); // из за того, что секция создается с // неинициализированным массивом пикселов pBits, // переносим в этот массив данные, полученные из файла StretchDIBits(memDC, 0, 0, pBitmapInfo^.bmiHeader.biWidth, pBitmapInfo^.bmiHeader.biHeight, 0, 0, pBitmapInfo^.bmiHeader.biWidth, pBitmapInfo^.bmiHeader.biHeight, Bits, pBitmapInfo^, DIB_RGB_COLORS, SRCCOPY); Result:=DIB_S; DeleteDC(memDC) FINALLY CloseHandle(F); END; end; |
Хотя все давным-давно уже сделано непосредственно в TBitmap. Класс TBitmap, кто бы что ни говорил, это очень удачная инкапсуляция всего того геморроя, который пришлось бы пережить, реализуя работу с DIB руками.
Доступ к массиву пикселей можно легко получить через свойство 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 27 28 29 30 |
//********************************************************************** // Доступ к пикселям через ScanLine //********************************************************************** const // Решили, что работаем с 32-битным форматом PixelSize = 4; function InvertBitmapScanLine(const ABitmap: TBitmap): TBitmap; var x,y: Integer; w,h: Integer; s,d: PByte; begin h := ABitmap.Height; w := ABitmap.Width; ABitmap.PixelFormat := pf32bit; Result := CreateBitmap(w, h, pf32bit); s := ABitmap.ScanLine[h-1]; d := Result.ScanLine[h-1]; x := w*h; for y := 0 to x-1 do begin PRGBTriple(d)^.rgbtRed := 255 - PRGBTriple(s)^.rgbtRed; PRGBTriple(d)^.rgbtGreen := 255 - PRGBTriple(s)^.rgbtGreen; PRGBTriple(d)^.rgbtBlue := 255 - PRGBTriple(s)^.rgbtBlue; inc(s,PixelSize); inc(d,PixelSize); end; end; |
Можно было бы конечно просто идти в цикле по Y и брать указатель на начало каждой новой строки, как ScanLine[Y] . Но метод GetScanLine, обслуживающий свойство ScanLine, помимо вычисления указателя на просимую строку bitmap, выполняет еще ряд действий и проверок. Этих действий хотелось бы избежать, потому что 1) уверены в своей непогрешимости, и 2) нам нужен просто указатель на нужный пиксель. Поэтому перед заходом в цикл, мы просто встаем на начала массивов источника и приемника и далее просто смещаем указатели.
Как-то странно выглядит фраза:
1 |
s := ABitmap.ScanLine[h-1] |
Как будто мы встаем не в начало, а в самый конец. Дело в том, что изображение в стандартном bitmap хранится в перевернутом виде. Когда мы требуем дать нам 0-ую строку (Row=0), метод GetScanLine вычисляет ее как:
1 |
Row := biHeight - Row – 1 |
Т.е. на самом деле нам вернут самую «нижнюю» и двигаться в этом случае надо снизу вверх. Если бы мы запрашивали ABitmap.ScanLine[0], нам пришлось бы декрементировать указатели на значение 2*Width всякий раз после цикла по X. Потому что один Width – только что прошли по строке, и надо вернуться снова на начало строки, и другой Width – чтобы выйти на начало строки, которая «выше». А это лишние действия, которые можно и нужно избежать.
Или можно заменить получение ссылки на начало строки чем-то более легковесным, нежели GetScanLine. Для этого нам нужно знать: стартовый указатель и ширину строки в байтах. Указатель получим как ScanLine[0] . В статье все исходники написаны для 32-битного формата. При использовании других форматов для подсчета ширины строки удобно использовать функцию BytesPerScanline, которая находится в Vcl.Graphics. Берет параметрами 3 значения:
- PixelsPerScanline — ширина, bitmap.Width;
- BitsPerPixel — сколько бит на цвет;
- Alignment — значение выравнивания, считаем, что 32.
В том случае, когда нужны пиксели строго по координатам, можно сделать следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// Где-то выше посчитали: // ScanBeg = ABitmap.ScanLine[0] // W - Ширина в байтах BytesPerScan(bmpWidth, Deep*8, 32) // Deep - глубина цвета. 1,2,3 или 4 байта function GetPixelColor(ScanBeg: PByte; x,y,w,deep: Integer): Longint; var PPixel: PLongint; begin // Выходим на линию Y. ScanBeg указывает на самую нижнюю, // поэтому идем вверх, т.е. минусуем, затем прибавляем // смещение внутри строки, т.е. выходим на X,Y PPixel := PLongint(Integer(ScanBeg) - Y * W + X * deep); // Что-то делаем по указателю и что-то возвращаем // ... // Например, "сырое" целочисленное значение цвета Result := PPixel^; end; |
«Живой» пример перевода изображения формата OpenCV в TBitmap. Функция, отвечающая за это в библиотеке для OpenCV 2.4.13 для Delphi (об этой библиотеке в следующей статье), а именно ocv.utils.cvImage2Bitmap, написана не самым лучшим образом. Поэтому пришлось переписать в свое время. Работает раза в 3 быстрее оригинала. Ограничений на размер изображения не имеет.
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 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
//******************************************************************** // Taken from ocv.utils, fixed, faster // Convert PIplImage to TBitmap //******************************************************************** function cvImage2Bitmap(img: PIplImage): TBitmap; var bmp: TBitmap; // result bitmap deep: Integer;// deep of image i, j, k: Integer; // loop variables // destonation bitmap pointers: d, p: PByte; // d - start (from end bits), p - current pointer // source bitmap pointers: s, v: PByte; // s - start, v - current pointer // byte ber pixel: Channels: Integer; // 1,2,3 or 4 // bytes per source width sStep: Integer; // bytes per destination width dStep: Integer; begin Result := NIL; if (img <> NIL) then begin bmp := TBitmap.Create; bmp.Width := img^.Width; bmp.Height := img^.Height; Channels := img^.nChannels; sStep := img^.WidthStep; deep := Channels * img^.depth; case deep of 16: bmp.PixelFormat := pf16bit; 24: bmp.PixelFormat := pf24bit; 32: bmp.PixelFormat := pf32bit; else begin bmp.PixelFormat := pf8bit; deep := 8; end; end; // calc bytes per result bitmap width dStep := BytesPerScanline(bmp.Width, deep, 32); // start pointer of source pixel bitmap s := Pointer(img^.ImageData); // start scanline destination bitmap d := bmp.Scanline[0]; // vertical scan for i := 0 to img^.Height - 1 do begin p := PByte(Integer(d) - i*dStep); v := PByte(Integer(s) + i*sStep); // horizontal scan for j := 0 to img^.Width - 1 do begin // scan by pixel channels for k := 0 to Channels - 1 do begin p^:= v^; inc(p); inc(v); end; end; end; Result := bmp; end; end; |
GDI+. TGPBitmap. LockBits
Для получения указателя на массив пикселей в GDI+ для класса TGPBitmap предусмотрена пара методов:
1 2 3 4 5 |
function LockBits(rect: TGPRect; flags: UINT; format: TPixelFormat; out lockedBitmapData: TBitmapData): TStatus; function UnlockBits(var lockedBitmapData: TBitmapData): TStatus; |
Первый метод блокирует битовый образ (весь или часть его) в системной памяти. Область блокировки задается прямоугольником rect. Режим чтения и/или записи задается параметром flags. Глубина цвета определяется параметром format. Просимая глубина может отличаться от текущей глубины изображения.
Заблокированный таким образом массив будет доступен в структуре lockedBitmapData. Указатель на массив находится в поле Scan0. Ширина строки в байтах содержится в поле Stride. Если Stride имеет отрицательное значение, это означает, что двигаться надо снизу вверх. Для экономии места и времени будем считать, что знак положительный. По окончании работы с массивом образ надо разблокировать методом UnlockBits.
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 52 53 54 55 56 57 58 59 60 |
//********************************************************************** // Доступ к пикселам через блокировку GDI+ //********************************************************************** function InvertGDIPLockEx(const ABitmap: TBitmap): TBitmap; var x,y: Integer; w,h: Integer; s,d: PRGBQuad; src: TGPBitmap; dst: TGPBitmap; srcData: TBitmapData; dstData: TBitmapData; begin h := ABitmap.Height; w := ABitmap.Width; ABitmap.PixelFormat := pf32bit; // Теперь создаем TGPBitmap без вызова LoadGPBitmapFromGraphic // У нас точно TBitmap и тратить время на анализ типа // изображения не имеет смысла src := TGPBitmap.Create(ABitmap.Handle, ABitmap.Palette); dst := TGPBitmap.Create(w,h); try // Блокируем источник src.LockBits(GDIPAPI.MakeRect(0,0,w,h),// Весь образ ImageLockModeRead, // Только чтение PixelFormat32bppARGB,// 4-байтовый пиксель srcData); // Блокируем приемник dst.LockBits(GDIPAPI.MakeRect(0,0,w,h), ImageLockModeWrite,// Только запись PixelFormat32bppARGB, dstData); // Встаем на начало массивов s := PRGBQuad(srcData.Scan0); d := PRGBQuad(dstData.Scan0); // Цикл по строкам for y := 0 to h-1 do begin // Цикл внутри строки for x := 0 to w-1 do begin d^.rgbRed := 255 - s^.rgbRed; d^.rgbGreen := 255 - s^.rgbGreen; d^.rgbBlue := 255 - s^.rgbBlue; // Смещение указателеей на след.пиксель в bitmap inc(s); inc(d); end; end; // Разблокировка образов src.UnlockBits(srcData); dst.UnlockBits(dstData); // Получение результата Result := SaveGPBitmapToBitmap(dst); finally FreeAndNil(Src); FreeAndNil(Dst); end; end; |
Как видно по коду, отказались от вызова LoadGPBitmapFromGraphic. Функция безусловно полезная и универсальная. Но у нас на входе TBitmap. Поэтому давайте сильно упростим себе жизнь и запишем просто:
1 |
src := TGPBitmap.Create(ABitmap.Handle, ABitmap.Palette); |
Чтобы обратиться к любому пикселю по координатам (X,Y) надо посчитать указатель на него. Будем считать, как индекс в массиве 32-разрядных целых.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
type // Объявим такой тип TPixels = array of DWORD; PPixels = ^TPixels; var ... idx: Integer; // Посчитанный индекс в массиве step: Integer; // Ширина строки begin ... // Ширина строки в пикселях step := srcData.Stride div PixelSize; // В циклах считаем индекс массива for y := 0 to h-1 do begin for x := 0 to w-1 do begin // посчитали индекс в массиве пикселей idx := y * step + x; s := PRGBQuad(@TPixels(srcData.Scan0)[idx]); d := PRGBQuad(@TPixels(dstData.Scan0)[idx]); ... end; end; |
Пример использования: Эффект огня
Direct2D. TWicImage. IWICBitmapLock
Добрались до DirectX. Это все по-прежнему есть в стандартной Delphi. Для работы с битовыми матрицами используется интерфейс IWICBitmap. Именно на его основе формируется ID2D1Bitmap для дальнейших манипуляций и рисований.
Строго говоря, массив пикселей в DirectX сокрыт в недрах CPU, и «в терем тот прекрасный нет входа никому». Это официальная парадигма. Однако, достучаться до массива пикселей все таки возможно.
Речь пойдет об интерфейсах IWICBitmap, IWICBitmapLock. Описаны в модуле Winapi.Wincodec. Отправной точкой будем считать эту статью. Но, будучи программистами, людьми прогрессивными, т.е. ленивыми, воспроизводить полностью код не будем, ибо многое уже сделано в классе TWicImage.
Класс TWICImage инкапсулирует Microsoft Windows Imaging Component (WIC), позволяющий загружать форматы графических файлов, зарегистрированные в WIC. Поддерживает форматы: BMP, GIF, ICO, JPEG, PNG, TIFF, Windows Media Photo и прочие.
Итак. Если верить статье, а оснований ей не верить нет, перед тем как начинать что-то делать необходимо создать экземпляр IWICImagingFactory, с помощью него создать IWICBitmapDecoder, получить первый (и единственный) фрейм IWICBitmapFrameDecode, и только потом создать IWICBitmap. Этого всего делать не будем, потому что все есть в начинке TWicImage, и как это делается всегда можно подсмотреть в исходниках. Итак, начнем с пункта 4 статьи. Свойство Handle класса TWicImage имеет тип IWICBitmap. Это как раз то, что нам нужно. Далее вызываем метод Lock (см.п.5. статьи), получаем интерфейс IWICBitmapLock.
1 2 3 4 5 6 7 8 9 |
if (SUCCEEDED(hr)) { // Obtain a bitmap lock for exclusive write. // The lock is for a 10x10 rectangle starting at the top // left of the bitmap hr = pIBitmap->Lock(&rcLock, WICBitmapLockWrite, &pILock); } |
Далее получаем указатель на начало битового массива с помощью метода GetDataPointer только что полученного экземпляра IWICBitmapLock.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
if (SUCCEEDED(hr)) { UINT cbBufferSize = 0; BYTE *pv = NULL; // Retrieve a pointer to the pixel data. if (SUCCEEDED(hr)) { hr = pILock->GetDataPointer(&cbBufferSize, &pv); } // Pixel manipulation using the image data pointer pv. // ... // Release the bitmap lock. SafeRelease(&pILock); } } |
У нас есть все, что нужно. Т.к. интересует не только чтение пикселей, но и запись, создаем два экземпляра TWicImage. Один для чтения – исходный bitmap, второй для записи — результат.
Сам процесс вполне уже привычный. Это та часть, которая во фрагменте выше звучит как «Pixel manipulation using the image data pointer pv».
Вот как это звучит в Delphi. Проверок на успех операций делать не стал, код и так объемный.
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 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
//********************************************************************** // Инвертируем bitmap через TWICImage IWICBitmap IWICBitmapLock //********************************************************************** function InvertIWICBitmap(const ABitmap: TBitmap): TBitmap; var x,y: Integer; w,h: Integer; s,d: PByte; src: TWICImage; dst: TWICImage; mem: TMemoryStream; srcRectLock: WICRect; srcStride: Cardinal; srcSize: Cardinal; srcLock: IWICBitmapLock; srcData: WICInProcPointer; dstLock: IWICBitmapLock; dstData: WICInProcPointer; begin // Стандартное начало h := ABitmap.Height; w := ABitmap.Width; ABitmap.PixelFormat := pf32bit; Result := CreateBitmap(w, h, pf32bit); // Создаем WIC изображение для исходника src := TWICImage.Create; src.Assign(ABitmap); // Создаем WIC изображение для приемника dst := TWICImage.Create; dst.Assign(Result); // Область блокировки - все изображение srcRectLock.X := 0; srcRectLock.Y := 0; srcRectLock.Width := w; srcRectLock.Height := h; // src.Handle имеет тип IWICBitmap - то что надо // Воспроизводим pIBitmap->Lock(&rcLock, WICBitmapLockWrite, &pILock) // Блокируем для чтения весь образ src.Handle.Lock(srcRectLock, WICBitmapLockRead, srcLock); // Получаем ширину строки в байтах srcLock.GetStride(srcStride); // Получаем указатель на массив пикселей // pILock->GetDataPointer(&cbBufferSize, &pv) srcLock.GetDataPointer(srcSize, srcData); // Аналогично поступаем с приемником, блок для записи dst.Handle.Lock(srcRectLock, WICBitmapLockWrite, dstLock); // Получаем указатель на массив пикселей приемника dstLock.GetDataPointer(srcSize, dstData); s := PByte(srcData); d := PByte(dstData); try for y := 0 to h-1 do begin for x := 0 to w-1 do begin PRGBTriple(d)^.rgbtRed := 255 - PRGBTriple(s)^.rgbtRed; PRGBTriple(d)^.rgbtGreen := 255 - PRGBTriple(s)^.rgbtGreen; PRGBTriple(d)^.rgbtBlue := 255 - PRGBTriple(s)^.rgbtBlue; inc(s, PixelSize); inc(d, PixelSize); end; end; finally // обязательно освободить, иначе ничего не получим на выходе srcLock := nil; dstLock := nil; end; // Сохраняем в результат в обычный TBitmap mem := TMemoryStream.Create; try dst.SaveToStream(mem); mem.Position := 0; Result.LoadFromStream(mem); finally // Не забываем все освободить FreeAndNil(Src); FreeAndNil(Dst); FreeAndNil(mem); end; end; |
Нельзя не заметить, что подход ровно такой же, как в GDI+. Заблокировать участок, получить массив, что-то сделать, разблокировать.
Конечно, много времени тратится на создание всех промежуточных классов и эту функцию в реальности использовать вряд ли стоит. Поэтому листинг для подсмотреть – как получить и модифицировать напрямую массив пикселей для IWICBitmap, как выгрузить в обычный TBitmap или как загрузиться из TBitmap.
Но даже при всех интерфейсных тормозах и потоковых выгрузках картинка 800×800 обрабатывается за 13-17 миллисекунд. Неплохо, правда?
FastDIB
Посмотрим, что есть хорошего вне Delphi. Хорошего, но без дополнительных dll. Есть ряд интересных библиотек, допустим, FreeImage или ImageEn, но они с собой тащат ряд дополнительных модулей. Последняя еще и платная. Для серьезного проекта, возможно, это и не зазорно. Но давайте исходить из концепции «маленького проекта». Быстро, легко, портабельно.
С древних времен люди пытаются найти свой особенный путь наибыстрейшей работы с битовой матрицей. Вот пример такого подхода. И, что ни говори, работа с bitmap с помощью этой библиотеки оправдывает свое название. Это действительно быстро. Однако, если заглянуть под капот, увидим код, очень похожий на тот, который реализован в TBitmap. И массив пикселей, в конечном счете, хранится все там же. Скачать можно тут.
Взял на себя смелость включить в состав демо проекта. Исходник очень небольшой. Ключевой класс – TFastDIB. Указателем на начало массива пикселей в случае TFastDIB является свойство Pixels32[0].
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 |
//********************************************************************** // Инвертируем bitmap через FastDIB //********************************************************************** function InvertFastDIB(const ABitmap: TBitmap): TBitmap; var x,y: Integer; w,h: Integer; src: TFastDIB; dst: TFastDIB; s,d: PByte; begin h := ABitmap.Height; w := ABitmap.Width; Result := CreateBitmap(w, h, pf32bit); ABitmap.PixelFormat := pf32bit; src := TFastDIB.Create; src.LoadFromHandle(ABitmap.Handle); dst := TFastDIB.Create(w,h,32); s := PByte(src.Pixels32[0]); d := PByte(dst.Pixels32[0]); try for y := 0 to h-1 do begin for x := 0 to w-1 do begin PRGBTriple(d)^.rgbtRed := 255 - PRGBTriple(s)^.rgbtRed; PRGBTriple(d)^.rgbtGreen := 255 - PRGBTriple(s)^.rgbtGreen; PRGBTriple(d)^.rgbtBlue := 255 - PRGBTriple(s)^.rgbtBlue; inc(s,PixelSize); inc(d,PixelSize); end; end; dst.Draw(Result.Canvas.Handle,0,0); finally FreeAndNil(Src); FreeAndNil(Dst); end; end; |
Стоит отметить, что тут, в отличие от TBitmap, при обращении к ScanLine (Pixels32), не происходит подсчета «настоящего» индекса. Хочешь 0-ю строку, пожалуйста, возвращает именно 0-ю строку.
Изображение, помним, перевернуто. Но для текущей задачи, как и для большинства прочих, это не имеет никакого значения.
В каждом методе мы получаем результирующий TBitmap разными способами. Здесь мы его просто рисуем. Для демонстрации возможностей TFastDIB.
Graphics32
Что тут скажешь… Это легенда. В отличие от многих аналогов, библиотека живет, дышит и активно развивается. Кто в теме, давно имеет ее в своем багаже. Кто еще не обзавелся, активно приглашаю сюда. Ее надо поставить. Изучать исходники. Наслаждаться результатом.
Однако, по прежнему ратую за то, что если есть возможность обойтись без использования дополнительных компонент, значит надо обходиться без них. Имейте ввиду, что исходный ход распространяется под лицензией MPL 1.1 / LGPL 2.1. Это означает, что ваш продукт, или часть его, использующая graphics32, должна идти под той же лицензией и с открытым кодом.
Не все мои заказчики испытывали радость от такой новости.
Снова взял на себя смелость включить в проект сильно урезанный вариант. Надеюсь, что вы все таки поставите полный комплект по ссылке выше.
Основа библиотеки — класс TBitmap32. И указателем на начало массива пикселей будет являться свойство Bits. Свойство ради этого и существует. Все просто.
1 |
s := PColor32Entry(src.Bits); |
Для работы с компонентами R, G, B используем тип PColor32Entry, который, по сути, повторяет PRGBQuad. Можем вообще использовать его вместо PColor32Entry. Просто хочется продемонстрировать типы Graphics32. Остальное все как обычно.
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 |
//********************************************************************** // Доступ к пикселам средствами Graphics32 //********************************************************************** function InvertGR32(const ABitmap: TBitmap): TBitmap; var x,y: Integer; w,h: Integer; src: TBitmap32; dst: TBitmap32; s,d: PColor32Entry; begin src := TBitmap32.Create; src.Assign(ABitmap); h := src.Height; w := src.Width; dst := TBitmap32.Create; dst.Width := w; dst.Height := h; Result := CreateBitmap(w, h, pf32bit); try s := PColor32Entry(src.Bits); d := PColor32Entry(dst.Bits); x := w*h; for y := 0 to x-1 do begin d^.R := 255-s^.R; d^.G := 255-s^.G; d^.B := 255-s^.B; inc(s); inc(d); end; Result.Assign(dst); finally FreeAndNil(Src); FreeAndNil(Dst); end; end; |
ПолучениеTBitmap (Result) из TBitmap32 (dst) также проще простого:
1 |
Result.Assign(dst); |
Дополнение 1. Скорость обработки
Вернемся к теме скоростной обработки битовой матрицы. В такой могучей библиотеке, как graphics32, наверняка есть что-то готовое для нашей операции инвертирования. И действительно есть. В модуле GR32_Filters находим прямо таки конкретную функцию для нашей операции. Пишем, пробуем.
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 |
function InvertGR32Filters(const ABitmap: TBitmap): TBitmap; var w,h: Integer; src: TBitmap32; dst: TBitmap32; begin src := TBitmap32.Create; src.Assign(ABitmap); h := src.Height; w := src.Width; dst := TBitmap32.Create; dst.Width := w; dst.Height := h; Result := CreateBitmap(w, h, pf32bit); try Invert(dst,src); Result.Assign(dst); finally FreeAndNil(Src); FreeAndNil(Dst); end; end; |
Интересуемся начинкой функции Invert и обнаруживаем, что внутри происходит точно такой же пробег по массиву пикселей, но для инвертирования используется операция XOR. Ну что же, напишем свой вариант.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
//********************************************************************** // Быстрое инвертирование через св-во ScanLine[y] //********************************************************************** function InvertScanLineFast(const ABitmap: TBitmap): TBitmap; var x,y: Integer; w,h: Integer; src: PRGBQuad; dst: PRGBQuad; begin h := ABitmap.Height; w := ABitmap.Width; Result := CreateBitmap(w, h, pf32bit); ABitmap.PixelFormat := pf32bit; src := PRGBQuad(ABitmap.ScanLine[h-1]); dst := PRGBQuad(Result.ScanLine[h-1]); x := w * h; for y := 0 to x - 1 do begin PINT(dst)^ := PINT(src)^ xor $FFFFFFFF; inc(src); inc(dst); end; end; |
И как тебе такое, Илон Маск?
Кстати, обычный пробег по TBitmap, описанный в самом начале, дает результат ничуть не хуже.
Дополнение 2. Попытка быстрой отрисовки
Может рисованием получиться добиться сногсшибательных скоростей? GDI+ не имеет продвинутых инструментов обработки массива пикселей, зато имеет просто уникальный набор эффектов при рисовании. Воспользуемся TColorMatrix при рисовании инвертированного изображения.
Инициализируем цветовую матрицу следующим образом:
Таким образом при выводе изображения, цвет будет считаться по каждой компоненте R, G, B, как:
R = –1.0*R + 1.0*255 = 255-R
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 |
//***************************************************************** // Нарисовать инвертированную матрицу силами GDI+ //***************************************************************** function InvertGDIPDraw(const ABitmap: TBitmap): TBitmap; var w,h: Integer; src: TGPBitmap; IA: TGPImageAttributes; CM: TColorMatrix; GP: TGPGraphics; begin h := ABitmap.Height; w := ABitmap.Width; ABitmap.PixelFormat := pf32bit; Result := CreateBitmap(w,h, pf32bit); src := TGPBitmap.Create(ABitmap.Handle, ABitmap.Palette); try // Создаем холст GDI+ прямо на холсте результата GP := TGPGraphics.Create(Result.Canvas.Handle); IA := TGPImageAttributes.Create; // Инициализируем цветовую матрицу ZeroMemory(@CM, SizeOf(CM)); CM[0,0] := -1; CM[1,1] := -1; CM[2,2] := -1; CM[3,3] := 1; CM[4,0] := 1; CM[4,1] := 1; CM[4,2] := 1; CM[4,3] := 0; CM[4,4] := 1; // Устанавливаем IA.SetColorMatrix(CM); // рисуем GP.DrawImage(src, MakeRect(0.0,0,w,h),0,0,w,h, UnitPixel, IA); finally FreeAndNil(Src); FreeAndNil(IA); FreeAndNil(GP); end; end; |
Скорость рисования оставляет желать лучшего. Намного быстрее обработать Bitmap до вывода.
Выводы
Посмотрим, что у нас по скорости при использовании разных методик.
Честно говоря, все, за исключением последней «рисовашки», показывают примерно одно и то же время. Одно и то же очень малое время. Обработка картинки 800 x 800 занимает примерно 3-4 миллисекунды.
Некоторые методы просто тратят время на создание и инициализацию своих классов. Сделано в угоду чистоты эксперимента и демонстрации, как добраться до массива пикселей в каждом конкретном случае.
Не надо гнаться за какими-то мифическими супер-быстрыми технологиями. Обычный TBitmap + ScanLine показывает просто сногсшибательный результат.
P.S.
В структуре TRGBQuad есть поле rgbReserved, которое в теории должно быть равно 0 и не использоваться. Да как бы не так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
//********************************************************************** // Установить альфа канал //********************************************************************** function SetAlpha(const ABitmap: TBitmap): TBitmap; var x,y: Integer; w,h: Integer; src: PByte; begin h := ABitmap.Height; w := ABitmap.Width; ABitmap.PixelFormat := pf32bit; Result := ABitmap; src := PByte(ABitmap.ScanLine[h-1]); for y := 0 to h - 1 do for x := 0 to w - 1 do begin PRGBQuad(src)^.rgbReserved := 255-trunc(x/w*PRGBTriple(src)^.rgbtRed); inc(src, 4); end; end; |
Перед установкой в TImage (imgRes) сделаем так:
1 2 3 |
FBitmap.AlphaFormat := afDefined; imgRes.Picture.Assign(FBitmap); |
Скачать
Друзья, спасибо за внимание!
Информация о новых статьях смотрим в телеграм-канале.
Не забываем комментировать и подписываться )))
Скачать (5.22 Мб): Исходники (Delphi XE 7-10)
Скачать (5.10 Мб): Исполняемый файл
Интересно бы ещё сравнить с FireMonkey TBitmap, там тоже есть прямой доступ
Имеете ввиду пару (FMX.Graphics.TBitmap):
И (FMX.Graphics.TBitmapData):
Если да, то принцип работы ровно тот же самый — получить указатель и гнать по массиву. По скорости сравнить, возможно, было бы интересно. Может напишите про это? У вас роль — Автор.
Сравнительные тесты штука полезная, и будут периодически публиковаться. Пока приоритет — сравнить GDI, GDI+ и Direct2D. Бытует мнение, дескать, Direct2D — не такой шустрый, GDI+ — вообще глючный тормоз. Это не совсем так. Просто разные технологии надо для разных областей применять. И по возможности, ближе к API.
Спасибо за доверие :). Может и напишу, но сейчас пока туго со временем..