JavaRush /Java блог /Архив info.javarush /World of Bytes 1. Работа с изображениями.
Joysi
41 уровень

World of Bytes 1. Работа с изображениями.

Статья из группы Архив info.javarush
Special for До этого я пояснял на сухих примерах. Попросили работу с изображениями - ловите.
Постановка учебной задачи.
Дан графический файл (jpeg, png ...). Необходимо сделать с ним некие манипуляции и записать результат в другой файл. Для упрощения рассмотрим три задачи: - получить негатив изображения - получить черно-белый вариант изображения (сбросить цветность) - изменить насыщенность зеленого цвета в изображении. Заметим, что аналогичным способом мы можем, добавляя новые методы реализовывать и другие задачи: - увеличить резкость или размытие - изменить размеры - повернуть по/против часовой. - и другие возможность Фотошопа :) вообщем реализовать любой алгоритм над изображением, насколько у нас хватит фантазии и знания матана (например, распознать количество возможных котиков на картинке).
Немного сухой теории.
Мы рассматриваем растровые изображения (есть еще векторные и другие). То есть, когда файл, помимо собственно заголовка со служебной информацией, хранит прямоугольную матрицу точек. Аналогично экрану современного HD-телевизора у которого разрешение 1920х1080 точек и каждая точка представлена как значения трех составляющих цвета: R(ed), G(reen), B(lue) = Красный, Зеленый и Синий. Эти цвета независимы и данная модель взята из биологии восприятия цвета. В глазу у нас есть колбочки и палочки. Колбочки трех разновидностей (реагируют на соответствующий одной из трех диапазонов длин волн), палочки "обрабатывают" яркость цвета (амплитуду световой волны). В модели RGB палочки отвечают за значение составляющей (0 - отсутствие, 255- самый яркий свет), а колбочки - соответственно в какие из R / G / B помещать соответствующую интенсивность. Например: Отсутствие света - палочки /колбочки не реагируют и RGB=(0,0,0). Яркий белый свет - все колбочки реагируют равномерно, палочки фигеют и RGB = (255,255,255). Серая мышка пробежала - все колбочки реагируют равномерно, палочки реагируют средне и RGB = (127,127,127). Темно-оранжевый - реагируют R и G палочки, палочки едва откликаются, RGB=(30, 30, 0) ...
Приступим к практике.
Я писал для примера работы с байтами, поэтому код не вылизан по всем правилам и далеко не оптимален: мы не проверяем входные параметры, не делаем полноценную проверку ошибок и т.п. Писалось в лоб, без рефакторинга. Основной упор - работа с байтами-битами. Напишем, по аналогии задач JavaRush консольную утилиту, которая при вызове в командной строке с соответствующими аргументами модифицирует изображение. Исходное изображение: Котенок
вызов с параметрами -n kitten.jpg newkitten.jpg создаст картинку: негатив
вызов с параметрами -b kitten.jpg newkitten.jpg создаст картинку: черно-белый котенок
вызов с параметрами -gr kitten.jpg newkitten.jpg создаст картинку: сумерки
Собственно котд.
package com.joysi.byteworld; import com.sun.imageio.plugins.jpeg.*; import com.sun.imageio.plugins.png.*; import javax.imageio.*; import javax.imageio.stream.*; import java.awt.image.BufferedImage; import java.io.*; public class image { public static void main(String[] args) throws IOException { CoolImage picture = new CoolImage(args[1]); // загружаем файл изображения if ("-n".equals(args[0])) picture.convertToNegative(); if ("-g".equals(args[0])) picture.addColorGreenChannel(-100); if ("-bw".equals(args[0])) picture.convertToBlackAndWhite(); picture.saveAsJpeg(args[2]); } public static class CoolImage{ private int height; // высота изображения private int width; // ширина изображения private int[] pixels; // собственно массив цветов точек составляющих изображение public int getPixel(int x, int y) { return pixels[y*width+x]; } // получить пиксель в формате RGB public int getRed(int color) { return color >> 16; } // получить красную составляющую цвета public int getGreen(int color) { return (color >> 8) & 0xFF; } // получить зеленую составляющую цвета public int getBlue(int color) { return color & 0xFF;} // получить синюю составляющую цвета // Конструктор - создание изображения из файла public CoolImage(String fileName) throws IOException { BufferedImage img = readFromFile(fileName); this.height = img.getHeight(); this.width = img.getWidth(); this.pixels = copyFromBufferedImage(img); } // Чтение изображения из файла в BufferedImage private BufferedImage readFromFile(String fileName) throws IOException { ImageReader r = new JPEGImageReader(new JPEGImageReaderSpi()); r.setInput(new FileImageInputStream(new File(fileName))); BufferedImage bi = r.read(0, new ImageReadParam()); ((FileImageInputStream) r.getInput()).close(); return bi; } // Формирование BufferedImage из массива pixels private BufferedImage copyToBufferedImage() { BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); for (int i = 0; i < height; i++) for (int j = 0; j < width; j++) bi.setRGB(j, i, pixels[i*width +j]); return bi; } // Формирование массива пикселей из BufferedImage private int[] copyFromBufferedImage(BufferedImage bi) { int[] pict = new int[height*width]; for (int i = 0; i < height; i++) for (int j = 0; j < width; j++) pict[i*width + j] = bi.getRGB(j, i) & 0xFFFFFF; // 0xFFFFFF: записываем только 3 младших байта RGB return pict; } // Запись изображения в jpeg-формате public void saveAsJpeg(String fileName) throws IOException { ImageWriter writer = new JPEGImageWriter(new JPEGImageWriterSpi()); saveToImageFile(writer, fileName); } // Запись изображения в png-формате (другие графические форматы по аналогии) public void saveAsPng(String fileName) throws IOException { ImageWriter writer = new PNGImageWriter(new PNGImageWriterSpi()); saveToImageFile(writer, fileName); } // Собственно запись файла (общая для всех форматов часть). private void saveToImageFile(ImageWriter iw, String fileName) throws IOException { iw.setOutput(new FileImageOutputStream(new File(fileName))); iw.write(copyToBufferedImage()); ((FileImageOutputStream) iw.getOutput()).close(); } // конвертация изображения в негатив public void convertToNegative() { for (int i = 0; i < height; i++) for (int j = 0; j < width; j++) // Применяем логическое отрицание и отбрасываем старший байт pixels[i*width + j] = ~pixels[i*width + j] & 0xFFFFFF; } // конвертация изображения в черно-белый вид public void convertToBlackAndWhite() { for (int i = 0; i < height; i++) for (int j = 0; j < width; j++) { // находим среднюю арифметическую интенсивность пикселя по всем цветам int intens = (getRed(pixels[i * width + j]) + getGreen(pixels[i * width + j]) + getBlue(pixels[i * width + j])) / 3; // ... и записываем ее в каждый цвет за раз , сдвигая байты RGB на свои места pixels[i * width + j] = intens + (intens << 8) + (intens << 16); } } // изменяем интесивность зеленого цвета public void addColorGreenChannel(int delta) { for (int i = 0; i < height; i++) for (int j = 0; j < width; j++) { int newGreen = getGreen(pixels[i * width + j]) + delta; if (newGreen > 255) newGreen=255; // Отсекаем при превышении границ байта if (newGreen < 0) newGreen=0; // В итоговом пикселе R и B цвета оставляем без изменений: & 0xFF00FF // Полученный новый G (зеленый) засунем в "серединку" RGB: | (newGreen << 8) pixels[i * width + j] = pixels[i * width + j] & 0xFF00FF | (newGreen << 8); } } } }
Комментарии (2)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Elizabeth Ray Уровень 38
23 октября 2018
Полезная статья, всегда было интересно, как устроен фотошоп изнутри)
Joysi Уровень 41
20 февраля 2016
Увеличение резкости
Было

Стало

Добавленный метод (жутко и ужасно неоптимальный, вычисления — все в лоб):
// увеличиваем резкость
public void addSharpen() {
    // Чтобы работать с неизмененными данными скопируем в новый массив
    int[] arrnew= Arrays.copyOf(pixels, width*height);

    for (int j = 1; j < height-1; j++)
        for (int i = 1; i < width-1; i++) {
            // покомпонентно применяем фильтр усиления резкости
            //  -0.1 -0.1 -0.1
            //  -0.1  1.8 -0.1
            //  -0.1 -0.1 -0.1
            int newRed=getRed(getPixel(i,j))*18/10 -
               (getRed(getPixel(i-1,j-1)) + getRed(getPixel(i-1,j)) + getRed(getPixel(i-1,j+1)) +
                 getRed(getPixel(i,j-1))   + getRed(getPixel(i,j+1)) +
                 getRed(getPixel(i+1,j-1)) + getRed(getPixel(i+1,j)) + getRed(getPixel(i+1,j+1)))/10;
            if (newRed > 255) newRed=255;  // Отсекаем при превышении границ байта
            if (newRed < 0)   newRed=0;
            
            int newGreen=getGreen(getPixel(i,j))*18/10 -
               (getGreen(getPixel(i-1,j-1)) + getGreen(getPixel(i-1,j)) + getGreen(getPixel(i-1,j+1)) +
                 getGreen(getPixel(i,j-1))   + getGreen(getPixel(i,j+1)) +
                 getGreen(getPixel(i+1,j-1)) + getGreen(getPixel(i+1,j)) + getGreen(getPixel(i+1,j+1)))/10;
            if (newGreen > 255) newGreen=255;  // Отсекаем при превышении границ байта
            if (newGreen < 0)   newGreen=0;

            int newBlue=getBlue(getPixel(i,j))*18/10 -
                (getBlue(getPixel(i-1,j-1)) + getBlue(getPixel(i-1,j)) + getBlue(getPixel(i