Как эффективно использовать WriteableBitmap.setpixel() в Windows 10 UWP?

У меня есть сценарий, где

  • выберите файл изображения, а затем с помощью BitmapDecoder преобразуйте источник в WriteableBitmap и установите для image.source значение WriteableBitmap.
  • Теперь, когда пользователь нажимает на изображение, я получаю координаты, а затем хочу закрасить весь регион, окружающий этот пиксель, определенным цветом (как опция заливки в рисовании).

Код, который я использовал

 private void setPixelColors(int xCord, int yCord, int newColor)
 {
    Color color = bit.GetPixel(xCord, yCord);
    if (color.R <= 5 && color.G <= 5 && color.B <= 5 || newColor == ConvertColorToInt(color))
    {
        //Debug.WriteLine("The color was black or same returning");
        return;
    }
    setPixelColors(xCord + 1, yCord, newColor);
    setPixelColors(xCord, yCord + 1, newColor);
    setPixelColors(xCord - 1, yCord, newColor);
    setPixelColors(xCord, yCord - 1, newColor);
    //Debug.WriteLine("Setting the color here");
    bit.SetPixel(xCord, yCord, newColor);
 }

Это работает, но ужасно неэффективно. Я хотел бы знать, есть ли лучший способ сделать это.

Редактировать: Использование библиотеки WriteableBitmapEx.

2 ответа

Решение

Методы расширения GetPixel и SetPixel очень дороги для множественных итеративных изменений, поскольку они извлекают BitmapContext (PixelBuffer объекта WriteableBitmap), вносят изменения, а затем записывают обратно обновленный PixelBuffer, когда вызов выполняется с помощью BitmapContext.

WriteableBitmapEx поделится BitmapContext между несколькими вызовами, если вы получите его первым и сохраните реальную ссылку. Это будет значительно быстрее считывать PixelBuffer только один раз, вносить все изменения и затем записывать его только один раз.

Для этого используйте объект BitmapContext объекта WriteableBitmapEx (достижимый с помощью метода расширения GetBitmapContext), чтобы извлечь PixelBuffer, затем вызывать Get и SetPixel так часто, как это необходимо для контекста растрового изображения. Когда закончите с этим, удалите BitmapContext, чтобы сохранить его обратно в PixelBuffer WriteableBitmap (как правило, будет проще автоматически удалить BitmapContext с помощью оператора using).

Существует пример кода, который даст общее представление о GitHub WriteableBitmapEx по адресу https://github.com/teichgraf/WriteableBitmapEx

Что-то вроде:

 private void setPixelColors(int xCord, int yCord, int newColor)
 {
     using (bit.GetBitmapContext())
     {
         _setPixelColors(xCord, yCord, newColor);
     }
 }
 private void _setPixelColors(int xCord, int yCord, int newColor)
 {
    Color color = bit.GetPixel(xCord, yCord);
    if (color.R <= 5 && color.G <= 5 && color.B <= 5 || newColor == ConvertColorToInt(color))
    {
        //Debug.WriteLine("The color was black or same returning");
        return;
    }
    setPixelColors(xCord + 1, yCord, newColor);
    setPixelColors(xCord, yCord + 1, newColor);
    setPixelColors(xCord - 1, yCord, newColor);
    setPixelColors(xCord, yCord - 1, newColor);
    //Debug.WriteLine("Setting the color here");
    bit.SetPixel(xCord, yCord, newColor);
 }

Это должно дать разумную общую скорость, но (как предлагает Алекс) вы должны изучить нерекурсивные алгоритмы заливки, особенно если у вас большие растровые изображения. Рекурсивный алгоритм переполнит стек для больших заливок. В Википедии есть несколько довольно простых опций: https://en.wikipedia.org/wiki/Flood_fill. Простой способ - сохранить в основном ту же структуру, что и у вас, но вместо повторения для обработки каждого нового пикселя, явно обрабатывать стек редактируемых пикселей. Если вы знаете, что нацеливаетесь на небольшие области, то, вероятно, это будет достаточно быстро само по себе. Для поддержки более крупных вы можете оптимизировать дальше.

Прежде всего, я не вижу, как вы проверяете, находятся ли xCord или yCord вне границ растрового изображения и за пределами области, которую вы хотите заполнить. Знаете ли вы форму области крана, которую вы хотите заполнить заранее? Например, если он имеет эллиптическую форму, не проще ли вместо этого вызвать FillEllipse? Или если прямоугольный - FillRect?

Во-вторых, я считаю, что рекурсивный алгоритм неэффективен. Конечно, он отклоняет уже обработанные пиксели и не делает бесполезных вызовов SetPixel, но он делает много ложных проверок, потому что он все еще получает пиксель, анализирует его и производит рекурсивные вызовы.

Попробуйте визуализировать это. Если у вас есть растровое изображение 10x10, и вы нажимаете в середине (5; 5) даже до того, как будет установлен первый пиксель (это будет пиксель в 10;5), у вас будет 5 рекурсивных вызовов, и каждый из них производит еще 4 звонка и тд. И каждый вызов будет иметь доступ к растровому изображению, брать пиксели и тратить процессорное время.

В качестве небольшого улучшения попробуйте поместить вызов SetPixel перед рекурсивными вызовами:

bit.SetPixel(xCord, yCord, newColor);
setPixelColors(xCord + 1, yCord, newColor);
setPixelColors(xCord, yCord + 1, newColor);
setPixelColors(xCord - 1, yCord, newColor);
setPixelColors(xCord, yCord - 1, newColor);

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

Другие вопросы по тегам