【问题标题】:gdi+ Graphics::DrawImage really slow~~gdi+ Graphics::DrawImage 真的很慢~~
【发布时间】:2010-09-20 20:28:00
【问题描述】:

我正在使用 GDI+ Graphic 将 4000*3000 的图像绘制到屏幕上,但速度真的很慢。大约需要 300 毫秒。我希望它只占用不到 10 毫秒。

Bitmap *bitmap = Bitmap::FromFile("XXXX",...);

//-------------------------------------------- // 这部分大约需要 300ms,太糟糕了!

int width = bitmap->GetWidth();
int height = bitmap->GetHeight();
DrawImage(bitmap,0,0,width,height);

//--------------------------------------------

我不能使用 CachedBitmap,因为我想稍后编辑位图。

我该如何改进它?还是有什么不对?

这个原生 GDI 函数也将图像绘制到屏幕上,只需要 1 毫秒:

SetStretchBltMode(hDC, COLORONCOLOR);   
StretchDIBits(hDC, rcDest.left, rcDest.top, 
        rcDest.right-rcDest.left, rcDest.bottom-rcDest.top, 
        0, 0, width, height,
        BYTE* dib, dibinfo, DIB_RGB_COLORS, SRCCOPY);

//--------------------------------------------- ------------------

如果我想使用 StretchDIBits,我需要传递 BITMAPINFO,但是如何从 Gdi+ 位图对象中获取 BITMAPINFO?我通过 FreeImage lib 进行了实验,我使用 FreeImageplus 对象调用 StretchDIBits,它绘制得非常快。但是现在我需要绘制Bitmap,并在Bitmap的位数组上写一些算法,如果我有一个Bitmap对象,我怎么能得到BITMAPINFO呢?真的很烦-_______________-|

【问题讨论】:

  • 在看第二个例子时,我会说 StretchDIBits 是 GPU 加速的,可能不像 DrawImage。请参阅我的更新答案。使用 StretchDIBits 是否能令人满意地解决您的问题?
  • 谢谢PhiLho,但是我不能用StretchDIBits~我只知道它更快,画大图也能那么快~~我附上了为什么我不能用的问题~~/
  • 也许您可以使用 Bitmap::GetHBITMAP 将位图转换为 GDI 对象(请参阅我的答案更新以获取链接)。
  • 我测试了GetHBITMAP函数,从大图获取HBitmap似乎很慢... -____________-b

标签: gdi+ performance drawimage


【解决方案1】:

如果您使用 GDI+,TextureBrush 类是您快速渲染图像所需要的。我已经用它编写了几个 2d 游戏,大约 30 FPS 左右。

我从来没有用 C++ 编写过 .NET 代码,所以这里有一个 C#-ish 示例:

Bitmap bmp = new Bitmap(...)
TextureBrush myBrush = new TextureBrush(bmp)

private void Paint(object sender, PaintEventArgs e):
{
    //Don't draw the bitmap directly. 
    //Only draw TextureBrush inside the Paint event.
    e.Graphics.FillRectangle(myBrush, ...)
}

【讨论】:

  • 应该是:e.Graphics.FillRectangle(myBrush, ClientRectangle); 不过有一个问题 - 每次位图更改时您都必须重新创建画笔,对吗?
  • 你是对的,没有 Graphics.DrawBrush 方法。没试过修改对应的位图后重用同一个画笔,不知道要不要重新创建。在我的 GDI+ 游戏中,所有纹理画笔对象都是在程序加载时预先创建的,其中每个纹理画笔都是精灵的单帧动画。
  • 可以选择提供的像cybis这样的绘图方法..但最快的方法可以通过使用低级dll来完成,欢迎在代码中添加额外的控件......
  • 如果使用此解决方案,请注意画笔图像的偏移/原点以及您have to do a transformation
  • 我使用了TextureBrush 和半透明图像。结果很糟糕。图片完全烂了。
【解决方案2】:

您的屏幕分辨率为 4000 x 3000?哇!

如果没有,你应该只绘制图像的可见部分,这样会快得多......

