2011年11月22日 星期二

Java與圖像的EXIF資訊


首先介紹一下什麼是EXIFEXIF Exchangeable Image File的縮寫,這是一種專門為數碼相機照片設定的格式。這種格式可以用來記錄數位照片的屬性資訊,例如相機的品牌及型號、相片的拍攝時間、拍攝時所設置的光圈大小、快門速度、ISO等等資訊。除此之外它還能夠記錄拍攝資料,以及照片格式化方式,這樣就可以輸出到相容EXIF格式的外設上,例如照片印表機等。

目前最常見的支援EXIF資訊的圖片格式是JPG,很多的圖像工具都可以直接顯示圖片的EXIF資訊,包括現在的一些著名的相冊網站也提供頁面用於顯示照片的EXIF資訊。本文主要介紹Java語言如何讀取圖像的EXIF資訊,包括如何根據EXIF資訊對圖像進行調整以適合使用者流覽。

目前最簡單易用的EXIF資訊處理的Java包是Drew Noakes寫的metadata-extractor,該專案最新的版本是2.3.0,支持EXIF 2.2版本。你可以直接從http://www.drewnoakes.com/code/exif/ 下載該項目的最新版本包括其源碼。

需要注意的是,並不是每個JPG影像檔都包含有EXIF資訊,你可以在Windows資源管理器按一下選中圖片後,如果該圖片包含EXIF資訊,則在視窗狀態列會顯示出相機的型號,如下圖所示:

拍攝設備的型號便是EXIF資訊中的其中一個。下面我們給出一段代碼將這個圖片的所有的EXIF資訊全部列印出來。
package com.liusoft.dlog4j.test;

import java.io.File;
import java.util.Iterator;

import com.drew.imaging.jpeg.JpegMetadataReader;
import com.drew.metadata.Directory;
import com.drew.metadata.Metadata;
import com.drew.metadata.Tag;
import com.drew.metadata.exif.ExifDirectory;

/**
 * 測試用於讀取圖片的EXIF資訊
 * @author Winter Lau
 */
public class ExifTester {
     public static void main(String[] args) throws Exception {
         File jpegFile = new File("D:\\我的文件\\我的相冊\\DSCF1749.JPG");
         Metadata metadata = JpegMetadataReader.readMetadata(jpegFile);
         Directory exif = metadata.getDirectory(ExifDirectory.class);
         Iterator tags = exif.getTagIterator();
         while (tags.hasNext()) {
             Tag tag = (Tag)tags.next();
             System.out.println(tag);
         }
     }
}


metadata-extractor-2.3.0.jar檔加入到類路徑中編譯並執行上面這段代碼後可得到下面的運行結果:

[Exif] Make - FUJIFILM
[Exif] Model - FinePix A205S
[Exif] Orientation - Top, left side (Horizontal / normal)
[Exif] X Resolution - 72 dots per inch
[Exif] Y Resolution - 72 dots per inch
[Exif] Resolution Unit - Inch
[Exif] Software - Digital Camera FinePix A205S  Ver1.00
[Exif] Date/Time - 2005:05:13 22:18:49
[Exif] YCbCr Positioning - Datum point
[Exif] Copyright -    
[Exif] Exposure Time - 1/60 sec
[Exif] F-Number - F3
[Exif] Exposure Program - Program normal
[Exif] ISO Speed Ratings - 320
[Exif] Exif Version - 2.20
[Exif] Date/Time Original - 2005:05:13 22:18:49
[Exif] Date/Time Digitized - 2005:05:13 22:18:49
[Exif] Components Configuration - YCbCr
[Exif] Compressed Bits Per Pixel - 3 bits/pixel
[Exif] Shutter Speed Value - 1/63 sec
[Exif] Aperture Value - F3
[Exif] Brightness Value - -61/100
[Exif] Exposure Bias Value - 0 EV
[Exif] Max Aperture Value - F3
[Exif] Metering Mode - Multi-segment
[Exif] Light Source - Unknown
[Exif] Flash - Flash fired, auto
[Exif] Focal Length - 5.5 mm
[Exif] FlashPix Version - 1.00
[Exif] Color Space - sRGB
[Exif] Exif Image Width - 1280 pixels
[Exif] Exif Image Height - 960 pixels
[Exif] Focal Plane X Resolution - 1/2415 cm
[Exif] Focal Plane Y Resolution - 1/2415 cm
[Exif] Focal Plane Resolution Unit - cm
[Exif] Sensing Method - One-chip color area sensor
[Exif] File Source - Digital Still Camera (DSC)
[Exif] Scene Type - Directly photographed image
[Exif] Custom Rendered - Normal process
[Exif] Exposure Mode - Auto exposure
[Exif] White Balance - Auto white balance
[Exif] Scene Capture Type - Standard
[Exif] Sharpness - None
[Exif] Subject Distance Range - Unknown
[Exif] Compression - JPEG (old-style)
[Exif] Thumbnail Offset - 1252 bytes
[Exif] Thumbnail Length - 7647 bytes
[Exif] Thumbnail Data - [7647 bytes of thumbnail data]
從這個執行的結果我們可以看出該照片是在20050513 221849秒拍攝的,拍攝用的相機型號是富士的FinePix A205S,曝光時間是1/60秒,光圈值F3,焦距5.5毫米,ISO值為320等等。

