如何根据方向元数据旋转 JPEG 图像?

Posted

技术标签:

【中文标题】如何根据方向元数据旋转 JPEG 图像?【英文标题】:How to rotate JPEG images based on the orientation metadata? 【发布时间】:2011-08-19 20:05:35 【问题描述】:

我有一些服务器代码在上传图片时生成缩略图。问题是,当拍摄图像并旋转相机/设备时,缩略图会旋转,即使完整尺寸的图像本身在任何图像查看软件中都以正确的方向显示。这只发生在 jpg 上。

在 OSX 上使用 Preview,我可以看到 jpg 中嵌入了方向元数据。当我使用 ImageTools (Grails Plugin) 生成缩略图时,EXIF 元数据不在缩略图中,这就是缩略图出现旋转的原因。

通过离线对话,我了解到,虽然读取 EXIF 元数据相对容易,但没有简单的方法来编写它,这就是生成 jpg 缩略图时数据丢失的原因。

看来我有两个选择:

    使用 ImageMagick 生成缩略图。缺点是需要在我们的服务器上安装更多软件。 读取 EXIF 方向数据代码并适当旋转缩略图。

有人知道其他选择吗?

【问题讨论】:

如果你只想要一个批处理命令行选项,imagickmagick 可以做到这一点。查看-auto-orient 命令行标志。如果您正在转换 jpeg 并希望避免重新压缩出现问题,您也可以使用 jhead 来执行此操作。 jhead -autorot *.jpg 应该做你需要的。不过,恐怕我没有 java 解决方案...... @joe,最后我想要的只是让缩略图“看起来正确”。如果可能的话,我想通过某种方式让浏览器意识到事物是面向的来解决这个问题。 【参考方案1】:

如果您想旋转图像,我建议使用元数据提取器库http://code.google.com/p/metadata-extractor/。可以通过以下代码获取图片信息:

// Inner class containing image information
public static class ImageInformation 
    public final int orientation;
    public final int width;
    public final int height;

    public ImageInformation(int orientation, int width, int height) 
        this.orientation = orientation;
        this.width = width;
        this.height = height;
    

    public String toString() 
        return String.format("%dx%d,%d", this.width, this.height, this.orientation);
    



public static ImageInformation readImageInformation(File imageFile)  throws IOException, MetadataException, ImageProcessingException 
    Metadata metadata = ImageMetadataReader.readMetadata(imageFile);
    Directory directory = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
    JpegDirectory jpegDirectory = metadata.getFirstDirectoryOfType(JpegDirectory.class);

    int orientation = 1;
    try 
        orientation = directory.getInt(ExifIFD0Directory.TAG_ORIENTATION);
     catch (MetadataException me) 
        logger.warn("Could not get orientation");
    
    int width = jpegDirectory.getImageWidth();
    int height = jpegDirectory.getImageHeight();

    return new ImageInformation(orientation, width, height);

然后根据您检索到的方向,您可以将图像旋转和/或翻转到正确的方向。 EXIF方向的仿射变换由以下方法给出:

// Look at http://chunter.tistory.com/143 for information
public static AffineTransform getExifTransformation(ImageInformation info) 

    AffineTransform t = new AffineTransform();

    switch (info.orientation) 
    case 1:
        break;
    case 2: // Flip X
        t.scale(-1.0, 1.0);
        t.translate(-info.width, 0);
        break;
    case 3: // PI rotation 
        t.translate(info.width, info.height);
        t.rotate(Math.PI);
        break;
    case 4: // Flip Y
        t.scale(1.0, -1.0);
        t.translate(0, -info.height);
        break;
    case 5: // - PI/2 and Flip X
        t.rotate(-Math.PI / 2);
        t.scale(-1.0, 1.0);
        break;
    case 6: // -PI/2 and -width
        t.translate(info.height, 0);
        t.rotate(Math.PI / 2);
        break;
    case 7: // PI/2 and Flip
        t.scale(-1.0, 1.0);
        t.translate(-info.height, 0);
        t.translate(0, info.width);
        t.rotate(  3 * Math.PI / 2);
        break;
    case 8: // PI / 2
        t.translate(0, info.width);
        t.rotate(  3 * Math.PI / 2);
        break;
    

    return t;

