Алгоритм Grayscale на Delphi

Август 26, 2011 — Шарахов А.П.

Преобразовать цветное изображения в черно-белое (оттенки серого) можно очень быстро, потратив 3 такта CPU на пиксель.

В типе TColor красный цвет хранится в младшем байте, далее зеленый, за ним синий. Они могут быть извлечены при помощи следующих операторов:

Red := Color and $FF;
Green := Color shr 8 and $FF;
Blue := Color shr 16 and $FF;

Хорошо известна формула для вычисления яркости цветного изображения по интенсивности цветовых составляющих:

Lum := Red * 0.299 + Green * 0.587 + Blue * 0.114;

или в целочисленной арифметике:

Lum := (Red * 77 + Green * 150 + Blue * 29) shr 8; // div 256

Новое значение цвета пикселя в градациях серого получается при помощи задания одинаковой интенсивности всех цветовых составляющих:

Color := Lum * $00010101;

Казалось бы, все просто. Но если запрограммировать алгоритм в лоб, то обнаружится, что преобразование изображениия размером 800x600 с разрешением 32bpp занимает больше секунды:

procedure BitmapToGrayscale(Bitmap: TBitmap);
var
  x, y, Color: integer;
begin;
  with Bitmap do
    for y:=0 to Height-1 do
      for x:=0 to Width-1 do begin;
        Color:=Canvas.Pixels[x, y];
        Canvas.Pixels[x, y]:=((77 * (Color        and $FF)  //Red
                            + 150 * (Color shr 8  and $FF)  //Green
                             + 29 * (Color shr 16 and $FF)  //Blue
                              ) shr 8
                             ) * $00010101;
        end;
  end;

Еще обиднее, что все это время потрачено отнюдь не на наше преобразование, а на вызовы функций получения и установки цвета пикселя. Давайте воспользуемся тем, что все пиксели изображения в формате pf32Bit лежат вплотную друг за другом. При этом учтем, что порядок следования цветовых составляющих не совпадает с типом TColor, а именно: синий цвет хранится в младшем байте, далее зеленый, за ним красный. Исправленная процедура работает в 1000-2000 раз быстрее:

procedure Grayscale1(pBuf: pInteger; Count: integer);
var
  Color: integer;
begin;
  repeat;
    Color:=pBuf^;
    pBuf^:=((29 * (Color        and $FF)  //Blue
          + 150 * (Color shr 8  and $FF)  //Green
           + 77 * (Color shr 16 and $FF)  //Red
            ) shr 8
           ) * $00010101;
    dec(Count);
    inc(pBuf);
    until Count<=0;
  end;
 
procedure ShaBitmapToGrayscale1(Bitmap: TBitmap);
var
  p, q: pInteger;
begin;
  with Bitmap do begin;
    p:=ScanLine[Height-1];
    q:=ScanLine[0];
    if cardinal(p)>cardinal(q) then p:=q;
    Grayscale1(p, Height * Width);
    end;
  end;

Попробуем ускорить вычисление нового цвета пикселя. Обратим внимание на то, что фактически вычисление яркости пикселя в процедуре Grayscale1 производится в байте с номером 1, который затем сдвигается в младший байт (байт номер 0). Все это требует предварительного перемещения значений цветовых составляющих в младший байт. Если же вычисление яркости выполнять в старшем байте (байт номер 3), то можно обойтись без предварительного сдвига:

    pBuf^:=(( (29 * 256 * 256) * (Color and $000000FF)  //Blue
                 + (150 * 256) * (Color and $0000FF00)  //Green
                          + 77 * (Color and $00FF0000)  //Red
            ) shr 24
           ) * $00010101;

Заметим также, что умножение синей и красной составляющих можно совместить. При этом в двух младших байтах будет дополнительно вычислено произведение Blue*77, которое не окажет влияния на результат из-за последующего сдвига:

    pBuf^:=(( (150 * 256) * (Color and $0000FF00)  //Green
            + (29 * 256 * 256 + 77) * (Color and $00FF00FF)  //Red & Blue
            ) shr 24
           ) * $00010101;

Кроме того, можно обойтись без выделения зеленой составляющей, и умножить на соответствующий ей весовой коэффициент все цвета, а красную и зеленую составляющие затем умножить на меньший коэффициент:

    pBuf^:=(( (150 * 256) * (Color and $00FFFFFF)
            + (29 * 256 * 256 - 150 * 256 + 77) * (Color and $00FF00FF)
            ) shr 24
           ) * $00010101;

И, наконец, логическое умножение на константу $00FFFFFF излишне, т.к. при умножении старшего байта Color на (150*256) все равно будет получен нуль. После очередного упрощения получаем:

    pBuf^:=(( (150 * 256) * Color
            + (29 * 256 * 256 - 150 * 256 + 77) * (Color and $00FF00FF)
            ) shr 24
           ) * $00010101;

В результате процедура преобразования стала в полтора-два раза быстрее. Полностью она теперь выглядит следующим образом:

procedure Grayscale2(pBuf: pInteger; Count: integer);
var
  Color: integer;
begin;
  repeat;
    Color:=pBuf^;
    pBuf^:=(( Color * (150 * 256)
           + (Color and $00FF00FF) * (29 * 256 * 256 - 150 * 256 + 77)
            ) shr 24
           ) * $00010101;
    dec(Count);
    inc(pBuf);
    until Count<=0;
  end;
 
procedure ShaBitmapToGrayscale2(Bitmap: TBitmap);
var
  p, q: pInteger;
