Исключение памяти в цикле обработки изображений (требуется лучшее решение, чем GC.collect)

Я делаю небольшое приложение, которое показывает прямую трансляцию с веб-камеры в форме окна, а также хранит изображения с водяными знаками для движения с заданным интервалом (конечной целью является создание замедленного видео).

Я использую библиотеку AForge для обработки изображений и видео.

У меня проблемы с утечкой памяти, хотя я стараюсь использовать операторы using в каждом месте, где происходит обработка изображений.

Ниже приведен код, в котором происходит обработка изображения (событие NewFrame).

    private void Video_NewFrame(object sender, NewFrameEventArgs eventArgs)
    {
        try
        {
            if (ImageProcessing) // If the previous frame is not done processing, let this one go
                return;
            else
                ImageProcessing = true;

            using (Bitmap frame = (Bitmap)eventArgs.Frame)
            {
                // Update the GUI picturebox to show live webcam feed
                Invoke((Action)(() =>
                {
                    webcam_PictureBox.Image = (Bitmap)frame.Clone();
                }));

                // During tests, store images to drive at a certain interval
                if (ImageStoreTimer.Elapsed.TotalSeconds > ImageStoreTime)
                {
                    DateTime dt = DateTime.Now;

                    using (Graphics graphics = Graphics.FromImage(frame))
                    {
                        PointF firstLocation = new PointF(frame.Width / 2, frame.Height / 72);
                        PointF secondLocation = new PointF(frame.Width / 2, frame.Height / 15);

                        StringFormat drawFormat = new StringFormat();
                        drawFormat.Alignment = StringAlignment.Center;

                        using (Font arialFont = new Font("Arial", 15))
                        {
                            graphics.DrawString(dt.ToString(), arialFont, Brushes.Red, firstLocation, drawFormat);
                            graphics.DrawString(Pressure.ToString("F0") + " mbar", arialFont, Brushes.Red, secondLocation, drawFormat);
                        }
                    }

                    // Place images in a folder with the same name as the test
                    string filePath = Application.StartupPath + "\\" + TestName + "\\";
                    // Name images by number 1....N
                    string fileName = (Directory.GetFiles(filePath).Length + 1).ToString() + ".jpeg";

                    frame.Save(filePath + fileName, ImageFormat.Jpeg);
                    ImageStoreTimer.Restart();
                }
            }
            //GC.Collect(); <----- I dont want this
        }
        catch
        {
            if (ProgramClosing == true){}
                // Empty catch for exceptions caused by the program being closed incorrectly
            else
                throw;
        }
        finally
        {
            ImageProcessing = false;
        }
    }       

Теперь, когда я запускаю программу, я вижу, что использование памяти увеличивается и уменьшается, обычно до сброса доходит до 900 МБ. Но иногда он увеличивается до 2 ГБ. Иногда я даже получаю исключение нехватки памяти в этой строке:

Graphics graphics = Graphics.FromImage(frame)

Итак, потратив час или около того на попытки изменить код и поиск утечки памяти, я наконец попробовал строку GC.Collect, которая закомментирована в коде (позор). После этого объем моей памяти остается постоянным и составляет менее 60 МБ. И я могу без проблем запускать программу в течение 24 часов.

Итак, я немного прочитал о GC.Collect и о том, насколько это плохо, например, что может потребоваться много вычислительной мощности, чтобы часто делать это в программе. Но когда я сравниваю мощность процессора, используемую моей программой, она на самом деле не меняется, независимо от того, закомментировал ли я строку или оставил ее. Но проблема с памятью исчезнет, ​​если я собираю в конце нового события кадра.

Я хотел бы найти решение моей проблемы, которое не связано с функцией GC.collect, поскольку я знаю, что это плохая практика программирования, и вместо этого я должен найти основной источник проблемы.

Спасибо всем заранее!