图像的旋转将通过以下方法完成:

public static BufferedImage transformImage(BufferedImage image, AffineTransform transform) throws Exception 

    AffineTransformOp op = new AffineTransformOp(transform, AffineTransformOp.TYPE_BICUBIC);

    BufferedImage destinationImage = op.createCompatibleDestImage(image, (image.getType() == BufferedImage.TYPE_BYTE_GRAY) ? image.getColorModel() : null );
    Graphics2D g = destinationImage.createGraphics();
    g.setBackground(Color.WHITE);
    g.clearRect(0, 0, destinationImage.getWidth(), destinationImage.getHeight());
    destinationImage = op.filter(image, destinationImage);
    return destinationImage;

在服务器环境中,别忘了用-Djava.awt.headless=true运行

【讨论】:

和我所做的完全一样,除了我使用了thumbnailarator 库,它有一个rotate 方法。由于您花时间展示赏金代码,因此您明白了。 请注意,在readImageInformation 中,directory(也可能还有jpegDirectory)可以是null 感谢您的回答,它几乎对我有用。如果我弄错了,请纠正我,但是 transformInage 中的颜色模型行应该是: BufferedImage destinationImage = op.createCompatibleDestImage(image, (image.getType() == BufferedImage.TYPE_BYTE_GRAY)? null : image.getColorModel()); 嗯,颜色模型对我来说太离谱了。最终得到一个 CMYK JPG 渲染非常糟糕或根本没有。 执行transform方法后图片的颜色发生了变化,为什么?【参考方案2】:

Thumbnailator 库支持 EXIF 方向标志。要以正确的方向读取完整尺寸的图像:

BufferedImage image = Thumbnails.of(inputStream).scale(1).asBufferedImage();

【讨论】:

太棒了!我不知道这种隐藏的能力。它非常适合在读取图像时自动旋转图像。而且比通过元数据提取器工作要容易得多。 很遗憾,Thumbnailator 旋转后的图像质量很差。 github.com/coobird/thumbnailator/issues/101【参考方案3】:

这可以通过使用image part of JavaXT core library 轻松完成:

// Browsers today can't handle images with Exif Orientation tag
Image image = new Image(uploadedFilename);
// Auto-rotate based on Exif Orientation tag, and remove all Exif tags
image.rotate(); 
image.saveAs(permanentFilename);

就是这样!

我尝试过 Apache Commons Imaging,但那是一团糟。 JavaXT 更加优雅。

【讨论】:

遗憾的是 javaxt 没有 maven 仓库据我所知(也许我错过了?),这意味着我需要做一堆自定义构建的东西才能使用 em :(跨度> 不幸的是,JavaXT 核心库在某些情况下无法正确旋转图像。它适用于某些图像,但不适用于其他图像。一个有效的图像具有 ExifVersion=Exif 版本 2.1,一个无效的图像具有 ExifVersion=Exif 版本 2.2。也许这就是问题所在,JavaXT 核心不处理 2.2 版。我不知道。 另外,image.saveAs() 使用内存映射文件,因此结果文件可以为空或在 Windows 中锁定。通过字节数组保存似乎效果更好。但无论如何我都会抛弃 JavaXT。 javaxt 可用于 maven:mvnrepository.com/artifact/javaxt/javaxt-core @PerLindberg,请参阅下面的答案。混合解决方案适用于 Exif 2.1 和 Exid 2.2,并利用您的建议。【参考方案4】:

Exif 似乎很难编写,因为其中包含专有内容。 但是,您可以考虑另一种选择

读取原件,但只将方向标签写入缩略图。

Apache Sanselan 似乎有很好的工具集来做这件事。

http://commons.apache.org/proper/commons-imaging/

以 ExifRewriter 类为例。

【讨论】:

【参考方案5】:

正如 dnault 在之前的评论中提到的,Thumbnaliator 库解决了这个问题。但是你应该使用正确的输入/输出格式来避免这种自动旋转的颜色变化。

ByteArrayOutputStream baos = new ByteArrayOutputStream();
ByteArrayInputStream in = new ByteArrayInputStream(file.getContents());
Thumbnails.of(in)
    .scale(1)
    .toOutputStream(baos);
byte[] bytes = baos.toByteArray();

【讨论】:

【参考方案6】:

我的解决方案是@PerLindberg 的答案和@AntoineMartin 的结合。我在 Windows 10 上使用 Java 8 尝试了其他答案,但似乎没有一个可以解决问题。 @AntoinMartin 的 com.drew.imaging 解决方案很慢,图像变成黑白并且充满了伪影。 @PerLindberg 的 JavaXT 解决方案没有读取 Exif 2.2 数据。

1)使用com.drew.imaging读取exif信息:

// Inner class containing image information
public static class ImageInformation 
    public final int orientation;
    public final int width;
    public final int height;

    public ImageInformation(int orientation, int width, int height) 
        this.orientation = orientation;
        this.width = width;
        this.height = height;
    

    public String toString() 
        return String.format("%dx%d,%d", this.width, this.height, this.orientation);
    


public ImageInformation readImageInformation(File imageFile)  throws IOException, MetadataException, ImageProcessingException 
    Metadata metadata = ImageMetadataReader.readMetadata(imageFile);
    Directory directory = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
    JpegDirectory jpegDirectory = metadata.getFirstDirectoryOfType(JpegDirectory.class);

    int orientation = 1;
    if (directory != null) 
        try 
            orientation = directory.getInt(ExifIFD0Directory.TAG_ORIENTATION);
         catch (MetadataException me) 
            logger.warn("Could not get orientation");
        
        int width = jpegDirectory.getImageWidth();
        int height = jpegDirectory.getImageHeight();

        return new ImageInformation(orientation, width, height);
     else 
        return null;
    

2) 使用JavaXT 执行基于Exif 数据的旋转。

public void rotateMyImage(String imageDownloadFilenme);
    File imageDownloadFile =  new File(imgageDownloadFilenme);
    Image image = new Image(imgageDownloadFilenme);
    ImageInformation imageInformation = readImageInformation(imageDownloadFile);
    if (imageInformation != null) 
        rotate(imageInformation, image);
    
    image.saveAs(imgageDownloadFilenme);


public void rotate(ImageInformation info, Image image) 

    switch(info.orientation) 
        case 1:
            return;
        case 2:
            image.flip();
            break;
        case 3:
            image.rotate(180.0D);
            break;
        case 4:
            image.flip();
            image.rotate(180.0D);
            break;
        case 5:
            image.flip();
            image.rotate(270.0D);
            break;
        case 6:
            image.rotate(90.0D);
            break;
        case 7:
            image.flip();
            image.rotate(90.0D);
            break;
        case 8:
            image.rotate(270.0D);
    


【讨论】:

或者,您可以将 JavaXT 与 TwelveMonkeys 一起使用。 TwelveMonkeys 提供了一个 JPEG ImageIO 插件,它允许 javaxt.io.Image 处理大部分(如果不是全部)Exif 元数据。更多信息和解决方法描述here。【参考方案7】:

如果你只是想让它看起来正确。根据您已经提取的“方向”,您可以根据需要添加“旋转”-PI/2(-90 度)、PI/2(90 度)或 PI(+180 度)。浏览器或任何其他程序将正确显示图像,因为将应用方向并且从缩略图输出中删除元数据。

【讨论】:

karmakaze -- 是的,我想我必须这样做 -- 你是在谈论服务器上的问题吗?我担心不同的相机可能有不同的元数据——这有效吗?另外,除了 jpg 之外,还有其他图像格式需要这个吗? 根据***,Exif 适用于 JPEG 和 TIFF 图像文件,以及一些音频文件格式。 JPEG 2000、PNG 或 GIF 不支持它。数码相机使用的许多原生格式都会带有 Exif 标签。【参考方案8】:

上面的旋转函数旋转图像数据。还有一件事要做。您必须设置 BufferedImage 的宽度和高度。旋转90度和270度时,宽度和高度必须互换。

    BufferedImage transformed;
    switch (orientation) 
        case 5:
        case 6:
        case 7:
        case 8:
            transformed = new BufferedImage(image.getHeight(), image.getWidth(), image.getType());
            break;
        default:
            transformed = new BufferedImage(image.getWidth(), image.getHeight(), image.getType());
    

【讨论】:

【参考方案9】:

根据 Antoine Martin 的回答,我创建了一个自己的类,用于根据图像的 exif 信息校正给定 jpeg 图像(在我的情况下为输入流)的方向。有了他的解决方案,我遇到了问题,结果图像的颜色是错误的,因此我创建了这个。 为了检索图像的元数据,我使用了metadata-extractor 库。

我希望它会帮助一些人。

public class ImageOrientationUtil 

/**
 * Checks the orientation of the image and corrects it if necessary.
 * <p>If the orientation of the image does not need to be corrected, no operation will be performed.</p>
 * @param inputStream
 * @return
 * @throws ImageProcessingException
 * @throws IOException
 * @throws MetadataException
 */
public static BufferedImage correctOrientation(InputStream inputStream) throws ImageProcessingException, IOException, MetadataException 
    Metadata metadata = ImageMetadataReader.readMetadata(inputStream);
    if(metadata != null) 
        if(metadata.containsDirectoryOfType(ExifIFD0Directory.class)) 
            // Get the current orientation of the image
            Directory directory = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
            int orientation = directory.getInt(ExifIFD0Directory.TAG_ORIENTATION);

            // Create a buffered image from the input stream
            BufferedImage bimg = ImageIO.read(inputStream);


            // Get the current width and height of the image
            int[] imageSize = bimg.getWidth(), bimg.getHeight();
            int width = imageSize[0];
            int height = imageSize[1];

            // Determine which correction is needed
            AffineTransform t = new AffineTransform();
            switch(orientation) 
            case 1:
                // no correction necessary skip and return the image
                return bimg;
            case 2: // Flip X
                t.scale(-1.0, 1.0);
                t.translate(-width, 0);
                return transform(bimg, t);
            case 3: // PI rotation 
                t.translate(width, height);
                t.rotate(Math.PI);
                return transform(bimg, t);
            case 4: // Flip Y
                t.scale(1.0, -1.0);
                t.translate(0, -height);
                return transform(bimg, t);
            case 5: // - PI/2 and Flip X
                t.rotate(-Math.PI / 2);
                t.scale(-1.0, 1.0);
                return transform(bimg, t);
            case 6: // -PI/2 and -width
                t.translate(height, 0);
                t.rotate(Math.PI / 2);
                return transform(bimg, t);
            case 7: // PI/2 and Flip
                t.scale(-1.0, 1.0);
                t.translate(height, 0);
                t.translate(0, width);
                t.rotate(  3 * Math.PI / 2);
                return transform(bimg, t);
            case 8: // PI / 2
                t.translate(0, width);
                t.rotate(  3 * Math.PI / 2);
                return transform(bimg, t);
            
        
    

    return null;


/**
 * Performs the tranformation
 * @param bimage
 * @param transform
 * @return
 * @throws IOException
 */
private static BufferedImage transform(BufferedImage bimage, AffineTransform transform) throws IOException 
    // Create an transformation operation
    AffineTransformOp op = new AffineTransformOp(transform, AffineTransformOp.TYPE_BICUBIC);

    // Create an instance of the resulting image, with the same width, height and image type than the referenced one
    BufferedImage destinationImage = new BufferedImage( bimage.getWidth(), bimage.getHeight(), bimage.getType() );
    op.filter(bimage, destinationImage);

   return destinationImage;


【讨论】:

这将不起作用,因为您正在读取输入流两次,一次获取Metadata,然后创建BufferedImage。您需要复制流或使用可以重置并再次读取的变体。目标图像也可能具有不正确的边界,例如 90 度 CW 或 CCW 旋转,除非它是方形的,您需要在创建目标图像时从变换操作中获取新的边界。 将仅适用于方形图像,因为旋转图像的尺寸与原始图像保持相同。

以上是关于如何根据方向元数据旋转 JPEG 图像?的主要内容,如果未能解决你的问题,请参考以下文章

读取 JPEG 元数据(方向)时出现问题

从python中的jpeg2000图像中提取元数据

如何使用 IPTC/EXIF 元数据对照片进行分类?

JS 客户端 Exif 方向:旋转和镜像 JPEG 图像

获取图像方向并根据方向旋转

StorageFile.GetScaledImageAsThumbnailAsync 不支持元数据旋转图像