简介Bitmap、YUV,NV21与Bitmap互转

1. Bitmap
1.1Bitmap简介

关于Bitmap,它和Drawable差不多就是一种图片,Bitmap相关的使用主要有两种:

  1. 给ImageView设置背景
  2. 当做画布来使用

分别对应下面两个方法:

imageView.setImageBitmap(Bitmap bm);
Canvas canvas = new Canvas(Bitmap bm)
1.2 Bitmap的格式

我们知道Bitmap是位图,是由像素点组成的,这就涉及到两个问题:

  • 一:如何存储每个像素点?

  • 二:怎么压缩像素点?

1.2.1 存储格式

Bitmap有四种存储方式,对应Bitmap.Config中的四个常量

ALPHA_8:   只存储透明度,不存储色值,1个像素点占1个字节
ARGB_4444: ARGB各用4位存储,1个像素点16位占2个字节
ARGB_8888: ARGB各用8位存储,1个像素点32位占4个字节
RGB_565:   只存储色值,不存储透明度,默认不透明,RGB分别占565位,一个像素点占用162个字节。

通常不会使用ALPHA_8,它只存储透明度,没啥用处。ARGB_4444的画质又太差,ARGB_8888的画质高但占内存较大,RGB_565还行,就是不可以设置透明度。
注意以下三点即可:

  1. 一般情况下用ARGB_8888格式存储Bitmap;

  2. ARGB_4444画面惨不忍睹,被弃用;

  3. 假如对图片没有透明度要求,可以使用RGB_565,比ARGB_8888节省一半的内存开销。

1.2.2 压缩格式

​ 我们不妨来计算一下,如果一张和手机屏幕大小一样的Bitmap图片,采用ARGB_8888格式存储需要多大的内存!

//按照1024*768的屏幕大小来计算,每个像素需要32位也就是4个字节,
result = 1024*768*32B=25165824B=24MB

必须要对图片进行压缩呀,压缩格式使用枚举类Bitmap.CompressFormat中,有以下三种:

Bitmap.CompressFormat.JPEG: 采用JPEG压缩算法,是一种有损压缩格式,会在压缩过程中改变图像原本质量,画质越差,对原来的图片质量损伤越大,但是得到的文件比较小,而且JPEG不支持透明度,当遇到透明度像素时,会以黑色背景填充。

Bitmap.CompressFormat.JPEG:采用JPEG压缩算法,是一种有损压缩格式,会在压缩过程中改变图像原本质量,画质越差,对原来的图片质量损伤越大,但是得到的文件比较小,而且JPEG不支持透明度,当遇到透明度像素时,会以黑色背景填充。

Bitmap.CompressFormat.WEBP:WEBP是一种同时提供了有损压缩和无损压缩的图片文件格式,在14<=api<=17时,WEBP是一种有损压缩格式,而且不支持透明度,在api18以后WEBP是一种无损压缩格式,而且支持透明度,有损压缩时,在质量相同的情况下,WEBP格式的图片体积比JPEG小40%,但是编码时间比JPEG长8倍。在无损压缩时,无损的WEBP图片比PNG压缩小26%,但是WEBP的压缩时间是PNG格式压缩时间的5倍。

1.3 Bitmap创建方法

BitmapFactory提供了多种创建bitmap的静态方法

//从资源文件中通过id加载bitmap
//Resources res:资源文件,可以context.getResources()获得
//id:资源文件的id,如R.drawable.xxx
public static Bitmap decodeResources(Resources res,int id)
//第二种只是第一种的重载方法,多了个Options参数
public static Bitmap decodeResources(Resources res,int id,Options opt)
//传入文件路径加载,比如加载sd卡中的文件
//pathName:文件的全路径名
public static Bitmap decodeFile(String pathName);
public static Bitmap decodeFile(String pathName,Options opt);
//从byte数组中加载
//offset:对应data数组的起始下标
//length:截取的data数组的长度
public static Bitmap decodeByteArray(byte[] data,int offset , int length);
public static Bitmap decodeByteArray(byte[] data,int offset , int length,Options opt);
//从输入流中加载图片
//InputStream is:输入流
//Rect outPadding:用于返回矩形的内边距
public static Bitmap decodeStream(InputStream is);
public static Bitmap decodeStream(InputStream is,Rect outPadding,Options opt);
//FileDescriptor :包含解码位图的数据文件的路径
//通过该方式从路径加载bitmap比decodeFile更节省内存,原因不解释了。
public static Bitmap decodeFileDescriptor(FileDescriptor fd);
public static Bitmap decodeFileDescriptor(FileDescriptor fd,Rect outPadding,Options opt);

