Шумозаглушення шляхом об'єднання зображень на Java

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

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

Через кілька років, коли вже почав займатися програмуванням, знову звернув увагу на те, що шум на зображеннях не є статичним. В голові виникла ідея: а що якщо взяти, зняти кілька абсолютно однакових зображень, а потім якимось чином об'єднати їх, усунувши різниця між знімками, тобто шум?

Отже, нижче представлені 4 зображення, які демонструють якісь фотографії одного і того ж об'єкта, з випадковим шумом на кожному знімку. В якості об'єкта представлені червоні кола, в якості шуму — білі.

приклад знімків

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

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

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

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

Через деякий час я знайшов сайт відомого в колах панорамних фотографів німецького математика Хельмута Дерша. На даний момент практично все для склейки панорам базується на алгоритмах. Крім ПЗ для обробки панорам на його сайті я натрапив на програму, знімає шум з зображень — PTAverage. Програма була неймовірно простий — просто перетягнеш фотографії на ярлик — і отримуєш результат. Як раз те, що я й шукав. Однак трохи погравшись із PTAverage, я зрозумів, що це зовсім не те, що хотілося б.

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

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

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

Саму програму писав на java, оскільки вивчав її до того часу вже близько року. Єдиною заковикою була завантаження зображень у форматі tiff, але пізніше я розібрався з бібліотекою JAI. Недоліком програми є велике споживання пам'яті — JAI не вміє (а може я просто не знайшов) читати зображення попіксельно, не завантажуючи всі зображення в пам'ять.

Код програмиЩоб код був зрозуміліше, прибрав все перевірки (такі як дозвіл зображень, біт на канал і т.д.):

public class Denoise {

/**
* @param inputFiles масив з файлами для обробки
* @param пропустити, результати файл, в який збережеться результат обробки
* @param difference максимальна різниця між пікселями (0-255)
* @throws IOException 
*/
Denoise(File[] inputFiles, File пропустити, результати, 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", пропустити, результати);
}

/**
* 
* @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+ кадрів).

порівняння шуму

Звичайно, описаний мною алгоритм неймовірно простий, і мало застосуємо в реальній практиці, але, я сподіваюся, можливо хтось буде користуватися ним, або почерпне з статті щось корисне.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.