Android:旋转图像而不将其加载到内存中

Posted

技术标签:

【中文标题】Android:旋转图像而不将其加载到内存中【英文标题】:Android: rotate image without loading it to memory 【发布时间】:2012-08-20 20:30:32 【问题描述】:

我想知道是否可以旋转存储在 sdcard 上的图像而不将其加载到内存中。

原因是我正在寻找著名的 OutOfMemoryError。我知道我可以通过对大图像进行下采样来避免它,但实际上我不想减小该图像的大小,我想要原始图像但旋转 90 度。

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

【问题讨论】:

评论是给那些仍在寻找解决方案的人 *当您需要将图像发送到服务器时,很可能会出现这种情况。在搜索了几个小时后,我发现它不可能在 android 中预先旋转图像而不将其加载到内存中。处理此问题的唯一解决方案是在服务器端使用 exif 数据并对其进行轮换。这是一个有用的链接***.com/questions/7489742/… 【参考方案1】:

对于 90 度旋转,我真的很喜欢 RenderScript,它专为处理位图而设计,出乎意料地比默认的 Bitmap.createBitmap() 还要快。进程内位图不存储在 Java 堆中,因此不会将您推入OutOfMemoryError

用几行代码在项目中设置 RenderScript 支持后,这里是要使用的 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;

注意ua.kulku.rs,这是你为自动生成RS Java接口选择的一些包名。

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,你仍然需要分配2个位图。 哦,我明白了.. 你切换到分配(堆外)然后回收输入位图 = 你在堆中同时只有 1 个位图。 这只是 API 16+ 不是吗?我收到Error:(8, 33) error: Non-root compute kernel rotate_90_clockwise() is not supported in SDK levels 11-15 逆时针怎么做? @Carpetfizz, rotate_270_clockwise 基本上是 == 90 counter clockwise【参考方案2】:

您应该使用Bitmap 对图像进行解码。您应该关注 google 提供的 Loading Large Image 操作方法。它对我有很大帮助,您会注意到 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 希望对你有帮助

【讨论】:

是的,我知道这种技术,但是如果我在内存中加载调整大小的位图,旋转然后将其保存在磁盘上,将会有这个下采样图像,但我想要原始大小的图像但是旋转(没有质量或尺寸损失) @A.A 将矩阵应用于内存位图并没有帮助,因为整个问题是位图可能太大而无法合理加载到内存中。 那么抱歉,我想我没办法了:p 谢谢,看来 exif 接口只是正确的解决方案,它有帮助 这个答案不正确。正如您所说的那样旋转,您需要将两个巨大的位图加载到内存中。 OP想避免这种情况,我不知道他为什么将其标记为答案。【参考方案3】:

注意:这个答案实际上只是扩展了 riwnodennyk 的答案更容易实现。这都是渲染脚本,不关心任何 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");
    


还有渲染脚本,在src/main/rs/rotator.rs 中创建文件,这是使用渲染脚本的默认位置,但您必须自己创建目录。

根据需要更改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;

【讨论】:

【参考方案4】:

我做了一个非常缓慢但对内存友好的解决方案here。

我确信有更好的方法并且很想知道它们

【讨论】:

【参考方案5】:

如果您必须处理不同的格式,那将会很痛苦。您必须能够理解不同的格式并能够读取/写入/转换它们,可能是通过流。在普通 PC 上,我会说查看 ImageMagick,它具有非常大的图像支持。我搜索了一个 Android 端口并想出了this。可能值得一试。不过它看起来还没有完成,因此您可能需要做一些工作才能获得更好的格式覆盖率。

【讨论】:

【参考方案6】:

我建议您使用一些在位图上操作时不将数据存储在进程堆上的第三方库。 在我的例子中,我使用了我已经在项目中用于其他目的的 ffmpeg。

【讨论】:

以上是关于Android:旋转图像而不将其加载到内存中的主要内容,如果未能解决你的问题,请参考以下文章

如何逐行读取大型文本文件,而不将其加载到内存中?

裁剪图像而不加载到内存中

捕获图像而不将其存储到内部/外部存储

如何从 PHP 扩展返回数组,而不将其复制到内存中?

在跨度中显示图像而不将其转换为块级元素

Gensim 构建字典而不将所有文本加载到内存中