4
\$\begingroup\$

I have some parallel.for one inside another.

the last parallel.for have a normal for that should Create images by combining other images.

the images are generated but the memory consumed by the process slowly increases.

I'm using net core 6, and as you can see I have dispatched all the Bitmaps and the Graphics objects.

also I'm forcing garbage collection so the memory stop growing (I ran the code for 4 hours without forcing collection and the dispatched objects were not collected)

here is the code:


using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;

Console.WriteLine("Generando!");

var count = 0;

Parallel.For(1, 11, (a) =>
{
    Parallel.For(1, 11, (b) =>
    {
        Parallel.For(1, 11, (c) =>
        {
            Parallel.For(1, 11, (d) =>
            {
                Parallel.For(1, 11, (e) =>
                {
                    for (int f = 1; f <= 10; f++)
                    {
                        Bitmap source1 = new Bitmap($"1/{a}.png");
                        Bitmap source2 = new Bitmap($"2/{b}.png");
                        Bitmap source3 = new Bitmap($"3/{c}.png");
                        Bitmap source4 = new Bitmap($"4/{d}.png");
                        Bitmap source5 = new Bitmap($"5/{e}.png");

                        Bitmap sourceBase = new Bitmap($"Rostro Base.png");
                        Bitmap source6 = new Bitmap($"6/{f}.png");
                        var target = new Bitmap(source1.Width, source1.Height, PixelFormat.Format32bppArgb);
                        var graphics = Graphics.FromImage(target);
                        graphics.CompositingMode = CompositingMode.SourceOver; // this is the default, but just to be clear
                        graphics.DrawImage(sourceBase, 0, 0);
                        graphics.DrawImage(source6, 0, 0);
                        graphics.DrawImage(source5, 0, 0);
                        graphics.DrawImage(source4, 0, 0);
                        graphics.DrawImage(source3, 0, 0);
                        graphics.DrawImage(source2, 0, 0);
                        graphics.DrawImage(source1, 0, 0);

                        count++;
                        var nombre = $"{count}_{a}-{b}-{c}-{d}-{e}-{f}";
                        var target2 = Cropimage(target);
                        target2.Save($"rostros/{nombre}.png", ImageFormat.Png);

                        source1.Dispose();
                        source2.Dispose();
                        source3.Dispose();
                        source4.Dispose();
                        source5.Dispose();
                        source6.Dispose();

                        sourceBase.Dispose();
                        target.Dispose();
                        target2.Dispose();
                        graphics.Dispose();

                        GC.Collect();
                    }
                });
                Console.Write($"\r{count} imagenes generadas                ");
            });
        });
    });
});

