Здраствуй! Хочу поделиться кодом простой программы, которую я использую для уменьшения шума с цифровых фотограффий.
Примерно восемь лет назад, рассматривая фотографии, снятые на свой первый цифровой фотоаппарат, я обнаружил, что некоторые снимки с тусклым освещением имеют какую-то странную мутность, цветные пятна, не резкость. В то время я еще не знал, что такое шум, как он зависит от параметра ISO и был очень разочарован, что фотоаппарат такой «некачественный». Однако, я обратил внимание, что на одинаковых снимках эти цветные пятна выглядят несколько по разному, меняются от кадра к кадру. Время шло, я научился снимать на ручных настройках, узнал, что такое шум, как правильно выставить светочуствительность и т.д.
Спустя несколько лет, когда уже начал заниматься программированием, снова обратил внимание на то, что шум на изображениях не является статичным. В голове возникла идея: а что если взять, снять несколько абсолютно одинаковых изображений, а потом неким образом объединить их, устранив разность между снимками, т.е. шум?
Итак, ниже представлены 4 изображения, демонстрирующие некие фотографии одного и того-же обьекта, со случайным шумом на каждом снимке. В качестве объекта представлены красные круги, в качестве шума — белые.
Первым делом, я решил попробовать банальным способом совместить эти изображения в фотошопе, установив каждому из изображений прозрачность = 50%. Конечно, ничего хорошего из этого не вышло.
Такой результат вполне логичен — пиксели не усредняются, а просто прибавляются один к другому, а так же каждый последующий слой имеет больший «вес» над нижестоящими.
Немного поискав в интернете, обнаружил, что программы такого типа уже существуют и активно используются в среде астрофотографов. Выдержки, на которых снимают звезды, огромны (и соответственно шум), а сам объект съемки статичен (при съемке с гидированием), что позволяет устранять шум путем объединения идентичных снимков. Однако осталось одно но: все найденные мной программы подобного типа являлись очень сложными для использования, заточенными именно для астрофото и практически все стоили больших денег. Такой функционал мне был не нужен, и я продолжил поиск.
Спустя некоторое время я нашел сайт известного в кругах панорамных фотографов немецкого математика Хельмута Дерша. На данный момент практически все ПО для склейки панорамных изображений основывается на его алгоритмах. Помимо ПО для обработки панорам на его сайте я наткнулся на программу, устраняющую шум с изображений — 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;
}
}
Закидываем четыре оригинальных кадра в программу и получаем изображение без шума.
Практическое применение алгоритму найти сложно — объект для съемки должен быть статичен, освещение объекта не должно изменяться, ну и ничего не получится без очень устойчивого штатива.
Кстати, у программы есть некий «побочный» эффект — удаляется не только шум, а любой не статичный объект. Например, сделав большое количество кадров с оживленной площади, теоретически можно «удалить» всех людей. Ниже небольшой пример.
Снимки до обработки; как видно по снимкам, таинственным образом перемещается банан.
А вот результат обработки — как видно, банан пропал не полностью. Однако, сделав большее количество кадров, при условии, что банан продолжил бы двигаться, можно было бы полностью от него избавиться.
А что насчет шума? Тут тоже все отлично, хватило всего трех кадров, чтобы значительно его уменьшить (астрофотографы, к примеру, используют, насколько я знаю, 15+ кадров).
Конечно, описанный мною алгоритм невероятно прост, и мало применим в реальной практике, но, я надеюсь, возможно кто-то будет пользоваться им, или почерпнет из статьи что-то полезное.
Автор: MightyRavendark