Манипулируем System.Drawing.Bitmap

    Класс System.Drawing.Bitmap очень полезен в инфраструктуре .NET, т.к. позволяет считывать и сохранять файлы различных графических форматов. Единственная проблема – это то, что он не очень полезен для попиксельной обработки – например если нужно перевести битмап в ч/б. Под катом – небольшой этюд на эту тему.

    Допустим у нас есть два битмапа, один из которых считан из файла, а другой должен содержать ч/б конверсию:
    // загружаем картинку
    sourceBitmap = (Bitmap) Image.FromFile("Zap.png");<br/>
    // делаем пустую картинку того же размера
    targetBitmap = new Bitmap(sourceBitmap.Width, sourceBitmap.Height, sourceBitmap.PixelFormat);<br/>
    Мы хотим чтобы targetBitmap был как sourceBitmap, только черно-белый. На самом деле, в C# делается это просто:

    void NaïveBlackAndWhite()<br/>
    {<br/>
      for (int y = 0; y < sourceBitmap.Height; ++y)<br/>
        for (int x = 0; x < sourceBitmap.Width; ++x)<br/>
        {<br/>
          Color c = sourceBitmap.GetPixel(x, y);<br/>
          byte rgb = (byte)(0.3 * c.R + 0.59 * c.G + 0.11 * c.B);<br/>
          targetBitmap.SetPixel(x, y, Color.FromArgb(c.A, rgb, rgb, rgb));<br/>
        }<br/>
    }<br/>
    Это решение понятное и простое, но к сожалению жуть как неэффективное. Чтобы получить более «резвый» код, можно попробовать написать все это дело на С++. Для начала создадим структурку для хранения цветовых значений пикселя

    // структура отражает один пиксель в 32bpp RGBA
    struct Pixel {<br/>
      BYTE Blue;<br/>
      BYTE Green;<br/>
      BYTE Red;<br/>
      BYTE Alpha;<br/>
    };<br/>
    Теперь можно написать функцию которая будет делать пиксель черно-белым:

    Pixel MakeGrayscale(Pixel& pixel)<br/>
    {<br/>
      const BYTE scale = static_cast<BYTE>(0.3 * pixel.Red + 0.59 * pixel.Green + 0.11 * pixel.Blue);<br/>
      Pixel p;<br/>
      p.Red = p.Green = p.Blue = scale;<br/>
      p.Alpha = pixel.Alpha;<br/>
      return p;<br/>
    }<br/>
    Теперь собственно пишем саму функцию обхода:

    CPPSIMDLIBRARY_API void AlterBitmap(BYTE* src, BYTE* dst, int width, int height, int stride)<br/>
    {<br/>
      for (int y = 0; y < height; ++y) {<br/>
        for (int x = 0; x < width; ++x)<br/>
        {<br/>
          int offset = x * sizeof(Pixel) + y * stride;<br/>
          Pixel& s = *reinterpret_cast<Pixel*>(src + offset);<br/>
          Pixel& d = *reinterpret_cast<Pixel*>(dst + offset);<br/>
          // изменяем d
          d = MakeGrayscale(s);<br/>
        }<br/>
      }<br/>
    }<br/>
    А дальше остается только использовать ее из C#.

    void UnmanagedBlackAndWhite()<br/>
    {<br/>
      // "зажимаем" байты обеих картинок
      Rectangle rect = new Rectangle(0, 0, sourceBitmap.Width, sourceBitmap.Height);<br/>
      BitmapData srcData = sourceBitmap.LockBits(rect, ImageLockMode.ReadWrite, sourceBitmap.PixelFormat);<br/>
      BitmapData dstData = targetBitmap.LockBits(rect, ImageLockMode.ReadWrite, sourceBitmap.PixelFormat);<br/>
      // отсылаем в unmanaged код для изменений
      AlterBitmap(srcData.Scan0, dstData.Scan0, srcData.Width, srcData.Height, srcData.Stride);<br/>
      // отпускаем картинки
      sourceBitmap.UnlockBits(srcData);<br/>
      targetBitmap.UnlockBits(dstData);<br/>
    }<br/>
    Это улучшило быстродействие, но мне захотелось еще большего. Я добавил директиву OpenMP перед циклом по y и получил предсказуемое ускорение в 2 раза. Дальше захотелось поэкспериментировать и попробовать применить еще и SIMD. Для этого я написал вот этот, не очень читабельный, код:

    CPPSIMDLIBRARY_API void AlterBitmap(BYTE* src, BYTE* dst, int width, int height, int stride)<br/>
    {<br/>
      // факторы для конверсии в ч/б
      static __m128 factor = _mm_set_ps(1.0f, 0.3f, 0.59f, 0.11f);<br/>
      #pragma omp parallel for<br/>
      for (int y = 0; y < height; ++y)<br/>
      {<br/>
        const int offset = y * stride;<br/>
        __m128i* s = (__m128i*)(src + offset);<br/>
        __m128i* d = (__m128i*)(dst + offset);<br/>
        for (int x = 0; x < (width >> 2); ++x) {<br/>
          // у нас 4 пикселя за раз
          for (int p = 0; p < 4; ++p)<br/>
          {<br/>
            // конвертируем пиксель
            __m128 pixel;<br/>
            pixel.m128_f32[0] = s->m128i_u8[(p<<2)];<br/>
            pixel.m128_f32[1] = s->m128i_u8[(p<<2)+1];<br/>
            pixel.m128_f32[2] = s->m128i_u8[(p<<2)+2];<br/>
            pixel.m128_f32[3] = s->m128i_u8[(p<<2)+3];<br/>
            // четыре операции умножения - одной командой!
            pixel = _mm_mul_ps(pixel, factor);<br/>
            // считаем сумму
            const BYTE sum = (BYTE)(pixel.m128_f32[0] + pixel.m128_f32[1] + pixel.m128_f32[2]);<br/>
            // пишем назад в битмап
            d->m128i_u8[p<<2] = d->m128i_u8[(p<<2)+1] = d->m128i_u8[(p<<2)+2] = sum;<br/>
            d->m128i_u8[(p<<2)+3] = (BYTE)pixel.m128_f32[3];<br/>
          }<br/>
          s++;<br/>
          d++;<br/>
        }<br/>
      }<br/>
    }<br/>
    Несмотря на то, что этот код делает 4 операции умножения за раз (инструкция _mm_mul_ps), все эти конверсии не дали никакого выигрыша по сравнению с обычными операциями – скорее наоборот, алгоритм начал работать медленнее. Вот результаты выполнения функций на картинке 360×480. Использовался 2х-ядерный MacBook с 4Гб RAM, результаты усредненные.




    А вот и конечный результат:




    Выводы:

    • SetPixel/GetPixel – это зло, их трогать не стоит.
    • OpenMP продолжает радовать, давая линейный scalability.
    • Использование SIMD не гарантирует повышение производительности. Зато доставляет много хлопот.

    Если кто-нибудь из читателей готов написать еще более эффективный алгоритм – милости просим! Протестирую и опубликую его прямо здесь.

    Similar posts

    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 54

      +1
      unsafe не пробовали?
        0
        пробовал давным-давно, результаты были чем-то промежуточным между c# и c++. сейчас поленился.
        +10
        Вы придумали новый логотип для Оперы… :P
        • UFO just landed and posted this here
          0
          Интересное у Вас решение. Зимой писал графредактор на C#, в редакторе были реализованы различные фильтры. Черно-белое изображение получал при помощи unsafe. Попробую и Ваш способ ради интереса :)
            0
            Мне тоже кажется, что unsafe будет ничуть не медленнее, хотя нужно проверить. Плюс к этому в шарпе можно использовать Parallel Extensions.

            Я как раз сейчас борюсь с долгой инициализацией больших картинок в WPF. При динамическом обновлении Image-контрола картинкой размером 3000×3000 задержка в трэде рисование почти полсекунды. Что, при обновление раз в секунду, делает интерфейс не юзабельным. Никто не сталкивался с таким?
              0
              Этот контрол для быстрых обновлений не годится, как впрочем и аналогичный контрол WinForms. С размером 3000х3000 без аппаратной поддержки вообще трудно, а в WPF она видимо не подвязана (хотя и обещали нам).
                0
                Кто ж такие картинки сразу в контрол пихает, надо сначала ресайзить, потом рисовать уже нужного размера.

                Или пилить на куски, грузить в текстуры и показывать через DX/OpenGL.
              +4
              Вот вариант с unsafe. У меня на Pentium D 3Ггц c картинкой 3000×3000 занимает около 340 миллисекунд.

              1. Bitmap MakeBW(Bitmap source)
              2.     {
              3.       //использование промежуточных переменных ускоряет
              4.       //код в несколько раз
              5.       var width = source.Width;
              6.       var height = source.Height;
              7.  
              8.       var sourceData = source.LockBits(new Rectangle(new System.Drawing.Point(0, 0), source.Size),
              9.                        ImageLockMode.ReadOnly,
              10.                        source.PixelFormat);
              11.      
              12.       result = new Bitmap(width, height, source.PixelFormat);
              13.       var resultData = result.LockBits(new Rectangle(new System.Drawing.Point(0, 0), result.Size),
              14.                        ImageLockMode.ReadWrite,
              15.                        source.PixelFormat);
              16.  
              17.  
              18.       var sourceStride = sourceData.Stride;
              19.       var resultStride = resultData.Stride;
              20.  
              21.       var sourceScan0 = sourceData.Scan0;
              22.       var resultScan0 = resultData.Scan0;
              23.  
              24.       var resultPixelSize = resultStride / width;
              25.      
              26.       unsafe
              27.       {
              28.         for(var y = 0 ; y < height ; y++)
              29.         {
              30.           var sourceRow = (byte*)sourceScan0 + (y * sourceStride);
              31.           var resultRow = (byte*)resultScan0 + (y * resultStride);
              32.           for(var x = 0 ; x < width ; x++)
              33.           {
              34.             var v =(byte)(0.3*sourceRow[x*resultPixelSize + 2] + 0.59*sourceRow[x*resultPixelSize + 1] +
              35.                0.11*sourceRow[x*resultPixelSize]);
              36.               resultRow[ x * resultPixelSize ] = v;
              37.               resultRow[ x * resultPixelSize + 1 ] = v;
              38.               resultRow[ x * resultPixelSize + 2 ] = v;
              39.           }
              40.         }
              41.  
              42.  
              43.       }
              44.  
              45.       source.UnlockBits(sourceData);
              46.       result.UnlockBits(resultData);
              47.       return result;
              48.     }
              * This source code was highlighted with Source Code Highlighter.
                0
                Не заметил сперва ваш размер картинки. С размер 360×480 — 8мс.
                  –6
                  Молодцом. Автор не в теме (того, что все знают, что Bitmap довольно медленная обертка для работы с картинкой попиксельно)
                    0
                    Вместо работы с unsafe можно использовать System.Runtime.InteropServices.Marshal.Copy и получать данные в managed массиве. Скорость работы такая же, а небезопасный код не используется.
                      0
                      Угу. Я так же делал, все нормально работало.
                        0
                        А лишняя копия данных в результате не вылазит?
                        +1
                        Немного не по теме, но это ярчайший пример, когда использование var ухудшает читаемость кода.
                          +1
                          Возможно, но так не намного понятнее:
                          BitmapData resultData = result.LockBits(new System.Drawing.Rectangle(new System.Drawing.Point(0, 0), result.Size),
                                                ImageLockMode.ReadWrite,
                                                System.Drawing.Imaging.PixelFormat.Format32bppArgb);


                          * This source code was highlighted with Source Code Highlighter.

                          Надо просто комментариев больше писать. :)
                            +1
                            Так хотябы можно сразу узнать тип переменной. Вот в комментарии его узнать уже почти невозможно, в студии же нужно будет делать дополнительные телодвижения.

                            Рекомендация Майкрософт:
                            «Overuse of var can make source code less readable for others. It is recommended to use var only when it is necessary, that is, when the variable will be used to store an anonymous type or a collection of anonymous types.»

                            Т.е. я за то, чтобы int i = 0 все-таки оставлять как есть, а вот:
                            List megaVar = new List()
                            можно и заменить на
                            var megaVar = new List()
                              0
                              Мой мега тип порезался хабрапарсером, но вобщем он был длинный. :)
                          0
                          Еще неплохо было бы выполнять все действия в unchecked { }. Это тоже может дать небольшой прирост производительности.
                          –14
                          Афтар — ты дизайнер и не более. Рисуй картинки и не лезь в программирование. Хочешь опустить .NET — пиши статьи на rsdn.ru, там тебе плюсики ни за хрен собачий никто ставить не станет.
                            0
                            А можно цветную 1600 на 1200?
                              +1
                              А через ColorMatrix не пробовали?
                                0
                                Нет, а как это? :)
                                +4
                                Диаграмма в статье — отличный пример того, как не надо делать графики и диаграммы :)
                                  0
                                  Я вот смотрю и думаю: где же лежат мои 8мс. :) Мозг взрывает.
                                  +1
                                  Примерно вот так.
                                    –1
                                      0
                                      А можно цветную 1600 на 1200?
                                        0
                                        Сорри, в таком разнешении у меня ее нет, это картинки для айфона :)
                                        0
                                        я так понимаю это результат преобразования через ColorMatrix, да?
                                        тогда вопрос — что со временем исполнения получилось?
                                        писать код для проверки лениво, раз уж у вас есть код то и замерить вам легче ;)
                                          0
                                          И я не менее ленив. :) Тем более, что в моем случае производительность большой роли не играет.
                                          Проще, мне кажется, автору данной статьи добавить еще итераций с использованием ColorMatrix (и других других улучшений из комментариев), т.к. у него уже был код для тестирования.
                                            0
                                            :)
                                            просто подумалось, что если есть результат, то значит есть и код с помощью которого он получен :")
                                            а так да, так надо ждать результатов от автора :)

                                            автор, не пробовали колорМатрикс?
                                        +2
                                        Сепия:
                                          0
                                          А вы не пробовали достать палитру из картинки, ее преобразовать в черно-белую и вставить обратно?
                                            0
                                            Это кстате тоже хорошая идея! Надо будет попробовать!
                                              0
                                              Вот-вот! Я все читаю камменты и думаю, когда уже кто-нибудь нормальный способ предложит
                                                0
                                                А что значит «нормальный»? Есть много разных способов…
                                                  0
                                                  Просто мне показалось, что ручной проход по всем пикселям — это не есть гуд.
                                                +2
                                                Судя по количеству цветов, картинка имеет 24 или 32 бита на пиксель, следовательно палитры нет.
                                                  0
                                                  Полностью согласен, именно эта картинка не имеет.
                                                  0
                                                  Для картинки с палитрой этот код работать не будет, там данные битмапа не в RGB хранятся, а в индексах цветов.
                                                  +3
                                                  И еще, зачем умножать, сделайте 3 массива по 256 цветов для каждого канала, где ключ — исходный цвет, а значение — цвет уже умноженный на константу. Дальше функция преобразования будет напоминать что то типа
                                                  gray_value = red[old.Red] + green[old.Green] + blue[old.Blue];
                                                  Лишних 3*256 байт памяти.
                                                    0
                                                    Класс! Что ни коммент то идея! Спасибо, буду тестировать!
                                                    0
                                                    Вопрос от дизайнера:
                                                    как делают такие фракталы вокруг буквы О?
                                                      0
                                                      Fluid mechanics? Navier-Stokes?
                                                      0
                                                      DirectX HLSL в помощь.
                                                      В WPF шейдеры на рас подключаются к эл-там (Effect).
                                                      Всю работу делает GPU.
                                                        0
                                                        Оффтоп: а что было использовано для дотнета на маке? Можно ссылочку на рантайм/IDE, посмотреть хочется.
                                                          0
                                                          MonoDevelop, например.
                                                            0
                                                            Прогуглил… Вобщем на макос нечем разрабатывать — тот же MonoDevelop даже до alpha-релизов не дошел.
                                                              0
                                                              ну почему? вполне дошел. Даже Helloworld можно написать. А вот дальше конечно уже *опа
                                                                0
                                                                o_0 взято с monodevelop.com
                                                                MonoDevelop 2.0 has been released. See the official announcement here.

                                                                March 30, 2009


                                                                если я не ошибаюсь, то моноДевелоп это форк шарпДевелопа, т.е. более-менее сносной IDE под .NET (если не сравнивать с VS :D ), неужели с ним действительно все так плачевно?!
                                                                  0
                                                                  Посмотри на его оффсайте комментарии к маковой версии ;)
                                                            0
                                                            красивая картинка =)
                                                              0
                                                              Для начала стоило бы попробывать
                                                              byte rgb = (byte)(( (uint32)c.R * 5033165 + (uint32)c.G * 9898557 + (uint32)c.B * 1845494 ) >> 24)

                                                              Only users with full accounts can post comments. Log in, please.