1. 前言
最近在使用OpenCV
处理图片的时候,经常会遇到需要转换图像的情况,网上相关资料比较少,也不全,有时候得费劲老半天才能搞定。
自己踩了坑后,在这里记录下,都是我在项目中遇到的图像转化操作,是一些常用的图像格式转换操作。
具体包括:
-
nv21、rgba、rgb
转换 -
OpenCV
的Mat
转为Bitmap
-
Bitmap
转成RGB888
-
NV21
转成Bitmap
-
Camera2
中的android.media.Image
转为NV21
-
Android
传递Bitmap
给JNI
,并转为rgba
的Mat
-
JPEG
转NV21
本文的操作都是基于
Activity
横屏的情况下进行的
2. nv21、rgba、rgb转换
nv21
是YUV420
格式中的一种,在Android
中,Camera1
获取的摄像头数据,就是NV21
格式的。
rgba、rgb
格式,是不同于YUV
的另一种色彩表示方式,通常我们需要转为RGB
格式,再去做图像检测和处理。
所以在Android
中,nv21
和rgb
的转换,是比较常用、比较普遍的。
2.1 nv21转为rgba格式的Mat
这里传入的jbyteArray data_
是nv21
格式,首先转成nv21
的Mat
,然后在通过cv::cvtColor
方法,通过cv::COLOR_YUV2RGBA_NV21
这个参数值,转为rgba
格式的Mat
。
extern "C"
JNIEXPORT jbyteArray JNICALL
Java_com_heiko_myncnnlib_NcnnNativeLib_nv21toARGB(JNIEnv *env, jobject thiz, jbyteArray data_,
jint h, jint w) {
jbyte *data = env->GetByteArrayElements(data_, NULL);
cv::Mat nv21(h + h / 2, w, CV_8UC1, data);
cv::Mat rgba(h, w, CV_8UC4);
//nv21转为rgba格式
cv::cvtColor(nv21, rgba, cv::COLOR_YUV2RGBA_NV21);
//省略了后续无关代码....
//释放资源
env->ReleaseByteArrayElements(data_, data, 0);
}
2.2 nv21转为rgb的Mat
将nv21
转成rgb
格式的Mat
,这里的COLOR_YUV420sp2RGB
和COLOR_YUV2RGB_NV21
是一样的。
cv::Mat rgba(h, w, CV_8UC3);
//将nv21的数据转为RGB
cv::cvtColor(nv21, rgb, cv::COLOR_YUV420sp2RGB); //也可以传COLOR_YUV2RGB_NV21
2.3 rgba转为rgb的Mat
cv::Mat rgb(rows, cols, CV_8UC3);
//将rgba转为rgb
cv::cvtColor(rgba, rgb, CV_RGBA2RGB);
3. OpenCV的Mat转为Bitmap
在JNI
中,用OpenCV
处理好图像后,得到的结果是Mat
,那么需要将其转为byteArray
,然后传递到Android
层,再转为Bitmap
,显示到ImageView
上。
3.1 RGBA转成Bitmap
转成RGBA
相对比较简单,只要将rgba
的Mat
,转为jbyteArray
,传递到Android
层就好。
extern "C"
JNIEXPORT jbyteArray JNICALL
Java_com_heiko_myncnnlib_NcnnNativeLib_nv21toARGB(JNIEnv *env, jobject thiz, jbyteArray data_,
jint h, jint w) {
jbyte *data = env->GetByteArrayElements(data_, NULL);
cv::Mat nv21(h + h / 2, w, CV_8UC1, data);
cv::Mat rgba(h, w, CV_8UC4);
cv::cvtColor(nv21, rgba, cv::COLOR_YUV2RGBA_NV21);
int rows = h;
int cols = w;
jbyteArray byteArray = env->NewByteArray(rows * cols * 4);
env->SetByteArrayRegion(byteArray, 0, rows * cols * 4, reinterpret_castjbyte*>(rgba.data));
env->ReleaseByteArrayElements(data_, data, 0);
return byteArray;
}
Android
层进行调用,这里创建Bitmap
的时候,使用的是Bitmap.Config.ARGB_8888
//由于前摄像头放置位置是90度方向的,所以这里height和width对调 (实际上应该是在JNI里进行旋转操作,这里是怎么方便怎么来)
var result = nativeLib.nv21toARGB(data,height,width)
//var result = nativeLib.nv21toARGB(data,width,height)
//byte数组转为ARGB8888的Bitmap
val bitmap = Bitmap.createBitmap(width,height,Bitmap.Config.ARGB_8888)
var buffer = ByteBuffer.wrap(result)
bitmap.copyPixelsFromBuffer(buffer)
runOnUiThread {
//显示到Bitmap上
binding.img1.setImageBitmap(bitmap)
}
3.2 RGB888转RGB565后,再转成Bitmap
先来看一下RGB888
转RGB565
的方法
uint16_t *rgb888toRgb565(cv::Mat &rgb, int rows, int cols) {
cv::Vec3b *data = rgb.ptrcv::Vec3b>(0);
uint16_t *rgb565 = new uint16_t[rows * cols];
for (int i = 0; i rows * cols; i++) {
int r = data[i][0];
int g = data[i][1];
int b = data[i][2];
rgb565[i] = ((r >> 3) 11) | ((g >> 2) 5) | (b >> 3);
}
return rgb565;
}
实现JNI
方法,这里传入的data_
是rgb888
格式,然后转成Mat
,再调用rgb888toRgb565
转成rgb565
,最后在转成jbyteArray
返回给Android
层。
extern "C"
JNIEXPORT jbyteArray JNICALL
Java_com_heiko_MyTest_rgb888ToRgb565(JNIEnv *env, jobject thiz, jbyteArray data_,jint w, jint h) {
jbyte *data = env->GetByteArrayElements(data_, NULL);
unsigned char *rgb_data = reinterpret_castunsigned char *>(data);
cv::Mat rgb(h, w, CV_8UC3, rgb_data);
int rows = h;
int cols = w;
jbyteArray byteArray = env->NewByteArray(rows * cols * 2);
uint16_t *rgb565 = rgb888toRgb565(rgb, rows, cols);
env->SetByteArrayRegion(byteArray, 0, rows * cols * 2, reinterpret_castjbyte *>(rgb565));
env->ReleaseByteArrayElements(data_, data, 0);
return byteArray;
}
Android
层进行调用,这里创建Bitmap
的时候,使用的是Bitmap.Config.RGB_565
//这里的data是RGB888格式,具体看4.x小节
val result : ByteArray = nativeLib.rgb888ToRgb565(data, imageWidth, imageHeight)
//byte数组转为Bitmap
val bitmap = Bitmap.createBitmap(imageWidth, imageHeight, Bitmap.Config.RGB_565)
var buffer = ByteBuffer.wrap(detectResult)
bitmap.copyPixelsFromBuffer(buffer)
runOnUiThread {
//显示到ImageView上
binding.img1.setImageBitmap(bitmap)
}
3.3 RGBA转RGB565
rgba
也可以先转成rgb565
后,再传递给Android
层,代码如下
uint16_t *rgbaToRgb565(cv::Mat &rgb, int rows, int cols) {
cv::Vec4b *data = rgb.ptrcv::Vec4b>(0);
uint16_t *rgb565 = new uint16_t[rows * cols];
for (int i = 0; i rows * cols; i++) {
int r = data[i][0];
int g = data[i][1];
int b = data[i][2];
rgb565[i] = ((r >> 3) 11) | ((g >> 2) 5) | (b >> 3);
}
return rgb565;
}
4. Bitmap转RGB888
Android
中的Bitmap
是ARGB
格式进行存储的,所以我们先取到Bitmap
的像素数组,然后对其进行遍历,分别取到每个像素点的R
、G
、B
数据,赋值到新的ByteArray
里,就得到RGB888
格式的图像数据了。
//解析bytes为bitmap,bytes是jpeg格式的图片流
val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
val width: Int = bitmap.width
val height: Int = bitmap.height
val pixels = IntArray(width * height)
//获取像素赋值给 pixels
bitmap.getPixels(pixels, 0, width, 0, 0, width, height)
val rgb888 = ByteArray(width * height * 3)
for (i in 0 until width * height) {
// 注意:Android的Bitmap是ARGB格式,而不是RGBA
rgb888[i * 3] = Color.red(pixels[i]).toByte()
rgb888[i * 3 + 1] = Color.green(pixels[i]).toByte()
rgb888[i * 3 + 2] = Color.blue(pixels[i]).toByte()
}
5. YUV420转Bitmap
这里的y服务器托管网uv420
的具体格式是NV21
,也就是将NV21
格式转为Bitamp
。
具体操作为先将nv21
的ByteArray
转化为YuvImage
对象,然后压缩为JPEG
格式的ByteArray
,最后通过BitmapFactory.decodeByteArray()
来得到Bitmap
。
fun convertYUV420ToBitmap(
yuv420Data: ByteArray?,
width: Int,
height: Int
): Bitmap {
// 创建YuvImage对象
val yuvImage = YuvImage(yuv420Data, ImageFormat.NV21, width, height, null)
// 创建ByteArrayOutputStream对象
val outputStream = ByteArrayOutputStream()
// 将YuvImage对象压缩为JPEG格式的数据
yuvImage.compressToJpeg(Rect(0, 0, width, height), 100, outputStream)
// 将JPEG数据解码为Bitmap对象
val jpegData = outputStream.toByteArray()
return BitmapFactory.decodeByteArray(jpegData, 0, jpegData.size)
}
6. android.media.Image 转为 NV21
Android Camera2
相机中取到的一帧数据是android.media.Image
,我们设置android.graphics.ImageFormat
为ImageFormat.YUV_420_888
,这个格式是YCbCr
的泛化格式,不会具体指明是YU12,YV12,NV12
,或是是NV21
。它能够服务器托管网表示任何4:2:0
的平面和半平面格式,每个分量用8 bits
表示。
这里,我们来将Image
转为NV21
格式。
fun imageToNV21(image: Image): ByteArray {
val planes: ArrayImage.Plane> = image.planes
val yBuffer = planes[0].buffer
val uBuffer = planes[1].buffer
val vBuffer = planes[2].buffer
val ySize = yBuffer.remaining()
val uSize = uBuffer.remaining()
val vSize = vBuffer.remaining()
val yuvData = ByteArray(ySize + uSize + vSize)
yBuffer[yuvData, 0, ySize]
vBuffer[yuvData, ySize, vSize]
uBuffer[yuvData, ySize + vSize, uSize]
return yuvData
}
7. Android传递Bitmap给JNI,并转为rgba的Mat
Android
中,也可以直接向JNI
传递Bitmap
对象,然后在JNI
中,再去对Bitmap进行操作。
extern "C"
JNIEXPORT jbyteArray JNICALL
Java_com_zeekr_ncnnlib_NcnnNativeLib_humanDetectBitmap(JNIEnv *env, jobject thiz, jobject bitmap) {
AndroidBitmapInfo bitmapInfo;
//获取Bitmap的信息
AndroidBitmap_getInfo(env, bitmap, &bitmapInfo);
int rows = bitmapInfo.height;
int cols = bitmapInfo.width;
void *bitmapPixels;
//获取Bitmap的像素
AndroidBitmap_lockPixels(env, bitmap, &bitmapPixels);
//转成rgba的Mat
cv::Mat rgba(rows, cols, CV_8UC4, bitmapPixels);
AndroidBitmap_unlockPixels(env, bitmap);
//省略了后续无关代码
}
关于在JNI中创建Bitmap,并传递到Android层,具体可以看我的这篇文章 : Android JNI/NDK 入门从一到二_氦客的博客-CSDN博客
8. JPEG转NV21
传入jpeg
格式的ByteArray
,返回NV21
格式的ByteArray
fun jpegToNV21(jpegData: ByteArray, width: Int, height: Int): ByteArray {
val bitmap = BitmapFactory.decodeByteArray(jpegData, 0, jpegData.size)
val argb = IntArray(width * height)
bitmap.getPixels(argb, 0, width, 0, 0, width, height)
val yuv = ByteArray(width * height * 3 / 2)
encodeYUV420SP(yuv, argb, width, height)
bitmap.recycle()
return yuv
}
private fun encodeYUV420SP(yuv420sp: ByteArray, argb: IntArray, width: Int, height: Int) {
val frameSize = width * height
var yIndex = 0
var uvIndex = frameSize
//var a: Int
var R: Int
var G: Int
var B: Int
var Y: Int
var U: Int
var V: Int
for (j in 0 until height) {
for (i in 0 until width) {
//a = argb[j * width + i] and -0x1000000 shr 24
R = argb[j * width + i] and 0xff0000 shr 16
G = argb[j * width + i] and 0xff00 shr 8
B = argb[j * width + i] and 0xff shr 0
Y = (66 * R + 129 * G + 25 * B + 128 shr 8) + 16
U = (-38 * R - 74 * G + 112 * B + 128 shr 8) + 128
V = (112 * R - 94 * G - 18 * B + 128 shr 8) + 128
yuv420sp[yIndex++] = (if (Y 0) 0 else if (Y > 255) 255 else Y).toByte()
if (j % 2 == 0 && i % 2 == 0) {
yuv420sp[uvIndex++] = (if (V 0) 0 else if (V > 255) 255 else V).toByte()
yuv420sp[uvIndex++] = (if (U 0) 0 else if (U > 255) 255 else U).toByte()
}
}
}
}
本文为氦客在
CSDN
上独家发布 : https://blog.csdn.net/EthanCo
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net