Bitmap.create静态方法

//width和height是长和宽单位px,config是存储格式
static Bitmap createBitmap(int width , int height Bitmap.Config config)
// 根据一幅图像创建一份一模一样的实例
static Bitmap createBitmap(Bitmap bm)
//截取一幅bitmap,起点是(x,y),width和height分别对应宽高
static Bitmap createBitmap(Bitmap bm,int x,int y,int width,int height)
//比上面的裁剪函数多了两个参数,Matrix:给裁剪后的图像添加矩阵 boolean filter:是否给图像添加滤波效果
static Bitmap createBitmap(Bitmap bm,int x,int y,int width,int height,Matrix m,boolean filter);
//用于缩放bitmap,dstWidth和dstHeight分别是目标宽高
createScaledBitmap(Bitmap bm,int dstWidth,int dstHeight,boolean filter)

总结:

  1. 加载图像可以使用BitmapFactory和Bitmap.create系列方法

  2. 可以通过Options实现缩放图片,获取图片信息,配置缩放比例等功能

  3. 如果需要裁剪或者缩放图片,只能使用create系列函数

  4. 注意加载和创建bitmap事通过try catch捕捉OOM异常

1.4 Bitmap与Drawable的转换
1.4.1 Drawable转换成Bitmap
public static Bitmap drawableToBitmap(Drawable drawable) {  
        // 取 drawable 的长宽  
        int w = drawable.getIntrinsicWidth();  
        int h = drawable.getIntrinsicHeight();  
  
        // 取 drawable 的颜色格式  
        Bitmap.Config config = drawable.getOpacity() != PixelFormat.OPAQUE ? Bitmap.Config.ARGB_8888  
                : Bitmap.Config.RGB_565;  
        // 建立对应 bitmap  
        Bitmap bitmap = Bitmap.createBitmap(w, h, config);  
        // 建立对应 bitmap 的画布  
        Canvas canvas = new Canvas(bitmap);  
        drawable.setBounds(0, 0, w, h);  
        // 把 drawable 内容画到画布中  
        drawable.draw(canvas);  
        return bitmap;  
    }  
5.4.2 Bitmap转换成Drawable
Bitmap bm=Bitmap.createBitmap(xxx); 
BitmapDrawable bd= new BitmapDrawable(getResource(), bm);
2.1 YUV简介

一个像素点一般由RGB三个颜色分量组成,其实YUV与RGB一样都是描述像素点的分量。 与RGB格式(红 - 绿 - 蓝)不同,YUV是用一个称为Y(相当于灰度)的“亮度”分量和两个“色度”分量表示,分别称为U(蓝色投影)和V(红色投影),由此得名。

YUV 与 RGB对应关系如下:知道了RGB就可以轻松的得到YUV了。

y = (( 66 * r + 129 * g + 25  * b + 128) >> 8) + 16  ;
u = ((-38 * r - 74  * g + 112 * b + 128) >> 8) + 128 ;
v = ((112 * r - 94  * g - 18  * b + 128) >> 8) + 128 ;

其实上面描述的只是一种 YUV,每个YUV都与RGB一一对应,称其为全采样的YUV即YUV 4:4:4。但是在实际存储过程中为了减少存储占用空间,YUV可以通过不同的采样方式来减少一些U、V分量,当然恢复为RGB的时候可以几个Y分量共用U、V分量来恢复为RGB(具体细节与原理自己百度)。

2.2 YUV的采样方式

采样的意思就是我们需要采集到的这些YUV分量的值,然后接下来我们需要存储下来这个值。所以根据采样、存储方式的不同,YUV有如下几种格式。

在采样时,我们可以临时使用 三个byte[]数组分别存储 采集到的Y、U、V的值。比如 byte[] byte = [YYYYY UUUUU VVVVV]这样的存储方法。

  1. YUV 4:4:4

如上文所描述的,YUV分量全部进行采样,每个YUV都与RGB一一对应;

  • YUV 4:4:4 采集到的数组长度(每个分量是byte类型数据)
    8bit + 8bit + 8bit 长度为 24bit
RGB   第一行 [(236,102,93) (39,186,106) (88,158,254)]      
      第二行 [(88,158,254) (39,186,106) (225,50, 43)]
      
    
YUV   第一行 [(136,104,187) (130,115,69) (143,180, 91)]        
      第二行 [(143,180, 91) (130,115,69) (136,104,187)]     
  1. YUV 4:2:2