[第一次评论后编辑]我的评论确实有点愚蠢,我想 DrawImage 会掩盖/跳过不需要的像素。

在您编辑后(显示 StretchDIBits),我猜可能是因为 StretchDIBits 是硬件加速的(“如果驱动程序不支持 JPEG 或 PNG 文件图像" 是一个提示...)而 DrawImage 可能(我没有证据证明这一点!)用 C 编码,依靠 CPU 能力而不是 GPU 能力...

如果我没记错的话,DIB 图像很快(尽管是“设备独立的”)。见High Speed Win32 Animation:“使用 CreateDIBSection 做高速动画”。好的,它适用于旧 Windows 版本(1996 年!)中的 DIB 与 GDI,但我认为它仍然是正确的。

[编辑] 也许Bitmap::GetHBITMAP 函数可能会帮助您使用 StretchDIBits(未测试...)。

【讨论】:

  • DrawImage 不会检查边界吗?如果这个函数不知道他正在画出屏幕,这似乎很愚蠢
  • Bitmap::GetHBITMAP 这个函数本身需要一些时间
  • @sxingfeng:DrawImage 按像素进行裁剪。较大的图像需要更长的时间来绘制。这很愚蠢。速度很慢。
  • 嗯,十多年后,4K UHD 不再是不常见了,恐怕!
【解决方案3】:

只是一个想法;与其在绘制之前检索图像的宽度和高度,不如在加载图像时缓存这些值?

【讨论】:

  • 这很可能是原因。 .NET 的 Bitmap 拥有编程史上最慢的 Width/Height getter;我曾多次发现它们在性能分析中占用了 95% 的 CPU。
【解决方案4】:

探索将插值模式显式设置为 NearestNeighbor 的影响(在您的示例中,看起来实际上不需要插值!但 300 毫秒是在不需要插值时进行高质量插值的成本,所以值得一试)

要探索的另一件事是更改位图的颜色深度。

【讨论】:

    【解决方案5】:

    不幸的是,当我遇到类似的问题时,我发现 GDI+ 已知比 GDI 慢得多,而且通常硬件加速也不行,但现在微软已经转向 WPF,他们不会回来改进 GDI+!

    所有显卡制造商都已转向 3D 性能,似乎对 2D 加速不感兴趣,并且没有明确的信息来源说明哪些功能可以或可以不进行硬件加速。非常令人沮丧,因为使用 GDI+ 在 .NET 中编写了一个应用程序,我不乐意更改为完全不同的技术以将其加速到合理的水平。

    【讨论】:

      【解决方案6】:

      我认为它们不会有太大的不同,但由于您实际上不需要调整图像大小,请尝试使用不会(尝试)调整大小的 DrawImage 的重载:

      DrawImage(bitmap,0,0);
      

      就像我说的,我怀疑它会不会有什么不同,因为我确定 DrawImage 检查了 Width位图的高度,如果不需要调整大小,只需调用此重载。 (我希望它不会费心通过所有 1200 万像素而没有实际工作)。

      更新:我的想法是错误的。我后来发现了,但是伙计们的评论让我想起了我的旧答案:你想要指定目标大小;即使它与源大小匹配:

      DrawImage(bitmap, 0, 0, bitmap.GetWidth, bitmap.GetHeight);
      

      原因是因为bitmap的dpi与目的地的dpi不同。 GDI+ 将执行缩放以使图像出现正确的“尺寸”(即以英寸为单位)


      自去年 10 月以来,我自己了解到的是,您确实想要绘制位图的 “缓存” 版本。 GDI+ 中有一个CachedBitmap 类。使用它有一些技巧。但最后我有一个功能位(Delphi)代码可以做到这一点。

      需要注意的是,CachedBitmap 可能会失效——这意味着它不能用于绘图。如果用户更改分辨率或颜色深度(例如远程桌面),就会发生这种情况。在这种情况下,DrawImage 将失败,您必须重新创建 CachedBitmap

      class procedure TGDIPlusHelper.DrawCachedBitmap(image: TGPImage;
            var cachedBitmap: TGPCachedBitmap;
            Graphics: TGPGraphics; x, y: Integer; width, height: Integer);
      var
         b: TGPBitmap;
      begin
         if (image = nil) then
         begin
            //i've chosen to not throw exceptions during paint code - it gets very nasty
            Exit;
         end;
      
         if (graphics = nil) then
         begin
            //i've chosen to not throw exceptions during paint code - it gets very nasty
            Exit;
         end;
      
         //Check if we have to invalidate the cached image because of size mismatch
         //i.e. if the user has "zoomed" the UI
         if (CachedBitmap <> nil) then
         begin
            if (CachedBitmap.BitmapWidth <> width) or (CachedBitmap.BitmapHeight <> height) then
               FreeAndNil(CachedBitmap); //nil'ing it will force it to be re-created down below
         end;
      
         //Check if we need to create the "cached" version of the bitmap
         if CachedBitmap = nil then
         begin
            b := TGDIPlusHelper.ResizeImage(image, width, height);
            try
               CachedBitmap := TGPCachedBitmap.Create(b, graphics);
            finally
               b.Free;
            end;
      end;
      
          if (graphics.DrawCachedBitmap(cachedBitmap, x, y) <> Ok) then
      begin
             //The calls to DrawCachedBitmap failed
             //The API is telling us we have to recreate the cached bitmap
             FreeAndNil(cachedBitmap);
             b := TGDIPlusHelper.ResizeImage(image, width, height);
             try
                CachedBitmap := TGPCachedBitmap.Create(b, graphics);
             finally
                b.Free;
             end;
      
             graphics.DrawCachedBitmap(cachedBitmap, x, y);
          end;
       end;
      

      cachedBitmap 是通过引用传入的。第一次调用DrawCachedBitmapcached 版本将被创建。然后在后续调用中传递它,例如:

      Image imgPrintInvoice = new Image.FromFile("printer.png");
      CachedBitmap imgPrintInvoiceCached = null;
      
      ...
      
      int glyphSize = 16 * (GetCurrentDpi() / 96);
      
      DrawCachedBitmap(imgPrintInvoice , ref imgPrintInvoiceCached , graphics, 
            0, 0, glyphSize, glyphSize);
      

      考虑到当前的 DPI,我使用例程在按钮上绘制字形。当用户运行高 dpi 时,Internet Explorer 团队也可以使用相同的方法绘制图像(即绘制缩放图像时速度非常慢,因为他们使用 GDI+)。

      【讨论】:

      【解决方案7】:

      尝试使用文件中的位图副本。某些文件的 FromFile 函数返回“慢”图像,但它的副本会绘制得更快。

      Bitmap *bitmap = Bitmap::FromFile("XXXX",...);
      
      Bitmap *bitmap2 = new Bitmap(bitmap); // make copy
      
      DrawImage(bitmap2,0,0,width,height);
      

      【讨论】:

        【解决方案8】:

        /* 首先对不起ma English,代码部分是波兰语,但很容易理解。 我有同样的问题,我找到了最好的解决方案。在这里。

        不要使用:图形图形(hdc); graphics.DrawImage(gpBitmap, 0, 0);速度很慢。

        使用:GetHBITMAP(Gdiplus::Color(), &g_hBitmap) 用于 HBITMAP 并使用我的函数 ShowBitmapStretch() 进行绘制。

        您可以调整它的大小,而且速度要快得多! Artur Czekalski / 波兰

        */

        //--------Global-----------
        Bitmap *g_pGDIBitmap; //for loading picture
        int gRozXOkna, gRozYOkna; //size of working window
        int gRozXObrazu, gRozYObrazu; //Size of picture X,Y
        HBITMAP g_hBitmap = NULL; //for displaying on window
        //------------------------------------------------------------------------------
        int ShowBitmapStretch(HDC hdc, HBITMAP hBmp, int RozX, int RozY, int RozXSkal, int RozYSkal, int PozX, int PozY) 
        {
            if (hBmp == NULL) return -1;
            HDC hdc_mem = CreateCompatibleDC(hdc); //utworzenie kontekstu pamięciowego
            if (NULL == hdc_mem) return -2;
            //Trzeba połączyć BMP z hdc_mem, tzn. umieścić bitmapę w naszym kontekście pamięciowym
            if (DeleteObject(SelectObject(hdc_mem, hBmp)) == NULL) return -3; 
        
            SetStretchBltMode(hdc, COLORONCOLOR); //important! for smoothness
            if (StretchBlt(hdc, PozX, PozY, RozXSkal, RozYSkal, hdc_mem, 0, 0, RozX, RozY, SRCCOPY) == 0) return -4;
        
            if (DeleteDC(hdc_mem) == 0) return -5;
            return 0; //OK
        }
        //---------------------------------------------------------------------------
        void ClearBitmaps(void)
        {
            if (g_hBitmap) { DeleteObject(g_hBitmap);  g_hBitmap = NULL; }
            if (g_pGDIBitmap) { delete g_pGDIBitmap;  g_pGDIBitmap = NULL; }
        }
        //---------------------------------------------------------------------------
        void MyOpenFile(HWND hWnd, szFileName)
        {
            ClearBitmaps(); //Important!
            g_pGDIBitmap = new Bitmap(szFileName); //load a picture from file
        
            if (g_pGDIBitmap == 0) return;
            //---Checking if picture was loaded
            gRozXObrazu = g_pGDIBitmap->GetWidth();
            gRozYObrazu = g_pGDIBitmap->GetHeight();
            if (gRozXObrazu == 0 || gRozYObrazu == 0) return;
        
            //---Uworzenie bitmapy do wyświatlaia; DO IT ONCE HERE!
            g_pGDIBitmap->GetHBITMAP(Gdiplus::Color(), &g_hBitmap); //creates a GDI bitmap from this Bitmap object
            if (g_hBitmap == 0) return;
        
            //---We need to force the window to redraw itself
            InvalidateRect(hWnd, NULL, TRUE);
            UpdateWindow(hWnd);
        }
        //---------------------------------------------------------------------------
        void MyOnPaint(HDC hdc, HWND hWnd) //in case WM_PAINT; DO IT MANY TIMES
        {
            if (g_hBitmap)
            {
                double SkalaX = 1.0, SkalaY = 1.0; //scale
                if (gRozXObrazu > gRozXOkna || gRozYObrazu > gRozYOkna || //too big picture, więc zmniejsz; 
                   (gRozXObrazu < gRozXOkna && gRozYObrazu < gRozYOkna)) //too small picture, można powiększyć 
                {
                    SkalaX = (double)gRozXOkna / (double)gRozXObrazu; //np. 0.7 dla zmniejszania; FOR DECREASE
                    SkalaY = (double)gRozYOkna / (double)gRozYObrazu; //np. 1.7 dla powiększania; FOR INCREASE
                    if (SkalaY < SkalaX) SkalaX = SkalaY; //ZAWSZE wybierz większe skalowanie, czyli mniejszą wartość i utaw w SkalaX
                }
        
            if (ShowBitmapStretch(hdc, g_hBitmap, gRozXObrazu, gRozYObrazu, (int)(gRozXObrazu*SkalaX), (int)(gRozYObrazu*SkalaX), 0, 0, msg) < 0) return;
        

        【讨论】:

          【解决方案9】:

          我进行了一些研究,但找不到比 GDI/GDI+ 更快地渲染图像的方法

          Graphics.DrawImage/DrawImageUnscaled
          

          同时又很简单。

          直到我发现

          ImageList.Draw(GFX,Point,Index)
          

          是的,它真的又快又简单。

          【讨论】:

            猜你喜欢
            • 2014-04-03
            • 1970-01-01
            • 2015-12-03
            • 2017-11-10
            • 2010-12-28
            • 2013-01-25
            • 1970-01-01
            • 2012-01-30
            • 1970-01-01
            相关资源
            最近更新 更多