GDI+ 中快速投影的算法 [英] Algorithm for fast Drop shadow in GDI+

查看:21
本文介绍了GDI+ 中快速投影的算法的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

在 GDI 中为图像添加阴影的有效方法是什么?

What is an efficient way to add a drop-shadow to an image in GDI?

现在我从我的形象开始:

Right now i start with my image:

我使用 ImageAttributes 和 ColorMatrix 将图像的 alpha 蒙版绘制到新图像上:

i use ImageAttributes and a ColorMatrix to draw the image's alpha mask to a new image:

colorMatrix = (
    ( 0,  0,  0, 0, 0),
    ( 0,  0,  0, 0, 0),
    ( 0,  0,  0, 0, 0),
    (-1, -1, -1, 1, 0),
    ( 1,  1,  1, 0, 1)
    );

然后我应用一个高斯模糊卷积核,并稍微偏移它:

i then apply a Gaussian Blur convolution kernel, and offset it slightly:

然后我把我的原始图像画回到顶部:

And then i draw my original image back over top:

问题是它太慢了,生成带有阴影的图像大约需要 170 毫秒,而没有阴影的图像则需要 2 毫秒(慢 70 倍):

Problem is that it's just too slow, it takes about 170ms to generate the image with drop-shadow, verses 2ms without the drop-shadow (70x slower):

  • 带阴影:171,332 µs
  • 没有阴影:2,457us

当用户(例如我)滚动浏览项目列表时,额外的 169 毫秒延迟非常很明显.

When a user (e.g. me) is scrolling through a list of items, that extra 169ms delay is very noticeable.

您可以忽略下面的代码,它不会为问题或答案添加任何内容:

You can ignore the code below, it doesn't add anything to the question, or the answer:

class function TImageEffects.GenerateDropShadow(image: TGPImage;
        const radius: Single; const OffsetX, OffsetY: Single; const Opacity: Single): TGPBitmap;
var
    width, height: Integer;
    alphaMask: TGPBitmap;
    shadow: TGPBitmap;
    graphics: TGPGraphics;
    imageAttributes: TGPImageAttributes;
    cm: TColorMatrix;
begin
{
    We generate a drop shadow by first getting the alpha mask. This will be a black
    sillouette on a transparent background. We then blur the black "shadow" by the amounts
    given.
    We then draw the original image on top of it's own shadow.
}

{
    http://msdn.microsoft.com/en-us/library/aa511280.aspx
    Windows Vista User Experience -> Guidelines -> Aesthetics -> Icons

    Basic Flat Icon Shadow Ranges

    Flat icons
    Flat icons are generally used for file icons and flat real-world objects,
    such as a document or a piece of paper.

    Flat icon lighting comes from the upper-left at 130 degrees.

    Smaller icons (for example, 16x16 and 32x32) are simplified for readability.
    However, if they contain a reflection within the icon (often simplified),
    they may have a tight drop shadow. The drop shadow ranges in opacity from
    30-50 percent.
    Layer effects can be used for flat icons, but should be compared with other
    flat icons. The shadows for objects will vary somewhat, according to what
    looks best and is most consistent within the size set and with the other
    icons in Windows Vista. On some occasions, it may even be necessary to
    modify the shadows. This will especially be true when objects are laid over
    others.
    A subtle range of colors may be used to achieve desired outcome. Shadows help
    objects sit in space. Color impacts the perceived weight of the shadow, and
    may distort the image if it is too heavy.

    Blend mode: Multiply
    Opacity: 22% to 50% - depends on color of the item.
    Angle: 130 to 120, use global light
    Distance: 3 (256 thru 48x), Distance = 1 (32x, 24x)
    Spread: 0
    Size: 7 (256x thru 48x), Spread = 2 (32x, 24x)
}
    width := image.GetWidth;
    height := image.GetHeight;

    //Get bitmap to hold final composited image and shadow
    Result := TGPBitmap.Create(width, height, PixelFormat32bppARGB);

    //Use ColorMatrix methods to "draw" the alpha image.
    alphaMask := TImageEffects.GetAlphaMask(image);
    try
        //Blur the black and white shadow image
