Android:无需加载图像即可旋转图像

24

我想知道是否可能在不加载图像到内存的情况下旋转存储在SD卡上的图像。

原因是我遇到了著名的OutOfMemoryError。我知道可以通过缩小大图像来避免它,但实际上我不想减小该图像的大小,我想保留原始图像但将其旋转90度。

对此有任何建议都非常感谢 :)


2
注释是给那些仍在寻找解决方案的人的。这种情况很可能发生在需要将图像发送到服务器时。经过几个小时的搜索,我发现在Android中不可能在不加载图像到内存中的情况下预先旋转图像。唯一的解决方案是在服务器端使用exif数据进行旋转处理。这里有一个有用的链接https://dev59.com/vGs05IYBdhLWcg3wCNhG。 - Muhammad Babar
6个回答

31

在此输入图片描述

对于90度旋转,我非常推荐使用RenderScript,它专门设计用来处理位图,而且比默认的Bitmap.createBitmap()还要快。这种在进程中处理位图不会将其存储在Java堆上,因此不会导致OutOfMemoryError

在您的项目中设置了几行代码之后,下面是要使用的RenderScript算法:

1)创建app\src\main\rs\rotator.rs RenderScript文件,并添加以下内容。

#pragma version(1)
#pragma rs java_package_name(ua.kulku.rs)

rs_allocation inImage;
int inWidth;
int inHeight;

uchar4 __attribute__ ((kernel)) rotate_90_clockwise (uchar4 in, uint32_t x, uint32_t y) {
    uint32_t inX  = inWidth - 1 - y;
    uint32_t inY = x;
    const uchar4 *out = rsGetElementAt(inImage, inX, inY);
    return *out;
}

uchar4 __attribute__ ((kernel)) rotate_270_clockwise (uchar4 in, uint32_t x, uint32_t y) {
    uint32_t inX = y;
    uint32_t inY = inHeight - 1 - x;

    const uchar4 *out = rsGetElementAt(inImage, inX, inY);
    return *out;
}

请注意选用的自动生成 RS Java 接口的包名为 ua.kulku.rs

2) 在您的 Java 代码中引用它:

import ua.kulku.rs.ScriptC_rotator;

    public Bitmap rotate(Bitmap bitmap) {
        RenderScript rs = RenderScript.create(mContext);
        ScriptC_rotator script = new ScriptC_rotator(rs);
        script.set_inWidth(bitmap.getWidth());
        script.set_inHeight(bitmap.getHeight());
        Allocation sourceAllocation = Allocation.createFromBitmap(rs, bitmap,
                Allocation.MipmapControl.MIPMAP_NONE,
                Allocation.USAGE_SCRIPT);
        bitmap.recycle();
        script.set_inImage(sourceAllocation);

        int targetHeight = bitmap.getWidth();
        int targetWidth = bitmap.getHeight();
        Bitmap.Config config = bitmap.getConfig();
        Bitmap target = Bitmap.createBitmap(targetWidth, targetHeight, config);
        final Allocation targetAllocation = Allocation.createFromBitmap(rs, target,
                Allocation.MipmapControl.MIPMAP_NONE,
                Allocation.USAGE_SCRIPT);
        script.forEach_rotate_90_clockwise(targetAllocation, targetAllocation);
        targetAllocation.copyTo(target);
        rs.destroy();
        return target;
    }

这里输入图片描述

对于180度的旋转,我认为由于使用了顺序数组项访问,NDK的解决方案表现优于RenderScript,因为180度旋转实际上是图像像素数组的反转。我在比较中使用的NDK算法来自https://github.com/AndroidDeveloperLB/AndroidJniBitmapOperations。处理中的位图也不存储在Java堆上,从而避免了OutOfMemoryError

统计条显示了我在三星S4(Android 5.0)上处理13 MP照片所用的时间,单位为毫秒。


