Parallel.For 语句返回具有位图处理的"System.InvalidOperationException"



好吧,我有一个代码可以在" x"图像中应用雨弓滤波器,我必须通过两种方式进行:顺序&并行,我的顺序代码无问题,但并行部分不起作用。我不知道,为什么?

代码

public static Bitmap RainbowFilterParallel(Bitmap bmp)
    {
        Bitmap temp = new Bitmap(bmp.Width, bmp.Height);
        int raz = bmp.Height / 4;
        Parallel.For(0, bmp.Width, i =>
        {
            Parallel.For(0, bmp.Height, x =>
            {
                if (i < (raz))
                {
                    temp.SetPixel(i, x, Color.FromArgb(bmp.GetPixel(i, x).R / 5, bmp.GetPixel(i, x).G, bmp.GetPixel(i, x).B));
                }
                else if (i < (raz * 2))
                {
                    temp.SetPixel(i, x, Color.FromArgb(bmp.GetPixel(i, x).R, bmp.GetPixel(i, x).G / 5, bmp.GetPixel(i, x).B));
                }
                else if (i < (raz * 3))
                {
                    temp.SetPixel(i, x, Color.FromArgb(bmp.GetPixel(i, x).R, bmp.GetPixel(i, x).G, bmp.GetPixel(i, x).B / 5));
                }
                else if (i < (raz * 4))
                {
                    temp.SetPixel(i, x, Color.FromArgb(bmp.GetPixel(i, x).R / 5, bmp.GetPixel(i, x).G, bmp.GetPixel(i, x).B / 5));
                }
                else
                {
                    temp.SetPixel(i, x, Color.FromArgb(bmp.GetPixel(i, x).R / 5, bmp.GetPixel(i, x).G / 5, bmp.GetPixel(i, x).B / 5));
                }
            });
        });
        return temp;
    }

此外,在片刻中,程序返回相同的错误,但说"对象已经在使用"。

ps。我是C#的初学者,我在另一篇文章中搜索了此主题,但我什么也没发现。

非常感谢您

正如评论者罗恩·贝耶(Ron Beyer)指出的那样,使用SetPixel()GetPixel()方法非常慢。对这些方法之一的每种调用都涉及托管代码之间的过渡到Bitmap对象所代表的实际二进制缓冲区的过渡中的 lot 。那里有很多层,通常涉及视频驱动程序,需要在用户和内核级执行之间进行过渡。

但是,除了慢外,这些方法还使对象"忙",因此,如果尝试使用位图(包括调用其中一种方法),则在调用其中一种方法之间,并且当它返回时(即,在呼叫正在进行时),您看到的例外发生了错误。

,由于将当前代码并行的唯一方法是有用的是,如果这些方法调用可能同时发生,并且由于它们根本无法进行,因此此方法无法正常工作。

另一方面,使用LockBits()方法不仅可以保证起作用,而且很有可能会发现使用LockBits()的性能要好得多,甚至不需要并行化算法。但是,您是否应该决定自己这样做,因为LockBits()的工作方式&mdash;您可以访问代表位图映像&mdash的字节的原始缓冲区;您可以轻松地并行化算法并利用多个CPU内核(如果存在)。

