0

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

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

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

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

我知道,从技术上讲,我可以采用 O(N²) 的方式,只需逐个检查像素即可找到绘制图像的“真实”边界。

但这会非常缓慢。

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

在此处输入图像描述

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

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

如何找到这个“最后一个”像素的坐标

4

1 回答 1

1

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

最直接的方法是处理 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仅获得“仅”约 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);
}

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

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

于 2018-11-29T11:23:12.493 回答