我不明白这如何避免OOM,你仍然需要分配两个位图。 - Daniele Segato
1
哦,我明白了...你切换到分配(堆外)然后回收输入位图=在堆中同时只有1个位图。 - Daniele Segato
这只支持 API 16 及以上版本,是吗?我收到了Error:(8, 33) error: Non-root compute kernel rotate_90_clockwise() is not supported in SDK levels 11-15的错误提示。 - Voy
@Carpetfizz,“rotate_270_clockwise”基本上等于“90逆时针旋转”。 - riwnodennyk
嘿,感谢您提供这个很棒的代码示例!我想使用它,但是在使用该函数时出现了错误:android.renderscript.RSRuntimeException: U8_4类型不匹配! 在bavarit.app.cinnac.ScriptC_imageRotation.forEach_rotate_90_clockwise(ScriptC_imageRotation.java:108)处。 - skaldesh
显示剩余3条评论

5
你应该使用Bitmap解码图片。你需要按照Google提供的大图加载指南进行操作,它帮助我很多,你会注意到RAM的使用量有很大的区别。 更新:如果你只想旋转图像,可以使用以下代码。
Matrix matrix = new Matrix();
matrix.setRotate(90);
result = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),bitmap.getHeight(), matrix, false);

如果您只需要设置图像方向(例如拍摄照片时的方向),可以使用以下方法:

ExifInterface exif = new ExifInterface(filePath);

使用属性ExifInterface.TAG_ORIENTATION

希望这能帮助您。


5
我知道这种技术,但如果我在内存中加载已调整大小的位图,将其旋转然后保存到磁盘上,将会有这种降采样的图像,但我想要旋转后的原始尺寸图像(没有质量或尺寸损失)。 - endryha
7
将矩阵应用于内存位图是无济于事的,因为整个问题在于位图可能太大而无法合理地加载到内存中。 - kabuko
抱歉,我想我已经没有更多的想法了 :p - ColdFire
13
这个答案是不正确的。按照你说的旋转方法,需要将两个巨大的位图加载到内存中。原帖提问者想要避免这种情况,我不知道他为什么会标记这个作为答案。 - Corbella
@Corbella,您可以使用EXIF设置图像方向。这不会自动旋转图像,但如果显示图像的任何内容处理EXIF数据,则可能已足够。如果您绝对需要位图本身旋转,则此答案将无法帮助您。请查看riwnodennyk或Android开发人员的答案。 - user276648
显示剩余2条评论

4
注意:本答案只是在扩展riwnodennyk的答案,使其更易实现。它全都使用renderscript并不涉及任何NDK相关内容。本方法覆盖了所有常见旋转角度以及获取方向。
要获取旋转角度,需要使用ExifInterface接口。使用支持库中的那个,因为sdk版本存在问题。它适用于JPEG和RAW(以及类似的)文件,因为这些信息嵌入在文件中(而不是解码后的位图中)。
将以下代码添加到build.gradle文件中。
implementation "com.android.support:exifinterface:28.0.0"

如果您具有文件句柄,请使用此选项

import android.renderscript.Allocation;
import android.renderscript.RenderScript;
import your.application.package.rs.ScriptC_rotator;
...

public static Bitmap getCorrectlyRotatedBitmap(@NonNull Context context, @NonNull File imageFile) throws IOException {
    ExifInterface ei = new ExifInterface(imageFile.getPath());
    Bitmap bitmap = BitmapFactory.decodeFile(imageFile.getPath());
    int neededRotationClockwise = ei.getRotationDegrees() % 360;
    return rotateClockwise(context, bitmap, neededRotationClockwise);
}

关于位图旋转本身