对图片中的所有像素点,Y分量全部采样。在每一相同像素水平行上,UV 分量分别 交替 进行采样,即 { YU-YV-YU-YV… },这样YUV数据占用的存储空间明显减少。

  • YUV 4:2:2 采集到的数组长度
    8bit + 4bit + 4bit 长度为 16bit,只有原来 YUV4:4:4 的2/3
RGB   第一行 [(236,102,93) (39,186,106) (88,158,254)]      
      第二行 [(88,158,254) (39,186,106) (225,50, 43)]
      
    
YUV   第一行 [(136,104,null) (130,null,69) (143,180,null)]        
      第二行 [(143,180,null) (130,null,69) (136,104,null)]      
  1. YUV 4:2:0(重点)

对图片中的所有像素点,Y分量全部采样。在第0像素水平行(偶数行), U 分量 间隔 进行采样,而不采样V分量。在第1行(奇数行), V 分量 间隔 进行采样,而不采样U分量,即 偶数行{ YU-Y-YU-Y… }、奇数行{ YV-Y-YV-Y… },这样YUV数据占用的存储空间减少一半。

  • YUV 4:2:2 采集到的数组长度
    8bit + 2bit + 2bit 长度为 12bit,只有原来 YUV4:4:4 的1/2
RGB   第一行 [(236,102,93) (39,186,106) (88,158,254)]      
      第二行 [(88,158,254) (39,186,106) (225,50, 43)]    
    
YUV   第一行 [(136,104,null) (130,null,null) (143,180,null)]        
      第二行 [(143,null, 91) (130,null,null) (136,null,187)]      
3. YUV的存储方式

上述描述的YUV分量分别存储在一个数组中 [YUV] ,然而实际上的存储方式是将上述三个数组合并为一个数组,即类似 [YUVYUVYUV] 这样的格式。

一般情况下我们采用 YUV420 方式来实现采样,通常把 Y、U、V 分量数组分别命名为 arrayY,arrayU,arrayV 。如上述所述,一个像素一个Y值,即Y的最大采样数就是8个bit,其他数组长度就是它的 1/4,2个bit。

byte[] arrayY = new byte[8];
byte[] arrayU = new byte[8/4];
byte[] arrayV = new byte[8/4];

那么采样完成的的数组分别为:

Y数组: [-119, -126, -113, -69, -69, -113, -126, -119]
U数组: [104, -76]
V数组: [-93, 69]

这里大家会产生疑惑,为什么上面展示的都是正值,而这里的数组中却是负值呢?

首先请注意下,我们开始的时候使用int类型来存储所有的RGB,YUV的值的,这是由于从图片中获取到RGB的值是 int 类型的,然而在存储时采用的是 byte 数组,因为RGB的取值范围是[0, 255] ,但是一个byte就可以存储从 [-128,127] 的值,也就是可以存储256个值,这样只需要映射即可表示。例如:第一个像素 Y 的int值是136,存储中的 byte 值是 -119(136-255=-119)。

为了方便演示清晰,将数组的值都抹去,用YUV来作示例表示:

Y数组: [Y, Y, Y, Y, Y, Y, Y, Y]
U数组: [U, U]
V数组: [V, V]

YUV的格式有两大类:planar和packed。

对于planar的YUV格式,先连续存储所有像素点的Y,紧接着存储所有像素点的U,随后是所有像素点的V。

对于packed的YUV格式,每个像素点的Y、U、V都是连续交叉存储的。

3.1planar(平面方式)