//      shadow := TImageEffects.BoxBlur(alphaMask, radius);
        shadow := TImageEffects.GaussianBlur(alphaMask, radius); //because Gaussian Blur is linearly-separable into two 1d kernels, it's actually faster than the box blur
    finally
        alphaMask.Free;
    end;

    //Draw
    graphics := TGPGraphics.Create(Result);
    try
        //Draw the "shadow", using the passed in opacity value.
        {
            Color transformations are of the form
        c =  (r, g, b, a)
        c' = (r, g, b, a)
        c' = c*M
            = (r, g, b, a, 1) * (0 0 0 0 0)  //r
                                      (0 0 0 0 0)  //g
                                      (0 0 0 0 0)  //b
                                      (1 1 1 1 0)  //a
                                      (0 0 0 0 1)  //1
        }

        imageAttributes := TGPImageAttributes.Create;
    {   cm := (
                ( 1, 0, 0, 0,   0),
                ( 0, 1, 0, 0,   0),
                ( 0, 0, 1, 0,   0),
                ( 0, 0, 0, 0.5, 0),
                ( 0, 0, 0, 0,   1)
            );}
        cm[0, 0] :=  1; cm[0, 1] :=  0; cm[0, 2] :=  0; cm[0, 3] := 0;       cm[0, 4] := 0;
        cm[1, 0] :=  0; cm[1, 1] :=  1; cm[1, 2] :=  0; cm[1, 3] := 0;       cm[1, 4] := 0;
        cm[2, 0] :=  0; cm[2, 1] :=  0; cm[2, 2] :=  1; cm[2, 3] := 0;       cm[2, 4] := 0;
        cm[3, 0] :=  0; cm[3, 1] :=  0; cm[3, 2] :=  0; cm[3, 3] := Opacity; cm[3, 4] := 0;
        cm[4, 0] :=  0; cm[4, 1] :=  0; cm[4, 2] :=  0; cm[4, 3] := 0;       cm[4, 4] := 1;


        imageAttributes.SetColorMatrix(
                cm,
                ColorMatrixFlagsDefault,
                ColorAdjustTypeBitmap);
        try
            graphics.DrawImage(shadow,
                        MakeRectF(OffsetX, OffsetY, width, height), //destination rectangle
                        0, 0, //source (x,y)
                        width, height, //source width, height
                        UnitPixel,
                        ImageAttributes);

            //Draw original image over-top of it's shadow
            graphics.DrawImage(image, 0, 0);
        finally
            imageAttributes.Free;
        end;
    finally
        graphics.Free;
    end;
end;

使用该函数获取灰度alpha蒙版:

Which uses the the function to get the grayscale alpha mask:

class function TImageEffects.GetAlphaMask(image: TGPImage): TGPBitmap;
var
    imageAttributes: TGPImageAttributes;
    cm: TColorMatrix;
    graphics: TGPGraphics;
    Width, Height: UINT;
begin
    {
        Color transformations are of the form
    c =  (r, g, b, a)
    c' = (r, g, b, a)
    c' = c*M
        = (r, g, b, a, 1) * (0 0 0 0 0)
                            (0 0 0 0 0)
                            (0 0 0 0 0)
                            (1 1 1 1 0)
                            (0 0 0 0 1)
    }

    imageAttributes := TGPImageAttributes.Create;