请注意,使用LockBits()时,您将以可能不习惯的级别使用Bitmap对象。如果您尚未了解位图在"引擎盖下"的真正工作方式,那么您将不得不熟悉位图实际存储在内存中的方式。这包括了解不同的像素格式的含义,如何解释和修改给定格式的像素以及如何在内存中列出一个位图(例如,行的顺序可能会根据位图而变化,以及"大步"。

这些东西并不难学习,但这需要耐心。不过,如果表现是您的目标,这是值得的。

平行在奇异的头脑上很难。并将其与传统GDI 代码混合会导致奇怪的结果。

您的代码有许多问题:

  • 您每位像素三次致电get像素,而不是一次
  • 您正在访问像素的水平,不像您应该
  • 您叫y x和x i;机器不介意,但我们的人会
  • 您正在使用方法太多并行化。没有比您拥有更多的核心的用途。它会产生头顶上的限制,除非您的内部循环要做的工作非常艰难,例如数百万的计算。

但是,您得到的例外与这些问题无关。您不犯的一个错误是并行访问同一像素...所以为什么崩溃?

清理代码后,我发现堆栈跟踪中的错误指向SetPixel,并在System.Drawing.Image.get_Width()上指向。前者很明显,后者不是我们代码的一部分。!?

所以我挖掘了reforencesource.microsoft.com上的源代码,并找到了以下内容:

    /// <include file='docBitmap.uex' path='docs/doc[@for="Bitmap.SetPixel"]/*' />
    /// <devdoc>
    ///    <para>
    ///       Sets the color of the specified pixel in this <see cref='System.Drawing.Bitmap'/> .
    ///    </para>
    /// </devdoc>
    public void SetPixel(int x, int y, Color color) {
        if ((PixelFormat & PixelFormat.Indexed) != 0) {
            throw new InvalidOperationException(SR.GetString(SR.GdiplusCannotSetPixelFromIndexedPixelFormat));
        }
        if (x < 0 || x >= Width) {
            throw new ArgumentOutOfRangeException("x", SR.GetString(SR.ValidRangeX));
        }
        if (y < 0 || y >= Height) {
            throw new ArgumentOutOfRangeException("y", SR.GetString(SR.ValidRangeY));
        }
        int status = SafeNativeMethods.Gdip.GdipBitmapSetPixel(new HandleRef(this, nativeImage), x, y, color.ToArgb());
        if (status != SafeNativeMethods.Gdip.Ok)
            throw SafeNativeMethods.Gdip.StatusException(status);
    }

实际工作是由SafeNativeMethods.Gdip.GdipBitmapSetPixel完成的,但是在此之前,该方法对位图的宽度和高度进行了界限。尽管在我们的情况下,这些系统当然永远不会更改,但系统仍然不允许并行访问它们,因此当检查发生在某个时候时,崩溃了。当然,完全不理想,但是您去了..

so GetPixel(具有相同行为)和SetPixel不能安全地用于并行处理。

有两种方式:

我们可以将locks添加到代码中,因此请确保在"同一"时间进行检查:

public static Bitmap RainbowFilterParallel(Bitmap bmp)
{
    Bitmap temp = new Bitmap(bmp);
    int raz = bmp.Height / 4;
    int height = bmp.Height;
    int width = bmp.Width;
    // set a limit to parallesim
    int maxCore = 7;
    int blockH = height / maxCore + 1;
    //lock (temp) 
    Parallel.For(0, maxCore, cor =>
    {
        //Parallel.For(0, bmp.Height, x =>
        for (int yb = 0; yb < blockH; yb++)
        {
            int i = cor * blockH + yb;
            if (i >= height) continue;
            for (int x = 0; x < width; x++)
            {
                {
                  Color c;
                  // lock the Bitmap just for the GetPixel: 
                  lock (temp) c  = temp.GetPixel(x, i);
                  byte R = c.R;
                  byte G = c.G;
                  byte B = c.B;
                  if (i < (raz)) { R = (byte)(c.R / 5); }
                  else if (i < raz + raz) { G = (byte)(c.G / 5); }
                  else if (i < raz * 3) { B = (byte)(c.B / 5); }
                  else if (i < raz * 4) { R = (byte)(c.R / 5); B = (byte)(c.B / 5); }
                  else { G = (byte)(c.G / 5); R = (byte)(c.R / 5); }
                  // lock the Bitmap just for the SetPixel:
                  lock (temp) temp.SetPixel(x, i, Color.FromArgb(R,G,B));
                };
            }
        };
    });
    return temp;
}

请注意,限制等分非常重要,在平行类别类中甚至还有一个成员,并且Parallel.For中的参数可以控制它!我将最大核心数设置为7,但这会更好:

int degreeOfParallelism = Environment.ProcessorCount - 1;

所以这应该为我们节省一些开销。但仍然:我希望它会比更正的顺序方法慢!

彼得和罗恩建议的方法使事情变得非常快(1Ox),而添加并行性甚至可能更快。

因此,最终要完成这个长度的答案,这里有一个锁定和有限并行解决方案:

public static Bitmap RainbowFilterParallelLockbits(Bitmap bmp)
{
    Bitmap temp = null;
    temp = new Bitmap(bmp);
    int raz = bmp.Height / 4;
    int height = bmp.Height;
    int width = bmp.Width;
    Rectangle rect = new Rectangle(Point.Empty, bmp.Size);
    BitmapData bmpData = temp.LockBits(rect,ImageLockMode.ReadOnly, temp.PixelFormat);
    int bpp = (temp.PixelFormat == PixelFormat.Format32bppArgb) ? 4 : 3;
    int size = bmpData.Stride * bmpData.Height;
    byte[] data = new byte[size];
    System.Runtime.InteropServices.Marshal.Copy(bmpData.Scan0, data, 0, size);
     var options = new ParallelOptions();
     int maxCore = Environment.ProcessorCount - 1;
     options.MaxDegreeOfParallelism = maxCore > 0 ? maxCore  : 1;
    Parallel.For(0, height, options, y =>
    {
        for (int x = 0; x < width; x++)
        {
            {
              int index = y * bmpData.Stride + x * bpp;
              if (y < (raz))   data[index + 2] = (byte) (data[index + 2] / 5);
              else if (y < (raz * 2))  data[index + 1] = (byte)(data[index + 1] / 5);
              else if (y < (raz * 3)) data[index ] = (byte)(data[index ] / 5);
              else if (y < (raz * 4))
                {   data[index + 2] = (byte)(data[index + 2] / 5); 
                    data[index] = (byte)(data[index] / 5); }
              else
              {   data[index + 2] = (byte)(data[index + 2] / 5);
                  data[index + 1] = (byte)(data[index + 1] / 5);
                  data[index] = (byte)(data[index] / 5);  }
          };
        };
    });
    System.Runtime.InteropServices.Marshal.Copy(data, 0, bmpData.Scan0, data.Length);
    temp.UnlockBits(bmpData);
    return temp;
}

虽然不是严格相关的,但我想发布的版本比答案中给出的任何一个更快。这是我通过位图迭代并将结果保存在C#中的最快方法。在我的工作中,我们需要浏览数百万张大图像,这只是我抓住红色频道并为自己的目的保存它,但它应该给您如何工作的想法

//Parallel Unsafe, Corrected Channel, Corrected Standard div 5x faster
     private void TakeApart_Much_Faster(Bitmap processedBitmap)
    {
        
        _RedMin = byte.MaxValue;
        _RedMax = byte.MinValue;
        _arr = new byte[BMP.Width, BMP.Height];
        long Sum = 0,
        SumSq = 0;
        BitmapData bitmapData = processedBitmap.LockBits(new Rectangle(0, 0, processedBitmap.Width, processedBitmap.Height), ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
//this is a much more useful datastructure than the array but it's slower to fill.
        points = new ConcurrentDictionary<Point, byte>();
        unsafe
        {
       int bytesPerPixel = Image.GetPixelFormatSize(bitmapData.PixelFormat) / 8;
            int heightInPixels = bitmapData.Height;
            int widthInBytes = bitmapData.Width * bytesPerPixel;
            _RedMin = byte.MaxValue;
            _RedMax = byte.MinValue;
            byte* PtrFirstPixel = (byte*)bitmapData.Scan0;
            Parallel.For(0, heightInPixels, y =>
            {
    //pointer to the first pixel so we don't lose track of where we are
         byte* currentLine = PtrFirstPixel + (y * bitmapData.Stride);
         for (int x = 0; x < widthInBytes; x = x + bytesPerPixel)
                {
                    //0+2 is red channel
                    byte redPixel = currentLine[x + 2];
                    Interlocked.Add(ref Sum, redPixel);
                    Interlocked.Add(ref SumSq, redPixel * redPixel);
          //divide by three since we are skipping ahead 3 at a time.
                 _arr[x/3, y] = redPixel;
                 _RedMin = redPixel < _RedMin ? _RedMin : redPixel;
                 _RedMax = redPixel > RedMax ? RedMax : redPixel;
                }
            });
         
            _RedMean = Sum / TotalPixels;
_RedStDev = Math.Sqrt((SumSq / TotalPixels) - (_RedMean * _RedMean));
            processedBitmap.UnlockBits(bitmapData);
        }
    }

最新更新