这种方式就是将数组arrayY、arrayU、arrayV中的数据 按顺序 组合起来。那么这种顺序可以是 先存Y,再存U,最后存V。也可以是先存Y,再存V,再存U。我们这里把前者称为 YU的存储方式,把后者称为 YV的存储方式,即:YU、YV这两种存储格式,又统称为 YUV420P 格式。

  1. 420采样方式 + YU存储方式 = YU12(又叫 I420
Y数组: [Y, Y, Y, Y, Y, Y, Y, Y]
U数组: [U, U]
V数组: [V, V]

新数组:[YYYYYYYY   UU   VV]
  1. 420采样方式 + YV存储方式 = YV12
Y数组: [Y, Y, Y, Y, Y, Y, Y, Y]
U数组: [U, U]
V数组: [V, V]

新数组:[YYYYYYYY   VV   UU]

同理可得:类似YU、YV这两种存储格式,还存在 UV交替存储的,还有VU交替存储的,那么我们就把前者称为UV存储,把后者称为VU存储。这两种特殊的平面存储方式又叫 Semi-Splanar ,又称为 YUV420SP 格式:

  1. 420采样方式 +UV存储方式 =NV12
Y数组: [Y, Y, Y, Y, Y, Y, Y, Y]
U数组: [U, U]
V数组: [V, V]

新数组:[YYYYYYYY   UVUV]
  1. 420采样方式 + VU存储方式 = NV21(常用的存储方式)****
Y数组: [Y, Y, Y, Y, Y, Y, Y, Y]
U数组: [U, U]
V数组: [V, V]

新数组:[YYYYYYYY   VUVU]
3.2 packed(打包方式)

一般使用422采样方式的时候会采用这种存储方式,这种方式就不像上面那种那么直白了,先用数组表示吧,注意是422采样模式,所以U、V数组长度也变化了,如下:

Y数组: [Y, Y, Y, Y, Y, Y, Y, Y]
U数组: [U, U, U, U]
V数组: [V, V, V, V]

新数组:[YUYV    YUYV    YUYV    YUYV]

如上所示,因为YUV的比例是2:1:1 ,所以取两个Y元素就需要分别取一个U和V元素,后面同理。所以根据上面这种格式:

  • 422采样方式 + YUYV打包存储方式 = YUYV

  • 422采样方式 + UYVY打包存储方式 = UYVY

4. NV21与Bitmap互转

Android有现成的类可以直接处理NV21的数据。

4.1 Bitmap转NV21

按照如下步骤:

1、BitmapFactory读取到这张图片;
2、bitmap.getPixels()获取图片所有像素点数据,可以得到RGB数据的数组;
3、根据RGB数组采样分别获取Y,U,V数组,并存储为NV21格式的数组;

/***1、获取bitmap图片***/
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.demo);
//或者读入SD卡中的图片
public static byte[] getNV21FromBitmap(Bitmap bitmap) {
    int width = bitmap.getWidth();
    int height = bitmap.getHeight();
    
    if (width > WIDTH_1280 || height > HEIGHT_720) {
        return null;//举例图像尺寸(1280,720)
    }

    /***2.bitmap.getPixels()获取图片所有像素点数据,可以得到RGB数据的数组***/
    int[] argb = new int[width * height];
    bitmap.getPixels(argb, 0, width, 0, 0, width, height);
    
    /***3.根据RGB数组采样分别获取Y,U,V数组,并存储为NV21格式的数组***/
    byte[] yuv = new byte[WIDTH_720 * HEIGHT_720 * 3 / 2];
    encodeARGBTO720PNV21(yuv, argb, width, height);//调用方法如下

    return yuv;
}
/***3.根据RGB数组采样分别获取Y,U,V数组,并存储为NV21格式的数组***/
private static void encodeARGBTO720PNV21(byte[] yuv420sp, int[] argb, int width, int height) {
    int yIndex = 0;
    int uvIndex = WIDTH_1280 * HEIGHT_720;
    int R, G, B, Y, U, V;
	int index = 0;
	for (int i = 0; i < height; i++) {
		for (int j = 0; j < width; j++) {
			R = (argb[index] & 0xff0000) >> 16;
			G = (argb[index] & 0xff00) >> 8;
			B = argb[index] & 0xff;

			Y = ((66 * R + 129 * G + 25 * B + 128) >> 8) + 16;
			U = ((-38 * R - 74 * G + 112 * B + 128) >> 8) + 128;
			V = ((112 * R - 94 * G - 18 * B + 128) >> 8) + 128;

			yuv420sp[yIndex++] = (byte) ((Y < 0) ? 0 : ((Y > 255) ? 255 : Y));
			
             //偶数行取U,基数行取V,并存储
            if (i % 2 == 0 && index % 2 == 0) {
				yuv420sp[uvIndex++] = (byte) ((V < 0) ? 0 : ((V > 255) ? 255 : V));
				yuv420sp[uvIndex++] = (byte) ((U < 0) ? 0 : ((U > 255) ? 255 : U));
			}
			index++;
		}
		yIndex += (WIDTH_720 - width);
		uvIndex += (WIDTH_720 - width) / 2;
	}
}
4.2 NV21转Bitmap

这里可以分两种方式将NV21数据转换为Bitmap:使用 YuvImage 或者 ScriptIntrinsicYuvToRGB

1.使用YuvImage+BitmapFactory来得到Bitmap图片