{   cm := (
            ( 0,  0,  0, 0, 0),
            ( 0,  0,  0, 0, 0),
            ( 0,  0,  0, 0, 0),
            (-1, -1, -1, 1, 0),
            ( 1,  1,  1, 0, 1)
        );}
    cm[0, 0] :=  0; cm[0, 1] :=  0; cm[0, 2] :=  0; cm[0, 3] := 0; cm[0, 4] := 0;
    cm[1, 0] :=  0; cm[1, 1] :=  0; cm[1, 2] :=  0; cm[1, 3] := 0; cm[1, 4] := 0;
    cm[2, 0] :=  0; cm[2, 1] :=  0; cm[2, 2] :=  0; cm[2, 3] := 0; cm[2, 4] := 0;
    cm[3, 0] := -1; cm[3, 1] := -1; cm[3, 2] := -1; cm[3, 3] := 1; cm[3, 4] := 0;
    cm[4, 0] :=  1; cm[4, 1] :=  1; cm[4, 2] :=  1; cm[4, 3] := 0; cm[4, 4] := 1;


    imageAttributes.SetColorMatrix(
            cm,
            ColorMatrixFlagsDefault,
            ColorAdjustTypeBitmap);

    width := image.GetWidth;
    height := image.GetHeight;

    Result := TGPBitmap.Create(Integer(width), Integer(height));
    graphics := TGPGraphics.Create(Result);
   try
        graphics.DrawImage(
                image,
                MakeRect(0, 0, width, height), //destination rectangle
             0, 0, //source (x,y)
             width, height,
             UnitPixel,
                ImageAttributes);
   finally
        graphics.Free;
    end;
end;

核心是高斯模糊:

class function TImageEffects.GaussianBlur(const bitmap: TGPBitmap;
  radius: Single): TGPBitmap;
var
    width, height: Integer;
    tempBitmap: TGPBitmap;
    bdSource: TBitmapData;
    bdTemp: TBitmapData;
    bdDest: TBitmapData;
    pSrc: PARGBArray;
    pTemp: PARGBArray;
    pDest: PARGBArray;
    stride: Integer;
    kernel: TKernel;
begin
//  kernel := MakeGaussianKernel2d(radius);
    kernel := MakeGaussianKernel1d(radius);
    try
//      Result := ConvolveBitmap(bitmap, kernel); brute 2d kernel

        width := bitmap.GetWidth;
        height := bitmap.GetHeight;

        // GDI+ still lies to us - the return format is BGR, NOT RGB.
        bitmap.LockBits(MakeRect(0, 0, width, height),
                ImageLockModeRead,
                PixelFormat32bppPARGB, bdSource);

        //intermediate bitmap
        tempBitmap := TGPBitmap.Create(width, height, PixelFormat32bppPARGB);
        tempBitmap.LockBits(MakeRect(0, 0, width, height),
                    ImageLockModeWrite,
                    PixelFormat32bppPARGB, bdTemp);

        //target bitmap
        Result := TGPBitmap.Create(width, height, PixelFormat32bppARGB);
        Result.LockBits(MakeRect(0, 0, width, height),
                    ImageLockModeWrite,
                    PixelFormat32bppPARGB, bdDest);

        pSrc := PARGBArray(bdSource.Scan0);
        pTemp := PARGBArray(bdTemp.Scan0);
        pDest := PARGBArray(bdDest.Scan0);
        stride := bdSource.Stride;

        ConvolveAndTranspose(kernel, pSrc^, pTemp^, width, height, stride, True, EdgeActionClampEdges);
        ConvolveAndTranspose(kernel, pTemp^, pDest^, height, width, stride, True, EdgeActionClampEdges);

        //Unlock source
       bitmap.UnlockBits(bdSource);
        tempBitmap.UnlockBits(bdTemp);
        Result.UnlockBits(bdDest);

        //get rid of temp
        tempBitmap.Free;
    finally
        kernel.Free;
    end;
end;

需要一维内核:

class function TImageEffects.MakeGaussianKernel1d(radius: Single): TKernel;
var
    r: Integer;
    rows: Integer;
    matrix: TSingleDynArray;
    sigma: Single;
    sigma22: Single;
    sigmaPi2: Single;
    sqrtSigmaPi2: Single;
    radius2: Single;
    total: Single;
    index: Integer;
    row: Integer;
    distance: Single;
    i: Integer;