public static Bitmap rotateClockwise(@NonNull Context context, @NonNull Bitmap bitmap, int degrees) {
    Log.i(TAG, "rotate bitmap degrees: " + degrees);
    if (degrees == 0F) return bitmap;

    RenderScript rs = RenderScript.create(context);
    ScriptC_rotator script = new ScriptC_rotator(rs);
    script.set_inWidth(bitmap.getWidth());
    script.set_inHeight(bitmap.getHeight());
    Allocation sourceAllocation = Allocation.createFromBitmap(rs, bitmap,
            Allocation.MipmapControl.MIPMAP_NONE,
            Allocation.USAGE_SCRIPT);
    bitmap.recycle();
    script.set_inImage(sourceAllocation);

    Bitmap.Config config = bitmap.getConfig();

    switch (degrees) {
        case 90: {
            Bitmap target = Bitmap.createBitmap(bitmap.getHeight(), bitmap.getWidth(), config);
            final Allocation targetAllocation = Allocation.createFromBitmap(rs, target,
                    Allocation.MipmapControl.MIPMAP_NONE,
                    Allocation.USAGE_SCRIPT);

            script.forEach_rotate_90_clockwise(targetAllocation, targetAllocation);
            targetAllocation.copyTo(target);
            rs.destroy();
            return target;
        }
        case 180: {
            Bitmap target = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), config);
            final Allocation targetAllocation = Allocation.createFromBitmap(rs, target,
                    Allocation.MipmapControl.MIPMAP_NONE,
                    Allocation.USAGE_SCRIPT);

            script.forEach_rotate_180(targetAllocation, targetAllocation);
            targetAllocation.copyTo(target);
            rs.destroy();
            return target;
        }
        case 270: {
            Bitmap target = Bitmap.createBitmap(bitmap.getHeight(), bitmap.getWidth(), config);
            final Allocation targetAllocation = Allocation.createFromBitmap(rs, target,
                    Allocation.MipmapControl.MIPMAP_NONE,
                    Allocation.USAGE_SCRIPT);

            script.forEach_rotate_270_clockwise(targetAllocation, targetAllocation);
            targetAllocation.copyTo(target);
            rs.destroy();
            return target;
        }
        default:
            throw new IllegalArgumentException("rotateClockwise() only supports 90 degree increments");
    }

}

并且,Renderscript可以在 src/main/rs/rotator.rs 中创建文件。这是Renderscript默认使用的位置,但您需要自己创建目录。

根据情况更改your.application.package.rs

#pragma version(1)
#pragma rs java_package_name(your.application.package.rs)

rs_allocation inImage;
int inWidth;
int inHeight;

uchar4 __attribute__ ((kernel)) rotate_270_clockwise (uchar4 in, uint32_t x, uint32_t y) {
    uint32_t inX  = inWidth - 1 - y;
    uint32_t inY = x;
    const uchar4 *out = rsGetElementAt(inImage, inX, inY);
    return *out;
}

uchar4 __attribute__ ((kernel)) rotate_90_clockwise (uchar4 in, uint32_t x, uint32_t y) {
    uint32_t inX = y;
    uint32_t inY = inHeight - 1 - x;

    const uchar4 *out = rsGetElementAt(inImage, inX, inY);
    return *out;
}

uchar4 __attribute__ ((kernel)) rotate_180 (uchar4 in, uint32_t x, uint32_t y) {
    uint32_t inX = inWidth - 1 - x;
    uint32_t inY = inHeight - 1 - y;

    const uchar4 *out = rsGetElementAt(inImage, inX, inY);
    return *out;
}

uchar4 __attribute__ ((kernel)) flip_vertical (uchar4 in, uint32_t x, uint32_t y) {
    uint32_t inX = x;
    uint32_t inY = inHeight - 1 - y;

    const uchar4 *out = rsGetElementAt(inImage, inX, inY);
    return *out;
}

uchar4 __attribute__ ((kernel)) flip_horizontal (uchar4 in, uint32_t x, uint32_t y) {
    uint32_t inX = inWidth - 1 - x;
    uint32_t inY = y;

    const uchar4 *out = rsGetElementAt(inImage, inX, inY);
    return *out;
}

1

我已经做出了一个非常慢但内存友好的解决方案在这里

我相信还有更好的方法,很想知道。


0

如果你需要处理不同的格式,那么这将是一件痛苦的事情。你必须能够理解不同的格式,并能够通过流读取/写入/转换它们。在普通的PC上,我建议看看ImageMagick,它具有非常大的图像支持。我搜索了一个Android端口,找到了this。也许值得一试。不过它看起来还没有完成,所以你可能需要做一些工作来获得更好的格式覆盖率。


0
我建议您使用一些第三方库,在操作位图时不会将数据存储在进程堆中。 在我的案例中,我使用了FFmpeg,这是我在项目中已经在其他用途中使用的库。

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