Bitmap Cropimage(Bitmap input)
{
    // Find the min/max non-white/transparent pixels
    Point min = new Point(int.MaxValue, int.MaxValue);
    Point max = new Point(int.MinValue, int.MinValue);

    for (int x = 0; x < input.Width; ++x)
    {
        for (int y = 0; y < input.Height; ++y)
        {
            Color pixelColor = input.GetPixel(x, y);
            if (pixelColor.A > 0)
            {
                if (x < min.X) min.X = x;
                if (y < min.Y) min.Y = y;

                if (x > max.X) max.X = x;
                if (y > max.Y) max.Y = y;
            }
        }
    }

    // Create a new bitmap from the crop rectangle
    Rectangle cropRectangle = new Rectangle(min.X, min.Y, max.X - min.X, max.Y - min.Y);

    Bitmap newBitmap = new Bitmap(cropRectangle.Width, cropRectangle.Height);
    using (Graphics g = Graphics.FromImage(newBitmap))
    {
        g.DrawImage(input, 0, 0, cropRectangle, GraphicsUnit.Pixel);
    }

    return newBitmap;

} 
\$\endgroup\$
3
  • 3
    \$\begingroup\$ When I saw this Parallel.For(1, 11, (a) => ... Parallel.For(1, 11, (e) => that's an instant NO. This is called over-parallelism. Most of the time it is enough if your outermost loop is the one which is parallel. \$\endgroup\$ Jan 17, 2022 at 7:18
  • 1
    \$\begingroup\$ @PeterCsala, My code works, the images are generated as expected, but its memory consumption increases over time (it goes up to 18 gb after running for 8 hours). also thank you for the over-parallelism article I'll check it \$\endgroup\$
    – Bloodday
    Jan 17, 2022 at 7:21
  • \$\begingroup\$ Cross-posted on Stack Overflow. \$\endgroup\$
    – Mast
    Jan 24, 2022 at 5:26

1 Answer 1

7
\$\begingroup\$

Just few tips

  • Prefer using over manual calling Dispose().
  • Consumed memory isn't always busy memory, GC can free the memory anytime it want. That is OK, trust GC. Just assume that there's no leaks in managed code possible unless you manually allocated unmanaged memory.
  • manual calling GC.Collect() is almost never effective but makes the app slower. The above code isn't an exception
  • NEVER use GetPixel/SetPixel if you don't want to die before the app ends working, it's superslow way to deal with Bitmap
  • count is shared counter, increment it thread-safely.
  • No need to read the same image from disk for each Thread, lock here is more efficient.
int count = 0;
using Bitmap sourceBase = new Bitmap($"Rostro Base.png");
Parallel.For(1, 11, (a) =>
{
    using Bitmap source1 = new Bitmap($"1/{a}.png");
    Parallel.For(1, 11, (b) =>
    {
        using Bitmap source2 = new Bitmap($"2/{b}.png");
        Parallel.For(1, 11, (c) =>
        {
            using Bitmap source3 = new Bitmap($"3/{c}.png");
            Parallel.For(1, 11, (d) =>
            {
                using Bitmap source4 = new Bitmap($"4/{d}.png");
                Parallel.For(1, 11, (e) =>
                {
                    using Bitmap source5 = new Bitmap($"5/{e}.png");

                    for (int f = 1; f <= 10; f++)
                    {
                        using Bitmap source6 = new Bitmap($"6/{f}.png");
                        using Bitmap target = new Bitmap(source1.Width, source1.Height, PixelFormat.Format32bppArgb);
                        using Graphics graphics = Graphics.FromImage(target);
                        graphics.CompositingMode = CompositingMode.SourceOver; // this is the default, but just to be clear
                        lock(sourceBase) graphics.DrawImage(sourceBase, 0, 0);
                        graphics.DrawImage(source6, 0, 0);
                        graphics.DrawImage(source5, 0, 0);
                        lock(source4) graphics.DrawImage(source4, 0, 0);
                        lock(source3) graphics.DrawImage(source3, 0, 0);
                        lock(source2) graphics.DrawImage(source2, 0, 0);
                        lock(source1) graphics.DrawImage(source1, 0, 0);

                        int localCount = Interlocked.Increment(ref count);
                        string nombre = $"{localCount}_{a}-{b}-{c}-{d}-{e}-{f}";
                        using Bitmap target2 = Cropimage(target);
                        target2.Save($"rostros/{nombre}.png", ImageFormat.Png);
                    }
                });
                Console.Write($"\r{count} imagenes generadas                ");
            });
        });
    });
});

    
Bitmap Cropimage(Bitmap input)
{
    // Find the min/max non-white/transparent pixels
    Point min = new Point(int.MaxValue, int.MaxValue);
    Point max = new Point(int.MinValue, int.MinValue);

    // Retreiving bitmap data to array
    BitmapData data = input.LockBits(new Rectangle(Point.Empty, input.Size), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
    byte[] bytes = new byte[input.Height * input.Width * 4]; // 32bpp
    Marshal.Copy(data.Scan0, bytes, 0, input.Height * data.Stride); // Stride can be negative in some bitmaps but Marshal supports that.
                                                                    // In short: (Math.Abs(data.Stride) == input.Width * 4) for 32bpp is always 'true'.
    input.UnlockBits(data);

    // bytes array contains sequence of 4-byte pixels like B G R A B G R A
    for (int y = 0; y < input.Height; ++y)
    {
        int rowOffset = y * input.Width * 4;
        for (int x = 0; x < input.Width; ++x)
        {
            int colOffset = x * 4;
            if (bytes[rowOffset + colOffset + 3] > 0)
            {
                if (x < min.X) min.X = x;
                if (y < min.Y) min.Y = y;

                if (x > max.X) max.X = x;
                if (y > max.Y) max.Y = y;
            }
        }
    }

    // Create a new bitmap from the crop rectangle
    Rectangle cropRectangle = new Rectangle(min.X, min.Y, max.X - min.X, max.Y - min.Y);

    Bitmap newBitmap = new Bitmap(cropRectangle.Width, cropRectangle.Height);
    using Graphics g = Graphics.FromImage(newBitmap);
    g.DrawImage(input, 0, 0, cropRectangle, GraphicsUnit.Pixel);

    return newBitmap;
}

This might work ~100x times faster than the initial code.

It also may work without locks but I'm not sure if DrawImage uses LockBits internally and didn't try accessing single Bitmap from multiple threads. But you may try. Anyway there will be no any sensitive difference in performance. Reading same images thousands times from disk is significantly slower in comparison to reading it from memory even locked for single-threaded access.

\$\endgroup\$
3
  • 2
    \$\begingroup\$ Thank you for the optimization, you are right there is no memory leak, the nested parallel.for created more overhead and the bitmap objects lived longer in the memory. I rewrited the code to generate an array and pushed all the combiniations of "a", "b".. "e", "f" in the array and then used a single parallel.for over the array, the memory consumption went down from 18 gb to 1.5 gb \$\endgroup\$
    – Bloodday
    Jan 17, 2022 at 23:11
  • 1
    \$\begingroup\$ rowOffset should use data.Stride instead of input.width. \$\endgroup\$
    – jdt
    Jan 18, 2022 at 13:21
  • \$\begingroup\$ @jdt correct but data.Stride can be negative depending on the saved Bitmap format. Also data is already unlocked at the point. \$\endgroup\$
    – aepot
    Jan 18, 2022 at 17:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.