begin
    r := Ceil(radius);
    rows := r*2+1;

    SetLength(matrix, rows);
    sigma := radius/3.0;
    sigma22 := 2*sigma*sigma;
    sigmaPi2 := 2*pi*sigma;
    sqrtSigmaPi2 := Sqrt(sigmaPi2);
    radius2 := radius*radius;
    total := 0;

    Index := 0;
    for row := -r to r do
    begin
        distance := row*row;
        if (distance > radius2) then
            matrix[index] := 0
        else
        begin
            matrix[index] := Exp((-distance)/sigma22) / sqrtSigmaPi2;
            total := total + matrix[index];
            Inc(index);
        end;
    end;

    //Normalize the values
    for i := 0 to rows-1 do
        matrix[i] := matrix[i] / total;


    Result := TKernel.Create(rows, 1, matrix);
end;

然后高斯函数的神奇之处在于它可以分为两个一维卷积:

And then the magic of the gaussian function is that it is separable into two 1D convolutions:

class procedure TImageEffects.convolveAndTranspose(kernel: TKernel;
  const inPixels: array of ARGB; var outPixels: array of ARGB; width,
  height, stride: Integer; alpha: Boolean; edgeAction: TEdgeAction);
var
    index: Integer;
    matrix: TSingleDynArray;
    rows: Integer; //number of rows in the kernel
    cols: Integer; //number of columns in the kernel
    rows2: Integer; //half row count
    cols2: Integer; //half column count

    x, y: Integer; //
    r, g, b, a: Single; //summed red, green, blue, alpha values
    row, col: Integer;
    ix, iy, ioffset: Integer;
    moffset: Integer;
    f: Single;
    rgb: ARGB;
    ir, ig, ib, ia: Integer;

   function ClampPixel(value: Single): Integer;
    begin
        Result := Trunc(value+0.5);
        if Result < 0 then
            Result := 0
        else if Result > 255 then
            Result := 255;
    end;
begin
    matrix := kernel.KernelData;
    cols := kernel.Width;
    cols2 := cols div 2;

    for y := 0 to height-1 do
    begin
        index := y;
        ioffset := y*width;
        for x := 0 to width-1 do
        begin
            r := 0;
            g := 0;
            b := 0;
            a := 0;

            moffset := cols2;
            for col := -cols2 to cols2 do
            begin
                f := matrix[moffset+col];

                if (f <> 0) then
                begin
                    ix := x+col;
                    if ( ix < 0 ) then
                    begin
                        if ( edgeAction = EdgeActionClampEdges ) then
                            ix := 0
                        else if ( edgeAction = EdgeActionWrapEdges ) then
                            ix := (x+width) mod width;
                    end
                    else if ( ix >= width) then
                    begin
                        if ( edgeAction = EdgeActionClampEdges ) then
                            ix := width-1
                        else if ( edgeAction = EdgeActionWrapEdges ) then
                            ix := (x+width) mod width;
                    end;
                    rgb := inPixels[ioffset+ix];
                    a := a + f * ((rgb shr 24) and $FF);
                    r := r + f * ((rgb shr 16) and $FF);
                    g := g + f * ((rgb shr  8) and $FF);
                    b := b + f * ((rgb       ) and $FF);
                end;
            end;
            if alpha then
                ia := ClampPixel(a)
         else
                ia := $FF;
            ir := ClampPixel(r);
            ig := ClampPixel(g);
            ib := ClampPixel(b);
            outPixels[index] := MakeARGB(ia, ir, ig, ib);

            Inc(index, height);
        end;
    end;
end;

使用示例,在我的 256x256 源图像上:

with sample usage, on my 256x256 source images:

image := TImageEffects.GenerateDropShadow(localImage, 14, 2.12132, 2.12132, 1.0);

Profiling 显示 88.62% 的时间花在这些行上:

Profiling shows that 88.62% time is spent in the lines:

a := a + f * ((rgb shr 24) and $FF);
r := r + f * ((rgb shr 16) and $FF);
g := g + f * ((rgb shr  8) and $FF);
b := b + f * ((rgb       ) and $FF);

这是每像素 alpha 混合.

which is the per-pixel alpha blending.

这让我觉得有一种更好的方法来应用柔和的阴影,即应用模糊效果,毕竟 Windows 和 OSX 是实时对窗口应用阴影的.

Which makes me think that there's a better way to apply a soft drop-shadow that applying a blur effect, after all Windows and OSX apply a drop-shadow to windows in real-time.

