查找 C# 图元文件的最后绘制像素

Find last drawn pixel of C# Metafile

我有一个图元文件对象。由于我无法控制的原因,它提供的尺寸比适合其中绘制的图像所需的尺寸大得多(大数千倍)。

例如,它可能是 40 000 x 40 000,但在 2000 x 1600 的区域中仅包含 "real"(非透明)像素。

最初,这个图元文件只是简单地绘制到一个控件上,控件边界将区域限制在一个合理的大小。

现在我正尝试根据用户输入将其拆分为不同的动态大小块。我想做的是计算这些块中有多少(在 x 和 y 中,甚至分割成块的二维网格)。

我知道,从技术上讲,我 可以 走 O(N²) 的路,只需一一检查像素以找到 "real" 的边界绘制的图像。

但这将非常缓慢。

我正在寻找一种方法来获取整个图元文件中最后绘制的像素的位置 (x,y),而无需遍历每个图元文件。

由于 DrawImage 方法并不慢,至少不是 N² 慢,我假设元文件对象在内部进行了一些优化,允许这样的事情。就像 List 对象有一个 .Count 属性 比实际计算对象快得多,是否有某种方法可以获取图元文件的实际范围?

在这种情况下,绘制的内容将始终为矩形。我可以安全地假设最后一个像素将是相同的,无论我先循环 x 然后 y,还是循环 y 然后 x。

如何找到这个"last"像素的坐标?

为如此大的图像寻找非透明像素的边界矩形确实是一个有趣的挑战。

最直接的方法是处理 WMF 内容,但这也是迄今为止最难做到的。

让我们将图像渲染为位图并查看位图。

首先是基本方法,然后是一些优化。

要获得边界,需要找到 left, top, rightbottom 边界。

这是一个简单的函数:

Rectangle getBounds(Bitmap bmp)
{
    int l, r, t, b; l = t = r = b = 0;
    for (int x = 0; x < bmp.Width - 1; x++) 
    for (int y = 0; y < bmp.Height - 1; y++) 
            if (bmp.GetPixel(x,y).A > 0) { l = x; goto l1; }
    l1:
    for (int x = bmp.Width - 1; x > l ; x--) 
    for (int y = 0; y < bmp.Height - 1; y++) 
            if (bmp.GetPixel(x,y).A > 0) { r = x; goto l2; }
    l2:
    for (int y = 0; y < bmp.Height - 1; y++) 
    for (int x = l; x < r; x++) 
            if (bmp.GetPixel(x,y).A > 0) { t = y; goto l3; }
    l3:
    for (int y = bmp.Height - 1; y > t; y--) 
    for (int x = l; x < r; x++) 
            if (bmp.GetPixel(x,y).A > 0) { b = y; goto l4; }
    l4:

    return Rectangle.FromLTRB(l,t,r,b);
}

请注意,它稍微优化了最后一个垂直循环,以仅查看水平循环尚未测试的部分。

它使用GetPixel,速度慢得令人痛苦;但即使 Lockbits 也只能获得 'only' 大约 10 倍左右。所以我们需要减少绝对数量;无论如何我们都需要这样做,因为 40k x 40k 像素对于 Bitmap.

来说太大了

由于 WMF 通常充满矢量数据,我们可能可以将其缩小很多。这是一个例子:

string fn = "D:\_test18b.emf";
Image img = Image.FromFile(fn);

int w = img.Width;
int h = img.Height;
float scale = 100;
Rectangle rScaled = Rectangle.Empty;

using (Bitmap bmp = new Bitmap((int)(w / scale), (int)(h / scale)))
using (Graphics g = Graphics.FromImage(bmp))
{
    g.ScaleTransform(1f/scale, 1f/scale);
    g.Clear(Color.Transparent);
    g.DrawImage(img, 0, 0);
    rScaled = getBounds(bmp);
    Rectangle rUnscaled = Rectangle.Round(
         new RectangleF(rScaled.Left * scale, rScaled.Top * scale, 
                        rScaled.Width * scale, rScaled.Height * scale ));

 }

请注意,要正确绘制 wmf 文件,可能需要调整分辨率。这是我用于测试的示例:

    using (Graphics g2 = pictureBox.CreateGraphics())
    {
        float scaleX = g2.DpiX / img.HorizontalResolution / scale;
        float scaleY = g2.DpiY / img.VerticalResolution / scale;

        g2.ScaleTransform(scaleX, scaleY);
        g2.DrawImage(img, 0, 0);    // draw the original emf image.. (*)
        g2.ResetTransform();
        // g2.DrawImage(bmp, 0, 0); // .. it will look the same as (*)
        g2.DrawRectangle(Pens.Black, rScaled);
    }

我省略了这个但是为了完全控制渲染,它也应该包含在上面的代码片段中..


这可能不够好,也可能不够好,具体取决于所需的准确性。

要完美地测量边界,可以使用以下技巧:使用缩小测试中的边界并测量未缩放但仅在四个边界数字周围的小条纹。创建渲染位图时,我们相应地移动原点。

右边界示例:

Rectangle rScaled2 = Rectangle.Empty;
int delta = 80;
int right = (int)(rScaled.Right * scale);

using (Bitmap bmp = new Bitmap((int)(delta * 2 ), (int)(h )))
using (Graphics g = Graphics.FromImage(bmp))
{
    g.Clear(Color.Transparent);
    g.DrawImage(img, - right - delta, 0);
    rScaled2 = getBounds(bmp);
}

我可以通过不超过整个高度而只超过我们已经找到的部分(加上 delte)来优化..

如果可以利用有关数据的知识,可以实现进一步的优化。如果我们知道图像数据是 connected 我们可以在循环中使用更大的步骤直到找到一个像素然后回溯一步..