Juravskiy Vitaliy`s blog

Ноябрь 19, 2010

 

Чтение EXIF данных из JPEG файла

По просьбе читателей глянул в сторону чтения мета данных (в формате EXIF) из файлов изображений, в частности из JPEG изображений. Когда цифровой формат производит снимок, он заносит в мета данные информацию о фото, например могут быть такие параметры как производитель фотокамеры, модель, информация о вспышке и другие интересные данные. Например в формате TIFF, который широко используется в геоинформационной сфере, хранят географические данные об объектах. В процессе поиска путей решения, выяснилось, что средствами стандартного JDK проблему задачу решить можно, но писать кучу кода реализующего чтение (запись) данных в файл изображения по стандарту EXIF не хотелось. Я пошел по пути наименьшего сопротивления.

В начале думалось, что можно все решить с помощью пакета javax.imageio.* в частности у класса javax.imageio.ImageReader есть метод getImageMetadata который подкупал своим названием, казалось пробелам решена.

Приведу пример кода, который я написал для работы с IIOMetadata. Может кому то пригодиться.

public static void displayMetadata(IIOMetadata metadata) { String[] names = metadata.getMetadataFormatNames(); for (int i = 0; i < names.length; ++i) { System.out.println(); System.out.println("METADATA FOR FORMAT: " + names[i]); System.out.println(displayTree(metadata.getAsTree(names[i]), 0, null).toString()); } } public static StringBuffer displayTree(Node node, int indent, StringBuffer b) { if(b == null) b = new StringBuffer(); indent(indent, b); String name = node.getNodeName(); b.append("<").append(name); if (node.hasAttributes()) { NamedNodeMap attrs = node.getAttributes(); for (int i = 0, ub = attrs.getLength(); i < ub; ++i) { Node attr = attrs.item(i); b.append(" ") .append(attr.getNodeName()) .append("=") .append(attr.getNodeValue()) .append(""); } } if (node.hasChildNodes()) { b.append(">\n"); NodeList children = node.getChildNodes(); for (int i = 0, ub = children.getLength(); i < ub; ++i) displayTree(children.item(i), indent + 4, b); indent(indent, b); b.append("</") .append(name) .append(">\n"); } else b.append("/>\n"); return b; } static void indent(int indent, StringBuffer b) { for (int i = 0; i < indent; ++i) b.append(" "); } public static void main(String[] args) throws IOException { ImageReader r = getImageReaderByFileExtension("c:/test_image/nikon.jpg"); r.setInput(new FileImageInputStream(new File("c:/test_image/nikon.jpg"))); IIOMetadata meta = r.getImageMetadata(0); displayMetadata(meta); System.out.println(displayTree(meta.getAsTree(meta.getNativeMetadataFormatName()), 0, null)); }

Результат выполнения программы следующий:

METADATA FOR FORMAT: javax_imageio_jpeg_image_1.0

<javax_imageio_jpeg_image_1.0>

    <JPEGvariety>

        <app0JFIF majorVersion=1 minorVersion=2 resUnits=1 Xdensity=72 Ydensity=72 thumbWidth=0 thumbHeight=0/>

    </JPEGvariety>

    <markerSequence>

        <unknown MarkerTag=225/>

        <unknown MarkerTag=237/>

        <app14Adobe version=100 flags0=0 flags1=0 transform=1/>

        <dqt>

            <dqtable elementPrecision=0 qtableId=0/>

            <dqtable elementPrecision=0 qtableId=1/>

        </dqt>

        <sof process=0 samplePrecision=8 numLines=600 samplesPerLine=800 numFrameComponents=3>

            <componentSpec componentId=1 HsamplingFactor=1 VsamplingFactor=1 QtableSelector=0/>

            <componentSpec componentId=2 HsamplingFactor=1 VsamplingFactor=1 QtableSelector=1/>

            <componentSpec componentId=3 HsamplingFactor=1 VsamplingFactor=1 QtableSelector=1/>

        </sof>

        <dri interval=100/>

        <dht>

            <dhtable class=0 htableId=0/>

            <dhtable class=0 htableId=1/>

            <dhtable class=1 htableId=0/>

            <dhtable class=1 htableId=1/>

        </dht>

        <sos numScanComponents=3 startSpectralSelection=0 endSpectralSelection=63 approxHigh=0 approxLow=0>

            <scanComponentSpec componentSelector=1 dcHuffTable=0 acHuffTable=0/>

            <scanComponentSpec componentSelector=2 dcHuffTable=1 acHuffTable=1/>

            <scanComponentSpec componentSelector=3 dcHuffTable=1 acHuffTable=1/>

        </sos>

    </markerSequence>

</javax_imageio_jpeg_image_1.0>

METADATA FOR FORMAT: javax_imageio_1.0

<javax_imageio_1.0>

    <Chroma>

        <ColorSpaceType name=YCbCr/>

        <NumChannels value=3/>

    </Chroma>

    <Compression>

        <CompressionTypeName value=JPEG/>

        <Lossless value=false/>

        <NumProgressiveScans value=1/>

    </Compression>

    <Dimension>

        <PixelAspectRatio value=1.0/>

        <ImageOrientation value=normal/>

        <HorizontalPixelSize value=0.35277778/>

        <VerticalPixelSize value=0.35277778/>

    </Dimension>

</javax_imageio_1.0>

<javax_imageio_jpeg_image_1.0>

    <JPEGvariety>

        <app0JFIF majorVersion=1 minorVersion=2 resUnits=1 Xdensity=72 Ydensity=72 thumbWidth=0 thumbHeight=0/>

    </JPEGvariety>

    <markerSequence>

        <unknown MarkerTag=225/>

        <unknown MarkerTag=237/>

        <app14Adobe version=100 flags0=0 flags1=0 transform=1/>

        <dqt>

            <dqtable elementPrecision=0 qtableId=0/>

            <dqtable elementPrecision=0 qtableId=1/>

        </dqt>

        <sof process=0 samplePrecision=8 numLines=600 samplesPerLine=800 numFrameComponents=3>

            <componentSpec componentId=1 HsamplingFactor=1 VsamplingFactor=1 QtableSelector=0/>

            <componentSpec componentId=2 HsamplingFactor=1 VsamplingFactor=1 QtableSelector=1/>

            <componentSpec componentId=3 HsamplingFactor=1 VsamplingFactor=1 QtableSelector=1/>

        </sof>

        <dri interval=100/>

        <dht>

            <dhtable class=0 htableId=0/>

            <dhtable class=0 htableId=1/>

            <dhtable class=1 htableId=0/>

            <dhtable class=1 htableId=1/>

        </dht>

        <sos numScanComponents=3 startSpectralSelection=0 endSpectralSelection=63 approxHigh=0 approxLow=0>

            <scanComponentSpec componentSelector=1 dcHuffTable=0 acHuffTable=0/>

            <scanComponentSpec componentSelector=2 dcHuffTable=1 acHuffTable=1/>

            <scanComponentSpec componentSelector=3 dcHuffTable=1 acHuffTable=1/>

        </sos>

    </markerSequence>

</javax_imageio_jpeg_image_1.0>

Вроде это и мета данные но не те, которые нам надо, где информация о фотокамере, и все остальное. Не в даваясь в подробности что мы получили занялся поиском библиотеки или решения стандартными средствами JDK. Нашел такую небольшую библиотеку metadata extraction in java она позволяет извлекать EXIF данные из изображений, и писать при этом мало кода.

Скачиваем последнюю библиотеку из списка релизов и добавляем в проект среды разработки. Примеры использования библиотеки можно глянуть на странице примеров.

У меня получился следующий код:

public static void main(String[] args) throws JpegProcessingException { File jpegFile = new File("c:/test_image/nikon.jpg"); Metadata metadata = new ExifReader(jpegFile).extract(); Iterator<Directory> directories = metadata.getDirectoryIterator(); while (directories.hasNext()) { Directory directory = directories.next(); Iterator<Tag> tags = directory.getTagIterator(); while (tags.hasNext()) { Tag tag = tags.next(); System.out.println(tag); } } }

А вывод EXIF данных следующий:

[Exif] Image Description -          
[Exif] Make - NIKON

[Exif] Model - E950

[Exif] Orientation - Top, left side (Horizontal / normal)

[Exif] X Resolution - 300 dots per inch

[Exif] Y Resolution - 300 dots per inch

[Exif] Resolution Unit - Inch

[Exif] Software - v981-79

[Exif] Date/Time - 2001:04:06 11:51:40

[Exif] YCbCr Positioning - Datum point

[Exif] Exposure Time - 1/77 sec

[Exif] F-Number - F5,5

[Exif] Exposure Program - Program normal

[Exif] ISO Speed Ratings - 80

[Exif] Exif Version - 2.10

[Exif] Date/Time Original - 2001:04:06 11:51:40

[Exif] Date/Time Digitized - 2001:04:06 11:51:40

[Exif] Components Configuration - YCbCr

[Exif] Compressed Bits Per Pixel - 4 bits/pixel

[Exif] Exposure Bias Value - 0 EV

[Exif] Max Aperture Value - F2,5

[Exif] Metering Mode - Multi-segment

[Exif] Light Source - Unknown

[Exif] Flash - Flash did not fire

[Exif] Focal Length - 12,8 mm

[Exif] User Comment -

[Exif] FlashPix Version - 1.00

[Exif] Color Space - sRGB

[Exif] Exif Image Width - 1600 pixels

[Exif] Exif Image Height - 1200 pixels

[Exif] File Source - Digital Still Camera (DSC)

[Exif] Scene Type - Directly photographed image

[Exif] Compression - JPEG (old-style)

[Exif] Thumbnail Offset - 2036 bytes

[Exif] Thumbnail Length - 4662 bytes

[Exif] Thumbnail Data - [4662 bytes of thumbnail data]

[Nikon Makernote] Makernote Unknown 1 - 08.00

[Nikon Makernote] Quality - Unknown (12)

[Nikon Makernote] Color Mode - Color

[Nikon Makernote] Image Adjustment - Contrast +

[Nikon Makernote] CCD Sensitivity - ISO80

[Nikon Makernote] White Balance - Auto

[Nikon Makernote] Focus - 0

[Nikon Makernote] Makernote Unknown 2 -

[Nikon Makernote] Digital Zoom - No digital zoom

[Nikon Makernote] Fisheye Converter - None

[Nikon Makernote] Makernote Unknown 3 - 0 0 16777216 0 -1609193200 0 34833 6931 16178 4372 4372 -972290529 -921882880 15112 0 0 1151495 252903424 17 0 0 844038208 55184128 218129428 1476410198 370540566 -250604010 16711749 204629079 1729

[Interoperability] Interoperability Index - Recommended Exif Interoperability Rules (ExifR98)

[Interoperability] Interoperability Version - 1.00

Информация очень полезна при обработке изображений или например сборе статистических данных на больших архивах изображений. Например сервис Flickr на основе EXIF данных предоставляет статистическую информацию о том какими фотоаппаратами пользователи делают снимки. А все современные интернет сервисы связанные с фотографиями предоставляют просмотр EXIF информации о фото, ее обычно можно найти рядом с просматриваемой фотографией.

Juravskiy Vitaliy`s blog