YuvImage image = new YuvImage(arrayNV21, ImageFormat.NV21, width, height, null);
ByteArrayOutputStream stream = new ByteArrayOutputStream();
image.compressToJpeg(new Rect(0, 0, width, height), 80, stream);
Bitmap newBitmap = BitmapFactory.decodeByteArray(stream.toByteArray(), 0, stream.size());
stream.close();
//ImageView显示图片
ImageView imageView = findViewById(R.id.img);
imageView.setImageBitmap(newBitmap);

而使用上述方法:图片亮度会降低,而且画质也不清楚。一般不建议采用这种方法。

2.使用ScriptIntrinsicYuvToRGB来得到Bitmap图片

mNV21ToBitmap = new NV21ToBitmap(this);
//调用即可
Bitmap bmp = mNV21ToBitmap.nv21ToBitmap(byte[] nv21, int width, int height);
public class NV21ToBitmap {
    
    private RenderScript rs;
    private ScriptIntrinsicYuvToRGB yuvToRgbIntrinsic;
    private Type.Builder yuvType, rgbaType;

	public NV21ToBitmap(Context context) {
        rs = RenderScript.create(context);
        yuvToRgbIntrinsic = ScriptIntrinsicYuvToRGB.create(rs, Element.U8_4(rs));
        yuvType = new Type.Builder(rs, Element.U8(rs));
        rgbaType = new Type.Builder(rs, Element.RGBA_8888(rs));
    }
    
	public Bitmap nv21ToBitmap(byte[] nv21, int width, int height) {
    	yuvType.setX(nv21.length);
    	Allocation in = Allocation.createTyped(rs, yuvType.create(), Allocation.USAGE_SCRIPT);
    	in.copyFrom(nv21);

		rgbaType.setX(width).setY(height);
		Allocation out = Allocation.createTyped(rs, rgbaType.create(), Allocation.USAGE_SCRIPT);
		yuvToRgbIntrinsic.setInput(in);
		yuvToRgbIntrinsic.forEach(out);

		Bitmap bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
		out.copyTo(bmp);
    
		return bmp;
    }
}    

使用该方法的话显示出来的图像和源图像就没有肉眼上的区别了,而且效率也更高,推荐使用!!!

  • 5
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,以下是一个简单的Java代码示例,演示了如何将Bitmap对象叠加在yuv数据上: ```java public class YuvUtils { public static void overlayBitmapOnYuv(byte[] yuvData, int width, int height, Bitmap bitmap, int x, int y) { // 将yuv数据换为RGB格式 int[] pixels = new int[width * height]; YuvImage yuvImage = new YuvImage(yuvData, ImageFormat.NV21, width, height, null); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); yuvImage.compressToJpeg(new Rect(0, 0, width, height), 100, outputStream); byte[] jpegData = outputStream.toByteArray(); BitmapFactory.Options options = new BitmapFactory.Options(); options.inPreferredConfig = Bitmap.Config.ARGB_8888; Bitmap rgbBitmap = BitmapFactory.decodeByteArray(jpegData, 0, jpegData.length, options); rgbBitmap.getPixels(pixels, 0, width, 0, 0, width, height); // 在RGB数据上绘制半透明的Bitmap Canvas canvas = new Canvas(rgbBitmap); Paint paint = new Paint(); paint.setAlpha(128); canvas.drawBitmap(bitmap, x, y, paint); // 将RGB数据换为yuv格式 int[] yuv = new int[width * height]; for (int i = 0; i < height; i++) { for (int j = 0; j < width; j++) { int pixel = pixels[i * width + j]; int r = (pixel >> 16) & 0xff; int g = (pixel >> 8) & 0xff; int b = pixel & 0xff; yuv[i * width + j] = ((66 * r + 129 * g + 25 * b + 128) >> 8) + 16; if (j % 2 == 0 && i % 2 == 0) { int u = ((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128; int v = ((112 * r - 94 * g - 18 * b + 128) >> 8) + 128; yuv[width * height + (i / 2) * (width / 2) + (j / 2)] = v; yuv[width * height + width * height / 4 + (i / 2) * (width / 2) + (j / 2)] = u; } } } // 将叠加了Bitmapyuv数据写回原始yuv数据 System.arraycopy(yuv, 0, yuvData, 0, yuvData.length); } } ``` 在调用该方法时,需要传入原始的yuv数据、宽高、Bitmap对象以及Bitmapyuv数据中的位置。该方法将在原始yuv数据上直接叠加Bitmap,并将结果写回原始yuv数据中。需要注意的是,该示例代码仅供参考,实际使用时需要根据具体需求进行修改和优化。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值