10

我有一个 DrawingContext(Visual 或 DrawingGroup 的一部分),我在其中绘制一堆矩形和/或 1 位图像。把它想象成一个掩蔽的 1 位图像。我想将其转换为位图图像文件。

使用RenderTargetBitmap不是一个选项,因为它只能以 32 位像素格式渲染,所以如果我必须渲染一个 20MB 的 1 位图像,我最终会在我的堆上获得 640MB (20*32) 的内存。这当然会产生巨大的 LOH 碎片,并且应用程序在第二次拍摄时内存不足。

所以,我基本上需要一种有效地从绘图上下文写入 1 位位图文件的方法。任何想法/建议/替代方法将不胜感激。

4

3 回答 3

7

很多想法,有些有点复杂......

打印到 XPS 然后提取位图

您可以打印Visual到 XPS 文档。

如果幸运的话,它将结合您在其中绘制的 1 位图像,DrawingContext并在 XPS 文件中生成一个位图。

对于矩形,它可能会在 XPS 中将矩形保留为基于矢量的信息(基于“形状”RectangleDrawingContext DrawRectangle可能两者都这样做)......如果发生这种情况,则创建一个位图,在其中绘制矩形部分并将该位图绘制到上下文。

一旦你有了 XPS 文件,你就可以解析它并提取“图像”,如果你幸运的话,那就是它在里面产生的东西。(XPS 只是一个 ZIP 文件,它使用 XAML 来描述内容并使用子目录来存储位图图像数据文件、字体等)。

(我认为您可以使用Package该类来访问 XPS 文档的原始部分,或者XpsDocument进行更高级别的解释)。

或者,如果您的意图只是提供一种方法来允许在不同的上下文中查看您的组合 1 位图像,则只需在 XPS 查看器中显示 XPS。

使用提供打印到图像功能的打印机驱动程序

这并不理想……尤其是因为您必须在机器上安装打印机驱动程序……但是您可以使用“打印到图像”打印机驱动程序,然后使用它来生成位图。以下是一些建议:

创建一个 GDI+ HBITMAP 并使用 GDI+ 调用进行绘图...然后将其包装以供 WPF 使用...或保存到磁盘。

[1] 首先创建一个足够大的 GDI+ 位图以容纳您的合成渲染

(有几种不同的方法可以做到这一点......一种方法是使用 WriteableBitmap 来提供后台缓冲区位/存储......如果你想访问内存中的位......在你的情况下我不认为你做/需要)。

var bmp = new System.Drawing.Bitmap( pixelwidth, pixelheight, System.Drawing.Imaging.Format1bppIndexed );

或者,如果您想要 WriteableBitmap 访问权限,请使用这种方式。

[2] 将任何 WPF 位图源转换为 GDI+ 位图,以便您可以使用 DrawImage 将它们绘制到 GDI+ 图形上下文中。

您可以使用 CopyPixels 在您的 BitmapSources 上执行此操作。

对于您的矩形,您可以只使用 GDI+ DrawRectangle 渲染命令。

[3] 将 GDI+ 位图保存到磁盘

使用 System.Image.Bitmap 上的 .Save 方法将其保存为位图。

[4] 使用保存的图像作为 Image 元素的 Source

(注意这可能会在加载图像时使用大量内存,即使它是 1bpp...因为 WPF 渲染路径都是 32bpp)。

[4] 或者包装 GDI+ 位图以在 WPF 中使用

您可以使用 InteropBitmap 来包装基于 GDI+ 的位图,以便您可以将其用作 WPF 中的 BitmapSource。(请注意,它可能必须是 32bpp 的……如果必须……那么您将回到第 1 格……但无论如何都要尝试)。

创建位图“服务”

创建另一个充当服务的进程(不必是 NT 服务...可以只是您启动的子进程)以呈现您的组合 1bpp 图像...有多种方式与它进行通信以提供它是渲染命令。

当内存消耗过高/LOH 碎片化时,您可以重新启动此服务。

其他想法

您可以使用 OpenGL 进行渲染(例如使用 OpenTK 或 SharpGL),或者使用 DirectX 进行渲染……通过带有 D3DImage 或 Direct2D 的 3D 路径(这在内存使用方面是否与 RenderTargetBitmap 相同……待查明)。

试用 NET 4.5,因为 LOH 堆中有许多改进:

于 2012-09-03T01:02:23.860 回答
2

我不认为你可以做得更好。由于 RenderTargetBitmap 使用您无法访问的 MILcore。而且我认为没有其他方法可以复制 Visual。但是我认为还有一个选择。它不会是一条线,但我认为它会足够好。

