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


67

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

使用OSX上的Preview,我可以看到jpg内嵌了方向元数据。当我使用ImageTools(Grails插件)生成缩略图时,EXIF元数据不在缩略图中,这就是缩略图显示为旋转状态的原因。

通过脱机对话,我了解到虽然读取EXIF元数据相对容易,但是没有简单的写入方法,这就是为什么在生成jpg缩略图时数据会丢失的原因。

所以看来我有两个选择:

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

有人知道其他选择吗?


2
如果只需要批处理命令行选项,imagickmagick可以执行此操作。查看-auto-orient命令行标志。如果要转换jpeg并希望避免重新压缩的问题,也可以使用jheadjpeg。jhead -autorot *.jpg应该做你需要的。恐怕我没有Java解决方案...
Joe Kington

@joe,最后我只想让缩略图“看起来正确”。如果可能的话,id希望通过某种方式使浏览器意识到事物的方向来解决该问题。
hvgotcodes 2011年

Answers:


71

如果要旋转图像,建议使用元数据提取器库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


1
除了我使用的thumbnailator库(其中包含旋转方法)外,其他操作与我的操作完全相同。由于您花时间显示了赏金代码,因此您可以理解。
hvgotcodes 2011年

请注意,inreadImageInformationdirectory(可能还有jpegDirectory)可以是null
塞缪尔

感谢您的回答,它几乎对我有用。如果我弄错了,请纠正我,但是transformInage中的colormodel行应该是:BufferedImage destinationImage = op.createCompatibleDestImage(image,(image.getType()== BufferedImage.TYPE_BYTE_GRAY)?null:image.getColorModel());
jsaven

嗯,颜色模型对我而言还是遥不可及的。最终生成的CMYK JPG渲染得很差或根本没有。
山姆·巴纳姆

2
执行变换方法后,图像的颜色已更改,为什么?
jet ma

21

Thumbnailator图书馆荣誉EXIF方向标志。要以正确的方向读取完整尺寸的图像:

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

太棒了!我不知道这种隐藏的功能。它非常适合在读取图像时自动旋转图像。它比通过元数据提取器工作要容易得多。
杰夫

1
不幸的是,Thumbnailator旋转后的图像质量很差。github.com/coobird/thumbnailator/issues/101
DylanYi'2

8

通过使用JavaXT核心库图像部分,可以轻松地完成此操作:

// 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更优雅。


1
不幸的是,据我所知,javaxt没有Maven回购协议(也许我错过了吗?),这意味着我需要做一堆自定义构建工作,才可以使用em :(
Gus 2014年

1
不幸的是,JavaXT核心库在某些情况下无法正确旋转图像。它适用于某些图像,但不适用于其他图像。一幅有效的映像具有ExifVersion = Exif版本2.1,一幅无效的映像具有ExifVersion = Exif版本2.2。也许这就是问题所在,JavaXT核心无法处理2.2版。我不知道。
Per Lindberg 2014年

2
另外,image.saveAs()使用内存映射文件,因此结果文件可以为空或在Windows中锁定。通过字节数组进行保存似乎更好。但是我还是要扔掉JavaXT。
Per Lindberg 2014年


@PerLindberg,请参阅下面的答案。混合解决方案适用于Exif 2.1和Exid 2.2,并利用您的建议。
Ted Gulesserian


2

dnault在先前的评论中所述,Thumbaliator库解决了该问题。但是您应该使用正确的输入/输出格式,以避免在此自动旋转时颜色发生变化。

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

2

我的解决方案是@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元数据。此处介绍更多信息和解决方法。
彼得

1

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


karmakaze-是的,我想我必须这样做-您在服务器上谈论的是吗?我担心不同的相机可能具有不同的元数据-这样有效吗?另外,除了jpg以外,是否还有其他图像格式需要这样做?
hvgotcodes 2011年

根据Wikipedia所述,Exif适用于JPEG和TIFF图像文件以及某些音频文件格式。JPEG 2000,PNG或GIF不支持此功能。数码相机使用的许多本机格式都将带有Exif标签。
karmakaze 2011年

-1

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

我希望它将对某些人有所帮助。

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旋转,除非它是正方形,否则您在创建目标图像时需要从变换操作中获取新的边界。
罗伯特·亨特

由于旋转图像的尺寸与原始图像相同,因此仅适用于正方形图像。
nishantbhardwaj2002 '18
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.