Найти последний нарисованный пиксель метафайла C # - PullRequest
0 голосов
/ 19 ноября 2018

У меня есть объект Metafile. По независящим от меня причинам он был предоставлен намного больше (в тысячи раз больше), чем требовалось для размещения изображения, нарисованного внутри него.

Например, это может быть 40 000 x 40 000, но содержит только «реальные» (непрозрачные) пиксели в области 2000 x 1600.

Первоначально этот метафайл был просто нарисован для элемента управления, и границы элемента управления ограничивали область разумным размером.

Теперь я пытаюсь разделить его на разные части динамического размера, в зависимости от ввода пользователя. То, что я хочу сделать, это подсчитать, сколько из этих кусков будет там (в x и y, даже разбиение происходит в двумерную сетку кусков).

Мне известно, что технически я мог бы пойти по пути O (N²) и просто проверять пиксели один за другим, чтобы найти "реальные" границы нарисованного изображения.

Но это будет мучительно медленно.

Я ищу способ получить позицию (x, y) самого последнего нарисованного пикселя во всем метафайле, без итераций по каждому из них.

enter image description here

Поскольку метод DrawImage не слишком болезненный, по крайней мере, не медленный N², я предполагаю, что у объекта метафайла есть некоторые оптимизации внутри, которые позволили бы сделать что-то подобное. Точно так же, как объект List имеет свойство .Count, которое намного быстрее, чем фактический подсчет объектов, есть ли способ получить практические границы метафайла?

Нарисованный контент в этом сценарии всегда будет прямоугольным. Я могу с уверенностью предположить, что последний пиксель будет таким же, независимо от того, буду я циклически повторять x, y или y, затем x.

Как найти координаты этого "последнего" пикселя ?

1 Ответ

0 голосов
/ 29 ноября 2018

Найти ограничивающий прямоугольник непрозрачных пикселей для такого большого изображения - действительно интересная задача.

Самым прямым подходом было бы заняться контентом WMF, но это также намного сложнее понять.

Давайте вместо этого отрендерим изображение в растровое изображение и посмотрим на растровое изображение.

Сначала базовый подход, затем несколько оптимизаций.

Чтобы получить границы, нужно найти границы l eft, top, right и bottom.

Вот простая функция для этого:

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);
}

Я мог бы оптимизировать, не перебирая всю высоту, а только ту часть (плюс дельта), которую мы уже нашли ..

Дальнейшая оптимизация может быть достигнута, если можно использовать знания о данных. Если мы знаем, что данные изображения связаны , мы можем использовать более крупные шаги в циклах, пока не будет найден пиксель, а затем проследить один шаг назад.

...