基本上,您将逐块渲染视觉块(PGBRA32)并将其动态转换为 BlackWhite,然后将其与 Blackwhite 缓冲区连接。我已经开始了一个小示例代码,但在中途决定它不会那么容易,但你可以完成它。

 /// <summary>
/// Renders only part of the visual and returns byte[] array
/// which holds only black/white information.
/// </summary>
/// <param name="oVisual"></param>
/// <param name="x"></param>
/// <param name="y"></param>
/// <param name="width"></param>
/// <param name="height"></param>
/// <returns>black/white pixel information. one pixel=one bit. 1=white, 0=black</returns>
public static byte[] RenderPartially(Visual oVisual, 
    int x, int y, double width, double height)
{
    int nWidth = (int)Math.Ceiling(width);
    int nHeight = (int)Math.Ceiling(height);

    RenderTargetBitmap oTargetBitmap = new RenderTargetBitmap(
        nWidth,
        nHeight,
        96,
        96,
        PixelFormats.Pbgra32
    );

    DrawingVisual oDrawingVisual = new DrawingVisual();
    using (DrawingContext oDrawingContext = oDrawingVisual.RenderOpen())
    {
        VisualBrush oVisualBrush = new VisualBrush(oVisual);

        oDrawingContext.DrawRectangle(
            oVisualBrush,
            null,
            new Rect(
                new Point(x, y),
                new Size(nWidth, nHeight)
            )
        );

        oDrawingContext.Close();
        oTargetBitmap.Render(oDrawingVisual);
    }

    //Pbgra32 == 32 bits ber pixel?!(4bytes)
    // Calculate stride of source and copy it over into new array.
    int bytesPerPixel = oTargetBitmap.Format.BitsPerPixel / 8;
    int stride = oTargetBitmap.PixelWidth * bytesPerPixel;
    byte[] data = new byte[stride * oTargetBitmap.PixelHeight];
    oTargetBitmap.CopyPixels(data, stride, 0);

    //assume pixels in byte[] are stored as PBGRA32 which means that 4 bytes form single PIXEL.
    //so the layout is like:
    // R1, G1, B1, A1,  R2, G2, B2, A2,  R3, G3, B3, A3, and so on.

    byte [] bitBufferBlackWhite = new byte[oTargetBitmap.PixelWidth
        * oTargetBitmap.PixelHeight / bytesPerPixel];

    for(int row = 0; row < oTargetBitmap.PixelHeight; row++)
    {
        for(int col = 0; col < oTargetBitmap.PixelWidth; col++)
        {
            //calculate concrete pixel from PBGRA32
            int index = stride * row + bytesPerPixel * col;
            int r = data[index];
            int g = data[index + 1];
            int b = data[index + 2];
            int transparency = data[index + 3];

            //determine whenever pixel is black or white.
            //note that I dont know the exact process how one converts
            //from PBGRA32 to BlackWhite format. But you should get the idea.
            bool isBlack = r + g + b + transparency == 0;

            //calculate target index and USE bitwise operators in order to
            //set right bits.
        }
    }

    return bitBufferBlackWhite;
}

所以本质上,用 BlackWhite 格式设置新的 WriteableBitmap,然后像这样调用这个函数:

WriteableBitmap blackWhiteFullBuffer = new WriteableBItmap(....., PIxelFormats.BlackWhite);
for(int x = 0; x < Visual.Width; x += <some uniform amount that divides correctly>)
{
    for(int y = 0; y < VIsual.Height; y+= <some uniform amount that divides correctly>)
    {
        byte[] partialBuffer = PartialRenderer.RenderPartially(Visual, x, y, <uniform amX>,
          <uniform amY>);
        //concat that partial blackWhite buffer with other blackWhite buffer.
        //you do this as long as needed and memory wont grow too much
        //hopefully it will be fast enough too.
        PartialRenderer.ConcateBuffers(blackWhiteFullBuffer, partialBuffer);
    }
}

//然后如果需要,将 blackWhiteFullBuffer 保存到 HDD。

于 2012-09-02T07:27:53.840 回答
0

PixelFormats怎么样

编辑:(感谢安德斯·古斯塔夫森)

较低的是PixelFormats.BlackWhite,每像素 1 位。

所以通过这种方式,你可以将任何 BitmapImage 转换为 FormatConvertedBitmap,在这里你可以将格式修改为更低的 bpp。

于 2012-08-31T11:20:07.010 回答