begin;
  with Bitmap do begin;
    p:=ScanLine[Height-1];
    q:=ScanLine[0];
    if cardinal(p)>cardinal(q) then p:=q;
    Grayscale2(p, Height * Width);
    end;
  end;

Если развернуть цикл, то можно получить ускорение еще примерно в 1.25 раза:

procedure Grayscale3(pBuf: pIntegerArray; Count: integer);
const
  Coeff = 150 * 256;
  Magic = 29 * 256 * 256 + 77 - Coeff;
var
  c0, c1, t0, t1: integer;
begin;
  while Count and 3<>0 do begin;
    c0:=pBuf[0];
    t0:=c0 * Coeff;
    pBuf[0]:=((t0 + (c0 and $00FF00FF) * Magic) shr 24) * $00010101;
    inc(integer(pBuf),SizeOf(pBuf[0]));
    dec(Count);
    end;
 
  if Count>0 then begin;
    c0:=pBuf[0];
    dec(integer(pBuf),2*SizeOf(pBuf[0]));
    while true do begin;
      c1:=pBuf[3];
      t1:=c1 * Coeff;
      t0:=c0 * Coeff;
      pBuf[2]:=((t0 + (c0 and $00FF00FF) * Magic) shr 24) * $00010101;
      c0:=pBuf[4];
      pBuf[3]:=((t1 + (c1 and $00FF00FF) * Magic) shr 24) * $00010101;
 
      dec(Count,4);
      inc(integer(pBuf),4*SizeOf(pBuf[0]));
      if count=0 then break;
 
      c1:=pBuf[1];
      t1:=c1 * Coeff;
      t0:=c0 * Coeff;
      pBuf[0]:=((t0 + (c0 and $00FF00FF) * Magic) shr 24) * $00010101;
      c0:=pBuf[2];
      pBuf[1]:=((t1 + (c1 and $00FF00FF) * Magic) shr 24) * $00010101;
      end;
 
    c1:=pBuf[1];
    t1:=c1 * Coeff;
    t0:=c0 * Coeff;
    pBuf[0]:=((t0 + (c0 and $00FF00FF) * Magic) shr 24) * $00010101;
    pBuf[1]:=((t1 + (c1 and $00FF00FF) * Magic) shr 24) * $00010101;
    end;
  end;
 
procedure ShaBitmapToGrayscale3(Bitmap: TBitmap);
var
  p, q: pIntegerArray;
begin;
  with Bitmap do begin;
    p:=ScanLine[Height-1];
    q:=ScanLine[0];
    if cardinal(p)>cardinal(q) then p:=q;
    Grayscale3(p, Height * Width);
    end;
  end;

Ниже результаты экспериментов сведены в таблицу. Для измерении производительности использовался фоновый рисунок 'Безмятежность.bmp' размером 800x600 из Windows XP. Все процедуры, за исключением первой, вызывались в цикле 4000 раз.

     Время и относительная скорость работы алгоритмов
====================================================================
     Test           Pentium D/2.8GHz   E6850/3GHz    i5-2300/2.8GHz
 Bitmap 800x600x32   Time(ms) Speed  Time(ms) Speed  Time(ms) Speed
--------------------------------------------------------------------
 BitmapToGrayscale     5078      1     1985      1     1468      1
 4000 * Grayscale1    10937   1857     5093   1559     4453   1319
 4000 * Grayscale2     7829   2584     2563   3098     2204   2664
 4000 * Grayscale3     5781   3514     1953   4066     1921   3057

Скорость работы первоначальной реализации превышена в 3000-4000 раз. При этом процессорам E6850 и i5-2300 потребовалось всего 3 такта на обработку одного пикселя.

на главную

Comments (3)

Спасибо

Стоит добавить, что оптимизированные функции используют переполнение Integer, соответственно нужно отключить проверку overflow:
{$Q-} // overflow check off
...
{$Q+} // overflow check on

Алгоритм Grayscale на Delphi

> p:=ScanLine[Height-1];
> q:=ScanLine[0];
> if cardinal(p)>cardinal(q) then p:=q;

Возвращаемое значение указателя может зависеть от внутренней реализации ScanLine. Даже если просто вызвать ScanLine[0] 2 раза подряд, может получиться 2 разных адреса (возвращается не адрес внутреннего сплошного буфера bmp, а адрес копии заказанной строки).
Remy Lebeau:
The ScanLine property itself frees and recreates the internal bitmap data everytime you access it, thus you get a different memory address each time you access the ScanLine property, even when using the same index each time. This is because TBitmap is reference-counted internally, so doing something that can potentially alter the internal bitmap data, such as altering the bitmaps of the ScanLine data, can effect multiple TBitmap instances. So, TBitmap first clones the internal data before then allowing direct access to the data.

Конечно, в вашей программе адреса получаются и сравниваются до модификации, так что вроде это работает. Но однажды это может перестать работать. Для определения расположения строк в bmp, существует BITMAPINFOHEADER и его поле biHeight (см. MSDN):

Specifies the height of the bitmap, in pixels. If biHeight is positive, the bitmap is a bottom-up DIB and its origin is the lower-left corner. If biHeight is negative, the bitmap is a top-down DIB and its origin is the upper-left corner.

Вообще, насколько я знаю, обращение к bmp как к сплошному массиву, нигде не документировано. Теоретически возможно, что данные могут быть разбросаны в памяти.

Спасибо за исследование

Круто! Добавил в избранное.

> if cardinal(p)>cardinal(q) then p:=q;

Можно брать адрес ScanLine[Height-1] за начало - он всегда указывает на первую строку.