2

I'm working on an image processing project and I'm facing an issue.

I have a method that takes a stream (of an image) as input and returns a list of streams containng subparts of the provided image :

public List<System.IO.Stream> ImageProcessing(System.IO.Stream input){
    Bitmap inputAsBitmap = new Bitmap(input);
    // --- Applying miscellaneous filters ---

    List<Bitmap> subParts = GetSubParts(inputAsBitmap);
    inputAsBitmap.Dispose();

    List<System.IO.Stream> streams = new List<Stream>();
    for(int i = 0; i < subParts.Count; i++){
        MemoryStream memoryStream = new MemoryStream();
        subParts[i].Save(memoryStream, ImageFormat.Jpeg);
        streams.Add(memoryStream);
        subParts[i].Dispose();
    }
    return streams;
}

When I call this method manually by selecting an image through an OpenFileDialog everything works fine:

using (Stream imageStream = File.OpenRead(filename)){
    List<Stream> streams = ImageProcessing(imageStream);
    for(int i = 0; i < streams.Count; i++){
        System.Drawing.Image.FromStream(streams[i]).Save();
        streams[i].Dispose();
    }
}

But when I try to select a folder and call it in a for loop with every image belonging to it, an OutOfMemoryException is thrown:

for(int i = 0; i < filenames.Count; i++){
    using (Stream imageStream = File.OpenRead(filenames[i])){
        List<Stream> streams = ImageProcessing(imageStream);
        for(int j = 0; j < streams.Count; j++){
            System.Drawing.Image.FromStream(streams[j]).Save();
            streams[j].Dispose();
        }
    }
}

While investigating, I found that having a block of instructions that takes around 20ms to execute allows the memory to get cleaned and thus prevents the exception to be thrown:

for(int i = 0; i < filenames.Count; i++){
    // --- A block of instructions placed here will prevent the exception ---
    using (Stream imageStream = File.OpenRead(filenames[i])){
        // -- Same code as above ---
    }
}

Moreover, while checking the memory on several points (by using Process.GetCurrentProcess().PrivateMemorySize64 or System.GC.GetTotalMemory()) it seems that disposing of Bitmap and Stream does not change anything concerning the amount of used memory.

Any hint on how I could get rid of this OutOfMemoryException?

Do you have any explanation concerning the fact that the memory does not get cleaned when calling the ImageProcessing method in a for loop while it does between two calls when they are done manually?


EDIT:

This is what I have now, but still not working:

for(int i = 0; i < filenames.Count; i++){
    using (Stream imageStream = File.OpenRead(filenames[i])){
        List<Stream> streams = ImageProcessing(imageStream);
        for(int j = 0; j < streams.Count; j++){
            using (var image = System.Drawing.Image.FromStream(streams[j]))
                image.Save();
            streams[j] = null;
            //System.GC.Collect();
        }
    }
}
//System.GC.Collect();
R.Duteil
  • 1,205
  • 1
  • 13
  • 26
  • How many files are selected in the folder? WHat is the size of all these images? Is the process compiled with AnyCPU (i.e. is it a 64 bits process)? – Kzryzstof Oct 03 '18 at 13:22
  • The folder contains around 20 files, but the first Exception is thrown while processing the fifth or sixth. Size of images range from 1MB to 4MB. The build configuration is set on Any CPU. The OutOfMemoryException is thrown when the used memory reaches 2GB – R.Duteil Oct 03 '18 at 13:26
  • 2
    You might be building 32 bit without realizing it. See [Compiling as AnyCPU produces assembly with 32BIT flag set on 64-bit machine](https://stackoverflow.com/q/37551064) and http://blogs.microsoft.co.il/sasha/2012/04/04/what-anycpu-really-means-as-of-net-45-and-visual-studio-11/ – dbc Oct 03 '18 at 13:29
  • 1
    Looks like you are indeed building an AnyCPU application but defaulted to 32 bits. – Kzryzstof Oct 03 '18 at 13:32
  • 2
    Is `streams[i]` actually a `MemoryStream`? If so, then disposing of it doesn't actually free any memory, see https://referencesource.microsoft.com/#mscorlib/system/io/memorystream.cs,0b83d17ca69bf8ea,references. You need to explicitly remove any references to it, so it can get garbage collected. – dbc Oct 03 '18 at 13:36
  • @dbc the only reference I have to a stream is via the list. I tried to remove each stream of the list in the for loop after using it but it doesn't change anything. – R.Duteil Oct 03 '18 at 13:42
  • @Kzrystof I was indeed building a 32 bits version, switching to a 64 bits version allows to use more memory but what if I have to handle 80 images ? Do you have any hint concerning why the value returned by Process.GetCurrentProcess().PrivateMemorySize64 doesn't go down in the for loop while it does when manually picking images ? – R.Duteil Oct 03 '18 at 13:43
  • @R.Duteil It is best to let the GC deal with the memory. However, you could start calling GC.Collect if you know this is a one-time operation that happens once in a while. – Kzryzstof Oct 03 '18 at 13:46
  • @Kzrystof already tried it, doesn't seem to actually free memory. It's like I always have a reference to every value of the stream list while in a for loop – R.Duteil Oct 03 '18 at 13:47
  • @R.Duteil Could you add the GC.Collect call in your question? – Kzryzstof Oct 03 '18 at 13:49
  • @Kzrystof done ! – R.Duteil Oct 03 '18 at 13:53
  • @R.Duteil There is the part saying about preventing exceptions around streams[j] that worries me a bit. Could you make sure streams[j] is used in an using statement or at least make sure Dispose is called in a try/finally block. – Kzryzstof Oct 03 '18 at 13:54
  • @Kzrystof I actually do not use a try/catch/finally block, neither do I use a using statement for streams[j] as it is an element of a list returned by a method. I just Dispose it in the for loop after using it. – R.Duteil Oct 03 '18 at 14:00
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/181225/discussion-between-kzrystof-and-r-duteil). – Kzryzstof Oct 03 '18 at 14:02
  • `System.Drawing.Image.FromStream()` returns something that is `IDisposable` – bommelding Oct 03 '18 at 14:05
  • @bommelding I do not get what you mean – R.Duteil Oct 03 '18 at 14:08
  • @Kzrystof What helped me the most was your advice to switch to 64 bits, would you mind writing an aswer about it so I can accept it ? Considering it solved my problem for small / medium sets of files and that I do not have to deal with large ones for now – R.Duteil Oct 03 '18 at 14:54
  • 1
    Switching to 64 bits is just postponing the problem - this should be able to run in 3GB when you fix your leaks. And you better not run this code in a service. – bommelding Oct 04 '18 at 06:50

1 Answers1

3

There are few things that you can do to improve things.

First, you should force the application to be run as a 64 bits application and run the program in Release mode as the Debug mode may cause the program to hold on instances a bit longer.

Then the call to System.Drawing.Image.FromStream(streams[j]).Save(); returns an Image so it must be disposed as well.

Finally, I am not a fan of putting null when I am done with some variable. The JIT / GC usually knows when a variable is not used anymore. However, in your case, the stream[j] instances are being kept in the list during the whole process. Maybe you could try changing the structure of your program a bit with the use of IEnumerable and yield return so that the instances are not stuck in a list for too long. Here are the key points:

public IEnumerable<Bitmap> GetSubParts()
{
   ...
   // Assuming there is a loop somewhere:
   for(***)
   { 
      yield return ** Bitmap
   }
}

Same with ImageProcessing:

public IEnumerable<System.IO.Stream> ImageProcessing(System.IO.Stream input)
{
    using(Bitmap inputAsBitmap = new Bitmap(input))
    {
        // --- Applying miscellaneous filters ---

        foreach(var subPart in GetSubParts(inputAsBitmap))
        {
            MemoryStream memoryStream = new MemoryStream();

            using(subPart)
            {
                subPart.Save(memoryStream, ImageFormat.Jpeg);
            }

            yield return memoryStream;
        }
    }
}

And finally:

for(int i = 0; i < filenames.Count; i++)
{
    using (Stream imageStream = File.OpenRead(filenames[i]))
    {
        foreach(var stream = ImageProcessing(imageStream))
        {
            using(stream)
            {        
                 using(var image = System.Drawing.Image.FromStream(stream))
                     image.Save();
            }
        }
    }
}
Kzryzstof
  • 7,688
  • 10
  • 61
  • 108