person A.Force    schedule 27.04.2018    source источник
comment
да GC - это плохо. Всегда лучше повторно использовать объект вместо создания новых, которые помогли мне много с проблемами памяти и лучше для производительности.   -  person GuidoG    schedule 27.04.2018
comment
вы пробовали закомментировать блок изображения магазина? Только для того, чтобы убедиться, что проблема не в этом.   -  person casiosmu    schedule 27.04.2018
comment
Если вы работаете с большим количеством мусора, то вызов сборки мусора, когда вы знаете о природе того, что может быть в поколениях 1 и 2, может быть полезным, а в некоторых случаях и необходимым. Да, сборщик мусора может снизить производительность, поэтому вам нужно понимать это, а также не пытаться исправить плохой дизайн неправильным инструментом. В целом 98 из 100 вопросов, относящихся к GC.collect, обычно являются результатом утечки памяти. хотя просто сказать, что этих звонков следует избегать любой ценой и что они плохи, слишком упрощает ситуацию.   -  person TheGeneral    schedule 27.04.2018


Ответы (1)


Я плохо разбираюсь в формах выигрыша, но думаю, что эта строчка:

webcam_PictureBox.Image = (Bitmap)frame.Clone();

Оставит предыдущее изображение нерасположенным, что приведет к утечке памяти (неуправляемая память, удерживаемая растровым изображением). Поскольку Bitmap имеет финализатор - он будет восстановлен GC в будущем (или когда вы вызовете GC.Collect), но, как вы уже понимаете, полагаться на GC в таком случае - не лучшая практика. Поэтому попробуйте сделать это так:

if (webcam_PictureBox.Image != null)
    webcam_PictureBox.Image.Dispose();
webcam_PictureBox.Image = (Bitmap)frame.Clone();

Разумный комментарий Ларса: возможно, лучше не удалять изображение, пока оно еще назначено для PictureBox.Image, потому что кто знает, возможно, PictureBox control что-нибудь делает со старым изображением, когда вы назначаете новое. Итак, альтернатива:

var oldImage = webcam_PictureBox.Image;
webcam_PictureBox.Image = (Bitmap)frame.Clone();
if (oldImage != null)
    oldImage.Dispose();
person Evk    schedule 27.04.2018
comment
Я также установил значение null в этих случаях, а не просто для утилизации, это излишнее? - person GuidoG; 27.04.2018
comment
Придирчиво, но, вероятно, было бы лучше вытащить изображение из коробки с картинками, прежде чем выбросить его. Я не вижу ничего в источнике ссылок, который обращается к изображению перед установкой нового, но (на мой взгляд) не рекомендуется избавляться от объекта, которым вы уже поделились с другим объектом. Так что я бы сделал что-нибудь вроде Image oldImage = webcam_PictureBox.Image; webcam_PictureBox.Image = (Bitmap)frame.Clone(); oldImage?.Dispose(); - person Lasse V. Karlsen; 27.04.2018
comment
@ LasseVågsætherKarlsen: Да, это действительно разумный поступок. - person Evk; 27.04.2018
comment
@GuidoG, но вы устанавливаете его на другое изображение в следующей строке, поэтому установка значения null не дает ничего полезного. - person Evk; 27.04.2018
comment
Верно, но в вашем новом коде oldImage не устанавливается на другое изображение сразу после удаления. Значит, было бы сложно установить здесь oldImage равным null? Просто пытаюсь здесь чему-то научиться - person GuidoG; 27.04.2018
comment
Сборщик мусора @GuidoG (без подключенного отладчика) без проблем собирает oldImage сразу после последнего использования (то есть после строки oldImage.Dispose()). Я не имею в виду, что он будет собираться сразу после последнего использования, но он может, и ему не нужно, например, ждать, пока завершится метод включения. По этой причине установка для локальной переменной значения null ничего не помогает, поскольку это сделано с предположением, что это поможет GC собрать ее раньше, но это не так. - person Evk; 27.04.2018
comment
@GuidoG - предполагая, что код выпуска, а не отладка, GC и JIT не нуждаются в большой помощи. Если oldImage не считывается снова после Dispose, эта переменная не сохраняет ничего живого во время сборки мусора. - person Damien_The_Unbeliever; 27.04.2018
comment
Похоже, в этом и была проблема! Я отредактировал код в соответствии с обновленным ответом Evk, и теперь мое использование памяти постоянно составляет 60 МБ, без сборки мусора! Спасибо вам всем. - person A.Force; 27.04.2018