Ноябрь 17, 2010

 

Работа с изображениями в Java

В предыдущем посте на эту тему я рассказывал о чтении JPEG файлов, сегодня я расскажу не только о чтении но и о записи изображений. А также о других возможностях пакета com.sun.imageio.*;. Приведу примеры чтения и записи изображений, изменения цвета пикселей, и конвертации из одного формата в другой.

Данный пакет предоставляет API для работы с изображениями и их обработкой. 

У нас есть набор классов для чтения изображений:

BMPImageReaderSpi GIFImageReaderSpi JPEGImageReaderSpi PNGImageReaderSpi WBMPImageReaderSpi

которые реализуют один общий интерфейс ImageReaderSpi.

Предлагаю простую функцию для определения реализации по расширению файла:

public static ImageReader getImageReaderByFileExtension(String fileName) { if(fileName.toLowerCase().endsWith(".png")) { return new PNGImageReader(new PNGImageReaderSpi()); } else if(fileName.toLowerCase().endsWith(".gif")) { return new GIFImageReader(new GIFImageReaderSpi()); } else if(fileName.toLowerCase().endsWith(".bmp")) { return new BMPImageReader(new BMPImageReaderSpi()); } else { return new JPEGImageReader(new JPEGImageReaderSpi()); } }

А что делать если расширение не известно, или не верно. Делаем метод более надежным, читая заголовок файла:

public static ImageReader getImageReaderByHeader(File file) throws IOException { byte [] header = new byte[10]; new DataInputStream(new FileInputStream(file)).read(header); String h = new String(header).trim(); if(h.contains("PNG")) { return new PNGImageReader(new PNGImageReaderSpi()); } else if(h.contains("GIF89")) { return new GIFImageReader(new GIFImageReaderSpi()); } else if(h.contains("BM")) { return new BMPImageReader(new BMPImageReaderSpi()); } else if(h.contains("JFIF")){ return new JPEGImageReader(new JPEGImageReaderSpi()); } else { return new WBMPImageReader(new WBMPImageReaderSpi()); } }

Для пяти типов файлов, достаточно прочитать 10 байт чтобы определить тип изображения. Замечу, в функции нет проверки на то, что это не изображение и если в аргумент передать например архив, то она его попытается открыть в формате WBMP.

В прошлом посте я приводил метод который преобразует BufferdImage к Color [][]  что не совсем хорошо, если вы захотите после изменений записать изображение в файл. Для более удобной работы изменяем метод до следующего вида:

public static BufferedImage getBufferedImage(String fileName) throws IOException { File file = new File(fileName); ImageReader r = getImageReaderByHeader(file); r.setInput(new FileImageInputStream(file)); ImageReadParam param = new ImageReadParam(); return r.read(0, param); }