推荐答案

算法来自这篇博文:http://blog.ivank.net/fastest-gaussian-blur.html.当然,它正在实施最后一个也是最快的版本.:-)

The algorithm came from this blog entry: http://blog.ivank.net/fastest-gaussian-blur.html . It's implementing the last and fastest version, of course. :-)

它是直接从我的工作代码中复制的,因此外部假设可能反映了这一点.该函数返回一个更大的位图以适应大小的增加.当然,在您的代码中,您需要相应地处理此问题.它假设是 32 位 alpha 图片,但可以轻松修改为仅处理 24 位(CHANNELS 常量和 PixelFormat 值).

It's copied directly from my working code, so the external assumptions might reflect that. The function returns a larger bitmap to accommodate the increase in size. In your code, you need to handle this accordingly, of course. It assumes a 32-bit alpha picture but can be easily modified to handle 24-bit only (CHANNELS constant and the PixelFormat values).

public static class DropShadow {
  const int CHANNELS = 4;

  public static Bitmap CreateShadow(Bitmap bitmap, int radius, float opacity) {
    // Alpha mask with opacity
    var matrix = new ColorMatrix(new float[][] {
            new float[] {  0F,  0F,  0F, 0F,      0F }, 
            new float[] {  0F,  0F,  0F, 0F,      0F }, 
            new float[] {  0F,  0F,  0F, 0F,      0F }, 
            new float[] { -1F, -1F, -1F, opacity, 0F },
            new float[] {  1F,  1F,  1F, 0F,      1F }
        });

    var imageAttributes = new ImageAttributes();
    imageAttributes.SetColorMatrix(matrix, ColorMatrixFlag.Default, ColorAdjustType.Bitmap);
    var shadow = new Bitmap(bitmap.Width + 4 * radius, bitmap.Height + 4 * radius);
    using (var graphics = Graphics.FromImage(shadow))
      graphics.DrawImage(bitmap, new Rectangle(2 * radius, 2 * radius, bitmap.Width, bitmap.Height), 0, 0, bitmap.Width, bitmap.Height, GraphicsUnit.Pixel, imageAttributes);

    // Gaussian blur
    var clone = shadow.Clone() as Bitmap;
    var shadowData = shadow.LockBits(new Rectangle(0, 0, shadow.Width, shadow.Height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);
    var cloneData = clone.LockBits(new Rectangle(0, 0, clone.Width, clone.Height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);

    var boxes = DetermineBoxes(radius, 3);
    BoxBlur(shadowData, cloneData, shadow.Width, shadow.Height, (boxes[0] - 1) / 2);
    BoxBlur(shadowData, cloneData, shadow.Width, shadow.Height, (boxes[1] - 1) / 2);
    BoxBlur(shadowData, cloneData, shadow.Width, shadow.Height, (boxes[2] - 1) / 2);

    shadow.UnlockBits(shadowData);
    clone.UnlockBits(cloneData);
    return shadow;
  }

  private static unsafe void BoxBlur(BitmapData data1, BitmapData data2, int width, int height, int radius) {
    byte* p1 = (byte*)(void*)data1.Scan0;
    byte* p2 = (byte*)(void*)data2.Scan0;

    int radius2 = 2 * radius + 1;
    int[] sum = new int[CHANNELS];
    int[] FirstValue = new int[CHANNELS];
    int[] LastValue = new int[CHANNELS];

    // Horizontal
    int stride = data1.Stride;
    for (var row = 0; row < height; row++) {
      int start = row * stride;
      int left = start;
      int right = start + radius * CHANNELS;

      for (int channel = 0; channel < CHANNELS; channel++) {
        FirstValue[channel] = p1[start + channel];
        LastValue[channel] = p1[start + (width - 1) * CHANNELS + channel];
        sum[channel] = (radius + 1) * FirstValue[channel];
      }
      for (var column = 0; column < radius; column++)
        for (int channel = 0; channel < CHANNELS; channel++)
          sum[channel] += p1[start + column * CHANNELS + channel];
      for (var column = 0; column <= radius; column++, right += CHANNELS, start += CHANNELS)
        for (int channel = 0; channel < CHANNELS; channel++) {
          sum[channel] += p1[right + channel] - FirstValue[channel];
          p2[start + channel] = (byte)(sum[channel] / radius2);
        }
      for (var column = radius + 1; column < width - radius; column++, left += CHANNELS, right += CHANNELS, start += CHANNELS)
        for (int channel = 0; channel < CHANNELS; channel++) {
          sum[channel] += p1[right + channel] - p1[left + channel];
          p2[start + channel] = (byte)(sum[channel] / radius2);
        }
      for (var column = width - radius; column < width; column++, left += CHANNELS, start += CHANNELS)
        for (int channel = 0; channel < CHANNELS; channel++) {
          sum[channel] += LastValue[channel] - p1[left + channel];
          p2[start + channel] = (byte)(sum[channel] / radius2);
        }
    }

    // Vertical
    stride = data2.Stride;
    for (int column = 0; column < width; column++) {
      int start = column * CHANNELS;
      int top = start;
      int bottom = start + radius * stride;

      for (int channel = 0; channel < CHANNELS; channel++) {
        FirstValue[channel] = p2[start + channel];
        LastValue[channel] = p2[start + (height - 1) * stride + channel];
        sum[channel] = (radius + 1) * FirstValue[channel];
      }
      for (int row = 0; row < radius; row++)
        for (int channel = 0; channel < CHANNELS; channel++)
          sum[channel] += p2[start + row * stride + channel];
      for (int row = 0; row <= radius; row++, bottom += stride, start += stride)
        for (int channel = 0; channel < CHANNELS; channel++) {
          sum[channel] += p2[bottom + channel] - FirstValue[channel];
          p1[start + channel] = (byte)(sum[channel] / radius2);
        }
      for (int row = radius + 1; row < height - radius; row++, top += stride, bottom += stride, start += stride)
        for (int channel = 0; channel < CHANNELS; channel++) {
          sum[channel] += p2[bottom + channel] - p2[top + channel];
          p1[start + channel] = (byte)(sum[channel] / radius2);
        }
      for (int row = height - radius; row < height; row++, top += stride, start += stride)
        for (int channel = 0; channel < CHANNELS; channel++) {
          sum[channel] += LastValue[channel] - p2[top + channel];
          p1[start + channel] = (byte)(sum[channel] / radius2);
        }
    }
  }

  private static int[] DetermineBoxes(double Sigma, int BoxCount) {
    double IdealWidth = Math.Sqrt((12 * Sigma * Sigma / BoxCount) + 1);
    int Lower = (int)Math.Floor(IdealWidth);
    if (Lower % 2 == 0)
      Lower--;
    int Upper = Lower + 2;

    double MedianWidth = (12 * Sigma * Sigma - BoxCount * Lower * Lower - 4 * BoxCount * Lower - 3 * BoxCount) / (-4 * Lower - 4);
    int Median = (int)Math.Round(MedianWidth);

    int[] BoxSizes = new int[BoxCount];
    for (int i = 0; i < BoxCount; i++)
      BoxSizes[i] = (i < Median) ? Lower : Upper;
    return BoxSizes;
  }

}

我认为将其转换为 Delphi 必须很简单.

I assume it must be straighforward to convert it to Delphi.

附录:根据那个博客的评论,如果你有一个整数半径和三个盒子,你实际上可以忘记DetermineBoxes()并使用:

Addendum: according to the comments on that blog, if you have an integer radius and three boxes, you can actually forget DetermineBoxes() and use:

BoxBlur(shadowData, cloneData, shadow.Width, shadow.Height, radius - 1);
BoxBlur(shadowData, cloneData, shadow.Width, shadow.Height, radius - 1);
BoxBlur(shadowData, cloneData, shadow.Width, shadow.Height, radius);

与位图本身相比,它的执行时间可以忽略不计,但仍然......

Its execution time is negligible compared to the bitmap itself but still...

这篇关于GDI+ 中快速投影的算法的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