Шумоподавление путем объединения изображений на Java

в 11:43, , рубрики: java, обработка изображений, шумоподавление

Здраствуй! Хочу поделиться кодом простой программы, которую я использую для уменьшения шума с цифровых фотограффий.

Примерно восемь лет назад, рассматривая фотографии, снятые на свой первый цифровой фотоаппарат, я обнаружил, что некоторые снимки с тусклым освещением имеют какую-то странную мутность, цветные пятна, не резкость. В то время я еще не знал, что такое шум, как он зависит от параметра ISO и был очень разочарован, что фотоаппарат такой «некачественный». Однако, я обратил внимание, что на одинаковых снимках эти цветные пятна выглядят несколько по разному, меняются от кадра к кадру. Время шло, я научился снимать на ручных настройках, узнал, что такое шум, как правильно выставить светочуствительность и т.д.

Спустя несколько лет, когда уже начал заниматься программированием, снова обратил внимание на то, что шум на изображениях не является статичным. В голове возникла идея: а что если взять, снять несколько абсолютно одинаковых изображений, а потом неким образом объединить их, устранив разность между снимками, т.е. шум?

Итак, ниже представлены 4 изображения, демонстрирующие некие фотографии одного и того-же обьекта, со случайным шумом на каждом снимке. В качестве объекта представлены красные круги, в качестве шума — белые.

пример снимков

Первым делом, я решил попробовать банальным способом совместить эти изображения в фотошопе, установив каждому из изображений прозрачность = 50%. Конечно, ничего хорошего из этого не вышло.

результат обработки в adobe photoshop

Такой результат вполне логичен — пиксели не усредняются, а просто прибавляются один к другому, а так же каждый последующий слой имеет больший «вес» над нижестоящими.

Немного поискав в интернете, обнаружил, что программы такого типа уже существуют и активно используются в среде астрофотографов. Выдержки, на которых снимают звезды, огромны (и соответственно шум), а сам объект съемки статичен (при съемке с гидированием), что позволяет устранять шум путем объединения идентичных снимков. Однако осталось одно но: все найденные мной программы подобного типа являлись очень сложными для использования, заточенными именно для астрофото и практически все стоили больших денег. Такой функционал мне был не нужен, и я продолжил поиск.

Спустя некоторое время я нашел сайт известного в кругах панорамных фотографов немецкого математика Хельмута Дерша. На данный момент практически все ПО для склейки панорамных изображений основывается на его алгоритмах. Помимо ПО для обработки панорам на его сайте я наткнулся на программу, устраняющую шум с изображений — PTAverage. Программа была невероятно простой — просто перетягиваешь фотографии на ярлык — и получаешь результат. Как раз то, что я и искал. Однако немного поигравшись с PTAverage, я понял, что это совсем не то, что хотелось бы.

Результат обработки изображений программой PTAverage:

результат обработки в PTAverage

Как видно, программа работает простейшим образом: получает цвет пикселя каждого изображения, складывает их между собой, а затем делит на общее количество всех изображений. Однако мне хотелось некой селективности: например, если на двух изображениях пиксель черного цвета, а на третьем изображении он белый — логично предположить, что на третьем изображении пиксель является шумом. В конечном итоге ничего подходящего я так и не нашел, и в итоге решил написать такое ПО самому, благо что все выглядело весьма легко.

Саму программу писал на java, т.к. изучал ее к тому времени уже около года. Единственной загвоздкой была загрузка изображений в формате tiff, но позже я разобрался с библиотекой JAI. Недостатком программы является огромное потребление памяти — JAI не умеет (а может я просто не нашел) читать изображение попиксельно, не загружая всё изображение в память.

Код программы

Чтобы код был понятнее, убрал все проверки (такие как разрешение изображений, бит на канал и т.д.):

public class Denoise {
    