Приведу пример простой пример, читаем пиксель с координатами x = 0, y = 0 и берем уровень красного цвета в RGB системе.

public static void main(String[] args) throws IOException { BufferedImage img = getBufferedImage("c:/test_images/1.gif"); System.out.println("Pixel 0,0 red = " + new Color(img.getRGB(0, 0)).getRed()); }

Перейдем к записи изображения, для реализации записи добавим в наш класс такой метод:

public void saveImage(BufferedImage image, File file, ImageWriter w) throws FileNotFoundException, IOException { w.setOutput(new FileImageOutputStream(file)); w.write(image); ((FileImageOutputStream)w.getOutput()).close(); }

В методе последний аргумент типа ImageWriter, это интерфейс который реализуют все <ImageType>Writer’ы, т.е получатся та же история с выбором необходимой реализации если вы хотите оставить файл в таком же формате. Мы пойдем простым путем, будем сохранять все изображения в JPG. Модифицируем наш простой пример, который будет изменять цвет у пикселя с координатами x = 5, y = 5 на черный (Color.BLACK):

public static void main(String[] args) throws IOException { BufferedImage img = getBufferedImage("c:/test_images/1.gif"); img.setRGB(5, 5, Color.BLACK.getRGB()); saveImage(img, new File("c:/test_images/1_edited.jpg"), new JPEGImageWriter(new JPEGImageWriterSpi())); }

Не трудно заметить, что на основе данных классов возможно реализовать конвертацию изображений между форматами PNG, GIF, JPEG, BMP, WBMP.

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