你也可以直接指定讀取其中任意參數的值,ExifDirectory類中定義了很多以TAG_開頭的整數常量,這些常量代表特定的一個參數值,例如我們要讀取相機的型號,我們可以用下面代碼來獲取。

Metadata metadata = JpegMetadataReader.readMetadata(jpegFile);
Directory exif = metadata.getDirectory(ExifDirectory.class);
String model = exif.getString(ExifDirectory.TAG_MODEL);

上述提到的是如何獲取照片的EXIF資訊,其中包含一個很重要的資訊就是——拍攝方向。例如上面例子所用的圖片的拍攝方向是:Orientation - Top, left side (Horizontal / normal)。我們在拍照的時候經常會根據場景的不同來選擇相機的方向,例如拍攝一顆高樹,我們會把相機豎著拍攝,使景物剛好適合整個取景框,但是這樣得到的圖片如果用普通的圖片流覽器看便是倒著的,需要調整角度才能得到一個正常的圖像,有如下面一張照片。

這張圖片正常的情況下需要向左調整90度,也就是順時針旋轉270度才適合觀看。通過讀取該圖片的EXIF資訊,我們得到關於拍攝方向的這樣一個結果:[Exif] Orientation - Left side, bottom (Rotate 270 CW)。而直接讀取ExitDirectory.TAG_ORIENTATION標籤的值是8。我們再來看這個項目是如何來定義這些返回值的,打開源碼包中的ExifDescriptor類的getOrientationDescription方法,該方法代碼如下:
public String getOrientationDescription() throws MetadataException
{
        if (!_directory.containsTag(ExifDirectory.TAG_ORIENTATION)) return null;
        int orientation = _directory.getInt(ExifDirectory.TAG_ORIENTATION);
        switch (orientation) {
            case 1: return "Top, left side (Horizontal / normal)";
            case 2: return "Top, right side (Mirror horizontal)";
            case 3: return "Bottom, right side (Rotate 180)";
            case 4: return "Bottom, left side (Mirror vertical)";
            case 5: return "Left side, top (Mirror horizontal and rotate 270 CW)";
            case 6: return "Right side, top (Rotate 90 CW)";
            case 7: return "Right side, bottom (Mirror horizontal and rotate 90 CW)";
            case 8: return "Left side, bottom (Rotate 270 CW)";
            default:
                return String.valueOf(orientation);
        }
}

從這個方法我們可以清楚看到各個返回值的意思,如此我們便可以根據實際的返回值來對圖像進行旋轉或者是鏡像處理了。在這個例子中我們需要將圖片順時針旋轉270度,或者是逆時針旋轉90度方可得到正常的圖片。

雖然圖片的旋轉不在本文範疇內,但為了善始善終,下面給出代碼用以旋轉圖片,其他的關於圖片的鏡像等處理讀者可以依此類推。

String path = "D:\\TEST.JPG";
File img = new File(path);
BufferedImage old_img = (BufferedImage)ImageIO.read(img); 
int w = old_img.getWidth();
int h = old_img.getHeight();

BufferedImage new_img = new BufferedImage(h,w,BufferedImage.TYPE_INT_BGR);     
Graphics2D g2d =new_img.createGraphics();
     
AffineTransform origXform = g2d.getTransform();
AffineTransform newXform = (AffineTransform)(origXform.clone());
// center of rotation is center of the panel
double xRot = w/2.0;
newXform.rotate(Math.toRadians(270.0), xRot, xRot); //旋轉270

g2d.setTransform(newXform);
// draw image centered in panel
g2d.drawImage(old_img, 0, 0, null);
// Reset to Original
g2d.setTransform(origXform);
//寫到新的檔
FileOutputStream out = new FileOutputStream("D:\\test2.jpg");
try{
ImageIO.write(new_img, "JPG", out);
}finally{
    out.close();
}

旋轉後的照片如下:

但是利用上面的代碼旋轉照片後,原有照片包含的EXIF資訊不復存在了。至於照片的鏡面翻轉可以直接利用Graphic2DdrawImage方法來實現,方法原形如下:
drawImage

public abstract boolean drawImage(Image img,
                   int dx1,
                                  int dy1,
                                  int dx2,
                                  int dy2,
                                  int sx1,
                                  int sy1,
                                  int sx2,
                                  int sy2,
                                  ImageObserver observer)
該方法的使用請參考JDKAPI文檔。關於照片旋轉後丟失EXIF資訊的問題,需要在照片旋轉之前先把EXIF資訊讀出,然後再在旋轉後寫入新的照片中,你可以使用MediaUtil包來寫EXIF資訊到圖片檔中,關於這個包的使用請參考該項目所給出的例子,本文不再敘述。
 
參考資料:
 
metadata-extractor  http://www.drewnoakes.com/code/exif/
MediaUtil        http://mediachest.sourceforge.net/mediautil/

沒有留言: