如何在Android上旋转JPEG文件而不失去质量并增加文件大小?

7

背景

我需要旋转由相机拍摄的图像,使其始终具有正常的方向。

为此,我使用下面的代码(参考这篇文章获取图像方向):

//<= get the angle of the image , and decode the image from the file
final Matrix matrix = new Matrix();
//<= prepare the matrix based on the EXIF data (based on https://gist.github.com/9re/1990019 )
final Bitmap rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(),matrix,false);
bitmap.recycle();
fileOutputStream = new FileOutputStream(tempFilePath);
rotatedBitmap.compress(CompressFormat.JPEG, 100, fileOutputStream);
rotatedBitmap.recycle();

这里的压缩率(也称为“质量”参数)为100。

问题

代码运行正常,但结果比原始文件要大得多,远远大于原始文件。

原始文件约为600-700 KB,而生成的文件约为3MB...

尽管输入文件和输出文件都是相同格式(JPEG),这种情况仍然存在。

相机设置为“超精细”质量。不确定它的含义,但我认为它与压缩比有关。

我的尝试

我尝试将“filter”参数设置为false或true。两者都导致文件较大。

即使没有旋转本身(只解码和编码),我也会得到更大的文件大小...

只有当我将压缩比率设置为85左右时,我才能得到类似的文件大小,但我想知道与原始文件相比质量受到了什么影响。

问题

为什么会发生这种情况?

有没有办法获得与输入文件完全相同的大小和质量?

使用与原始文件相同的压缩率是否会发生这种情况?是否可能获取原始文件的压缩率?

什么是100%的压缩率?


编辑:我发现此链接讨论了JPEG文件旋转而不失去质量和文件大小,但 Android 上是否有解决方案?

这里另一个链接说这是可能的,但我找不到任何允许旋转JPEG文件而不失去其质量的库。


1
设置方向标签是最好的选择。我见过唯一忽略方向标签的图像查看器是旧版本的“Windows 图像查看器”(内置应用程序)。我的 C 图像库允许无损旋转,但它不是免费的,并且需要您调用本机代码。 - BitBank
"Windows Image Viewer" 的旧版本?所以 Windows 8 是旧的... :) 无论如何,我希望避免假设人们如何显示图像。 - android developer
嗨,你解决了吗?我尝试了LLJTran,但读取图像文件时抛出了异常。 - Yeung
@android开发者,我查看了这个使用LLJTran的方法。在llj.read(LLJTran.READ_ALL, true);这一行会抛出异常。我将第二个参数改为false,就可以工作了。输出jpeg的大小与原始大小大致相同。如果处理时间不是问题(2.5MB jpeg需要约10秒至20秒),我推荐使用此方法。我无法使用此库检查Exif数据,所以我从问题中提到的文章中获取方向信息。 - Yeung
也许 这个链接 可以控制相机将照片保存为 JPEG 格式。我还没有尝试过。 - Yeung
显示剩余5条评论
4个回答

3
我尝试了两种方法,但发现这些方法在我的情况下花费时间太长。我仍然分享我使用的内容。
方法1:Android的LLJTran
从这里获取LLJTran: https://github.com/bkhall/AndroidMediaUtil 代码如下:
public static boolean rotateJpegFileBaseOnExifWithLLJTran(File imageFile, File outFile){
    try {

        int operation = 0;
        int degree = getExifRotateDegree(imageFile.getAbsolutePath());
        //int degree = 90;
        switch(degree){
            case 90:operation = LLJTran.ROT_90;break;
            case 180:operation = LLJTran.ROT_180;break;
            case 270:operation = LLJTran.ROT_270;break;
        }   
        if (operation == 0){
            Log.d(TAG, "Image orientation is already correct");
            return false;
        }

        OutputStream output = null;
        LLJTran llj = null;
        try {   
            // Transform image
            llj = new LLJTran(imageFile);
            llj.read(LLJTran.READ_ALL, false); //don't know why setting second param to true will throw exception...
            llj.transform(operation, LLJTran.OPT_DEFAULTS
                    | LLJTran.OPT_XFORM_ORIENTATION);

            // write out file
            output = new BufferedOutputStream(new FileOutputStream(outFile));
            llj.save(output, LLJTran.OPT_WRITE_ALL);
            return true;
        } catch(Exception e){
            e.printStackTrace();
            return false;
        }finally {
            if(output != null)output.close();
            if(llj != null)llj.freeMemory();
        }
    } catch (Exception e) {
        // Unable to rotate image based on EXIF data
        e.printStackTrace();
        return false;
    }
}

public static int getExifRotateDegree(String imagePath){
    try {
        ExifInterface exif;
        exif = new ExifInterface(imagePath);
        String orientstring = exif.getAttribute(ExifInterface.TAG_ORIENTATION);
        int orientation = orientstring != null ? Integer.parseInt(orientstring) : ExifInterface.ORIENTATION_NORMAL;
        if(orientation == ExifInterface.ORIENTATION_ROTATE_90) 
            return 90;
        if(orientation == ExifInterface.ORIENTATION_ROTATE_180) 
            return 180;
        if(orientation == ExifInterface.ORIENTATION_ROTATE_270) 
            return 270;
    } catch (IOException e) {
        e.printStackTrace();
    }       
    return 0;
}

方法二:使用libjepg-turbo的jpegtran可执行文件

1、按照以下步骤进行操作: https://dev59.com/eWct5IYBdhLWcg3wNq0g#12296343

不同之处在于,您不需要在ndk-build中使用obj/local/armeabi/libjpeg.a,因为我只需要jpegtran可执行文件而不涉及JNI和libjepg.a

2、将jpegtran可执行文件放置在asset文件夹中。 代码如下:

public static boolean rotateJpegFileBaseOnExifWithJpegTran(Context context, File imageFile, File outFile){
    try {

        int operation = 0;
        int degree = getExifRotateDegree(imageFile.getAbsolutePath());
        //int degree = 90;
        String exe = prepareJpegTranExe(context);
        //chmod ,otherwise  premission denied
        boolean ret = runCommand("chmod 777 "+exe); 
        if(ret == false){
            Log.d(TAG, "chmod jpegTran failed");
            return false;
        }           
        //rotate the jpeg with jpegtran
        ret = runCommand(exe+
                " -rotate "+degree+" -outfile "+outFile.getAbsolutePath()+" "+imageFile.getAbsolutePath());         

        return ret;         
    } catch (Exception e) {
        // Unable to rotate image based on EXIF data
        e.printStackTrace();
        return false;
    }
}

public static String prepareJpegTranExe(Context context){
    File exeDir = context.getDir("JpegTran", 0);
    File exe = new File(exeDir, "jpegtran");
    if(!exe.exists()){
        try {
            InputStream is = context.getAssets().open("jpegtran");
            FileOutputStream os = new FileOutputStream(exe);
            int bufferSize = 16384;
            byte[] buffer = new byte[bufferSize];
            int count;
            while ((count=is.read(buffer, 0, bufferSize))!=-1) {
                os.write(buffer, 0, count);
            }               
            is.close();
            os.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    return exe.getAbsolutePath();
}

public static boolean runCommand(String cmd){
    try{
        Process process = Runtime.getRuntime().exec(cmd);

        BufferedReader reader = new BufferedReader(
                new InputStreamReader(process.getInputStream()));
        int read;
        char[] buffer = new char[4096];
        StringBuffer output = new StringBuffer();
        while ((read = reader.read(buffer)) > 0) {
            output.append(buffer, 0, read);
        }
        reader.close();

        // Waits for the command to finish.
        process.waitFor();

        return true;            
    } catch (Exception e) {
        e.printStackTrace();
        return false;
    }
}

很遗憾,两者都太慢了。在我的三星Galaxy S1上需要16秒!!!但我发现这个应用程序(https://play.google.com/store/apps/details?id=com.lunohod.jpegtool)只需要3-4秒钟。肯定有方法可以解决。


你检查过输出的位图,确认它们与原始位图旋转后完全一致了吗? - android developer
@安卓开发者,我将原始的JPEG文件保存下来,并用jepgtran旋转了90度4次到电脑上,然后使用diffimg进行了检查。结果显示没有任何像素差异。 - Yeung
很酷。顺便说一下,你不必使用PC,你可以将位图存储在设备上并比较像素...这个答案看起来很有前途。我希望我能有机会去尝试一下。现在,这里是一个赞(+1)。 :) - android developer
您链接的应用程序不存在。您已经成功地优化了代码吗?请将其分享到Github上好吗?另外,您能否解决所有方向的问题?其中一些需要翻转图像... - android developer

0

一旦您完成了设置最佳预览大小,现在您必须为最佳图片大小进行设置。每个手机都支持不同的图片尺寸,因此要获得最佳图片质量,您必须检查支持的图片尺寸,然后将最佳尺寸设置为相机参数。您必须在surfaceChanged中设置这些参数以获取宽度和高度。surfaceChanged将在启动时调用,因此您的新参数将被设置。

 @Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {


 Camera.Parameters myParameters = camera.getParameters();

        myPicSize = getBestPictureSize(width, height);

        if (myBestSize != null && myPicSize != null) {

            myParameters.setPictureSize(myPicSize.width, myPicSize.height);

            myParameters.setJpegQuality(100);
            camera.setParameters(myParameters);

            Toast.makeText(getApplicationContext(),
                    "CHANGED:Best PICTURE SIZE:\n" +
                            String.valueOf(myPicSize.width) + " ::: " + String.valueOf(myPicSize.height),
                    Toast.LENGTH_LONG).show();
      }
}

现在getBestPictureSize..

 private Camera.Size getBestPictureSize(int width, int height)
    {
    Camera.Size result=null;
    Camera.Parameters p = camera.getParameters();
    for (Camera.Size size : p.getSupportedPictureSizes()) {
        if (size.width>width || size.height>height) {
            if (result==null) {
                result=size;
            } else {
                int resultArea=result.width*result.height;
                int newArea=size.width*size.height;

                if (newArea>resultArea) {
                    result=size;
                }
            }
        }
    }
    return result;

} 

问题是关于旋转JPEG文件而不失去质量,但这也很好。 - android developer
请查看此链接以了解如何旋转图片 @androiddeveloper link - KD_
这是关于相机的一个不错的知识点,但问题是关于图像文件的(可以通过相机创建)。 - android developer
如果您将此旋转设置为相机,则在保存图像文件时,它将自动旋转,您无需为图像数据创建位图并应用其他旋转方法。 - KD_
用户可以随时使用第三方相机应用程序。我想处理相机应用程序生成的图像文件,该文件具有自己的元数据(EXIF),关于如何捕获图像的方向等信息。 - android developer

-1

对于旋转,请尝试这个...

final Matrix matrix = new Matrix(); 
matrix.setRotate(90): 
final Bitmap rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(),matrix,false);

我正在使用PNG图像,它运行良好... 对于JPEG图像,请检查上面的代码。


以上的代码是什么?同时,我的问题是关于JPEG格式的,因为它们是有损压缩的(与原始图像相比,在压缩时会失去质量),如何在不失去原始JPEG文件的质量的情况下制作一个文件。您的代码与我所问的任何内容都没有关系。 - android developer
@416E64726577 这不是一个真正的答案。它会再次解码和编码文件,而我明确写过我不想要这样做,因为这会导致重新压缩而失去质量。 - android developer

-2

100%的质量率可能比文件原始保存设置更高的质量设置。这会导致更大的文件大小,但(几乎)相同的图像。

我不确定如何获得完全相同的大小,也许只需将质量设置为85%即可(快速且简单)。

但是,如果您只想将图片旋转90度,您可以仅编辑JPEG元数据而不触及像素数据本身。

不确定在Android中如何完成,但是这就是它的工作方式。


JPEG不是无损的,它总是有损的。关于编辑元数据,这并没有帮助,因为我想让所有图像查看器正确地查看图像,而相机应用程序有时会将方向添加到元数据中,因此如果您显示图像,则不会看起来像用户所做的那样。例如,如果您以90度顺时针拍摄了一张图像,并且用户打开了该图像,则图像数据将不考虑方向而被拍摄,因此它将被旋转,而元数据只会保留文件的方向。一个好的图像查看器将考虑到这一点并自行旋转。 - android developer
顺便提一下,这里有一个关于JPEG的神话页面:http://graphicssoft.about.com/od/formatsjpeg/a/jpegmythsfacts.htm,其中包括你所写的关于100%质量率的内容。 - android developer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接