    /**
     * @param inputFiles массив с файлами для обработки
     * @param outputFile файл, в который сохранится результат обработки
     * @param difference максимальная разница между пикселями (0-255)
     * @throws IOException 
     */
    Denoise(File[] inputFiles, File outputFile, int difference) throws IOException {
        
        //Создаем массив для данных изображений
        Raster[] rasters = new Raster[inputFiles.length];
        
        //В цикле читаем каждое изображение
        for(int i = 0; i<inputFiles.length; i++) {
            try (ImageInputStream is = ImageIO.createImageInputStream(inputFiles[i])) {
                Iterator<ImageReader> imageReaders = ImageIO.getImageReaders(is) ;
                ImageReader imageReader = imageReaders.next();
                imageReader.setInput(is);
                if(imageReader.canReadRaster()) {
                    rasters[i] = imageReader.readRaster(0, null);
                }
                else {
                    rasters[i] =  imageReader.readAsRenderedImage(0, null).getData();
                }
            }
        }
        
        //Получаем ширину и высоту первого изображения, считая что размеры всех изображений равны
        int width = rasters[0].getWidth();
        int height = rasters[0].getHeight();
        
        //Создаем растр для записи результирующего изображения, используя характеристики первого изображения
        WritableRaster outputRaster = rasters[0].createCompatibleWritableRaster();
        
        //В цикле обходим каждый пиксель каждого изображения, усредняя значения по каждому каналу
        for(int x = 0; x<width; x++){
            for(int y = 0; y<height; y++){
                //Массив, со значениями цветов пикселя
                int[] color = new int[3];
                
                for(int band = 0; band<3; band++){
                    //Массив, со значениями канала определенного пикселя
                    int data[] = new int[rasters.length];
                    
                    for (int imageNum = 0; imageNum<rasters.length; imageNum++) {
                        data[imageNum] = rasters[imageNum].getSample(x, y, band);
                    }
                    
                    //Получаем усредненное значение канала
                    color[band]  = average(data, difference); 
                }
                
                //Устанавливаем цвет пикселю результирующего изображения
                outputRaster.setPixel(x, y, color);
            }
        }
        
        //Сохраняем изображение
        BufferedImage output = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        output.setData(outputRaster);
        ImageIO.write(output, "tiff", outputFile);
    }
    
    /**
     * 
     * @param data массив с данными пикселя всех изображений для отдельного канала 
     * @param difference максимальная разница между пикселем
     * @return усредненное значение канала
     */
    private int average(int[] data, int difference){
        /**Количество изображений*/
        int imagesCount = data.length;
        /**Медианное значение цвета пикселей*/
        int median;
        
        //Сортируем массив, чтобы цвет пикселя выстроился в порядке возрастания
        Arrays.sort(data);
        
        //Если количество изображений является четным, используем для получения медианного значения 
        //среднее арифметическое значение двух центральных пикселей
        if(imagesCount % 2 == 0) { 
            median = (data[imagesCount / 2 - 1] + data[imagesCount / 2]) / 2;
        }
        else {
            median = data[(int)Math.floor(imagesCount / 2)];
        }
        
        //Максимальное и минимальное отклонение цвета пикселя от медианного значения
        int min = median - difference;
        int max = median + difference;

        //сумма значений канала всех изображений
        int sumBands = 0;
        //Общее количество изображений, не выходящих за рамки min и max
        int counter = 0;

        //В цикле рассчитываем сумму значений канала всех изображений
        for(int i = 0; i<imagesCount; i++){
            //Если значение не превышает указанные пороги - добавляем его к общему значению
            if(data[i]>=min && data[i]<= max){
                sumBands = sumBands+data[i];
                counter++;
            }
        }
        
        //Если отклонение от медианного значения пикселя не превышает только одно (или ни одно)
        //из изображений - просто усредняем все полученные значения,
        //в противном случае - усредняем только те, которые вошли в указанные рамки
        if(counter <= 1){
            sumBands = 0;
            for(int i = 0; i<imagesCount; i++){
                sumBands = sumBands + data[i];
            }
            sumBands = sumBands/imagesCount;
        }
        else {
            sumBands = sumBands / counter;
        }
        
        return sumBands;
    }
    
}

Закидываем четыре оригинальных кадра в программу и получаем изображение без шума.

результат обработки

Практическое применение алгоритму найти сложно — объект для съемки должен быть статичен, освещение объекта не должно изменяться, ну и ничего не получится без очень устойчивого штатива.

Кстати, у программы есть некий «побочный» эффект — удаляется не только шум, а любой не статичный объект. Например, сделав большое количество кадров с оживленной площади, теоретически можно «удалить» всех людей. Ниже небольшой пример.

Снимки до обработки; как видно по снимкам, таинственным образом перемещается банан.

Шумоподавление путем объединения изображений на Java

А вот результат обработки — как видно, банан пропал не полностью. Однако, сделав большее количество кадров, при условии, что банан продолжил бы двигаться, можно было бы полностью от него избавиться.

результат обработки

А что насчет шума? Тут тоже все отлично, хватило всего трех кадров, чтобы значительно его уменьшить (астрофотографы, к примеру, используют, насколько я знаю, 15+ кадров).

сравнение шума

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

Автор: MightyRavendark

Источник

* - обязательные к заполнению поля